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

(Install package: clbiwiki-nations-panel-perf-instrument-20260710 / js/Common.js)
(Install package: clbiwiki-portal-section-fit-20260721-008 / js/Common.js)
 
(같은 사용자의 중간 판 67개는 보이지 않습니다)
3번째 줄: 3번째 줄:
/* Raw MediaWiki script cache key.
/* Raw MediaWiki script cache key.
   This is not a semantic version. It only prevents stale raw JS during active editing. */
   This is not a semantic version. It only prevents stale raw JS during active editing. */
window.CLBI_RAW_LOAD_BUST = window.CLBI_RAW_LOAD_BUST || 'entry-raw-revision-ledger-20260710';
window.CLBI_RAW_LOAD_BUST = 'portal-frame-section-fit-20260721-008';


/*
/*
65번째 줄: 65번째 줄:


/* =========================================
/* =========================================
  Boot gate prelude
Main-page fresh assets with fallback
  =========================================
========================================= */
  This tiny prelude runs before the full EntryStore/EntryLoader/BootGate implementation.
 
  Its job is only to guarantee that the user sees the separate loading surface before the
/*
  normal wiki shell can paint.  The full BootGate later adopts the same DOM node and keeps
Common.css의 MainPage.css import는 안전망으로 그대로 유지한다.
  it open until the real full-entry readiness contract is satisfied.
대문에서는 새로고침마다 timestamp가 붙은 CSS/JS를 추가로 불러와
브라우저·프록시의 오래된 raw 응답을 우회한다.


  Maintenance rule:
이 보조 로더가 실패해도 기존 정적 import와 기존 화면은 남는다.
  - Do not move first-screen hiding into a late subsystem callback.  If the boot gate is
    meant to protect the first impression, the surface must be activated before sidebars,
    navbars, document wells, nations panels, or other entry surfaces can visibly fill in.
  - New names remain unprefixed.  Existing prefixed names elsewhere are legacy aliases only.
*/
*/
(function installBootGatePrelude(window, document) {
window.MainPageFreshAssets = window.MainPageFreshAssets || (function (window, document, mw) {
     'use strict';
     'use strict';


     var SCREEN_ID = 'boot-gate-screen';
     var sessionToken = String(Date.now());
     var STYLE_ID = 'boot-gate-prelude-style';
     var PORTAL_FRAME_BUILD = '20260721-portal-frame-section-fit-008';
     var START_TIME = Date.now ? Date.now() : new Date().getTime();
     var ASSET_LOAD_TIMEOUT_MS = 5000;
     function hasQueryFlag(name, value) {
     var scriptPromises = {};
        var search = String(window.location && window.location.search || '');
    var styleNode = null;
        var re = new RegExp('[?&]' + name + '=([^&]+)');
        var match = search.match(re);
        return !!(match && decodeURIComponent(match[1]) === value);
    }


     function getMwConfig(name, fallback) {
     function isMainPage() {
        var page = '';
         try {
         try {
             if (window.mw && window.mw.config && typeof window.mw.config.get === 'function') {
             page = String(mw && mw.config ? mw.config.get('wgPageName') || '' : '');
                var value = window.mw.config.get(name);
                return value == null ? fallback : value;
            }
         } catch (err) {}
         } catch (err) {}
         return fallback;
         return page.replace(/_/g, ' ') === '대문';
     }
     }


     function normalizePageName(value) {
     function rawUrl(title, type) {
         return String(value || '').split('?')[0].replace(/^\/index\.php\//, '').replace(/_/g, ' ').trim();
         var ctype = type === 'style' ? 'text/css' : 'text/javascript';
        return '/index.php?title=' + encodeURIComponent(title) +
            '&action=raw&ctype=' + encodeURIComponent(ctype) +
            '&_mainPageFresh=' + encodeURIComponent(sessionToken);
     }
     }


     function isDeveloperOrEditingPage() {
     function loadStyle() {
         var ns = Number(getMwConfig('wgNamespaceNumber', NaN));
         if (!isMainPage()) return Promise.resolve(false);
         var action = String(getMwConfig('wgAction', 'view') || 'view').toLowerCase();
 
         var model = String(getMwConfig('wgPageContentModel', '') || '').toLowerCase();
         if (styleNode && styleNode.parentNode) {
        var name = normalizePageName(getMwConfig('wgPageName', '') || window.location.pathname || '');
            return Promise.resolve(true);
        var systemNamespaces = {
         }
             '-1': true, '4': true, '5': true, '6': true, '7': true, '8': true, '9': true,
 
             '10': true, '11': true, '12': true, '13': true, '14': true, '15': true,
        return new Promise(function (resolve) {
             '828': true, '829': true
            var link = document.createElement('link');
        };
            var settled = false;
             var timeout = 0;
 
             function settle(ok) {
                if (settled) return;
                settled = true;
                window.clearTimeout(timeout);
                resolve(ok);
             }


        /*
            link.rel = 'stylesheet';
        * Developer/system pages must stay practical. Editing MediaWiki:, File:,
            link.href = rawUrl('MediaWiki:MainPage.css', 'style');
        * Project:, Template:, Module:, Category:, or any non-view action already forces
            link.setAttribute('data-main-page-fresh-style', sessionToken);
        * a full page load in MediaWiki, so showing the entry boot screen there only slows
            link.onload = function () {
        * maintenance work and does not improve the public first impression.
                styleNode = link;
        */
                settle(true);
        if (hasQueryFlag('bootGatePreview', '1')) return false;
            };
        if (action && action !== 'view') return true;
            link.onerror = function () {
        if (systemNamespaces[String(ns)]) return true;
                /* Common.css의 정적 import가 그대로 남아 있으므로 화면은 유지된다. */
        if (model === 'css' || model === 'javascript' || model === 'json' || model === 'sanitized-css') return true;
                settle(false);
        if (/\.(?:css|js|json)$/i.test(name)) return true;
            };
        if (/^(?:mediawiki|미디어위키|file|파일|project|프로젝트|template|틀|module|모듈|category|분류|special|특수):/i.test(name)) return true;
            timeout = window.setTimeout(function () { settle(false); }, ASSET_LOAD_TIMEOUT_MS);
         return false;
            (document.head || document.documentElement).appendChild(link);
         });
     }
     }


     var disabled = hasQueryFlag('bootGate', '0') || isDeveloperOrEditingPage();
     function loadScript(title) {
        var key = String(title || '').trim();
 
        if (!key || !isMainPage()) return Promise.resolve(false);
        if (scriptPromises[key]) return scriptPromises[key];
 
        scriptPromises[key] = new Promise(function (resolve) {
            var script = document.createElement('script');
            var settled = false;
            var timeout = 0;


    function injectStyle() {
            function settle(ok) {
        var style;
                if (settled) return;
        if (disabled || document.getElementById(STYLE_ID)) return;
                settled = true;
        style = document.createElement('style');
                window.clearTimeout(timeout);
        style.id = STYLE_ID;
                resolve(ok);
        style.textContent = [
             }
             'html.boot-gate-active,body.boot-gate-active{overflow:hidden!important;}',
 
             'html.boot-gate-active body{background:#080808!important;}',
             script.src = rawUrl(key, 'script');
            'html.boot-gate-active body>:not(#boot-gate-screen){visibility:hidden!important;pointer-events:none!important;}',
             script.async = false;
             'html.boot-gate-active #boot-gate-screen,html.boot-gate-active #boot-gate-screen *{visibility:visible!important;}',
             script.setAttribute('data-main-page-fresh-script', key);
             '#boot-gate-screen{position:fixed!important;inset:0!important;z-index:2147483000!important;display:flex!important;align-items:center!important;justify-content:center!important;background:#080808!important;color:#d8d8d8!important;opacity:1;pointer-events:auto!important;}',
             script.onload = function () { settle(true); };
             '#boot-gate-screen .boot-gate-decoration-layer{position:absolute;inset:0;z-index:1;pointer-events:none;overflow:hidden;}',
             script.onerror = function () { settle(false); };
             '#boot-gate-screen .boot-gate-panel{position:relative;z-index:2;}',
             timeout = window.setTimeout(function () {
             '#boot-gate-screen .boot-gate-close{display:none;position:absolute;right:7px;top:6px;z-index:3;height:18px;min-width:18px;border:1px solid #000;background:#141414;color:#ddd;font-size:11px;line-height:16px;padding:0 5px;cursor:pointer;}',
                if (script.parentNode) script.parentNode.removeChild(script);
            '#boot-gate-screen.is-preview{inset:auto!important;left:18px!important;top:18px!important;width:min(720px,calc(100vw - 380px))!important;height:360px!important;z-index:99990!important;overflow:hidden!important;border:1px solid #000!important;box-shadow:0 10px 28px rgba(0,0,0,.55)!important;pointer-events:none!important;}',
                settle(false);
             '#boot-gate-screen.is-preview .boot-gate-panel,#boot-gate-screen.is-preview .boot-gate-close{pointer-events:auto!important;}',
             }, ASSET_LOAD_TIMEOUT_MS);
            '#boot-gate-screen.is-preview .boot-gate-close{display:block;}'
            (document.head || document.documentElement).appendChild(script);
        ].join('');
        });
        (document.head || document.documentElement).appendChild(style);
    }


    function activate() {
         return scriptPromises[key];
         if (disabled) return;
        injectStyle();
        if (document.documentElement) document.documentElement.classList.add('boot-gate-active');
        if (document.body) document.body.classList.add('boot-gate-active');
     }
     }


     function makeNode() {
     function validatePortalFrameSet() {
         var node;
         var modules = [
        var panel;
            ['PortalFrame', window.PortalFrame],
        var title;
            ['CategoryNav', window.CategoryNav],
        var status;
            ['CategoryPillar', window.CategoryPillar],
         var meter;
            ['PortalSectionNav', window.PortalSectionNav],
         var fill;
            ['BottomGuideNav', window.BottomGuideNav]
        var progress;
         ];
        var detail;
         var mismatches = modules.filter(function (entry) {
        var decoLayer;
            var module = entry[1];
         var close;
            var version = module && (module.frameVersion || module.version);
            return version !== PORTAL_FRAME_BUILD;
         });


         if (disabled || !document.body) return null;
         document.documentElement.toggleAttribute(
         activate();
            'data-portal-frame-build-mismatch',
            mismatches.length > 0
        );
         document.documentElement.setAttribute('data-portal-frame-build', PORTAL_FRAME_BUILD);


        node = document.getElementById(SCREEN_ID);
         if (mismatches.length && window.console && typeof window.console.error === 'function') {
         if (node) return node;
            window.console.error(
 
                '[PortalFrame] 원자적 배포 세트의 버전이 일치하지 않습니다:',
        node = document.createElement('div');
                mismatches.map(function (entry) { return entry[0]; }).join(', ')
        node.id = SCREEN_ID;
            );
        node.className = 'boot-gate-screen is-active';
         }
        node.setAttribute('role', 'status');
        return mismatches.length === 0;
        node.setAttribute('aria-live', 'polite');
    }
         node.setAttribute('data-boot-gate-prelude', '1');


        panel = document.createElement('div');
    function ensure() {
         panel.className = 'boot-gate-panel';
         if (!isMainPage()) return Promise.resolve(false);


         title = document.createElement('div');
         return loadStyle()
         title.className = 'boot-gate-title';
            .then(function () {
         title.textContent = 'ARCHIVE INITIALIZATION';
                return loadScript('MediaWiki:CategoryNav.js');
            })
            .then(function () {
                return loadScript('MediaWiki:CategoryPillar.js');
            })
            .then(function () {
                return loadScript('MediaWiki:PortalSectionNav.js');
            })
            .then(function () {
                return loadScript('MediaWiki:BottomGuideNav.js');
            })
            .then(function () {
                validatePortalFrameSet();
                if (window.CategoryNav && typeof window.CategoryNav.renderAll === 'function') {
                    window.CategoryNav.renderAll(document);
                }
                if (window.CategoryPillar && typeof window.CategoryPillar.renderAll === 'function') {
                    window.CategoryPillar.renderAll(document);
                }
                if (window.PortalSectionNav && typeof window.PortalSectionNav.renderAll === 'function') {
                    window.PortalSectionNav.renderAll(document);
                }
                if (window.BottomGuideNav && typeof window.BottomGuideNav.render === 'function') {
                    window.BottomGuideNav.render();
                }
                return true;
            });
    }
 
    function status() {
         return {
            mainPage: isMainPage(),
            token: sessionToken,
            style: !!document.querySelector('link[data-main-page-fresh-style]'),
            category: !!document.querySelector(
                'script[data-main-page-fresh-script="MediaWiki:CategoryNav.js"]'
            ),
            pillar: !!document.querySelector(
                'script[data-main-page-fresh-script="MediaWiki:CategoryPillar.js"]'
            ),
            section: !!document.querySelector(
                'script[data-main-page-fresh-script="MediaWiki:PortalSectionNav.js"]'
            ),
            guide: !!document.querySelector(
                'script[data-main-page-fresh-script="MediaWiki:BottomGuideNav.js"]'
            ),
            frameBuild: PORTAL_FRAME_BUILD,
            frameConsistent: validatePortalFrameSet()
        };
    }
 
    if (mw && mw.hook) {
         mw.hook('wikipage.content').add(function () {
            ensure();
        });
    }
 
    window.setTimeout(ensure, 0);
 
    return {
        ensure: ensure,
        status: status
    };
}(window, document, window.mw));


        status = document.createElement('div');
        status.className = 'boot-gate-status';
        status.textContent = 'Preparing site entry systems';


        meter = document.createElement('div');
/* =========================================
        meter.className = 'boot-gate-meter';
  Account access page marker
        fill = document.createElement('div');
  =========================================
        fill.className = 'boot-gate-meter-fill';
  New shared systems use unprefixed names. The server body class differs
        meter.appendChild(fill);
  between skins/locales, so the canonical special-page name is normalized
  once and exposed as a stable page-state class for the login surface.
*/
(function markAccountLoginPage(window, document) {
    'use strict';


        progress = document.createElement('div');
    var canonical = '';
        progress.className = 'boot-gate-progress';
    var pageName = '';
        progress.textContent = '0%';
    var isLoginPage = false;


         detail = document.createElement('div');
    try {
         detail.className = 'boot-gate-detail';
         canonical = String(window.mw && mw.config ? mw.config.get('wgCanonicalSpecialPageName') || '' : '').toLowerCase();
        detail.textContent = 'boot gate prelude';
         pageName = String(window.mw && mw.config ? mw.config.get('wgPageName') || '' : '').replace(/_/g, ' ').toLowerCase();
    } catch (err) {}


        close = document.createElement('button');
    isLoginPage = canonical === 'userlogin' || /^(?:special|특수):(?:userlogin|로그인)$/.test(pageName);
        close.type = 'button';
    if (!isLoginPage) return;
        close.className = 'boot-gate-close';
        close.setAttribute('aria-label', 'Close boot preview');
        close.textContent = '×';
        close.addEventListener('click', function () { release(node); });


        decoLayer = document.createElement('div');
    document.documentElement.classList.add('account-login-page-root');
        decoLayer.className = 'boot-gate-decoration-layer';
        decoLayer.setAttribute('data-decoration-target', 'boot-gate');
        decoLayer.setAttribute('aria-hidden', 'true');


        panel.appendChild(title);
    function isCreateAccountTarget(link) {
         panel.appendChild(status);
         var href = '';
         panel.appendChild(meter);
         var title = '';
         panel.appendChild(progress);
         var text = '';
         panel.appendChild(detail);
         var sample = '';
        node.appendChild(decoLayer);
        node.appendChild(panel);
        node.appendChild(close);


         document.body.insertBefore(node, document.body.firstChild || null);
         if (!link) return false;
         return node;
        href = String(link.getAttribute('href') || '');
        title = String(link.getAttribute('title') || '');
        text = String(link.textContent || '');
        try { href = decodeURIComponent(href); } catch (err) {}
        sample = (href + ' ' + title + ' ' + text).replace(/_/g, ' ').toLowerCase();
         return /(?:special|특수)\s*[:%]\s*(?:createaccount|계정\s*(?:만들기|생성))|createaccount|계정\s*(?:만들기|생성)/i.test(sample);
     }
     }


     function onBody(callback) {
     function suppressCreateAccountSurface(root) {
         if (document.body) {
         var scope = root && root.querySelectorAll ? root : document;
             callback();
        var fixedSelectors = [
             return;
            '.mw-createacct-benefits-container',
         }
            '.mw-createacct-benefits-list',
         if (document.readyState === 'loading') {
            '.mw-createaccount-cta',
             document.addEventListener('DOMContentLoaded', callback, { once: true });
            '.mw-createaccount-join',
        }
            '#mw-createaccount-join',
        window.setTimeout(function tick() {
            '.mw-userlogin-create'
             if (document.body) return callback();
        ];
            window.setTimeout(tick, 10);
 
         }, 0);
        fixedSelectors.forEach(function (selector) {
             Array.prototype.forEach.call(scope.querySelectorAll(selector), function (node) {
                node.style.setProperty('display', 'none', 'important');
                node.setAttribute('aria-hidden', 'true');
             });
         });
 
         Array.prototype.forEach.call(scope.querySelectorAll('a[href]'), function (link) {
             var container;
            if (!isCreateAccountTarget(link)) return;
 
            link.style.setProperty('display', 'none', 'important');
            link.setAttribute('aria-hidden', 'true');
            link.setAttribute('tabindex', '-1');
 
            container = link.closest('.mw-ui-vform-field, .oo-ui-fieldLayout, .mw-userlogin-create, p, li');
             if (container && container.querySelectorAll('a').length === 1) {
                container.style.setProperty('display', 'none', 'important');
                container.setAttribute('aria-hidden', 'true');
            }
         });
     }
     }


     function release(node) {
     function applyMarker() {
        node = node || document.getElementById(SCREEN_ID);
         if (!document.body) return;
         if (node) {
         document.body.classList.add('account-login-page');
            node.classList.add('is-complete');
        suppressCreateAccountSurface(document);
            node.classList.remove('is-active');
            window.setTimeout(function () {
                if (node.parentNode) node.parentNode.removeChild(node);
            }, 240);
         }
        window.setTimeout(function () {
            if (document.documentElement) document.documentElement.classList.remove('boot-gate-active');
            if (document.body) document.body.classList.remove('boot-gate-active');
        }, 250);
     }
     }


     activate();
     if (document.readyState === 'loading') {
    onBody(function () {
         document.addEventListener('DOMContentLoaded', applyMarker, { once: true });
         activate();
    } else {
         makeNode();
         applyMarker();
     });
     }


     window.__BootGatePrelude = {
     try {
        startTime: START_TIME,
        if (window.mw && mw.hook) {
        activate: activate,
            mw.hook('wikipage.content').add(function (content) {
        ensure: makeNode,
                suppressCreateAccountSurface(content && content[0] ? content[0] : document);
        release: release,
            });
        node: function () { return document.getElementById(SCREEN_ID); },
         }
         disabled: disabled
     } catch (err) {}
     };
})(window, document);
})(window, document);


/* =========================================
/* =========================================
   Site boot gate and entry artifact contract
   Boot gate prelude
   =========================================
   =========================================
   This is the initial-load full/half entry artifact system.
   This tiny prelude runs before the full EntryStore/EntryLoader/BootGate implementation.
  Its job is only to guarantee that the user sees the separate loading surface before the
  normal wiki shell can paint.  The full BootGate later adopts the same DOM node and keeps
  it open until the real full-entry readiness contract is satisfied.


   Purpose:
   Maintenance rule:
   - The loading screen is not decorative. It exists only during the first site entry in a
   - Do not move first-screen hiding into a late subsystem callback.  If the boot gate is
    tab, before the user is allowed into the normal wiki surface.
    meant to protect the first impression, the surface must be activated before sidebars,
  - A "full" entry must mean that the first view of that system can appear without an
     navbars, document wells, nations panels, or other entry surfaces can visibly fill in.
    additional visible data load. For the nations system, the 1950 entry is full only when
   - New names remain unprefixedExisting prefixed names elsewhere are legacy aliases only.
    its nation list/link-map data and first-view pixel decorations are ready in this tab.
*/
  - A "half" entry is a predictive warm state for the next likely path. It may fetch and
(function installBootGatePrelude(window, document) {
    parse data, but may skip expensive final work such as image/canvas preparation until it
     is promoted to full.
  - This principle is a site-wide design priority, like SPA continuity. New viewers,
    document systems, and information panels should define their entry full/half packs
    before exposing a first screen that can visibly fill in later.
   - SPA navigation is a consumer phase, not a blocking phaseDo not add BootGate holds
    to SPA route changes.  If a route needs seamless first paint, prepare its artifacts
    during the initial boot pack and have the subsystem consume EntryStore synchronously
    before inserting or painting visible DOM.
 
  Naming rule:
  - New public APIs, globals, classes, functions, files, and components must not use a
    project prefix. Old prefixed globals are legacy compatibility surfaces only.
  - New code should use names such as BootGate, EntryLoader, EntryStore, and boot-gate-*.
*/
(function (window, document, mw) {
     'use strict';
     'use strict';


     var MANIFEST_TITLE = 'MediaWiki:EntryManifest.json';
     var SCREEN_ID = 'boot-gate-screen';
    var BUILD_ID = '20260710-nations-panel-perf-instrument-001';
     var STYLE_ID = 'boot-gate-prelude-style';
     var deferredEntryWarmups = [];
     var START_TIME = Date.now ? Date.now() : new Date().getTime();
    var READY_KEY = 'boot-gate-ready-version';
     var DISMISS_PARAM = 'bootGate';
    var bootStarted = false;
    var bootPromise = null;
    var bootNode = null;
    var bootStatusNode = null;
    var bootProgressNode = null;
    var bootDetailNode = null;
    var bootFillNode = null;
    var bootStartTime = 0;
    var earlyBootStyleInjected = false;
 
     function hasQueryFlag(name, value) {
     function hasQueryFlag(name, value) {
         var search = String(window.location && window.location.search || '');
         var search = String(window.location && window.location.search || '');
333번째 줄: 388번째 줄:
     }
     }


     function readConfig(name, fallback) {
     function getMwConfig(name, fallback) {
         try {
         try {
             if (mw && mw.config && typeof mw.config.get === 'function') {
             if (window.mw && window.mw.config && typeof window.mw.config.get === 'function') {
                 var value = mw.config.get(name);
                 var value = window.mw.config.get(name);
                 return value == null ? fallback : value;
                 return value == null ? fallback : value;
             }
             }
343번째 줄: 398번째 줄:
     }
     }


     function normalizeBootPageName(value) {
     function normalizePageName(value) {
         return String(value || '').split('?')[0].replace(/^\/index\.php\//, '').replace(/_/g, ' ').trim();
         return String(value || '').split('?')[0].replace(/^\/index\.php\//, '').replace(/_/g, ' ').trim();
     }
     }


     function isBootExcludedPage() {
     function isDeveloperOrEditingPage() {
         var ns = Number(readConfig('wgNamespaceNumber', NaN));
         var ns = Number(getMwConfig('wgNamespaceNumber', NaN));
         var action = String(readConfig('wgAction', 'view') || 'view').toLowerCase();
         var action = String(getMwConfig('wgAction', 'view') || 'view').toLowerCase();
         var model = String(readConfig('wgPageContentModel', '') || '').toLowerCase();
         var model = String(getMwConfig('wgPageContentModel', '') || '').toLowerCase();
         var name = normalizeBootPageName(readConfig('wgPageName', '') || window.location.pathname || '');
         var name = normalizePageName(getMwConfig('wgPageName', '') || window.location.pathname || '');
         var systemNamespaces = {
         var systemNamespaces = {
             '-1': true, '4': true, '5': true, '6': true, '7': true, '8': true, '9': true,
             '-1': true, '4': true, '5': true, '6': true, '7': true, '8': true, '9': true,
359번째 줄: 414번째 줄:


         /*
         /*
         * The public boot gate is for normal reading surfaces.  Developer/system
         * Developer/system pages must stay practical.  Editing MediaWiki:, File:,
         * namespaces and edit/diff/history actions already reload outside SPA, so
         * Project:, Template:, Module:, Category:, or any non-view action already forces
         * blocking them would turn every maintenance save into another entry boot.
        * a full page load in MediaWiki, so showing the entry boot screen there only slows
         * maintenance work and does not improve the public first impression.
         */
         */
         if (hasQueryFlag('bootGatePreview', '1')) return false;
         if (hasQueryFlag('bootGatePreview', '1')) return false;
372번째 줄: 428번째 줄:
     }
     }


     var BOOT_EXCLUDED_PAGE = isBootExcludedPage();
     function isAnonymousUser() {
        var userName = getMwConfig('wgUserName', null);
        var userId = Number(getMwConfig('wgUserId', 0) || 0);
        return !userName && !userId;
    }


     function injectEarlyBootStyle() {
     function isAuthenticationPage() {
        var canonical = String(getMwConfig('wgCanonicalSpecialPageName', '') || '').toLowerCase();
        var name = normalizePageName(getMwConfig('wgPageName', '') || window.location.pathname || '').toLowerCase();
        var allowed = {
            userlogin: true,
            passwordreset: true,
            resetpass: true,
            confirmemail: true
        };
 
        if (allowed[canonical]) return true;
        return /^(?:special|특수):(?:userlogin|로그인|passwordreset|비밀번호 ?재설정|resetpass|confirmemail)/i.test(name);
    }
 
    var anonymousUser = isAnonymousUser();
    var disabled = isAuthenticationPage() || (!anonymousUser && (hasQueryFlag('bootGate', '0') || isDeveloperOrEditingPage()));
 
    function injectStyle() {
         var style;
         var style;
         if (earlyBootStyleInjected || !document.documentElement) return;
         if (disabled || document.getElementById(STYLE_ID)) return;
        earlyBootStyleInjected = true;
         style = document.createElement('style');
         style = document.createElement('style');
         style.id = 'boot-gate-early-style';
         style.id = STYLE_ID;
         style.textContent = [
         style.textContent = [
             'html.boot-gate-active, body.boot-gate-active{overflow:hidden!important;}',
             'html.boot-gate-active,body.boot-gate-active{overflow:hidden!important;}',
             'html.boot-gate-active body{background:#080808!important;}',
             'html.boot-gate-active body{background:#080808!important;}',
             'html.boot-gate-active body> :not(#boot-gate-screen){visibility:hidden!important;pointer-events:none!important;}',
             'html.boot-gate-active body>:not(#boot-gate-screen){visibility:hidden!important;pointer-events:none!important;}',
             'html.boot-gate-active #boot-gate-screen,html.boot-gate-active #boot-gate-screen *{visibility:visible!important;}'
             'html.boot-gate-active #boot-gate-screen,html.boot-gate-active #boot-gate-screen *{visibility:visible!important;}',
            '#boot-gate-screen{position:fixed!important;inset:0!important;z-index:2147483000!important;display:flex!important;align-items:center!important;justify-content:center!important;background:#080808!important;color:#d8d8d8!important;opacity:1;pointer-events:auto!important;}',
            '#boot-gate-screen .boot-gate-decoration-layer{position:absolute;inset:0;z-index:1;pointer-events:none;overflow:hidden;}',
            '#boot-gate-screen .boot-gate-panel{position:relative;z-index:2;}',
            '#boot-gate-screen .boot-gate-close{display:none;position:absolute;right:7px;top:6px;z-index:3;height:18px;min-width:18px;border:1px solid #000;background:#141414;color:#ddd;font-size:11px;line-height:16px;padding:0 5px;cursor:pointer;}',
            '#boot-gate-screen.is-preview{inset:auto!important;left:18px!important;top:18px!important;width:min(720px,calc(100vw - 380px))!important;height:360px!important;z-index:99990!important;overflow:hidden!important;border:1px solid #000!important;box-shadow:0 10px 28px rgba(0,0,0,.55)!important;pointer-events:none!important;}',
            '#boot-gate-screen.is-preview .boot-gate-panel,#boot-gate-screen.is-preview .boot-gate-close{pointer-events:auto!important;}',
            '#boot-gate-screen.is-preview .boot-gate-close{display:block;}'
         ].join('');
         ].join('');
         (document.head || document.documentElement).appendChild(style);
         (document.head || document.documentElement).appendChild(style);
     }
     }


     function activateBootSurface() {
     function activate() {
         injectEarlyBootStyle();
         if (disabled) return;
        injectStyle();
         if (document.documentElement) document.documentElement.classList.add('boot-gate-active');
         if (document.documentElement) document.documentElement.classList.add('boot-gate-active');
         if (document.body) document.body.classList.add('boot-gate-active');
         if (document.body) document.body.classList.add('boot-gate-active');
     }
     }


     if (!BOOT_EXCLUDED_PAGE) activateBootSurface();
     function makeNode() {
    else if (window.__BootGatePrelude && window.__BootGatePrelude.release) window.__BootGatePrelude.release();
        var node;
        var panel;
        var title;
        var status;
        var meter;
        var fill;
        var progress;
        var detail;
        var decoLayer;
        var close;
 
        if (disabled || !document.body) return null;
        activate();
 
        node = document.getElementById(SCREEN_ID);
        if (node) return node;
 
        node = document.createElement('div');
        node.id = SCREEN_ID;
        node.className = 'boot-gate-screen is-active';
        node.setAttribute('role', 'status');
        node.setAttribute('aria-live', 'polite');
        node.setAttribute('data-boot-gate-prelude', '1');
 
        panel = document.createElement('div');
        panel.className = 'boot-gate-panel';


    var defaultManifest = {
        title = document.createElement('div');
        version: '20260710-globe-vhs-restore-entry-001',
         title.className = 'boot-gate-title';
         boot: {
        title.textContent = 'ARCHIVE INITIALIZATION';
            minDisplayMs: 950,
            cachedMinDisplayMs: 350,
            maxBlockingMs: 15000
        },
        initial: {
            full: [
                {
                    id: 'decorations-registry',
                    label: 'DECORATION REGISTRY',
                    type: 'decorations',
                    ref: 'MediaWiki:Decorations.json',
                    page: '국가 및 조합',
                    era: '1950',
                    preparePixels: true
                },
                {
                    id: 'nations-1950-entry',
                    label: '1950 NATIONS ENTRY',
                    type: 'nations-era',
                    era: '1950',
                    level: 'full'
                }
            ],
            half: [
                {
                    id: 'nations-1960-half',
                    label: '1960 NATIONS HALF',
                    type: 'nations-era',
                    era: '1960',
                    level: 'half'
                }
            ]
        }
    };


    function now() {
        status = document.createElement('div');
         return Date.now ? Date.now() : new Date().getTime();
         status.className = 'boot-gate-status';
    }
        status.textContent = 'Preparing site entry systems';


        meter = document.createElement('div');
        meter.className = 'boot-gate-meter';
        fill = document.createElement('div');
        fill.className = 'boot-gate-meter-fill';
        meter.appendChild(fill);


    var BootPerf = window.BootPerf = window.BootPerf || (function () {
        progress = document.createElement('div');
        var t0 = now();
         progress.className = 'boot-gate-progress';
         var entries = [];
         progress.textContent = '0%';
         var active = {};
        var seq = 0;


         function cloneMeta(meta) {
         detail = document.createElement('div');
            var out = {};
        detail.className = 'boot-gate-detail';
            Object.keys(meta || {}).forEach(function (key) {
        detail.textContent = 'boot gate prelude';
                var value = meta[key];
                if (value == null) return;
                if (typeof value === 'string' || typeof value === 'number' || typeof value === 'boolean') out[key] = value;
                else {
                    try { out[key] = JSON.parse(JSON.stringify(value)); }
                    catch (err) { out[key] = String(value); }
                }
            });
            return out;
        }


         function start(name, meta) {
         close = document.createElement('button');
            var id = String(++seq);
        close.type = 'button';
            active[id] = { id: id, name: String(name || 'entry'), start: now(), meta: cloneMeta(meta) };
        close.className = 'boot-gate-close';
            return id;
        close.setAttribute('aria-label', 'Close boot preview');
        }
        close.textContent = '×';
        close.addEventListener('click', function () { release(node); });


         function end(id, meta) {
         decoLayer = document.createElement('div');
            var item = active[id];
        decoLayer.className = 'boot-gate-decoration-layer';
            var ended = now();
        decoLayer.setAttribute('data-decoration-target', 'boot-gate');
            if (!item) return null;
        decoLayer.setAttribute('aria-hidden', 'true');
            delete active[id];
 
            item.end = ended;
        panel.appendChild(title);
            item.ms = ended - item.start;
        panel.appendChild(status);
            item.offset = item.start - t0;
        panel.appendChild(meter);
            item.meta = Object.assign({}, item.meta || {}, cloneMeta(meta));
        panel.appendChild(progress);
            entries.push(item);
        panel.appendChild(detail);
            return item;
        node.appendChild(decoLayer);
         }
        node.appendChild(panel);
         node.appendChild(close);


         function instant(name, meta) {
         document.body.insertBefore(node, document.body.firstChild || null);
            var t = now();
         return node;
            entries.push({ id: String(++seq), name: String(name || 'mark'), start: t, end: t, ms: 0, offset: t - t0, meta: cloneMeta(meta) });
    }
         }


        function measure(name, meta, fn) {
    function onBody(callback) {
            var id = start(name, meta);
        if (document.body) {
            try {
            callback();
                return Promise.resolve(fn()).then(function (value) {
             return;
                    end(id, { ok: true });
                    return value;
                }, function (err) {
                    end(id, { ok: false, error: err && (err.message || String(err)) });
                    throw err;
                });
             } catch (err) {
                end(id, { ok: false, error: err && (err.message || String(err)) });
                return Promise.reject(err);
            }
         }
         }
 
         if (document.readyState === 'loading') {
         function rows() {
             document.addEventListener('DOMContentLoaded', callback, { once: true });
             return entries.slice().sort(function (a, b) { return a.start - b.start; }).map(function (item) {
                return {
                    offset: item.offset,
                    ms: item.ms,
                    name: item.name,
                    meta: item.meta || {}
                };
            });
         }
         }
        window.setTimeout(function tick() {
            if (document.body) return callback();
            window.setTimeout(tick, 10);
        }, 0);
    }


        function summary() {
    function release(node) {
            var list = rows();
        node = node || document.getElementById(SCREEN_ID);
            var total = list.reduce(function (max, item) { return Math.max(max, item.offset + item.ms); }, 0);
        if (node) {
             return {
            node.classList.add('is-complete');
                 build: BUILD_ID,
            node.classList.remove('is-active');
                startedAt: t0,
             window.setTimeout(function () {
                totalMs: total,
                 if (node.parentNode) node.parentNode.removeChild(node);
                entries: list,
             }, 240);
                active: Object.keys(active).map(function (id) { return active[id]; })
             };
         }
         }
        window.setTimeout(function () {
            if (document.documentElement) document.documentElement.classList.remove('boot-gate-active');
            if (document.body) document.body.classList.remove('boot-gate-active');
        }, 250);
    }


        function print() {
    activate();
            var list = rows();
    onBody(function () {
            if (!window.console || !console.log) return summary();
        activate();
            try {
        makeNode();
                console.groupCollapsed('[BootPerf] entry loading timeline · ' + BUILD_ID);
    });
                if (console.table) console.table(list.map(function (item) {
                    return {
                        offset: item.offset + 'ms',
                        duration: item.ms + 'ms',
                        name: item.name,
                        detail: JSON.stringify(item.meta || {})
                    };
                }));
                else list.forEach(function (item) { console.log(item.offset + 'ms', item.ms + 'ms', item.name, item.meta || {}); });
                console.groupEnd();
            } catch (err) {}
            return summary();
        }


        return {
    window.__BootGatePrelude = {
            start: start,
        startTime: START_TIME,
            end: end,
        activate: activate,
            mark: instant,
        ensure: makeNode,
            measure: measure,
        release: release,
            rows: rows,
        node: function () { return document.getElementById(SCREEN_ID); },
            summary: summary,
        disabled: disabled
            print: print
    };
        };
})(window, document);
    }());


    function toArray(value) {
/* =========================================
        return Array.prototype.slice.call(value || []);
  Site boot gate and entry artifact contract
    }
  =========================================
  This is the initial-load full/half entry artifact system.
 
  Purpose:
  - The loading screen is not decorative. It exists only during the first site entry in a
    tab, before the user is allowed into the normal wiki surface.
  - A "full" entry must mean that the first view of that system can appear without an
    additional visible data load. For the nations system, the 1950 entry is full only when
    its nation list/link-map data and first-view pixel decorations are ready in this tab.
  - A "half" entry is a predictive warm state for the next likely path. It may fetch and
    parse data, but may skip expensive final work such as image/canvas preparation until it
    is promoted to full.
  - This principle is a site-wide design priority, like SPA continuity. New viewers,
    document systems, and information panels should define their entry full/half packs
    before exposing a first screen that can visibly fill in later.
  - SPA navigation is a consumer phase, not a blocking phase.  Do not add BootGate holds
    to SPA route changes.  If a route needs seamless first paint, prepare its artifacts
    during the initial boot pack and have the subsystem consume EntryStore synchronously
    before inserting or painting visible DOM.


    function unique(list) {
  Naming rule:
        var seen = {};
  - New public APIs, globals, classes, functions, files, and components must not use a
        var out = [];
    project prefix. Old prefixed globals are legacy compatibility surfaces only.
        (list || []).forEach(function (item) {
  - New code should use names such as BootGate, EntryLoader, EntryStore, and boot-gate-*.
            item = String(item || '').trim();
*/
            if (!item || seen[item]) return;
(function (window, document, mw) {
            seen[item] = true;
    'use strict';
            out.push(item);
 
        });
    var MANIFEST_TITLE = 'MediaWiki:EntryManifest.json';
        return out;
    var BUILD_ID = '20260711-existing-account-login-001';
     }
    var deferredEntryWarmups = [];
    var READY_KEY = 'boot-gate-ready-version';
    var DISMISS_PARAM = 'bootGate';
    var bootStarted = false;
    var bootPromise = null;
    var bootNode = null;
    var bootStatusNode = null;
    var bootProgressNode = null;
    var bootDetailNode = null;
    var bootFillNode = null;
    var bootStartTime = 0;
    var earlyBootStyleInjected = false;
    var loginGateLocked = false;
     var loginGateActionNode = null;


     function hasBootParam(value) {
     function hasQueryFlag(name, value) {
         var search = String(window.location && window.location.search || '');
         var search = String(window.location && window.location.search || '');
         var re = new RegExp('[?&]' + DISMISS_PARAM + '=([^&]+)');
         var re = new RegExp('[?&]' + name + '=([^&]+)');
         var match = search.match(re);
         var match = search.match(re);
         return match && decodeURIComponent(match[1]) === value;
         return !!(match && decodeURIComponent(match[1]) === value);
     }
     }


     function normalizeTitle(value) {
     function readConfig(name, fallback) {
         return String(value || '')
         try {
             .split('#')[0]
             if (mw && mw.config && typeof mw.config.get === 'function') {
            .replace(/_/g, ' ')
                var value = mw.config.get(name);
            .trim();
                return value == null ? fallback : value;
    }
            }
 
         } catch (err) {}
    function extractTitleFromUrl(value) {
         return fallback;
        var text = String(value || '');
        var match = text.match(/[?&]title=([^&]+)/i);
         if (match) return normalizeTitle(decodeURIComponent(match[1].replace(/\+/g, ' ')));
         return '';
     }
     }


     function normalizeRefKey(ref) {
     function normalizeBootPageName(value) {
         var text = String(ref || '').trim();
         return String(value || '').split('?')[0].replace(/^\/index\.php\//, '').replace(/_/g, ' ').trim();
        var title;
        if (!text) return '';
        title = extractTitleFromUrl(text);
        if (title) return 'title:' + title.toLowerCase();
        if (text.indexOf('/') === -1 && text.indexOf(':') !== -1) return 'title:' + normalizeTitle(text).toLowerCase();
        return 'url:' + text;
     }
     }


     function rawUrlForRef(ref, ctype) {
     function isCreateAccountPage() {
         var text = String(ref || '').trim();
         var canonical = String(readConfig('wgCanonicalSpecialPageName', '') || '').toLowerCase();
         var title;
         var name = normalizeBootPageName(readConfig('wgPageName', '') || window.location.pathname || '').toLowerCase();
        if (!text) return '';
         return canonical === 'createaccount' || /^(?:special|특수):(?:createaccount|계정 ?(?:만들기|생성))/i.test(name);
        if (/^(?:https?:)?\/\//i.test(text) || text.charAt(0) === '/') return text;
        title = normalizeTitle(text.indexOf(':') !== -1 ? text : ('MediaWiki:' + text));
         if (mw && mw.util && typeof mw.util.getUrl === 'function') {
            return mw.util.getUrl(title, { action: 'raw', ctype: ctype || 'application/json' });
        }
        return '/index.php?title=' + encodeURIComponent(title) + '&action=raw&ctype=' + encodeURIComponent(ctype || 'application/json');
     }
     }


     function getApiEndpoint() {
     function isAnonymousUser() {
         return (mw && mw.util && typeof mw.util.wikiScript === 'function') ? mw.util.wikiScript('api') : '/api.php';
         var userName = readConfig('wgUserName', null);
        var userId = Number(readConfig('wgUserId', 0) || 0);
        return !userName && !userId;
     }
     }


     function fetchApi(params) {
     function isAuthenticationPage() {
         var body = new URLSearchParams();
         var canonical = String(readConfig('wgCanonicalSpecialPageName', '') || '').toLowerCase();
        Object.keys(params || {}).forEach(function (key) {
         var name = normalizeBootPageName(readConfig('wgPageName', '') || window.location.pathname || '').toLowerCase();
            body.append(key, params[key]);
         var allowed = {
         });
            userlogin: true,
        return fetch(getApiEndpoint(), {
            passwordreset: true,
            method: 'POST',
            resetpass: true,
            credentials: 'same-origin',
            confirmemail: true
            headers: { 'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8' },
        };
            body: body.toString()
        }).then(function (res) {
            if (!res.ok) throw new Error('HTTP ' + res.status);
            return res.json();
         });
    }
 
 
    /* =========================================
      Revision manifest and persistent entry cache
      =========================================
      The first boot still prepares current-tab artifacts, but freshness is not guessed from
      filenames or old localStorage flags.  The client reads a tiny server-side current-state
      manifest, compares page revisions / file sha1 values, and only keeps cached raw resources
      whose revision token still matches the server.  The manifest is a latest-state table, not
      an append-only client log.
    */
    var REVISION_MANIFEST_ACTION = 'entryrevisionmanifest';
    var REVISION_MANIFEST_LOCAL_KEY = 'entry-revision-manifest-current-v1';
    var REVISION_MANIFEST_PRUNE_LOCAL_KEY = 'entry-revision-manifest-last-client-prune-v1';
    var ENTRY_CACHE_NAME = 'entry-cache-v1';
    var ENTRY_CACHE_INDEX_KEY = 'entry-cache-index-v1';
    var ENTRY_CACHE_REQUEST_PREFIX = '/__entry-cache__/';
    var ENTRY_CACHE_PACK_KEY = 'entry-cache-pack-state-v1';


    function hasCacheStorage() {
        if (allowed[canonical]) return true;
         return !!(window.caches && typeof window.caches.open === 'function');
         return /^(?:special|특수):(?:userlogin|로그인|passwordreset|비밀번호 ?재설정|resetpass|confirmemail)/i.test(name);
     }
     }


     function safeJsonParse(text, fallback) {
     function requiresLoginGate() {
         try { return JSON.parse(text); } catch (err) { return fallback; }
         return isAnonymousUser() && !isAuthenticationPage();
     }
     }


     function readLocalJson(key, fallback) {
     function isBootExcludedPage() {
         try {
        var ns = Number(readConfig('wgNamespaceNumber', NaN));
            var text = window.localStorage ? window.localStorage.getItem(key) : null;
        var action = String(readConfig('wgAction', 'view') || 'view').toLowerCase();
            return text ? safeJsonParse(text, fallback) : fallback;
         var model = String(readConfig('wgPageContentModel', '') || '').toLowerCase();
         } catch (err) {
        var name = normalizeBootPageName(readConfig('wgPageName', '') || window.location.pathname || '');
            return fallback;
        var systemNamespaces = {
         }
            '-1': true, '4': true, '5': true, '6': true, '7': true, '8': true, '9': true,
            '10': true, '11': true, '12': true, '13': true, '14': true, '15': true,
            '828': true, '829': true
        };
 
        /*
        * The public boot gate is for normal reading surfaces. Developer/system
        * namespaces and edit/diff/history actions already reload outside SPA, so
        * blocking them would turn every maintenance save into another entry boot.
        */
        if (hasQueryFlag('bootGatePreview', '1')) return false;
        if (action && action !== 'view') return true;
        if (systemNamespaces[String(ns)]) return true;
        if (model === 'css' || model === 'javascript' || model === 'json' || model === 'sanitized-css') return true;
        if (/\.(?:css|js|json)$/i.test(name)) return true;
        if (/^(?:mediawiki|미디어위키|file|파일|project|프로젝트|template|틀|module|모듈|category|분류|special|특수):/i.test(name)) return true;
         return false;
    }
 
    var LOGIN_REQUIRED = requiresLoginGate();
    var BOOT_EXCLUDED_PAGE = isBootExcludedPage() && !LOGIN_REQUIRED;
 
    function injectEarlyBootStyle() {
        var style;
        if (earlyBootStyleInjected || !document.documentElement) return;
         earlyBootStyleInjected = true;
        style = document.createElement('style');
        style.id = 'boot-gate-early-style';
        style.textContent = [
            'html.boot-gate-active, body.boot-gate-active{overflow:hidden!important;}',
            'html.boot-gate-active body{background:#080808!important;}',
            'html.boot-gate-active body> :not(#boot-gate-screen){visibility:hidden!important;pointer-events:none!important;}',
            'html.boot-gate-active #boot-gate-screen,html.boot-gate-active #boot-gate-screen *{visibility:visible!important;}'
        ].join('');
        (document.head || document.documentElement).appendChild(style);
     }
     }


     function writeLocalJson(key, value) {
     function activateBootSurface() {
         try {
         injectEarlyBootStyle();
            if (window.localStorage) window.localStorage.setItem(key, JSON.stringify(value));
        if (document.documentElement) document.documentElement.classList.add('boot-gate-active');
         } catch (err) {}
         if (document.body) document.body.classList.add('boot-gate-active');
     }
     }


     function normalizeManifestTitle(value) {
     if (!BOOT_EXCLUDED_PAGE) activateBootSurface();
        var text = String(value || '').trim();
    else if (window.__BootGatePrelude && window.__BootGatePrelude.release) window.__BootGatePrelude.release();
        var match;
 
        var i;
    var defaultManifest = {
         if (!text) return '';
         version: '20260710-globe-vhs-restore-entry-001',
         for (i = 0; i < 3; i += 1) {
         boot: {
             try {
             minDisplayMs: 950,
                if (/%[0-9a-f]{2}/i.test(text)) text = decodeURIComponent(text);
            cachedMinDisplayMs: 350,
             } catch (err) { break; }
             maxBlockingMs: 15000
         }
         },
         match = text.match(/[?&]title=([^&#]+)/i);
         initial: {
        if (match) text = match[1];
            full: [
        text = text.replace(/^https?:\/\/[^/]+/i, '')
                {
            .replace(/^\/+/, '')
                    id: 'decorations-registry',
            .replace(/^index\.php\/?/i, '')
                    label: 'DECORATION REGISTRY',
            .replace(/^wiki\/?/i, '')
                    type: 'decorations',
            .trim();
                    ref: 'MediaWiki:Decorations.json',
        match = text.match(/(?:^|\/)(?:(?:Special|특수)\s*[:/]\s*)?(?:Redirect|넘겨주기)\s*\/\s*file\s*\/([^?#]+)(?:[?#].*)?$/i);
                    page: '시대',
        if (match) text = 'File:' + match[1];
                    era: '1950',
        text = text.split('#')[0].replace(/_/g, ' ').replace(/\s+/g, ' ').trim();
                    preparePixels: true
        text = text.replace(/^(?:파일|Image|이미지)\s*:/i, 'File:');
                },
        if (/^(?:mediawiki|template|module|manage|file)\s*:/i.test(text)) {
                {
             text = text.replace(/^([^:]+)\s*:\s*/, function (_, ns) { return ns.charAt(0).toUpperCase() + ns.slice(1).toLowerCase() + ':'; });
                    id: 'nations-1950-entry',
                    label: '1950 ERA ENTRY',
                    type: 'nations-era',
                    era: '1950',
                    level: 'full'
                }
            ],
            half: [
                {
                    id: 'nations-1960-half',
                    label: '1960 ERA HALF',
                    type: 'nations-era',
                    era: '1960',
                    level: 'half'
                }
             ]
         }
         }
        if (/^File:/i.test(text)) text = 'File:' + text.slice(text.indexOf(':') + 1).trim();
     };
        if (/^Mediawiki:/i.test(text)) text = 'MediaWiki:' + text.slice(text.indexOf(':') + 1).trim();
        return text;
     }


     function resourceToken(resource) {
     function now() {
         if (!resource || typeof resource !== 'object') return '';
         return Date.now ? Date.now() : new Date().getTime();
        return String(resource.revision || resource.sha1 || resource.hash || resource.timestamp || resource.updatedAt || resource.url || '').trim();
     }
     }


    function createEntryCache() {
        var index = readLocalJson(ENTRY_CACHE_INDEX_KEY, { entries: {} }) || { entries: {} };
        var packs = readLocalJson(ENTRY_CACHE_PACK_KEY, { packs: {} }) || { packs: {} };
        var objectUrls = {};
        var stats = {
            textHits: 0,
            blobHits: 0,
            misses: 0,
            networkStores: 0,
            stores: 0,
            deletes: 0
        };


        function ensureIndex() {
    var BootPerf = window.BootPerf = window.BootPerf || (function () {
            if (!index || typeof index !== 'object') index = { entries: {} };
        var t0 = now();
            if (!index.entries || typeof index.entries !== 'object') index.entries = {};
        var entries = [];
         }
        var active = {};
         var seq = 0;


         function compactIndexForLocalStorage() {
         function cloneMeta(meta) {
             var entries;
             var out = {};
            var next = {};
             Object.keys(meta || {}).forEach(function (key) {
             ensureIndex();
                 var value = meta[key];
            entries = index.entries || {};
                 if (value == null) return;
            Object.keys(entries).forEach(function (key) {
                 if (typeof value === 'string' || typeof value === 'number' || typeof value === 'boolean') out[key] = value;
                 var entry = entries[key] || {};
                 else {
                 var kind = String(entry.kind || 'raw').toLowerCase();
                    try { out[key] = JSON.parse(JSON.stringify(value)); }
                /*
                    catch (err) { out[key] = String(value); }
                * CacheStorage already owns image/blob bodies.  Keeping one localStorage
                }
                * metadata row per flag/blob competes with MediaWiki ResourceLoader's own
                * localStorage module store and can trigger QuotaExceededError before our
                * code even runs on the next page load.  Text/json/file URL rows stay indexed
                * because they are small and useful for invalidation diagnostics.
                */
                 if (kind === 'image' || kind === 'blob') return;
                 next[key] = entry;
             });
             });
             index.entries = next;
             return out;
         }
         }


         function ensurePacks() {
         function start(name, meta) {
             if (!packs || typeof packs !== 'object') packs = { packs: {} };
             var id = String(++seq);
            if (!packs.packs || typeof packs.packs !== 'object') packs.packs = {};
            active[id] = { id: id, name: String(name || 'entry'), start: now(), meta: cloneMeta(meta) };
            return id;
         }
         }


         function saveIndex() {
         function end(id, meta) {
             ensureIndex();
             var item = active[id];
             compactIndexForLocalStorage();
            var ended = now();
             writeLocalJson(ENTRY_CACHE_INDEX_KEY, index);
             if (!item) return null;
            delete active[id];
             item.end = ended;
            item.ms = ended - item.start;
            item.offset = item.start - t0;
            item.meta = Object.assign({}, item.meta || {}, cloneMeta(meta));
            entries.push(item);
            return item;
         }
         }


         function savePacks() {
         function instant(name, meta) {
             ensurePacks();
             var t = now();
             writeLocalJson(ENTRY_CACHE_PACK_KEY, packs);
             entries.push({ id: String(++seq), name: String(name || 'mark'), start: t, end: t, ms: 0, offset: t - t0, meta: cloneMeta(meta) });
         }
         }


         function requestForKey(key) {
         function measure(name, meta, fn) {
             return new Request(ENTRY_CACHE_REQUEST_PREFIX + encodeURIComponent(String(key || '')), { credentials: 'same-origin' });
             var id = start(name, meta);
            try {
                return Promise.resolve(fn()).then(function (value) {
                    end(id, { ok: true });
                    return value;
                }, function (err) {
                    end(id, { ok: false, error: err && (err.message || String(err)) });
                    throw err;
                });
            } catch (err) {
                end(id, { ok: false, error: err && (err.message || String(err)) });
                return Promise.reject(err);
            }
         }
         }


         function openCache() {
         function rows() {
             if (!hasCacheStorage()) return Promise.resolve(null);
             return entries.slice().sort(function (a, b) { return a.start - b.start; }).map(function (item) {
            return window.caches.open(ENTRY_CACHE_NAME).catch(function () { return null; });
                return {
        }
                    offset: item.offset,
 
                    ms: item.ms,
        function cacheEntry(key, meta) {
                    name: item.name,
            key = String(key || '');
                    meta: item.meta || {}
            ensureIndex();
                 };
            index.entries[key] = Object.assign({}, index.entries[key] || {}, {
                key: key,
                resourceKey: String(meta && meta.resourceKey || ''),
                token: String(meta && meta.token || ''),
                kind: String(meta && meta.kind || 'raw'),
                contentType: String(meta && meta.contentType || ''),
                size: Number(meta && meta.size || 0) || null,
                 createdAt: index.entries[key] && index.entries[key].createdAt ? index.entries[key].createdAt : now(),
                updatedAt: now(),
                lastHitAt: index.entries[key] && index.entries[key].lastHitAt ? index.entries[key].lastHitAt : null,
                hits: Number(index.entries[key] && index.entries[key].hits || 0)
             });
             });
            saveIndex();
         }
         }


         function markHit(key, field) {
         function summary() {
             key = String(key || '');
             var list = rows();
             ensureIndex();
             var total = list.reduce(function (max, item) { return Math.max(max, item.offset + item.ms); }, 0);
            if (index.entries[key]) {
            return {
                index.entries[key].hits = Number(index.entries[key].hits || 0) + 1;
                build: BUILD_ID,
                 index.entries[key].lastHitAt = now();
                startedAt: t0,
                saveIndex();
                totalMs: total,
             }
                 entries: list,
            if (field && stats[field] != null) stats[field] += 1;
                active: Object.keys(active).map(function (id) { return active[id]; })
             };
         }
         }


         function getResponse(key) {
         function print() {
             key = String(key || '');
             var list = rows();
             if (!key || !hasCacheStorage()) return Promise.resolve(null);
             if (!window.console || !console.log) return summary();
             return openCache().then(function (cache) {
             try {
                 if (!cache) return null;
                console.groupCollapsed('[BootPerf] entry loading timeline · ' + BUILD_ID);
                return cache.match(requestForKey(key)).then(function (res) {
                 if (console.table) console.table(list.map(function (item) {
                     if (!res) {
                     return {
                         stats.misses += 1;
                         offset: item.offset + 'ms',
                         return null;
                        duration: item.ms + 'ms',
                    }
                         name: item.name,
                     return res;
                        detail: JSON.stringify(item.meta || {})
                 });
                     };
            }).catch(function () { return null; });
                 }));
                else list.forEach(function (item) { console.log(item.offset + 'ms', item.ms + 'ms', item.name, item.meta || {}); });
                console.groupEnd();
            } catch (err) {}
            return summary();
         }
         }


         function putResponse(key, response, meta) {
         return {
            key = String(key || '');
             start: start,
            if (!key || !response || !hasCacheStorage()) return Promise.resolve(false);
            end: end,
            meta = meta || {};
            mark: instant,
             return openCache().then(function (cache) {
            measure: measure,
                var cloned;
            rows: rows,
                var headers;
            summary: summary,
                var bodyPromise;
            print: print
                if (!cache) return false;
        };
                cloned = response.clone();
    }());
                bodyPromise = cloned.blob().catch(function () { return null; });
 
                return bodyPromise.then(function (blob) {
                    if (!blob) return false;
                    headers = new Headers(response.headers || {});
                    if (!headers.get('Content-Type')) headers.set('Content-Type', meta.contentType || blob.type || 'application/octet-stream');
                    headers.set('X-Entry-Cache-Key', key);
                    headers.set('X-Entry-Resource-Key', String(meta.resourceKey || ''));
                    headers.set('X-Entry-Revision-Token', String(meta.token || ''));
                    headers.set('X-Entry-Cached-At', String(now()));
                    return cache.put(requestForKey(key), new Response(blob, { status: 200, headers: headers })).then(function () {
                        stats.stores += 1;
                        cacheEntry(key, {
                            resourceKey: meta.resourceKey,
                            token: meta.token,
                            kind: meta.kind || 'blob',
                            contentType: headers.get('Content-Type') || blob.type || '',
                            size: blob.size
                        });
                        return true;
                    });
                });
            }).catch(function () { return false; });
        }


        function getText(key) {
    var InteractionPerf = window.InteractionPerf = window.InteractionPerf || (function () {
            return getResponse(key).then(function (res) {
        var BUILD = '20260710-interaction-perf-instrument-001';
                if (!res) return null;
        var t0 = (window.performance && performance.now ? performance.now() : now());
                markHit(key, 'textHits');
        var entries = [];
                return res.text();
        var active = {};
             }).catch(function () { return null; });
        var seq = 0;
        var maxEntries = 1600;
 
        function perfNow() {
             return window.performance && performance.now ? performance.now() : now();
         }
         }


         function putText(key, text, meta) {
         function cloneMeta(meta) {
             key = String(key || '');
             var out = {};
             if (!key || !hasCacheStorage()) return Promise.resolve(false);
             Object.keys(meta || {}).forEach(function (key) {
            meta = meta || {};
                 var value = meta[key];
            return openCache().then(function (cache) {
                 if (value == null) return;
                 var headers;
                 if (typeof value === 'string' || typeof value === 'number' || typeof value === 'boolean') out[key] = value;
                var body = String(text || '');
                else {
                 if (!cache) return false;
                     try { out[key] = JSON.parse(JSON.stringify(value)); }
                 headers = new Headers({
                    catch (err) { out[key] = String(value); }
                    'Content-Type': meta.contentType || 'text/plain; charset=UTF-8',
                }
                    'X-Entry-Cache-Key': key,
            });
                    'X-Entry-Resource-Key': String(meta.resourceKey || ''),
             return out;
                     'X-Entry-Revision-Token': String(meta.token || ''),
                    'X-Entry-Cached-At': String(now())
                });
                return cache.put(requestForKey(key), new Response(body, { status: 200, headers: headers })).then(function () {
                    stats.stores += 1;
                    cacheEntry(key, {
                        resourceKey: meta.resourceKey,
                        token: meta.token,
                        kind: meta.kind || 'text',
                        contentType: headers.get('Content-Type'),
                        size: body.length
                    });
                    return true;
                });
             }).catch(function () { return false; });
         }
         }


         function getBlobUrl(key) {
         function push(entry) {
             key = String(key || '');
             entries.push(entry);
            if (!key) return Promise.resolve('');
             if (entries.length > maxEntries) entries.splice(0, entries.length - maxEntries);
             if (objectUrls[key]) {
             return entry;
                markHit(key, 'blobHits');
                return Promise.resolve(objectUrls[key]);
            }
            return getResponse(key).then(function (res) {
                if (!res) return '';
                return res.blob().then(function (blob) {
                    if (!blob || !blob.size) return '';
                    objectUrls[key] = URL.createObjectURL(blob);
                    markHit(key, 'blobHits');
                    return objectUrls[key];
                });
             }).catch(function () { return ''; });
         }
         }


         function fetchBlobUrl(url, key, meta, options) {
         function start(name, meta) {
             url = String(url || '').trim();
             var id = String(++seq);
             key = String(key || '').trim();
             active[id] = { id: id, name: String(name || 'interaction'), start: perfNow(), meta: cloneMeta(meta) };
             if (!url || !key) return Promise.resolve('');
             return id;
             return getBlobUrl(key).then(function (cachedUrl) {
        }
                if (cachedUrl) return cachedUrl;
 
                return fetch(url, {
        function end(id, meta) {
                    credentials: 'same-origin',
            var item = active[id];
                    cache: options && options.noStore ? 'no-store' : 'force-cache'
             var ended = perfNow();
                }).then(function (res) {
            if (!item) return null;
                    if (!res.ok) throw new Error('HTTP ' + res.status);
            delete active[id];
                    stats.networkStores += 1;
            item.end = ended;
                    return putResponse(key, res, Object.assign({}, meta || {}, { kind: meta && meta.kind ? meta.kind : 'blob' })).then(function () {
            item.ms = Math.round((ended - item.start) * 100) / 100;
                        return getBlobUrl(key);
            item.offset = Math.round((item.start - t0) * 100) / 100;
                    });
            item.meta = Object.assign({}, item.meta || {}, cloneMeta(meta));
                }).catch(function () {
            return push(item);
                    return '';
                });
            });
         }
         }


         function deleteKey(key) {
         function mark(name, meta) {
             key = String(key || '');
             var t = perfNow();
             if (!key || !hasCacheStorage()) return Promise.resolve(false);
             return push({ id: String(++seq), name: String(name || 'mark'), start: t, end: t, ms: 0, offset: Math.round((t - t0) * 100) / 100, meta: cloneMeta(meta) });
            if (objectUrls[key]) {
                try { URL.revokeObjectURL(objectUrls[key]); } catch (err) {}
                delete objectUrls[key];
            }
            return openCache().then(function (cache) {
                if (!cache) return false;
                return cache.delete(requestForKey(key)).then(function (ok) {
                    ensureIndex();
                    if (index.entries && index.entries[key]) {
                        delete index.entries[key];
                        stats.deletes += 1;
                        saveIndex();
                    }
                    return ok;
                });
            }).catch(function () { return false; });
         }
         }


         function invalidateResources(resourceKeys) {
         function measureSync(name, meta, fn) {
             var map = {};
             var id = start(name, meta);
            var entries;
             try {
            var keys = [];
                 var value = fn();
            ensureIndex();
                 end(id, { ok: true });
             entries = index.entries || {};
                return value;
            (resourceKeys || []).forEach(function (resourceKey) {
             } catch (err) {
                 resourceKey = String(resourceKey || '').toLowerCase();
                 end(id, { ok: false, error: err && (err.message || String(err)) || String(err) });
                 if (resourceKey) map[resourceKey] = true;
                throw err;
            });
             }
             Object.keys(entries).forEach(function (cacheKey) {
                 var resourceKey = String(entries[cacheKey] && entries[cacheKey].resourceKey || '').toLowerCase();
                if (map[resourceKey]) keys.push(cacheKey);
            });
             return Promise.all(keys.map(deleteKey)).then(function () { return keys.length; });
         }
         }


         function prune(maxAgeDays) {
         function measureAsync(name, meta, fn) {
             var days = Math.max(1, Number(maxAgeDays) || 7);
             var id = start(name, meta);
             var cutoff = now() - days * 24 * 60 * 60 * 1000;
             try {
            var entries;
                return Promise.resolve(fn()).then(function (value) {
            var keys;
                    end(id, { ok: true });
            ensureIndex();
                    return value;
            entries = index.entries || {};
                }, function (err) {
            keys = Object.keys(entries).filter(function (key) {
                    end(id, { ok: false, error: err && (err.message || String(err)) || String(err) });
                return Number(entries[key] && entries[key].createdAt || 0) < cutoff;
                    throw err;
            });
                });
             window.localStorage && window.localStorage.setItem(REVISION_MANIFEST_PRUNE_LOCAL_KEY, String(now()));
             } catch (err) {
            return Promise.all(keys.map(deleteKey)).then(function () { return keys.length; });
                end(id, { ok: false, error: err && (err.message || String(err)) || String(err) });
                return Promise.reject(err);
            }
         }
         }


         function reset() {
         function summary() {
             Object.keys(objectUrls).forEach(function (key) {
             return {
                 try { URL.revokeObjectURL(objectUrls[key]); } catch (err) {}
                 build: BUILD,
            });
                startedAt: t0,
            objectUrls = {};
                totalMs: Math.round((perfNow() - t0) * 100) / 100,
            index = { entries: {} };
                entries: entries.slice().sort(function (a, b) {
            packs = { packs: {} };
                    if (a.offset !== b.offset) return a.offset - b.offset;
            saveIndex();
                    return b.ms - a.ms;
            savePacks();
                }),
            if (!hasCacheStorage()) return Promise.resolve(false);
                active: Object.keys(active).map(function (id) {
            return window.caches.delete(ENTRY_CACHE_NAME).catch(function () { return false; });
                    var item = active[id];
        }
                    return { offset: Math.round((item.start - t0) * 100) / 100, ms: Math.round((perfNow() - item.start) * 100) / 100, name: item.name, meta: item.meta || {} };
 
                })
        function packReady(key, token) {
            };
            key = String(key || '');
            token = String(token || '');
            ensurePacks();
            if (!key || !token) return false;
            return !!(packs.packs[key] && String(packs.packs[key].token || '') === token && packs.packs[key].ready);
         }
         }


         function setPackReady(key, token, meta) {
         function table() {
             key = String(key || '');
             var data = summary().entries.map(function (entry) {
            token = String(token || '');
                 return { offset: entry.offset, ms: entry.ms, name: entry.name, meta: entry.meta };
            if (!key || !token) return false;
            ensurePacks();
            packs.packs[key] = Object.assign({}, meta || {}, {
                 key: key,
                token: token,
                ready: true,
                updatedAt: now()
             });
             });
             savePacks();
             if (window.console && console.table) console.table(data);
             return true;
             return data;
         }
         }


         function packInfo() {
         try {
            ensurePacks();
            if (window.PerformanceObserver && !window.CLBI_InteractionLongTaskObserverBound) {
            return Object.assign({}, packs.packs || {});
                window.CLBI_InteractionLongTaskObserverBound = true;
         }
                new PerformanceObserver(function (list) {
                    list.getEntries().forEach(function (entry) {
                        mark('browser long task', {
                            ms: Math.round(entry.duration * 100) / 100,
                            start: Math.round(entry.startTime * 100) / 100,
                            attribution: entry.attribution && entry.attribution.length ? entry.attribution.length : 0
                        });
                    });
                }).observe({ entryTypes: ['longtask'] });
            }
         } catch (ignoreLongTaskObserver) {}


         function info() {
         return {
             var lastClientPrune = 0;
             build: BUILD,
            var entries;
             start: start,
            var byKind = {};
            end: end,
            try { lastClientPrune = Number(window.localStorage && window.localStorage.getItem(REVISION_MANIFEST_PRUNE_LOCAL_KEY) || 0); } catch (err) {}
            mark: mark,
            ensureIndex();
            measureSync: measureSync,
            ensurePacks();
            measureAsync: measureAsync,
            entries = index.entries || {};
            summary: summary,
            Object.keys(entries).forEach(function (key) {
            table: table
                var kind = String(entries[key] && entries[key].kind || 'raw');
        };
                byKind[kind] = (byKind[kind] || 0) + 1;
    }());
             });
            return {
                supported: hasCacheStorage(),
                name: ENTRY_CACHE_NAME,
                entries: Object.keys(entries).length,
                byKind: byKind,
                packs: Object.keys(packs.packs || {}).length,
                lastClientPruneAt: lastClientPrune || null,
                stats: Object.assign({}, stats)
            };
        }


         /* Trim legacy image/blob metadata rows produced by older builds. */
    function toArray(value) {
        try { saveIndex(); } catch (err) {}
         return Array.prototype.slice.call(value || []);
    }


         return {
    function unique(list) {
            getResponse: getResponse,
         var seen = {};
            putResponse: putResponse,
        var out = [];
             getText: getText,
        (list || []).forEach(function (item) {
             putText: putText,
             item = String(item || '').trim();
             getBlobUrl: getBlobUrl,
             if (!item || seen[item]) return;
             fetchBlobUrl: fetchBlobUrl,
             seen[item] = true;
            invalidateResources: invalidateResources,
             out.push(item);
            prune: prune,
        });
            reset: reset,
         return out;
            packReady: packReady,
            setPackReady: setPackReady,
            packInfo: packInfo,
            info: info
         };
     }
     }


     window.EntryCache = window.EntryCache || createEntryCache();
     function hasBootParam(value) {
        var search = String(window.location && window.location.search || '');
        var re = new RegExp('[?&]' + DISMISS_PARAM + '=([^&]+)');
        var match = search.match(re);
        return match && decodeURIComponent(match[1]) === value;
    }


     function createRevisionManifestService() {
     function normalizeTitle(value) {
         var current = null;
         return String(value || '')
        var previous = readLocalJson(REVISION_MANIFEST_LOCAL_KEY, null);
            .split('#')[0]
        var loadPromise = null;
            .replace(/_/g, ' ')
        var changedResources = [];
            .trim();
    }


        function unwrap(payload) {
    function extractTitleFromUrl(value) {
            return payload && (payload.entryrevisionmanifest || payload.revisionManifest || payload) || null;
        var text = String(value || '');
         }
        var match = text.match(/[?&]title=([^&]+)/i);
        if (match) return normalizeTitle(decodeURIComponent(match[1].replace(/\+/g, ' ')));
         return '';
    }


        function resourcesOf(manifest) {
    function normalizeRefKey(ref) {
            return manifest && manifest.resources && typeof manifest.resources === 'object' ? manifest.resources : {};
        var text = String(ref || '').trim();
        }
        var title;
        if (!text) return '';
        title = extractTitleFromUrl(text);
        if (title) return 'title:' + title.toLowerCase();
        if (text.indexOf('/') === -1 && text.indexOf(':') !== -1) return 'title:' + normalizeTitle(text).toLowerCase();
        return 'url:' + text;
    }


        function compactRevisionManifest(manifest) {
    function rawUrlForRef(ref, ctype) {
            var out;
        var text = String(ref || '').trim();
            var resources;
        var title;
            if (!manifest || typeof manifest !== 'object') return null;
        if (!text) return '';
            out = {
        if (/^(?:https?:)?\/\//i.test(text) || text.charAt(0) === '/') return text;
                manifestVersion: manifest.manifestVersion || manifest.version || '',
        title = normalizeTitle(text.indexOf(':') !== -1 ? text : ('MediaWiki:' + text));
                version: manifest.version || manifest.manifestVersion || '',
        if (mw && mw.util && typeof mw.util.getUrl === 'function') {
                generatedAt: manifest.generatedAt || '',
            return mw.util.getUrl(title, { action: 'raw', ctype: ctype || 'application/json' });
                pruneDays: manifest.pruneDays,
                clientPruneDays: manifest.clientPruneDays,
                resources: {}
            };
            resources = resourcesOf(manifest);
            Object.keys(resources).forEach(function (title) {
                var src = resources[title];
                var dst;
                if (!src || typeof src !== 'object') return;
                dst = {};
                ['revision', 'sha1', 'hash', 'timestamp', 'updatedAt', 'url'].forEach(function (key) {
                    if (src[key] !== undefined && src[key] !== null && String(src[key]) !== '') dst[key] = src[key];
                });
                out.resources[title] = dst;
            });
            return out;
         }
         }
        return '/index.php?title=' + encodeURIComponent(title) + '&action=raw&ctype=' + encodeURIComponent(ctype || 'application/json');
    }


        function buildLookup(manifest) {
    function getApiEndpoint() {
            var lookup = {};
        return (mw && mw.util && typeof mw.util.wikiScript === 'function') ? mw.util.wikiScript('api') : '/api.php';
            Object.keys(resourcesOf(manifest)).forEach(function (title) {
    }
                lookup[normalizeManifestTitle(title).toLowerCase()] = resourcesOf(manifest)[title];
            });
            return lookup;
        }


        function computeChanged(prev, next) {
    function fetchApi(params) {
            var prevLookup = buildLookup(prev);
        var body = new URLSearchParams();
            var nextLookup = buildLookup(next);
        Object.keys(params || {}).forEach(function (key) {
            var out = [];
            body.append(key, params[key]);
            Object.keys(nextLookup).forEach(function (key) {
        });
                if (resourceToken(prevLookup[key]) !== resourceToken(nextLookup[key])) out.push(key);
        return fetch(getApiEndpoint(), {
             });
            method: 'POST',
             Object.keys(prevLookup).forEach(function (key) {
            credentials: 'same-origin',
                if (!nextLookup[key]) out.push(key);
             headers: { 'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8' },
             });
             body: body.toString()
            return unique(out);
        }).then(function (res) {
        }
            if (!res.ok) throw new Error('HTTP ' + res.status);
             return res.json();
        });
    }


        function load(options) {
            if (loadPromise && !(options && options.force)) return loadPromise;
            loadPromise = fetchApi({
                action: REVISION_MANIFEST_ACTION,
                format: 'json',
                formatversion: '2'
            }).then(function (payload) {
                var manifest = unwrap(payload);
                if (!manifest || !manifest.resources) throw new Error('invalid revision manifest');
                current = compactRevisionManifest(manifest) || manifest;
                changedResources = computeChanged(previous, current);
                if (changedResources.length && window.EntryCache && typeof window.EntryCache.invalidateResources === 'function') {
                    window.EntryCache.invalidateResources(changedResources);
                }
                writeLocalJson(REVISION_MANIFEST_LOCAL_KEY, current);
                previous = current;
                maybePrune();
                return current;
            }).catch(function () {
                current = previous || null;
                return current;
            });
            return loadPromise;
        }


        function ensureLoaded() {
    /* =========================================
            return current ? Promise.resolve(current) : load();
      Revision manifest and persistent entry cache
        }
      =========================================
      The first boot still prepares current-tab artifacts, but freshness is not guessed from
      filenames or old localStorage flags. The client reads a tiny server-side current-state
      manifest, compares page revisions / file sha1 values, and only keeps cached raw resources
      whose revision token still matches the server.  The manifest is a latest-state table, not
      an append-only client log.
    */
    var REVISION_MANIFEST_ACTION = 'entryrevisionmanifest';
    var REVISION_MANIFEST_LOCAL_KEY = 'entry-revision-manifest-current-v1';
    var REVISION_MANIFEST_PRUNE_LOCAL_KEY = 'entry-revision-manifest-last-client-prune-v1';
    var ENTRY_CACHE_NAME = 'entry-cache-v1';
    var ENTRY_CACHE_INDEX_KEY = 'entry-cache-index-v1';
    var ENTRY_CACHE_REQUEST_PREFIX = '/__entry-cache__/';
    var ENTRY_CACHE_PACK_KEY = 'entry-cache-pack-state-v1';


        function resourceForRef(ref) {
    function hasCacheStorage() {
            var title = normalizeManifestTitle(ref);
        return !!(window.caches && typeof window.caches.open === 'function');
            var lookup;
    }
            if (!title || !current) return null;
 
            lookup = buildLookup(current);
    function safeJsonParse(text, fallback) {
            return lookup[title.toLowerCase()] || null;
        try { return JSON.parse(text); } catch (err) { return fallback; }
        }
    }


        function tokenForRef(ref) {
    function readLocalJson(key, fallback) {
             return resourceToken(resourceForRef(ref));
        try {
            var text = window.localStorage ? window.localStorage.getItem(key) : null;
             return text ? safeJsonParse(text, fallback) : fallback;
        } catch (err) {
            return fallback;
         }
         }
    }


        function resourceKeyForRef(ref) {
    function writeLocalJson(key, value) {
             var title = normalizeManifestTitle(ref);
        try {
            return title ? title.toLowerCase() : '';
             if (window.localStorage) window.localStorage.setItem(key, JSON.stringify(value));
         }
         } catch (err) {}
    }


        function cacheKeyForRef(ref, type) {
    function normalizeManifestTitle(value) {
            var resourceKey = resourceKeyForRef(ref);
        var text = String(value || '').trim();
            var token = tokenForRef(ref);
        var match;
            if (!resourceKey || !token) return '';
        var i;
             return String(type || 'raw') + ':' + resourceKey + '@' + token;
        if (!text) return '';
        for (i = 0; i < 3; i += 1) {
            try {
                if (/%[0-9a-f]{2}/i.test(text)) text = decodeURIComponent(text);
             } catch (err) { break; }
         }
         }
 
        match = text.match(/[?&]title=([^&#]+)/i);
         function manifestToken(extra) {
         if (match) text = match[1];
             var base = current && (current.manifestVersion || current.version || current.generatedAt || '') || '';
        text = text.replace(/^https?:\/\/[^/]+/i, '')
            return String(base || 'no-manifest') + (extra ? (':' + String(extra)) : '');
            .replace(/^\/+/, '')
            .replace(/^index\.php\/?/i, '')
            .replace(/^wiki\/?/i, '')
             .trim();
        match = text.match(/(?:^|\/)(?:(?:Special|특수)\s*[:/]\s*)?(?:Redirect|넘겨주기)\s*\/\s*file\s*\/([^?#]+)(?:[?#].*)?$/i);
        if (match) text = 'File:' + match[1];
        text = text.split('#')[0].replace(/_/g, ' ').replace(/\s+/g, ' ').trim();
        text = text.replace(/^(?:파일|Image|이미지)\s*:/i, 'File:');
        if (/^(?:mediawiki|template|module|manage|file)\s*:/i.test(text)) {
            text = text.replace(/^([^:]+)\s*:\s*/, function (_, ns) { return ns.charAt(0).toUpperCase() + ns.slice(1).toLowerCase() + ':'; });
         }
         }
        if (/^File:/i.test(text)) text = 'File:' + text.slice(text.indexOf(':') + 1).trim();
        if (/^Mediawiki:/i.test(text)) text = 'MediaWiki:' + text.slice(text.indexOf(':') + 1).trim();
        return text;
    }


        function tokenFromUrl(url) {
    function resourceToken(resource) {
            var text = String(url || '');
        if (!resource || typeof resource !== 'object') return '';
            var match = text.match(/[?&](?:_entryFileRev|_entryAsset|_entryRev)=([^&]+)/);
        return String(resource.revision || resource.sha1 || resource.hash || resource.timestamp || resource.updatedAt || resource.url || '').trim();
             if (!match) return '';
    }
             try { return decodeURIComponent(match[1]); } catch (err) { return match[1]; }
 
    function createEntryCache() {
        var index = readLocalJson(ENTRY_CACHE_INDEX_KEY, { entries: {} }) || { entries: {} };
        var packs = readLocalJson(ENTRY_CACHE_PACK_KEY, { packs: {} }) || { packs: {} };
        var objectUrls = {};
        var stats = {
            textHits: 0,
            blobHits: 0,
            misses: 0,
            networkStores: 0,
            stores: 0,
            deletes: 0
        };
 
        function ensureIndex() {
             if (!index || typeof index !== 'object') index = { entries: {} };
             if (!index.entries || typeof index.entries !== 'object') index.entries = {};
         }
         }


         function cacheKeyForUrl(url, type) {
         function compactIndexForLocalStorage() {
             var text = String(url || '').trim();
             var entries;
             var token = tokenFromUrl(text) || manifestToken('url');
             var next = {};
             var normalized;
            ensureIndex();
             if (!text) return '';
             entries = index.entries || {};
            try {
             Object.keys(entries).forEach(function (key) {
                 normalized = new URL(text, window.location.href);
                 var entry = entries[key] || {};
                 text = normalized.pathname + (normalized.search || '');
                 var kind = String(entry.kind || 'raw').toLowerCase();
            } catch (err) {}
                /*
            text = text.replace(/[?&](?:_entryFileRev|_entryAsset|_entryRev)=[^&]*/g, '').replace(/[?&]$/, '');
                * CacheStorage already owns image/blob bodies.  Keeping one localStorage
             try { text = decodeURI(text); } catch (err2) {}
                * metadata row per flag/blob competes with MediaWiki ResourceLoader's own
             return String(type || 'blob') + ':url:' + text.toLowerCase() + '@' + token;
                * localStorage module store and can trigger QuotaExceededError before our
                * code even runs on the next page load.  Text/json/file URL rows stay indexed
                * because they are small and useful for invalidation diagnostics.
                */
                if (kind === 'image' || kind === 'blob') return;
                next[key] = entry;
             });
             index.entries = next;
         }
         }


         function addRevisionParam(url, ref) {
         function ensurePacks() {
             var token = tokenForRef(ref);
             if (!packs || typeof packs !== 'object') packs = { packs: {} };
            var text = String(url || '').trim();
             if (!packs.packs || typeof packs.packs !== 'object') packs.packs = {};
            var sep;
             if (!text || !token || /[?&]_entryRev=/.test(text)) return text;
            sep = text.indexOf('?') === -1 ? '?' : '&';
            return text + sep + '_entryRev=' + encodeURIComponent(token);
         }
         }


         function maybePrune() {
         function saveIndex() {
             var days = current && Number(current.pruneDays || current.clientPruneDays) || 7;
             ensureIndex();
             var last = 0;
             compactIndexForLocalStorage();
            var due;
             writeLocalJson(ENTRY_CACHE_INDEX_KEY, index);
            try { last = Number(window.localStorage && window.localStorage.getItem(REVISION_MANIFEST_PRUNE_LOCAL_KEY) || 0); } catch (err) {}
             due = !last || (now() - last >= days * 24 * 60 * 60 * 1000);
            if (due && window.EntryCache && typeof window.EntryCache.prune === 'function') {
                window.EntryCache.prune(days);
            }
         }
         }


         function countdown() {
         function savePacks() {
             var days = current && Number(current.pruneDays || current.clientPruneDays) || 7;
             ensurePacks();
             var last = 0;
             writeLocalJson(ENTRY_CACHE_PACK_KEY, packs);
            var next;
            var remain;
            try { last = Number(window.localStorage && window.localStorage.getItem(REVISION_MANIFEST_PRUNE_LOCAL_KEY) || 0); } catch (err) {}
            if (!last) return { days: days, lastClientPruneAt: null, nextClientPruneAt: null, remainingMs: 0, remainingDays: 0, due: true };
            next = last + days * 24 * 60 * 60 * 1000;
            remain = Math.max(0, next - now());
            return {
                days: days,
                lastClientPruneAt: last,
                nextClientPruneAt: next,
                remainingMs: remain,
                remainingDays: Math.ceil(remain / (24 * 60 * 60 * 1000)),
                due: remain <= 0
            };
         }
         }


         function status() {
         function requestForKey(key) {
             return {
             return new Request(ENTRY_CACHE_REQUEST_PREFIX + encodeURIComponent(String(key || '')), { credentials: 'same-origin' });
                available: !!current,
                manifestVersion: current && (current.manifestVersion || current.version || ''),
                generatedAt: current && current.generatedAt || '',
                changedResources: changedResources.slice(0, 50),
                changedCount: changedResources.length,
                prune: countdown(),
                cache: window.EntryCache && typeof window.EntryCache.info === 'function' ? window.EntryCache.info() : null
            };
         }
         }


         return {
         function openCache() {
             load: load,
             if (!hasCacheStorage()) return Promise.resolve(null);
             ensureLoaded: ensureLoaded,
             return window.caches.open(ENTRY_CACHE_NAME).catch(function () { return null; });
            current: function () { return current; },
         }
            resourceForRef: resourceForRef,
            tokenForRef: tokenForRef,
            cacheKeyForRef: cacheKeyForRef,
            cacheKeyForUrl: cacheKeyForUrl,
            manifestToken: manifestToken,
            resourceKeyForRef: resourceKeyForRef,
            addRevisionParam: addRevisionParam,
            maybePrune: maybePrune,
            countdown: countdown,
            status: status
         };
    }


    window.RevisionManifest = window.RevisionManifest || createRevisionManifestService();
        function cacheEntry(key, meta) {
 
            key = String(key || '');
    function createEntryStore() {
            ensureIndex();
        var jsonCache = {};
            index.entries[key] = Object.assign({}, index.entries[key] || {}, {
        var flagUrlCache = {};
                key: key,
        var fileUrlCache = {};
                resourceKey: String(meta && meta.resourceKey || ''),
        var imageReadyCache = {};
                token: String(meta && meta.token || ''),
        var imageObjectCache = {};
                kind: String(meta && meta.kind || 'raw'),
        var imageDisplayUrlCache = {};
                contentType: String(meta && meta.contentType || ''),
        var imagePromiseCache = {};
                size: Number(meta && meta.size || 0) || null,
        var rawPromiseCache = {};
                createdAt: index.entries[key] && index.entries[key].createdAt ? index.entries[key].createdAt : now(),
        var textCache = {};
                updatedAt: now(),
         var textPromiseCache = {};
                lastHitAt: index.entries[key] && index.entries[key].lastHitAt ? index.entries[key].lastHitAt : null,
                hits: Number(index.entries[key] && index.entries[key].hits || 0)
            });
            saveIndex();
         }


         function fetchJsonRef(ref, options) {
         function markHit(key, field) {
             var key = normalizeRefKey(ref);
             key = String(key || '');
            var cacheKey;
             ensureIndex();
            var promiseKey;
             if (index.entries[key]) {
            var url;
                index.entries[key].hits = Number(index.entries[key].hits || 0) + 1;
            var resourceKey;
                index.entries[key].lastHitAt = now();
            var token;
                 saveIndex();
            if (!key) return Promise.reject(new Error('empty json ref'));
             cacheKey = window.RevisionManifest && typeof window.RevisionManifest.cacheKeyForRef === 'function' ? window.RevisionManifest.cacheKeyForRef(ref, 'json') : '';
             promiseKey = cacheKey ? (key + '@' + cacheKey) : key;
            if (jsonCache[promiseKey]) return Promise.resolve(jsonCache[promiseKey].data);
            if (rawPromiseCache[promiseKey]) return rawPromiseCache[promiseKey];
            url = rawUrlForRef(ref, 'application/json');
            if (window.RevisionManifest && typeof window.RevisionManifest.addRevisionParam === 'function') {
                 url = window.RevisionManifest.addRevisionParam(url, ref);
             }
             }
             resourceKey = window.RevisionManifest && typeof window.RevisionManifest.resourceKeyForRef === 'function' ? window.RevisionManifest.resourceKeyForRef(ref) : key;
             if (field && stats[field] != null) stats[field] += 1;
             token = window.RevisionManifest && typeof window.RevisionManifest.tokenForRef === 'function' ? window.RevisionManifest.tokenForRef(ref) : '';
        }
             if (cacheKey && !(options && options.noStore) && window.EntryCache && typeof window.EntryCache.getText === 'function') {
 
                rawPromiseCache[promiseKey] = window.EntryCache.getText(cacheKey).then(function (cachedText) {
        function getResponse(key) {
                    if (cachedText !== null && cachedText !== undefined) {
             key = String(key || '');
                        var cachedData = cachedText && cachedText.trim() ? JSON.parse(cachedText) : {};
             if (!key || !hasCacheStorage()) return Promise.resolve(null);
                        jsonCache[promiseKey] = { key: promiseKey, ref: ref, url: url, data: cachedData, loadedAt: now(), persistent: true };
            return openCache().then(function (cache) {
                         jsonCache[key] = jsonCache[promiseKey];
                if (!cache) return null;
                         return cachedData;
                return cache.match(requestForKey(key)).then(function (res) {
                    if (!res) {
                         stats.misses += 1;
                         return null;
                     }
                     }
                     return fetch(url, {
                     return res;
                        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 data = text && text.trim() ? JSON.parse(text) : {};
                        jsonCache[promiseKey] = { key: promiseKey, ref: ref, url: url, data: data, loadedAt: now() };
                        jsonCache[key] = jsonCache[promiseKey];
                        window.EntryCache.putText(cacheKey, text, { resourceKey: resourceKey, token: token });
                        return data;
                    });
                 });
                 });
                return rawPromiseCache[promiseKey];
             }).catch(function () { return null; });
            }
            rawPromiseCache[promiseKey] = fetch(url, {
                credentials: 'same-origin',
                cache: options && options.noStore ? 'no-store' : 'force-cache'
            }).then(function (res) {
                if (!res.ok) throw new Error('HTTP ' + res.status);
                return res.text();
             }).then(function (text) {
                var data = text && text.trim() ? JSON.parse(text) : {};
                jsonCache[promiseKey] = { key: promiseKey, ref: ref, url: url, data: data, loadedAt: now() };
                jsonCache[key] = jsonCache[promiseKey];
                return data;
            });
            return rawPromiseCache[promiseKey];
         }
         }


         function getJsonSync(ref) {
         function putResponse(key, response, meta) {
             var key = normalizeRefKey(ref);
             key = String(key || '');
             return key && jsonCache[key] ? jsonCache[key].data : null;
            if (!key || !response || !hasCacheStorage()) return Promise.resolve(false);
             meta = meta || {};
            return openCache().then(function (cache) {
                var cloned;
                var headers;
                var bodyPromise;
                if (!cache) return false;
                cloned = response.clone();
                bodyPromise = cloned.blob().catch(function () { return null; });
                return bodyPromise.then(function (blob) {
                    if (!blob) return false;
                    headers = new Headers(response.headers || {});
                    if (!headers.get('Content-Type')) headers.set('Content-Type', meta.contentType || blob.type || 'application/octet-stream');
                    headers.set('X-Entry-Cache-Key', key);
                    headers.set('X-Entry-Resource-Key', String(meta.resourceKey || ''));
                    headers.set('X-Entry-Revision-Token', String(meta.token || ''));
                    headers.set('X-Entry-Cached-At', String(now()));
                    return cache.put(requestForKey(key), new Response(blob, { status: 200, headers: headers })).then(function () {
                        stats.stores += 1;
                        cacheEntry(key, {
                            resourceKey: meta.resourceKey,
                            token: meta.token,
                            kind: meta.kind || 'blob',
                            contentType: headers.get('Content-Type') || blob.type || '',
                            size: blob.size
                        });
                        return true;
                    });
                });
            }).catch(function () { return false; });
         }
         }


         function setJsonRef(ref, data) {
         function getText(key) {
             var key = normalizeRefKey(ref);
             return getResponse(key).then(function (res) {
            if (!key) return;
                if (!res) return null;
            jsonCache[key] = { key: key, ref: ref, url: rawUrlForRef(ref, 'application/json'), data: data, loadedAt: now() };
                markHit(key, 'textHits');
                return res.text();
            }).catch(function () { return null; });
         }
         }


         function normalizeFile(value) {
         function putText(key, text, meta) {
             return String(value || '')
            key = String(key || '');
                 .replace(/^(?:file|파일):/i, '')
            if (!key || !hasCacheStorage()) return Promise.resolve(false);
                 .trim();
            meta = meta || {};
             return openCache().then(function (cache) {
                var headers;
                var body = String(text || '');
                 if (!cache) return false;
                headers = new Headers({
                    'Content-Type': meta.contentType || 'text/plain; charset=UTF-8',
                    'X-Entry-Cache-Key': key,
                    'X-Entry-Resource-Key': String(meta.resourceKey || ''),
                    'X-Entry-Revision-Token': String(meta.token || ''),
                    'X-Entry-Cached-At': String(now())
                });
                return cache.put(requestForKey(key), new Response(body, { status: 200, headers: headers })).then(function () {
                    stats.stores += 1;
                    cacheEntry(key, {
                        resourceKey: meta.resourceKey,
                        token: meta.token,
                        kind: meta.kind || 'text',
                        contentType: headers.get('Content-Type'),
                        size: body.length
                    });
                    return true;
                 });
            }).catch(function () { return false; });
         }
         }


         function fileKey(value) {
         function getBlobUrl(key) {
             return normalizeFile(value).replace(/_/g, ' ').replace(/\s+/g, ' ').toLowerCase();
             key = String(key || '');
            if (!key) return Promise.resolve('');
            if (objectUrls[key]) {
                markHit(key, 'blobHits');
                return Promise.resolve(objectUrls[key]);
            }
            return getResponse(key).then(function (res) {
                if (!res) return '';
                return res.blob().then(function (blob) {
                    if (!blob || !blob.size) return '';
                    objectUrls[key] = URL.createObjectURL(blob);
                    markHit(key, 'blobHits');
                    return objectUrls[key];
                });
            }).catch(function () { return ''; });
         }
         }


         function setFlagUrl(file, url) {
         function fetchBlobUrl(url, key, meta, options) {
             var key = fileKey(file);
            url = String(url || '').trim();
             if (!key) return;
             key = String(key || '').trim();
             flagUrlCache[key] = String(url || '');
             if (!url || !key) return Promise.resolve('');
        }
             return getBlobUrl(key).then(function (cachedUrl) {
 
                if (cachedUrl) return cachedUrl;
        function getFlagUrl(file) {
                return fetch(url, {
            var key = fileKey(file);
                    credentials: 'same-origin',
            return key ? (flagUrlCache[key] || '') : '';
                    cache: options && options.noStore ? 'no-store' : 'force-cache'
                }).then(function (res) {
                    if (!res.ok) throw new Error('HTTP ' + res.status);
                    stats.networkStores += 1;
                    return putResponse(key, res, Object.assign({}, meta || {}, { kind: meta && meta.kind ? meta.kind : 'blob' })).then(function () {
                        return getBlobUrl(key);
                    });
                }).catch(function () {
                    return '';
                });
            });
         }
         }


         function resolveFlagUrls(files) {
         function deleteKey(key) {
             var clean = unique((files || []).map(normalizeFile).filter(Boolean));
             key = String(key || '');
 
            if (!key || !hasCacheStorage()) return Promise.resolve(false);
             function cacheKeyForFile(file) {
             if (objectUrls[key]) {
                 return window.RevisionManifest && typeof window.RevisionManifest.cacheKeyForRef === 'function' ? window.RevisionManifest.cacheKeyForRef('File:' + file, 'flagurl') : '';
                 try { URL.revokeObjectURL(objectUrls[key]); } catch (err) {}
                delete objectUrls[key];
             }
             }
 
             return openCache().then(function (cache) {
             function resourceKeyForFile(file) {
                 if (!cache) return false;
                return window.RevisionManifest && typeof window.RevisionManifest.resourceKeyForRef === 'function' ? window.RevisionManifest.resourceKeyForRef('File:' + file) : ('file:' + fileKey(file));
                 return cache.delete(requestForKey(key)).then(function (ok) {
            }
                    ensureIndex();
 
                     if (index.entries && index.entries[key]) {
            function tokenForFile(file) {
                         delete index.entries[key];
                return window.RevisionManifest && typeof window.RevisionManifest.tokenForRef === 'function' ? window.RevisionManifest.tokenForRef('File:' + file) : '';
                        stats.deletes += 1;
            }
                         saveIndex();
 
            function hydrateCachedFlagUrl(file) {
                var key = fileKey(file);
                var cacheKey = cacheKeyForFile(file);
                 if (!key || flagUrlCache[key] !== undefined || !cacheKey || !window.EntryCache || typeof window.EntryCache.getText !== 'function') {
                    return Promise.resolve(false);
                }
                 return window.EntryCache.getText(cacheKey).then(function (cachedUrl) {
                     if (cachedUrl !== null && cachedUrl !== undefined) {
                         flagUrlCache[key] = String(cachedUrl || '');
                         return true;
                     }
                     }
                     return false;
                     return ok;
                }).catch(function () { return false; });
            }
 
            return Promise.all(clean.map(hydrateCachedFlagUrl)).then(function () {
                var pending = clean.filter(function (file) {
                    return getFlagUrl(file) === '' && flagUrlCache[fileKey(file)] === undefined;
                 });
                 });
                var chunks = [];
            }).catch(function () { return false; });
        }


                 while (pending.length) chunks.push(pending.splice(0, 20));
        function invalidateResources(resourceKeys) {
                 if (!chunks.length) return flagUrlCache;
            var map = {};
            var entries;
            var keys = [];
            ensureIndex();
            entries = index.entries || {};
            (resourceKeys || []).forEach(function (resourceKey) {
                resourceKey = String(resourceKey || '').toLowerCase();
                if (resourceKey) map[resourceKey] = true;
            });
            Object.keys(entries).forEach(function (cacheKey) {
                var resourceKey = String(entries[cacheKey] && entries[cacheKey].resourceKey || '').toLowerCase();
                 if (map[resourceKey]) keys.push(cacheKey);
            });
            return Promise.all(keys.map(deleteKey)).then(function () { return keys.length; });
        }
 
        function prune(maxAgeDays) {
            var days = Math.max(1, Number(maxAgeDays) || 7);
            var cutoff = now() - days * 24 * 60 * 60 * 1000;
            var entries;
            var keys;
            ensureIndex();
            entries = index.entries || {};
            keys = Object.keys(entries).filter(function (key) {
                return Number(entries[key] && entries[key].createdAt || 0) < cutoff;
            });
            window.localStorage && window.localStorage.setItem(REVISION_MANIFEST_PRUNE_LOCAL_KEY, String(now()));
            return Promise.all(keys.map(deleteKey)).then(function () { return keys.length; });
        }
 
        function reset() {
            Object.keys(objectUrls).forEach(function (key) {
                 try { URL.revokeObjectURL(objectUrls[key]); } catch (err) {}
            });
            objectUrls = {};
            index = { entries: {} };
            packs = { packs: {} };
            saveIndex();
            savePacks();
            if (!hasCacheStorage()) return Promise.resolve(false);
            return window.caches.delete(ENTRY_CACHE_NAME).catch(function () { return false; });
        }


                return Promise.all(chunks.map(function (chunk) {
        function packReady(key, token) {
                    var titleToKey = {};
            key = String(key || '');
                    var keyToFile = {};
            token = String(token || '');
                    var titles = [];
            ensurePacks();
                    chunk.forEach(function (file) {
            if (!key || !token) return false;
                        var key = fileKey(file);
            return !!(packs.packs[key] && String(packs.packs[key].token || '') === token && packs.packs[key].ready);
                        if (!key) return;
        }
                        titleToKey[fileKey('File:' + file)] = key;
 
                        titleToKey[fileKey('파일:' + file)] = key;
        function setPackReady(key, token, meta) {
                        keyToFile[key] = file;
            key = String(key || '');
                        titles.push('File:' + file);
            token = String(token || '');
                        titles.push('파일:' + file);
            if (!key || !token) return false;
                    });
            ensurePacks();
                    return fetchApi({
            packs.packs[key] = Object.assign({}, meta || {}, {
                        action: 'query',
                key: key,
                        format: 'json',
                token: token,
                        formatversion: '2',
                ready: true,
                        redirects: '1',
                updatedAt: now()
                        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 key = titleToKey[fileKey(page && page.title)] || fileKey(page && page.title);
                            var file = keyToFile[key] || (page && page.title || '').replace(/^(?:File|파일):/i, '');
                            var imageinfo = page && page.imageinfo && page.imageinfo[0];
                            var url;
                            var rev;
                            var sep;
                            var cacheKey;
                            if (!key) return;
                            seen[key] = true;
                            url = imageinfo && (imageinfo.thumburl || imageinfo.url) ? (imageinfo.thumburl || imageinfo.url) : '';
                            if (url) {
                                rev = imageinfo && (imageinfo.sha1 || imageinfo.timestamp || imageinfo.size) ? (imageinfo.sha1 || imageinfo.timestamp || imageinfo.size) : '';
                                if (rev && !/[?&]_entryFileRev=/.test(url)) {
                                    sep = url.indexOf('?') === -1 ? '?' : '&';
                                    url += sep + '_entryFileRev=' + encodeURIComponent(String(rev));
                                }
                            }
                            flagUrlCache[key] = url;
                            cacheKey = cacheKeyForFile(file);
                            if (cacheKey && window.EntryCache && typeof window.EntryCache.putText === 'function') {
                                window.EntryCache.putText(cacheKey, url || '', {
                                    resourceKey: resourceKeyForFile(file),
                                    token: tokenForFile(file),
                                    kind: 'flagurl',
                                    contentType: 'text/plain; charset=UTF-8'
                                });
                            }
                        });
                        chunk.forEach(function (file) {
                            var key = fileKey(file);
                            if (key && !seen[key] && flagUrlCache[key] === undefined) flagUrlCache[key] = '';
                        });
                    }).catch(function () {
                        chunk.forEach(function (file) {
                            var key = fileKey(file);
                            if (key && flagUrlCache[key] === undefined) flagUrlCache[key] = '';
                        });
                    });
                })).then(function () { return flagUrlCache; });
             });
             });
            savePacks();
            return true;
         }
         }


         function normalizeGenericFileTitle(value) {
         function packInfo() {
             var text = String(value || '').trim();
             ensurePacks();
            var match;
            return Object.assign({}, packs.packs || {});
            var i;
        }


            if (!text) return '';
        function info() {
             for (i = 0; i < 4; i += 1) {
             var lastClientPrune = 0;
                try {
            var entries;
                    if (/%[0-9a-f]{2}/i.test(text)) text = decodeURIComponent(text);
            var byKind = {};
                } catch (err) {
            try { lastClientPrune = Number(window.localStorage && window.localStorage.getItem(REVISION_MANIFEST_PRUNE_LOCAL_KEY) || 0); } catch (err) {}
                    break;
             ensureIndex();
                }
             ensurePacks();
             }
             entries = index.entries || {};
             match = text.match(/[?&]title=([^&#]+)/i);
             Object.keys(entries).forEach(function (key) {
             if (match) text = match[1];
                var kind = String(entries[key] && entries[key].kind || 'raw');
             text = text.replace(/^https?:\/\/[^/]+/i, '').replace(/^\/+/, '').replace(/^index\.php\/?/i, '').replace(/^wiki\/?/i, '').trim();
                byKind[kind] = (byKind[kind] || 0) + 1;
            match = text.match(/(?:^|\/)(?:(?:Special|특수)\s*[:/]\s*)?(?:Redirect|넘겨주기)\s*\/\s*file\s*\/(.+)$/i);
            });
             if (match) text = match[1];
             return {
            text = text.replace(/^(?:File|파일|Image|이미지)\s*:/i, '').replace(/^:+/, '').trim();
                supported: hasCacheStorage(),
             return text;
                name: ENTRY_CACHE_NAME,
                entries: Object.keys(entries).length,
                byKind: byKind,
                packs: Object.keys(packs.packs || {}).length,
                lastClientPruneAt: lastClientPrune || null,
                stats: Object.assign({}, stats)
             };
         }
         }


         function isFileRef(value) {
         /* Trim legacy image/blob metadata rows produced by older builds. */
             var text = String(value || '').trim();
        try { saveIndex(); } catch (err) {}
             return /^(?:file|파일)\s*:/i.test(text) || /(?:^|\/)(?:(?:Special|특수)\s*[:/]\s*)?(?:Redirect|넘겨주기)\s*\/\s*file\s*\//i.test(text);
 
        }
        return {
             getResponse: getResponse,
            putResponse: putResponse,
            getText: getText,
            putText: putText,
            getBlobUrl: getBlobUrl,
            fetchBlobUrl: fetchBlobUrl,
             invalidateResources: invalidateResources,
            prune: prune,
            reset: reset,
            packReady: packReady,
            setPackReady: setPackReady,
            packInfo: packInfo,
            info: info
        };
    }


        function fileUrlKey(value) {
    window.EntryCache = window.EntryCache || createEntryCache();
            return normalizeGenericFileTitle(value).replace(/_/g, ' ').replace(/\s+/g, ' ').toLowerCase();
        }


        function stableDirectImageUrl(url) {
    function createRevisionManifestService() {
            var text = String(url || '').trim();
        var current = null;
            var separator;
        var previous = readLocalJson(REVISION_MANIFEST_LOCAL_KEY, null);
            var bust;
        var loadPromise = null;
            if (!text) return '';
        var changedResources = [];
             if (/[?&]_entryAsset=/.test(text) || /[?&]_=/.test(text)) return text;
 
            bust = String(BUILD_ID || 'entry');
        function unwrap(payload) {
             separator = text.indexOf('?') === -1 ? '?' : '&';
             return payload && (payload.entryrevisionmanifest || payload.revisionManifest || payload) || null;
            return text + separator + '_entryAsset=' + encodeURIComponent(bust);
        }
 
        function resourcesOf(manifest) {
             return manifest && manifest.resources && typeof manifest.resources === 'object' ? manifest.resources : {};
         }
         }


         function resolveFileUrl(ref) {
         function compactRevisionManifest(manifest) {
             var original = String(ref || '').trim();
             var out;
            var file;
             var resources;
            var key;
             if (!manifest || typeof manifest !== 'object') return null;
            var cacheKey;
             out = {
             var resourceKey;
                manifestVersion: manifest.manifestVersion || manifest.version || '',
            var token;
                version: manifest.version || manifest.manifestVersion || '',
             if (!original) return Promise.resolve('');
                generatedAt: manifest.generatedAt || '',
            if (!isFileRef(original)) return Promise.resolve(stableDirectImageUrl(original));
                pruneDays: manifest.pruneDays,
             file = normalizeGenericFileTitle(original);
                clientPruneDays: manifest.clientPruneDays,
            key = fileUrlKey(file);
                resources: {}
            if (!file || !key) return Promise.resolve(stableDirectImageUrl(original));
            };
            if (fileUrlCache[key]) return Promise.resolve(fileUrlCache[key]);
             resources = resourcesOf(manifest);
            cacheKey = window.RevisionManifest && typeof window.RevisionManifest.cacheKeyForRef === 'function' ? window.RevisionManifest.cacheKeyForRef('File:' + file, 'fileurl') : '';
             Object.keys(resources).forEach(function (title) {
            resourceKey = window.RevisionManifest && typeof window.RevisionManifest.resourceKeyForRef === 'function' ? window.RevisionManifest.resourceKeyForRef('File:' + file) : ('file:' + key);
                var src = resources[title];
             token = window.RevisionManifest && typeof window.RevisionManifest.tokenForRef === 'function' ? window.RevisionManifest.tokenForRef('File:' + file) : '';
                var dst;
             if (cacheKey && window.EntryCache && typeof window.EntryCache.getText === 'function') {
                if (!src || typeof src !== 'object') return;
                return window.EntryCache.getText(cacheKey).then(function (cachedUrl) {
                dst = {};
                    if (cachedUrl !== null && cachedUrl !== undefined) {
                ['revision', 'sha1', 'hash', 'timestamp', 'updatedAt', 'url'].forEach(function (key) {
                        fileUrlCache[key] = String(cachedUrl || '');
                    if (src[key] !== undefined && src[key] !== null && String(src[key]) !== '') dst[key] = src[key];
                        return fileUrlCache[key];
                });
                    }
                 out.resources[title] = dst;
                    return fetchApi({
            });
                        action: 'query',
             return out;
                        format: 'json',
        }
                        formatversion: '2',
 
                        redirects: '1',
        function buildLookup(manifest) {
                        prop: 'imageinfo',
            var lookup = {};
                        iiprop: 'url|sha1|timestamp|size|mime',
            Object.keys(resourcesOf(manifest)).forEach(function (title) {
                        titles: 'File:' + file
                 lookup[normalizeManifestTitle(title).toLowerCase()] = resourcesOf(manifest)[title];
                    }).then(function (json) {
                        var pages = (json && json.query && json.query.pages) || [];
                        var info = pages[0] && pages[0].imageinfo && pages[0].imageinfo[0] ? pages[0].imageinfo[0] : null;
                        var url = info && info.url ? info.url : original;
                        var rev = info && (info.sha1 || info.timestamp || info.size) ? (info.sha1 || info.timestamp || info.size) : String(BUILD_ID || 'entry-file');
                        var separator = url.indexOf('?') === -1 ? '?' : '&';
                        var resolved = url + separator + '_entryFileRev=' + encodeURIComponent(String(rev));
                        fileUrlCache[key] = resolved;
                        window.EntryCache.putText(cacheKey, resolved, { resourceKey: resourceKey, token: token, kind: 'fileurl', contentType: 'text/plain; charset=UTF-8' });
                        return resolved;
                    });
                 }).catch(function () {
                    return stableDirectImageUrl(original);
                });
            }
             return fetchApi({
                action: 'query',
                format: 'json',
                formatversion: '2',
                redirects: '1',
                prop: 'imageinfo',
                iiprop: 'url|sha1|timestamp|size|mime',
                titles: 'File:' + file
            }).then(function (json) {
                var pages = (json && json.query && json.query.pages) || [];
                var info = pages[0] && pages[0].imageinfo && pages[0].imageinfo[0] ? pages[0].imageinfo[0] : null;
                var url = info && info.url ? info.url : original;
                var rev = info && (info.sha1 || info.timestamp || info.size) ? (info.sha1 || info.timestamp || info.size) : String(BUILD_ID || 'entry-file');
                var separator = url.indexOf('?') === -1 ? '?' : '&';
                var resolved = url + separator + '_entryFileRev=' + encodeURIComponent(String(rev));
                 fileUrlCache[key] = resolved;
                return resolved;
            }).catch(function () {
                return stableDirectImageUrl(original);
             });
             });
            return lookup;
         }
         }


         function preloadImageUrl(url, options) {
         function computeChanged(prev, next) {
             var key = String(url || '').trim();
             var prevLookup = buildLookup(prev);
             var cacheKey;
             var nextLookup = buildLookup(next);
             var sourcePromise;
             var out = [];
             var persistent = !(options && options.persistent === false);
             Object.keys(nextLookup).forEach(function (key) {
            if (!key) return Promise.resolve(false);
                if (resourceToken(prevLookup[key]) !== resourceToken(nextLookup[key])) out.push(key);
             if (imageReadyCache[key]) return Promise.resolve(true);
             });
             if (imagePromiseCache[key]) return imagePromiseCache[key];
            Object.keys(prevLookup).forEach(function (key) {
                if (!nextLookup[key]) out.push(key);
             });
            return unique(out);
        }


            cacheKey = persistent && window.RevisionManifest && typeof window.RevisionManifest.cacheKeyForUrl === 'function' ? window.RevisionManifest.cacheKeyForUrl(key, 'image') : '';
        function load(options) {
            sourcePromise = Promise.resolve('');
             if (loadPromise && !(options && options.force)) return loadPromise;
             if (cacheKey && window.EntryCache && typeof window.EntryCache.fetchBlobUrl === 'function') {
            loadPromise = fetchApi({
                sourcePromise = window.EntryCache.fetchBlobUrl(key, cacheKey, {
                action: REVISION_MANIFEST_ACTION,
                    resourceKey: '',
                format: 'json',
                    token: '',
                formatversion: '2'
                    kind: 'image'
            }).then(function (payload) {
                }).catch(function () { return ''; });
                 var manifest = unwrap(payload);
            }
                if (!manifest || !manifest.resources) throw new Error('invalid revision manifest');
 
                current = compactRevisionManifest(manifest) || manifest;
            imagePromiseCache[key] = sourcePromise.then(function (cachedObjectUrl) {
                changedResources = computeChanged(previous, current);
                 return new Promise(function (resolve) {
                if (changedResources.length && window.EntryCache && typeof window.EntryCache.invalidateResources === 'function') {
                    var img = new Image();
                    window.EntryCache.invalidateResources(changedResources);
                    var settled = false;
                }
                    var src = cachedObjectUrl || key;
                writeLocalJson(REVISION_MANIFEST_LOCAL_KEY, current);
                    function finish(ok) {
                previous = current;
                        if (settled) return;
                maybePrune();
                        settled = true;
                return current;
                        if (ok) {
            }).catch(function () {
                            imageReadyCache[key] = true;
                current = previous || null;
                            imageReadyCache[src] = true;
                 return current;
                            imageObjectCache[key] = img;
                            imageObjectCache[src] = img;
                            imageDisplayUrlCache[key] = src;
                            imageDisplayUrlCache[src] = src;
                        }
                        resolve(!!ok);
                    }
                    img.onload = function () {
                        if (img.decode) {
                            img.decode().then(function () { finish(true); }).catch(function () { finish(true); });
                        } else {
                            finish(true);
                        }
                    };
                    img.onerror = function () {
                        if (src !== key) {
                            src = key;
                            img.src = key;
                            return;
                        }
                        finish(false);
                    };
                    img.decoding = 'async';
                    img.loading = 'eager';
                    img.src = src;
                    if (img.complete && img.naturalWidth) {
                        if (img.decode) img.decode().then(function () { finish(true); }).catch(function () { finish(true); });
                        else finish(true);
                    }
                 });
             });
             });
             return imagePromiseCache[key];
             return loadPromise;
         }
         }


         function preloadImages(urls, options) {
         function ensureLoaded() {
             var list = unique((urls || []).filter(Boolean));
             return current ? Promise.resolve(current) : load();
            var limit = options && Number(options.limit);
        }
             var concurrency = Math.max(1, Math.min(48, Number(options && options.concurrency) || 24));
 
             var index = 0;
        function resourceForRef(ref) {
            var ok = 0;
             var title = normalizeManifestTitle(ref);
            var fail = 0;
             var lookup;
             if (Number.isFinite(limit) && limit > 0) list = list.slice(0, limit);
             if (!title || !current) return null;
             if (!list.length) return Promise.resolve({ total: 0, ok: 0, fail: 0 });
             lookup = buildLookup(current);
             return new Promise(function (resolve) {
             return lookup[title.toLowerCase()] || null;
                function pump() {
                    while (index < list.length && concurrency > 0) {
                        (function (url) {
                            concurrency -= 1;
                            preloadImageUrl(url, options || {}).then(function (result) {
                                if (result) ok += 1;
                                else fail += 1;
                            }).catch(function () {
                                fail += 1;
                            }).then(function () {
                                concurrency += 1;
                                if (index >= list.length && ok + fail >= list.length) resolve({ total: list.length, ok: ok, fail: fail });
                                else pump();
                            });
                        })(list[index++]);
                    }
                }
                pump();
            });
         }
         }


         function isImageReady(url) {
         function tokenForRef(ref) {
             var key = String(url || '').trim();
             return resourceToken(resourceForRef(ref));
            return !!(key && imageReadyCache[key]);
         }
         }


         function getImageElement(url) {
         function resourceKeyForRef(ref) {
             var key = String(url || '').trim();
             var title = normalizeManifestTitle(ref);
             return key ? (imageObjectCache[key] || null) : null;
             return title ? title.toLowerCase() : '';
         }
         }


         function getImageDisplayUrl(url) {
         function cacheKeyForRef(ref, type) {
             var key = String(url || '').trim();
             var resourceKey = resourceKeyForRef(ref);
             return key ? (imageDisplayUrlCache[key] || key) : '';
            var token = tokenForRef(ref);
            if (!resourceKey || !token) return '';
             return String(type || 'raw') + ':' + resourceKey + '@' + token;
         }
         }


         function normalizeUrlKey(url) {
         function manifestToken(extra) {
             var text = String(url || '').trim();
             var base = current && (current.manifestVersion || current.version || current.generatedAt || '') || '';
            var a;
             return String(base || 'no-manifest') + (extra ? (':' + String(extra)) : '');
            if (!text) return '';
            try {
                a = document.createElement('a');
                a.href = text;
                text = a.pathname + (a.search || '');
             } catch (err) {}
            try {
                text = decodeURI(text);
            } catch (err2) {}
            text = text.replace(/([?&])_=[^&]*/g, '$1').replace(/[?&]$/, '');
            text = text.replace(/_/g, '_');
            return text;
         }
         }


         function fetchTextUrl(url, options) {
         function tokenFromUrl(url) {
             var key = normalizeUrlKey(url);
             var text = String(url || '');
             var cacheKey;
             var match = text.match(/[?&](?:_entryFileRev|_entryAsset|_entryRev)=([^&]+)/);
            var resourceRef;
             if (!match) return '';
            var resourceKey;
            try { return decodeURIComponent(match[1]); } catch (err) { return match[1]; }
            var token;
            if (!key) return Promise.reject(new Error('empty text url'));
            if (textCache[key]) return Promise.resolve(textCache[key].text);
            if (textPromiseCache[key]) return textPromiseCache[key];
            resourceRef = options && options.resourceRef ? options.resourceRef : (options && options.ref ? options.ref : '');
            cacheKey = resourceRef && window.RevisionManifest && typeof window.RevisionManifest.cacheKeyForRef === 'function' ? window.RevisionManifest.cacheKeyForRef(resourceRef, 'text') : '';
            if (!cacheKey && window.RevisionManifest && typeof window.RevisionManifest.cacheKeyForUrl === 'function') cacheKey = window.RevisionManifest.cacheKeyForUrl(url, 'text');
            resourceKey = resourceRef && window.RevisionManifest && typeof window.RevisionManifest.resourceKeyForRef === 'function' ? window.RevisionManifest.resourceKeyForRef(resourceRef) : ('url:' + key);
            token = resourceRef && window.RevisionManifest && typeof window.RevisionManifest.tokenForRef === 'function' ? window.RevisionManifest.tokenForRef(resourceRef) : '';
             if (cacheKey && !(options && options.noStore) && window.EntryCache && typeof window.EntryCache.getText === 'function') {
                textPromiseCache[key] = window.EntryCache.getText(cacheKey).then(function (cachedText) {
                    if (cachedText !== null && cachedText !== undefined) {
                        textCache[key] = { key: key, url: url, text: cachedText, loadedAt: now(), persistent: true };
                        return cachedText;
                    }
                    return fetch(url, {
                        credentials: 'same-origin',
                        cache: 'force-cache'
                    }).then(function (res) {
                        if (!res.ok) throw new Error('HTTP ' + res.status);
                        return res.text();
                    }).then(function (text) {
                        textCache[key] = { key: key, url: url, text: text, loadedAt: now() };
                        window.EntryCache.putText(cacheKey, text, { resourceKey: resourceKey, token: token, kind: 'text', contentType: 'text/html; charset=UTF-8' });
                        return text;
                    });
                });
                return textPromiseCache[key];
            }
            textPromiseCache[key] = fetch(url, {
                credentials: 'same-origin',
                cache: options && options.noStore ? 'no-store' : 'force-cache'
            }).then(function (res) {
                if (!res.ok) throw new Error('HTTP ' + res.status);
                return res.text();
            }).then(function (text) {
                textCache[key] = { key: key, url: url, text: text, loadedAt: now() };
                return text;
            });
            return textPromiseCache[key];
         }
         }


         function getTextSync(url) {
         function cacheKeyForUrl(url, type) {
             var key = normalizeUrlKey(url);
             var text = String(url || '').trim();
             return key && textCache[key] ? textCache[key].text : '';
            var token = tokenFromUrl(text) || manifestToken('url');
         }
             var normalized;
 
            if (!text) return '';
         function setTextUrl(url, text) {
            try {
             var key = normalizeUrlKey(url);
                normalized = new URL(text, window.location.href);
             if (!key) return;
                text = normalized.pathname + (normalized.search || '');
             textCache[key] = { key: key, url: url, text: String(text || ''), loadedAt: now() };
            } catch (err) {}
            text = text.replace(/[?&](?:_entryFileRev|_entryAsset|_entryRev)=[^&]*/g, '').replace(/[?&]$/, '');
            try { text = decodeURI(text); } catch (err2) {}
            return String(type || 'blob') + ':url:' + text.toLowerCase() + '@' + token;
         }
 
         function addRevisionParam(url, ref) {
             var token = tokenForRef(ref);
            var text = String(url || '').trim();
            var sep;
             if (!text || !token || /[?&]_entryRev=/.test(text)) return text;
             sep = text.indexOf('?') === -1 ? '?' : '&';
            return text + sep + '_entryRev=' + encodeURIComponent(token);
        }
 
        function maybePrune() {
            var days = current && Number(current.pruneDays || current.clientPruneDays) || 7;
            var last = 0;
            var due;
            try { last = Number(window.localStorage && window.localStorage.getItem(REVISION_MANIFEST_PRUNE_LOCAL_KEY) || 0); } catch (err) {}
            due = !last || (now() - last >= days * 24 * 60 * 60 * 1000);
            if (due && window.EntryCache && typeof window.EntryCache.prune === 'function') {
                window.EntryCache.prune(days);
            }
        }
 
        function countdown() {
            var days = current && Number(current.pruneDays || current.clientPruneDays) || 7;
            var last = 0;
            var next;
            var remain;
            try { last = Number(window.localStorage && window.localStorage.getItem(REVISION_MANIFEST_PRUNE_LOCAL_KEY) || 0); } catch (err) {}
            if (!last) return { days: days, lastClientPruneAt: null, nextClientPruneAt: null, remainingMs: 0, remainingDays: 0, due: true };
            next = last + days * 24 * 60 * 60 * 1000;
            remain = Math.max(0, next - now());
            return {
                days: days,
                lastClientPruneAt: last,
                nextClientPruneAt: next,
                remainingMs: remain,
                remainingDays: Math.ceil(remain / (24 * 60 * 60 * 1000)),
                due: remain <= 0
            };
         }
         }


         function cacheInfo() {
         function status() {
             return {
             return {
                 json: Object.keys(jsonCache).length,
                 available: !!current,
                 jsonKeys: Object.keys(jsonCache),
                 manifestVersion: current && (current.manifestVersion || current.version || ''),
                 flags: Object.keys(flagUrlCache).length,
                 generatedAt: current && current.generatedAt || '',
                 files: Object.keys(fileUrlCache).length,
                 changedResources: changedResources.slice(0, 50),
                 images: Object.keys(imageReadyCache).length,
                 changedCount: changedResources.length,
                 retainedImages: Object.keys(imageObjectCache).length,
                 prune: countdown(),
                 displayImages: Object.keys(imageDisplayUrlCache).length,
                 cache: window.EntryCache && typeof window.EntryCache.info === 'function' ? window.EntryCache.info() : null
                text: Object.keys(textCache).length,
                textKeys: Object.keys(textCache)
             };
             };
         }
         }


         return {
         return {
             fetchJsonRef: fetchJsonRef,
             load: load,
             getJsonSync: getJsonSync,
             ensureLoaded: ensureLoaded,
             setJsonRef: setJsonRef,
             current: function () { return current; },
            normalizeRefKey: normalizeRefKey,
             resourceForRef: resourceForRef,
            rawUrlForRef: rawUrlForRef,
             tokenForRef: tokenForRef,
            revisionUrlForRef: function (ref, ctype) {
             cacheKeyForRef: cacheKeyForRef,
                var url = rawUrlForRef(ref, ctype);
             cacheKeyForUrl: cacheKeyForUrl,
                if (window.RevisionManifest && typeof window.RevisionManifest.addRevisionParam === 'function') {
             manifestToken: manifestToken,
                    url = window.RevisionManifest.addRevisionParam(url, ref);
             resourceKeyForRef: resourceKeyForRef,
                }
             addRevisionParam: addRevisionParam,
                return url;
             maybePrune: maybePrune,
            },
             countdown: countdown,
             resolveFlagUrls: resolveFlagUrls,
             status: status
             setFlagUrl: setFlagUrl,
             getFlagUrl: getFlagUrl,
             resolveFileUrl: resolveFileUrl,
             preloadImageUrl: preloadImageUrl,
             preloadImages: preloadImages,
             isImageReady: isImageReady,
            getImageElement: getImageElement,
            getImageDisplayUrl: getImageDisplayUrl,
            fetchTextUrl: fetchTextUrl,
            getTextSync: getTextSync,
             setTextUrl: setTextUrl,
             stableDirectImageUrl: stableDirectImageUrl,
             cacheInfo: cacheInfo
         };
         };
     }
     }


     window.EntryStore = window.EntryStore || createEntryStore();
     window.RevisionManifest = window.RevisionManifest || createRevisionManifestService();


     function updateBootProgress(done, total, label) {
     function createEntryStore() {
         var pct = total ? Math.max(0, Math.min(100, Math.round((done / total) * 100))) : 0;
         var jsonCache = {};
         if (bootProgressNode) bootProgressNode.textContent = pct + '%';
        var flagUrlCache = {};
         if (bootFillNode) bootFillNode.style.width = pct + '%';
        var fileUrlCache = {};
         if (bootDetailNode && label) bootDetailNode.textContent = label;
        var imageReadyCache = {};
    }
        var imageObjectCache = {};
        var imageDisplayUrlCache = {};
         var imagePromiseCache = {};
         var rawPromiseCache = {};
         var textCache = {};
        var textPromiseCache = {};


    function adoptBootScreen(node) {
        function fetchJsonRef(ref, options) {
        if (!node) return null;
            var key = normalizeRefKey(ref);
        bootNode = node;
            var cacheKey;
        bootStatusNode = bootNode.querySelector('.boot-gate-status');
            var promiseKey;
        bootProgressNode = bootNode.querySelector('.boot-gate-progress');
            var url;
        bootDetailNode = bootNode.querySelector('.boot-gate-detail');
            var resourceKey;
        bootFillNode = bootNode.querySelector('.boot-gate-meter-fill');
            var token;
        bootNode.classList.add('is-active');
            if (!key) return Promise.reject(new Error('empty json ref'));
        bootNode.classList.remove('is-complete');
            cacheKey = window.RevisionManifest && typeof window.RevisionManifest.cacheKeyForRef === 'function' ? window.RevisionManifest.cacheKeyForRef(ref, 'json') : '';
         return bootNode;
            promiseKey = cacheKey ? (key + '@' + cacheKey) : key;
    }
            if (jsonCache[promiseKey]) return Promise.resolve(jsonCache[promiseKey].data);
            if (rawPromiseCache[promiseKey]) return rawPromiseCache[promiseKey];
            url = rawUrlForRef(ref, 'application/json');
            if (window.RevisionManifest && typeof window.RevisionManifest.addRevisionParam === 'function') {
                url = window.RevisionManifest.addRevisionParam(url, ref);
            }
            resourceKey = window.RevisionManifest && typeof window.RevisionManifest.resourceKeyForRef === 'function' ? window.RevisionManifest.resourceKeyForRef(ref) : key;
            token = window.RevisionManifest && typeof window.RevisionManifest.tokenForRef === 'function' ? window.RevisionManifest.tokenForRef(ref) : '';
            if (cacheKey && !(options && options.noStore) && window.EntryCache && typeof window.EntryCache.getText === 'function') {
                rawPromiseCache[promiseKey] = window.EntryCache.getText(cacheKey).then(function (cachedText) {
                    if (cachedText !== null && cachedText !== undefined) {
                        var cachedData = cachedText && cachedText.trim() ? JSON.parse(cachedText) : {};
                        jsonCache[promiseKey] = { key: promiseKey, ref: ref, url: url, data: cachedData, loadedAt: now(), persistent: true };
                        jsonCache[key] = jsonCache[promiseKey];
                        return cachedData;
                    }
                    return fetch(url, {
                        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 data = text && text.trim() ? JSON.parse(text) : {};
                        jsonCache[promiseKey] = { key: promiseKey, ref: ref, url: url, data: data, loadedAt: now() };
                        jsonCache[key] = jsonCache[promiseKey];
                        window.EntryCache.putText(cacheKey, text, { resourceKey: resourceKey, token: token });
                        return data;
                    });
                });
                return rawPromiseCache[promiseKey];
            }
            rawPromiseCache[promiseKey] = fetch(url, {
                credentials: 'same-origin',
                cache: options && options.noStore ? 'no-store' : 'force-cache'
            }).then(function (res) {
                if (!res.ok) throw new Error('HTTP ' + res.status);
                return res.text();
            }).then(function (text) {
                var data = text && text.trim() ? JSON.parse(text) : {};
                jsonCache[promiseKey] = { key: promiseKey, ref: ref, url: url, data: data, loadedAt: now() };
                jsonCache[key] = jsonCache[promiseKey];
                return data;
            });
            return rawPromiseCache[promiseKey];
         }
 
        function getJsonSync(ref) {
            var key = normalizeRefKey(ref);
            return key && jsonCache[key] ? jsonCache[key].data : null;
        }


    function ensureBootScreen(options) {
        function setJsonRef(ref, data) {
        var panel;
            var key = normalizeRefKey(ref);
        var header;
            if (!key) return;
        var meter;
            jsonCache[key] = { key: key, ref: ref, url: rawUrlForRef(ref, 'application/json'), data: data, loadedAt: now() };
        var existing;
        }
        var decoLayer;
        var close;
        var previewMode;
        options = options || {};
        previewMode = !!options.preview;
        if (bootNode && bootNode.parentNode) return bootNode;
        if (!document.body) return null;


         if (BOOT_EXCLUDED_PAGE && !previewMode) return null;
         function normalizeFile(value) {
        if (!previewMode) activateBootSurface();
            return String(value || '')
                .replace(/^(?:file|파일):/i, '')
                .trim();
        }


         existing = document.getElementById('boot-gate-screen') || (!previewMode && window.__BootGatePrelude && window.__BootGatePrelude.ensure ? window.__BootGatePrelude.ensure() : null);
         function fileKey(value) {
         if (existing) return adoptBootScreen(existing);
            return normalizeFile(value).replace(/_/g, ' ').replace(/\s+/g, ' ').toLowerCase();
         }


         bootNode = document.createElement('div');
         function setFlagUrl(file, url) {
        bootNode.id = 'boot-gate-screen';
            var key = fileKey(file);
        bootNode.className = 'boot-gate-screen is-active';
            if (!key) return;
        bootNode.setAttribute('role', 'status');
            flagUrlCache[key] = String(url || '');
         bootNode.setAttribute('aria-live', 'polite');
         }


         panel = document.createElement('div');
         function getFlagUrl(file) {
         panel.className = 'boot-gate-panel';
            var key = fileKey(file);
            return key ? (flagUrlCache[key] || '') : '';
         }


         header = document.createElement('div');
         function resolveFlagUrls(files) {
        header.className = 'boot-gate-title';
            var clean = unique((files || []).map(normalizeFile).filter(Boolean));
        header.textContent = 'ARCHIVE INITIALIZATION';


        bootStatusNode = document.createElement('div');
            function cacheKeyForFile(file) {
        bootStatusNode.className = 'boot-gate-status';
                return window.RevisionManifest && typeof window.RevisionManifest.cacheKeyForRef === 'function' ? window.RevisionManifest.cacheKeyForRef('File:' + file, 'flagurl') : '';
        bootStatusNode.textContent = 'Preparing entry systems';
            }


        meter = document.createElement('div');
            function resourceKeyForFile(file) {
        meter.className = 'boot-gate-meter';
                return window.RevisionManifest && typeof window.RevisionManifest.resourceKeyForRef === 'function' ? window.RevisionManifest.resourceKeyForRef('File:' + file) : ('file:' + fileKey(file));
        bootFillNode = document.createElement('div');
            }
        bootFillNode.className = 'boot-gate-meter-fill';
        meter.appendChild(bootFillNode);


        bootProgressNode = document.createElement('div');
            function tokenForFile(file) {
        bootProgressNode.className = 'boot-gate-progress';
                return window.RevisionManifest && typeof window.RevisionManifest.tokenForRef === 'function' ? window.RevisionManifest.tokenForRef('File:' + file) : '';
        bootProgressNode.textContent = '0%';
            }


        bootDetailNode = document.createElement('div');
            function hydrateCachedFlagUrl(file) {
        bootDetailNode.className = 'boot-gate-detail';
                var key = fileKey(file);
        bootDetailNode.textContent = 'loading manifest';
                var cacheKey = cacheKeyForFile(file);
                if (!key || flagUrlCache[key] !== undefined || !cacheKey || !window.EntryCache || typeof window.EntryCache.getText !== 'function') {
                    return Promise.resolve(false);
                }
                return window.EntryCache.getText(cacheKey).then(function (cachedUrl) {
                    if (cachedUrl !== null && cachedUrl !== undefined) {
                        flagUrlCache[key] = String(cachedUrl || '');
                        return true;
                    }
                    return false;
                }).catch(function () { return false; });
            }


        close = document.createElement('button');
            return Promise.all(clean.map(hydrateCachedFlagUrl)).then(function () {
        close.type = 'button';
                var pending = clean.filter(function (file) {
        close.className = 'boot-gate-close';
                    return getFlagUrl(file) === '' && flagUrlCache[fileKey(file)] === undefined;
        close.setAttribute('aria-label', 'Close boot preview');
                });
        close.textContent = '×';
                var chunks = [];
        close.addEventListener('click', function () { hideBootScreen(); });


        decoLayer = document.createElement('div');
                while (pending.length) chunks.push(pending.splice(0, 20));
        decoLayer.className = 'boot-gate-decoration-layer';
                if (!chunks.length) return flagUrlCache;
        decoLayer.setAttribute('data-decoration-target', 'boot-gate');
        decoLayer.setAttribute('aria-hidden', 'true');


        panel.appendChild(header);
                return Promise.all(chunks.map(function (chunk) {
        panel.appendChild(bootStatusNode);
                    var titleToKey = {};
        panel.appendChild(meter);
                    var keyToFile = {};
        panel.appendChild(bootProgressNode);
                    var titles = [];
        panel.appendChild(bootDetailNode);
                    chunk.forEach(function (file) {
        bootNode.appendChild(decoLayer);
                        var key = fileKey(file);
        bootNode.appendChild(panel);
                        if (!key) return;
        bootNode.appendChild(close);
                        titleToKey[fileKey('File:' + file)] = key;
        document.body.appendChild(bootNode);
                        titleToKey[fileKey('파일:' + file)] = key;
        return bootNode;
                        keyToFile[key] = file;
    }
                        titles.push('File:' + file);
 
                        titles.push('파일:' + file);
    function hideBootScreen() {
                    });
        var node = bootNode || document.getElementById('boot-gate-screen');
                    return fetchApi({
        if (!node) {
                        action: 'query',
            document.documentElement.classList.remove('boot-gate-active');
                        format: 'json',
            if (document.body) document.body.classList.remove('boot-gate-active');
                        formatversion: '2',
            return;
                        redirects: '1',
        }
                        prop: 'imageinfo',
        node.classList.add('is-complete');
                        iiprop: 'url|sha1|timestamp|size',
        node.classList.remove('is-active');
                        iiurlwidth: '16',
        window.setTimeout(function () {
                        titles: titles.join('|')
            if (node.parentNode) node.parentNode.removeChild(node);
                    }).then(function (json) {
            if (bootNode === node) bootNode = null;
                        var pages = (json && json.query && json.query.pages) || [];
            document.documentElement.classList.remove('boot-gate-active');
                        var seen = {};
             if (document.body) document.body.classList.remove('boot-gate-active');
                        pages.forEach(function (page) {
        }, 240);
                            var key = titleToKey[fileKey(page && page.title)] || fileKey(page && page.title);
    }
                            var file = keyToFile[key] || (page && page.title || '').replace(/^(?:File|파일):/i, '');
                            var imageinfo = page && page.imageinfo && page.imageinfo[0];
                            var url;
                            var rev;
                            var sep;
                            var cacheKey;
                            if (!key) return;
                            seen[key] = true;
                            url = imageinfo && (imageinfo.thumburl || imageinfo.url) ? (imageinfo.thumburl || imageinfo.url) : '';
                            if (url) {
                                rev = imageinfo && (imageinfo.sha1 || imageinfo.timestamp || imageinfo.size) ? (imageinfo.sha1 || imageinfo.timestamp || imageinfo.size) : '';
                                if (rev && !/[?&]_entryFileRev=/.test(url)) {
                                    sep = url.indexOf('?') === -1 ? '?' : '&';
                                    url += sep + '_entryFileRev=' + encodeURIComponent(String(rev));
                                }
                            }
                            flagUrlCache[key] = url;
                            cacheKey = cacheKeyForFile(file);
                            if (cacheKey && window.EntryCache && typeof window.EntryCache.putText === 'function') {
                                window.EntryCache.putText(cacheKey, url || '', {
                                    resourceKey: resourceKeyForFile(file),
                                    token: tokenForFile(file),
                                    kind: 'flagurl',
                                    contentType: 'text/plain; charset=UTF-8'
                                });
                            }
                        });
                        chunk.forEach(function (file) {
                            var key = fileKey(file);
                            if (key && !seen[key] && flagUrlCache[key] === undefined) flagUrlCache[key] = '';
                        });
                    }).catch(function () {
                        chunk.forEach(function (file) {
                            var key = fileKey(file);
                            if (key && flagUrlCache[key] === undefined) flagUrlCache[key] = '';
                        });
                    });
                })).then(function () { return flagUrlCache; });
             });
        }
 
        function normalizeGenericFileTitle(value) {
            var text = String(value || '').trim();
            var match;
            var i;


    function collectFlagsFromNationPayload(payload) {
             if (!text) return '';
        var files = [];
             for (i = 0; i < 4; i += 1) {
        function add(value) {
                 try {
             if (!value) return;
                    if (/%[0-9a-f]{2}/i.test(text)) text = decodeURIComponent(text);
             if (typeof value === 'string') {
                 } catch (err) {
                 files.push(value);
                    break;
                 return;
                }
             }
             }
             if (typeof value === 'object') files.push(value.file || value.flag_file || value.flag || value.flag_title || '');
            match = text.match(/[?&]title=([^&#]+)/i);
             if (match) text = match[1];
            text = text.replace(/^https?:\/\/[^/]+/i, '').replace(/^\/+/, '').replace(/^index\.php\/?/i, '').replace(/^wiki\/?/i, '').trim();
            match = text.match(/(?:^|\/)(?:(?:Special|특수)\s*[:/]\s*)?(?:Redirect|넘겨주기)\s*\/\s*file\s*\/(.+)$/i);
            if (match) text = match[1];
            text = text.replace(/^(?:File|파일|Image|이미지)\s*:/i, '').replace(/^:+/, '').trim();
            return text;
         }
         }
         function scanItem(item) {
 
             if (!item || typeof item !== 'object') return;
         function isFileRef(value) {
             if (Array.isArray(item.flags)) item.flags.forEach(add);
             var text = String(value || '').trim();
            add(item.flag_file || item.flag || item.flag_title || '');
             return /^(?:file|파일)\s*:/i.test(text) || /(?:^|\/)(?:(?:Special|특수)\s*[:/]\s*)?(?:Redirect|넘겨주기)\s*\/\s*file\s*\//i.test(text);
         }
         }
        (payload && payload.continents || []).forEach(function (continent) {
            (continent.regions || []).forEach(function (region) {
                (region.items || []).forEach(scanItem);
            });
        });
        return unique(files);
    }


    function collectFlagsFromLinkMap(payload) {
        function fileUrlKey(value) {
        var files = [];
            return normalizeGenericFileTitle(value).replace(/_/g, ' ').replace(/\s+/g, ' ').toLowerCase();
        var source = payload && payload.items ? payload.items : {};
         }
        Object.keys(source || {}).forEach(function (key) {
            var item = source[key];
            if (!item || typeof item !== 'object') return;
            files.push(item.flag_file || item.flag || item.flag_title || '');
        });
         return unique(files);
    }


    function prewarmImages(urls, limit) {
        function stableDirectImageUrl(url) {
        return window.EntryStore.preloadImages(urls, {
            var text = String(url || '').trim();
             limit: Number(limit) > 0 ? Number(limit) : 0,
            var separator;
             concurrency: 16,
            var bust;
             persistent: false
            if (!text) return '';
        });
             if (/[?&]_entryAsset=/.test(text) || /[?&]_=/.test(text)) return text;
    }
             bust = String(BUILD_ID || 'entry');
             separator = text.indexOf('?') === -1 ? '?' : '&';
            return text + separator + '_entryAsset=' + encodeURIComponent(bust);
        }


    function getDecorationRuntime() {
        function resolveFileUrl(ref) {
        return window.Decorations || window.CLBI_DECORATIONS || null;
            var original = String(ref || '').trim();
    }
            var file;
 
            var key;
    function waitForDecorationRuntime() {
            var cacheKey;
        return new Promise(function (resolve) {
            var resourceKey;
             var tries = 0;
            var token;
             function tick() {
            if (!original) return Promise.resolve('');
                var runtime = getDecorationRuntime();
            if (!isFileRef(original)) return Promise.resolve(stableDirectImageUrl(original));
                if (runtime) return resolve(runtime);
             file = normalizeGenericFileTitle(original);
                tries += 1;
             key = fileUrlKey(file);
                 if (tries > 40) return resolve(null);
            if (!file || !key) return Promise.resolve(stableDirectImageUrl(original));
                window.setTimeout(tick, 25);
            if (fileUrlCache[key]) return Promise.resolve(fileUrlCache[key]);
            cacheKey = window.RevisionManifest && typeof window.RevisionManifest.cacheKeyForRef === 'function' ? window.RevisionManifest.cacheKeyForRef('File:' + file, 'fileurl') : '';
            resourceKey = window.RevisionManifest && typeof window.RevisionManifest.resourceKeyForRef === 'function' ? window.RevisionManifest.resourceKeyForRef('File:' + file) : ('file:' + key);
            token = window.RevisionManifest && typeof window.RevisionManifest.tokenForRef === 'function' ? window.RevisionManifest.tokenForRef('File:' + file) : '';
            if (cacheKey && window.EntryCache && typeof window.EntryCache.getText === 'function') {
                 return window.EntryCache.getText(cacheKey).then(function (cachedUrl) {
                    if (cachedUrl !== null && cachedUrl !== undefined) {
                        fileUrlCache[key] = String(cachedUrl || '');
                        return fileUrlCache[key];
                    }
                    return fetchApi({
                        action: 'query',
                        format: 'json',
                        formatversion: '2',
                        redirects: '1',
                        prop: 'imageinfo',
                        iiprop: 'url|sha1|timestamp|size|mime',
                        titles: 'File:' + file
                    }).then(function (json) {
                        var pages = (json && json.query && json.query.pages) || [];
                        var info = pages[0] && pages[0].imageinfo && pages[0].imageinfo[0] ? pages[0].imageinfo[0] : null;
                        var url = info && info.url ? info.url : original;
                        var rev = info && (info.sha1 || info.timestamp || info.size) ? (info.sha1 || info.timestamp || info.size) : String(BUILD_ID || 'entry-file');
                        var separator = url.indexOf('?') === -1 ? '?' : '&';
                        var resolved = url + separator + '_entryFileRev=' + encodeURIComponent(String(rev));
                        fileUrlCache[key] = resolved;
                        window.EntryCache.putText(cacheKey, resolved, { resourceKey: resourceKey, token: token, kind: 'fileurl', contentType: 'text/plain; charset=UTF-8' });
                        return resolved;
                    });
                }).catch(function () {
                    return stableDirectImageUrl(original);
                });
             }
             }
             tick();
             return fetchApi({
        });
                action: 'query',
    }
                format: 'json',
 
                formatversion: '2',
    function matchesDecorationEntry(entry, filter) {
                redirects: '1',
        var era = String(filter && filter.era || '').trim();
                prop: 'imageinfo',
        var page = String(filter && filter.page || '').trim();
                iiprop: 'url|sha1|timestamp|size|mime',
        var entryPage = String(entry && entry.page || '').replace(/_/g, ' ').trim();
                titles: 'File:' + file
        if (!entry || typeof entry !== 'object') return false;
            }).then(function (json) {
        if (page && entryPage && entryPage !== page) return false;
                var pages = (json && json.query && json.query.pages) || [];
        if (era && String(entry.era || '').trim() && String(entry.era || '').trim() !== era) return false;
                var info = pages[0] && pages[0].imageinfo && pages[0].imageinfo[0] ? pages[0].imageinfo[0] : null;
        return true;
                 var url = info && info.url ? info.url : original;
    }
                 var rev = info && (info.sha1 || info.timestamp || info.size) ? (info.sha1 || info.timestamp || info.size) : String(BUILD_ID || 'entry-file');
 
                 var separator = url.indexOf('?') === -1 ? '?' : '&';
    function prepareDecorationSet(task, level) {
                 var resolved = url + separator + '_entryFileRev=' + encodeURIComponent(String(rev));
        var ref = task.ref || 'MediaWiki:Decorations.json';
                fileUrlCache[key] = resolved;
        return window.EntryStore.fetchJsonRef(ref, { noStore: !!task.noStore }).then(function (registry) {
                return resolved;
            var list = registry && Array.isArray(registry.decorations) ? registry.decorations : [];
            }).catch(function () {
            var pixelRefs = [];
                 return stableDirectImageUrl(original);
            list.forEach(function (entry) {
                 var type = String(entry && entry.assetType || '').toLowerCase();
                 var asset = String(entry && (entry.asset || entry.src) || '').trim();
                 if (!asset) return;
                 if (type !== 'pixel-json' && !/\.json(?:[?#].*)?$/i.test(asset)) return;
                if (!matchesDecorationEntry(entry, task)) return;
                 pixelRefs.push(asset);
             });
             });
            if (level !== 'full' || task.preparePixels === false || !pixelRefs.length) return registry;
         }
            return waitForDecorationRuntime().then(function (runtime) {
                if (!runtime || typeof runtime.preparePixelCanvas !== 'function') {
                    return Promise.all(pixelRefs.map(function (pixelRef) {
                        return window.EntryStore.fetchJsonRef(pixelRef).catch(function () { return null; });
                    })).then(function () { return registry; });
                }
                return Promise.all(pixelRefs.map(function (pixelRef) {
                    return runtime.preparePixelCanvas(pixelRef).catch(function () { return null; });
                })).then(function () { return registry; });
            });
         });
    }


    function prepareNationsEra(task, level) {
        function preloadImageUrl(url, options) {
        var era = String(task.era || '1950');
            var key = String(url || '').trim();
        var listRef = task.listRef || ('MediaWiki:' + era + '_Nation_List.json');
            var cacheKey;
        var linkRef = task.linkMapRef || ('MediaWiki:' + era + '_Nation_Link_Map.json');
            var sourcePromise;
        var jsonPhaseId = BootPerf.start('nations-era json fetch', { era: era, level: level, listRef: listRef, linkRef: linkRef });
            var persistent = !(options && options.persistent === false);
        var listPromise = window.EntryStore.fetchJsonRef(listRef).catch(function () { return null; });
            if (!key) return Promise.resolve(false);
        var linkPromise = window.EntryStore.fetchJsonRef(linkRef).catch(function () { return null; });
            if (imageReadyCache[key]) return Promise.resolve(true);
            if (imagePromiseCache[key]) return imagePromiseCache[key];


        return Promise.all([listPromise, linkPromise]).then(function (results) {
            cacheKey = persistent && window.RevisionManifest && typeof window.RevisionManifest.cacheKeyForUrl === 'function' ? window.RevisionManifest.cacheKeyForUrl(key, 'image') : '';
            BootPerf.end(jsonPhaseId, { ok: true });
             sourcePromise = Promise.resolve('');
            var files;
             if (cacheKey && window.EntryCache && typeof window.EntryCache.fetchBlobUrl === 'function') {
            var flagUrls;
                 sourcePromise = window.EntryCache.fetchBlobUrl(key, cacheKey, {
            if (level !== 'full' && level !== 'warm') return results;
                     resourceKey: '',
             files = unique(collectFlagsFromNationPayload(results[0]).concat(collectFlagsFromLinkMap(results[1])));
                     token: '',
             return BootPerf.measure('nations-era flag url resolve', { era: era, count: files.length }, function () {
                     kind: 'image'
                return window.EntryStore.resolveFlagUrls(files);
                 }).catch(function () { return ''; });
            }).then(function () {
             }
                if (level === 'warm') {
                    /*
                    * Warm tab contract — 20260708.
                    *
                    * A previous tab already stored the same-revision flag URL and image
                    * blobs.  Re-decoding every flag here makes the second tab feel as slow
                    * as the first one.  In warm mode we only hydrate the flag URL table from
                    * EntryCache so NationsPanel can render stable URLs immediately; image
                    * decode remains demand-driven by the live panel instead of blocking the
                    * boot screen.
                    */
                    return results;
                }
                 flagUrls = files.map(function (file) { return window.EntryStore.getFlagUrl(file); }).filter(Boolean);
                /*
                * 20260710: Flag URLs are part of the entry contract, but decoding every
                * flag image is not.  The 1950 list has more than 150 flags, and waiting
                * for all image decodes kept the public boot gate open for several seconds.
                * Keep revision-aware URLs ready for the panel, then let the browser load
                * the actual images from the live DOM.  A low-priority background warm-up
                * may run after the boot gate has opened, so it never competes with the
                * current entry surface or the globe ready contract.
                */
                BootPerf.mark('nations-era flag image prewarm skipped', {
                     era: era,
                     count: flagUrls.length,
                    limit: task.flagLimit == null ? 0 : Number(task.flagLimit),
                     reason: 'deferred-after-boot-gate'
                 });
                if (flagUrls.length && task.deferFlagImages !== false) {
                    deferredEntryWarmups.push(function () {
                        return BootPerf.measure('deferred nations-era flag image prewarm', {
                            era: era,
                            count: flagUrls.length,
                            limit: task.flagLimit == null ? 0 : Number(task.flagLimit)
                        }, function () {
                            return prewarmImages(flagUrls, task.flagLimit == null ? 0 : Number(task.flagLimit));
                        });
                    });
                }
                return results;
             });
        });
    }


    function prepareGlobeSharedAssets(task, level) {
            imagePromiseCache[key] = sourcePromise.then(function (cachedObjectUrl) {
        var refs = Array.isArray(task && task.assets) ? task.assets : [];
                return new Promise(function (resolve) {
        if (!refs.length) return Promise.resolve(null);
                    var img = new Image();
        return BootPerf.measure('globe-shared url resolve', { count: refs.length, level: level }, function () {
                    var settled = false;
            return Promise.all(refs.map(function (ref) {
                    var src = cachedObjectUrl || key;
                return window.EntryStore.resolveFileUrl(ref).catch(function () { return ''; });
                    function finish(ok) {
            }));
                        if (settled) return;
        }).then(function (urls) {
                        settled = true;
            urls = urls.filter(Boolean);
                        if (ok) {
            /*
                            imageReadyCache[key] = true;
            * Globe shared assets are heavy 6K texture sources. The live
                            imageReadyCache[src] = true;
            * NationsGlobe ready contract already waits for the actual Three.js
                            imageObjectCache[key] = img;
            * texture path, so decoding the same images here only duplicates work
                            imageObjectCache[src] = img;
            * and holds the boot screen. Keep revision-aware URL resolution in the
                            imageDisplayUrlCache[key] = src;
            * entry ledger, but make the image warm-up non-blocking.
                            imageDisplayUrlCache[src] = src;
            */
                        }
            BootPerf.mark('globe-shared image prewarm skipped', {
                        resolve(!!ok);
                count: urls.length,
                    }
                level: level,
                    img.onload = function () {
                 reason: 'deferred-to-nations-globe-ready-contract'
                        if (img.decode) {
                            img.decode().then(function () { finish(true); }).catch(function () { finish(true); });
                        } else {
                            finish(true);
                        }
                    };
                    img.onerror = function () {
                        if (src !== key) {
                            src = key;
                            img.src = key;
                            return;
                        }
                        finish(false);
                    };
                    img.decoding = 'async';
                    img.loading = 'eager';
                    img.src = src;
                    if (img.complete && img.naturalWidth) {
                        if (img.decode) img.decode().then(function () { finish(true); }).catch(function () { finish(true); });
                        else finish(true);
                    }
                 });
             });
             });
            window.setTimeout(function () {
             return imagePromiseCache[key];
                if (!window.EntryStore || typeof window.EntryStore.preloadImages !== 'function') return;
         }
                window.EntryStore.preloadImages(urls, {
                    limit: task.imageLimit == null ? 0 : Number(task.imageLimit),
                    concurrency: Math.max(1, Math.min(16, Number(task.concurrency) || 4)),
                    persistent: false
                }).catch(function () {});
            }, 0);
             return urls;
         });
    }


    function prepareImageAssets(task, level) {
        function preloadImages(urls, options) {
        var refs = Array.isArray(task && task.assets) ? task.assets : [];
            var list = unique((urls || []).filter(Boolean));
        var full = level === 'full';
            var limit = options && Number(options.limit);
        var exposeAs = String(task && task.exposeAs || '').trim();
            var concurrency = Math.max(1, Math.min(48, Number(options && options.concurrency) || 24));
        if (!refs.length) return Promise.resolve(null);
             var index = 0;
        return BootPerf.measure('image-assets url resolve', { count: refs.length, exposeAs: exposeAs, level: level }, function () {
             var ok = 0;
            return Promise.all(refs.map(function (ref) {
            var fail = 0;
                return window.EntryStore.resolveFileUrl(ref).catch(function () { return ''; });
             if (Number.isFinite(limit) && limit > 0) list = list.slice(0, limit);
             }));
            if (!list.length) return Promise.resolve({ total: 0, ok: 0, fail: 0 });
        }).then(function (urls) {
             return new Promise(function (resolve) {
            urls = urls.filter(Boolean);
                function pump() {
             if (exposeAs === 'nationsGlobeLoadingGif') {
                    while (index < list.length && concurrency > 0) {
                window.NationsGlobeLoadingGifRef = refs[0] || '';
                        (function (url) {
                window.NationsGlobeLoadingGifUrl = urls[0] || '';
                            concurrency -= 1;
                window.NationsGlobeLoadingGifFile = 'Gfx-vhs-glitch-001.gif';
                            preloadImageUrl(url, options || {}).then(function (result) {
            }
                                if (result) ok += 1;
             if (!full) return urls;
                                else fail += 1;
            return BootPerf.measure('image-assets image prewarm', { count: urls.length, exposeAs: exposeAs, limit: task.imageLimit == null ? 0 : Number(task.imageLimit), concurrency: Math.max(1, Math.min(16, Number(task.concurrency) || 2)) }, function () {
                            }).catch(function () {
                return window.EntryStore.preloadImages(urls, {
                                fail += 1;
                    limit: task.imageLimit == null ? 0 : Number(task.imageLimit),
                            }).then(function () {
                    concurrency: Math.max(1, Math.min(16, Number(task.concurrency) || 2))
                                concurrency += 1;
                });
                                if (index >= list.length && ok + fail >= list.length) resolve({ total: list.length, ok: ok, fail: fail });
             }).then(function () { return urls; });
                                else pump();
        });
                            });
    }
                        })(list[index++]);
 
                     }
    function prepareHtmlEntry(task) {
        var url = String(task && (task.url || task.ref) || '').trim();
        if (!url) return Promise.resolve(null);
        return window.EntryStore.fetchTextUrl(url, { noStore: !!task.noStore, resourceRef: task.resourceRef || task.ref || task.title || '' });
    }
 
    function prepareTask(task, defaultLevel) {
        var level = String(task && (task.level || defaultLevel) || 'half').toLowerCase();
        var type = String(task && task.type || '').toLowerCase();
        if (!task || typeof task !== 'object') return Promise.resolve(null);
        if (type === 'html') return prepareHtmlEntry(task);
        if (type === 'json') return window.EntryStore.fetchJsonRef(task.ref, { noStore: !!task.noStore });
        if (type === 'pixel-json') {
            return waitForDecorationRuntime().then(function (runtime) {
                if (level === 'full' && runtime && typeof runtime.preparePixelCanvas === 'function') {
                     return runtime.preparePixelCanvas(task.ref || task.asset);
                 }
                 }
                 return window.EntryStore.fetchJsonRef(task.ref || task.asset);
                 pump();
             });
             });
         }
         }
        if (type === 'decorations') return prepareDecorationSet(task, level);
        if (type === 'nations-era') return prepareNationsEra(task, level);
        if (type === 'globe-shared-assets') return prepareGlobeSharedAssets(task, level);
        if (type === 'image-assets') return prepareImageAssets(task, level);
        return Promise.resolve(null);
    }


    function loadManifest() {
         function isImageReady(url) {
         return BootPerf.measure('revision manifest load', {}, function () {
             var key = String(url || '').trim();
             return (window.RevisionManifest && typeof window.RevisionManifest.load === 'function' ? window.RevisionManifest.load() : Promise.resolve(null));
             return !!(key && imageReadyCache[key]);
        }).then(function () {
         }
            return BootPerf.measure('entry manifest json fetch', { ref: MANIFEST_TITLE }, function () {
                return window.EntryStore.fetchJsonRef(MANIFEST_TITLE, { noStore: false });
            });
        }).then(function (manifest) {
            if (!manifest || typeof manifest !== 'object' || !manifest.version) {
                BootPerf.mark('entry manifest fallback', { reason: 'invalid manifest' });
                return defaultManifest;
            }
            BootPerf.mark('entry manifest ready', { version: manifest.version });
             return manifest;
        }).catch(function (err) {
            BootPerf.mark('entry manifest fallback', { reason: err && (err.message || String(err)) || 'load failed' });
            return defaultManifest;
         });
    }


    function flattenInitialTasks(manifest) {
         function getImageElement(url) {
        var initial = manifest && manifest.initial ? manifest.initial : {};
             var key = String(url || '').trim();
         var full = Array.isArray(initial.full) ? initial.full : [];
             return key ? (imageObjectCache[key] || null) : null;
        var half = Array.isArray(initial.half) ? initial.half : [];
         }
        var tasks = [];
        full.forEach(function (task) {
             task = Object.assign({}, task);
            task.level = task.level || 'full';
            task.blocking = true;
            tasks.push(task);
        });
        half.forEach(function (task) {
             task = Object.assign({}, task);
            task.level = task.level || 'half';
            task.blocking = false;
            tasks.push(task);
        });
         return tasks;
    }


        function getImageDisplayUrl(url) {
            var key = String(url || '').trim();
            return key ? (imageDisplayUrlCache[key] || key) : '';
        }


    function flattenWarmInitialTasks(tasks) {
        function normalizeUrlKey(url) {
        var warmed = [];
            var text = String(url || '').trim();
        (tasks || []).forEach(function (task) {
             var a;
             var copy;
            if (!text) return '';
             var type = String(task && task.type || '').toLowerCase();
             try {
             if (!task || task.blocking === false) return;
                a = document.createElement('a');
                a.href = text;
                text = a.pathname + (a.search || '');
            } catch (err) {}
            try {
                text = decodeURI(text);
             } catch (err2) {}
            text = text.replace(/([?&])_=[^&]*/g, '$1').replace(/[?&]$/, '');
            text = text.replace(/_/g, '_');
            return text;
        }


             /*
        function fetchTextUrl(url, options) {
            * Warm boot fast path — 20260708.
             var key = normalizeUrlKey(url);
            *
            var cacheKey;
            * Cold boot intentionally performs the expensive work: resolving every flag,
            var resourceRef;
            * downloading/decoding images, preparing decoration canvases, and warming shared
            var resourceKey;
            * globe textures. Once a tab has certified the pack against the current revision
            var token;
            * manifest, later tabs must not replay that full workload. They only hydrate the
            if (!key) return Promise.reject(new Error('empty text url'));
            * small tables needed by the live page and let already-cached image/blob data be
            if (textCache[key]) return Promise.resolve(textCache[key].text);
            * consumed on demand. This is the missing layer that made second tabs feel almost
            if (textPromiseCache[key]) return textPromiseCache[key];
            * as slow as first tabs even though the string checks were true.
            resourceRef = options && options.resourceRef ? options.resourceRef : (options && options.ref ? options.ref : '');
            */
             cacheKey = resourceRef && window.RevisionManifest && typeof window.RevisionManifest.cacheKeyForRef === 'function' ? window.RevisionManifest.cacheKeyForRef(resourceRef, 'text') : '';
             if (type === 'globe-shared-assets') return;
             if (!cacheKey && window.RevisionManifest && typeof window.RevisionManifest.cacheKeyForUrl === 'function') cacheKey = window.RevisionManifest.cacheKeyForUrl(url, 'text');
             if (type === 'image-assets') return;
             resourceKey = resourceRef && window.RevisionManifest && typeof window.RevisionManifest.resourceKeyForRef === 'function' ? window.RevisionManifest.resourceKeyForRef(resourceRef) : ('url:' + key);
             copy = Object.assign({}, task);
             token = resourceRef && window.RevisionManifest && typeof window.RevisionManifest.tokenForRef === 'function' ? window.RevisionManifest.tokenForRef(resourceRef) : '';
             if (type === 'nations-era') copy.level = 'warm';
             if (cacheKey && !(options && options.noStore) && window.EntryCache && typeof window.EntryCache.getText === 'function') {
             else if (type === 'decorations') {
                 textPromiseCache[key] = window.EntryCache.getText(cacheKey).then(function (cachedText) {
                 copy.level = 'half';
                    if (cachedText !== null && cachedText !== undefined) {
                copy.preparePixels = false;
                        textCache[key] = { key: key, url: url, text: cachedText, loadedAt: now(), persistent: true };
            } else if (type === 'pixel-json') copy.level = 'half';
                        return cachedText;
             warmed.push(copy);
                    }
        });
                    return fetch(url, {
        return warmed;
                        credentials: 'same-origin',
    }
                        cache: 'force-cache'
                    }).then(function (res) {
                        if (!res.ok) throw new Error('HTTP ' + res.status);
                        return res.text();
                    }).then(function (text) {
                        textCache[key] = { key: key, url: url, text: text, loadedAt: now() };
                        window.EntryCache.putText(cacheKey, text, { resourceKey: resourceKey, token: token, kind: 'text', contentType: 'text/html; charset=UTF-8' });
                        return text;
                    });
                });
                return textPromiseCache[key];
            }
            textPromiseCache[key] = fetch(url, {
                credentials: 'same-origin',
                cache: options && options.noStore ? 'no-store' : 'force-cache'
            }).then(function (res) {
                if (!res.ok) throw new Error('HTTP ' + res.status);
                return res.text();
             }).then(function (text) {
                textCache[key] = { key: key, url: url, text: text, loadedAt: now() };
                return text;
            });
            return textPromiseCache[key];
        }


    function currentInitialPackKey(manifest) {
        function getTextSync(url) {
        var version = manifest && manifest.version ? String(manifest.version) : BUILD_ID;
            var key = normalizeUrlKey(url);
        return 'initial-entry-full:' + version;
            return key && textCache[key] ? textCache[key].text : '';
    }
        }


    function currentInitialPackToken(manifest) {
        function setTextUrl(url, text) {
        var version = manifest && manifest.version ? String(manifest.version) : BUILD_ID;
            var key = normalizeUrlKey(url);
        var revToken = window.RevisionManifest && typeof window.RevisionManifest.manifestToken === 'function' ? window.RevisionManifest.manifestToken(version) : version;
            if (!key) return;
         return revToken;
            textCache[key] = { key: key, url: url, text: String(text || ''), loadedAt: now() };
    }
         }


    function isInitialPackWarm(manifest) {
        function cacheInfo() {
        var key = currentInitialPackKey(manifest);
            return {
        var token = currentInitialPackToken(manifest);
                json: Object.keys(jsonCache).length,
        return !!(window.EntryCache && typeof window.EntryCache.packReady === 'function' && window.EntryCache.packReady(key, token));
                jsonKeys: Object.keys(jsonCache),
    }
                flags: Object.keys(flagUrlCache).length,
 
                files: Object.keys(fileUrlCache).length,
    function markInitialPackWarm(manifest, meta) {
                images: Object.keys(imageReadyCache).length,
        var key = currentInitialPackKey(manifest);
                retainedImages: Object.keys(imageObjectCache).length,
        var token = currentInitialPackToken(manifest);
                displayImages: Object.keys(imageDisplayUrlCache).length,
        if (window.EntryCache && typeof window.EntryCache.setPackReady === 'function') {
                text: Object.keys(textCache).length,
            window.EntryCache.setPackReady(key, token, Object.assign({ manifestVersion: manifest && manifest.version || BUILD_ID }, meta || {}));
                textKeys: Object.keys(textCache)
            };
         }
         }
    }


 
         return {
    function waitMs(ms) {
            fetchJsonRef: fetchJsonRef,
         return new Promise(function (resolve) { window.setTimeout(resolve, Math.max(0, ms || 0)); });
            getJsonSync: getJsonSync,
    }
            setJsonRef: setJsonRef,
 
            normalizeRefKey: normalizeRefKey,
    function waitForBody() {
            rawUrlForRef: rawUrlForRef,
        if (document.body) return Promise.resolve(document.body);
            revisionUrlForRef: function (ref, ctype) {
        return new Promise(function (resolve) {
                var url = rawUrlForRef(ref, ctype);
            function tick() {
                 if (window.RevisionManifest && typeof window.RevisionManifest.addRevisionParam === 'function') {
                 if (document.body) return resolve(document.body);
                    url = window.RevisionManifest.addRevisionParam(url, ref);
                window.setTimeout(tick, 10);
                }
             }
                return url;
             tick();
             },
         });
            resolveFlagUrls: resolveFlagUrls,
            setFlagUrl: setFlagUrl,
            getFlagUrl: getFlagUrl,
            resolveFileUrl: resolveFileUrl,
            preloadImageUrl: preloadImageUrl,
            preloadImages: preloadImages,
            isImageReady: isImageReady,
            getImageElement: getImageElement,
            getImageDisplayUrl: getImageDisplayUrl,
            fetchTextUrl: fetchTextUrl,
            getTextSync: getTextSync,
            setTextUrl: setTextUrl,
             stableDirectImageUrl: stableDirectImageUrl,
            cacheInfo: cacheInfo
         };
     }
     }


     function waitForDomReady() {
     window.EntryStore = window.EntryStore || createEntryStore();
        if (document.readyState !== 'loading') return Promise.resolve();
        return new Promise(function (resolve) {
            document.addEventListener('DOMContentLoaded', resolve, { once: true });
        });
    }


     function waitForAnimationFrames(count) {
     function updateBootProgress(done, total, label) {
         count = Math.max(1, Math.round(count || 1));
         var pct = total ? Math.max(0, Math.min(100, Math.round((done / total) * 100))) : 0;
         return new Promise(function (resolve) {
         if (bootProgressNode) bootProgressNode.textContent = pct + '%';
            function next(left) {
        if (bootFillNode) bootFillNode.style.width = pct + '%';
                if (left <= 0) return resolve();
         if (bootDetailNode && label) bootDetailNode.textContent = label;
                window.requestAnimationFrame(function () { next(left - 1); });
            }
            next(count);
         });
     }
     }


     function waitUntil(predicate, options) {
     function adoptBootScreen(node) {
         var started = now();
         if (!node) return null;
         var timeout = options && options.timeoutMs ? options.timeoutMs : 8000;
         bootNode = node;
         var interval = options && options.intervalMs ? options.intervalMs : 60;
         bootStatusNode = bootNode.querySelector('.boot-gate-status');
         return new Promise(function (resolve) {
         bootProgressNode = bootNode.querySelector('.boot-gate-progress');
            function tick() {
        bootDetailNode = bootNode.querySelector('.boot-gate-detail');
                var result = false;
        bootFillNode = bootNode.querySelector('.boot-gate-meter-fill');
                try { result = predicate(); } catch (err) { result = false; }
        bootNode.classList.add('is-active');
                if (result) return resolve({ ok: true, value: result });
        bootNode.classList.remove('is-complete');
                if (now() - started >= timeout) return resolve({ ok: false, timeout: true });
         return bootNode;
                window.setTimeout(tick, interval);
            }
            tick();
         });
     }
     }


     function waitForTrackedScripts() {
     function ensureBootScreen(options) {
         var list = (window.EntryScriptLoads || []).slice();
         var panel;
         if (!list.length) return Promise.resolve([]);
        var header;
         return Promise.all(list.map(function (promise) {
        var meter;
            return Promise.resolve(promise).catch(function (err) { return { ok: false, error: err }; });
        var existing;
        }));
        var decoLayer;
    }
        var close;
        var previewMode;
        options = options || {};
        previewMode = !!options.preview;
         if (bootNode && bootNode.parentNode) return bootNode;
         if (!document.body) return null;


    function waitForDocumentSurface() {
         if (BOOT_EXCLUDED_PAGE && !previewMode) return null;
         return waitForDomReady().then(function () {
         if (!previewMode) activateBootSurface();
            return waitForBody();
         }).then(function () {
            return waitUntil(function () {
                return document.querySelector('.content-wrapper') && document.querySelector('.liberty-content-main');
            }, { timeoutMs: 8000, intervalMs: 50 });
        });
    }


    function isNationsPageSurface() {
         existing = document.getElementById('boot-gate-screen') || (!previewMode && window.__BootGatePrelude && window.__BootGatePrelude.ensure ? window.__BootGatePrelude.ensure() : null);
         var page = String(mw && mw.config ? (mw.config.get('wgPageName') || mw.config.get('wgTitle') || '') : '');
         if (existing) return adoptBootScreen(existing);
         return !!document.querySelector('.clbi-nations-panel-stack') || /국가[_ ]및[_ ]조합/.test(page);
    }


    function getInitialNationsEra(manifest) {
         bootNode = document.createElement('div');
         var tasks = flattenInitialTasks(manifest);
         bootNode.id = 'boot-gate-screen';
         var i;
         bootNode.className = 'boot-gate-screen is-active';
         for (i = 0; i < tasks.length; i += 1) {
        bootNode.setAttribute('role', 'status');
            if (tasks[i] && tasks[i].type === 'nations-era' && String(tasks[i].level || '').toLowerCase() === 'full') {
        bootNode.setAttribute('aria-live', 'polite');
                return String(tasks[i].era || '1950');
            }
        }
        return '1950';
    }


    function waitForNationsPanelReady(era) {
         panel = document.createElement('div');
         return BootPerf.measure('wait nations panel api', { era: era }, function () {
         panel.className = 'boot-gate-panel';
            return waitUntil(function () { return window.NationsPanel && typeof window.NationsPanel.whenEraReady === 'function'; }, {
                timeoutMs: 8000,
                intervalMs: 50
            });
        }).then(function (result) {
            if (!result.ok || !window.NationsPanel || typeof window.NationsPanel.whenEraReady !== 'function') return null;
            return BootPerf.measure('wait nations panel era ready', { era: era }, function () {
                return window.NationsPanel.whenEraReady(era, { timeoutMs: 15000 }).catch(function () { return null; });
            });
         }).then(function () {
            return BootPerf.measure('wait nations panel dom ready', { era: era }, function () {
                return waitUntil(function () {
                    var panel = document.querySelector('.clbi-nations-era-content[data-era-content="' + era + '"] .clbi-nations-tabpanel[data-nation-list-source="1"]') ||
                        document.querySelector('.clbi-nations-tabpanel[data-nation-list-source="1"]');
                    return panel && panel.classList.contains('clbi-nations-list-json-ready') && panel.getAttribute('data-nation-list-year-loaded') === String(era);
                }, { timeoutMs: 15000, intervalMs: 80 });
            });
        });
    }


    function waitForNationsGlobeReady(options) {
         header = document.createElement('div');
         var globe = document.querySelector('.clbi-nations-globe-window[data-nations-globe], [data-nations-globe="real-world"]');
        header.className = 'boot-gate-title';
         var soft;
        header.textContent = 'ARCHIVE INITIALIZATION';
         var timeout;
 
        bootStatusNode = document.createElement('div');
         bootStatusNode.className = 'boot-gate-status';
         bootStatusNode.textContent = 'Preparing entry systems';


         options = options || {};
         meter = document.createElement('div');
         soft = !!options.soft;
        meter.className = 'boot-gate-meter';
         timeout = Number(options.timeoutMs || (soft ? 450 : 30000));
         bootFillNode = document.createElement('div');
         bootFillNode.className = 'boot-gate-meter-fill';
        meter.appendChild(bootFillNode);


         if (!globe) {
         bootProgressNode = document.createElement('div');
            BootPerf.mark('wait nations globe skipped', { reason: 'no globe node', soft: soft });
        bootProgressNode.className = 'boot-gate-progress';
            return Promise.resolve(null);
         bootProgressNode.textContent = '0%';
         }


         return BootPerf.measure(soft ? 'wait nations globe warm attach' : 'wait nations globe ready contract', { soft: soft, timeoutMs: timeout }, function () {
         bootDetailNode = document.createElement('div');
            if (soft) {
        bootDetailNode.className = 'boot-gate-detail';
                /*
        bootDetailNode.textContent = 'loading manifest';
                Warm-tab policy:
                Raw globe images are already revision-checked and blob-cached by EntryCache,
                but WebGL scene creation, GPU upload, topojson parsing, and per-tab Three.js
                objects cannot be shared across browser tabs.  Waiting for the full is-ready
                contract here makes a warm tab slower than the first tab.  In warm mode the
                initial gate only waits until the globe consumer has been attached, then the
                globe finishes its own per-tab hydrate without holding the whole page hostage.
                */
                return waitUntil(function () {
                    var node = document.querySelector('.clbi-nations-globe-window[data-nations-globe], [data-nations-globe="real-world"]');
                    if (!node) return false;
                    return node.getAttribute('data-nations-globe-ready') === '1' ||
                        !!node.CLBI_NationsGlobeInstance ||
                        !!node.querySelector('.clbi-nations-globe-stage') ||
                        node.classList.contains('is-ready') ||
                        node.classList.contains('has-error');
                }, { timeoutMs: timeout, intervalMs: 40 });
            }


            return waitUntil(function () {
        close = document.createElement('button');
                var node = document.querySelector('.clbi-nations-globe-window[data-nations-globe], [data-nations-globe="real-world"]');
        close.type = 'button';
                return node && (node.classList.contains('is-ready') || node.classList.contains('has-error'));
        close.className = 'boot-gate-close';
            }, { timeoutMs: timeout, intervalMs: 100 });
        close.setAttribute('aria-label', 'Close boot preview');
        });
        close.textContent = '×';
    }
        close.addEventListener('click', function () { hideBootScreen({ force: true }); });


    function waitForDecorationsReady(options) {
        decoLayer = document.createElement('div');
         options = options || {};
         decoLayer.className = 'boot-gate-decoration-layer';
         return waitForDecorationRuntime().then(function (runtime) {
         decoLayer.setAttribute('data-decoration-target', 'boot-gate');
            if (!runtime) return null;
        decoLayer.setAttribute('aria-hidden', 'true');
            if (options.warm) {
 
                /*
        panel.appendChild(header);
                * Warm tab contract — 20260708.
        panel.appendChild(bootStatusNode);
                *
        panel.appendChild(meter);
                * The decoration registry and pixel canvases were prepared by the cold tab.
        panel.appendChild(bootProgressNode);
                * Calling reload() here re-reads and re-syncs the decoration layer during the
        panel.appendChild(bootDetailNode);
                * boot gate, which is visible as a pointless delay. A warm tab only needs the
        bootNode.appendChild(decoLayer);
                * current visibility pass so boot can release quickly and editing/preview tools
        bootNode.appendChild(panel);
                * remain responsive.
        bootNode.appendChild(close);
                */
        document.body.appendChild(bootNode);
                try {
        return bootNode;
                    if (typeof runtime.updateVisibility === 'function') runtime.updateVisibility(document);
    }
                    else if (typeof runtime.sync === 'function') runtime.sync();
 
                } catch (err) {}
    function buildLoginUrl() {
                 return waitForAnimationFrames(1);
        var pageName = String(readConfig('wgPageName', '') || '').trim();
        var params = {};
 
        if (pageName && !isAuthenticationPage() && !isCreateAccountPage()) params.returnto = pageName;
        try {
            if (mw && mw.util && typeof mw.util.getUrl === 'function') {
                 return mw.util.getUrl('Special:UserLogin', params);
             }
             }
            if (typeof runtime.reload !== 'function') return null;
        } catch (err) {}
            return runtime.reload().catch(function () { return null; }).then(function () {
 
                return waitForAnimationFrames(2);
        return '/index.php?title=Special%3AUserLogin' + (params.returnto ? '&returnto=' + encodeURIComponent(params.returnto) : '');
            });
        });
     }
     }


     function waitForCurrentEntrySurface(manifest, options) {
     function ensureLoginGateAction(panel) {
         var era = getInitialNationsEra(manifest);
         var copy;
         var warmPack;
        var action;
 
        if (loginGateActionNode && loginGateActionNode.parentNode) return loginGateActionNode;
        loginGateActionNode = document.createElement('div');
        loginGateActionNode.className = 'boot-gate-login';
 
        copy = document.createElement('div');
         copy.className = 'boot-gate-login-copy';
        copy.textContent = 'SIGN IN TO ENTER THE WIKI.';


         options = options || {};
         action = document.createElement('a');
         warmPack = !!options.warmPack;
        action.className = 'boot-gate-login-action';
         action.href = buildLoginUrl();
        action.textContent = 'LOGIN';
        action.setAttribute('role', 'button');


         if (!isNationsPageSurface()) {
         loginGateActionNode.appendChild(copy);
            return waitForDecorationsReady({ warm: !!warmPack }).then(function () { return waitForAnimationFrames(1); });
         loginGateActionNode.appendChild(action);
        }
         panel.appendChild(loginGateActionNode);
         if (bootStatusNode) bootStatusNode.textContent = warmPack ? 'Hydrating current information surface from warm cache' : 'Preparing current information surface';
        return loginGateActionNode;
        if (bootDetailNode) bootDetailNode.textContent = 'waiting for nations ' + era + ' ready contract';
         return waitForNationsPanelReady(era)
            .then(function () {
                if (warmPack) {
                    if (bootDetailNode) bootDetailNode.textContent = 'attaching globe warm consumer';
                    return waitForNationsGlobeReady({ soft: true, timeoutMs: 450 });
                }
                if (bootDetailNode) bootDetailNode.textContent = 'waiting for globe ready contract';
                return waitForNationsGlobeReady();
            })
            .then(function () {
                if (bootDetailNode) bootDetailNode.textContent = 'waiting for decoration surface';
                return waitForDecorationsReady({ warm: warmPack });
            })
            .then(function () { return waitForAnimationFrames(warmPack ? 1 : 2); });
     }
     }


     function runDeferredEntryWarmups() {
     function showLoginGate() {
         var queue = deferredEntryWarmups.splice(0);
         var node = ensureBootScreen();
         if (!queue.length) return;
        var panel;
         window.setTimeout(function () {
        var title;
            var chain = Promise.resolve();
        var action;
            queue.forEach(function (job) {
 
                chain = chain.then(function () {
         if (!node || !requiresLoginGate()) return false;
                    try { return job(); }
        activateBootSurface();
                    catch (err) { return null; }
        loginGateLocked = true;
                }).catch(function () { return null; });
        node.classList.remove('is-complete');
        node.classList.add('is-active', 'is-login-required');
        node.setAttribute('role', 'dialog');
        node.setAttribute('aria-modal', 'true');
        node.setAttribute('aria-live', 'off');
 
        panel = node.querySelector('.boot-gate-panel');
         title = node.querySelector('.boot-gate-title');
        if (title) title.textContent = 'ACCOUNT AUTHENTICATION';
        if (bootStatusNode) bootStatusNode.textContent = 'LOGIN REQUIRED';
        if (bootDetailNode) bootDetailNode.textContent = '';
        if (bootProgressNode) bootProgressNode.textContent = '';
        if (bootFillNode) bootFillNode.style.width = '100%';
 
        action = panel ? ensureLoginGateAction(panel).querySelector('.boot-gate-login-action') : null;
        if (action) {
            action.href = buildLoginUrl();
            window.requestAnimationFrame(function () {
                try { action.focus({ preventScroll: true }); }
                catch (err) { try { action.focus(); } catch (ignore) {} }
             });
             });
         }, 1200);
         }
        return true;
     }
     }


     function runInitialLoad(options) {
     function hideBootScreen(options) {
         var done = 0;
         var node = bootNode || document.getElementById('boot-gate-screen');
        var manifestRef;
        var tasks;
        var bootOptions;
        var timedOut = false;
        var warmPack = false;
 
         options = options || {};
         options = options || {};
         bootStartTime = window.__BootGatePrelude && window.__BootGatePrelude.startTime ? window.__BootGatePrelude.startTime : now();
         if (loginGateLocked && requiresLoginGate() && !options.force) return false;
         activateBootSurface();
        if (!node) {
         ensureBootScreen();
            document.documentElement.classList.remove('boot-gate-active');
         updateBootProgress(0, 1, 'loading entry manifest');
            if (document.body) document.body.classList.remove('boot-gate-active');
            return true;
        }
         node.classList.add('is-complete');
         node.classList.remove('is-active');
         window.setTimeout(function () {
            if (node.parentNode) node.parentNode.removeChild(node);
            if (bootNode === node) bootNode = null;
            loginGateActionNode = null;
            document.documentElement.classList.remove('boot-gate-active');
            if (document.body) document.body.classList.remove('boot-gate-active');
        }, 240);
        return true;
    }


        manifestRef = loadManifest().then(function (manifest) {
    function collectFlagsFromNationPayload(payload) {
            var minDisplay;
        var files = [];
            var totalSteps;
        function add(value) {
            var maxBlockingMs;
             if (!value) return;
            var timeoutHandle;
             if (typeof value === 'string') {
            bootOptions = manifest.boot || {};
                 files.push(value);
            warmPack = isInitialPackWarm(manifest);
                 return;
            minDisplay = options.minDisplayMs || (warmPack ? (bootOptions.cachedMinDisplayMs || 220) : (bootOptions.minDisplayMs || 950));
            }
            tasks = flattenInitialTasks(manifest);
            if (typeof value === 'object') files.push(value.file || value.flag_file || value.flag || value.flag_title || '');
             if (!tasks.length) tasks = flattenInitialTasks(defaultManifest);
        }
             if (warmPack) tasks = flattenWarmInitialTasks(tasks);
        function scanItem(item) {
 
            if (!item || typeof item !== 'object') return;
            /*
            if (Array.isArray(item.flags)) item.flags.forEach(add);
            Full/half contract:
            add(item.flag_file || item.flag || item.flag_title || '');
            These manifest tasks only prepare data assets.  The gate is not allowed to open
        }
            until the current page surface has consumed that data and reported a real ready
        (payload && payload.continents || []).forEach(function (continent) {
            state below.  Do not reintroduce localStorage-only skip logic here; a cached
             (continent.regions || []).forEach(function (region) {
            version number is not the same as current-tab readiness.
                 (region.items || []).forEach(scanItem);
            */
            totalSteps = tasks.length + 4;
            updateBootProgress(0, totalSteps, warmPack ? 'hydrating warm entry cache' : 'starting entry packs');
 
            return new Promise(function (resolve) {
                 maxBlockingMs = Number(options.maxBlockingMs || bootOptions.maxBlockingMs || 30000);
                 timeoutHandle = window.setTimeout(function () {
                    timedOut = true;
                    resolve();
                }, maxBlockingMs);
 
                Promise.all(tasks.map(function (task) {
                    var label = task.label || task.id || task.type || 'entry task';
                    return BootPerf.measure('boot task: ' + label, { id: task.id || '', type: task.type || '', level: task.level || task.level === 0 ? task.level : '' }, function () {
                        return prepareTask(task, task.level);
                    }).catch(function () {
                        return null;
                    }).then(function () {
                        done += 1;
                        updateBootProgress(done, totalSteps, label);
                    });
                })).then(function () {
                    updateBootProgress(++done, totalSteps, 'loading subsystem scripts');
                    return BootPerf.measure('wait tracked subsystem scripts', { count: (window.EntryScriptLoads || []).length }, function () { return waitForTrackedScripts(); });
                }).then(function () {
                    updateBootProgress(++done, totalSteps, 'waiting for document shell');
                    return BootPerf.measure('wait document shell', {}, function () { return waitForDocumentSurface(); });
                }).then(function () {
                    updateBootProgress(++done, totalSteps, 'waiting for current entry surface');
                    return BootPerf.measure('wait current entry surface', { warmPack: warmPack }, function () { return waitForCurrentEntrySurface(manifest, { warmPack: warmPack }); });
                }).then(function () {
                    updateBootProgress(totalSteps, totalSteps, warmPack ? 'warm entry surface complete' : 'entry surface complete');
                    window.clearTimeout(timeoutHandle);
                    resolve();
                }).catch(function () {
                    window.clearTimeout(timeoutHandle);
                    resolve();
                });
             }).then(function () {
                var elapsed = now() - bootStartTime;
                var wait = Math.max(0, minDisplay - elapsed);
                return waitMs(wait);
            }).then(function () {
                 localStorage.setItem(READY_KEY, String(manifest.version));
                if (!timedOut) markInitialPackWarm(manifest, { warmSource: warmPack ? 'cache-hit' : 'cold-build' });
                if (bootStatusNode) bootStatusNode.textContent = timedOut ? 'Entry surface ready with deferred items' : (warmPack ? 'Entry surface ready from warm cache' : 'Entry surface ready');
                updateBootProgress(tasks.length + 4, tasks.length + 4, timedOut ? 'timeout: continuing deferred loading' : (warmPack ? 'warm cache complete' : 'complete'));
                BootPerf.mark('boot gate complete', { timedOut: timedOut, warmPack: warmPack, manifestVersion: manifest && manifest.version || '' });
                BootPerf.print();
                runDeferredEntryWarmups();
                window.setTimeout(hideBootScreen, 120);
                return manifest;
             });
             });
         });
         });
 
         return unique(files);
         return manifestRef;
     }
     }


     function startBoot(options) {
     function collectFlagsFromLinkMap(payload) {
         if (BOOT_EXCLUDED_PAGE && !(options && options.force)) {
         var files = [];
            hideBootScreen();
        var source = payload && payload.items ? payload.items : {};
            return Promise.resolve({ skipped: true, reason: 'developer-or-editing-page' });
        Object.keys(source || {}).forEach(function (key) {
        }
            var item = source[key];
        if (hasBootParam('0') && !(options && options.force)) return Promise.resolve(null);
            if (!item || typeof item !== 'object') return;
        if (bootStarted && bootPromise && !(options && options.force)) return bootPromise;
            files.push(item.flag_file || item.flag || item.flag_title || '');
        bootStarted = true;
         });
         bootPromise = runInitialLoad(options || {});
         return unique(files);
         return bootPromise;
     }
     }


     function resetBoot() {
     function prewarmImages(urls, limit) {
         localStorage.removeItem(READY_KEY);
         return window.EntryStore.preloadImages(urls, {
        bootStarted = false;
            limit: Number(limit) > 0 ? Number(limit) : 0,
         bootPromise = null;
            concurrency: 16,
            persistent: false
         });
     }
     }


     /*
     function getDecorationRuntime() {
    BootGate intentionally has no SPA hold/release API.
        return window.Decorations || window.CLBI_DECORATIONS || null;
    Initial boot is the only blocking phase; later SPA routes must consume prepared
     }
    EntryStore artifacts without reopening the loading surface.
     */


     function buildBootReport() {
     function waitForDecorationRuntime() {
         return {
         return new Promise(function (resolve) {
             build: BUILD_ID,
             var tries = 0;
             state: {
             function tick() {
                 started: bootStarted,
                 var runtime = getDecorationRuntime();
                excludedPage: BOOT_EXCLUDED_PAGE,
                 if (runtime) return resolve(runtime);
                 hasNode: !!((bootNode && bootNode.parentNode) || document.getElementById('boot-gate-screen')),
                 tries += 1;
                 htmlActive: !!(document.documentElement && document.documentElement.classList.contains('boot-gate-active')),
                 if (tries > 40) return resolve(null);
                 bodyActive: !!(document.body && document.body.classList.contains('boot-gate-active'))
                window.setTimeout(tick, 25);
            },
             }
            store: window.EntryStore && window.EntryStore.cacheInfo ? window.EntryStore.cacheInfo() : null,
             tick();
            bootPerf: window.BootPerf && typeof window.BootPerf.summary === 'function' ? window.BootPerf.summary() : null,
         });
            revisionManifest: window.RevisionManifest && typeof window.RevisionManifest.status === 'function' ? window.RevisionManifest.status() : null,
             nations: window.NationsPanel && typeof window.NationsPanel.diagnostics === 'function' ? window.NationsPanel.diagnostics() : null,
             decorations: window.Decorations && typeof window.Decorations.diagnostics === 'function' ? window.Decorations.diagnostics() : null
         };
     }
     }


     window.EntryLoader = window.EntryLoader || {
     function matchesDecorationEntry(entry, filter) {
         loadManifest: loadManifest,
        var era = String(filter && filter.era || '').trim();
         prepareTask: prepareTask,
         var page = String(filter && filter.page || '').trim();
         prepareNationsEra: prepareNationsEra,
         var entryPage = String(entry && entry.page || '').replace(/_/g, ' ').trim();
         prepareDecorationSet: prepareDecorationSet,
         if (!entry || typeof entry !== 'object') return false;
         prepareGlobeSharedAssets: prepareGlobeSharedAssets,
         if (page && entryPage && entryPage !== page) return false;
         prepareImageAssets: prepareImageAssets,
         if (era && String(entry.era || '').trim() && String(entry.era || '').trim() !== era) return false;
        prepareHtmlEntry: prepareHtmlEntry,
         return true;
        runInitialLoad: runInitialLoad
     }
     };


     function showBootPreview(options) {
     function prepareDecorationSet(task, level) {
         var node;
         var ref = task.ref || 'MediaWiki:Decorations.json';
        options = options || {};
         return window.EntryStore.fetchJsonRef(ref, { noStore: !!task.noStore }).then(function (registry) {
         /*
            var list = registry && Array.isArray(registry.decorations) ? registry.decorations : [];
        * Boot preview is a design/editing surface, not a real boot gate.
            var pixelRefs = [];
        * The earlier preview reused activateBootSurface(), which applied
            list.forEach(function (entry) {
        * html.boot-gate-active and hid the entire wiki shell, including
                var type = String(entry && entry.assetType || '').toLowerCase();
        * DevTools. That made it impossible to edit loading-screen
                var asset = String(entry && (entry.asset || entry.src) || '').trim();
        * decorations while previewing them. Keep the real first-entry gate
                if (!asset) return;
        * full-screen, but make preview a small non-blocking surface below the
                if (type !== 'pixel-json' && !/\.json(?:[?#].*)?$/i.test(asset)) return;
        * DevTools z-index so the owner can keep using the editor.
                if (!matchesDecorationEntry(entry, task)) return;
        */
                pixelRefs.push(asset);
        node = ensureBootScreen({ preview: true });
            });
        if (!node) return null;
            if (level !== 'full' || task.preparePixels === false || !pixelRefs.length) return registry;
        node.classList.add('is-preview');
            return waitForDecorationRuntime().then(function (runtime) {
        if (bootStatusNode) bootStatusNode.textContent = options.status || 'Loading screen preview';
                if (!runtime || typeof runtime.preparePixelCanvas !== 'function') {
        updateBootProgress(Number(options.progress || 64), 100, options.detail || 'preview mode: no entry tasks are running');
                    return Promise.all(pixelRefs.map(function (pixelRef) {
        try {
                        return window.EntryStore.fetchJsonRef(pixelRef).catch(function () { return null; });
            if (window.Decorations && typeof window.Decorations.sync === 'function') window.Decorations.sync();
                    })).then(function () { return registry; });
        } catch (err) {}
                }
         return node;
                return Promise.all(pixelRefs.map(function (pixelRef) {
                    return runtime.preparePixelCanvas(pixelRef).catch(function () { return null; });
                })).then(function () { return registry; });
            });
         });
     }
     }


     window.BootGate = window.BootGate || {
     function prepareNationsEra(task, level) {
        version: BUILD_ID,
        var era = String(task.era || '1950');
        start: startBoot,
        var listRef = task.listRef || ('MediaWiki:' + era + '_Nation_List.json');
        show: function (options) {
         var linkRef = task.linkMapRef || ('MediaWiki:' + era + '_Nation_Link_Map.json');
            options = options || {};
        var jsonPhaseId = BootPerf.start('nations-era json fetch', { era: era, level: level, listRef: listRef, linkRef: linkRef });
            options.force = true;
        var listPromise = window.EntryStore.fetchJsonRef(listRef).catch(function () { return null; });
            resetBoot();
        var linkPromise = window.EntryStore.fetchJsonRef(linkRef).catch(function () { return null; });
            return startBoot(options);
         },
        reset: resetBoot,
        hide: hideBootScreen,
        preview: showBootPreview,
        report: buildBootReport,
        state: function () {
            return {
                started: bootStarted,
                hasNode: !!((bootNode && bootNode.parentNode) || document.getElementById('boot-gate-screen')),
                htmlActive: !!(document.documentElement && document.documentElement.classList.contains('boot-gate-active')),
                bodyActive: !!(document.body && document.body.classList.contains('boot-gate-active')),
                prelude: !!window.__BootGatePrelude,
                store: window.EntryStore && window.EntryStore.cacheInfo ? window.EntryStore.cacheInfo() : null,
                revisionManifest: window.RevisionManifest && typeof window.RevisionManifest.status === 'function' ? window.RevisionManifest.status() : null
            };
        }
    };


    function prime() {
        return Promise.all([listPromise, linkPromise]).then(function (results) {
        if (BOOT_EXCLUDED_PAGE) {
             BootPerf.end(jsonPhaseId, { ok: true });
             hideBootScreen();
             var files;
             return;
            var flagUrls;
        }
            if (level !== 'full' && level !== 'warm') return results;
        activateBootSurface();
            files = unique(collectFlagsFromNationPayload(results[0]).concat(collectFlagsFromLinkMap(results[1])));
        waitForBody().then(function () {
             return BootPerf.measure('nations-era flag url resolve', { era: era, count: files.length }, function () {
             ensureBootScreen();
                return window.EntryStore.resolveFlagUrls(files);
            return waitForAnimationFrames(1);
            }).then(function () {
        }).then(function () {
                if (level === 'warm') {
            startBoot();
                    /*
        });
                    * Warm tab contract — 20260708.
    }
                    *
 
                    * A previous tab already stored the same-revision flag URL and image
    waitForBody().then(function () {
                    * blobs.  Re-decoding every flag here makes the second tab feel as slow
        if (!BOOT_EXCLUDED_PAGE) ensureBootScreen();
                    * as the first one.  In warm mode we only hydrate the flag URL table from
        else hideBootScreen();
                    * EntryCache so NationsPanel can render stable URLs immediately; image
    });
                    * decode remains demand-driven by the live panel instead of blocking the
    window.setTimeout(prime, 0);
                    * boot screen.
})(window, document, window.mediaWiki || window.mw);
                    */
 
                    return results;
 
                }
loadClbiRawScript('MediaWiki:DevTools.js');
                flagUrls = files.map(function (file) { return window.EntryStore.getFlagUrl(file); }).filter(Boolean);
loadClbiRawScript('MediaWiki:CategoryNav.js');
                /*
loadClbiRawScript('MediaWiki:NationsPanel.js');
                * 20260710: Flag URLs are part of the entry contract, but decoding every
loadClbiRawScript('MediaWiki:NationsGlobe.js');
                * flag image is not.  The 1950 list has more than 150 flags, and waiting
                * for all image decodes kept the public boot gate open for several seconds.
                * Keep revision-aware URLs ready for the panel, then let the browser load
                * the actual images from the live DOM. A low-priority background warm-up
                * may run after the boot gate has opened, so it never competes with the
                * current entry surface or the globe ready contract.
                */
                BootPerf.mark('nations-era flag image prewarm skipped', {
                    era: era,
                    count: flagUrls.length,
                    limit: task.flagLimit == null ? 0 : Number(task.flagLimit),
                    reason: 'deferred-after-boot-gate'
                });
                if (flagUrls.length && task.deferFlagImages !== false) {
                    deferredEntryWarmups.push(function () {
                        return BootPerf.measure('deferred nations-era flag image prewarm', {
                            era: era,
                            count: flagUrls.length,
                            limit: task.flagLimit == null ? 0 : Number(task.flagLimit)
                        }, function () {
                            return prewarmImages(flagUrls, task.flagLimit == null ? 0 : Number(task.flagLimit));
                        });
                    });
                }
                return results;
            });
        });
    }


/* CLBI safety guard: adaptive reset functions must exist before shell metric callbacks run. */
    function prepareGlobeSharedAssets(task, level) {
function resetLeftRecentAdaptiveState() {
        var refs = Array.isArray(task && task.assets) ? task.assets : [];
    var list = document.getElementById('clbi-left-recent-list');
        if (!refs.length) return Promise.resolve(null);
    var newsBox = list ? list.closest('.clbi-left-news-box') : null;
        return BootPerf.measure('globe-shared url resolve', { count: refs.length, level: level }, function () {
    var items = list ? Array.prototype.slice.call(list.querySelectorAll('.news-recent-item')) : [];
            return Promise.all(refs.map(function (ref) {
 
                return window.EntryStore.resolveFileUrl(ref).catch(function () { return ''; });
    if (newsBox) {
            }));
        newsBox.classList.remove('is-adaptive-constrained');
        }).then(function (urls) {
        newsBox.style.removeProperty('--adaptive-news-h');
            urls = urls.filter(Boolean);
            /*
            * Globe shared assets are heavy 6K texture sources.  The live
            * NationsGlobe ready contract already waits for the actual Three.js
            * texture path, so decoding the same images here only duplicates work
            * and holds the boot screen. Keep revision-aware URL resolution in the
            * entry ledger, but make the image warm-up non-blocking.
            */
            BootPerf.mark('globe-shared image prewarm skipped', {
                count: urls.length,
                level: level,
                reason: 'deferred-to-nations-globe-ready-contract'
            });
            window.setTimeout(function () {
                if (!window.EntryStore || typeof window.EntryStore.preloadImages !== 'function') return;
                window.EntryStore.preloadImages(urls, {
                    limit: task.imageLimit == null ? 0 : Number(task.imageLimit),
                    concurrency: Math.max(1, Math.min(16, Number(task.concurrency) || 4)),
                    persistent: false
                }).catch(function () {});
            }, 0);
            return urls;
        });
     }
     }


     if (list) {
     function prepareImageAssets(task, level) {
         list.classList.remove('is-adaptive-faded');
         var refs = Array.isArray(task && task.assets) ? task.assets : [];
         list.removeAttribute('data-adaptive-limit');
        var full = level === 'full';
         list.style.removeProperty('--adaptive-recent-h');
        var exposeAs = String(task && task.exposeAs || '').trim();
         if (!refs.length) return Promise.resolve(null);
        return BootPerf.measure('image-assets url resolve', { count: refs.length, exposeAs: exposeAs, level: level }, function () {
            return Promise.all(refs.map(function (ref) {
                return window.EntryStore.resolveFileUrl(ref).catch(function () { return ''; });
            }));
         }).then(function (urls) {
            urls = urls.filter(Boolean);
            if (exposeAs === 'nationsGlobeLoadingGif') {
                window.NationsGlobeLoadingGifRef = refs[0] || '';
                window.NationsGlobeLoadingGifUrl = urls[0] || '';
                window.NationsGlobeLoadingGifFile = 'Gfx-vhs-glitch-001.gif';
            }
            if (!full) return urls;
            return BootPerf.measure('image-assets image prewarm', { count: urls.length, exposeAs: exposeAs, limit: task.imageLimit == null ? 0 : Number(task.imageLimit), concurrency: Math.max(1, Math.min(16, Number(task.concurrency) || 2)) }, function () {
                return window.EntryStore.preloadImages(urls, {
                    limit: task.imageLimit == null ? 0 : Number(task.imageLimit),
                    concurrency: Math.max(1, Math.min(16, Number(task.concurrency) || 2))
                });
            }).then(function () { return urls; });
        });
     }
     }


     items.forEach(function (item) {
     function prepareHtmlEntry(task) {
         item.classList.remove('is-adaptive-hidden');
         var url = String(task && (task.url || task.ref) || '').trim();
    });
        if (!url) return Promise.resolve(null);
}
        return window.EntryStore.fetchTextUrl(url, { noStore: !!task.noStore, resourceRef: task.resourceRef || task.ref || task.title || '' });
    }


function resetLeftBillboardAdaptiveState() {
    function prepareTask(task, defaultLevel) {
    var box = document.querySelector('#clbi-left-sidebar .left-billboard-box');
        var level = String(task && (task.level || defaultLevel) || 'half').toLowerCase();
        var type = String(task && task.type || '').toLowerCase();
        if (!task || typeof task !== 'object') return Promise.resolve(null);
        if (type === 'html') return prepareHtmlEntry(task);
        if (type === 'json') return window.EntryStore.fetchJsonRef(task.ref, { noStore: !!task.noStore });
        if (type === 'pixel-json') {
            return waitForDecorationRuntime().then(function (runtime) {
                if (level === 'full' && runtime && typeof runtime.preparePixelCanvas === 'function') {
                    return runtime.preparePixelCanvas(task.ref || task.asset);
                }
                return window.EntryStore.fetchJsonRef(task.ref || task.asset);
            });
        }
        if (type === 'decorations') return prepareDecorationSet(task, level);
        if (type === 'nations-era') return prepareNationsEra(task, level);
        if (type === 'globe-shared-assets') return prepareGlobeSharedAssets(task, level);
        if (type === 'image-assets') return prepareImageAssets(task, level);
        return Promise.resolve(null);
    }


     if (!box) return;
     function loadManifest() {
 
        return BootPerf.measure('revision manifest load', {}, function () {
    box.classList.remove('is-left-ad-title-only', 'is-left-ad-extended');
            return (window.RevisionManifest && typeof window.RevisionManifest.load === 'function' ? window.RevisionManifest.load() : Promise.resolve(null));
    box.style.removeProperty('--left-billboard-h');
        }).then(function () {
    box.style.removeProperty('--left-billboard-finish-h');
            return BootPerf.measure('entry manifest json fetch', { ref: MANIFEST_TITLE }, function () {
}
                return window.EntryStore.fetchJsonRef(MANIFEST_TITLE, { noStore: false });
            });
        }).then(function (manifest) {
            if (!manifest || typeof manifest !== 'object' || !manifest.version) {
                BootPerf.mark('entry manifest fallback', { reason: 'invalid manifest' });
                return defaultManifest;
            }
            BootPerf.mark('entry manifest ready', { version: manifest.version });
            return manifest;
        }).catch(function (err) {
            BootPerf.mark('entry manifest fallback', { reason: err && (err.message || String(err)) || 'load failed' });
            return defaultManifest;
        });
    }


window.resetLeftRecentAdaptiveState = resetLeftRecentAdaptiveState;
    function flattenInitialTasks(manifest) {
window.resetLeftBillboardAdaptiveState = resetLeftBillboardAdaptiveState;
        var initial = manifest && manifest.initial ? manifest.initial : {};
        var full = Array.isArray(initial.full) ? initial.full : [];
        var half = Array.isArray(initial.half) ? initial.half : [];
        var tasks = [];
        full.forEach(function (task) {
            task = Object.assign({}, task);
            task.level = task.level || 'full';
            task.blocking = true;
            tasks.push(task);
        });
        half.forEach(function (task) {
            task = Object.assign({}, task);
            task.level = task.level || 'half';
            task.blocking = false;
            tasks.push(task);
        });
        return tasks;
    }


loadClbiRawScript('MediaWiki:AnecdoteViewer.js');


    function flattenWarmInitialTasks(tasks) {
        var warmed = [];
        (tasks || []).forEach(function (task) {
            var copy;
            var type = String(task && task.type || '').toLowerCase();
            if (!task || task.blocking === false) return;


(function () {
            /*
    'use strict';
            * Warm boot fast path — 20260708.
 
            *
    var SYSTEM_TITLE_NAMESPACES = {
            * Cold boot intentionally performs the expensive work: resolving every flag,
        '-1': true,
            * downloading/decoding images, preparing decoration canvases, and warming shared
        '4': true,
            * globe textures.  Once a tab has certified the pack against the current revision
        '5': true,
            * manifest, later tabs must not replay that full workload.  They only hydrate the
        '6': true,
            * small tables needed by the live page and let already-cached image/blob data be
        '7': true,
            * consumed on demand.  This is the missing layer that made second tabs feel almost
        '8': true,
            * as slow as first tabs even though the string checks were true.
        '9': true,
            */
        '10': true,
            if (type === 'globe-shared-assets') return;
        '11': true,
            if (type === 'image-assets') return;
        '12': true,
            copy = Object.assign({}, task);
        '13': true,
            if (type === 'nations-era') copy.level = 'warm';
         '14': true,
            else if (type === 'decorations') {
         '15': true,
                copy.level = 'half';
         '828': true,
                copy.preparePixels = false;
         '829': true
            } else if (type === 'pixel-json') copy.level = 'half';
     };
            warmed.push(copy);
         });
         return warmed;
    }
 
    function currentInitialPackKey(manifest) {
         var version = manifest && manifest.version ? String(manifest.version) : BUILD_ID;
         return 'initial-entry-full:' + version;
     }


     function normalizePageNameForShell(value) {
     function currentInitialPackToken(manifest) {
         return String(value || '')
         var version = manifest && manifest.version ? String(manifest.version) : BUILD_ID;
            .split('?')[0]
        var revToken = window.RevisionManifest && typeof window.RevisionManifest.manifestToken === 'function' ? window.RevisionManifest.manifestToken(version) : version;
            .replace(/^\/index\.php\//, '')
        return revToken;
            .replace(/_/g, ' ')
            .trim();
     }
     }


     function readCurrentPageNameForShell() {
     function isInitialPackWarm(manifest) {
         var pageName = mw.config.get('wgPageName') || '';
         var key = currentInitialPackKey(manifest);
        var token = currentInitialPackToken(manifest);
        return !!(window.EntryCache && typeof window.EntryCache.packReady === 'function' && window.EntryCache.packReady(key, token));
    }


         if (pageName) {
    function markInitialPackWarm(manifest, meta) {
             return normalizePageNameForShell(pageName);
        var key = currentInitialPackKey(manifest);
        var token = currentInitialPackToken(manifest);
         if (window.EntryCache && typeof window.EntryCache.setPackReady === 'function') {
             window.EntryCache.setPackReady(key, token, Object.assign({ manifestVersion: manifest && manifest.version || BUILD_ID }, meta || {}));
         }
         }
        return normalizePageNameForShell(window.location.pathname || '');
     }
     }


    function isAnecdoteNamespaceForShell() {
        var namespaceNumber = Number(mw.config.get('wgNamespaceNumber'));
        var canonicalNamespace = String(mw.config.get('wgCanonicalNamespace') || '').toLowerCase();
        var pageName = readCurrentPageNameForShell();


         return namespaceNumber === 3000 ||
    function waitMs(ms) {
            canonicalNamespace === 'anecdote' ||
         return new Promise(function (resolve) { window.setTimeout(resolve, Math.max(0, ms || 0)); });
            /^(anecdote|에넥도트):/i.test(pageName);
     }
     }


     function isBackendOrSystemPageForShell() {
     function waitForBody() {
         var namespaceNumber = Number(mw.config.get('wgNamespaceNumber'));
         if (document.body) return Promise.resolve(document.body);
        var action = String(mw.config.get('wgAction') || 'view').toLowerCase();
         return new Promise(function (resolve) {
         var contentModel = String(mw.config.get('wgPageContentModel') || '').toLowerCase();
             function tick() {
        var pageName = readCurrentPageNameForShell();
                if (document.body) return resolve(document.body);
        var lowerPageName = pageName.toLowerCase();
                window.setTimeout(tick, 10);
 
             }
        if (action && action !== 'view') {
            tick();
             return true;
         });
        }
 
        if (pageName === '대문') {
            return false;
        }
 
        if (SYSTEM_TITLE_NAMESPACES[String(namespaceNumber)]) {
            return true;
        }
 
        if (contentModel === 'css' || contentModel === 'javascript' || contentModel === 'json' || contentModel === 'sanitized-css') {
            return true;
        }
 
        if (/\.(css|js|json)$/i.test(pageName)) {
             return true;
        }
 
        if (/^(mediawiki|미디어위키|special|특수):/i.test(pageName)) {
            return true;
         }
 
        return false;
     }
     }


     function isMediaWikiSystemAssetPageForShell() {
     function waitForDomReady() {
         var namespaceNumber = Number(mw.config.get('wgNamespaceNumber'));
         if (document.readyState !== 'loading') return Promise.resolve();
        var pageName = readCurrentPageNameForShell();
         return new Promise(function (resolve) {
        var contentModel = String(mw.config.get('wgPageContentModel') || '').toLowerCase();
            document.addEventListener('DOMContentLoaded', resolve, { once: true });
 
        });
         return namespaceNumber === 8 &&
            (/\.(css|js)$/i.test(pageName) || contentModel === 'css' || contentModel === 'javascript' || contentModel === 'sanitized-css');
     }
     }


 
     function waitForAnimationFrames(count) {
    var systemDocRawFetchToken = 0;
         count = Math.max(1, Math.round(count || 1));
 
        return new Promise(function (resolve) {
     function cleanupLegacySystemDocCodeMutationsForShell() {
             function next(left) {
         document.querySelectorAll('.clbi-system-doc-codepane').forEach(function (pane) {
                if (left <= 0) return resolve();
             var parent;
                window.requestAnimationFrame(function () { next(left - 1); });
 
            if (!pane || !pane.parentNode) return;
 
            parent = pane.parentNode;
            while (pane.firstChild) {
                parent.insertBefore(pane.firstChild, pane);
             }
             }
             parent.removeChild(pane);
             next(count);
         });
         });
    }


         document.querySelectorAll('.clbi-system-doc-codebox').forEach(function (node) {
    function waitUntil(predicate, options) {
             node.classList.remove('clbi-system-doc-codebox');
        var started = now();
            node.removeAttribute('data-clbi-system-doc-codebox');
         var timeout = options && options.timeoutMs ? options.timeoutMs : 8000;
             node.removeAttribute('style');
        var interval = options && options.intervalMs ? options.intervalMs : 60;
        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 (now() - started >= timeout) return resolve({ ok: false, timeout: true });
                window.setTimeout(tick, interval);
             }
            tick();
         });
         });
     }
     }


     function getSystemDocOutputForShell() {
     function waitForTrackedScripts() {
         return document.querySelector('.liberty-content-main .mw-parser-output');
         var list = (window.EntryScriptLoads || []).slice();
        if (!list.length) return Promise.resolve([]);
        return Promise.all(list.map(function (promise) {
            return Promise.resolve(promise).catch(function (err) { return { ok: false, error: err }; });
        }));
     }
     }


     function findSystemDocSourceNodeForShell() {
     function waitForDocumentSurface() {
         var output = getSystemDocOutputForShell();
         return waitForDomReady().then(function () {
         var children;
            return waitForBody();
         var preferred;
         }).then(function () {
            return waitUntil(function () {
                return document.querySelector('.content-wrapper') && document.querySelector('.liberty-content-main');
            }, { timeoutMs: 8000, intervalMs: 50 });
         });
    }


         if (!output) return null;
    function isNationsPageSurface() {
         var page = String(mw && mw.config ? (mw.config.get('wgPageName') || mw.config.get('wgTitle') || '') : '');
        return !!document.querySelector('.clbi-nations-panel-stack') || /(?:^|[_ ])시대(?:$|[_ ])/.test(page) || /(?:^|[_ ])Era(?:$|[_ ])/i.test(page);
    }


         children = Array.prototype.slice.call(output.children || [])
    function getInitialNationsEra(manifest) {
            .filter(function (el) {
         var tasks = flattenInitialTasks(manifest);
                return el && el.nodeType === 1 &&
        var i;
                    el.id !== 'clbi-system-doc-indicator-row' &&
        for (i = 0; i < tasks.length; i += 1) {
                    el.id !== 'clbi-system-source-viewer' &&
            if (tasks[i] && tasks[i].type === 'nations-era' && String(tasks[i].level || '').toLowerCase() === 'full') {
                    !el.classList.contains('catlinks') &&
                return String(tasks[i].era || '1950');
                    (el.textContent || '').trim().length > 200;
             }
             });
         }
 
         return '1950';
        preferred = children.filter(function (el) {
            return el.matches && el.matches('.mw-highlight, .mw-code, pre');
         })[0];
 
         return preferred || children.sort(function (a, b) {
            return (b.textContent || '').trim().length - (a.textContent || '').trim().length;
        })[0] || null;
     }
     }


     function getSystemDocRawUrlForShell() {
     function waitForNationsPanelReady(era) {
         var title = mw.config.get('wgPageName') || readCurrentPageNameForShell();
         return BootPerf.measure('wait nations panel api', { era: era }, function () {
        var url;
            return waitUntil(function () { return window.NationsPanel && typeof window.NationsPanel.whenEraReady === 'function'; }, {
 
                timeoutMs: 8000,
        if (window.mw && mw.util && typeof mw.util.getUrl === 'function') {
                intervalMs: 50
             url = mw.util.getUrl(title, {
            });
                 action: 'raw',
        }).then(function (result) {
                 ctype: 'text/plain'
            if (!result.ok || !window.NationsPanel || typeof window.NationsPanel.whenEraReady !== 'function') return null;
             return BootPerf.measure('wait nations panel era ready', { era: era }, function () {
                 return window.NationsPanel.whenEraReady(era, { timeoutMs: 15000 }).catch(function () { return null; });
            });
        }).then(function () {
            return BootPerf.measure('wait nations panel dom ready', { era: era }, function () {
                 return waitUntil(function () {
                    var panel = document.querySelector('.clbi-nations-era-content[data-era-content="' + era + '"] .clbi-nations-tabpanel[data-nation-list-source="1"]') ||
                        document.querySelector('.clbi-nations-tabpanel[data-nation-list-source="1"]');
                    return panel && panel.classList.contains('clbi-nations-list-json-ready') && panel.getAttribute('data-nation-list-year-loaded') === String(era);
                }, { timeoutMs: 15000, intervalMs: 80 });
             });
             });
        } else {
            url = '/index.php?title=' + encodeURIComponent(title) + '&action=raw&ctype=text/plain';
        }
        if (window.RevisionManifest && typeof window.RevisionManifest.addRevisionParam === 'function') {
            url = window.RevisionManifest.addRevisionParam(url, title);
        }
        return url;
    }
    function removeSystemDocSourceViewerForShell() {
        var viewer = document.getElementById('clbi-system-source-viewer');
        if (viewer && viewer.parentNode) {
            viewer.parentNode.removeChild(viewer);
        }
        document.querySelectorAll('.clbi-system-original-source-hidden').forEach(function (node) {
            node.classList.remove('clbi-system-original-source-hidden');
            node.removeAttribute('data-clbi-system-source-hidden');
            node.style.removeProperty('display');
         });
         });
        cleanupLegacySystemDocCodeMutationsForShell();
     }
     }


     function ensureSystemDocSourceViewerForShell() {
     function waitForNationsGlobeReady(options) {
         var output = getSystemDocOutputForShell();
         var globe = document.querySelector('.clbi-nations-globe-window[data-nations-globe], [data-nations-globe="real-world"]');
         var source;
         var soft;
        var viewer;
         var timeout;
         var fallbackText;


         if (!output || !isMediaWikiSystemAssetPageForShell()) return null;
         options = options || {};
        soft = !!options.soft;
        timeout = Number(options.timeoutMs || (soft ? 450 : 30000));


         cleanupLegacySystemDocCodeMutationsForShell();
         if (!globe) {
            BootPerf.mark('wait nations globe skipped', { reason: 'no globe node', soft: soft });
            return Promise.resolve(null);
        }


         source = findSystemDocSourceNodeForShell();
         return BootPerf.measure(soft ? 'wait nations globe warm attach' : 'wait nations globe ready contract', { soft: soft, timeoutMs: timeout }, function () {
        if (!source) return null;
            if (soft) {
                /*
                Warm-tab policy:
                Raw globe images are already revision-checked and blob-cached by EntryCache,
                but WebGL scene creation, GPU upload, topojson parsing, and per-tab Three.js
                objects cannot be shared across browser tabs.  Waiting for the full is-ready
                contract here makes a warm tab slower than the first tab.  In warm mode the
                initial gate only waits until the globe consumer has been attached, then the
                globe finishes its own per-tab hydrate without holding the whole page hostage.
                */
                return waitUntil(function () {
                    var node = document.querySelector('.clbi-nations-globe-window[data-nations-globe], [data-nations-globe="real-world"]');
                    if (!node) return false;
                    return node.getAttribute('data-nations-globe-ready') === '1' ||
                        !!node.CLBI_NationsGlobeInstance ||
                        !!node.querySelector('.clbi-nations-globe-stage') ||
                        node.classList.contains('is-ready') ||
                        node.classList.contains('has-error');
                }, { timeoutMs: timeout, intervalMs: 40 });
            }


        viewer = document.getElementById('clbi-system-source-viewer');
            return waitUntil(function () {
                var node = document.querySelector('.clbi-nations-globe-window[data-nations-globe], [data-nations-globe="real-world"]');
                return node && (node.classList.contains('is-ready') || node.classList.contains('has-error'));
            }, { timeoutMs: timeout, intervalMs: 100 });
        });
    }


         if (!viewer) {
    function waitForDecorationsReady(options) {
            viewer = document.createElement('pre');
        options = options || {};
            viewer.id = 'clbi-system-source-viewer';
         return waitForDecorationRuntime().then(function (runtime) {
             viewer.className = 'clbi-system-source-viewer';
            if (!runtime) return null;
             output.appendChild(viewer);
            if (options.warm) {
         }
                /*
                * Warm tab contract — 20260708.
                *
                * The decoration registry and pixel canvases were prepared by the cold tab.
                * Calling reload() here re-reads and re-syncs the decoration layer during the
                * boot gate, which is visible as a pointless delay.  A warm tab only needs the
                * current visibility pass so boot can release quickly and editing/preview tools
                * remain responsive.
                */
                try {
                    if (typeof runtime.updateVisibility === 'function') runtime.updateVisibility(document);
                    else if (typeof runtime.sync === 'function') runtime.sync();
                } catch (err) {}
                return waitForAnimationFrames(1);
             }
            if (typeof runtime.reload !== 'function') return null;
             return runtime.reload().catch(function () { return null; }).then(function () {
                return waitForAnimationFrames(2);
            });
         });
    }


         fallbackText = source.textContent || '';
    function waitForCurrentEntrySurface(manifest, options) {
         var era = getInitialNationsEra(manifest);
        var warmPack;


         if (!viewer.textContent && fallbackText) {
         options = options || {};
            viewer.textContent = fallbackText;
        warmPack = !!options.warmPack;
        }


         source.classList.add('clbi-system-original-source-hidden');
         if (!isNationsPageSurface()) {
         source.setAttribute('data-clbi-system-source-hidden', 'true');
            return waitForDecorationsReady({ warm: !!warmPack }).then(function () { return waitForAnimationFrames(1); });
         source.style.setProperty('display', 'none', 'important');
        }
        if (bootStatusNode) bootStatusNode.textContent = warmPack ? 'Hydrating current information surface from warm cache' : 'Preparing current information surface';
         if (bootDetailNode) bootDetailNode.textContent = 'waiting for era ' + era + ' ready contract';
         return waitForNationsPanelReady(era)
            .then(function () {
                if (warmPack) {
                    if (bootDetailNode) bootDetailNode.textContent = 'attaching globe warm consumer';
                    return waitForNationsGlobeReady({ soft: true, timeoutMs: 450 });
                }
                if (bootDetailNode) bootDetailNode.textContent = 'waiting for globe ready contract';
                return waitForNationsGlobeReady();
            })
            .then(function () {
                if (bootDetailNode) bootDetailNode.textContent = 'waiting for decoration surface';
                return waitForDecorationsReady({ warm: warmPack });
            })
            .then(function () { return waitForAnimationFrames(warmPack ? 1 : 2); });
    }


         return viewer;
    function runDeferredEntryWarmups() {
         var queue = deferredEntryWarmups.splice(0);
        if (!queue.length) return;
        window.setTimeout(function () {
            var chain = Promise.resolve();
            queue.forEach(function (job) {
                chain = chain.then(function () {
                    try { return job(); }
                    catch (err) { return null; }
                }).catch(function () { return null; });
            });
        }, 1200);
     }
     }


     function renderSystemDocSourceViewerForShell() {
     function runInitialLoad(options) {
         var viewer;
         var done = 0;
         var pageName;
         var manifestRef;
         var token;
         var tasks;
         var currentScrollTop;
         var bootOptions;
        var timedOut = false;
        var warmPack = false;


         if (!isMediaWikiSystemAssetPageForShell()) return;
         options = options || {};
        bootStartTime = window.__BootGatePrelude && window.__BootGatePrelude.startTime ? window.__BootGatePrelude.startTime : now();
        activateBootSurface();
        ensureBootScreen();
        updateBootProgress(0, 1, 'loading entry manifest');


         pageName = String(mw.config.get('wgPageName') || readCurrentPageNameForShell());
         manifestRef = loadManifest().then(function (manifest) {
        viewer = document.getElementById('clbi-system-source-viewer');
            var minDisplay;
            var totalSteps;
            var maxBlockingMs;
            var timeoutHandle;
            bootOptions = manifest.boot || {};
            warmPack = isInitialPackWarm(manifest);
            minDisplay = options.minDisplayMs || (warmPack ? (bootOptions.cachedMinDisplayMs || 220) : (bootOptions.minDisplayMs || 950));
            tasks = flattenInitialTasks(manifest);
            if (!tasks.length) tasks = flattenInitialTasks(defaultManifest);
            if (warmPack) tasks = flattenWarmInitialTasks(tasks);


        /*
            /*
        시스템 문서 뷰어가 이미 만들어져 있고 raw 원문도 로드된 상태라면
            Full/half contract:
        다시 source 탐색/숨김/스타일 재적용을 하지 않는다.
            These manifest tasks only prepare data assets. The gate is not allowed to open
        DevTools Elements 패널에서 body가 계속 파랗게 깜빡이던 원인은
            until the current page surface has consumed that data and reported a real ready
        MutationObserver가 이 재적용을 반복해서 DOM attribute mutation을 만들었기 때문이다.
            state below.  Do not reintroduce localStorage-only skip logic here; a cached
        */
            version number is not the same as current-tab readiness.
        if (
            */
             viewer &&
             totalSteps = tasks.length + 4;
            viewer.getAttribute('data-clbi-raw-title') === pageName &&
             updateBootProgress(0, totalSteps, warmPack ? 'hydrating warm entry cache' : 'starting entry packs');
             viewer.getAttribute('data-clbi-raw-loaded') === '1'
        ) {
            return;
        }


        viewer = ensureSystemDocSourceViewerForShell();
            return new Promise(function (resolve) {
        if (!viewer) return;
                maxBlockingMs = Number(options.maxBlockingMs || bootOptions.maxBlockingMs || 30000);
                timeoutHandle = window.setTimeout(function () {
                    timedOut = true;
                    resolve();
                }, maxBlockingMs);


        currentScrollTop = viewer.scrollTop || 0;
                Promise.all(tasks.map(function (task) {
        viewer.setAttribute('data-clbi-raw-title', pageName);
                    var label = task.label || task.id || task.type || 'entry task';
        token = ++systemDocRawFetchToken;
                    return BootPerf.measure('boot task: ' + label, { id: task.id || '', type: task.type || '', level: task.level || task.level === 0 ? task.level : '' }, function () {
 
                        return prepareTask(task, task.level);
        fetch(getSystemDocRawUrlForShell(), { credentials: 'same-origin' })
                    }).catch(function () {
            .then(function (res) {
                        return null;
                 if (!res.ok) throw new Error('raw fetch failed ' + res.status);
                    }).then(function () {
                 return res.text();
                        done += 1;
            })
                        updateBootProgress(done, totalSteps, label);
             .then(function (text) {
                    });
                 if (token !== systemDocRawFetchToken) return;
                })).then(function () {
 
                    updateBootProgress(++done, totalSteps, 'loading subsystem scripts');
                 currentScrollTop = viewer.scrollTop || currentScrollTop || 0;
                    return BootPerf.measure('wait tracked subsystem scripts', { count: (window.EntryScriptLoads || []).length }, function () { return waitForTrackedScripts(); });
 
                }).then(function () {
                 if (text && viewer.textContent !== text) {
                    updateBootProgress(++done, totalSteps, 'waiting for document shell');
                    viewer.textContent = text;
                    return BootPerf.measure('wait document shell', {}, function () { return waitForDocumentSurface(); });
                 }
                }).then(function () {
 
                    updateBootProgress(++done, totalSteps, 'waiting for current entry surface');
                 viewer.setAttribute('data-clbi-raw-loaded', '1');
                    return BootPerf.measure('wait current entry surface', { warmPack: warmPack }, function () { return waitForCurrentEntrySurface(manifest, { warmPack: warmPack }); });
                 viewer.scrollTop = currentScrollTop;
                 }).then(function () {
            })
                    updateBootProgress(totalSteps, totalSteps, warmPack ? 'warm entry surface complete' : 'entry surface complete');
            .catch(function () {
                    window.clearTimeout(timeoutHandle);
                 viewer.setAttribute('data-clbi-raw-loaded', '0');
                    resolve();
                 }).catch(function () {
                    window.clearTimeout(timeoutHandle);
                    resolve();
                });
             }).then(function () {
                 var elapsed = now() - bootStartTime;
                var wait = Math.max(0, minDisplay - elapsed);
                return waitMs(wait);
            }).then(function () {
                 localStorage.setItem(READY_KEY, String(manifest.version));
                 if (!timedOut) markInitialPackWarm(manifest, { warmSource: warmPack ? 'cache-hit' : 'cold-build' });
                if (bootStatusNode) bootStatusNode.textContent = timedOut ? 'Entry surface ready with deferred items' : (warmPack ? 'Entry surface ready from warm cache' : 'Entry surface ready');
                 updateBootProgress(tasks.length + 4, tasks.length + 4, timedOut ? 'timeout: continuing deferred loading' : (warmPack ? 'warm cache complete' : 'complete'));
                 BootPerf.mark('boot gate complete', { timedOut: timedOut, warmPack: warmPack, manifestVersion: manifest && manifest.version || '' });
                 BootPerf.print();
                runDeferredEntryWarmups();
                window.setTimeout(function () {
                    if (requiresLoginGate()) showLoginGate();
                    else hideBootScreen();
                 }, 120);
                return manifest;
             });
             });
        });
        return manifestRef;
     }
     }


     function removeSystemDocIndicatorForShell() {
     function startBoot(options) {
         var existing = document.getElementById('clbi-system-doc-indicator-row');
         if (BOOT_EXCLUDED_PAGE && !(options && options.force)) {
 
            hideBootScreen();
        if (document.body) {
             return Promise.resolve({ skipped: true, reason: 'developer-or-editing-page' });
             document.body.classList.remove('clbi-system-doc-page');
         }
         }
        if (hasBootParam('0') && !requiresLoginGate() && !(options && options.force)) return Promise.resolve(null);
        if (bootStarted && bootPromise && !(options && options.force)) return bootPromise;
        bootStarted = true;
        bootPromise = runInitialLoad(options || {});
        return bootPromise;
    }


        if (existing && existing.parentNode) {
    function resetBoot() {
            existing.parentNode.removeChild(existing);
        localStorage.removeItem(READY_KEY);
         }
         bootStarted = false;
 
        bootPromise = null;
         removeSystemDocSourceViewerForShell();
         loginGateLocked = false;
     }
     }


     function renderSystemDocIndicatorForShell() {
     /*
        var pageName;
    BootGate intentionally has no SPA hold/release API.
        var extMatch;
    Initial boot is the only blocking phase; later SPA routes must consume prepared
        var ext;
    EntryStore artifacts without reopening the loading surface.
        var row;
    */
        var box;
        var meta;
        var label;
        var type;
        var title;
        var anchor;
        var main;


         if (!document.body || !isMediaWikiSystemAssetPageForShell()) return;
    function buildBootReport() {
 
         return {
        pageName = readCurrentPageNameForShell();
            build: BUILD_ID,
        extMatch = pageName.match(/\.(css|js)$/i);
            state: {
        ext = extMatch ? extMatch[1].toUpperCase() : 'DOC';
                started: bootStarted,
                excludedPage: BOOT_EXCLUDED_PAGE,
                loginRequired: requiresLoginGate(),
                loginLocked: loginGateLocked,
                hasNode: !!((bootNode && bootNode.parentNode) || document.getElementById('boot-gate-screen')),
                htmlActive: !!(document.documentElement && document.documentElement.classList.contains('boot-gate-active')),
                bodyActive: !!(document.body && document.body.classList.contains('boot-gate-active'))
            },
            store: window.EntryStore && window.EntryStore.cacheInfo ? window.EntryStore.cacheInfo() : null,
            bootPerf: window.BootPerf && typeof window.BootPerf.summary === 'function' ? window.BootPerf.summary() : null,
            revisionManifest: window.RevisionManifest && typeof window.RevisionManifest.status === 'function' ? window.RevisionManifest.status() : null,
            nations: window.NationsPanel && typeof window.NationsPanel.diagnostics === 'function' ? window.NationsPanel.diagnostics() : null,
            decorations: window.Decorations && typeof window.Decorations.diagnostics === 'function' ? window.Decorations.diagnostics() : null
        };
    }


        document.body.classList.add('clbi-system-doc-page');
    window.EntryLoader = window.EntryLoader || {
        loadManifest: loadManifest,
        prepareTask: prepareTask,
        prepareNationsEra: prepareNationsEra,
        prepareDecorationSet: prepareDecorationSet,
        prepareGlobeSharedAssets: prepareGlobeSharedAssets,
        prepareImageAssets: prepareImageAssets,
        prepareHtmlEntry: prepareHtmlEntry,
        runInitialLoad: runInitialLoad
    };


         row = document.getElementById('clbi-system-doc-indicator-row');
    function showBootPreview(options) {
         var node;
        options = options || {};
        /*
        * Boot preview is a design/editing surface, not a real boot gate.
        * The earlier preview reused activateBootSurface(), which applied
        * html.boot-gate-active and hid the entire wiki shell, including
        * DevTools.  That made it impossible to edit loading-screen
        * decorations while previewing them.  Keep the real first-entry gate
        * full-screen, but make preview a small non-blocking surface below the
        * DevTools z-index so the owner can keep using the editor.
        */
        node = ensureBootScreen({ preview: true });
        if (!node) return null;
        node.classList.add('is-preview');
        if (bootStatusNode) bootStatusNode.textContent = options.status || 'Loading screen preview';
        updateBootProgress(Number(options.progress || 64), 100, options.detail || 'preview mode: no entry tasks are running');
        try {
            if (window.Decorations && typeof window.Decorations.sync === 'function') window.Decorations.sync();
        } catch (err) {}
        return node;
    }


         if (!row) {
    window.BootGate = window.BootGate || {
             row = document.createElement('div');
        version: BUILD_ID,
             row.id = 'clbi-system-doc-indicator-row';
        start: startBoot,
             row.className = 'clbi-system-doc-indicator-row';
         show: function (options) {
 
             options = options || {};
            box = document.createElement('div');
            options.force = true;
            box.className = 'clbi-system-doc-indicator';
            resetBoot();
 
             return startBoot(options);
            meta = document.createElement('div');
        },
            meta.className = 'clbi-system-doc-meta';
        reset: resetBoot,
        hide: hideBootScreen,
        preview: showBootPreview,
        report: buildBootReport,
        state: function () {
             return {
                started: bootStarted,
                loginRequired: requiresLoginGate(),
                loginLocked: loginGateLocked,
                hasNode: !!((bootNode && bootNode.parentNode) || document.getElementById('boot-gate-screen')),
                htmlActive: !!(document.documentElement && document.documentElement.classList.contains('boot-gate-active')),
                bodyActive: !!(document.body && document.body.classList.contains('boot-gate-active')),
                prelude: !!window.__BootGatePrelude,
                store: window.EntryStore && window.EntryStore.cacheInfo ? window.EntryStore.cacheInfo() : null,
                revisionManifest: window.RevisionManifest && typeof window.RevisionManifest.status === 'function' ? window.RevisionManifest.status() : null
            };
        }
    };


             label = document.createElement('span');
    function prime() {
             label.className = 'clbi-system-doc-label';
        if (BOOT_EXCLUDED_PAGE) {
             label.textContent = 'SYSTEM DOCUMENT';
             hideBootScreen();
             return;
        }
        activateBootSurface();
        waitForBody().then(function () {
            ensureBootScreen();
             return waitForAnimationFrames(1);
        }).then(function () {
            startBoot();
        });
    }


            type = document.createElement('span');
    waitForBody().then(function () {
            type.className = 'clbi-system-doc-type';
        if (!BOOT_EXCLUDED_PAGE) ensureBootScreen();
        else hideBootScreen();
    });
    window.setTimeout(prime, 0);
})(window, document, window.mediaWiki || window.mw);


            title = document.createElement('div');
            title.className = 'clbi-system-doc-title';


            meta.appendChild(label);
loadClbiRawScript('MediaWiki:DevTools.js');
            meta.appendChild(type);
loadClbiRawScript('MediaWiki:NationsPanel.js');
            box.appendChild(meta);
loadClbiRawScript('MediaWiki:NationsGlobe.js');
            box.appendChild(title);
            row.appendChild(box);


            anchor = getSystemDocOutputForShell();
/* CLBI safety guard: adaptive reset functions must exist before shell metric callbacks run. */
            main = document.querySelector('.liberty-content-main');
function resetLeftRecentAdaptiveState() {
    var list = document.getElementById('clbi-left-recent-list');
    var newsBox = list ? list.closest('.clbi-left-news-box') : null;
    var items = list ? Array.prototype.slice.call(list.querySelectorAll('.news-recent-item')) : [];


            if (anchor && anchor.parentNode) {
    if (newsBox) {
                anchor.parentNode.insertBefore(row, anchor);
        newsBox.classList.remove('is-adaptive-constrained');
            } else if (main) {
        newsBox.style.removeProperty('--adaptive-news-h');
                main.insertBefore(row, main.firstChild);
    }
            }
        }


         type = row.querySelector('.clbi-system-doc-type');
    if (list) {
         title = row.querySelector('.clbi-system-doc-title');
         list.classList.remove('is-adaptive-faded');
 
         list.removeAttribute('data-adaptive-limit');
         if (type) type.textContent = ext;
         list.style.removeProperty('--adaptive-recent-h');
        if (title) title.textContent = pageName;
 
        renderSystemDocSourceViewerForShell();
     }
     }


     var PAGE_TITLE_TARGET_SELECTORS = [
     items.forEach(function (item) {
        '.liberty-content-header',
         item.classList.remove('is-adaptive-hidden');
         '.liberty-content-header .title',
     });
        '.liberty-content-header .title h1',
}
        '.liberty-content-header h1',
        '#firstHeading',
        '.firstHeading',
        '.mw-first-heading',
        '.page-heading',
        '.page-header',
        '.mw-page-title-main',
        '.mw-page-title-namespace',
        '.mw-page-title-separator'
     ];


    var pageShellObserverStarted = false;
function resetLeftBillboardAdaptiveState() {
     var pageShellObserverTimer = null;
     var box = document.querySelector('#clbi-left-sidebar .left-billboard-box');


     function setPageTitleDomHidden(hidden) {
     if (!box) return;
        var nodes = document.querySelectorAll(PAGE_TITLE_TARGET_SELECTORS.join(','));


        nodes.forEach(function (node) {
    box.classList.remove('is-left-ad-title-only', 'is-left-ad-extended');
            if (!node || !node.style) return;
    box.style.removeProperty('--left-billboard-h');
    box.style.removeProperty('--left-billboard-finish-h');
}


            if (hidden) {
window.resetLeftRecentAdaptiveState = resetLeftRecentAdaptiveState;
                node.setAttribute('data-clbi-title-hidden', 'true');
window.resetLeftBillboardAdaptiveState = resetLeftBillboardAdaptiveState;
                node.style.setProperty('display', 'none', 'important');
            } else if (node.getAttribute('data-clbi-title-hidden') === 'true') {
                node.removeAttribute('data-clbi-title-hidden');
                node.style.removeProperty('display');
            }
        });
    }


    function applyPageShellClasses() {
loadClbiRawScript('MediaWiki:AnecdoteViewer.js');
        var body = document.body;
        var isSystemPage;


        if (!body) return;


        isSystemPage = isBackendOrSystemPageForShell();
(function () {
    'use strict';


         body.classList.remove('page-title-hidden', 'page-title-visible', 'backend-system-page', 'anecdote-namespace-page');
    var SYSTEM_TITLE_NAMESPACES = {
 
         '-1': true,
         if (!isMediaWikiSystemAssetPageForShell()) {
        '4': true,
             body.classList.remove('clbi-system-doc-page');
        '5': true,
             removeSystemDocIndicatorForShell();
        '6': true,
        }
        '7': true,
        '8': true,
        '9': true,
        '10': true,
        '11': true,
        '12': true,
        '13': true,
        '14': true,
        '15': true,
        '828': true,
        '829': true
    };
 
    function normalizePageNameForShell(value) {
         return String(value || '')
            .split('?')[0]
            .replace(/^\/index\.php\//, '')
             .replace(/_/g, ' ')
             .trim();
    }


        if (isAnecdoteNamespaceForShell()) {
    function readCurrentPageNameForShell() {
            body.classList.add('anecdote-namespace-page');
        var pageName = mw.config.get('wgPageName') || '';
        }


         if (isMediaWikiSystemAssetPageForShell()) {
         if (pageName) {
            body.classList.add('page-title-hidden', 'backend-system-page', 'clbi-system-doc-page');
             return normalizePageNameForShell(pageName);
            setPageTitleDomHidden(true);
            renderSystemDocIndicatorForShell();
        } else if (isSystemPage) {
            body.classList.add('page-title-visible', 'backend-system-page');
            setPageTitleDomHidden(false);
        } else {
             body.classList.add('page-title-hidden');
            setPageTitleDomHidden(true);
         }
         }
    }


    function applyPageShellClassesDeferred() {
         return normalizePageNameForShell(window.location.pathname || '');
         applyPageShellClasses();
        window.setTimeout(applyPageShellClasses, 0);
        window.setTimeout(applyPageShellClasses, 80);
        window.setTimeout(applyPageShellClasses, 250);
     }
     }


     function startPageShellObserver() {
     function isAnecdoteNamespaceForShell() {
         var observer;
         var namespaceNumber = Number(mw.config.get('wgNamespaceNumber'));
        var canonicalNamespace = String(mw.config.get('wgCanonicalNamespace') || '').toLowerCase();
        var pageName = readCurrentPageNameForShell();


         if (pageShellObserverStarted || !window.MutationObserver || !document.body) return;
         return namespaceNumber === 3000 ||
            canonicalNamespace === 'anecdote' ||
            /^(anecdote|에넥도트):/i.test(pageName);
    }


         pageShellObserverStarted = true;
    function isBackendOrSystemPageForShell() {
         observer = new MutationObserver(function (mutations) {
         var namespaceNumber = Number(mw.config.get('wgNamespaceNumber'));
            var i;
        var action = String(mw.config.get('wgAction') || 'view').toLowerCase();
             var target;
         var contentModel = String(mw.config.get('wgPageContentModel') || '').toLowerCase();
        var pageName = readCurrentPageNameForShell();
        var lowerPageName = pageName.toLowerCase();
 
        if (action && action !== 'view') {
             return true;
        }


            /*
        if (pageName === '대문') {
            시스템 CSS/JS 문서는 applyPageShellClasses()가 초기에 한 번
            return false;
            인디케이터와 source viewer를 만든 뒤에는 MutationObserver가 다시
        }
            같은 렌더링을 반복할 필요가 없다. 이 반복이 DevTools에서 body/요소가
            계속 플래시되는 직접 원인이다.
            SPA 전환 뒤의 처리는 loadPage()와 wikipage.content hook에서 따로 호출된다.
            */
            if (isMediaWikiSystemAssetPageForShell()) {
                for (i = 0; i < mutations.length; i += 1) {
                    target = mutations[i] && mutations[i].target;


                    if (
        if (SYSTEM_TITLE_NAMESPACES[String(namespaceNumber)]) {
                        target &&
            return true;
                        target.nodeType === 1 &&
        }
                        (
                            target.id === 'clbi-system-source-viewer' ||
                            target.id === 'clbi-system-doc-indicator-row' ||
                            (target.closest && target.closest('#clbi-system-source-viewer, #clbi-system-doc-indicator-row'))
                        )
                    ) {
                        return;
                    }
                }


                if (
        if (contentModel === 'css' || contentModel === 'javascript' || contentModel === 'json' || contentModel === 'sanitized-css') {
                    document.getElementById('clbi-system-doc-indicator-row') &&
            return true;
                    document.getElementById('clbi-system-source-viewer')
        }
                ) {
                    return;
                }
            }


            if (pageShellObserverTimer) return;
        if (/\.(css|js|json)$/i.test(pageName)) {
            return true;
        }


            pageShellObserverTimer = window.setTimeout(function () {
        if (/^(mediawiki|미디어위키|special|특수):/i.test(pageName)) {
                pageShellObserverTimer = null;
             return true;
                applyPageShellClasses();
         }
             }, 50);
         });


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


     if (document.readyState === 'loading') {
     function isMediaWikiSystemAssetPageForShell() {
         document.addEventListener('DOMContentLoaded', function () {
         var namespaceNumber = Number(mw.config.get('wgNamespaceNumber'));
            applyPageShellClassesDeferred();
        var pageName = readCurrentPageNameForShell();
            startPageShellObserver();
         var contentModel = String(mw.config.get('wgPageContentModel') || '').toLowerCase();
         });
    } else {
        applyPageShellClassesDeferred();
        startPageShellObserver();
    }


    if (mw.hook) {
        return namespaceNumber === 8 &&
        mw.hook('wikipage.content').add(applyPageShellClassesDeferred);
            (/\.(css|js)$/i.test(pageName) || contentModel === 'css' || contentModel === 'javascript' || contentModel === 'sanitized-css');
     }
     }


    window.CLBI_PAGE_SHELL = {
        refresh: applyPageShellClasses,
        isBackendOrSystemPage: isBackendOrSystemPageForShell,
        isSystemAssetPage: isMediaWikiSystemAssetPageForShell,
        renderSystemDocIndicator: renderSystemDocIndicatorForShell,
        removeSystemDocIndicator: removeSystemDocIndicatorForShell,
        refreshSystemDocSourceViewer: renderSystemDocSourceViewerForShell
    };
}());


function loadLangScript(done) {
     var systemDocRawFetchToken = 0;
     $.getScript('/index.php?title=미디어위키:Lang.js&action=raw&ctype=text/javascript')
        .done(function() {
            if (typeof done === 'function') done();
        })
        .fail(function(a, b, c) {
            console.error('Lang.js load failed:', b, c);
            if (typeof done === 'function') done();
        });
}


    function cleanupLegacySystemDocCodeMutationsForShell() {
        document.querySelectorAll('.clbi-system-doc-codepane').forEach(function (pane) {
            var parent;


function initHalftoneBackground() {
            if (!pane || !pane.parentNode) return;
    try {
        initWebGLHalftoneBackground();
    } catch (err) {
        console.error('WebGL halftone background failed:', err);
    }
}


function initWebGLHalftoneBackground() {
            parent = pane.parentNode;
    var canvasId = 'site-halftone-bg';
            while (pane.firstChild) {
    var existing = document.getElementById(canvasId);
                parent.insertBefore(pane.firstChild, pane);
    var canvas = existing || document.createElement('canvas');
            }
    var halftoneState = window.SiteHalftoneBackgroundState || (window.SiteHalftoneBackgroundState = { runId: 0 });
            parent.removeChild(pane);
    var runId = halftoneState.runId + 1;
        });


    halftoneState.runId = runId;
        document.querySelectorAll('.clbi-system-doc-codebox').forEach(function (node) {
            node.classList.remove('clbi-system-doc-codebox');
            node.removeAttribute('data-clbi-system-doc-codebox');
            node.removeAttribute('style');
        });
    }


     if (!existing) {
     function getSystemDocOutputForShell() {
         canvas.id = canvasId;
         return document.querySelector('.liberty-content-main .mw-parser-output');
        canvas.setAttribute('aria-hidden', 'true');
        document.body.insertBefore(canvas, document.body.firstChild || null);
     }
     }


     canvas.style.position = 'fixed';
     function findSystemDocSourceNodeForShell() {
    canvas.style.inset = '0';
        var output = getSystemDocOutputForShell();
    canvas.style.width = '100vw';
        var children;
    canvas.style.height = '100vh';
        var preferred;
    canvas.style.pointerEvents = 'none';
 
    canvas.style.background = '#000000';
        if (!output) return null;
    canvas.style.display = 'block';


    if (!canvas.getAttribute('data-halftone-context-watch')) {
        children = Array.prototype.slice.call(output.children || [])
        canvas.setAttribute('data-halftone-context-watch', '1');
            .filter(function (el) {
        canvas.addEventListener('webglcontextlost', function (event) {
                return el && el.nodeType === 1 &&
            event.preventDefault();
                    el.id !== 'clbi-system-doc-indicator-row' &&
            canvas.style.display = 'none';
                    el.id !== 'clbi-system-source-viewer' &&
            if (window.SiteHalftoneBackgroundState) {
                    !el.classList.contains('catlinks') &&
                window.SiteHalftoneBackgroundState.contextLost = true;
                    (el.textContent || '').trim().length > 200;
                window.SiteHalftoneBackgroundState.runId += 1;
             });
            }
        }, false);
        canvas.addEventListener('webglcontextrestored', function () {
            if (window.SiteHalftoneBackgroundState) {
                window.SiteHalftoneBackgroundState.contextLost = false;
             }
            window.setTimeout(initWebGLHalftoneBackground, 0);
        }, false);
    }


    var gl = canvas.getContext('webgl', {
        preferred = children.filter(function (el) {
        alpha: false,
            return el.matches && el.matches('.mw-highlight, .mw-code, pre');
        antialias: false,
         })[0];
        depth: false,
        stencil: false,
         preserveDrawingBuffer: false,
        powerPreference: 'low-power'
    }) || canvas.getContext('experimental-webgl');


    if (!gl) {
        return preferred || children.sort(function (a, b) {
        canvas.style.display = 'none';
            return (b.textContent || '').trim().length - (a.textContent || '').trim().length;
        console.warn('WebGL background unavailable.');
         })[0] || null;
         return;
     }
     }


     var vertexSrc = [
     function getSystemDocRawUrlForShell() {
        'attribute vec2 a_position;',
         var title = mw.config.get('wgPageName') || readCurrentPageNameForShell();
        'void main() {',
         var url;
         '  gl_Position = vec4(a_position, 0.0, 1.0);',
         '}'
    ].join('\n');


    var fragmentSrc = [
        if (window.mw && mw.util && typeof mw.util.getUrl === 'function') {
        'precision mediump float;',
            url = mw.util.getUrl(title, {
        'uniform vec2 u_resolution;',
                action: 'raw',
        'uniform float u_time;',
                ctype: 'text/plain'
        'const float TAU = 6.28318530718;',
            });
        'float gaussian(float v, float r) {',
         } else {
        ' return exp(-((v * v) / max(0.0001, r * r)));',
            url = '/index.php?title=' + encodeURIComponent(title) + '&action=raw&ctype=text/plain';
        '}',
         }
        'float hash(vec2 p) {',
 
        '  return fract(sin(dot(p, vec2(127.1, 311.7))) * 43758.5453123);',
         if (window.RevisionManifest && typeof window.RevisionManifest.addRevisionParam === 'function') {
         '}',
            url = window.RevisionManifest.addRevisionParam(url, title);
        'float bucketAlpha(float a) {',
         }
        ' float i = floor(a * 9.0);',
         return url;
        ' if (i < 1.0) return 0.040;',
    }
         '  if (i < 2.0) return 0.080;',
 
         if (i < 3.0) return 0.135;',
    function removeSystemDocSourceViewerForShell() {
        ' if (i < 4.0) return 0.210;',
         var viewer = document.getElementById('clbi-system-source-viewer');
        '  if (i < 5.0) return 0.310;',
 
        '  if (i < 6.0) return 0.430;',
         if (viewer && viewer.parentNode) {
        '  if (i < 7.0) return 0.580;',
            viewer.parentNode.removeChild(viewer);
         '  if (i < 8.0) return 0.760;',
         }
         return 0.920;',
 
        '}',
         document.querySelectorAll('.clbi-system-original-source-hidden').forEach(function (node) {
        'void main() {',
            node.classList.remove('clbi-system-original-source-hidden');
         '  vec2 frag = gl_FragCoord.xy;',
            node.removeAttribute('data-clbi-system-source-hidden');
        '  float spacing = 5.0;',
            node.style.removeProperty('display');
        '  float dotSize = 1.08;',
         });
        '  vec2 grid = floor(frag / spacing);',
 
        ' vec2 inCell = mod(frag, spacing);',
         cleanupLegacySystemDocCodeMutationsForShell();
        '  vec2 dotOrigin = vec2(1.0, 1.0);',
    }
         '  vec2 dotCenter = dotOrigin + vec2(dotSize * 0.5);',
 
        '  vec2 local = abs(inCell - dotCenter);',
    function ensureSystemDocSourceViewerForShell() {
        '  float noise = hash(grid);',
         var output = getSystemDocOutputForShell();
        '  float size = dotSize + noise * 0.18;',
         var source;
        '  float dotMask = 1.0 - smoothstep(size * 0.5, size * 0.5 + 0.22, max(local.x, local.y));',
         var viewer;
         '  vec2 uv = frag / u_resolution;',
         var fallbackText;
        '  float centerLine = 0.50 +',
 
         '    sin((uv.y * 1.32 + 0.08) * TAU) * 0.070 +',
         if (!output || !isMediaWikiSystemAssetPageForShell()) return null;
        '    sin((uv.y * 3.18 + 0.34) * TAU) * 0.030;',
        '  float u = uv.x - centerLine;',
        ' float absU = abs(u);',
        '  float sideLift = smoothstep(0.065, 0.44, absU);',
        '  float valley = gaussian(u, 0.150);',
        '  float t = u_time;',
        '  float leftRibbonCenter = -0.28 + sin((uv.y * 3.20 + 0.12) * TAU) * 0.050;',
        '  float rightRibbonCenter = 0.27 + sin((uv.y * 2.85 + 0.56) * TAU) * 0.055;',
        '  float leftRibbon = gaussian(u - leftRibbonCenter, 0.105);',
        ' float rightRibbon = gaussian(u - rightRibbonCenter, 0.110);',
        '  float foldedU = u +',
        '    sin((uv.y * 4.40 + 0.22) * TAU) * 0.050 * (0.3 + sideLift) +',
        '    sin((uv.y * 7.20 + uv.x * 1.10) * TAU) * 0.022;',
        ' float verticalFold = pow(0.5 + 0.5 * cos(((foldedU * 3.05) + (sin(uv.y * TAU * 2.35) * 0.18)) * TAU), 2.5);',
        '  float diagonalFold = pow(0.5 + 0.5 * cos(((foldedU * 1.80) - (uv.y * 1.12) + 0.18) * TAU), 2.1);',
        ' float waist = gaussian(uv.y - 0.50, 0.25) * gaussian(absU - 0.20, 0.19);',
         '  float grain = (noise - 0.5) * 0.050;',
        '  float staticField =',
        '    0.055 +',
        '    sideLift * 0.210 +',
        '    (leftRibbon + rightRibbon) * 0.145 +',
         '    verticalFold * (0.055 + sideLift * 0.115) +',
        '    diagonalFold * 0.045 +',
        '    waist * 0.060 -',
        '    valley * 0.150 +',
        '    grain;',
        '  float alpha = staticField;',
        '  alpha += 0.115 * (leftRibbon + rightRibbon) * sin(t * 0.00030 + ((uv.y * 1.9) + sideLift * 0.4) * TAU);',
        '  alpha += 0.095 * verticalFold * (0.4 + sideLift) * sin(t * 0.00041 + ((uv.y * 2.7) + foldedU * 0.65) * TAU);',
         '  alpha += 0.070 * waist * sin(t * 0.00053 + ((uv.y * 3.1) - absU * 0.8) * TAU);',
         '  alpha += 0.060 * (1.0 - valley) * diagonalFold * sin(t * 0.00067 + ((uv.y * 1.4) + uv.x * 0.6) * TAU);',
         '  alpha += 0.038 * (0.35 + sideLift) * (0.35 + noise) * sin(t * 0.00079 + ((uv.y * 4.6) + noise * 0.8) * TAU);',
         '  alpha = bucketAlpha(clamp(alpha, 0.025, 0.96));',
        '  float value = alpha * dotMask;',
         '  gl_FragColor = vec4(vec3(0.8862745 * value), 1.0);',
        '}'
    ].join('\n');


    function compileShader(type, source) {
         cleanupLegacySystemDocCodeMutationsForShell();
        var shader = gl.createShader(type);
        gl.shaderSource(shader, source);
         gl.compileShader(shader);


         if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) {
         source = findSystemDocSourceNodeForShell();
            console.error('WebGL shader compile error:', gl.getShaderInfoLog(shader));
        if (!source) return null;
            gl.deleteShader(shader);
            return null;
        }


         return shader;
         viewer = document.getElementById('clbi-system-source-viewer');
    }


    var vertexShader = compileShader(gl.VERTEX_SHADER, vertexSrc);
        if (!viewer) {
    var fragmentShader = compileShader(gl.FRAGMENT_SHADER, fragmentSrc);
            viewer = document.createElement('pre');
            viewer.id = 'clbi-system-source-viewer';
            viewer.className = 'clbi-system-source-viewer';
            output.appendChild(viewer);
        }


    if (!vertexShader || !fragmentShader) return;
        fallbackText = source.textContent || '';


    var program = gl.createProgram();
        if (!viewer.textContent && fallbackText) {
    gl.attachShader(program, vertexShader);
            viewer.textContent = fallbackText;
    gl.attachShader(program, fragmentShader);
        }
    gl.linkProgram(program);
 
        source.classList.add('clbi-system-original-source-hidden');
        source.setAttribute('data-clbi-system-source-hidden', 'true');
        source.style.setProperty('display', 'none', 'important');


    if (!gl.getProgramParameter(program, gl.LINK_STATUS)) {
         return viewer;
        console.error('WebGL program link error:', gl.getProgramInfoLog(program));
         return;
     }
     }


     var positionLoc = gl.getAttribLocation(program, 'a_position');
     function renderSystemDocSourceViewerForShell() {
    var resolutionLoc = gl.getUniformLocation(program, 'u_resolution');
        var viewer;
    var timeLoc = gl.getUniformLocation(program, 'u_time');
        var pageName;
        var token;
        var currentScrollTop;


    var buffer = gl.createBuffer();
        if (!isMediaWikiSystemAssetPageForShell()) return;
    gl.bindBuffer(gl.ARRAY_BUFFER, buffer);
    gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([
        -1, -1,
        1, -1,
        -1,  1,
        -1,  1,
        1, -1,
        1,  1
    ]), gl.STATIC_DRAW);


    gl.useProgram(program);
        pageName = String(mw.config.get('wgPageName') || readCurrentPageNameForShell());
    gl.bindBuffer(gl.ARRAY_BUFFER, buffer);
        viewer = document.getElementById('clbi-system-source-viewer');
    gl.enableVertexAttribArray(positionLoc);
    gl.vertexAttribPointer(positionLoc, 2, gl.FLOAT, false, 0, 0);


    function resize() {
        /*
         var dpr = Math.min(window.devicePixelRatio || 1, 1.5);
        시스템 문서 뷰어가 이미 만들어져 있고 raw 원문도 로드된 상태라면
         var cssW = Math.max(1, window.innerWidth || document.documentElement.clientWidth || 1);
         다시 source 탐색/숨김/스타일 재적용을 하지 않는다.
         var cssH = Math.max(1, window.innerHeight || document.documentElement.clientHeight || 1);
         DevTools Elements 패널에서 body가 계속 파랗게 깜빡이던 원인은
         var w = Math.max(1, Math.floor(cssW * dpr));
        MutationObserver가 이 재적용을 반복해서 DOM attribute mutation을 만들었기 때문이다.
        var h = Math.max(1, Math.floor(cssH * dpr));
         */
         if (
            viewer &&
            viewer.getAttribute('data-clbi-raw-title') === pageName &&
            viewer.getAttribute('data-clbi-raw-loaded') === '1'
        ) {
            return;
        }


         if (canvas.width !== w || canvas.height !== h) {
         viewer = ensureSystemDocSourceViewerForShell();
            canvas.width = w;
        if (!viewer) return;
            canvas.height = h;
            canvas.style.width = cssW + 'px';
            canvas.style.height = cssH + 'px';
            gl.viewport(0, 0, w, h);
        }
    }


    var prefersReducedMotion = false;
        currentScrollTop = viewer.scrollTop || 0;
    try {
         viewer.setAttribute('data-clbi-raw-title', pageName);
         prefersReducedMotion = window.matchMedia && window.matchMedia('(prefers-reduced-motion: reduce)').matches;
        token = ++systemDocRawFetchToken;
    } catch (err) {}


    function getNationsGlobeHalftoneState() {
        fetch(getSystemDocRawUrlForShell(), { credentials: 'same-origin' })
        var globe = document.querySelector('.clbi-nations-globe-window[data-nations-globe], [data-nations-globe="real-world"]');
            .then(function (res) {
                if (!res.ok) throw new Error('raw fetch failed ' + res.status);
                return res.text();
            })
            .then(function (text) {
                if (token !== systemDocRawFetchToken) return;


        if (!globe) {
                currentScrollTop = viewer.scrollTop || currentScrollTop || 0;
            return 'none';
        }


        if (globe.classList && globe.classList.contains('is-dragging')) {
                if (text && viewer.textContent !== text) {
            return 'active';
                    viewer.textContent = text;
        }
                }


        return 'present';
                viewer.setAttribute('data-clbi-raw-loaded', '1');
                viewer.scrollTop = currentScrollTop;
            })
            .catch(function () {
                viewer.setAttribute('data-clbi-raw-loaded', '0');
            });
     }
     }


     function getFrameInterval() {
     function removeSystemDocIndicatorForShell() {
         var globeState;
         var existing = document.getElementById('clbi-system-doc-indicator-row');


         if (prefersReducedMotion) {
         if (document.body) {
             return 1000;
             document.body.classList.remove('clbi-system-doc-page');
         }
         }


         globeState = getNationsGlobeHalftoneState();
         if (existing && existing.parentNode) {
 
            existing.parentNode.removeChild(existing);
        /*
        * Nations page background guard — 20260709.
        *
        * The old guard returned without drawing while the nations globe existed.
        * That protected the globe, but it also left the fixed WebGL canvas alive
        * with preserveDrawingBuffer=false.  After the browser discarded the last
        * buffer, the decorative background became a black fixed canvas.  Keep the
        * background alive, but lower its cadence while the globe owns the page.
        */
        if (globeState === 'active') {
            return 500;
         }
         }


         if (globeState === 'present') {
         removeSystemDocSourceViewerForShell();
            return 250;
        }
 
        return 66;
     }
     }


     var lastFrame = 0;
     function renderSystemDocIndicatorForShell() {
    var startTime = performance.now();
        var pageName;
        var extMatch;
        var ext;
        var row;
        var box;
        var meta;
        var label;
        var type;
        var title;
        var anchor;
        var main;


    function draw(now) {
        if (!document.body || !isMediaWikiSystemAssetPageForShell()) return;
        resize();


         gl.clearColor(0, 0, 0, 1);
         pageName = readCurrentPageNameForShell();
         gl.clear(gl.COLOR_BUFFER_BIT);
         extMatch = pageName.match(/\.(css|js)$/i);
        gl.uniform2f(resolutionLoc, canvas.width, canvas.height);
         ext = extMatch ? extMatch[1].toUpperCase() : 'DOC';
        gl.uniform1f(timeLoc, now - startTime);
         gl.drawArrays(gl.TRIANGLES, 0, 6);
    }


    function render(now) {
         document.body.classList.add('clbi-system-doc-page');
         if (halftoneState.runId !== runId || halftoneState.contextLost) {
            return;
        }


         requestAnimationFrame(render);
         row = document.getElementById('clbi-system-doc-indicator-row');


         if (document.hidden) {
         if (!row) {
             return;
            row = document.createElement('div');
        }
            row.id = 'clbi-system-doc-indicator-row';
            row.className = 'clbi-system-doc-indicator-row';
 
             box = document.createElement('div');
            box.className = 'clbi-system-doc-indicator';


        if (now - lastFrame < getFrameInterval()) {
            meta = document.createElement('div');
             return;
             meta.className = 'clbi-system-doc-meta';
        }


        lastFrame = now;
            label = document.createElement('span');
        draw(now);
            label.className = 'clbi-system-doc-label';
    }
            label.textContent = 'SYSTEM DOCUMENT';


    draw(performance.now());
            type = document.createElement('span');
    requestAnimationFrame(render);
            type.className = 'clbi-system-doc-type';
}


var CLBI_SVG_BELL = '<svg class="profile-svg profile-svg-bell" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.75" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M10.268 21a2 2 0 0 0 3.464 0"/><path d="M3.262 15.326A1 1 0 0 0 4 17h16a1 1 0 0 0 .74-1.673C19.41 13.956 18 12.499 18 8A6 6 0 0 0 6 8c0 4.499-1.411 5.956-2.738 7.326"/></svg>';
            title = document.createElement('div');
var CLBI_SVG_BELL_DOT = '<svg class="profile-svg profile-svg-bell-dot" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.75" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M10.268 21a2 2 0 0 0 3.464 0"/><path d="M11.68 2.009A6 6 0 0 0 6 8c0 4.499-1.411 5.956-2.738 7.326A1 1 0 0 0 4 17h16a1 1 0 0 0 .74-1.673c-.824-.85-1.678-1.731-2.21-3.348"/><circle cx="18" cy="5" r="3"/></svg>';
            title.className = 'clbi-system-doc-title';
var CLBI_SVG_LIST = '<svg class="profile-svg profile-svg-list" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.75" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M3 5h.01"/><path d="M3 12h.01"/><path d="M3 19h.01"/><path d="M8 5h13"/><path d="M8 12h13"/><path d="M8 19h13"/></svg>';
var CLBI_SVG_LANGUAGES = '<svg class="profile-svg profile-svg-languages" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.75" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="m5 8 6 6"/><path d="m4 14 6-6 2-3"/><path d="M2 5h12"/><path d="M7 2h1"/><path d="m22 22-5-10-5 10"/><path d="M14 18h6"/></svg>';
var CLBI_SVG_POWER = '<svg class="profile-svg profile-svg-power" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.75" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M12 2v10"/><path d="M18.4 6.6a9 9 0 1 1-12.77.04"/></svg>';
var CLBI_SVG_SETTINGS = '<svg class="profile-svg profile-svg-settings" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.75" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M9.671 4.136a2.34 2.34 0 0 1 4.659 0 2.34 2.34 0 0 0 3.319 1.915 2.34 2.34 0 0 1 2.33 4.033 2.34 2.34 0 0 0 0 3.831 2.34 2.34 0 0 1-2.33 4.033 2.34 2.34 0 0 0-3.319 1.915 2.34 2.34 0 0 1-4.659 0 2.34 2.34 0 0 0-3.32-1.915 2.34 2.34 0 0 1-2.33-4.033 2.34 2.34 0 0 0 0-3.831A2.34 2.34 0 0 1 6.35 6.051a2.34 2.34 0 0 0 3.319-1.915"/><circle cx="12" cy="12" r="3"/></svg>';
var CLBI_SVG_SCAN_TEXT = '<svg class="profile-svg profile-svg-scan-text" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.75" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M3 7V5a2 2 0 0 1 2-2h2"/><path d="M17 3h2a2 2 0 0 1 2 2v2"/><path d="M21 17v2a2 2 0 0 1-2 2h-2"/><path d="M7 21H5a2 2 0 0 1-2-2v-2"/><path d="M7 8h8"/><path d="M7 12h10"/><path d="M7 16h6"/></svg>';
var CLBI_SVG_SCAN_EYE = '<svg class="profile-svg profile-svg-scan-eye" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.75" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M3 7V5a2 2 0 0 1 2-2h2"/><path d="M17 3h2a2 2 0 0 1 2 2v2"/><path d="M21 17v2a2 2 0 0 1-2 2h-2"/><path d="M7 21H5a2 2 0 0 1-2-2v-2"/><circle cx="12" cy="12" r="1"/><path d="M18.944 12.33a1 1 0 0 0 0-.66 7.5 7.5 0 0 0-13.888 0 1 1 0 0 0 0 .66 7.5 7.5 0 0 0 13.888 0"/></svg>';
var CLBI_SVG_NEWSPAPER = '<svg class="profile-svg profile-svg-newspaper" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.75" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M15 18h-5"/><path d="M18 14h-8"/><path d="M4 22h16a2 2 0 0 0 2-2V4a2 2 0 0 0-2-2H8a2 2 0 0 0-2 2v16a2 2 0 0 1-4 0v-9a2 2 0 0 1 2-2h2"/><rect width="8" height="4" x="10" y="6" rx="1"/></svg>';
var CLBI_SVG_GREAT_WALL = '<svg class="profile-svg profile-svg-great-wall lucide lucide-paint-roller" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect width="16" height="6" x="2" y="2" rx="2"/><path d="M10 16v-2a2 2 0 0 1 2-2h8a2 2 0 0 0 2-2V7a2 2 0 0 0-2-2h-2"/><rect width="4" height="6" x="8" y="16" rx="1"/></svg>';
var CLBI_SVG_TROPHY = '<svg class="profile-svg profile-svg-trophy" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.75" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M10 14.66v1.626a2 2 0 0 1-.976 1.696A5 5 0 0 0 7 21.978"/><path d="M14 14.66v1.626a2 2 0 0 0 .976 1.696A5 5 0 0 1 17 21.978"/><path d="M18 9h1.5a1 1 0 0 0 0-5H18"/><path d="M4 22h16"/><path d="M6 9a6 6 0 0 0 12 0V3a1 1 0 0 0-1-1H7a1 1 0 0 0-1 1z"/><path d="M6 9H4.5a1 1 0 0 1 0-5H6"/></svg>';
var CLBI_SVG_PACKAGE = '<svg class="profile-svg profile-svg-package" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.75" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M12 3v6"/><path d="M16.76 3a2 2 0 0 1 1.8 1.1l2.23 4.479a2 2 0 0 1 .21.891V19a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V9.472a2 2 0 0 1 .211-.894L5.45 4.1A2 2 0 0 1 7.24 3z"/><path d="M3.054 9.013h17.893"/></svg>';


var PROFILE_RENDER_TOKEN = 0;
            meta.appendChild(label);
            meta.appendChild(type);
            box.appendChild(meta);
            box.appendChild(title);
            row.appendChild(box);


function invalidateProfileRender() {
            anchor = getSystemDocOutputForShell();
    PROFILE_RENDER_TOKEN++;
            main = document.querySelector('.liberty-content-main');
}


$(function() {
            if (anchor && anchor.parentNode) {
    initHalftoneBackground();
                anchor.parentNode.insertBefore(row, anchor);
            } else if (main) {
                main.insertBefore(row, main.firstChild);
            }
        }


// ── 하단 Plank 단축키 가이드 ──
        type = row.querySelector('.clbi-system-doc-type');
function escapeClbiBottomGuideHtml(value) {
         title = row.querySelector('.clbi-system-doc-title');
    return String(value == null ? '' : value)
        .replace(/&/g, '&amp;')
        .replace(/</g, '&lt;')
        .replace(/>/g, '&gt;')
         .replace(/"/g, '&quot;')
        .replace(/'/g, '&#039;');
}


function readClbiBottomGuidePageName() {
        if (type) type.textContent = ext;
    var pageName = window.mw && mw.config ? (mw.config.get('wgPageName') || '') : '';
        if (title) title.textContent = pageName;


    if (!pageName) {
        renderSystemDocSourceViewerForShell();
        pageName = window.location.pathname || '';
     }
     }


     return String(pageName)
     var PAGE_TITLE_TARGET_SELECTORS = [
         .split('?')[0]
         '.liberty-content-header',
         .replace(/^\/index\.php\//, '')
        '.liberty-content-header .title',
         .replace(/_/g, ' ')
        '.liberty-content-header .title h1',
         .trim();
        '.liberty-content-header h1',
}
        '#firstHeading',
        '.firstHeading',
         '.mw-first-heading',
        '.page-heading',
        '.page-header',
         '.mw-page-title-main',
        '.mw-page-title-namespace',
         '.mw-page-title-separator'
    ];


function isClbiNationsShortcutContext() {
    var pageShellObserverStarted = false;
     var pageName = readClbiBottomGuidePageName();
     var pageShellObserverTimer = null;


     return pageName === '국가 및 조합' ||
     function setPageTitleDomHidden(hidden) {
         pageName === 'Nations & Factions' ||
         var nodes = document.querySelectorAll(PAGE_TITLE_TARGET_SELECTORS.join(','));
        !!document.querySelector('.clbi-nations-panel-stack, .clbi-nations-globe-window, .clbi-nations-tabpanel');
}


function isClbiWikiEditShortcutContext() {
        nodes.forEach(function (node) {
    var action = window.mw && mw.config ? String(mw.config.get('wgAction') || '') : '';
            if (!node || !node.style) return;
    var search = String(window.location.search || '');


    return action === 'edit' ||
            if (hidden) {
        action === 'submit' ||
                node.setAttribute('data-clbi-title-hidden', 'true');
        /(?:^|[?&])action=(?:edit|submit)(?:&|$)/.test(search) ||
                node.style.setProperty('display', 'none', 'important');
        !!document.querySelector('#editform, #wpSave, input[name="wpSave"], button[name="wpSave"]');
            } else if (node.getAttribute('data-clbi-title-hidden') === 'true') {
}
                node.removeAttribute('data-clbi-title-hidden');
                node.style.removeProperty('display');
            }
        });
    }


    function applyPageShellClasses() {
        var body = document.body;
        var isSystemPage;


function isClbiNationListManagerShortcutContext() {
        if (!body) return;
    return !!document.querySelector('.nation-list-manager');
}


function getClbiBottomShortcutItems() {
        isSystemPage = isBackendOrSystemPageForShell();
    if (isClbiWikiEditShortcutContext()) {
 
        return [
        body.classList.remove('page-title-hidden', 'page-title-visible', 'backend-system-page', 'anecdote-namespace-page');
             { key: 'Ctrl+S', label: '변경사항 저장' }
 
        ];
        if (!isMediaWikiSystemAssetPageForShell()) {
    }
             body.classList.remove('clbi-system-doc-page');
            removeSystemDocIndicatorForShell();
        }


        if (isAnecdoteNamespaceForShell()) {
            body.classList.add('anecdote-namespace-page');
        }


    if (isClbiNationListManagerShortcutContext()) {
        if (isMediaWikiSystemAssetPageForShell()) {
         return [
            body.classList.add('page-title-hidden', 'backend-system-page', 'clbi-system-doc-page');
             { key: 'Ctrl+S', label: '국가 목록 저장' }
            setPageTitleDomHidden(true);
         ];
            renderSystemDocIndicatorForShell();
         } else if (isSystemPage) {
             body.classList.add('page-title-visible', 'backend-system-page');
            setPageTitleDomHidden(false);
        } else {
            body.classList.add('page-title-hidden');
            setPageTitleDomHidden(true);
         }
     }
     }


     if (isClbiNationsShortcutContext()) {
     function applyPageShellClassesDeferred() {
         return [
         applyPageShellClasses();
            { key: 'Q', label: '이전 시대' },
        window.setTimeout(applyPageShellClasses, 0);
            { key: 'E', label: '다음 시대' },
        window.setTimeout(applyPageShellClasses, 80);
            { key: 'Shift+Q', label: '이전 연도' },
        window.setTimeout(applyPageShellClasses, 250);
            { key: 'Shift+E', label: '다음 연도' },
            { key: 'Ctrl+Q', label: '이전 대륙' },
            { key: 'Ctrl+E', label: '다음 대륙' }
        ];
     }
     }


     return [];
     function startPageShellObserver() {
}
        var observer;


function renderClbiBottomShortcutGuide() {
        if (pageShellObserverStarted || !window.MutationObserver || !document.body) return;
    var guide = document.getElementById('clbi-bottom-shortcut-guide');
    var items;
    var html;


    if (!guide) return;
        pageShellObserverStarted = true;
        observer = new MutationObserver(function (mutations) {
            var i;
            var target;


    items = getClbiBottomShortcutItems();
            /*
 
            시스템 CSS/JS 문서는 applyPageShellClasses()가 초기에 한 번
    if (!items.length) {
            인디케이터와 source viewer를 만든 뒤에는 MutationObserver가 다시
        guide.classList.add('is-empty');
            같은 렌더링을 반복할 필요가 없다. 이 반복이 DevTools에서 body/요소가
        guide.innerHTML = '';
            계속 플래시되는 직접 원인이다.
        return;
            SPA 전환 뒤의 처리는 loadPage()와 wikipage.content hook에서 따로 호출된다.
    }
            */
            if (isMediaWikiSystemAssetPageForShell()) {
                for (i = 0; i < mutations.length; i += 1) {
                    target = mutations[i] && mutations[i].target;


    guide.classList.remove('is-empty');
                    if (
                        target &&
                        target.nodeType === 1 &&
                        (
                            target.id === 'clbi-system-source-viewer' ||
                            target.id === 'clbi-system-doc-indicator-row' ||
                            (target.closest && target.closest('#clbi-system-source-viewer, #clbi-system-doc-indicator-row'))
                        )
                    ) {
                        return;
                    }
                }


    html = '<div class="clbi-bottom-shortcut-list">';
                if (
                    document.getElementById('clbi-system-doc-indicator-row') &&
                    document.getElementById('clbi-system-source-viewer')
                ) {
                    return;
                }
            }


    items.forEach(function (item) {
             if (pageShellObserverTimer) return;
        html += '<div class="clbi-bottom-shortcut-item">' +
             '<span class="clbi-bottom-shortcut-key">' + escapeClbiBottomGuideHtml(item.key) + '</span>' +
            '<span class="clbi-bottom-shortcut-label">' + escapeClbiBottomGuideHtml(item.label) + '</span>' +
        '</div>';
    });


    html += '</div>';
            pageShellObserverTimer = window.setTimeout(function () {
    guide.innerHTML = html;
                pageShellObserverTimer = null;
}
                applyPageShellClasses();
            }, 50);
        });


function buildClbiBottomPlankHtml(wrapId, navId, mainId) {
        /*
    return '' +
        SPA 본문 교체는 wikipage.content 훅이 담당한다.
         '<div id="' + wrapId + '">' +
         body 전체 subtree를 감시하면 대문 SVG·장식·DevTools 내부 변경까지
            '<div id="' + navId + '">' +
        페이지 셸 재판정으로 증폭되므로 body 직계 자식 변화만 감시한다.
                '<div id="' + mainId + '">' +
        */
                    '<div id="clbi-bottom-shortcut-guide" class="is-empty" aria-label="단축키 안내"></div>' +
        observer.observe(document.body, {
                '</div>' +
            childList: true,
             '</div>' +
             subtree: false
         '</div>';
         });
}
    }


var CLBI_NATIONS_LAST_POINTER_X = null;
    if (document.readyState === 'loading') {
var CLBI_NATIONS_LAST_POINTER_Y = null;
        document.addEventListener('DOMContentLoaded', function () {
 
            applyPageShellClassesDeferred();
function getClbiNationsTabAtPointer(tabpanel) {
            startPageShellObserver();
     var element;
        });
    var tab;
    } else {
        applyPageShellClassesDeferred();
        startPageShellObserver();
    }
 
    if (mw.hook) {
        mw.hook('wikipage.content').add(applyPageShellClassesDeferred);
     }
 
    window.CLBI_PAGE_SHELL = {
        refresh: applyPageShellClasses,
        isBackendOrSystemPage: isBackendOrSystemPageForShell,
        isSystemAssetPage: isMediaWikiSystemAssetPageForShell,
        renderSystemDocIndicator: renderSystemDocIndicatorForShell,
        removeSystemDocIndicator: removeSystemDocIndicatorForShell,
        refreshSystemDocSourceViewer: renderSystemDocSourceViewerForShell
    };
}());


    if (!tabpanel) return null;
function loadLangScript(done) {
     if (CLBI_NATIONS_LAST_POINTER_X === null || CLBI_NATIONS_LAST_POINTER_Y === null) return null;
     $.getScript('/index.php?title=미디어위키:Lang.js&action=raw&ctype=text/javascript')
    if (typeof document.elementFromPoint !== 'function') return null;
        .done(function() {
            if (typeof done === 'function') done();
        })
        .fail(function(a, b, c) {
            console.error('Lang.js load failed:', b, c);
            if (typeof done === 'function') done();
        });
}


    element = document.elementFromPoint(CLBI_NATIONS_LAST_POINTER_X, CLBI_NATIONS_LAST_POINTER_Y);
    if (!element) return null;


    tab = element.closest ? element.closest('.clbi-nations-tabpanel-tab[data-continent]') : null;
/*
DevTools 같은 내부 스크롤은 셸 레이아웃을 바꾸지 않는다. 이 짧은 상호작용 동안
전체 화면 WebGL/CRT가 새 GPU 프레임을 계속 제출하면, transform 셸과 고정 네비의
합성 타일 갱신이 서로 경합한다. 마지막 정상 프레임은 그대로 유지하고 시각 시계도
정지시켜, 스크롤 종료 후 끊김 없이 이어지게 한다.
*/
var CLBI_COMPOSITOR_BUSY_UNTIL = 0;


     if (!tab || !tabpanel.contains(tab)) return null;
function markClbiCompositorBusy(duration) {
     var now = window.performance && performance.now ? performance.now() : Date.now();
    CLBI_COMPOSITOR_BUSY_UNTIL = Math.max(CLBI_COMPOSITOR_BUSY_UNTIL, now + Math.max(80, Number(duration) || 140));
}


     return tab;
function isClbiCompositorBusy() {
    var now = window.performance && performance.now ? performance.now() : Date.now();
     return now < CLBI_COMPOSITOR_BUSY_UNTIL;
}
}


function validateClbiNationsPointerHover(tabpanel, forceSuppress) {
window.markClbiCompositorBusy = markClbiCompositorBusy;
    var tab = getClbiNationsTabAtPointer(tabpanel);
window.isClbiCompositorBusy = isClbiCompositorBusy;
    var suppressContinent;
    var tabContinent;
    var tabIsActive;


    if (!tabpanel) return;
if (!window.CLBI_COMPOSITOR_ACTIVITY_BOUND) {
    window.CLBI_COMPOSITOR_ACTIVITY_BOUND = true;


     if (!tab) {
     document.addEventListener('scroll', function (event) {
        clearClbiNationsKeyboardHoverSuppressed(tabpanel);
        var target = event.target && event.target.nodeType === 1 ? event.target : null;
         return;
        if (target && target.closest && target.closest('#dev-tools-panel')) {
     }
            markClbiCompositorBusy(160);
         }
     }, true);


     tabContinent = tab.getAttribute('data-continent') || '';
     document.addEventListener('wheel', function (event) {
    tabIsActive = tab.classList.contains('is-active') || tab.getAttribute('aria-selected') === 'true';
        var target = event.target && event.target.nodeType === 1 ? event.target : null;
        if (target && target.closest && target.closest('#dev-tools-panel')) {
            markClbiCompositorBusy(160);
        }
    }, { capture:true, passive:true });
}


    if (tabIsActive) {
function initHalftoneBackground() {
         clearClbiNationsKeyboardHoverSuppressed(tabpanel);
    try {
         return;
         initWebGLHalftoneBackground();
    } catch (err) {
         console.error('WebGL halftone background failed:', err);
     }
     }
}


     suppressContinent = tabpanel.getAttribute('data-clbi-hover-suppress-continent') || '';
function initWebGLHalftoneBackground() {
     var canvasId = 'site-halftone-bg';
    var existing = document.getElementById(canvasId);
    var canvas = existing || document.createElement('canvas');
    var halftoneState = window.SiteHalftoneBackgroundState || (window.SiteHalftoneBackgroundState = { runId: 0 });
    var runId = halftoneState.runId + 1;


     if (forceSuppress || !suppressContinent) {
    halftoneState.runId = runId;
         tabpanel.classList.add('is-keyboard-switching');
 
         tabpanel.setAttribute('data-clbi-hover-suppress-continent', tabContinent);
     if (!existing) {
         return;
         canvas.id = canvasId;
         canvas.setAttribute('aria-hidden', 'true');
         document.body.insertBefore(canvas, document.body.firstChild || null);
     }
     }


     if (suppressContinent === tabContinent) {
     canvas.style.position = 'fixed';
         tabpanel.classList.add('is-keyboard-switching');
    canvas.style.inset = '0';
         return;
    canvas.style.width = '100vw';
    canvas.style.height = '100vh';
    canvas.style.pointerEvents = 'none';
    canvas.style.background = '#000000';
    canvas.style.display = 'block';
 
    if (!canvas.getAttribute('data-halftone-context-watch')) {
         canvas.setAttribute('data-halftone-context-watch', '1');
        canvas.addEventListener('webglcontextlost', function (event) {
            event.preventDefault();
            canvas.style.display = 'none';
            if (window.SiteHalftoneBackgroundState) {
                window.SiteHalftoneBackgroundState.contextLost = true;
                window.SiteHalftoneBackgroundState.runId += 1;
            }
        }, false);
        canvas.addEventListener('webglcontextrestored', function () {
            if (window.SiteHalftoneBackgroundState) {
                window.SiteHalftoneBackgroundState.contextLost = false;
            }
            window.setTimeout(initWebGLHalftoneBackground, 0);
         }, false);
     }
     }


     /*
     var gl = canvas.getContext('webgl', {
    * The pointer actually moved onto another tab after the keyboard switch.
        alpha: false,
    * At that point this is no longer stale browser :hover; let normal hover work.
        antialias: false,
    */
        depth: false,
     clearClbiNationsKeyboardHoverSuppressed(tabpanel);
        stencil: false,
}
        preserveDrawingBuffer: false,
        powerPreference: 'low-power'
     }) || canvas.getContext('experimental-webgl');


function validateAllClbiNationsPointerHovers() {
    if (!gl) {
    var panels = document.querySelectorAll('.clbi-nations-tabpanel.is-keyboard-switching');
        canvas.style.display = 'none';
        console.warn('WebGL background unavailable.');
        return;
    }


     Array.prototype.forEach.call(panels, function (tabpanel) {
     var vertexSrc = [
         if (isClbiNationsPanelOwnedTabpanel(tabpanel)) return;
        'attribute vec2 a_position;',
         validateClbiNationsPointerHover(tabpanel, false);
        'void main() {',
     });
         '  gl_Position = vec4(a_position, 0.0, 1.0);',
}
         '}'
     ].join('\n');


function isClbiNationsPanelOwnedTabpanel(tabpanel) {
    var fragmentSrc = [
    /*
        'precision mediump float;',
    * Mouse continent tab regression guard.
        'uniform vec2 u_resolution;',
    * -------------------------------------
        'uniform float u_time;',
    * Detached SPA preparation serializes attributes/classes but it cannot
        'const float TAU = 6.28318530718;',
    * serialize DOM event listeners or JS propertiesThe NationsPanel fix
        'float gaussian(float v, float r) {',
    * therefore uses the DOM property CLBI_NationsPanelOwned /
        '  return exp(-((v * v) / max(0.0001, r * r)));',
    * CLBI_NationsTabPanelBound as the real ownership marker.
        '}',
    *
        'float hash(vec2 p) {',
    * Do NOT treat data-nations-tabpanel-ready or clbi-nations-continent-cache
        '  return fract(sin(dot(p, vec2(127.1, 311.7))) * 43758.5453123);',
    * as ownership hereThose can survive innerHTML insertion while the real
        '}',
    * click listeners were lost, causing Common.js to delegate to a panel that
        'float bucketAlpha(float a) {',
    * NationsPanel has not rebound yetThe fallback below must remain able to
        '  float i = floor(a * 9.0);',
    * handle mouse clicks until NationsPanel reclaims the live DOM node.
        ' if (i < 1.0) return 0.040;',
    */
        '  if (i < 2.0) return 0.080;',
    return !!(tabpanel && tabpanel.CLBI_NationsPanelOwned);
        '  if (i < 3.0) return 0.135;',
}
        '  if (i < 4.0) return 0.210;',
 
        '  if (i < 5.0) return 0.310;',
function isClbiNationsLiveContinentActive(tabpanel, continent) {
        '  if (i < 6.0) return 0.430;',
    var tab;
        ' if (i < 7.0) return 0.580;',
    var panel;
        '  if (i < 8.0) return 0.760;',
 
        '  return 0.920;',
    if (!tabpanel || !continent) return false;
        '}',
 
        'void main() {',
    tab = Array.prototype.slice.call(tabpanel.querySelectorAll('.clbi-nations-tabpanel-tab[data-continent]')).find(function (candidate) {
        '  vec2 frag = gl_FragCoord.xy;',
         return candidate.getAttribute('data-continent') === continent;
        ' float spacing = 5.0;',
    });
        '  float dotSize = 1.08;',
 
        '  vec2 grid = floor(frag / spacing);',
    panel = Array.prototype.slice.call(tabpanel.querySelectorAll('.clbi-nations-tabpanel-continent[data-continent-panel]')).find(function (candidate) {
        '  vec2 inCell = mod(frag, spacing);',
         return candidate.getAttribute('data-continent-panel') === continent;
        '  vec2 dotOrigin = vec2(1.0, 1.0);',
     });
        '  vec2 dotCenter = dotOrigin + vec2(dotSize * 0.5);',
        '  vec2 local = abs(inCell - dotCenter);',
        '  float noise = hash(grid);',
        '  float size = dotSize + noise * 0.18;',
        '  float dotMask = 1.0 - smoothstep(size * 0.5, size * 0.5 + 0.22, max(local.x, local.y));',
        '  vec2 uv = frag / u_resolution;',
        ' float centerLine = 0.50 +',
        '   sin((uv.y * 1.32 + 0.08) * TAU) * 0.070 +',
        '    sin((uv.y * 3.18 + 0.34) * TAU) * 0.030;',
         '  float u = uv.x - centerLine;',
        '  float absU = abs(u);',
        '  float sideLift = smoothstep(0.065, 0.44, absU);',
        ' float valley = gaussian(u, 0.150);',
        '  float t = u_time;',
        '  float leftRibbonCenter = -0.28 + sin((uv.y * 3.20 + 0.12) * TAU) * 0.050;',
        '  float rightRibbonCenter = 0.27 + sin((uv.y * 2.85 + 0.56) * TAU) * 0.055;',
        '  float leftRibbon = gaussian(u - leftRibbonCenter, 0.105);',
        '  float rightRibbon = gaussian(u - rightRibbonCenter, 0.110);',
        '  float foldedU = u +',
        '    sin((uv.y * 4.40 + 0.22) * TAU) * 0.050 * (0.3 + sideLift) +',
        '    sin((uv.y * 7.20 + uv.x * 1.10) * TAU) * 0.022;',
        '  float verticalFold = pow(0.5 + 0.5 * cos(((foldedU * 3.05) + (sin(uv.y * TAU * 2.35) * 0.18)) * TAU), 2.5);',
        '  float diagonalFold = pow(0.5 + 0.5 * cos(((foldedU * 1.80) - (uv.y * 1.12) + 0.18) * TAU), 2.1);',
        '  float waist = gaussian(uv.y - 0.50, 0.25) * gaussian(absU - 0.20, 0.19);',
        '  float grain = (noise - 0.5) * 0.050;',
        '  float staticField =',
        '    0.055 +',
        '    sideLift * 0.210 +',
        '    (leftRibbon + rightRibbon) * 0.145 +',
        '    verticalFold * (0.055 + sideLift * 0.115) +',
        '    diagonalFold * 0.045 +',
        '    waist * 0.060 -',
        '    valley * 0.150 +',
        '    grain;',
        '  float alpha = staticField;',
        '  alpha += 0.115 * (leftRibbon + rightRibbon) * sin(t * 0.00030 + ((uv.y * 1.9) + sideLift * 0.4) * TAU);',
        '  alpha += 0.095 * verticalFold * (0.4 + sideLift) * sin(t * 0.00041 + ((uv.y * 2.7) + foldedU * 0.65) * TAU);',
         '  alpha += 0.070 * waist * sin(t * 0.00053 + ((uv.y * 3.1) - absU * 0.8) * TAU);',
        '  alpha += 0.060 * (1.0 - valley) * diagonalFold * sin(t * 0.00067 + ((uv.y * 1.4) + uv.x * 0.6) * TAU);',
        ' alpha += 0.038 * (0.35 + sideLift) * (0.35 + noise) * sin(t * 0.00079 + ((uv.y * 4.6) + noise * 0.8) * TAU);',
        '  alpha = bucketAlpha(clamp(alpha, 0.025, 0.96));',
        '  float value = alpha * dotMask;',
        '  gl_FragColor = vec4(vec3(0.8862745 * value), 1.0);',
        '}'
     ].join('\n');


     return !!(
     function compileShader(type, source) {
         tab &&
         var shader = gl.createShader(type);
        panel &&
         gl.shaderSource(shader, source);
        (tab.classList.contains('is-active') || tab.getAttribute('aria-selected') === 'true') &&
         gl.compileShader(shader);
        panel.classList.contains('is-active') &&
         panel.getAttribute('aria-hidden') !== 'true' &&
         !panel.hasAttribute('hidden')
    );
}


function activateClbiNationsContinent(tabpanel, targetContinent, options) {
        if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) {
    var tabs;
            console.error('WebGL shader compile error:', gl.getShaderInfoLog(shader));
    var panels;
            gl.deleteShader(shader);
    var skipOwner = !!(options && options.skipOwner);
            return null;
        }


    if (!tabpanel || !targetContinent) return false;
        return shader;
    }


     if (!skipOwner && isClbiNationsPanelOwnedTabpanel(tabpanel)) {
     var vertexShader = compileShader(gl.VERTEX_SHADER, vertexSrc);
        var owner = window.NationsPanel || window.CLBI_NATIONS_PANEL || null;
    var fragmentShader = compileShader(gl.FRAGMENT_SHADER, fragmentSrc);
 
    if (!vertexShader || !fragmentShader) return;


        if (owner && typeof owner.activateContinent === 'function') {
    var program = gl.createProgram();
            try {
    gl.attachShader(program, vertexShader);
                if (owner.activateContinent(targetContinent, {
    gl.attachShader(program, fragmentShader);
                    source: 'common-legacy-click',
    gl.linkProgram(program);
                    panel: tabpanel
                })) {
                    return true;
                }
            } catch (err) {}
        }


        /*
    if (!gl.getProgramParameter(program, gl.LINK_STATUS)) {
        * Ownership markers can be stale during SPA/hydration edge cases. If
        console.error('WebGL program link error:', gl.getProgramInfoLog(program));
        * the owner API is missing or refuses the live panel, do not drop the
        return;
        * mouse click. Fall through to the local fallback so pointer users are
        * never left with keyboard-only continent tabs.
        */
     }
     }


     tabs = Array.prototype.slice.call(tabpanel.querySelectorAll('.clbi-nations-tabpanel-tab[data-continent]'));
     var positionLoc = gl.getAttribLocation(program, 'a_position');
     panels = Array.prototype.slice.call(tabpanel.querySelectorAll('.clbi-nations-tabpanel-continent[data-continent-panel]'));
    var resolutionLoc = gl.getUniformLocation(program, 'u_resolution');
     var timeLoc = gl.getUniformLocation(program, 'u_time');


     if (!tabs.length || !panels.length) return false;
     var buffer = gl.createBuffer();
    gl.bindBuffer(gl.ARRAY_BUFFER, buffer);
    gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([
        -1, -1,
        1, -1,
        -1,  1,
        -1,  1,
        1, -1,
        1,  1
    ]), gl.STATIC_DRAW);


     tabs.forEach(function (tab) {
     gl.useProgram(program);
        var active = tab.getAttribute('data-continent') === targetContinent;
    gl.bindBuffer(gl.ARRAY_BUFFER, buffer);
        tab.classList.toggle('is-active', active);
    gl.enableVertexAttribArray(positionLoc);
        tab.setAttribute('aria-selected', active ? 'true' : 'false');
    gl.vertexAttribPointer(positionLoc, 2, gl.FLOAT, false, 0, 0);
        tab.setAttribute('tabindex', active ? '0' : '-1');
    });


     panels.forEach(function (panel) {
     function resize() {
         var active = panel.getAttribute('data-continent-panel') === targetContinent;
         var dpr = Math.min(window.devicePixelRatio || 1, 1.5);
         panel.classList.toggle('is-active', active);
        var cssW = Math.max(1, window.innerWidth || document.documentElement.clientWidth || 1);
        var cssH = Math.max(1, window.innerHeight || document.documentElement.clientHeight || 1);
        var w = Math.max(1, Math.floor(cssW * dpr));
         var h = Math.max(1, Math.floor(cssH * dpr));


         if (active) {
         if (canvas.width !== w || canvas.height !== h) {
             panel.removeAttribute('hidden');
             canvas.width = w;
        } else {
            canvas.height = h;
             panel.setAttribute('hidden', 'hidden');
            canvas.style.width = cssW + 'px';
            canvas.style.height = cssH + 'px';
             gl.viewport(0, 0, w, h);
         }
         }
     });
     }


    var prefersReducedMotion = false;
     try {
     try {
         if (window.Decorations && typeof window.Decorations.sync === 'function') window.Decorations.sync();
         prefersReducedMotion = window.matchMedia && window.matchMedia('(prefers-reduced-motion: reduce)').matches;
        else if (window.CLBI_DECORATIONS && typeof window.CLBI_DECORATIONS.sync === 'function') window.CLBI_DECORATIONS.sync();
     } catch (err) {}
     } catch (err) {}


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


function setClbiNationsKeyboardHoverSuppressed(tabpanel) {
        if (!globe) {
    validateClbiNationsPointerHover(tabpanel, true);
            return 'none';
}
        }


function clearClbiNationsKeyboardHoverSuppressed(tabpanel) {
        if (globe.classList && globe.classList.contains('is-dragging')) {
    if (!tabpanel) return;
            return 'active';
    tabpanel.classList.remove('is-keyboard-switching');
        }
    tabpanel.removeAttribute('data-clbi-hover-suppress-continent');
}


function moveClbiNationsContinent(direction) {
        instance = globe.CLBI_NationsGlobeInstance || null;
    var tabpanel = document.querySelector('.clbi-nations-tabpanel');
        if (instance && typeof instance.isHalftoneBackgroundBusy === 'function' && instance.isHalftoneBackgroundBusy()) {
    var tabs;
            return 'busy';
    var activeIndex;
        }
    var nextIndex;
    var target;


    if (!tabpanel) return false;
        if (globe.getAttribute && globe.getAttribute('data-clbi-globe-bg-busy') === '1') {
            return 'busy';
        }


    if (isClbiNationsPanelOwnedTabpanel(tabpanel) &&
         return 'present';
        window.CLBI_NATIONS_PANEL &&
        typeof window.CLBI_NATIONS_PANEL.moveContinent === 'function') {
         return !!window.CLBI_NATIONS_PANEL.moveContinent(direction, {
            source: 'common-legacy-keyboard',
            panel: tabpanel
        });
     }
     }


     tabs = Array.prototype.slice.call(tabpanel.querySelectorAll('.clbi-nations-tabpanel-tab[data-continent]'));
     function shouldSkipHalftoneDrawForGlobe() {
    if (!tabs.length) return false;
        var globe = getNationsGlobeHalftoneNode();
        var instance;


    activeIndex = tabs.findIndex(function (tab) {
        if (!globe) return false;
        return tab.classList.contains('is-active') || tab.getAttribute('aria-selected') === 'true';
    });


    if (activeIndex < 0) activeIndex = 0;
        instance = globe.CLBI_NationsGlobeInstance || null;


    nextIndex = (activeIndex + direction + tabs.length) % tabs.length;
        /*
    target = tabs[nextIndex].getAttribute('data-continent');
        * Background pause/resume — 20260710.
        *
        * While the nations globe is being held, keep the last rendered halftone
        * frame on screen and pause the halftone clock.  The important detail is
        * that this is not a wall-clock catch-up: skipped seconds are not replayed
        * or fast-forwarded when the pointer is released.
        */
        if (globe.classList && globe.classList.contains('is-dragging')) return true;
        if (instance && instance.dragging) return true;


    if (activateClbiNationsContinent(tabpanel, target)) {
        if (instance && typeof instance.isHalftoneBackgroundHardBusy === 'function') {
        setClbiNationsKeyboardHoverSuppressed(tabpanel);
             return instance.isHalftoneBackgroundHardBusy();
        window.setTimeout(function () {
         }
             validateClbiNationsPointerHover(tabpanel, false);
         }, 0);
        return true;
    }


    return false;
        if (globe.getAttribute && globe.getAttribute('data-clbi-globe-bg-busy') === '1') return true;
}
 
        return false;
    }


function initClbiNationsTabpanelControls(root) {
    function getFrameInterval() {
    var scope = root && root.querySelectorAll ? root : document;
        if (prefersReducedMotion) {
    var panels = scope.querySelectorAll('.clbi-nations-tabpanel');
            return 1000;
        }


    Array.prototype.forEach.call(panels, function (tabpanel) {
         /*
         /*
         * Common.js is only a safety fallback for continent tabs; NationsPanel
         * Nations background parity — 20260710.
         * is the primary ownerThis guard must be a DOM property, not a
         *
         * serialized attributeSPA warm prepaint can preserve
        * The nations page used to permanently lower the halftone cadence while
         * data-clbi-nations-tabs-ready="1" while losing the actual event
        * the globe existedProfiling showed that the steady halftone draw is
        * listeners, which was the root cause of mouse clicks no longer moving
        * normally cheap; the real contention happens during globe drag, first
         * continents while keyboard shortcuts still worked.
         * WebGL texture upload, and long layout/render tasksKeep the same
         * full-quality cadence as ordinary pages, and skip only the frames that
         * would directly collide with an active/busy globe moment.
         */
         */
         if (tabpanel.CLBI_NationsCommonTabsBound) return;
         return 66;
         tabpanel.CLBI_NationsCommonTabsBound = true;
    }
         tabpanel.setAttribute('data-clbi-nations-tabs-ready', '1');
 
    var lastFrame = 0;
    var startTime = performance.now();
    var visualTime = 0;
    var lastVisualNow = 0;
 
    function draw(now) {
         var state = getNationsGlobeHalftoneState();
         var interval = getFrameInterval();
        var perf = window.InteractionPerf;
        var delta;


         tabpanel.addEventListener('pointerdown', function () {
         if (!lastVisualNow) {
             clearClbiNationsKeyboardHoverSuppressed(tabpanel);
             lastVisualNow = now;
         });
        }
        delta = Math.max(0, Math.min(120, now - lastVisualNow));
         lastVisualNow = now;
        visualTime += delta;


         tabpanel.addEventListener('pointerleave', function () {
         function runDraw() {
             clearClbiNationsKeyboardHoverSuppressed(tabpanel);
             resize();
        });


        tabpanel.addEventListener('click', function (event) {
            gl.clearColor(0, 0, 0, 1);
             var tab = event.target.closest ? event.target.closest('.clbi-nations-tabpanel-tab[data-continent]') : null;
            gl.clear(gl.COLOR_BUFFER_BIT);
             gl.uniform2f(resolutionLoc, canvas.width, canvas.height);
            gl.uniform1f(timeLoc, visualTime);
            gl.drawArrays(gl.TRIANGLES, 0, 6);
        }


            if (!tab || !tabpanel.contains(tab)) return;
        if (perf && typeof perf.measureSync === 'function') {
            return perf.measureSync('background halftone draw', { globeState: state, interval: interval }, runDraw);
        }
        return runDraw();
    }


            clearClbiNationsKeyboardHoverSuppressed(tabpanel);
    var frameTimer = 0;
    var frameRaf = 0;


             if (activateClbiNationsContinent(tabpanel, tab.getAttribute('data-continent'))) {
    function scheduleNextFrame(delay) {
                 event.preventDefault();
        window.clearTimeout(frameTimer);
        frameTimer = window.setTimeout(function () {
            frameTimer = 0;
             if (halftoneState.runId !== runId || halftoneState.contextLost) return;
            if (window.requestAnimationFrame) {
                frameRaf = window.requestAnimationFrame(render);
            } else {
                 render(performance.now());
             }
             }
         });
         }, Math.max(16, Number(delay) || getFrameInterval()));
    }


        tabpanel.addEventListener('keydown', function (event) {
    function render(now) {
            var tab = event.target.closest ? event.target.closest('.clbi-nations-tabpanel-tab[data-continent]') : null;
        var interval = getFrameInterval();
            var handled = false;


            if (!tab || !tabpanel.contains(tab)) return;
        frameRaf = 0;
        if (halftoneState.runId !== runId || halftoneState.contextLost) return;


            if (event.key === 'ArrowLeft') handled = moveClbiNationsContinent(-1);
        if (document.hidden) {
             else if (event.key === 'ArrowRight') handled = moveClbiNationsContinent(1);
            lastVisualNow = now;
            scheduleNextFrame(250);
             return;
        }


            if (handled) {
        if (isClbiCompositorBusy()) {
                event.preventDefault();
            lastFrame = now;
                event.stopPropagation();
            lastVisualNow = now;
             }
             scheduleNextFrame(interval);
        });
            return;
    });
        }
}


function handleClbiNationsContinentDocumentClick(event) {
        if (shouldSkipHalftoneDrawForGlobe()) {
    var target;
            lastFrame = now;
    var tab;
            halftoneState.lastGlobeBusySkipAt = now;
    var tabpanel;
            lastVisualNow = now;
    var continent;
            scheduleNextFrame(interval);
    var owner;
            return;
    var handled = false;
        }


    if (!event || event.__clbiNationsContinentHandled) return;
        lastFrame = now;
    target = event.target;
        draw(now);
     if (!target || !target.closest) return;
        scheduleNextFrame(interval);
     }


     tab = target.closest('.clbi-nations-tabpanel-tab[data-continent]');
     draw(performance.now());
     if (!tab) return;
     scheduleNextFrame(getFrameInterval());


     tabpanel = tab.closest ? tab.closest('.clbi-nations-tabpanel') : null;
     document.addEventListener('visibilitychange', function () {
    if (!tabpanel || !tabpanel.contains(tab)) return;
        if (!document.hidden && halftoneState.runId === runId && !halftoneState.contextLost) {
            window.clearTimeout(frameTimer);
            if (frameRaf && window.cancelAnimationFrame) window.cancelAnimationFrame(frameRaf);
            frameRaf = 0;
            scheduleNextFrame(16);
        }
    });


    continent = tab.getAttribute('data-continent') || '';
}
    if (!continent) return;


    /*
var CLBI_SVG_BELL = '<svg class="profile-svg profile-svg-bell" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.75" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M10.268 21a2 2 0 0 0 3.464 0"/><path d="M3.262 15.326A1 1 0 0 0 4 17h16a1 1 0 0 0 .74-1.673C19.41 13.956 18 12.499 18 8A6 6 0 0 0 6 8c0 4.499-1.411 5.956-2.738 7.326"/></svg>';
    * Final live-DOM mouse delegate.
var CLBI_SVG_BELL_DOT = '<svg class="profile-svg profile-svg-bell-dot" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.75" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M10.268 21a2 2 0 0 0 3.464 0"/><path d="M11.68 2.009A6 6 0 0 0 6 8c0 4.499-1.411 5.956-2.738 7.326A1 1 0 0 0 4 17h16a1 1 0 0 0 .74-1.673c-.824-.85-1.678-1.731-2.21-3.348"/><circle cx="18" cy="5" r="3"/></svg>';
    * --------------------------------
var CLBI_SVG_LIST = '<svg class="profile-svg profile-svg-list" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.75" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M3 5h.01"/><path d="M3 12h.01"/><path d="M3 19h.01"/><path d="M8 5h13"/><path d="M8 12h13"/><path d="M8 19h13"/></svg>';
    * The continent tab UI has two initialization phases: detached preparation
var CLBI_SVG_LANGUAGES = '<svg class="profile-svg profile-svg-languages" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.75" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="m5 8 6 6"/><path d="m4 14 6-6 2-3"/><path d="M2 5h12"/><path d="M7 2h1"/><path d="m22 22-5-10-5 10"/><path d="M14 18h6"/></svg>';
    * for seamless SPA entry, then live DOM hydration. Detached preparation can
var CLBI_SVG_POWER = '<svg class="profile-svg profile-svg-power" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.75" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M12 2v10"/><path d="M18.4 6.6a9 9 0 1 1-12.77.04"/></svg>';
    * leave serialized "ready" attributes behind while losing event listeners.
var CLBI_SVG_SETTINGS = '<svg class="profile-svg profile-svg-settings" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.75" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M9.671 4.136a2.34 2.34 0 0 1 4.659 0 2.34 2.34 0 0 0 3.319 1.915 2.34 2.34 0 0 1 2.33 4.033 2.34 2.34 0 0 0 0 3.831 2.34 2.34 0 0 1-2.33 4.033 2.34 2.34 0 0 0-3.319 1.915 2.34 2.34 0 0 1-4.659 0 2.34 2.34 0 0 0-3.32-1.915 2.34 2.34 0 0 1-2.33-4.033 2.34 2.34 0 0 0 0-3.831A2.34 2.34 0 0 1 6.35 6.051a2.34 2.34 0 0 0 3.319-1.915"/><circle cx="12" cy="12" r="3"/></svg>';
    * Keyboard shortcuts still work because they call the public NationsPanel
var CLBI_SVG_SCAN_TEXT = '<svg class="profile-svg profile-svg-scan-text" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.75" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M3 7V5a2 2 0 0 1 2-2h2"/><path d="M17 3h2a2 2 0 0 1 2 2v2"/><path d="M21 17v2a2 2 0 0 1-2 2h-2"/><path d="M7 21H5a2 2 0 0 1-2-2v-2"/><path d="M7 8h8"/><path d="M7 12h10"/><path d="M7 16h6"/></svg>';
    * API directly, but mouse clicks depend on live listeners. This capturing
var CLBI_SVG_SCAN_EYE = '<svg class="profile-svg profile-svg-scan-eye" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.75" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M3 7V5a2 2 0 0 1 2-2h2"/><path d="M17 3h2a2 2 0 0 1 2 2v2"/><path d="M21 17v2a2 2 0 0 1-2 2h-2"/><path d="M7 21H5a2 2 0 0 1-2-2v-2"/><circle cx="12" cy="12" r="1"/><path d="M18.944 12.33a1 1 0 0 0 0-.66 7.5 7.5 0 0 0-13.888 0 1 1 0 0 0 0 .66 7.5 7.5 0 0 0 13.888 0"/></svg>';
    * delegate is intentionally independent of per-panel ready attributes: it
var CLBI_SVG_NEWSPAPER = '<svg class="profile-svg profile-svg-newspaper" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.75" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M15 18h-5"/><path d="M18 14h-8"/><path d="M4 22h16a2 2 0 0 0 2-2V4a2 2 0 0 0-2-2H8a2 2 0 0 0-2 2v16a2 2 0 0 1-4 0v-9a2 2 0 0 1 2-2h2"/><rect width="8" height="4" x="10" y="6" rx="1"/></svg>';
    * always resolves the clicked live tab and hands it to NationsPanel first,
var CLBI_SVG_GREAT_WALL = '<svg class="profile-svg profile-svg-great-wall lucide lucide-paint-roller" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect width="16" height="6" x="2" y="2" rx="2"/><path d="M10 16v-2a2 2 0 0 1 2-2h8a2 2 0 0 0 2-2V7a2 2 0 0 0-2-2h-2"/><rect width="4" height="6" x="8" y="16" rx="1"/></svg>';
    * then falls back to the local Common.js switcher if needed.
var CLBI_SVG_TROPHY = '<svg class="profile-svg profile-svg-trophy" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.75" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M10 14.66v1.626a2 2 0 0 1-.976 1.696A5 5 0 0 0 7 21.978"/><path d="M14 14.66v1.626a2 2 0 0 0 .976 1.696A5 5 0 0 1 17 21.978"/><path d="M18 9h1.5a1 1 0 0 0 0-5H18"/><path d="M4 22h16"/><path d="M6 9a6 6 0 0 0 12 0V3a1 1 0 0 0-1-1H7a1 1 0 0 0-1 1z"/><path d="M6 9H4.5a1 1 0 0 1 0-5H6"/></svg>';
    */
var CLBI_SVG_PACKAGE = '<svg class="profile-svg profile-svg-package" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.75" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M12 3v6"/><path d="M16.76 3a2 2 0 0 1 1.8 1.1l2.23 4.479a2 2 0 0 1 .21.891V19a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V9.472a2 2 0 0 1 .211-.894L5.45 4.1A2 2 0 0 1 7.24 3z"/><path d="M3.054 9.013h17.893"/></svg>';
    event.__clbiNationsContinentHandled = true;
    clearClbiNationsKeyboardHoverSuppressed(tabpanel);


    owner = window.NationsPanel || window.CLBI_NATIONS_PANEL || null;
var PROFILE_RENDER_TOKEN = 0;
    if (owner && typeof owner.activateContinent === 'function') {
        try {
            handled = !!owner.activateContinent(continent, {
                source: 'mouse-click-live-delegate',
                panel: tabpanel
            });
        } catch (err) {
            handled = false;
        }


        /*
function invalidateProfileRender() {
        * Owner API stale-cache guard.
    PROFILE_RENDER_TOKEN++;
        *
}
        * A previous regression came from NationsPanel returning true after it
 
        * updated cached/detached tab nodes instead of the clicked live tab. A
$(function() {
        * true return value alone is therefore not enough for mouse input. Verify
    initHalftoneBackground();
        * the live DOM that received the click. If it did not become active,
 
        * bypass the owner and run the Common.js live query fallback directly.
// ── 하단 Plank 단축키 가이드 ──
        */
function escapeClbiBottomGuideHtml(value) {
         if (handled && !isClbiNationsLiveContinentActive(tabpanel, continent)) {
    return String(value == null ? '' : value)
            handled = false;
        .replace(/&/g, '&amp;')
        }
        .replace(/</g, '&lt;')
    }
        .replace(/>/g, '&gt;')
         .replace(/"/g, '&quot;')
        .replace(/'/g, '&#039;');
}
 
function readClbiBottomGuidePageName() {
    var pageName = window.mw && mw.config ? (mw.config.get('wgPageName') || '') : '';


     if (!handled) {
     if (!pageName) {
         handled = activateClbiNationsContinent(tabpanel, continent, { skipOwner: true });
         pageName = window.location.pathname || '';
     }
     }


     if (handled) {
     return String(pageName)
         try { playStaticSound(); } catch (err2) {}
         .split('?')[0]
         event.preventDefault();
         .replace(/^\/index\.php\//, '')
         event.stopPropagation();
         .replace(/_/g, ' ')
         if (typeof event.stopImmediatePropagation === 'function') event.stopImmediatePropagation();
         .trim();
    }
}
}


function initClbiNationsGlobalClickDelegate() {
function isClbiNationsShortcutContext() {
     if (document.body.getAttribute('data-clbi-nations-global-click-ready') === '1') return;
     var pageName = readClbiBottomGuidePageName();
    document.body.setAttribute('data-clbi-nations-global-click-ready', '1');
 
    document.addEventListener('click', handleClbiNationsContinentDocumentClick, true);
    return pageName === '시대' ||
        pageName === 'Era' ||
        !!document.querySelector('.clbi-nations-panel-stack, .clbi-nations-globe-window, .clbi-nations-tabpanel');
}
}


function isClbiWikiEditShortcutContext() {
    var action = window.mw && mw.config ? String(mw.config.get('wgAction') || '') : '';
    var search = String(window.location.search || '');


function initClbiNationsPointerHoverValidation() {
    return action === 'edit' ||
    if (document.body.getAttribute('data-clbi-nations-pointer-hover-ready') === '1') return;
        action === 'submit' ||
        /(?:^|[?&])action=(?:edit|submit)(?:&|$)/.test(search) ||
        !!document.querySelector('#editform, #wpSave, input[name="wpSave"], button[name="wpSave"]');
}


    document.body.setAttribute('data-clbi-nations-pointer-hover-ready', '1');


     document.addEventListener('pointermove', function (event) {
function isClbiNationListManagerShortcutContext() {
         CLBI_NATIONS_LAST_POINTER_X = event.clientX;
     return !!document.querySelector('.nation-list-manager');
        CLBI_NATIONS_LAST_POINTER_Y = event.clientY;
}
         validateAllClbiNationsPointerHovers();
 
     }, true);
function getClbiBottomShortcutItems() {
    if (isClbiWikiEditShortcutContext()) {
         return [
            { key: 'Ctrl+S', label: '변경사항 저장' }
         ];
     }


    document.addEventListener('pointerleave', function () {
        CLBI_NATIONS_LAST_POINTER_X = null;
        CLBI_NATIONS_LAST_POINTER_Y = null;
        validateAllClbiNationsPointerHovers();
    }, true);


     window.addEventListener('blur', function () {
     if (isClbiNationListManagerShortcutContext()) {
         CLBI_NATIONS_LAST_POINTER_X = null;
         return [
        CLBI_NATIONS_LAST_POINTER_Y = null;
            { key: 'Ctrl+S', label: '국가 목록 저장' }
         validateAllClbiNationsPointerHovers();
         ];
     });
     }
}


    if (isClbiNationsShortcutContext()) {
        return [
            { key: 'Q', label: '이전 시대' },
            { key: 'E', label: '다음 시대' },
            { key: 'Shift+Q', label: '이전 연도' },
            { key: 'Shift+E', label: '다음 연도' },
            { key: 'Ctrl+Q', label: '이전 대륙' },
            { key: 'Ctrl+E', label: '다음 대륙' }
        ];
    }


function initClbiBottomShortcutSystem(root) {
     return [];
     renderClbiBottomShortcutGuide();
    initClbiNationsGlobalClickDelegate();
    initClbiNationsTabpanelControls(root || document);
    initClbiNationsPointerHoverValidation();
}
}


// ── 상·하단 네비게이션 바 ──
function renderClbiBottomShortcutGuide() {
function buildClbiNavHtml(position) {
     var guide = document.getElementById('clbi-bottom-shortcut-guide');
     var isBottom = position === 'bottom';
     var items;
    var base = isBottom ? 'clbi-bottom' : 'clbi-top';
     var html;
    var shortBase = isBottom ? 'clbi-bnav' : 'clbi-tnav';
 
    var wrapId = base + '-nav-wrap';
     if (!guide) return;
     var navId = base + '-nav';
     var mainId = base + '-nav-main';
    var tabsId = base + '-nav-tabs';
     var searchId = base + '-nav-search';
    var inputId = isBottom ? 'clbi-bottom-search-input' : 'clbi-top-search-input';
    var rightId = base + '-nav-right';
    var worldId = shortBase + '-worldbuilding';
    var infoId = shortBase + '-info';
    var subId = isBottom ? 'clbi-bottom-sub-worldbuilding' : 'clbi-sub-worldbuilding';
    var subInnerId = subId + '-inner';


     if (isBottom) {
     items = getClbiBottomShortcutItems();
        return buildClbiBottomPlankHtml(wrapId, navId, mainId);
    }


     return '' +
    if (!items.length) {
         '<div id="' + wrapId + '">' +
        guide.classList.add('is-empty');
        guide.innerHTML = '';
        return;
    }
 
    guide.classList.remove('is-empty');
 
    html = '<div class="clbi-bottom-shortcut-list">';
 
    items.forEach(function (item) {
        html += '<div class="clbi-bottom-shortcut-item">' +
            '<span class="clbi-bottom-shortcut-key">' + escapeClbiBottomGuideHtml(item.key) + '</span>' +
            '<span class="clbi-bottom-shortcut-label">' + escapeClbiBottomGuideHtml(item.label) + '</span>' +
        '</div>';
    });
 
    html += '</div>';
    guide.innerHTML = html;
}
 
function buildClbiBottomPlankHtml(wrapId, navId, mainId) {
     return '' +
         '<div id="' + wrapId + '">' +
             '<div id="' + navId + '">' +
             '<div id="' + navId + '">' +
                 '<div id="' + mainId + '">' +
                 '<div id="' + mainId + '">' +
                     '<div id="' + tabsId + '">' +
                     '<div id="clbi-bottom-shortcut-guide" class="is-empty" aria-label="단축키 안내"></div>' +
                        '<a class="clbi-top-nav-item" href="/index.php/대문">' +
                            '<img class="clbi-tnav-icon" src="/index.php?title=특수:Redirect/file/Ic-main-menu-001.png" alt="">' +
                            '<span class="clbi-tnav-label">메인 메뉴</span>' +
                        '</a>' +
                        '<a class="clbi-top-nav-item" href="/index.php/프로젝트:소개">' +
                            '<img class="clbi-tnav-icon" src="/index.php?title=특수:Redirect/file/Ic-project-001.png" alt="">' +
                            '<span class="clbi-tnav-label">프로젝트</span>' +
                        '</a>' +
                        '<div class="clbi-top-nav-item" id="' + worldId + '">' +
                            '<img class="clbi-tnav-icon" src="/index.php?title=특수:Redirect/file/Ic-worldbuilding-001.png" alt="">' +
                            '<span class="clbi-tnav-label">세계관</span>' +
                            '<span class="clbi-tnav-arrow">▾</span>' +
                        '</div>' +
                    '</div>' +
                    (isBottom ? '' : (
                        '<div id="' + searchId + '">' +
                            '<input type="text" id="' + inputId + '" placeholder="검색...">' +
                        '</div>'
                    )) +
                    '<div id="' + rightId + '">' +
                        '<div class="clbi-top-nav-item" id="' + infoId + '">' +
                            '<span class="clbi-tnav-label">ℹ</span>' +
                        '</div>' +
                    '</div>' +
                '</div>' +
                '<div id="' + subId + '">' +
                    '<div id="' + subInnerId + '">' +
                        '<div class="clbi-tnav-sub-list">' +
                            '<a class="clbi-tnav-sub-item" href="/index.php/역사적_사건">역사적 사건</a>' +
                            '<a class="clbi-tnav-sub-item" href="/index.php/설정">설정</a>' +
                            '<a class="clbi-tnav-sub-item" href="/index.php/국가_및_조합">국가 및 조합</a>' +
                            '<a class="clbi-tnav-sub-item" href="/index.php/기업_및_공동체">기업 및 공동체</a>' +
                            '<a class="clbi-tnav-sub-item" href="/index.php/군_정치집단">군, 정치집단</a>' +
                            '<a class="clbi-tnav-sub-item" href="/index.php/인물">인물</a>' +
                        '</div>' +
                    '</div>' +
                 '</div>' +
                 '</div>' +
             '</div>' +
             '</div>' +
4,156번째 줄: 4,403번째 줄:
}
}


function normalizeClbiShellDomOrder() {
var CLBI_NATIONS_LAST_POINTER_X = null;
     var contentWrapper = document.querySelector('.content-wrapper');
var CLBI_NATIONS_LAST_POINTER_Y = null;
     var topNav = document.getElementById('clbi-top-nav-wrap');
 
     var bottomNav = document.getElementById('clbi-bottom-nav-wrap');
function getClbiNationsTabAtPointer(tabpanel) {
     var canvas = document.getElementById('site-halftone-bg');
     var element;
     var anchor;
    var tab;
    var viewportH;
 
    var topRect;
    if (!tabpanel) return null;
    var wrapperRect;
     if (CLBI_NATIONS_LAST_POINTER_X === null || CLBI_NATIONS_LAST_POINTER_Y === null) return null;
    var needsRecovery;
     if (typeof document.elementFromPoint !== 'function') return null;
    var expectedWrapperTop;
 
    var host;
     element = document.elementFromPoint(CLBI_NATIONS_LAST_POINTER_X, CLBI_NATIONS_LAST_POINTER_Y);
     if (!element) return null;


     if (!contentWrapper || !topNav || !bottomNav || !document.body) return;
     tab = element.closest ? element.closest('.clbi-nations-tabpanel-tab[data-continent]') : null;


     /*
     if (!tab || !tabpanel.contains(tab)) return null;
    CLBI shell can live inside a Liberty <section>. Some skins/layouts give that
    section a flow context that lets the top nav visually overlap the content
    wrapper even when DOM sibling order is correct.  Mark the common parent and
    let Layout.css force a simple vertical flow for the shell.
    */
    host = topNav.parentElement === contentWrapper.parentElement &&
        contentWrapper.parentElement === bottomNav.parentElement
        ? contentWrapper.parentElement
        : null;


     if (host) {
     return tab;
        host.classList.add('clbi-shell-host');
}
    }


     viewportH = window.innerHeight || document.documentElement.clientHeight || 0;
function validateClbiNationsPointerHover(tabpanel, forceSuppress) {
     topRect = topNav.getBoundingClientRect();
     var tab = getClbiNationsTabAtPointer(tabpanel);
     wrapperRect = contentWrapper.getBoundingClientRect();
     var suppressContinent;
     expectedWrapperTop = topRect.bottom + 8;
     var tabContinent;
     var tabIsActive;


     needsRecovery = false;
     if (!tabpanel) return;


     if (viewportH > 0) {
     if (!tab) {
         if (topRect.top >= viewportH * 0.55) needsRecovery = true;
         clearClbiNationsKeyboardHoverSuppressed(tabpanel);
         if (wrapperRect.top >= viewportH * 0.60) needsRecovery = true;
         return;
     }
     }


     if (topRect.top > 240 || wrapperRect.top > 320) {
     tabContinent = tab.getAttribute('data-continent') || '';
         needsRecovery = true;
    tabIsActive = tab.classList.contains('is-active') || tab.getAttribute('aria-selected') === 'true';
 
    if (tabIsActive) {
         clearClbiNationsKeyboardHoverSuppressed(tabpanel);
        return;
     }
     }


     if (wrapperRect.top < expectedWrapperTop - 1) {
    suppressContinent = tabpanel.getAttribute('data-clbi-hover-suppress-continent') || '';
         needsRecovery = true;
 
     if (forceSuppress || !suppressContinent) {
        tabpanel.classList.add('is-keyboard-switching');
        tabpanel.setAttribute('data-clbi-hover-suppress-continent', tabContinent);
         return;
     }
     }


     if (!needsRecovery) {
     if (suppressContinent === tabContinent) {
         document.body.classList.add('clbi-shell-ready');
         tabpanel.classList.add('is-keyboard-switching');
         return;
         return;
     }
     }


     anchor = canvas && canvas.parentNode === document.body
     /*
        ? canvas.nextSibling
    * The pointer actually moved onto another tab after the keyboard switch.
        : document.body.firstChild;
    * At that point this is no longer stale browser :hover; let normal hover work.
    */
    clearClbiNationsKeyboardHoverSuppressed(tabpanel);
}


    document.body.insertBefore(topNav, anchor);
function validateAllClbiNationsPointerHovers() {
     document.body.insertBefore(contentWrapper, topNav.nextSibling);
     var panels = document.querySelectorAll('.clbi-nations-tabpanel.is-keyboard-switching');
    document.body.insertBefore(bottomNav, contentWrapper.nextSibling);


     document.body.classList.add('clbi-shell-ready');
     Array.prototype.forEach.call(panels, function (tabpanel) {
        if (isClbiNationsPanelOwnedTabpanel(tabpanel)) return;
        validateClbiNationsPointerHover(tabpanel, false);
    });
}
}


window.normalizeClbiShellDomOrder = normalizeClbiShellDomOrder;
function isClbiNationsPanelOwnedTabpanel(tabpanel) {
    /*
    * Mouse continent tab regression guard.
    * -------------------------------------
    * Detached SPA preparation serializes attributes/classes but it cannot
    * serialize DOM event listeners or JS properties.  The NationsPanel fix
    * therefore uses the DOM property CLBI_NationsPanelOwned /
    * CLBI_NationsTabPanelBound as the real ownership marker.
    *
    * Do NOT treat data-nations-tabpanel-ready or clbi-nations-continent-cache
    * as ownership here.  Those can survive innerHTML insertion while the real
    * click listeners were lost, causing Common.js to delegate to a panel that
    * NationsPanel has not rebound yet.  The fallback below must remain able to
    * handle mouse clicks until NationsPanel reclaims the live DOM node.
    */
    return !!(tabpanel && tabpanel.CLBI_NationsPanelOwned);
}


function isClbiNationsLiveContinentActive(tabpanel, continent) {
    var tab;
    var panel;


function ensureClbiVerticalScaleHost() {
     if (!tabpanel || !continent) return false;
    var topNav = document.getElementById('clbi-top-nav-wrap');
     var contentWrapper = document.querySelector('.content-wrapper');
    var bottomNav = document.getElementById('clbi-bottom-nav-wrap');
    var host;
    var existing;
    var parent;


     if (!topNav || !contentWrapper || !bottomNav || !document.body) return null;
     tab = Array.prototype.slice.call(tabpanel.querySelectorAll('.clbi-nations-tabpanel-tab[data-continent]')).find(function (candidate) {
        return candidate.getAttribute('data-continent') === continent;
    });


     existing = document.getElementById('clbi-shell-scale-host');
     panel = Array.prototype.slice.call(tabpanel.querySelectorAll('.clbi-nations-tabpanel-continent[data-continent-panel]')).find(function (candidate) {
        return candidate.getAttribute('data-continent-panel') === continent;
    });


     if (existing && existing.contains(topNav) && existing.contains(contentWrapper) && existing.contains(bottomNav)) {
     return !!(
         existing.classList.add('clbi-shell-host');
        tab &&
        return existing;
        panel &&
     }
        (tab.classList.contains('is-active') || tab.getAttribute('aria-selected') === 'true') &&
 
        panel.classList.contains('is-active') &&
     parent = topNav.parentElement === contentWrapper.parentElement && contentWrapper.parentElement === bottomNav.parentElement
        panel.getAttribute('aria-hidden') !== 'true' &&
        ? topNav.parentElement
         !panel.hasAttribute('hidden')
        : null;
    );
}
 
function activateClbiNationsContinent(tabpanel, targetContinent, options) {
    var tabs;
     var panels;
     var skipOwner = !!(options && options.skipOwner);
 
    if (!tabpanel || !targetContinent) return false;


     if (parent && parent !== document.body) {
     if (!skipOwner && isClbiNationsPanelOwnedTabpanel(tabpanel)) {
         parent.classList.add('clbi-shell-host');
         var owner = window.NationsPanel || window.CLBI_NATIONS_PANEL || null;
        parent.id = parent.id || 'clbi-shell-scale-host';
        return parent;
    }


    host = existing || document.createElement('div');
        if (owner && typeof owner.activateContinent === 'function') {
    host.id = 'clbi-shell-scale-host';
            try {
    host.className = 'clbi-shell-host';
                if (owner.activateContinent(targetContinent, {
                    source: 'common-legacy-click',
                    panel: tabpanel
                })) {
                    return true;
                }
            } catch (err) {}
        }


    if (!host.parentNode) {
        /*
        document.body.insertBefore(host, topNav);
        * Ownership markers can be stale during SPA/hydration edge cases. If
        * the owner API is missing or refuses the live panel, do not drop the
        * mouse click. Fall through to the local fallback so pointer users are
        * never left with keyboard-only continent tabs.
        */
     }
     }


     host.appendChild(topNav);
     tabs = Array.prototype.slice.call(tabpanel.querySelectorAll('.clbi-nations-tabpanel-tab[data-continent]'));
     host.appendChild(contentWrapper);
     panels = Array.prototype.slice.call(tabpanel.querySelectorAll('.clbi-nations-tabpanel-continent[data-continent-panel]'));
    host.appendChild(bottomNav);


     return host;
     if (!tabs.length || !panels.length) return false;
}


function readClbiRootPx(name, fallback) {
    tabs.forEach(function (tab) {
    var raw = '';
        var active = tab.getAttribute('data-continent') === targetContinent;
     var value;
        tab.classList.toggle('is-active', active);
        tab.setAttribute('aria-selected', active ? 'true' : 'false');
        tab.setAttribute('tabindex', active ? '0' : '-1');
     });


     try {
     panels.forEach(function (panel) {
         raw = getComputedStyle(document.documentElement).getPropertyValue(name);
         var active = panel.getAttribute('data-continent-panel') === targetContinent;
    } catch (err) {}
        panel.classList.toggle('is-active', active);


    value = parseFloat(raw);
        if (active) {
    return isFinite(value) && value > 0 ? value : fallback;
            panel.removeAttribute('hidden');
}
        } else {
 
            panel.setAttribute('hidden', 'hidden');
function resetLeftRecentAdaptiveState() {
        }
    var list = document.getElementById('clbi-left-recent-list');
     });
     var newsBox = list ? list.closest('.clbi-left-news-box') : null;
 
    var items = list ? Array.prototype.slice.call(list.querySelectorAll('.news-recent-item')) : [];
    try {
        if (window.Decorations && typeof window.Decorations.sync === 'function') window.Decorations.sync();
        else if (window.CLBI_DECORATIONS && typeof window.CLBI_DECORATIONS.sync === 'function') window.CLBI_DECORATIONS.sync();
    } catch (err) {}


     if (newsBox) {
     return true;
        newsBox.classList.remove('is-adaptive-constrained');
}
        newsBox.style.removeProperty('--adaptive-news-h');
    }


    if (list) {
function setClbiNationsKeyboardHoverSuppressed(tabpanel) {
        list.classList.remove('is-adaptive-faded');
    validateClbiNationsPointerHover(tabpanel, true);
        list.removeAttribute('data-adaptive-limit');
}
        list.style.removeProperty('--adaptive-recent-h');
    }


    items.forEach(function (item) {
function clearClbiNationsKeyboardHoverSuppressed(tabpanel) {
        item.classList.remove('is-adaptive-hidden');
    if (!tabpanel) return;
     });
    tabpanel.classList.remove('is-keyboard-switching');
     tabpanel.removeAttribute('data-clbi-hover-suppress-continent');
}
}


function resetLeftBillboardAdaptiveState() {
function moveClbiNationsContinent(direction) {
     var box = document.querySelector('#clbi-left-sidebar .left-billboard-box');
     var tabpanel = document.querySelector('.clbi-nations-tabpanel');
    var tabs;
    var activeIndex;
    var nextIndex;
    var target;
 
    if (!tabpanel) return false;


     if (!box) return;
     if (isClbiNationsPanelOwnedTabpanel(tabpanel) &&
        window.CLBI_NATIONS_PANEL &&
        typeof window.CLBI_NATIONS_PANEL.moveContinent === 'function') {
        return !!window.CLBI_NATIONS_PANEL.moveContinent(direction, {
            source: 'common-legacy-keyboard',
            panel: tabpanel
        });
    }


     box.classList.remove('is-left-ad-title-only', 'is-left-ad-extended');
     tabs = Array.prototype.slice.call(tabpanel.querySelectorAll('.clbi-nations-tabpanel-tab[data-continent]'));
    box.style.removeProperty('--left-billboard-h');
     if (!tabs.length) return false;
     box.style.removeProperty('--left-billboard-finish-h');
}


function updateClbiShellVerticalScale() {
    activeIndex = tabs.findIndex(function (tab) {
    var root = document.documentElement;
        return tab.classList.contains('is-active') || tab.getAttribute('aria-selected') === 'true';
    var body = document.body;
     });
     var host;
    var topInner;
    var bottomInner;
    var thresholdH;
    var gap;
    var outerGapTotal;
    var baseStageH;
    var stageW;
    var stageH;
    var viewportW;
    var viewportH;
    var availableW;
    var availableH;
    var topH;
    var bottomH;
    var contentH;
    var widthScale;
    var heightScale;
    var scale;
    var shouldScale;


     if (!root || !body) return;
     if (activeIndex < 0) activeIndex = 0;


     host = ensureClbiVerticalScaleHost();
     nextIndex = (activeIndex + direction + tabs.length) % tabs.length;
     if (!host) return;
     target = tabs[nextIndex].getAttribute('data-continent');


     thresholdH = readClbiRootPx('--clbi-vertical-scale-threshold-h', 1080);
     if (activateClbiNationsContinent(tabpanel, target)) {
    gap = readClbiRootPx('--layout-gap', 8);
        setClbiNationsKeyboardHoverSuppressed(tabpanel);
    outerGapTotal = gap * 2;
        window.setTimeout(function () {
    baseStageH = Math.max(420, thresholdH - outerGapTotal);
            validateClbiNationsPointerHover(tabpanel, false);
    stageW = readClbiRootPx('--layout-shell-w', 1880);
        }, 0);
        return true;
    }


     viewportW = Math.max(320, window.innerWidth || document.documentElement.clientWidth || stageW);
     return false;
    viewportH = Math.max(320, window.innerHeight || document.documentElement.clientHeight || thresholdH);
}
    availableW = Math.max(240, viewportW - outerGapTotal);
    availableH = Math.max(240, viewportH - outerGapTotal);


    /*
function initClbiNationsTabpanelControls(root) {
     평상시에는 기존 세로 채움 레이아웃을 기준으로 삼는다.
     var scope = root && root.querySelectorAll ? root : document;
     세로가 기준점보다 작아지는 경우에는 기준 높이를 고정점으로 삼고,
     var panels = scope.querySelectorAll('.clbi-nations-tabpanel');
    가로만 부족한 경우에는 현재 사용 가능한 세로 높이를 고정점으로 삼는다.
    이렇게 해야 가로 부족으로 scale에 들어갈 때 본문 높이가 갑자기 접히지 않는다.
    */
    stageH = availableH >= baseStageH ? availableH : baseStageH;


     topInner = document.getElementById('clbi-top-nav');
     Array.prototype.forEach.call(panels, function (tabpanel) {
    bottomInner = document.getElementById('clbi-bottom-nav');
        /*
        * Common.js is only a safety fallback for continent tabs; NationsPanel
        * is the primary owner.  This guard must be a DOM property, not a
        * serialized attribute.  SPA warm prepaint can preserve
        * data-clbi-nations-tabs-ready="1" while losing the actual event
        * listeners, which was the root cause of mouse clicks no longer moving
        * continents while keyboard shortcuts still worked.
        */
        if (tabpanel.CLBI_NationsCommonTabsBound) return;
        tabpanel.CLBI_NationsCommonTabsBound = true;
        tabpanel.setAttribute('data-clbi-nations-tabs-ready', '1');


    topH = topInner ? Math.ceil(topInner.offsetHeight || topInner.getBoundingClientRect().height || 38) : 38;
        tabpanel.addEventListener('pointerdown', function () {
    bottomH = bottomInner ? Math.ceil(bottomInner.offsetHeight || bottomInner.getBoundingClientRect().height || 38) : 38;
            clearClbiNationsKeyboardHoverSuppressed(tabpanel);
    contentH = Math.max(360, Math.floor(stageH - topH - bottomH - (gap * 2)));
        });


    widthScale = availableW < stageW ? availableW / stageW : 1;
        tabpanel.addEventListener('pointerleave', function () {
    heightScale = availableH < baseStageH ? availableH / baseStageH : 1;
            clearClbiNationsKeyboardHoverSuppressed(tabpanel);
    scale = Math.min(1, widthScale, heightScale);
        });
    scale = Math.max(0.50, Math.min(1, Math.floor(scale * 1000) / 1000));
    shouldScale = scale < 0.999;


    root.style.setProperty('--clbi-stage-design-w', stageW + 'px');
        tabpanel.addEventListener('click', function (event) {
    root.style.setProperty('--clbi-stage-design-h', Math.floor(stageH) + 'px');
            var tab = event.target.closest ? event.target.closest('.clbi-nations-tabpanel-tab[data-continent]') : null;
    root.style.setProperty('--clbi-stage-content-h', contentH + 'px');
    root.style.setProperty('--clbi-shell-scale', String(scale));


    body.classList.toggle('clbi-shell-vertical-scale', shouldScale);
            if (!tab || !tabpanel.contains(tab)) return;


    if (shouldScale) {
            clearClbiNationsKeyboardHoverSuppressed(tabpanel);
        resetLeftRecentAdaptiveState();
        resetLeftBillboardAdaptiveState();
    }
}
window.updateClbiShellVerticalScale = updateClbiShellVerticalScale;


var $contentWrapper = $('.content-wrapper').first();
            if (activateClbiNationsContinent(tabpanel, tab.getAttribute('data-continent'))) {
                event.preventDefault();
            }
        });


if ($contentWrapper.length) {
        tabpanel.addEventListener('keydown', function (event) {
    $('#clbi-top-nav-wrap, #clbi-bottom-nav-wrap').remove();
            var tab = event.target.closest ? event.target.closest('.clbi-nations-tabpanel-tab[data-continent]') : null;
    $contentWrapper.before(buildClbiNavHtml('top'));
            var handled = false;
    $contentWrapper.after(buildClbiNavHtml('bottom'));
    renderClbiBottomShortcutGuide();
    initClbiNationsTabpanelControls(document);
    if (typeof window.normalizeClbiShellDomOrder === 'function') window.normalizeClbiShellDomOrder();
}


var CLBI_SHELL_METRICS_RAF = null;
            if (!tab || !tabpanel.contains(tab)) return;


function runClbiShellMetricsBatch() {
            if (event.key === 'ArrowLeft') handled = moveClbiNationsContinent(-1);
    var top = document.getElementById('clbi-top-nav-wrap');
            else if (event.key === 'ArrowRight') handled = moveClbiNationsContinent(1);
    var bottom = document.getElementById('clbi-bottom-nav-wrap');
    var root = document.documentElement;
    var topH = 0;
    var bottomH = 0;


     CLBI_SHELL_METRICS_RAF = null;
            if (handled) {
                event.preventDefault();
                event.stopPropagation();
            }
        });
     });
}


    if (!root) return;
function handleClbiNationsContinentDocumentClick(event) {
    var target;
    var tab;
    var tabpanel;
    var continent;
    var owner;
    var handled = false;


     if (top) {
     if (!event || event.__clbiNationsContinentHandled) return;
        topH = Math.ceil(top.getBoundingClientRect().height || top.offsetHeight || 0);
    target = event.target;
    }
    if (!target || !target.closest) return;


     if (bottom) {
    tab = target.closest('.clbi-nations-tabpanel-tab[data-continent]');
        bottomH = Math.ceil(bottom.getBoundingClientRect().height || bottom.offsetHeight || 0);
     if (!tab) return;
     }
 
    tabpanel = tab.closest ? tab.closest('.clbi-nations-tabpanel') : null;
    if (!tabpanel || !tabpanel.contains(tab)) return;
 
    continent = tab.getAttribute('data-continent') || '';
    if (!continent) return;
 
    /*
    * Final live-DOM mouse delegate.
    * --------------------------------
    * The continent tab UI has two initialization phases: detached preparation
    * for seamless SPA entry, then live DOM hydration.  Detached preparation can
    * leave serialized "ready" attributes behind while losing event listeners.
    * Keyboard shortcuts still work because they call the public NationsPanel
    * API directly, but mouse clicks depend on live listeners.  This capturing
    * delegate is intentionally independent of per-panel ready attributes: it
    * always resolves the clicked live tab and hands it to NationsPanel first,
    * then falls back to the local Common.js switcher if needed.
    */
    event.__clbiNationsContinentHandled = true;
     clearClbiNationsKeyboardHoverSuppressed(tabpanel);


     root.style.setProperty('--clbi-top-nav-outer-h', topH + 'px');
     owner = window.NationsPanel || window.CLBI_NATIONS_PANEL || null;
    root.style.setProperty('--clbi-bottom-nav-outer-h', bottomH + 'px');
    if (owner && typeof owner.activateContinent === 'function') {
        try {
            handled = !!owner.activateContinent(continent, {
                source: 'mouse-click-live-delegate',
                panel: tabpanel
            });
        } catch (err) {
            handled = false;
        }


    if (typeof updateClbiShellVerticalScale === 'function') {
        /*
         updateClbiShellVerticalScale();
        * Owner API stale-cache guard.
        *
        * A previous regression came from NationsPanel returning true after it
        * updated cached/detached tab nodes instead of the clicked live tab. A
        * true return value alone is therefore not enough for mouse input. Verify
        * the live DOM that received the click. If it did not become active,
        * bypass the owner and run the Common.js live query fallback directly.
        */
        if (handled && !isClbiNationsLiveContinentActive(tabpanel, continent)) {
            handled = false;
         }
     }
     }


     if (typeof scheduleAdaptiveLeftRecentItems === 'function') {
     if (!handled) {
         scheduleAdaptiveLeftRecentItems();
         handled = activateClbiNationsContinent(tabpanel, continent, { skipOwner: true });
     }
     }


     if (typeof scheduleClbiContentBottomGap === 'function') {
     if (handled) {
        scheduleClbiContentBottomGap();
        try { playStaticSound(); } catch (err2) {}
        event.preventDefault();
        event.stopPropagation();
        if (typeof event.stopImmediatePropagation === 'function') event.stopImmediatePropagation();
     }
     }
}
}


function requestClbiShellMetricsFrame() {
function initClbiNationsGlobalClickDelegate() {
     if (CLBI_SHELL_METRICS_RAF !== null) return;
     if (document.body.getAttribute('data-clbi-nations-global-click-ready') === '1') return;
    document.body.setAttribute('data-clbi-nations-global-click-ready', '1');
    document.addEventListener('click', handleClbiNationsContinentDocumentClick, true);
}


    CLBI_SHELL_METRICS_RAF = window.requestAnimationFrame
        ? window.requestAnimationFrame(runClbiShellMetricsBatch)
        : window.setTimeout(runClbiShellMetricsBatch, 16);
}


function scheduleClbiShellMetrics() {
function initClbiNationsPointerHoverValidation() {
     requestClbiShellMetricsFrame();
     if (document.body.getAttribute('data-clbi-nations-pointer-hover-ready') === '1') return;


     window.setTimeout(requestClbiShellMetricsFrame, 0);
     document.body.setAttribute('data-clbi-nations-pointer-hover-ready', '1');
    window.setTimeout(requestClbiShellMetricsFrame, 80);
    window.setTimeout(requestClbiShellMetricsFrame, 240);
}
function watchClbiShellMetrics() {
    var top = document.getElementById('clbi-top-nav-wrap');
    var bottom = document.getElementById('clbi-bottom-nav-wrap');
    var observer;


     scheduleClbiShellMetrics();
     document.addEventListener('pointermove', function (event) {
        CLBI_NATIONS_LAST_POINTER_X = event.clientX;
        CLBI_NATIONS_LAST_POINTER_Y = event.clientY;
        validateAllClbiNationsPointerHovers();
    }, true);


    $(window).on('resize orientationchange', scheduleClbiShellMetrics);
     document.addEventListener('pointerleave', function () {
    $(window).on('pageshow.clbiShellScale focus.clbiShellScale', scheduleClbiShellMetrics);
         CLBI_NATIONS_LAST_POINTER_X = null;
     document.addEventListener('visibilitychange', function () {
        CLBI_NATIONS_LAST_POINTER_Y = null;
         if (!document.hidden) scheduleClbiShellMetrics();
        validateAllClbiNationsPointerHovers();
    });
     }, true);
    $(window).on('resize.clbiLeftBillboard orientationchange.clbiLeftBillboard', scheduleLeftSidebarVerticalFit);
     $(window).on('resize.clbiRecentViewport orientationchange.clbiRecentViewport', function () { scheduleAdaptiveLeftRecentItems(); scheduleClbiContentBottomGap(); });
    $(window).on('resize.clbiContentBottomGap orientationchange.clbiContentBottomGap', scheduleClbiContentBottomGap);


     if (window.ResizeObserver) {
     window.addEventListener('blur', function () {
        observer = new ResizeObserver(scheduleClbiShellMetrics);
         CLBI_NATIONS_LAST_POINTER_X = null;
        if (top) observer.observe(top);
         CLBI_NATIONS_LAST_POINTER_Y = null;
        if (bottom) observer.observe(bottom);
         validateAllClbiNationsPointerHovers();
        window.CLBI_SHELL_RESIZE_OBSERVER = observer;
    }
}
 
function bindClbiWorldbuildingToggle(buttonSelector, menuSelector) {
    $(buttonSelector).on('click', function() {
         var $menu = $(menuSelector);
         var $btn = $(this);
 
        $menu.toggleClass('worldbuilding-open');
        $btn.toggleClass('clbi-tnav-active', $menu.hasClass('worldbuilding-open'));
         scheduleClbiShellMetrics();
     });
     });
}
}


bindClbiWorldbuildingToggle('#clbi-tnav-worldbuilding', '#clbi-sub-worldbuilding');
bindClbiWorldbuildingToggle('#clbi-bnav-worldbuilding', '#clbi-bottom-sub-worldbuilding');
$('#clbi-top-search-input, #clbi-bottom-search-input').on('keydown', function(e) {
    if (e.key === 'Enter') {
        var q = $(this).val().trim();
        if (q) window.location.href = '/index.php?search=' + encodeURIComponent(q);
    }
});


if (window.mw && mw.hook) {
function initClbiBottomShortcutSystem(root) {
     mw.hook('wikipage.content').add(function ($content) {
     renderClbiBottomShortcutGuide();
        initClbiBottomShortcutSystem($content && $content[0] ? $content[0] : document);
    initClbiNationsGlobalClickDelegate();
     });
    initClbiNationsTabpanelControls(root || document);
     initClbiNationsPointerHoverValidation();
}
}


watchClbiShellMetrics();
// ── 상·하단 네비게이션 바 ──
function buildClbiNavHtml(position) {
    var isBottom = position === 'bottom';
    var base = isBottom ? 'clbi-bottom' : 'clbi-top';
    var shortBase = isBottom ? 'clbi-bnav' : 'clbi-tnav';
    var wrapId = base + '-nav-wrap';
    var navId = base + '-nav';
    var mainId = base + '-nav-main';
    var tabsId = base + '-nav-tabs';
    var searchId = base + '-nav-search';
    var inputId = isBottom ? 'clbi-bottom-search-input' : 'clbi-top-search-input';
    var worldId = shortBase + '-worldbuilding';
    var subId = isBottom ? 'clbi-bottom-sub-worldbuilding' : 'clbi-sub-worldbuilding';
    var subInnerId = subId + '-inner';


});
    if (isBottom) {
        return buildClbiBottomPlankHtml(wrapId, navId, mainId);
    }


// 페이지 전환 사운드
    return '' +
var transitionSound = new Audio('/index.php?title=특수:Redirect/file/Sfx-ui-001.mp3');
        '<div id="' + wrapId + '">' +
            '<div id="' + navId + '">' +
                '<div id="' + mainId + '">' +
                    '<div id="' + tabsId + '">' +
                        '<a class="clbi-top-nav-item" href="/index.php/대문">' +
                            '<img class="clbi-tnav-icon" src="/index.php?title=특수:Redirect/file/Ic-main-menu-001.png" alt="">' +
                            '<span class="clbi-tnav-label">메인 메뉴</span>' +
                        '</a>' +
                        '<a class="clbi-top-nav-item" href="/index.php/프로젝트:소개">' +
                            '<img class="clbi-tnav-icon" src="/index.php?title=특수:Redirect/file/Ic-project-001.png" alt="">' +
                            '<span class="clbi-tnav-label">프로젝트</span>' +
                        '</a>' +
                        '<div class="clbi-top-nav-item" id="' + worldId + '">' +
                            '<img class="clbi-tnav-icon" src="/index.php?title=특수:Redirect/file/Ic-worldbuilding-001.png" alt="">' +
                            '<span class="clbi-tnav-label">세계관</span>' +
                            '<span class="clbi-tnav-arrow">▾</span>' +
                        '</div>' +
                    '</div>' +
                    (isBottom ? '' : (
                        '<div id="' + searchId + '">' +
                            '<input type="text" id="' + inputId + '" placeholder="검색...">' +
                        '</div>'
                    )) +
                '</div>' +
                '<div id="' + subId + '">' +
                    '<div id="' + subInnerId + '">' +
                        '<div class="clbi-tnav-sub-list">' +
                            '<a class="clbi-tnav-sub-item" href="/index.php/시대">시대</a>' +
                            '<a class="clbi-tnav-sub-item" href="/index.php/설정">설정</a>' +
                            '<a class="clbi-tnav-sub-item" href="/index.php/기업_및_공동체">기업 및 공동체</a>' +
                            '<a class="clbi-tnav-sub-item" href="/index.php/군_정치집단">군, 정치집단</a>' +
                            '<a class="clbi-tnav-sub-item" href="/index.php/인물">인물</a>' +
                        '</div>' +
                    '</div>' +
                '</div>' +
            '</div>' +
        '</div>';
}


(function() {
function normalizeClbiShellDomOrder() {
     var master = parseFloat(localStorage.getItem('clbi-audio-master') || 80) / 100;
     var contentWrapper = document.querySelector('.content-wrapper');
     var sfx = parseFloat(localStorage.getItem('clbi-audio-sfx') || 60) / 100;
     var topNav = document.getElementById('clbi-top-nav-wrap');
     var sfxOn = localStorage.getItem('clbi-audio-sfxOn') !== 'false';
     var bottomNav = document.getElementById('clbi-bottom-nav-wrap');
     transitionSound.volume = sfxOn ? master * sfx : 0;
    var canvas = document.getElementById('site-halftone-bg');
})();
    var anchor;
    var viewportH;
    var topRect;
    var wrapperRect;
    var needsRecovery;
     var expectedWrapperTop;
    var host;


function playStaticSound() {
     if (!contentWrapper || !topNav || !bottomNav || !document.body) return;
     var master = parseFloat(localStorage.getItem('clbi-audio-master') || 80) / 100;
    var sfx = parseFloat(localStorage.getItem('clbi-audio-sfx') || 60) / 100;
    var sfxOn = localStorage.getItem('clbi-audio-sfxOn') !== 'false';


     if (!sfxOn) return;
     /*
    CLBI shell can live inside a Liberty <section>.  Some skins/layouts give that
    section a flow context that lets the top nav visually overlap the content
    wrapper even when DOM sibling order is correct.  Mark the common parent and
    let Layout.css force a simple vertical flow for the shell.
    */
    host = topNav.parentElement === contentWrapper.parentElement &&
        contentWrapper.parentElement === bottomNav.parentElement
        ? contentWrapper.parentElement
        : null;


     transitionSound.volume = master * sfx;
     if (host) {
    transitionSound.currentTime = 0;
        host.classList.add('clbi-shell-host');
    transitionSound.play();
    }
}


// 현재 언어 감지
    viewportH = window.innerHeight || document.documentElement.clientHeight || 0;
function getCurrentLang() {
    topRect = topNav.getBoundingClientRect();
     var langData = document.getElementById('clbi-lang-data');
     wrapperRect = contentWrapper.getBoundingClientRect();
     return langData ? (langData.getAttribute('data-lang') || 'ko') : 'ko';
     expectedWrapperTop = topRect.bottom + 8;
}


function normalizePageName(value) {
     needsRecovery = false;
     return String(value || '')
        .split('?')[0]
        .replace(/^\/index\.php\//, '')
        .replace(/_/g, ' ')
        .trim();
}


function buildWikiPath(title) {
    if (viewportH > 0) {
    return '/index.php/' + encodeURI(String(title || '').replace(/ /g, '_'));
        if (topRect.top >= viewportH * 0.55) needsRecovery = true;
}
        if (wrapperRect.top >= viewportH * 0.60) needsRecovery = true;
    }


function getLangShortCode(lang) {
    if (topRect.top > 240 || wrapperRect.top > 320) {
    var map = { ko: 'KR', en: 'EN', zh: 'ZH', ja: 'JA', ru: 'RU', es: 'ES' };
        needsRecovery = true;
     return map[lang] || String(lang || '').toUpperCase();
     }
}


function getLanguageTargetTitle(lang) {
    if (wrapperRect.top < expectedWrapperTop - 1) {
    var data = document.getElementById('clbi-lang-data');
        needsRecovery = true;
     if (!data || !lang) return '';
     }


     var keys = [
     if (!needsRecovery) {
         'data-' + lang,
         document.body.classList.add('clbi-shell-ready');
        'data-page-' + lang,
         return;
         'data-title-' + lang,
     }
        'data-target-' + lang,
        'data-lang-' + lang
     ];


     for (var i = 0; i < keys.length; i++) {
     anchor = canvas && canvas.parentNode === document.body
         var value = data.getAttribute(keys[i]);
         ? canvas.nextSibling
         if (value) return value;
         : document.body.firstChild;
    }


     return '';
     document.body.insertBefore(topNav, anchor);
}
    document.body.insertBefore(contentWrapper, topNav.nextSibling);
    document.body.insertBefore(bottomNav, contentWrapper.nextSibling);


function escapeClbiHtml(value) {
     document.body.classList.add('clbi-shell-ready');
     return String(value == null ? '' : value)
        .replace(/&/g, '&amp;')
        .replace(/</g, '&lt;')
        .replace(/>/g, '&gt;')
        .replace(/"/g, '&quot;')
        .replace(/'/g, '&#039;');
}
}


window.normalizeClbiShellDomOrder = normalizeClbiShellDomOrder;


var SIDEBAR_LANG_SVG_NS = 'http://www.w3.org/2000/svg';
var SIDEBAR_LANGUAGE_STATUS_TITLE = 'MediaWiki:LanguageStatus.json';
var SIDEBAR_LANGUAGE_LABELS = {
    ko: '한국어',
    en: 'English',
    zh: '中文',
    ja: '日本語',
    ru: 'Русский',
    es: 'Español'
};
var SIDEBAR_LANGUAGE_DIAL_LABELS = {
    ko: '한국어',
    en: 'ENG',
    zh: '中文',
    ja: '日本語',
    ru: 'РУС',
    es: 'ESP'
};
var SIDEBAR_LANGUAGE_STATUS_VALUES = {
    available: true,
    wip: true,
    unavailable: true
};


var sidebarLanguageStatusRegistry = {};
function ensureClbiVerticalScaleHost() {
var sidebarLanguageStatusLoaded = false;
    var topNav = document.getElementById('clbi-top-nav-wrap');
var sidebarLanguageStatusLoading = false;
    var contentWrapper = document.querySelector('.content-wrapper');
var sidebarLanguageStatusCallbacks = [];
    var bottomNav = document.getElementById('clbi-bottom-nav-wrap');
    var host;
    var existing;
    var parent;


var sidebarLanguageState = {
     if (!topNav || !contentWrapper || !bottomNav || !document.body) return null;
     order: ['ko', 'en', 'zh', 'ja', 'ru', 'es'],
    currentLang: 'ko',
    baseIndex: 0,
    selectedIndex: 0,
    rotation: 0,
    dragging: false,
    dragMoved: false,
    dragStartX: 0,
    dragStartY: 0,
    dragStartRotation: 0,
    dragAxis: null,
    pointerCaptured: false,
    lastX: 0,
    lastTime: 0,
    releaseVelocity: 0,
    suppressClickUntil: 0,
    raf: null,
    pendingRotation: 0,
    snapTimer: null,
    inertiaRaf: null,
    navigateTimer: null,
    bound: false,
    boundElement: null,
    rotor: null,
    cx: 101,
    cy: 119,
    outerR: 109,
    innerR: 28,
    sectorAngle: 30,
    halfSector: 15,
    repeats: 8,
    dragSensitivity: 0.58,
    maxSpinVelocity: 1.75,
    minSpinVelocity: 0.055,
    spinDecel: 0.00185
};


function createSidebarLanguageSvgEl(tag) {
     existing = document.getElementById('clbi-shell-scale-host');
     return document.createElementNS(SIDEBAR_LANG_SVG_NS, tag);
}


function normalizeSidebarLanguageIndex(index) {
    if (existing && existing.contains(topNav) && existing.contains(contentWrapper) && existing.contains(bottomNav)) {
    var length = sidebarLanguageState.order.length;
        existing.classList.add('clbi-shell-host');
    var normalized = index % length;
        return existing;
     return normalized < 0 ? normalized + length : normalized;
     }
}


function getSidebarLanguageName(lang) {
    parent = topNav.parentElement === contentWrapper.parentElement && contentWrapper.parentElement === bottomNav.parentElement
    return SIDEBAR_LANGUAGE_LABELS[lang] || String(lang || '').toUpperCase();
        ? topNav.parentElement
}
        : null;


function getSidebarLanguageDialName(lang) {
    if (parent && parent !== document.body) {
    return SIDEBAR_LANGUAGE_DIAL_LABELS[lang] || getSidebarLanguageName(lang);
        parent.classList.add('clbi-shell-host');
}
        parent.id = parent.id || 'clbi-shell-scale-host';
        return parent;
    }


function normalizeSidebarLanguageStatusValue(value) {
    host = existing || document.createElement('div');
     value = String(value == null ? '' : value).toLowerCase().trim();
     host.id = 'clbi-shell-scale-host';
     return SIDEBAR_LANGUAGE_STATUS_VALUES[value] ? value : '';
    host.className = 'clbi-shell-host';
 
    if (!host.parentNode) {
        document.body.insertBefore(host, topNav);
    }
 
    host.appendChild(topNav);
    host.appendChild(contentWrapper);
    host.appendChild(bottomNav);
 
     return host;
}
}


function getSidebarLanguageStatusPageKey() {
function readClbiRootPx(name, fallback) {
     var raw = String(mw.config.get('wgPageName') || '').trim();
     var raw = '';
     var normalized = normalizePageName(raw);
    var value;
 
    try {
        raw = getComputedStyle(document.documentElement).getPropertyValue(name);
     } catch (err) {}


     return normalized || raw || '대문';
    value = parseFloat(raw);
     return isFinite(value) && value > 0 ? value : fallback;
}
}


function getSidebarLanguageStatusEntry() {
function resetLeftRecentAdaptiveState() {
     var registry = sidebarLanguageStatusRegistry || {};
     var list = document.getElementById('clbi-left-recent-list');
    var pages = registry.pages && typeof registry.pages === 'object' ? registry.pages : registry;
     var newsBox = list ? list.closest('.clbi-left-news-box') : null;
     var raw = String(mw.config.get('wgPageName') || '').trim();
     var items = list ? Array.prototype.slice.call(list.querySelectorAll('.news-recent-item')) : [];
     var normalized = normalizePageName(raw);
    var title = String(mw.config.get('wgTitle') || '').trim();
    var keys = [
        normalized,
        raw,
        raw.replace(/_/g, ' '),
        normalized.replace(/ /g, '_'),
        title,
        title.replace(/_/g, ' ')
    ];
    var i;


     for (i = 0; i < keys.length; i += 1) {
     if (newsBox) {
         if (keys[i] && pages[keys[i]] && typeof pages[keys[i]] === 'object') {
         newsBox.classList.remove('is-adaptive-constrained');
            return pages[keys[i]];
         newsBox.style.removeProperty('--adaptive-news-h');
         }
     }
     }


     return {};
     if (list) {
}
        list.classList.remove('is-adaptive-faded');
        list.removeAttribute('data-adaptive-limit');
        list.style.removeProperty('--adaptive-recent-h');
    }


function getSidebarLanguageStatusOverride(lang) {
    items.forEach(function (item) {
    var entry = getSidebarLanguageStatusEntry();
        item.classList.remove('is-adaptive-hidden');
     return normalizeSidebarLanguageStatusValue(entry[lang]);
     });
}
}


function flushSidebarLanguageStatusCallbacks() {
function resetLeftBillboardAdaptiveState() {
     var callbacks = sidebarLanguageStatusCallbacks.slice();
     var box = document.querySelector('#clbi-left-sidebar .left-billboard-box');
    sidebarLanguageStatusCallbacks.length = 0;


     callbacks.forEach(function(callback) {
     if (!box) return;
        if (typeof callback === 'function') {
 
            callback(sidebarLanguageStatusRegistry);
    box.classList.remove('is-left-ad-title-only', 'is-left-ad-extended');
        }
    box.style.removeProperty('--left-billboard-h');
     });
     box.style.removeProperty('--left-billboard-finish-h');
}
}


function loadSidebarLanguageStatusRegistry(callback, force) {
function updateClbiShellVerticalScale() {
     if (typeof callback === 'function') {
     var root = document.documentElement;
        sidebarLanguageStatusCallbacks.push(callback);
    var body = document.body;
     }
    var host;
    var topInner;
    var bottomInner;
    var thresholdH;
    var gap;
    var outerGapTotal;
    var baseStageH;
    var stageW;
    var stageH;
    var viewportW;
    var viewportH;
    var availableW;
    var availableH;
    var topH;
    var bottomH;
    var contentH;
    var widthScale;
    var heightScale;
    var scale;
     var shouldScale;


     if (sidebarLanguageStatusLoaded && !force) {
     if (!root || !body) return;
        flushSidebarLanguageStatusCallbacks();
        return;
    }


     if (sidebarLanguageStatusLoading) return;
    host = ensureClbiVerticalScaleHost();
     if (!host) return;


     sidebarLanguageStatusLoading = true;
     thresholdH = readClbiRootPx('--clbi-vertical-scale-threshold-h', 1080);
    gap = readClbiRootPx('--layout-gap', 8);
    outerGapTotal = gap * 2;
    baseStageH = Math.max(420, thresholdH - outerGapTotal);
    stageW = readClbiRootPx('--layout-shell-w', 1880);


     function finishLanguageStatus(parsed) {
     viewportW = Math.max(320, window.innerWidth || document.documentElement.clientWidth || stageW);
        sidebarLanguageStatusRegistry = parsed && typeof parsed === 'object' ? parsed : {};
    viewportH = Math.max(320, window.innerHeight || document.documentElement.clientHeight || thresholdH);
        sidebarLanguageStatusLoaded = true;
    availableW = Math.max(240, viewportW - outerGapTotal);
        sidebarLanguageStatusLoading = false;
    availableH = Math.max(240, viewportH - outerGapTotal);
        flushSidebarLanguageStatusCallbacks();
    }


     if (!force && window.EntryStore && typeof window.EntryStore.fetchJsonRef === 'function') {
     /*
        window.EntryStore.fetchJsonRef(SIDEBAR_LANGUAGE_STATUS_TITLE, { noStore: false })
    평상시에는 기존 세로 채움 레이아웃을 기준으로 삼는다.
            .then(function (parsed) { finishLanguageStatus(parsed); })
    세로가 기준점보다 작아지는 경우에는 기준 높이를 고정점으로 삼고,
            .catch(function () { finishLanguageStatus({}); });
    가로만 부족한 경우에는 현재 사용 가능한 세로 높이를 고정점으로 삼는다.
         return;
    이렇게 해야 가로 부족으로 scale에 들어갈 때 본문 높이가 갑자기 접히지 않는다.
    */
    stageH = availableH >= baseStageH ? availableH : baseStageH;
 
    topInner = document.getElementById('clbi-top-nav');
    bottomInner = document.getElementById('clbi-bottom-nav');
 
    topH = topInner ? Math.ceil(topInner.offsetHeight || topInner.getBoundingClientRect().height || 38) : 38;
    bottomH = bottomInner ? Math.ceil(bottomInner.offsetHeight || bottomInner.getBoundingClientRect().height || 38) : 38;
    contentH = Math.max(360, Math.floor(stageH - topH - bottomH - (gap * 2)));
 
    widthScale = availableW < stageW ? availableW / stageW : 1;
    heightScale = availableH < baseStageH ? availableH / baseStageH : 1;
    scale = Math.min(1, widthScale, heightScale);
    scale = Math.max(0.50, Math.min(1, Math.floor(scale * 1000) / 1000));
    shouldScale = scale < 0.999;
 
    setClbiRootMetric(root, '--clbi-stage-design-w', stageW + 'px');
    setClbiRootMetric(root, '--clbi-stage-design-h', Math.floor(stageH) + 'px');
    setClbiRootMetric(root, '--clbi-stage-content-h', contentH + 'px');
    setClbiRootMetric(root, '--clbi-shell-scale', String(scale));
 
    if (body.classList.contains('clbi-shell-vertical-scale') !== shouldScale) {
         body.classList.toggle('clbi-shell-vertical-scale', shouldScale);
     }
     }


     (function () {
     if (shouldScale) {
         var url = mw.util.getUrl(SIDEBAR_LANGUAGE_STATUS_TITLE, {
         resetLeftRecentAdaptiveState();
            action: 'raw',
         resetLeftBillboardAdaptiveState();
            ctype: 'application/json'
    }
        });
}
         if (window.RevisionManifest && typeof window.RevisionManifest.addRevisionParam === 'function') {
window.updateClbiShellVerticalScale = updateClbiShellVerticalScale;
            url = window.RevisionManifest.addRevisionParam(url, SIDEBAR_LANGUAGE_STATUS_TITLE);
        }
        $.ajax({
            url: url,
            dataType: 'text',
            cache: true
        }).done(function(text) {
            var parsed = {};


            try {
var $contentWrapper = $('.content-wrapper').first();
                parsed = text ? JSON.parse(text) : {};
            } catch (err) {
                console.error('LanguageStatus.json parse failed:', err);
                parsed = {};
            }


            finishLanguageStatus(parsed);
if ($contentWrapper.length) {
        }).fail(function() {
    $('#clbi-top-nav-wrap, #clbi-bottom-nav-wrap').remove();
            finishLanguageStatus({});
    $contentWrapper.before(buildClbiNavHtml('top'));
        });
    $contentWrapper.after(buildClbiNavHtml('bottom'));
     })();
    renderClbiBottomShortcutGuide();
    initClbiNationsTabpanelControls(document);
     if (typeof window.normalizeClbiShellDomOrder === 'function') window.normalizeClbiShellDomOrder();
}
}


window.CLBI_LANGUAGE_STATUS = {
var CLBI_SHELL_METRICS_RAF = null;
    title: SIDEBAR_LANGUAGE_STATUS_TITLE,
var CLBI_SHELL_METRICS_SETTLE_TIMER = null;
    languages: sidebarLanguageState.order.slice(),
var CLBI_SHELL_METRICS_LAST = { topH:-1, bottomH:-1 };
    labels: SIDEBAR_LANGUAGE_LABELS,
    dialLabels: SIDEBAR_LANGUAGE_DIAL_LABELS,
    getPageKey: getSidebarLanguageStatusPageKey,
    getRegistry: function() {
        return sidebarLanguageStatusRegistry || {};
    },
    reload: function(callback) {
        sidebarLanguageStatusLoaded = false;
        loadSidebarLanguageStatusRegistry(function() {
            renderSidebarLanguageBox();
            if (typeof callback === 'function') callback(sidebarLanguageStatusRegistry);
        }, true);
    },
    refreshDial: function() {
        renderSidebarLanguageBox();
    }
};


function getSidebarLanguageMeta(lang) {
function setClbiRootMetric(root, name, value) {
     var currentLang = getCurrentLang();
     var next = String(value);
     var targetTitle = getLanguageTargetTitle(lang);
     if (root.style.getPropertyValue(name) === next) return false;
     var isCurrent = lang === currentLang;
    root.style.setProperty(name, next);
     return true;
}


    return {
function runClbiShellMetricsBatch() {
        lang: lang,
    var top = document.getElementById('clbi-top-nav-wrap');
        code: getLangShortCode(lang),
    var bottom = document.getElementById('clbi-bottom-nav-wrap');
        name: getSidebarLanguageName(lang),
    var root = document.documentElement;
        dialName: getSidebarLanguageDialName(lang),
    var topH = 0;
        targetTitle: targetTitle,
     var bottomH = 0;
        isCurrent: isCurrent,
        canMove: !!targetTitle && !isCurrent
     };
}


function getSidebarLanguageStatus(meta) {
    CLBI_SHELL_METRICS_RAF = null;
    var override;
    if (!root) return;


     if (!meta) {
     if (top) topH = Math.ceil(top.offsetHeight || top.getBoundingClientRect().height || 0);
        return {
     if (bottom) bottomH = Math.ceil(bottom.offsetHeight || bottom.getBoundingClientRect().height || 0);
            className: 'is-locked',
            label: 'UNAVAILABLE',
            canApply: false
        };
     }


     if (meta.isCurrent) {
     if (topH !== CLBI_SHELL_METRICS_LAST.topH) {
         return {
         CLBI_SHELL_METRICS_LAST.topH = topH;
            className: 'is-current',
        setClbiRootMetric(root, '--clbi-top-nav-outer-h', topH + 'px');
            label: 'CURRENT',
            canApply: false
        };
     }
     }
 
     if (bottomH !== CLBI_SHELL_METRICS_LAST.bottomH) {
    override = getSidebarLanguageStatusOverride(meta.lang);
         CLBI_SHELL_METRICS_LAST.bottomH = bottomH;
 
        setClbiRootMetric(root, '--clbi-bottom-nav-outer-h', bottomH + 'px');
     if (override === 'wip') {
         return {
            className: 'is-locked',
            label: 'WIP',
            canApply: false
        };
     }
     }


     if (override === 'unavailable') {
     if (typeof updateClbiShellVerticalScale === 'function') updateClbiShellVerticalScale();
        return {
    if (typeof scheduleAdaptiveLeftRecentItems === 'function') scheduleAdaptiveLeftRecentItems();
            className: 'is-locked',
    if (typeof scheduleClbiContentBottomGap === 'function') scheduleClbiContentBottomGap();
            label: 'UNAVAILABLE',
}
            canApply: false
        };
    }


     if (override === 'available' || meta.targetTitle) {
function requestClbiShellMetricsFrame() {
         return {
     if (CLBI_SHELL_METRICS_RAF !== null) return;
            className: meta.targetTitle ? 'is-ready' : 'is-locked',
    CLBI_SHELL_METRICS_RAF = window.requestAnimationFrame
            label: meta.targetTitle ? 'AVAILABLE' : 'UNAVAILABLE',
         ? window.requestAnimationFrame(runClbiShellMetricsBatch)
            canApply: !!meta.targetTitle
        : window.setTimeout(runClbiShellMetricsBatch, 16);
        };
}
    }


     return {
function scheduleClbiShellMetrics() {
         className: 'is-locked',
    requestClbiShellMetricsFrame();
         label: 'UNAVAILABLE',
     window.clearTimeout(CLBI_SHELL_METRICS_SETTLE_TIMER);
        canApply: false
    CLBI_SHELL_METRICS_SETTLE_TIMER = window.setTimeout(function () {
     };
         CLBI_SHELL_METRICS_SETTLE_TIMER = null;
         requestClbiShellMetricsFrame();
     }, 120);
}
}
function watchClbiShellMetrics() {
    var top = document.getElementById('clbi-top-nav-wrap');
    var bottom = document.getElementById('clbi-bottom-nav-wrap');
    var observer;
    scheduleClbiShellMetrics();
    $(window).on('resize orientationchange', scheduleClbiShellMetrics);
    $(window).on('pageshow.clbiShellScale focus.clbiShellScale', scheduleClbiShellMetrics);
    document.addEventListener('visibilitychange', function () {
        if (!document.hidden) scheduleClbiShellMetrics();
    });
    $(window).on('resize.clbiLeftBillboard orientationchange.clbiLeftBillboard', scheduleLeftSidebarVerticalFit);
    $(window).on('resize.clbiRecentViewport orientationchange.clbiRecentViewport', function () { scheduleAdaptiveLeftRecentItems(); scheduleClbiContentBottomGap(); });
    $(window).on('resize.clbiContentBottomGap orientationchange.clbiContentBottomGap', scheduleClbiContentBottomGap);


function sidebarLanguageRad(deg) {
    if (window.ResizeObserver) {
    return (deg * Math.PI) / 180;
        observer = new ResizeObserver(scheduleClbiShellMetrics);
        if (top) observer.observe(top);
        if (bottom) observer.observe(bottom);
        window.CLBI_SHELL_RESIZE_OBSERVER = observer;
    }
}
}


function sidebarLanguagePointAt(radius, deg) {
function bindClbiWorldbuildingToggle(buttonSelector, menuSelector) {
     var state = sidebarLanguageState;
     $(buttonSelector).on('click', function() {
    var angle = sidebarLanguageRad(deg);
        var $menu = $(menuSelector);
        var $btn = $(this);


    return {
         $menu.toggleClass('worldbuilding-open');
         x: state.cx + Math.sin(angle) * radius,
         $btn.toggleClass('clbi-tnav-active', $menu.hasClass('worldbuilding-open'));
         y: state.cy - Math.cos(angle) * radius
        scheduleClbiShellMetrics();
     };
     });
}
}


function getSidebarLanguageSectorPath(start, end) {
bindClbiWorldbuildingToggle('#clbi-tnav-worldbuilding', '#clbi-sub-worldbuilding');
    var state = sidebarLanguageState;
bindClbiWorldbuildingToggle('#clbi-bnav-worldbuilding', '#clbi-bottom-sub-worldbuilding');
    var p1 = sidebarLanguagePointAt(state.outerR, start);
    var p2 = sidebarLanguagePointAt(state.outerR, end);
    var p3 = sidebarLanguagePointAt(state.innerR, end);
    var p4 = sidebarLanguagePointAt(state.innerR, start);
    var largeArc = Math.abs(end - start) > 180 ? 1 : 0;


    return [
$('#clbi-top-search-input, #clbi-bottom-search-input').on('keydown', function(e) {
        'M', p1.x.toFixed(3), p1.y.toFixed(3),
    if (e.key === 'Enter') {
        'A', state.outerR, state.outerR, 0, largeArc, 1, p2.x.toFixed(3), p2.y.toFixed(3),
        var q = $(this).val().trim();
         'L', p3.x.toFixed(3), p3.y.toFixed(3),
         if (q) window.location.href = '/index.php?search=' + encodeURIComponent(q);
        'A', state.innerR, state.innerR, 0, largeArc, 0, p4.x.toFixed(3), p4.y.toFixed(3),
    }
         'Z'
});
     ].join(' ');
 
if (window.mw && mw.hook) {
    mw.hook('wikipage.content').add(function ($content) {
         initClbiBottomShortcutSystem($content && $content[0] ? $content[0] : document);
     });
}
}


function getSidebarLanguageShellPath() {
watchClbiShellMetrics();
     return getSidebarLanguageSectorPath(-68, 68);
 
}
});
 
// 페이지 전환 사운드
var transitionSound = new Audio('/index.php?title=특수:Redirect/file/Sfx-ui-001.mp3');
 
(function() {
     var master = parseFloat(localStorage.getItem('clbi-audio-master') || 80) / 100;
    var sfx = parseFloat(localStorage.getItem('clbi-audio-sfx') || 60) / 100;
    var sfxOn = localStorage.getItem('clbi-audio-sfxOn') !== 'false';
    transitionSound.volume = sfxOn ? master * sfx : 0;
})();
 
function playStaticSound() {
    var master = parseFloat(localStorage.getItem('clbi-audio-master') || 80) / 100;
    var sfx = parseFloat(localStorage.getItem('clbi-audio-sfx') || 60) / 100;
    var sfxOn = localStorage.getItem('clbi-audio-sfxOn') !== 'false';


function getSidebarLanguageByStep(step) {
     if (!sfxOn) return;
    var state = sidebarLanguageState;
     var index = normalizeSidebarLanguageIndex(state.baseIndex + step);


     return {
     transitionSound.volume = master * sfx;
        index: index,
    transitionSound.currentTime = 0;
        meta: getSidebarLanguageMeta(state.order[index])
    transitionSound.play();
    };
}
}


function getSidebarLanguagePreviewIndex() {
// 현재 언어 감지
     var state = sidebarLanguageState;
function getCurrentLang() {
    var step = Math.round(-state.rotation / state.sectorAngle);
     var langData = document.getElementById('clbi-lang-data');
     return normalizeSidebarLanguageIndex(state.baseIndex + step);
     return langData ? (langData.getAttribute('data-lang') || 'ko') : 'ko';
}
}


function getSidebarLanguagePreviewMeta() {
function normalizePageName(value) {
    var state = sidebarLanguageState;
     return String(value || '')
     return getSidebarLanguageMeta(state.order[getSidebarLanguagePreviewIndex()]);
        .split('?')[0]
        .replace(/^\/index\.php\//, '')
        .replace(/_/g, ' ')
        .trim();
}
}


function makeSidebarLanguageSector(step) {
function buildWikiPath(title) {
     var state = sidebarLanguageState;
     return '/index.php/' + encodeURI(String(title || '').replace(/ /g, '_'));
    var item = getSidebarLanguageByStep(step);
}
    var group = createSidebarLanguageSvgEl('g');
    var path = createSidebarLanguageSvgEl('path');
    var label = createSidebarLanguageSvgEl('text');
    var labelY = state.cy - 78;
    var angle = step * state.sectorAngle;


    group.setAttribute('class', 'sidebar-lang-sector-group');
function getLangShortCode(lang) {
     group.setAttribute('data-step', String(step));
     var map = { ko: 'KR', en: 'EN', zh: 'ZH', ja: 'JA', ru: 'RU', es: 'ES' };
    group.setAttribute('data-index', String(item.index));
    return map[lang] || String(lang || '').toUpperCase();
    group.setAttribute('data-lang', item.meta.lang);
}
    group.setAttribute('transform', 'rotate(' + angle + ' ' + state.cx + ' ' + state.cy + ')');


     path.setAttribute('class', 'sidebar-lang-sector');
function getLanguageTargetTitle(lang) {
     path.setAttribute('d', getSidebarLanguageSectorPath(-state.halfSector, state.halfSector));
     var data = document.getElementById('clbi-lang-data');
     if (!data || !lang) return '';


     label.setAttribute('class', 'sidebar-lang-sector-label');
     var keys = [
    label.setAttribute('x', String(state.cx));
        'data-' + lang,
    label.setAttribute('y', String(labelY + 5));
        'data-page-' + lang,
     label.textContent = item.meta.dialName || item.meta.name;
        'data-title-' + lang,
        'data-target-' + lang,
        'data-lang-' + lang
     ];


     group.appendChild(path);
     for (var i = 0; i < keys.length; i++) {
    group.appendChild(label);
        var value = data.getAttribute(keys[i]);
        if (value) return value;
    }


     group.addEventListener('click', function(e) {
     return '';
        if (sidebarLanguageState.dragging || performance.now() < sidebarLanguageState.suppressClickUntil) return;
}


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


        cancelSidebarLanguageSpin();
        snapSidebarLanguageToStep(parseInt(group.getAttribute('data-step') || '0', 10), true);
    });


    return group;
var SIDEBAR_LANG_SVG_NS = 'http://www.w3.org/2000/svg';
}
var SIDEBAR_LANGUAGE_STATUS_TITLE = 'MediaWiki:LanguageStatus.json';
 
var SIDEBAR_LANGUAGE_LABELS = {
function renderSidebarLanguageWheel() {
     ko: '한국어',
    var state = sidebarLanguageState;
     en: 'English',
    var fan = document.getElementById('clbi-sidebar-lang-fan');
     zh: '中文',
    var svg;
     ja: '日本語',
     var defs;
     ru: 'Русский',
     var clip;
     es: 'Español'
     var clipPath;
};
     var shadowBlur;
var SIDEBAR_LANGUAGE_DIAL_LABELS = {
     var blur;
     ko: '한국어',
     var fixedDepthGradient;
     en: 'ENG',
    var shell;
     zh: '中文',
     var clipped;
     ja: '日本語',
     var rotor;
     ru: 'РУС',
     var fixedDepthPath;
     es: 'ESP'
     var fixedFocus;
};
     var shadowSoft;
var SIDEBAR_LANGUAGE_STATUS_VALUES = {
     var shadowHard;
     available: true,
    var rim;
     wip: true,
     var pointer;
     unavailable: true
     var tri;
};
     var line;
    var step;


    if (!fan) return;
var sidebarLanguageStatusRegistry = {};
var sidebarLanguageStatusLoaded = false;
var sidebarLanguageStatusLoading = false;
var sidebarLanguageStatusCallbacks = [];


     fan.innerHTML = '';
var sidebarLanguageState = {
 
     order: ['ko', 'en', 'zh', 'ja', 'ru', 'es'],
    svg = createSidebarLanguageSvgEl('svg');
     currentLang: 'ko',
    svg.setAttribute('class', 'sidebar-lang-fan-svg');
    baseIndex: 0,
     svg.setAttribute('viewBox', '0 0 202 150');
    selectedIndex: 0,
     svg.setAttribute('preserveAspectRatio', 'xMidYMid meet');
    rotation: 0,
     svg.setAttribute('role', 'img');
    dragging: false,
     svg.setAttribute('aria-label', '언어 선택 다이얼');
    dragMoved: false,
 
    dragStartX: 0,
     defs = createSidebarLanguageSvgEl('defs');
    dragStartY: 0,
 
    dragStartRotation: 0,
     clip = createSidebarLanguageSvgEl('clipPath');
    dragAxis: null,
     clip.setAttribute('id', 'clbi-sidebar-language-fan-clip');
    pointerCaptured: false,
     clipPath = createSidebarLanguageSvgEl('path');
    lastX: 0,
     clipPath.setAttribute('d', getSidebarLanguageShellPath());
    lastTime: 0,
     clip.appendChild(clipPath);
    releaseVelocity: 0,
    suppressClickUntil: 0,
    raf: null,
    pendingRotation: 0,
    snapTimer: null,
    inertiaRaf: null,
    navigateTimer: null,
    bound: false,
    boundElement: null,
    rotor: null,
     cx: 101,
     cy: 119,
     outerR: 109,
    innerR: 28,
     sectorAngle: 30,
    halfSector: 15,
     repeats: 8,
     dragSensitivity: 0.58,
     maxSpinVelocity: 1.75,
     minSpinVelocity: 0.055,
     spinDecel: 0.00185
};


    shadowBlur = createSidebarLanguageSvgEl('filter');
function createSidebarLanguageSvgEl(tag) {
    shadowBlur.setAttribute('id', 'clbi-sidebar-language-shadow-blur');
     return document.createElementNS(SIDEBAR_LANG_SVG_NS, tag);
    shadowBlur.setAttribute('x', '-20%');
}
    shadowBlur.setAttribute('y', '-20%');
    shadowBlur.setAttribute('width', '140%');
    shadowBlur.setAttribute('height', '140%');
    blur = createSidebarLanguageSvgEl('feGaussianBlur');
     blur.setAttribute('stdDeviation', '3');
    shadowBlur.appendChild(blur);


    fixedDepthGradient = createSidebarLanguageSvgEl('linearGradient');
function normalizeSidebarLanguageIndex(index) {
     fixedDepthGradient.setAttribute('id', 'clbi-sidebar-language-fixed-depth');
     var length = sidebarLanguageState.order.length;
    fixedDepthGradient.setAttribute('x1', '0');
     var normalized = index % length;
     fixedDepthGradient.setAttribute('y1', '0');
     return normalized < 0 ? normalized + length : normalized;
     fixedDepthGradient.setAttribute('x2', '0');
}
    fixedDepthGradient.setAttribute('y2', '1');


    [
function getSidebarLanguageName(lang) {
        ['0%', '#ffffff', '0.030'],
    return SIDEBAR_LANGUAGE_LABELS[lang] || String(lang || '').toUpperCase();
        ['34%', '#ffffff', '0.006'],
}
        ['58%', '#000000', '0.030'],
        ['100%', '#000000', '0.250']
    ].forEach(function(item) {
        var stop = createSidebarLanguageSvgEl('stop');
        stop.setAttribute('offset', item[0]);
        stop.setAttribute('stop-color', item[1]);
        stop.setAttribute('stop-opacity', item[2]);
        fixedDepthGradient.appendChild(stop);
    });


     defs.appendChild(clip);
function getSidebarLanguageDialName(lang) {
     defs.appendChild(shadowBlur);
     return SIDEBAR_LANGUAGE_DIAL_LABELS[lang] || getSidebarLanguageName(lang);
    defs.appendChild(fixedDepthGradient);
}
     svg.appendChild(defs);
 
function normalizeSidebarLanguageStatusValue(value) {
     value = String(value == null ? '' : value).toLowerCase().trim();
     return SIDEBAR_LANGUAGE_STATUS_VALUES[value] ? value : '';
}


    shell = createSidebarLanguageSvgEl('path');
function getSidebarLanguageStatusPageKey() {
     shell.setAttribute('class', 'sidebar-lang-shell');
     var raw = String(mw.config.get('wgPageName') || '').trim();
    shell.setAttribute('d', getSidebarLanguageShellPath());
     var normalized = normalizePageName(raw);
     svg.appendChild(shell);


     clipped = createSidebarLanguageSvgEl('g');
     return normalized || raw || '대문';
    clipped.setAttribute('clip-path', 'url(#clbi-sidebar-language-fan-clip)');
}


     rotor = createSidebarLanguageSvgEl('g');
function getSidebarLanguageStatusEntry() {
     rotor.setAttribute('id', 'clbi-sidebar-lang-wheel-rotor');
    var registry = sidebarLanguageStatusRegistry || {};
     rotor.setAttribute('class', 'sidebar-lang-wheel-rotor');
     var pages = registry.pages && typeof registry.pages === 'object' ? registry.pages : registry;
    var raw = String(mw.config.get('wgPageName') || '').trim();
     var normalized = normalizePageName(raw);
    var title = String(mw.config.get('wgTitle') || '').trim();
     var keys = [
        normalized,
        raw,
        raw.replace(/_/g, ' '),
        normalized.replace(/ /g, '_'),
        title,
        title.replace(/_/g, ' ')
    ];
    var i;


     for (step = -state.repeats; step <= state.repeats; step += 1) {
     for (i = 0; i < keys.length; i += 1) {
         rotor.appendChild(makeSidebarLanguageSector(step));
         if (keys[i] && pages[keys[i]] && typeof pages[keys[i]] === 'object') {
            return pages[keys[i]];
        }
     }
     }


     clipped.appendChild(rotor);
     return {};
    svg.appendChild(clipped);
}


    fixedDepthPath = createSidebarLanguageSvgEl('path');
function getSidebarLanguageStatusOverride(lang) {
     fixedDepthPath.setAttribute('class', 'sidebar-lang-fixed-depth');
     var entry = getSidebarLanguageStatusEntry();
     fixedDepthPath.setAttribute('d', getSidebarLanguageShellPath());
     return normalizeSidebarLanguageStatusValue(entry[lang]);
    svg.appendChild(fixedDepthPath);
}


    fixedFocus = createSidebarLanguageSvgEl('path');
function flushSidebarLanguageStatusCallbacks() {
     fixedFocus.setAttribute('class', 'sidebar-lang-fixed-focus');
     var callbacks = sidebarLanguageStatusCallbacks.slice();
     fixedFocus.setAttribute('d', getSidebarLanguageSectorPath(-state.halfSector, state.halfSector));
     sidebarLanguageStatusCallbacks.length = 0;
    svg.appendChild(fixedFocus);


     shadowSoft = createSidebarLanguageSvgEl('path');
     callbacks.forEach(function(callback) {
    shadowSoft.setAttribute('class', 'sidebar-lang-inner-shadow-soft');
        if (typeof callback === 'function') {
    shadowSoft.setAttribute('d', getSidebarLanguageShellPath());
            callback(sidebarLanguageStatusRegistry);
     svg.appendChild(shadowSoft);
        }
     });
}


    shadowHard = createSidebarLanguageSvgEl('path');
function loadSidebarLanguageStatusRegistry(callback, force) {
     shadowHard.setAttribute('class', 'sidebar-lang-inner-shadow-hard');
     if (typeof callback === 'function') {
    shadowHard.setAttribute('d', getSidebarLanguageShellPath());
        sidebarLanguageStatusCallbacks.push(callback);
     svg.appendChild(shadowHard);
     }


     rim = createSidebarLanguageSvgEl('path');
     if (sidebarLanguageStatusLoaded && !force) {
    rim.setAttribute('class', 'sidebar-lang-rim');
        flushSidebarLanguageStatusCallbacks();
    rim.setAttribute('d', getSidebarLanguageShellPath());
        return;
     svg.appendChild(rim);
     }


     pointer = createSidebarLanguageSvgEl('g');
     if (sidebarLanguageStatusLoading) return;
    pointer.setAttribute('class', 'sidebar-lang-fixed-pointer');
    pointer.setAttribute('clip-path', 'url(#clbi-sidebar-language-fan-clip)');


     tri = createSidebarLanguageSvgEl('path');
     sidebarLanguageStatusLoading = true;
    tri.setAttribute('class', 'sidebar-lang-pointer-triangle');
    tri.setAttribute('d', 'M ' + (state.cx - 10) + ' 10 L ' + (state.cx + 10) + ' 10 L ' + state.cx + ' 26 Z');
    pointer.appendChild(tri);


     line = createSidebarLanguageSvgEl('line');
     function finishLanguageStatus(parsed) {
    line.setAttribute('class', 'sidebar-lang-pointer-line');
        sidebarLanguageStatusRegistry = parsed && typeof parsed === 'object' ? parsed : {};
    line.setAttribute('x1', String(state.cx));
        sidebarLanguageStatusLoaded = true;
    line.setAttribute('x2', String(state.cx));
        sidebarLanguageStatusLoading = false;
    line.setAttribute('y1', '24');
        flushSidebarLanguageStatusCallbacks();
     line.setAttribute('y2', '112');
     }
    pointer.appendChild(line);


     svg.appendChild(pointer);
     if (!force && window.EntryStore && typeof window.EntryStore.fetchJsonRef === 'function') {
    fan.appendChild(svg);
        window.EntryStore.fetchJsonRef(SIDEBAR_LANGUAGE_STATUS_TITLE, { noStore: false })
 
            .then(function (parsed) { finishLanguageStatus(parsed); })
    state.rotor = rotor;
            .catch(function () { finishLanguageStatus({}); });
    setSidebarLanguageRotation(state.rotation, false);
         return;
}
 
function updateSidebarLanguageDial() {
    var state = sidebarLanguageState;
    var meta = getSidebarLanguagePreviewMeta();
    var status = getSidebarLanguageStatus(meta);
    var selector = document.getElementById('clbi-sidebar-lang-selector');
    var apply = document.getElementById('clbi-sidebar-lang-apply');
    var selectedValue = document.getElementById('clbi-sidebar-lang-selected-value');
    var availabilityPanel = document.getElementById('clbi-sidebar-lang-availability-panel');
    var availabilityValue = document.getElementById('clbi-sidebar-lang-availability-value');
 
    if (selectedValue) {
         selectedValue.textContent = meta.name;
     }
     }


     if (availabilityPanel) {
     (function () {
         availabilityPanel.classList.remove('is-ready', 'is-current', 'is-locked');
         var url = mw.util.getUrl(SIDEBAR_LANGUAGE_STATUS_TITLE, {
         availabilityPanel.classList.add(status.className);
            action: 'raw',
    }
            ctype: 'application/json'
        });
        if (window.RevisionManifest && typeof window.RevisionManifest.addRevisionParam === 'function') {
            url = window.RevisionManifest.addRevisionParam(url, SIDEBAR_LANGUAGE_STATUS_TITLE);
         }
        $.ajax({
            url: url,
            dataType: 'text',
            cache: true
        }).done(function(text) {
            var parsed = {};


    if (availabilityValue) {
            try {
        availabilityValue.textContent = status.label;
                parsed = text ? JSON.parse(text) : {};
    }
            } catch (err) {
                console.error('LanguageStatus.json parse failed:', err);
                parsed = {};
            }


    if (apply) {
            finishLanguageStatus(parsed);
         apply.classList.toggle('is-disabled', !status.canApply);
         }).fail(function() {
         apply.setAttribute('aria-disabled', status.canApply ? 'false' : 'true');
            finishLanguageStatus({});
        apply.setAttribute('aria-label', status.canApply ? (meta.name + ' 적용') : (meta.isCurrent ? '현재 언어' : '사용할 수 없는 언어'));
         });
    }
    })();
}


     if (selector) {
window.CLBI_LANGUAGE_STATUS = {
         selector.setAttribute('data-selected-lang', meta.lang);
    title: SIDEBAR_LANGUAGE_STATUS_TITLE,
         selector.setAttribute('data-selected-code', meta.code);
    languages: sidebarLanguageState.order.slice(),
        selector.classList.toggle('is-current', meta.isCurrent);
    labels: SIDEBAR_LANGUAGE_LABELS,
        selector.classList.toggle('is-ready', status.canApply);
    dialLabels: SIDEBAR_LANGUAGE_DIAL_LABELS,
         selector.classList.toggle('is-locked', !status.canApply && !meta.isCurrent);
    getPageKey: getSidebarLanguageStatusPageKey,
        selector.classList.toggle('is-dragging', !!state.dragging);
     getRegistry: function() {
         selector.classList.toggle('is-spinning', !!state.inertiaRaf);
         return sidebarLanguageStatusRegistry || {};
    },
    reload: function(callback) {
        sidebarLanguageStatusLoaded = false;
         loadSidebarLanguageStatusRegistry(function() {
            renderSidebarLanguageBox();
            if (typeof callback === 'function') callback(sidebarLanguageStatusRegistry);
         }, true);
    },
    refreshDial: function() {
         renderSidebarLanguageBox();
     }
     }
};


     return {
function getSidebarLanguageMeta(lang) {
         meta: meta,
    var currentLang = getCurrentLang();
         status: status
    var targetTitle = getLanguageTargetTitle(lang);
    var isCurrent = lang === currentLang;
 
     return {
         lang: lang,
        code: getLangShortCode(lang),
        name: getSidebarLanguageName(lang),
        dialName: getSidebarLanguageDialName(lang),
        targetTitle: targetTitle,
        isCurrent: isCurrent,
         canMove: !!targetTitle && !isCurrent
     };
     };
}
}


function setSidebarLanguageRotation(value, animate) {
function getSidebarLanguageStatus(meta) {
     var state = sidebarLanguageState;
     var override;
    state.rotation = value;
    updateSidebarLanguageDial();


     if (!state.rotor) return;
     if (!meta) {
        return {
            className: 'is-locked',
            label: 'UNAVAILABLE',
            canApply: false
        };
    }


     if (animate) {
     if (meta.isCurrent) {
         $('#clbi-sidebar-lang-selector').addClass('is-snapping');
         return {
    } else {
            className: 'is-current',
         $('#clbi-sidebar-lang-selector').removeClass('is-snapping');
            label: 'CURRENT',
            canApply: false
         };
     }
     }


     state.rotor.style.transform = 'rotate(' + state.rotation.toFixed(3) + 'deg)';
     override = getSidebarLanguageStatusOverride(meta.lang);
}


function requestSidebarLanguageRotation(value) {
    if (override === 'wip') {
    var state = sidebarLanguageState;
        return {
     state.pendingRotation = value;
            className: 'is-locked',
            label: 'WIP',
            canApply: false
        };
     }


     if (state.raf) return;
     if (override === 'unavailable') {
        return {
            className: 'is-locked',
            label: 'UNAVAILABLE',
            canApply: false
        };
    }


     state.raf = requestAnimationFrame(function() {
     if (override === 'available' || meta.targetTitle) {
         state.raf = null;
         return {
        setSidebarLanguageRotation(state.pendingRotation, false);
            className: meta.targetTitle ? 'is-ready' : 'is-locked',
    });
            label: meta.targetTitle ? 'AVAILABLE' : 'UNAVAILABLE',
}
            canApply: !!meta.targetTitle
 
        };
function cancelSidebarLanguageSpin() {
    }
    var state = sidebarLanguageState;


     if (state.inertiaRaf) {
     return {
         cancelAnimationFrame(state.inertiaRaf);
         className: 'is-locked',
         state.inertiaRaf = null;
        label: 'UNAVAILABLE',
    }
         canApply: false
    };
}


     $('#clbi-sidebar-lang-selector').removeClass('is-spinning');
function sidebarLanguageRad(deg) {
     return (deg * Math.PI) / 180;
}
}


function finishSidebarLanguageSnap(nearestIndex, callback) {
function sidebarLanguagePointAt(radius, deg) {
     var state = sidebarLanguageState;
     var state = sidebarLanguageState;
    var angle = sidebarLanguageRad(deg);


     state.baseIndex = normalizeSidebarLanguageIndex(nearestIndex);
     return {
    state.selectedIndex = state.baseIndex;
        x: state.cx + Math.sin(angle) * radius,
     state.rotation = 0;
        y: state.cy - Math.cos(angle) * radius
    state.dragging = false;
     };
}


     $('#clbi-sidebar-lang-selector').removeClass('is-snapping is-dragging is-spinning');
function getSidebarLanguageSectorPath(start, end) {
    var state = sidebarLanguageState;
    var p1 = sidebarLanguagePointAt(state.outerR, start);
    var p2 = sidebarLanguagePointAt(state.outerR, end);
    var p3 = sidebarLanguagePointAt(state.innerR, end);
     var p4 = sidebarLanguagePointAt(state.innerR, start);
    var largeArc = Math.abs(end - start) > 180 ? 1 : 0;


     renderSidebarLanguageWheel();
     return [
     updateSidebarLanguageDial();
        'M', p1.x.toFixed(3), p1.y.toFixed(3),
        'A', state.outerR, state.outerR, 0, largeArc, 1, p2.x.toFixed(3), p2.y.toFixed(3),
        'L', p3.x.toFixed(3), p3.y.toFixed(3),
        'A', state.innerR, state.innerR, 0, largeArc, 0, p4.x.toFixed(3), p4.y.toFixed(3),
        'Z'
     ].join(' ');
}


    if (typeof callback === 'function') {
function getSidebarLanguageShellPath() {
        callback(getSidebarLanguageMeta(state.order[state.selectedIndex]));
    return getSidebarLanguageSectorPath(-68, 68);
    }
}
}


function snapSidebarLanguageToStep(step, animate, callback) {
function getSidebarLanguageByStep(step) {
     var state = sidebarLanguageState;
     var state = sidebarLanguageState;
     var targetRotation = -step * state.sectorAngle;
     var index = normalizeSidebarLanguageIndex(state.baseIndex + step);
    var nearestIndex = normalizeSidebarLanguageIndex(state.baseIndex + step);


     cancelSidebarLanguageSpin();
     return {
    clearTimeout(state.snapTimer);
        index: index,
        meta: getSidebarLanguageMeta(state.order[index])
    };
}


     state.selectedIndex = nearestIndex;
function getSidebarLanguagePreviewIndex() {
     setSidebarLanguageRotation(targetRotation, !!animate);
     var state = sidebarLanguageState;
 
     var step = Math.round(-state.rotation / state.sectorAngle);
     state.snapTimer = setTimeout(function() {
     return normalizeSidebarLanguageIndex(state.baseIndex + step);
        finishSidebarLanguageSnap(nearestIndex, callback);
    }, animate ? 230 : 0);
}
}


function snapSidebarLanguageNearest(callback) {
function getSidebarLanguagePreviewMeta() {
     var state = sidebarLanguageState;
     var state = sidebarLanguageState;
     var step = Math.round(-state.rotation / state.sectorAngle);
     return getSidebarLanguageMeta(state.order[getSidebarLanguagePreviewIndex()]);
    snapSidebarLanguageToStep(step, true, callback);
}
}


function startSidebarLanguageInertiaSpin(initialVelocity) {
function makeSidebarLanguageSector(step) {
     var state = sidebarLanguageState;
     var state = sidebarLanguageState;
     var velocity;
     var item = getSidebarLanguageByStep(step);
     var lastFrame;
     var group = createSidebarLanguageSvgEl('g');
    var path = createSidebarLanguageSvgEl('path');
    var label = createSidebarLanguageSvgEl('text');
    var labelY = state.cy - 78;
    var angle = step * state.sectorAngle;


     cancelSidebarLanguageSpin();
     group.setAttribute('class', 'sidebar-lang-sector-group');
    group.setAttribute('data-step', String(step));
    group.setAttribute('data-index', String(item.index));
    group.setAttribute('data-lang', item.meta.lang);
    group.setAttribute('transform', 'rotate(' + angle + ' ' + state.cx + ' ' + state.cy + ')');


     velocity = Math.max(-state.maxSpinVelocity, Math.min(state.maxSpinVelocity, initialVelocity));
     path.setAttribute('class', 'sidebar-lang-sector');
    path.setAttribute('d', getSidebarLanguageSectorPath(-state.halfSector, state.halfSector));


     if (Math.abs(velocity) < state.minSpinVelocity) {
     label.setAttribute('class', 'sidebar-lang-sector-label');
        snapSidebarLanguageNearest();
    label.setAttribute('x', String(state.cx));
        return;
    label.setAttribute('y', String(labelY + 5));
    }
    label.textContent = item.meta.dialName || item.meta.name;


     $('#clbi-sidebar-lang-selector').addClass('is-spinning');
     group.appendChild(path);
     lastFrame = performance.now();
     group.appendChild(label);


     function frame(now) {
     group.addEventListener('click', function(e) {
         var dt = Math.min(34, Math.max(1, now - lastFrame));
         if (sidebarLanguageState.dragging || performance.now() < sidebarLanguageState.suppressClickUntil) return;
        var sign = velocity < 0 ? -1 : 1;
        var nextSpeed;


         lastFrame = now;
         e.preventDefault();
         state.rotation += velocity * dt;
         e.stopPropagation();
        setSidebarLanguageRotation(state.rotation, false);


         nextSpeed = Math.max(0, Math.abs(velocity) - (state.spinDecel * dt));
         cancelSidebarLanguageSpin();
        velocity = sign * nextSpeed;
        snapSidebarLanguageToStep(parseInt(group.getAttribute('data-step') || '0', 10), true);
    });


        if (nextSpeed <= state.minSpinVelocity) {
    return group;
            state.inertiaRaf = null;
}
            $('#clbi-sidebar-lang-selector').removeClass('is-spinning');
            snapSidebarLanguageNearest();
            return;
        }


        state.inertiaRaf = requestAnimationFrame(frame);
function renderSidebarLanguageWheel() {
     }
    var state = sidebarLanguageState;
    var fan = document.getElementById('clbi-sidebar-lang-fan');
     var svg;
    var defs;
    var clip;
    var clipPath;
    var shadowBlur;
    var blur;
    var fixedDepthGradient;
    var shell;
    var clipped;
    var rotor;
    var fixedDepthPath;
    var fixedFocus;
    var shadowSoft;
    var shadowHard;
    var rim;
    var pointer;
    var tri;
    var line;
    var step;


     state.inertiaRaf = requestAnimationFrame(frame);
     if (!fan) return;
}


function scheduleSidebarLanguageNavigation(meta) {
     fan.innerHTML = '';
     var status = getSidebarLanguageStatus(meta);


     if (!meta || !status.canApply) return;
     svg = createSidebarLanguageSvgEl('svg');
    svg.setAttribute('class', 'sidebar-lang-fan-svg');
    svg.setAttribute('viewBox', '0 0 202 150');
    svg.setAttribute('preserveAspectRatio', 'xMidYMid meet');
    svg.setAttribute('role', 'img');
    svg.setAttribute('aria-label', '언어 선택 다이얼');


     clearTimeout(sidebarLanguageState.navigateTimer);
     defs = createSidebarLanguageSvgEl('defs');
    sidebarLanguageState.navigateTimer = setTimeout(function() {
        var title = getLanguageTargetTitle(meta.lang);


        if (!title || meta.lang === getCurrentLang()) return;
    clip = createSidebarLanguageSvgEl('clipPath');
    clip.setAttribute('id', 'clbi-sidebar-language-fan-clip');
    clipPath = createSidebarLanguageSvgEl('path');
    clipPath.setAttribute('d', getSidebarLanguageShellPath());
    clip.appendChild(clipPath);


        window.location.href = buildWikiPath(title);
    shadowBlur = createSidebarLanguageSvgEl('filter');
     }, 70);
    shadowBlur.setAttribute('id', 'clbi-sidebar-language-shadow-blur');
}
    shadowBlur.setAttribute('x', '-20%');
    shadowBlur.setAttribute('y', '-20%');
    shadowBlur.setAttribute('width', '140%');
    shadowBlur.setAttribute('height', '140%');
    blur = createSidebarLanguageSvgEl('feGaussianBlur');
     blur.setAttribute('stdDeviation', '3');
    shadowBlur.appendChild(blur);


function setSidebarLanguageSelection(lang) {
    fixedDepthGradient = createSidebarLanguageSvgEl('linearGradient');
     var state = sidebarLanguageState;
     fixedDepthGradient.setAttribute('id', 'clbi-sidebar-language-fixed-depth');
     var index = state.order.indexOf(lang);
    fixedDepthGradient.setAttribute('x1', '0');
    fixedDepthGradient.setAttribute('y1', '0');
     fixedDepthGradient.setAttribute('x2', '0');
    fixedDepthGradient.setAttribute('y2', '1');


     if (index < 0) index = state.order.indexOf(getCurrentLang());
     [
     if (index < 0) index = 0;
        ['0%', '#ffffff', '0.030'],
        ['34%', '#ffffff', '0.006'],
        ['58%', '#000000', '0.030'],
        ['100%', '#000000', '0.250']
    ].forEach(function(item) {
        var stop = createSidebarLanguageSvgEl('stop');
        stop.setAttribute('offset', item[0]);
        stop.setAttribute('stop-color', item[1]);
        stop.setAttribute('stop-opacity', item[2]);
        fixedDepthGradient.appendChild(stop);
     });


     if (state.raf) {
     defs.appendChild(clip);
        cancelAnimationFrame(state.raf);
    defs.appendChild(shadowBlur);
        state.raf = null;
    defs.appendChild(fixedDepthGradient);
     }
     svg.appendChild(defs);


     cancelSidebarLanguageSpin();
     shell = createSidebarLanguageSvgEl('path');
     clearTimeout(state.snapTimer);
     shell.setAttribute('class', 'sidebar-lang-shell');
    shell.setAttribute('d', getSidebarLanguageShellPath());
    svg.appendChild(shell);


     state.currentLang = lang;
     clipped = createSidebarLanguageSvgEl('g');
    state.baseIndex = index;
     clipped.setAttribute('clip-path', 'url(#clbi-sidebar-language-fan-clip)');
    state.selectedIndex = index;
     state.rotation = 0;
    state.dragging = false;
    state.dragMoved = false;
    state.releaseVelocity = 0;


     renderSidebarLanguageWheel();
     rotor = createSidebarLanguageSvgEl('g');
     updateSidebarLanguageDial();
     rotor.setAttribute('id', 'clbi-sidebar-lang-wheel-rotor');
}
    rotor.setAttribute('class', 'sidebar-lang-wheel-rotor');


function moveSidebarLanguageSelection(delta) {
    for (step = -state.repeats; step <= state.repeats; step += 1) {
    snapSidebarLanguageToStep(-delta, true);
        rotor.appendChild(makeSidebarLanguageSector(step));
}
    }


function bindSidebarLanguageSelector() {
     clipped.appendChild(rotor);
    var state = sidebarLanguageState;
     svg.appendChild(clipped);
    var selector = document.getElementById('clbi-sidebar-lang-selector');
     var fan = document.getElementById('clbi-sidebar-lang-fan');
     var apply = document.getElementById('clbi-sidebar-lang-apply');


     if (!selector || !fan || !apply) return;
     fixedDepthPath = createSidebarLanguageSvgEl('path');
    fixedDepthPath.setAttribute('class', 'sidebar-lang-fixed-depth');
    fixedDepthPath.setAttribute('d', getSidebarLanguageShellPath());
    svg.appendChild(fixedDepthPath);


     if (state.bound && state.boundElement === selector) return;
     fixedFocus = createSidebarLanguageSvgEl('path');
    fixedFocus.setAttribute('class', 'sidebar-lang-fixed-focus');
    fixedFocus.setAttribute('d', getSidebarLanguageSectorPath(-state.halfSector, state.halfSector));
    svg.appendChild(fixedFocus);


     state.bound = true;
     shadowSoft = createSidebarLanguageSvgEl('path');
     state.boundElement = selector;
    shadowSoft.setAttribute('class', 'sidebar-lang-inner-shadow-soft');
    shadowSoft.setAttribute('d', getSidebarLanguageShellPath());
     svg.appendChild(shadowSoft);


     fan.addEventListener('pointerdown', function(e) {
     shadowHard = createSidebarLanguageSvgEl('path');
        cancelSidebarLanguageSpin();
    shadowHard.setAttribute('class', 'sidebar-lang-inner-shadow-hard');
        clearTimeout(state.snapTimer);
    shadowHard.setAttribute('d', getSidebarLanguageShellPath());
    svg.appendChild(shadowHard);


        state.dragging = true;
    rim = createSidebarLanguageSvgEl('path');
        state.dragMoved = false;
    rim.setAttribute('class', 'sidebar-lang-rim');
        state.dragStartX = e.clientX;
    rim.setAttribute('d', getSidebarLanguageShellPath());
        state.dragStartY = e.clientY || 0;
    svg.appendChild(rim);
        state.dragStartRotation = state.rotation;
        state.dragAxis = null;
        state.pointerCaptured = false;
        state.lastX = e.clientX;
        state.lastTime = performance.now();
        state.releaseVelocity = 0;


        selector.classList.add('is-dragging');
    pointer = createSidebarLanguageSvgEl('g');
        selector.classList.remove('is-snapping');
    pointer.setAttribute('class', 'sidebar-lang-fixed-pointer');
    pointer.setAttribute('clip-path', 'url(#clbi-sidebar-language-fan-clip)');


        /*
    tri = createSidebarLanguageSvgEl('path');
        Vertical page scrolling must stay available when the pointer starts on
    tri.setAttribute('class', 'sidebar-lang-pointer-triangle');
        the language dial. Capture and preventDefault are delayed until a
    tri.setAttribute('d', 'M ' + (state.cx - 10) + ' 10 L ' + (state.cx + 10) + ' 10 L ' + state.cx + ' 26 Z');
        horizontal drag is confirmed.
     pointer.appendChild(tri);
        */
     });


     fan.addEventListener('pointermove', function(e) {
     line = createSidebarLanguageSvgEl('line');
        var now;
    line.setAttribute('class', 'sidebar-lang-pointer-line');
        var totalDx;
    line.setAttribute('x1', String(state.cx));
        var totalDy;
    line.setAttribute('x2', String(state.cx));
        var frameDx;
    line.setAttribute('y1', '24');
        var dt;
    line.setAttribute('y2', '112');
        var instantVelocity;
    pointer.appendChild(line);


        if (!state.dragging) return;
    svg.appendChild(pointer);
    fan.appendChild(svg);


        totalDx = e.clientX - state.dragStartX;
    state.rotor = rotor;
        totalDy = (e.clientY || 0) - state.dragStartY;
    setSidebarLanguageRotation(state.rotation, false);
}


        if (!state.dragAxis && (Math.abs(totalDx) > 4 || Math.abs(totalDy) > 4)) {
function updateSidebarLanguageDial() {
            state.dragAxis = Math.abs(totalDx) >= Math.abs(totalDy) ? 'x' : 'y';
    var state = sidebarLanguageState;
 
    var meta = getSidebarLanguagePreviewMeta();
            if (state.dragAxis === 'y') {
    var status = getSidebarLanguageStatus(meta);
                state.dragging = false;
    var selector = document.getElementById('clbi-sidebar-lang-selector');
                state.dragMoved = false;
    var apply = document.getElementById('clbi-sidebar-lang-apply');
                state.dragAxis = null;
    var selectedValue = document.getElementById('clbi-sidebar-lang-selected-value');
                state.pointerCaptured = false;
    var availabilityPanel = document.getElementById('clbi-sidebar-lang-availability-panel');
                selector.classList.remove('is-dragging');
    var availabilityValue = document.getElementById('clbi-sidebar-lang-availability-value');
                return;
            }


            if (fan.setPointerCapture && e.pointerId != null) {
    if (selectedValue) {
                try {
        selectedValue.textContent = meta.name;
                    fan.setPointerCapture(e.pointerId);
    }
                    state.pointerCaptured = true;
                } catch (err) {
                    state.pointerCaptured = false;
                }
            }
        }


        if (state.dragAxis !== 'x') return;
    if (availabilityPanel) {
        availabilityPanel.classList.remove('is-ready', 'is-current', 'is-locked');
        availabilityPanel.classList.add(status.className);
    }


        now = performance.now();
    if (availabilityValue) {
         frameDx = e.clientX - state.lastX;
         availabilityValue.textContent = status.label;
        dt = Math.max(1, now - state.lastTime);
    }


        if (Math.abs(totalDx) > 3) state.dragMoved = true;
    if (apply) {
        apply.classList.toggle('is-disabled', !status.canApply);
        apply.setAttribute('aria-disabled', status.canApply ? 'false' : 'true');
        apply.setAttribute('aria-label', status.canApply ? (meta.name + ' 적용') : (meta.isCurrent ? '현재 언어' : '사용할 수 없는 언어'));
    }


         instantVelocity = (frameDx * state.dragSensitivity) / dt;
    if (selector) {
         state.releaseVelocity = (state.releaseVelocity * 0.62) + (instantVelocity * 0.38);
         selector.setAttribute('data-selected-lang', meta.lang);
         state.lastX = e.clientX;
        selector.setAttribute('data-selected-code', meta.code);
         state.lastTime = now;
         selector.classList.toggle('is-current', meta.isCurrent);
        selector.classList.toggle('is-ready', status.canApply);
        selector.classList.toggle('is-locked', !status.canApply && !meta.isCurrent);
         selector.classList.toggle('is-dragging', !!state.dragging);
         selector.classList.toggle('is-spinning', !!state.inertiaRaf);
    }


        requestSidebarLanguageRotation(state.dragStartRotation + totalDx * state.dragSensitivity);
    return {
         e.preventDefault();
         meta: meta,
         e.stopPropagation();
         status: status
     });
     };
}


    function finishDrag(e) {
function setSidebarLanguageRotation(value, animate) {
        var velocityAge;
    var state = sidebarLanguageState;
        var throwVelocity;
    state.rotation = value;
        var wasHorizontal;
    updateSidebarLanguageDial();


        if (!state.dragging) return;
    if (!state.rotor) return;


         wasHorizontal = state.dragAxis === 'x';
    if (animate) {
        state.dragging = false;
         $('#clbi-sidebar-lang-selector').addClass('is-snapping');
         selector.classList.remove('is-dragging');
    } else {
         $('#clbi-sidebar-lang-selector').removeClass('is-snapping');
    }


        if (fan.releasePointerCapture && state.pointerCaptured && e && e.pointerId != null) {
    state.rotor.style.transform = 'rotate(' + state.rotation.toFixed(3) + 'deg)';
            try { fan.releasePointerCapture(e.pointerId); } catch (err) {}
}
        }


        state.pointerCaptured = false;
function requestSidebarLanguageRotation(value) {
        state.dragAxis = null;
    var state = sidebarLanguageState;
    state.pendingRotation = value;


        if (!wasHorizontal && !state.dragMoved) {
    if (state.raf) return;
            return;
        }


        velocityAge = performance.now() - state.lastTime;
    state.raf = requestAnimationFrame(function() {
         throwVelocity = velocityAge > 120 ? 0 : state.releaseVelocity;
        state.raf = null;
         setSidebarLanguageRotation(state.pendingRotation, false);
    });
}


        if (state.dragMoved) {
function cancelSidebarLanguageSpin() {
            state.suppressClickUntil = performance.now() + 180;
    var state = sidebarLanguageState;
        }


        if (state.dragMoved && Math.abs(throwVelocity) >= state.minSpinVelocity) {
    if (state.inertiaRaf) {
            startSidebarLanguageInertiaSpin(throwVelocity);
         cancelAnimationFrame(state.inertiaRaf);
         } else {
         state.inertiaRaf = null;
            snapSidebarLanguageNearest();
         }
 
        if (e) {
            e.preventDefault();
            e.stopPropagation();
        }
     }
     }


     fan.addEventListener('pointerup', finishDrag);
     $('#clbi-sidebar-lang-selector').removeClass('is-spinning');
    fan.addEventListener('pointercancel', finishDrag);
}
    fan.addEventListener('lostpointercapture', function() {
        if (!state.dragging) return;


        state.dragging = false;
function finishSidebarLanguageSnap(nearestIndex, callback) {
        state.pointerCaptured = false;
    var state = sidebarLanguageState;
        state.dragAxis = null;
        selector.classList.remove('is-dragging');


        if (state.dragMoved && Math.abs(state.releaseVelocity) >= state.minSpinVelocity) {
    state.baseIndex = normalizeSidebarLanguageIndex(nearestIndex);
            state.suppressClickUntil = performance.now() + 180;
    state.selectedIndex = state.baseIndex;
            startSidebarLanguageInertiaSpin(state.releaseVelocity);
    state.rotation = 0;
        } else {
    state.dragging = false;
            snapSidebarLanguageNearest();
        }
    });


     apply.addEventListener('click', function(e) {
     $('#clbi-sidebar-lang-selector').removeClass('is-snapping is-dragging is-spinning');
        e.preventDefault();
        e.stopPropagation();


        snapSidebarLanguageNearest(function(meta) {
    renderSidebarLanguageWheel();
            scheduleSidebarLanguageNavigation(meta);
     updateSidebarLanguageDial();
        });
     });


     selector.addEventListener('keydown', function(e) {
     if (typeof callback === 'function') {
        if (e.key === 'ArrowLeft') {
        callback(getSidebarLanguageMeta(state.order[state.selectedIndex]));
            moveSidebarLanguageSelection(-1);
    }
            e.preventDefault();
}
        }


        if (e.key === 'ArrowRight') {
function snapSidebarLanguageToStep(step, animate, callback) {
            moveSidebarLanguageSelection(1);
    var state = sidebarLanguageState;
            e.preventDefault();
    var targetRotation = -step * state.sectorAngle;
        }
    var nearestIndex = normalizeSidebarLanguageIndex(state.baseIndex + step);


        if (e.key === 'Enter' || e.key === ' ') {
    cancelSidebarLanguageSpin();
            apply.click();
    clearTimeout(state.snapTimer);
            e.preventDefault();
        }
    });
}


function renderSidebarLanguageBox() {
     state.selectedIndex = nearestIndex;
     bindSidebarLanguageSelector();
     setSidebarLanguageRotation(targetRotation, !!animate);
     setSidebarLanguageSelection(getCurrentLang());


     if (!sidebarLanguageStatusLoaded) {
     state.snapTimer = setTimeout(function() {
        loadSidebarLanguageStatusRegistry(function() {
        finishSidebarLanguageSnap(nearestIndex, callback);
            setSidebarLanguageSelection(getCurrentLang());
    }, animate ? 230 : 0);
        });
    }
}
}


function loadRecentChangesList(targetSelector, limit) {
function snapSidebarLanguageNearest(callback) {
     var $target = $(targetSelector);
     var state = sidebarLanguageState;
    var step = Math.round(-state.rotation / state.sectorAngle);
    snapSidebarLanguageToStep(step, true, callback);
}


    if (!$target.length) return;
function startSidebarLanguageInertiaSpin(initialVelocity) {
    var state = sidebarLanguageState;
    var velocity;
    var lastFrame;


     var lang = getCurrentLang();
     cancelSidebarLanguageSpin();
     var t = (window.LANG && window.LANG[lang]) ? window.LANG[lang] : (window.LANG ? window.LANG.ko : null);
 
    var isNewsList = $target.closest('.clbi-left-news-box').length > 0;
     velocity = Math.max(-state.maxSpinVelocity, Math.min(state.maxSpinVelocity, initialVelocity));


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


     $target.html((t && t.loading) ? t.loading : '불러오는 중...');
     $('#clbi-sidebar-lang-selector').addClass('is-spinning');
    lastFrame = performance.now();
 
    function frame(now) {
        var dt = Math.min(34, Math.max(1, now - lastFrame));
        var sign = velocity < 0 ? -1 : 1;
        var nextSpeed;
 
        lastFrame = now;
        state.rotation += velocity * dt;
        setSidebarLanguageRotation(state.rotation, false);
 
        nextSpeed = Math.max(0, Math.abs(velocity) - (state.spinDecel * dt));
        velocity = sign * nextSpeed;


    $.getJSON(
        if (nextSpeed <= state.minSpinVelocity) {
        '/api.php?action=query&list=recentchanges&rclimit=' + encodeURIComponent(limit || 5) + '&rcprop=title|timestamp|user&format=json&rcnamespace=0&rctype=edit|new',
            state.inertiaRaf = null;
        function(data) {
            $('#clbi-sidebar-lang-selector').removeClass('is-spinning');
             var items = data && data.query ? data.query.recentchanges : [];
            snapSidebarLanguageNearest();
            var html = '';
             return;
        }


            if (!items || !items.length) {
        state.inertiaRaf = requestAnimationFrame(frame);
                $target.html('표시할 변경 사항이 없습니다.');
    }
                return;
            }


            $.each(items, function(i, item) {
    state.inertiaRaf = requestAnimationFrame(frame);
                var label = timeAgo(item.timestamp);
}
                var title = item.title || '';
                var userName = item.user || 'Unknown';
                var pageHref = buildWikiPath(title);
                var avatarSrc = '/index.php?title=특수:Redirect/file/Pfp-' + encodeURIComponent(userName) + '.png';


                if (isNewsList) {
function scheduleSidebarLanguageNavigation(meta) {
                    html +=
    var status = getSidebarLanguageStatus(meta);
                        '<a href="' + escapeHtml(pageHref) + '" class="news-recent-item">' +
                            '<img class="news-recent-avatar" src="' + escapeHtml(avatarSrc) + '" alt="" onerror="this.onerror=null;this.src=\'/index.php?title=특수:Redirect/file/Pfp-default.png\';">' +
                            '<div class="news-recent-main">' +
                                '<div class="news-recent-title-wrap">' +
                                    '<span class="news-recent-title">' + escapeHtml(title) + '</span>' +
                                '</div>' +
                                '<div class="news-recent-meta">' +
                                    '<span class="news-recent-user">@' + escapeHtml(userName) + '</span>' +
                                '</div>' +
                            '</div>' +
                            '<span class="news-recent-time">' + escapeHtml(label) + '</span>' +
                        '</a>';
                } else {
                    html +=
                        '<div class="clbi-recent-item">' +
                            '<div class="clbi-recent-title-wrap">' +
                                '<a href="' + escapeHtml(pageHref) + '" class="clbi-recent-title">' + escapeHtml(title) + '</a>' +
                            '</div>' +
                            '<span class="clbi-recent-time">' + escapeHtml(label) + '</span>' +
                        '</div>';
                }
            });


            if (isNewsList) {
    if (!meta || !status.canApply) return;
                $target.html(
                    '<div class="news-recent-viewport">' +
                        '<div class="news-recent-stack">' + html + '</div>' +
                    '</div>'
                );


                if (typeof ensureNewsBottomFinish === 'function') {
    clearTimeout(sidebarLanguageState.navigateTimer);
                    ensureNewsBottomFinish();
    sidebarLanguageState.navigateTimer = setTimeout(function() {
                }
        var title = getLanguageTargetTitle(meta.lang);
            } else {
                $target.html(html);
            }


            if (isNewsList && typeof scheduleAdaptiveLeftRecentItems === 'function') {
        if (!title || meta.lang === getCurrentLang()) return;
                scheduleAdaptiveLeftRecentItems();
            }


            $target.find(isNewsList ? '.news-recent-item' : '.clbi-recent-item').each(function() {
        window.location.href = buildWikiPath(title);
                var wrap = $(this).find(isNewsList ? '.news-recent-title-wrap' : '.clbi-recent-title-wrap');
    }, 70);
                var title = $(this).find(isNewsList ? '.news-recent-title' : '.clbi-recent-title');
}


                if (!wrap.length || !title.length) return;
function setSidebarLanguageSelection(lang) {
    var state = sidebarLanguageState;
    var index = state.order.indexOf(lang);


                var wrapW = wrap.width();
    if (index < 0) index = state.order.indexOf(getCurrentLang());
                var titleW = title[0].scrollWidth;
    if (index < 0) index = 0;


                if (titleW > wrapW + 20) {
    if (state.raf) {
                    var duration = titleW / 40;
        cancelAnimationFrame(state.raf);
        state.raf = null;
    }


                    title.css({
    cancelSidebarLanguageSpin();
                        animation: 'clbi-scroll ' + duration + 's linear infinite',
     clearTimeout(state.snapTimer);
                        '--scroll-dist': '-' + (titleW - wrapW + 8) + 'px'
                    });
                }
            });
        }
     ).fail(function() {
        var lang = getCurrentLang();
        var t = (window.LANG && window.LANG[lang]) ? window.LANG[lang] : (window.LANG ? window.LANG.ko : null);


        $target.html((t && t.loadFail) ? t.loadFail : '불러오기 실패');
    state.currentLang = lang;
     });
    state.baseIndex = index;
    state.selectedIndex = index;
    state.rotation = 0;
    state.dragging = false;
    state.dragMoved = false;
    state.releaseVelocity = 0;
 
    renderSidebarLanguageWheel();
     updateSidebarLanguageDial();
}
}


function moveSidebarLanguageSelection(delta) {
    snapSidebarLanguageToStep(-delta, true);
}


function ensureRecentViewport() {
function bindSidebarLanguageSelector() {
     var list = document.getElementById('clbi-left-recent-list');
     var state = sidebarLanguageState;
     var viewport;
    var selector = document.getElementById('clbi-sidebar-lang-selector');
     var stack;
     var fan = document.getElementById('clbi-sidebar-lang-fan');
    var children;
     var apply = document.getElementById('clbi-sidebar-lang-apply');


     if (!list) return null;
     if (!selector || !fan || !apply) return;


     viewport = list.querySelector(':scope > .news-recent-viewport');
     if (state.bound && state.boundElement === selector) return;
    stack = viewport ? viewport.querySelector(':scope > .news-recent-stack') : null;


     if (viewport && stack) return viewport;
     state.bound = true;
    state.boundElement = selector;


     children = Array.prototype.slice.call(list.children || []);
     fan.addEventListener('pointerdown', function(e) {
        cancelSidebarLanguageSpin();
        clearTimeout(state.snapTimer);


    viewport = document.createElement('div');
        state.dragging = true;
    viewport.className = 'news-recent-viewport';
        state.dragMoved = false;
        state.dragStartX = e.clientX;
        state.dragStartY = e.clientY || 0;
        state.dragStartRotation = state.rotation;
        state.dragAxis = null;
        state.pointerCaptured = false;
        state.lastX = e.clientX;
        state.lastTime = performance.now();
        state.releaseVelocity = 0;


    stack = document.createElement('div');
        selector.classList.add('is-dragging');
    stack.className = 'news-recent-stack';
        selector.classList.remove('is-snapping');


    children.forEach(function (child) {
        /*
         if (child.classList && child.classList.contains('news-recent-viewport')) return;
        Vertical page scrolling must stay available when the pointer starts on
         stack.appendChild(child);
        the language dial. Capture and preventDefault are delayed until a
         horizontal drag is confirmed.
         */
     });
     });


     viewport.appendChild(stack);
     fan.addEventListener('pointermove', function(e) {
    list.appendChild(viewport);
        var now;
        var totalDx;
        var totalDy;
        var frameDx;
        var dt;
        var instantVelocity;


    return viewport;
        if (!state.dragging) return;
}


function ensureNewsBottomFinish() {
        totalDx = e.clientX - state.dragStartX;
    var newsBox = document.querySelector('#clbi-left-sidebar .clbi-left-news-box');
        totalDy = (e.clientY || 0) - state.dragStartY;
    var content = newsBox ? newsBox.querySelector('.clbi-news-box') : null;
    var finish;


    if (!content) return null;
        if (!state.dragAxis && (Math.abs(totalDx) > 4 || Math.abs(totalDy) > 4)) {
            state.dragAxis = Math.abs(totalDx) >= Math.abs(totalDy) ? 'x' : 'y';


    finish = content.querySelector(':scope > .news-bottom-finish');
            if (state.dragAxis === 'y') {
                state.dragging = false;
                state.dragMoved = false;
                state.dragAxis = null;
                state.pointerCaptured = false;
                selector.classList.remove('is-dragging');
                return;
            }


    if (!finish) {
            if (fan.setPointerCapture && e.pointerId != null) {
        finish = document.createElement('div');
                try {
        finish.className = 'news-bottom-finish';
                    fan.setPointerCapture(e.pointerId);
        finish.setAttribute('aria-hidden', 'true');
                    state.pointerCaptured = true;
        content.appendChild(finish);
                } catch (err) {
    }
                    state.pointerCaptured = false;
                }
            }
        }


    return finish;
        if (state.dragAxis !== 'x') return;
}


function updateAdaptiveLeftRecentItems() {
        now = performance.now();
    /*
        frameDx = e.clientX - state.lastX;
    134 기준: 좌측 사이드는 뉴스 확장형 flex 레이아웃이 높이를 담당한다.
        dt = Math.max(1, now - state.lastTime);
    이전 adaptive 코드는 항목을 숨기거나 mask/fade를 걸기 위한 것이었으므로
    여기서는 잔여 상태만 정리하고 DOM 래퍼만 보장한다.
    */
    resetLeftRecentAdaptiveState();


    if (typeof ensureRecentViewport === 'function') {
        if (Math.abs(totalDx) > 3) state.dragMoved = true;
        ensureRecentViewport();
    }


    if (typeof ensureNewsBottomFinish === 'function') {
        instantVelocity = (frameDx * state.dragSensitivity) / dt;
         ensureNewsBottomFinish();
         state.releaseVelocity = (state.releaseVelocity * 0.62) + (instantVelocity * 0.38);
    }
        state.lastX = e.clientX;
        state.lastTime = now;


    if (typeof scheduleClbiContentBottomGap === 'function') {
        requestSidebarLanguageRotation(state.dragStartRotation + totalDx * state.dragSensitivity);
         scheduleClbiContentBottomGap();
        e.preventDefault();
     }
         e.stopPropagation();
}
     });


function scheduleAdaptiveLeftRecentItems() {
    function finishDrag(e) {
    window.requestAnimationFrame(function () {
        var velocityAge;
         updateAdaptiveLeftRecentItems();
        var throwVelocity;
    });
         var wasHorizontal;
 
        if (!state.dragging) return;


    window.setTimeout(updateAdaptiveLeftRecentItems, 80);
        wasHorizontal = state.dragAxis === 'x';
    window.setTimeout(updateAdaptiveLeftRecentItems, 240);
        state.dragging = false;
}
        selector.classList.remove('is-dragging');


        if (fan.releasePointerCapture && state.pointerCaptured && e && e.pointerId != null) {
            try { fan.releasePointerCapture(e.pointerId); } catch (err) {}
        }


        state.pointerCaptured = false;
        state.dragAxis = null;


function updateClbiContentBottomGap(iteration) {
        if (!wasHorizontal && !state.dragMoved) {
    var content = document.querySelector('.container-fluid.liberty-content');
            return;
    var main = document.querySelector('.liberty-content-main');
        }
    var bottomNav = document.getElementById('clbi-bottom-nav-wrap');
    var desiredGap = 8;
    var rootStyle;
    var scale = 1;
    var contentRect;
    var bottomTop;
    var targetHeight;
    var currentHeight;
    var visualGap;


    iteration = iteration || 0;
        velocityAge = performance.now() - state.lastTime;
        throwVelocity = velocityAge > 120 ? 0 : state.releaseVelocity;


    if (!content || !main || !bottomNav) return;
        if (state.dragMoved) {
            state.suppressClickUntil = performance.now() + 180;
        }


    /*
        if (state.dragMoved && Math.abs(throwVelocity) >= state.minSpinVelocity) {
    하단 간격은 scale 모드에서도 같은 기준으로 계산한다.
            startSidebarLanguageInertiaSpin(throwVelocity);
    transform:scale()이 걸리면 getBoundingClientRect()는 축소된 화면 좌표를 반환하므로,
        } else {
    목표 간격 8px도 scale을 곱한 화면 좌표로 비교하고 다시 design px로 환산한다.
            snapSidebarLanguageNearest();
        }


    목표:
        if (e) {
    .liberty-content-main.bottom === #clbi-bottom-nav-wrap.top - 8px
            e.preventDefault();
    */
            e.stopPropagation();
    if (document.body && document.body.classList && document.body.classList.contains('clbi-shell-vertical-scale')) {
         }
        rootStyle = window.getComputedStyle(document.documentElement);
        scale = parseFloat(rootStyle.getPropertyValue('--clbi-shell-scale')) || 1;
         scale = Math.max(0.25, scale);
     }
     }


     contentRect = content.getBoundingClientRect();
     fan.addEventListener('pointerup', finishDrag);
     bottomTop = bottomNav.getBoundingClientRect().top;
     fan.addEventListener('pointercancel', finishDrag);
     visualGap = desiredGap * scale;
     fan.addEventListener('lostpointercapture', function() {
    targetHeight = Math.floor((bottomTop - contentRect.top - visualGap) / scale);
        if (!state.dragging) return;
    targetHeight = Math.max(120, targetHeight);
 
        state.dragging = false;
        state.pointerCaptured = false;
        state.dragAxis = null;
        selector.classList.remove('is-dragging');


    currentHeight = Math.round(content.getBoundingClientRect().height / scale);
        if (state.dragMoved && Math.abs(state.releaseVelocity) >= state.minSpinVelocity) {
            state.suppressClickUntil = performance.now() + 180;
            startSidebarLanguageInertiaSpin(state.releaseVelocity);
        } else {
            snapSidebarLanguageNearest();
        }
    });


     content.style.setProperty('--clbi-content-extra', '0px');
     apply.addEventListener('click', function(e) {
    content.style.setProperty('height', targetHeight + 'px', 'important');
        e.preventDefault();
    content.style.setProperty('max-height', targetHeight + 'px', 'important');
        e.stopPropagation();


    if (Math.abs(currentHeight - targetHeight) >= 1 && iteration < 4) {
         snapSidebarLanguageNearest(function(meta) {
         window.requestAnimationFrame(function () {
             scheduleSidebarLanguageNavigation(meta);
             updateClbiContentBottomGap(iteration + 1);
         });
         });
     }
     });
}
 
    selector.addEventListener('keydown', function(e) {
        if (e.key === 'ArrowLeft') {
            moveSidebarLanguageSelection(-1);
            e.preventDefault();
        }
 
        if (e.key === 'ArrowRight') {
            moveSidebarLanguageSelection(1);
            e.preventDefault();
        }


function scheduleClbiContentBottomGap() {
        if (e.key === 'Enter' || e.key === ' ') {
    window.requestAnimationFrame(function () {
            apply.click();
        updateClbiContentBottomGap(0);
            e.preventDefault();
        }
     });
     });
    window.setTimeout(function () {
        updateClbiContentBottomGap(0);
    }, 40);
    window.setTimeout(function () {
        updateClbiContentBottomGap(0);
    }, 120);
    window.setTimeout(function () {
        updateClbiContentBottomGap(0);
    }, 280);
    window.setTimeout(function () {
        updateClbiContentBottomGap(0);
    }, 520);
}
}


function renderSidebarLanguageBox() {
    bindSidebarLanguageSelector();
    setSidebarLanguageSelection(getCurrentLang());


function updateLeftBillboardAdaptive() {
    if (!sidebarLanguageStatusLoaded) {
    /*
        loadSidebarLanguageStatusRegistry(function() {
    134 기준: Ad는 이미지/CRT 비율을 유지하는 고정 슬롯이다.
            setSidebarLanguageSelection(getCurrentLang());
    남는 세로 공간은 뉴스 박스가 흡수하므로, Ad에 하단 finish를 늘리거나
        });
    title-only 상태로 접는 adaptive 보정은 사용하지 않는다.
     }
    */
     resetLeftBillboardAdaptiveState();
}
}


function scheduleLeftBillboardAdaptive() {
function loadRecentChangesList(targetSelector, limit) {
     window.requestAnimationFrame(updateLeftBillboardAdaptive);
     var $target = $(targetSelector);
     window.setTimeout(updateLeftBillboardAdaptive, 80);
 
    window.setTimeout(updateLeftBillboardAdaptive, 240);
     if (!$target.length) return;
}


function scheduleLeftSidebarVerticalFit() {
    var lang = getCurrentLang();
     if (typeof scheduleAdaptiveLeftRecentItems === 'function') {
     var t = (window.LANG && window.LANG[lang]) ? window.LANG[lang] : (window.LANG ? window.LANG.ko : null);
        scheduleAdaptiveLeftRecentItems();
     var isNewsList = $target.closest('.clbi-left-news-box').length > 0;
     }


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


     window.setTimeout(function () {
     $target.html((t && t.loading) ? t.loading : '불러오는 중...');
        if (typeof scheduleAdaptiveLeftRecentItems === 'function') {
            scheduleAdaptiveLeftRecentItems();
        }


         if (typeof scheduleLeftBillboardAdaptive === 'function') {
    $.getJSON(
             scheduleLeftBillboardAdaptive();
         '/api.php?action=query&list=recentchanges&rclimit=' + encodeURIComponent(limit || 5) + '&rcprop=title|timestamp|user&format=json&rcnamespace=0&rctype=edit|new',
        }
        function(data) {
    }, 120);
             var items = data && data.query ? data.query.recentchanges : [];
}
            var html = '';


            if (!items || !items.length) {
                $target.html('표시할 변경 사항이 없습니다.');
                return;
            }


// 국가_및_조합 전용 왼쪽 사이드바 이미지
            $.each(items, function(i, item) {
function updateLeftSidebarNationsImage() {
                var label = timeAgo(item.timestamp);
    $('#clbi-left-nations-image').remove();
                var title = item.title || '';
}
                var userName = item.user || 'Unknown';
                var pageHref = buildWikiPath(title);
                var avatarSrc = '/index.php?title=특수:Redirect/file/Pfp-' + encodeURIComponent(userName) + '.png';


function setProfileActionLabel(selector, text) {
                if (isNewsList) {
    var target = $(selector);
                    html +=
    var label = target.find('.profile-action-label');
                        '<a href="' + escapeHtml(pageHref) + '" class="news-recent-item">' +
                            '<img class="news-recent-avatar" src="' + escapeHtml(avatarSrc) + '" alt="" onerror="this.onerror=null;this.src=\'/index.php?title=특수:Redirect/file/Pfp-default.png\';">' +
                            '<div class="news-recent-main">' +
                                '<div class="news-recent-title-wrap">' +
                                    '<span class="news-recent-title">' + escapeHtml(title) + '</span>' +
                                '</div>' +
                                '<div class="news-recent-meta">' +
                                    '<span class="news-recent-user">@' + escapeHtml(userName) + '</span>' +
                                '</div>' +
                            '</div>' +
                            '<span class="news-recent-time">' + escapeHtml(label) + '</span>' +
                        '</a>';
                } else {
                    html +=
                        '<div class="clbi-recent-item">' +
                            '<div class="clbi-recent-title-wrap">' +
                                '<a href="' + escapeHtml(pageHref) + '" class="clbi-recent-title">' + escapeHtml(title) + '</a>' +
                            '</div>' +
                            '<span class="clbi-recent-time">' + escapeHtml(label) + '</span>' +
                        '</div>';
                }
            });


    if (label.length) {
            if (isNewsList) {
        label.text(text);
                $target.html(
    } else {
                    '<div class="news-recent-viewport">' +
        target.text(text);
                        '<div class="news-recent-stack">' + html + '</div>' +
    }
                    '</div>'
}
                );


// 사이드바 업데이트
                if (typeof ensureNewsBottomFinish === 'function') {
function updateSidebar() {
                    ensureNewsBottomFinish();
    if (!window.LANG) {
                }
        setTimeout(updateSidebar, 100);
            } else {
        return;
                $target.html(html);
    }
            }


    var currentLang = getCurrentLang();
            if (isNewsList && typeof scheduleAdaptiveLeftRecentItems === 'function') {
    var t = (window.LANG && window.LANG[currentLang]) ? window.LANG[currentLang] : window.LANG.ko;
                scheduleAdaptiveLeftRecentItems();
            }


    var newsTitle = t.news || '뉴스';
            $target.find(isNewsList ? '.news-recent-item' : '.clbi-recent-item').each(function() {
    var changelogTitle = t.changelog || '체인지로그';
                var wrap = $(this).find(isNewsList ? '.news-recent-title-wrap' : '.clbi-recent-title-wrap');
    var recentTitle = t.recentChanges || '최근 변경';
                var title = $(this).find(isNewsList ? '.news-recent-title' : '.clbi-recent-title');
    var languageTitle = t.language || '언어';


    $('#clbi-title-left-language').text(languageTitle);
                if (!wrap.length || !title.length) return;
    renderSidebarLanguageBox();


    $('#clbi-title-left-news').text(newsTitle);
                var wrapW = wrap.width();
    $('#clbi-left-news-changelog-main').text(changelogTitle);
                var titleW = title[0].scrollWidth;
    $('#clbi-left-news-recent-main').text(recentTitle);


    $('#clbi-title-search a').text(t.search);
                if (titleW > wrapW + 20) {
    $('#clbi-search-input').attr('placeholder', t.search + '...');
                    var duration = titleW / 40;
    $('#clbi-title-recent a').text(recentTitle);
    $('#clbi-title-guide-label').text(t.guide);
    $('#clbi-guide-link').text(t.getStarted);
    $('#clbi-title-links-label').text(t.links);


    setProfileActionLabel('#clbi-btn-contribution', t.contribution);
                    title.css({
    setProfileActionLabel('#clbi-btn-watchlist', t.watchlist);
                        animation: 'clbi-scroll ' + duration + 's linear infinite',
    setProfileActionLabel('#clbi-btn-preferences', t.preferences);
                        '--scroll-dist': '-' + (titleW - wrapW + 8) + 'px'
     setProfileActionLabel('#clbi-btn-logout', t.logout);
                    });
    setProfileActionLabel('#clbi-btn-login', t.login);
                }
            });
        }
     ).fail(function() {
        var lang = getCurrentLang();
        var t = (window.LANG && window.LANG[lang]) ? window.LANG[lang] : (window.LANG ? window.LANG.ko : null);


    var pageName = normalizePageName(mw.config.get('wgPageName'));
        $target.html((t && t.loadFail) ? t.loadFail : '불러오기 실패');
     var specialPage = String(mw.config.get('wgCanonicalSpecialPageName') || '');
     });
}


$('#clbi-left-news-changelog-main').text(changelogTitle);
$('#clbi-left-news-recent-title').text('RECENT CHANGES');


    $('.clbi-user-btn').removeClass('clbi-user-btn-active');
function ensureRecentViewport() {
    var list = document.getElementById('clbi-left-recent-list');
    var viewport;
    var stack;
    var children;


     if (
     if (!list) return null;
        specialPage === 'Contributions' ||
        specialPage === '기여' ||
        pageName.indexOf('특수:기여') === 0 ||
        pageName.indexOf('Special:Contributions') === 0
    ) {
        $('#clbi-btn-contribution').addClass('clbi-user-btn-active');
    }


     if (specialPage === 'Watchlist') {
     viewport = list.querySelector(':scope > .news-recent-viewport');
        $('#clbi-btn-watchlist').addClass('clbi-user-btn-active');
    stack = viewport ? viewport.querySelector(':scope > .news-recent-stack') : null;
    }


     if (
     if (viewport && stack) return viewport;
        specialPage === '설정' ||
        pageName === '특수:설정' ||
        pageName === 'Special:설정'
    ) {
        $('#clbi-btn-preferences').addClass('clbi-user-btn-active');
    }


     $('.toggleBtn').each(function() {
     children = Array.prototype.slice.call(list.children || []);
        var btn = $(this);


        if (!$('#' + btn.data('target')).hasClass('folding-open')) {
    viewport = document.createElement('div');
            btn.text(t.expand);
    viewport.className = 'news-recent-viewport';
        } else {
            btn.text(t.collapse);
        }
    });


     updateLeftSidebarNationsImage();
     stack = document.createElement('div');
    stack.className = 'news-recent-stack';
 
    children.forEach(function (child) {
        if (child.classList && child.classList.contains('news-recent-viewport')) return;
        stack.appendChild(child);
    });
 
    viewport.appendChild(stack);
    list.appendChild(viewport);
 
    return viewport;
}
}


function canShowContentTools() {
function ensureNewsBottomFinish() {
     // 비로그인 사용자는 편집/역사/공유 버튼을 숨김
     var newsBox = document.querySelector('#clbi-left-sidebar .clbi-left-news-box');
     if (!mw.config.get('wgUserName')) {
     var content = newsBox ? newsBox.querySelector('.clbi-news-box') : null;
        return false;
    var finish;
     }
 
     if (!content) return null;


     // MediaWiki가 현재 문서를 편집 가능하지 않다고 판단하면 숨김
     finish = content.querySelector(':scope > .news-bottom-finish');
    var isEditable = mw.config.get('wgIsProbablyEditable');
    if (isEditable === false) {
        return false;
    }


     var relevantEditable = mw.config.get('wgRelevantPageIsProbablyEditable');
     if (!finish) {
    if (relevantEditable === false) {
        finish = document.createElement('div');
         return false;
        finish.className = 'news-bottom-finish';
        finish.setAttribute('aria-hidden', 'true');
         content.appendChild(finish);
     }
     }


     return true;
     return finish;
}
}


function getCatlinkNodes(root) {
function updateAdaptiveLeftRecentItems() {
     var seen = [];
     /*
     var nodes = [];
    134 기준: 좌측 사이드는 뉴스 확장형 flex 레이아웃이 높이를 담당한다.
     var $root = root ? $(root) : $(document);
    이전 adaptive 코드는 항목을 숨기거나 mask/fade를 걸기 위한 것이었으므로
    여기서는 잔여 상태만 정리하고 DOM 래퍼만 보장한다.
    */
     resetLeftRecentAdaptiveState();
 
     if (typeof ensureRecentViewport === 'function') {
        ensureRecentViewport();
    }


     $root.find('#catlinks, .catlinks').add($root.filter('#catlinks, .catlinks')).each(function () {
     if (typeof ensureNewsBottomFinish === 'function') {
         if (seen.indexOf(this) !== -1) return;
         ensureNewsBottomFinish();
        seen.push(this);
     }
        nodes.push(this);
     });


     return nodes;
     if (typeof scheduleClbiContentBottomGap === 'function') {
        scheduleClbiContentBottomGap();
    }
}
}


function getCatlinksTarget(root) {
function scheduleAdaptiveLeftRecentItems() {
     var $root = root ? $(root) : $(document);
     window.requestAnimationFrame(function () {
    var parserOutput = $root.find('.liberty-content-main .mw-parser-output').first();
        updateAdaptiveLeftRecentItems();
     var main = $root.find('.liberty-content-main').first();
     });


     if (!parserOutput.length && root && $(root).is('.liberty-content-main')) {
     window.setTimeout(updateAdaptiveLeftRecentItems, 80);
        parserOutput = $(root).find('.mw-parser-output').first();
    window.setTimeout(updateAdaptiveLeftRecentItems, 240);
        main = $(root);
}
    }


    if (!parserOutput.length && root && $(root).is('.mw-parser-output')) {
        parserOutput = $(root);
    }


    if (parserOutput.length) return parserOutput;
    if (main.length) return main;


    if (!root) {
function updateClbiContentBottomGap(iteration) {
        parserOutput = $('.liberty-content-main .mw-parser-output').first();
    var content = document.querySelector('.container-fluid.liberty-content');
        main = $('.liberty-content-main').first();
    var main = document.querySelector('.liberty-content-main');
        if (parserOutput.length) return parserOutput;
    var bottomNav = document.getElementById('clbi-bottom-nav-wrap');
        if (main.length) return main;
    var desiredGap = 8;
     }
    var rootStyle;
    var scale = 1;
    var contentRect;
    var bottomTop;
    var targetHeight;
    var currentHeight;
     var visualGap;


     return $();
     iteration = iteration || 0;
}


var CLBI_CATLINKS_FETCH_TOKEN = 0;
    if (!content || !main || !bottomNav) return;


function getCurrentPageTitleForCatlinks() {
    /*
     return String(
     하단 간격은 scale 모드에서도 같은 기준으로 계산한다.
        mw.config.get('wgPageName') ||
    transform:scale()이 걸리면 getBoundingClientRect()는 축소된 화면 좌표를 반환하므로,
        mw.config.get('wgRelevantPageName') ||
     목표 간격 8px도 scale을 곱한 화면 좌표로 비교하고 다시 design px로 환산한다.
        ''
     ).trim();
}


function shouldFetchCatlinks() {
    목표:
    var pageName = getCurrentPageTitleForCatlinks();
    .liberty-content-main.bottom === #clbi-bottom-nav-wrap.top - 8px
    var specialPage = mw.config.get('wgCanonicalSpecialPageName');
    */
    if (document.body && document.body.classList && document.body.classList.contains('clbi-shell-vertical-scale')) {
        rootStyle = window.getComputedStyle(document.documentElement);
        scale = parseFloat(rootStyle.getPropertyValue('--clbi-shell-scale')) || 1;
        scale = Math.max(0.25, scale);
    }


     if (!pageName) return false;
     contentRect = content.getBoundingClientRect();
     if (specialPage) return false;
     bottomTop = bottomNav.getBoundingClientRect().top;
    visualGap = desiredGap * scale;
    targetHeight = Math.floor((bottomTop - contentRect.top - visualGap) / scale);
    targetHeight = Math.max(120, targetHeight);


     return true;
     currentHeight = Math.round(content.getBoundingClientRect().height / scale);
}


function clearCatlinksInlineHiding(cat) {
    content.style.setProperty('--clbi-content-extra', '0px');
     if (!cat || !cat.style) return;
     content.style.setProperty('height', targetHeight + 'px', 'important');
    content.style.setProperty('max-height', targetHeight + 'px', 'important');


     cat.style.removeProperty('display');
     if (Math.abs(currentHeight - targetHeight) >= 1 && iteration < 4) {
    cat.style.removeProperty('visibility');
        window.requestAnimationFrame(function () {
    cat.style.removeProperty('height');
            updateClbiContentBottomGap(iteration + 1);
    cat.style.removeProperty('max-height');
        });
     cat.style.removeProperty('overflow');
     }
}


    $(cat).find('.mw-normal-catlinks, #mw-normal-catlinks, .mw-hidden-catlinks, #mw-hidden-catlinks, .mw-hidden-cats-hidden, .mw-hidden-cats-user-shown').each(function () {
function scheduleClbiContentBottomGap() {
        if (!this.style) return;
    window.requestAnimationFrame(function () {
        this.style.removeProperty('display');
         updateClbiContentBottomGap(0);
        this.style.removeProperty('visibility');
        this.style.removeProperty('height');
        this.style.removeProperty('max-height');
         this.style.removeProperty('overflow');
     });
     });
    window.setTimeout(function () {
        updateClbiContentBottomGap(0);
    }, 40);
    window.setTimeout(function () {
        updateClbiContentBottomGap(0);
    }, 120);
    window.setTimeout(function () {
        updateClbiContentBottomGap(0);
    }, 280);
    window.setTimeout(function () {
        updateClbiContentBottomGap(0);
    }, 520);
}
}


function exposeHiddenCatlinks(cat) {
    if (!cat) return;


    $(cat).find('.mw-hidden-catlinks, #mw-hidden-catlinks, .mw-hidden-cats-hidden, .mw-hidden-cats-user-shown').each(function () {
function updateLeftBillboardAdaptive() {
        this.classList.remove('mw-hidden-cats-hidden');
    /*
        this.classList.add('mw-hidden-cats-user-shown');
    134 기준: Ad는 이미지/CRT 비율을 유지하는 고정 슬롯이다.
    남는 세로 공간은 뉴스 박스가 흡수하므로, Ad에 하단 finish를 늘리거나
    title-only 상태로 접는 adaptive 보정은 사용하지 않는다.
    */
    resetLeftBillboardAdaptiveState();
}


        if (this.style) {
function scheduleLeftBillboardAdaptive() {
            this.style.removeProperty('display');
    window.requestAnimationFrame(updateLeftBillboardAdaptive);
            this.style.removeProperty('visibility');
    window.setTimeout(updateLeftBillboardAdaptive, 80);
            this.style.removeProperty('height');
    window.setTimeout(updateLeftBillboardAdaptive, 240);
            this.style.removeProperty('max-height');
            this.style.removeProperty('overflow');
        }
    });
}
}


function getCatlinkTextContent(cat) {
function scheduleLeftSidebarVerticalFit() {
     var clone;
     if (typeof scheduleAdaptiveLeftRecentItems === 'function') {
     var text;
        scheduleAdaptiveLeftRecentItems();
     }


     if (!cat) return '';
     if (typeof scheduleLeftBillboardAdaptive === 'function') {
        scheduleLeftBillboardAdaptive();
    }


     clone = cat.cloneNode(true);
     window.setTimeout(function () {
    $(clone).find('script, style').remove();
        if (typeof scheduleAdaptiveLeftRecentItems === 'function') {
            scheduleAdaptiveLeftRecentItems();
        }


    text = String(clone.textContent || '')
        if (typeof scheduleLeftBillboardAdaptive === 'function') {
        .replace(/\s+/g, ' ')
            scheduleLeftBillboardAdaptive();
         .replace(/^(분류|숨은 분류|Category|Hidden categories)\s*:\s*/i, '')
         }
        .replace(/^(분류|숨은 분류|Category|Hidden categories)\s*$/i, '')
    }, 120);
        .trim();
 
    return text;
}
}


function hasRenderedCatlinkItems(cat) {
    var hasLink;
    var hasListText;


    if (!cat) return false;
// 시대 문서 전용 왼쪽 사이드바 이미지
 
function updateLeftSidebarNationsImage() {
    hasLink = false;
     $('#clbi-left-nations-image').remove();
    $(cat).find('a').each(function () {
        var text = String($(this).text() || '').trim();
        var href = String(this.getAttribute('href') || '').trim();
        if (text || href) hasLink = true;
    });
    if (hasLink) return true;
 
    hasListText = false;
     $(cat).find('li').each(function () {
        if (String($(this).text() || '').trim()) hasListText = true;
    });
    if (hasListText) return true;
 
    return !!getCatlinkTextContent(cat);
}
}


function normalizeCategoryTitle(rawTitle) {
function setProfileActionLabel(selector, text) {
     var title = String(rawTitle == null ? '' : rawTitle).trim();
     var target = $(selector);
    var label = target.find('.profile-action-label');


     if (!title) return '';
     if (label.length) {
 
        label.text(text);
    title = title.replace(/_/g, ' ');
    } else {
 
        target.text(text);
    if (/^(Category|분류):/i.test(title)) {
        return title;
     }
     }
    return '분류:' + title;
}
}


function makeCategoryLinkTitle(rawTitle) {
// 사이드바 업데이트
     return String(rawTitle || '')
function updateSidebar() {
        .replace(/^Category:/i, '')
     if (!window.LANG) {
         .replace(/^분류:/, '')
         setTimeout(updateSidebar, 100);
         .replace(/_/g, ' ')
         return;
        .trim();
    }
}


function dedupeCatlinkCategories(categories) {
     var currentLang = getCurrentLang();
     var seen = {};
     var t = (window.LANG && window.LANG[currentLang]) ? window.LANG[currentLang] : window.LANG.ko;
     var result = [];


     (categories || []).forEach(function (item) {
     var newsTitle = t.news || '뉴스';
        var title = '';
    var changelogTitle = t.changelog || '체인지로그';
        var hidden = false;
    var recentTitle = t.recentChanges || '최근 변경';
    var languageTitle = t.language || '언어';


        if (typeof item === 'string') {
    $('#clbi-title-left-language').text(languageTitle);
            title = normalizeCategoryTitle(item);
    renderSidebarLanguageBox();
        } else if (item && typeof item === 'object') {
            title = normalizeCategoryTitle(item.title || item.name || item.category || '');
            hidden = item.hidden !== undefined || item.isHidden === true;
        }


        if (!title) return;
    $('#clbi-title-left-news').text(newsTitle);
        if (seen[title]) return;
    $('#clbi-left-news-changelog-main').text(changelogTitle);
    $('#clbi-left-news-recent-main').text(recentTitle);


        seen[title] = true;
    $('#clbi-title-search a').text(t.search);
        result.push({ title: title, hidden: hidden });
    $('#clbi-search-input').attr('placeholder', t.search + '...');
     });
    $('#clbi-title-recent a').text(recentTitle);
    $('#clbi-title-guide-label').text(t.guide);
    $('#clbi-guide-link').text(t.getStarted);
     $('#clbi-title-links-label').text(t.links);


     return result;
     setProfileActionLabel('#clbi-btn-contribution', t.contribution);
}
    setProfileActionLabel('#clbi-btn-watchlist', t.watchlist);
    setProfileActionLabel('#clbi-btn-preferences', t.preferences);
    setProfileActionLabel('#clbi-btn-logout', t.logout);
    setProfileActionLabel('#clbi-btn-login', t.login);


function getConfigCatlinksCategories() {
     var pageName = normalizePageName(mw.config.get('wgPageName'));
     var normal = mw.config.get('wgCategories') || [];
     var specialPage = String(mw.config.get('wgCanonicalSpecialPageName') || '');
     var hidden = mw.config.get('wgHiddenCategories') || [];
    var categories = [];


    if (!Array.isArray(normal)) normal = [];
$('#clbi-left-news-changelog-main').text(changelogTitle);
    if (!Array.isArray(hidden)) hidden = [];
$('#clbi-left-news-recent-title').text('RECENT CHANGES');


     normal.forEach(function (name) {
     $('.clbi-user-btn').removeClass('clbi-user-btn-active');
        categories.push({ title: normalizeCategoryTitle(name), hidden: false });
    });


     hidden.forEach(function (name) {
     if (
         categories.push({ title: normalizeCategoryTitle(name), hidden: true });
        specialPage === 'Contributions' ||
        specialPage === '기여' ||
        pageName.indexOf('특수:기여') === 0 ||
        pageName.indexOf('Special:Contributions') === 0
    ) {
        $('#clbi-btn-contribution').addClass('clbi-user-btn-active');
    }
 
    if (specialPage === 'Watchlist') {
        $('#clbi-btn-watchlist').addClass('clbi-user-btn-active');
    }
 
    if (
        specialPage === '설정' ||
        pageName === '특수:설정' ||
        pageName === 'Special:설정'
    ) {
        $('#clbi-btn-preferences').addClass('clbi-user-btn-active');
    }
 
    $('.toggleBtn').each(function() {
         var btn = $(this);
 
        if (!$('#' + btn.data('target')).hasClass('folding-open')) {
            btn.text(t.expand);
        } else {
            btn.text(t.collapse);
        }
     });
     });


     return dedupeCatlinkCategories(categories);
     updateLeftSidebarNationsImage();
}
}


function markCatlinksReady(cat, pageTitle) {
function canShowContentTools() {
     if (!cat) return;
    // 비로그인 사용자는 편집/역사/공유 버튼을 숨김
     if (!mw.config.get('wgUserName')) {
        return false;
    }


     cat.classList.add('catlinks');
     // MediaWiki가 현재 문서를 편집 가능하지 않다고 판단하면 숨김
    cat.classList.add('clbi-catlinks-ready');
     var isEditable = mw.config.get('wgIsProbablyEditable');
     cat.classList.remove('clbi-catlinks-empty');
     if (isEditable === false) {
     cat.classList.remove('clbi-catlinks-pending');
        return false;
    cat.classList.remove('clbi-catlinks-loading');
     }
     cat.removeAttribute('data-clbi-catlinks-fetching');
    cat.setAttribute('data-clbi-catlinks-page', pageTitle || getCurrentPageTitleForCatlinks());
    clearCatlinksInlineHiding(cat);
    exposeHiddenCatlinks(cat);
}


function markCatlinksEmpty(cat) {
    var relevantEditable = mw.config.get('wgRelevantPageIsProbablyEditable');
     if (!cat) return;
     if (relevantEditable === false) {
        return false;
    }


     cat.classList.add('catlinks');
     return true;
    cat.classList.add('clbi-catlinks-empty');
    cat.classList.remove('clbi-catlinks-ready');
    cat.classList.remove('clbi-catlinks-pending');
    cat.classList.remove('clbi-catlinks-loading');
    cat.removeAttribute('data-clbi-catlinks-fetching');
    cat.removeAttribute('data-clbi-catlinks-page');
}
}


function markCatlinksPending(cat, pageTitle) {
function getCatlinkNodes(root) {
    if (!cat) return;
    var seen = [];
    var nodes = [];
    var $root = root ? $(root) : $(document);
 
    $root.find('#catlinks, .catlinks').add($root.filter('#catlinks, .catlinks')).each(function () {
        if (seen.indexOf(this) !== -1) return;
        seen.push(this);
        nodes.push(this);
    });


     cat.classList.add('catlinks');
     return nodes;
    cat.classList.remove('clbi-catlinks-ready');
    cat.classList.remove('clbi-catlinks-empty');
    cat.classList.add('clbi-catlinks-pending');
    cat.classList.add('clbi-catlinks-loading');
    cat.setAttribute('data-clbi-catlinks-fetching', '1');
    cat.setAttribute('data-clbi-catlinks-page', pageTitle || getCurrentPageTitleForCatlinks());
}
}


function renderFetchedCatlinks(cat, categories, pageTitle) {
function getCatlinksTarget(root) {
     var container;
     var $root = root ? $(root) : $(document);
     var ul;
     var parserOutput = $root.find('.liberty-content-main .mw-parser-output').first();
     var normalized;
     var main = $root.find('.liberty-content-main').first();


     if (!cat) return false;
     if (!parserOutput.length && root && $(root).is('.liberty-content-main')) {
        parserOutput = $(root).find('.mw-parser-output').first();
        main = $(root);
    }


    normalized = dedupeCatlinkCategories(categories);
     if (!parserOutput.length && root && $(root).is('.mw-parser-output')) {
 
         parserOutput = $(root);
     if (!normalized.length) {
         markCatlinksEmpty(cat);
        return false;
     }
     }


     cat.innerHTML = '';
     if (parserOutput.length) return parserOutput;
     cat.classList.add('catlinks');
     if (main.length) return main;
    cat.classList.add('clbi-catlinks-api-populated');


     container = document.createElement('div');
     if (!root) {
    container.className = 'mw-normal-catlinks';
        parserOutput = $('.liberty-content-main .mw-parser-output').first();
    container.appendChild(document.createTextNode('분류: '));
        main = $('.liberty-content-main').first();
        if (parserOutput.length) return parserOutput;
        if (main.length) return main;
    }


     ul = document.createElement('ul');
     return $();
}


    normalized.forEach(function (item) {
var CLBI_CATLINKS_FETCH_TOKEN = 0;
        var title = String(item && item.title ? item.title : '').trim();
        var li;
        var a;


         if (!title) return;
function getCurrentPageTitleForCatlinks() {
    return String(
        mw.config.get('wgPageName') ||
        mw.config.get('wgRelevantPageName') ||
         ''
    ).trim();
}


        li = document.createElement('li');
function shouldFetchCatlinks() {
        a = document.createElement('a');
    var pageName = getCurrentPageTitleForCatlinks();
        a.href = mw.util.getUrl(title);
    var specialPage = mw.config.get('wgCanonicalSpecialPageName');
        a.title = title;
        a.textContent = makeCategoryLinkTitle(title);


        if (item.hidden) {
    if (!pageName) return false;
            li.className = 'clbi-hidden-category-item';
    if (specialPage) return false;
        }
 
        li.appendChild(a);
        ul.appendChild(li);
    });


    if (!ul.children.length) {
        markCatlinksEmpty(cat);
        return false;
    }
    container.appendChild(ul);
    cat.appendChild(container);
    markCatlinksReady(cat, pageTitle);
     return true;
     return true;
}
}


function fetchCatlinksForPage(pageTitle, callback) {
function clearCatlinksInlineHiding(cat) {
     var api;
     if (!cat || !cat.style) return;


     if (typeof pageTitle === 'function') {
     cat.style.removeProperty('display');
        callback = pageTitle;
    cat.style.removeProperty('visibility');
        pageTitle = getCurrentPageTitleForCatlinks();
    cat.style.removeProperty('height');
     }
     cat.style.removeProperty('max-height');
    cat.style.removeProperty('overflow');


     pageTitle = String(pageTitle || '').trim();
     $(cat).find('.mw-normal-catlinks, #mw-normal-catlinks, .mw-hidden-catlinks, #mw-hidden-catlinks, .mw-hidden-cats-hidden, .mw-hidden-cats-user-shown').each(function () {
        if (!this.style) return;
        this.style.removeProperty('display');
        this.style.removeProperty('visibility');
        this.style.removeProperty('height');
        this.style.removeProperty('max-height');
        this.style.removeProperty('overflow');
    });
}


    if (!pageTitle || !shouldFetchCatlinks()) {
function exposeHiddenCatlinks(cat) {
        if (typeof callback === 'function') callback([], pageTitle);
    if (!cat) return;
        return;
    }


     if (!mw.Api) {
     $(cat).find('.mw-hidden-catlinks, #mw-hidden-catlinks, .mw-hidden-cats-hidden, .mw-hidden-cats-user-shown').each(function () {
         if (typeof callback === 'function') callback([], pageTitle);
         this.classList.remove('mw-hidden-cats-hidden');
         return;
         this.classList.add('mw-hidden-cats-user-shown');
    }


    api = new mw.Api();
        if (this.style) {
    api.get({
            this.style.removeProperty('display');
        action: 'query',
            this.style.removeProperty('visibility');
        prop: 'categories',
            this.style.removeProperty('height');
        titles: pageTitle,
            this.style.removeProperty('max-height');
        cllimit: 'max',
            this.style.removeProperty('overflow');
        clprop: 'hidden',
        }
        formatversion: 2
    }).done(function (data) {
        var pages = data && data.query && data.query.pages ? data.query.pages : [];
        var page = pages && pages.length ? pages[0] : null;
        var categories = page && page.categories ? page.categories : [];
 
        if (typeof callback === 'function') callback(categories || [], pageTitle);
    }).fail(function () {
        if (typeof callback === 'function') callback([], pageTitle);
     });
     });
}
}


function finalizeEmptyCatlinks(cat) {
function getCatlinkTextContent(cat) {
     if (!cat) return;
     var clone;
     if (!cat.isConnected) return;
    var text;
 
     if (!cat) return '';


     clearCatlinksInlineHiding(cat);
     clone = cat.cloneNode(true);
     exposeHiddenCatlinks(cat);
     $(clone).find('script, style').remove();


     if (hasRenderedCatlinkItems(cat)) {
     text = String(clone.textContent || '')
         markCatlinksReady(cat, getCurrentPageTitleForCatlinks());
        .replace(/\s+/g, ' ')
         return;
        .replace(/^(분류|숨은 분류|Category|Hidden categories)\s*:\s*/i, '')
    }
         .replace(/^(분류|숨은 분류|Category|Hidden categories)\s*$/i, '')
         .trim();


     markCatlinksEmpty(cat);
     return text;
}
}


function fetchCatlinksIfNeeded(cat) {
function hasRenderedCatlinkItems(cat) {
     var configCategories;
     var hasLink;
     var pageTitle;
     var hasListText;
    var requestToken;


     if (!cat) return;
     if (!cat) return false;
    if (!cat.isConnected) return;


     pageTitle = getCurrentPageTitleForCatlinks();
     hasLink = false;
    $(cat).find('a').each(function () {
        var text = String($(this).text() || '').trim();
        var href = String(this.getAttribute('href') || '').trim();
        if (text || href) hasLink = true;
    });
    if (hasLink) return true;


     clearCatlinksInlineHiding(cat);
     hasListText = false;
     exposeHiddenCatlinks(cat);
    $(cat).find('li').each(function () {
        if (String($(this).text() || '').trim()) hasListText = true;
    });
     if (hasListText) return true;


     if (hasRenderedCatlinkItems(cat)) {
     return !!getCatlinkTextContent(cat);
        markCatlinksReady(cat, pageTitle);
}
        return;
 
     }
function normalizeCategoryTitle(rawTitle) {
    var title = String(rawTitle == null ? '' : rawTitle).trim();
 
    if (!title) return '';
 
     title = title.replace(/_/g, ' ');


     configCategories = getConfigCatlinksCategories();
     if (/^(Category|분류):/i.test(title)) {
    if (configCategories.length) {
         return title;
        renderFetchedCatlinks(cat, configCategories, pageTitle);
         return;
     }
     }


     if (!shouldFetchCatlinks()) {
     return '분류:' + title;
        finalizeEmptyCatlinks(cat);
}
        return;
    }


     if (cat.getAttribute('data-clbi-catlinks-fetching') === '1' && cat.getAttribute('data-clbi-catlinks-page') === pageTitle) return;
function makeCategoryLinkTitle(rawTitle) {
     return String(rawTitle || '')
        .replace(/^Category:/i, '')
        .replace(/^분류:/, '')
        .replace(/_/g, ' ')
        .trim();
}


     requestToken = ++CLBI_CATLINKS_FETCH_TOKEN;
function dedupeCatlinkCategories(categories) {
     markCatlinksPending(cat, pageTitle);
     var seen = {};
     var result = [];


     fetchCatlinksForPage(pageTitle, function (categories, requestedPage) {
     (categories || []).forEach(function (item) {
         if (requestToken !== CLBI_CATLINKS_FETCH_TOKEN) return;
         var title = '';
         if (requestedPage !== getCurrentPageTitleForCatlinks()) return;
         var hidden = false;
        if (!cat || !cat.isConnected) return;
        if (cat.getAttribute('data-clbi-catlinks-page') !== requestedPage) return;


         if (!renderFetchedCatlinks(cat, categories, requestedPage)) {
         if (typeof item === 'string') {
             finalizeEmptyCatlinks(cat);
            title = normalizeCategoryTitle(item);
        } else if (item && typeof item === 'object') {
             title = normalizeCategoryTitle(item.title || item.name || item.category || '');
            hidden = item.hidden !== undefined || item.isHidden === true;
         }
         }
    });
}


function normalizeCatlinksPanel(cat) {
        if (!title) return;
    if (!cat) return;
        if (seen[title]) return;


    cat.classList.add('catlinks');
        seen[title] = true;
    clearCatlinksInlineHiding(cat);
        result.push({ title: title, hidden: hidden });
     exposeHiddenCatlinks(cat);
     });


     if (hasRenderedCatlinkItems(cat)) {
     return result;
        markCatlinksReady(cat, getCurrentPageTitleForCatlinks());
        return;
    }
 
    fetchCatlinksIfNeeded(cat);
}
}


function createCatlinksPanel(target, className) {
function getConfigCatlinksCategories() {
     var cat;
     var normal = mw.config.get('wgCategories') || [];
    var hidden = mw.config.get('wgHiddenCategories') || [];
    var categories = [];


     if (!target || !target.length) return null;
     if (!Array.isArray(normal)) normal = [];
    if (!Array.isArray(hidden)) hidden = [];


     cat = document.createElement('div');
     normal.forEach(function (name) {
    cat.id = 'catlinks';
        categories.push({ title: normalizeCategoryTitle(name), hidden: false });
    cat.className = className || 'catlinks clbi-catlinks-created clbi-catlinks-pending';
     });
    target.append(cat);
     return cat;
}


function prepareSpaCatlinksBeforeInsert(root) {
     hidden.forEach(function (name) {
    var nodes;
         categories.push({ title: normalizeCategoryTitle(name), hidden: true });
    var target;
    var configCategories;
    var pageTitle;
 
    if (!root) return;
 
     pageTitle = getCurrentPageTitleForCatlinks();
    configCategories = getConfigCatlinksCategories();
    nodes = getCatlinkNodes(root);
 
    nodes.forEach(function (node) {
         node.classList.add('catlinks');
        clearCatlinksInlineHiding(node);
        exposeHiddenCatlinks(node);
 
        if (hasRenderedCatlinkItems(node)) {
            markCatlinksReady(node, pageTitle);
        } else if (configCategories.length) {
            renderFetchedCatlinks(node, configCategories, pageTitle);
        } else {
            markCatlinksEmpty(node);
        }
     });
     });


     if (nodes.length || !configCategories.length) return;
     return dedupeCatlinkCategories(categories);
}


    target = getCatlinksTarget(root);
function markCatlinksReady(cat, pageTitle) {
     if (!target.length) target = $(root);
     if (!cat) return;


     renderFetchedCatlinks(createCatlinksPanel(target, 'catlinks catlinks-allhidden clbi-catlinks-created clbi-catlinks-pending'), configCategories, pageTitle);
     cat.classList.add('catlinks');
    cat.classList.add('clbi-catlinks-ready');
    cat.classList.remove('clbi-catlinks-empty');
    cat.classList.remove('clbi-catlinks-pending');
    cat.classList.remove('clbi-catlinks-loading');
    cat.removeAttribute('data-clbi-catlinks-fetching');
    cat.setAttribute('data-clbi-catlinks-page', pageTitle || getCurrentPageTitleForCatlinks());
    clearCatlinksInlineHiding(cat);
    exposeHiddenCatlinks(cat);
}
}


function moveCatlinksToBottom() {
function markCatlinksEmpty(cat) {
     var main = $('.liberty-content-main').first();
     if (!cat) return;
     var target = getCatlinksTarget();
 
     var catlinks = getCatlinkNodes();
    cat.classList.add('catlinks');
     var configCategories;
    cat.classList.add('clbi-catlinks-empty');
     var pageTitle;
    cat.classList.remove('clbi-catlinks-ready');
    var requestToken;
     cat.classList.remove('clbi-catlinks-pending');
     cat.classList.remove('clbi-catlinks-loading');
     cat.removeAttribute('data-clbi-catlinks-fetching');
     cat.removeAttribute('data-clbi-catlinks-page');
}


     if (!main.length || !target.length) return;
function markCatlinksPending(cat, pageTitle) {
     if (!cat) return;


     pageTitle = getCurrentPageTitleForCatlinks();
     cat.classList.add('catlinks');
    cat.classList.remove('clbi-catlinks-ready');
    cat.classList.remove('clbi-catlinks-empty');
    cat.classList.add('clbi-catlinks-pending');
    cat.classList.add('clbi-catlinks-loading');
    cat.setAttribute('data-clbi-catlinks-fetching', '1');
    cat.setAttribute('data-clbi-catlinks-page', pageTitle || getCurrentPageTitleForCatlinks());
}


    catlinks.forEach(function (node) {
function renderFetchedCatlinks(cat, categories, pageTitle) {
        var catNode = $(node);
    var container;
    var ul;
    var normalized;


        if (node.parentNode !== target[0]) {
    if (!cat) return false;
            catNode.appendTo(target);
        }


        normalizeCatlinksPanel(node);
    normalized = dedupeCatlinkCategories(categories);
    });


     if (catlinks.length) return;
     if (!normalized.length) {
        markCatlinksEmpty(cat);
        return false;
    }


     configCategories = getConfigCatlinksCategories();
     cat.innerHTML = '';
     if (configCategories.length) {
    cat.classList.add('catlinks');
        renderFetchedCatlinks(createCatlinksPanel(target, 'catlinks catlinks-allhidden clbi-catlinks-created clbi-catlinks-pending'), configCategories, pageTitle);
     cat.classList.add('clbi-catlinks-api-populated');
        return;
    }


     if (!shouldFetchCatlinks()) return;
     container = document.createElement('div');
    container.className = 'mw-normal-catlinks';
    container.appendChild(document.createTextNode('분류: '));


     requestToken = ++CLBI_CATLINKS_FETCH_TOKEN;
     ul = document.createElement('ul');


     fetchCatlinksForPage(pageTitle, function (categories, requestedPage) {
     normalized.forEach(function (item) {
         var cat;
         var title = String(item && item.title ? item.title : '').trim();
        var li;
        var a;


         if (requestToken !== CLBI_CATLINKS_FETCH_TOKEN) return;
         if (!title) return;
        if (requestedPage !== getCurrentPageTitleForCatlinks()) return;
        if (getCatlinkNodes().length) return;
        if (!categories || !categories.length) return;


         cat = createCatlinksPanel(target, 'catlinks catlinks-allhidden clbi-catlinks-created clbi-catlinks-pending');
         li = document.createElement('li');
         renderFetchedCatlinks(cat, categories, requestedPage);
         a = document.createElement('a');
    });
        a.href = mw.util.getUrl(title);
}
        a.title = title;
        a.textContent = makeCategoryLinkTitle(title);


// 대문 스타일
        if (item.hidden) {
function initCategoryNavIfAvailable(root) {
            li.className = 'clbi-hidden-category-item';
    /*
        }
    CategoryNav.js는 대문 카테고리 네비를 SVG로 생성한다.


    Common.js가 SPA로 본문을 갈아끼운 뒤에는 MediaWiki 원래 페이지 로드와 달리
        li.appendChild(a);
    CategoryNav.js의 초기 DOMContentLoaded만으로는 새 mount를 다시 잡지 못할 수 있다.
        ul.appendChild(li);
    CategoryNav.js 자체도 mw.hook('wikipage.content')를 듣지만, 로드 순서와 SPA 타이밍이
     });
     엇갈릴 수 있으므로 Common.js 쪽에서도 존재 여부를 확인한 뒤 한 번 더 호출한다.


    이 함수는 CategoryNav.js가 아직 로드되지 않았으면 아무 것도 하지 않는다.
     if (!ul.children.length) {
    */
         markCatlinksEmpty(cat);
     if (
        return false;
        window.CLBI &&
        window.CLBI.categoryNav &&
        typeof window.CLBI.categoryNav.init === 'function'
    ) {
         window.CLBI.categoryNav.init(root || document);
     }
     }
    container.appendChild(ul);
    cat.appendChild(container);
    markCatlinksReady(cat, pageTitle);
    return true;
}
}


function removeLegacyMainPageHero() {
function fetchCatlinksForPage(pageTitle, callback) {
     /*
     var api;
    기존 대문 전용 레거시 요소 정리
    -----------------------------------------
    이전 대문 구조에서는 Common.js가 본문 바깥에 #clbi-main-logo를 직접 삽입하고,
    본문 안의 #clbi-main-crt-hero를 #clbi-main-crt-hero-wrap으로 감싸서
    .liberty-content-main 위쪽으로 재배치했다.


     새 대문은 본문 내부의 .main-portal이 로고, 알림, 카테고리 네비, 이미지 피드,
     if (typeof pageTitle === 'function') {
    방명록, 상태 패널을 모두 담당한다. 따라서 Common.js가 별도 로고나 CRT 래퍼를
        callback = pageTitle;
     삽입하면 새 로고/콘텐츠와 중복된다.
        pageTitle = getCurrentPageTitleForCatlinks();
     }


     여기서는 JS가 만들던 바깥 로고와 CRT 래퍼를 제거하고, 예전 대문 원본이나
     pageTitle = String(pageTitle || '').trim();
    캐시된 렌더 결과에 남아 있을 수 있는 #clbi-main-crt-hero도 제거한다.
    */
    $('#clbi-main-logo').remove();
    $('#clbi-main-crt-hero-wrap').remove();
    $('#clbi-main-crt-hero').remove();
}


    if (!pageTitle || !shouldFetchCatlinks()) {
        if (typeof callback === 'function') callback([], pageTitle);
        return;
    }


function setNativePageTitleHiddenHard(hidden) {
    if (!mw.Api) {
    var selectors = [
        if (typeof callback === 'function') callback([], pageTitle);
        '.liberty-content-header',
         return;
         '.liberty-content-header .title',
     }
        '.liberty-content-header .title h1',
        '.liberty-content-header h1',
        '#firstHeading',
        '.firstHeading',
        '.mw-first-heading',
        '.page-heading',
        '.page-header',
        '.mw-page-title-main',
        '.mw-page-title-namespace',
        '.mw-page-title-separator'
     ];


     document.querySelectorAll(selectors.join(',')).forEach(function(node) {
     api = new mw.Api();
         if (!node || !node.style) return;
    api.get({
        action: 'query',
        prop: 'categories',
        titles: pageTitle,
        cllimit: 'max',
        clprop: 'hidden',
        formatversion: 2
    }).done(function (data) {
         var pages = data && data.query && data.query.pages ? data.query.pages : [];
        var page = pages && pages.length ? pages[0] : null;
        var categories = page && page.categories ? page.categories : [];


         if (hidden) {
         if (typeof callback === 'function') callback(categories || [], pageTitle);
            node.setAttribute('data-clbi-title-hidden', 'true');
    }).fail(function () {
            node.style.setProperty('display', 'none', 'important');
         if (typeof callback === 'function') callback([], pageTitle);
            node.style.setProperty('visibility', 'hidden', 'important');
            node.style.setProperty('height', '0', 'important');
            node.style.setProperty('min-height', '0', 'important');
            node.style.setProperty('margin', '0', 'important');
            node.style.setProperty('padding', '0', 'important');
            node.style.setProperty('overflow', 'hidden', 'important');
         } else if (node.getAttribute('data-clbi-title-hidden') === 'true') {
            node.removeAttribute('data-clbi-title-hidden');
            node.style.removeProperty('display');
            node.style.removeProperty('visibility');
            node.style.removeProperty('height');
            node.style.removeProperty('min-height');
            node.style.removeProperty('margin');
            node.style.removeProperty('padding');
            node.style.removeProperty('overflow');
        }
     });
     });
}
}


function applyDefaultPageTitleVisibility() {
function finalizeEmptyCatlinks(cat) {
     var hideTitle = true;
     if (!cat) return;
     var isSystemAssetPage = false;
    if (!cat.isConnected) return;
 
     clearCatlinksInlineHiding(cat);
    exposeHiddenCatlinks(cat);


     if (window.CLBI_PAGE_SHELL && typeof window.CLBI_PAGE_SHELL.isSystemAssetPage === 'function') {
     if (hasRenderedCatlinkItems(cat)) {
         isSystemAssetPage = window.CLBI_PAGE_SHELL.isSystemAssetPage();
         markCatlinksReady(cat, getCurrentPageTitleForCatlinks());
        return;
     }
     }


     if (isSystemAssetPage) {
     markCatlinksEmpty(cat);
        hideTitle = true;
}
     } else if (window.CLBI_PAGE_SHELL && typeof window.CLBI_PAGE_SHELL.isBackendOrSystemPage === 'function') {
 
        hideTitle = !window.CLBI_PAGE_SHELL.isBackendOrSystemPage();
function fetchCatlinksIfNeeded(cat) {
    }
    var configCategories;
    var pageTitle;
    var requestToken;
 
     if (!cat) return;
    if (!cat.isConnected) return;


     $('body')
     pageTitle = getCurrentPageTitleForCatlinks();
        .toggleClass('page-title-hidden', hideTitle)
        .toggleClass('page-title-visible', !hideTitle)
        .toggleClass('clbi-system-doc-page', isSystemAssetPage);


     $('.content-tools').css('display', 'none');
     clearCatlinksInlineHiding(cat);
    exposeHiddenCatlinks(cat);


     if (isSystemAssetPage && window.CLBI_PAGE_SHELL && typeof window.CLBI_PAGE_SHELL.renderSystemDocIndicator === 'function') {
     if (hasRenderedCatlinkItems(cat)) {
         window.CLBI_PAGE_SHELL.renderSystemDocIndicator();
         markCatlinksReady(cat, pageTitle);
    } else if (!isSystemAssetPage && window.CLBI_PAGE_SHELL && typeof window.CLBI_PAGE_SHELL.removeSystemDocIndicator === 'function') {
         return;
         window.CLBI_PAGE_SHELL.removeSystemDocIndicator();
     }
     }


     if (hideTitle) {
     configCategories = getConfigCatlinksCategories();
        $('.liberty-content-header').css('display', 'none');
    if (configCategories.length) {
        $('.mw-page-title-main, .mw-page-title-namespace, .mw-page-title-separator').addClass('clbi-hide');
         renderFetchedCatlinks(cat, configCategories, pageTitle);
        $('#firstHeading, .firstHeading, .mw-first-heading, .page-heading, .page-header').css('display', 'none');
         return;
        setNativePageTitleHiddenHard(true);
    } else {
         $('.liberty-content-header').css('display', '');
        $('.mw-page-title-main, .mw-page-title-namespace, .mw-page-title-separator').removeClass('clbi-hide');
        $('#firstHeading, .firstHeading, .mw-first-heading, .page-heading, .page-header').css('display', '');
         setNativePageTitleHiddenHard(false);
     }
     }
}


function applyMainPageStyle() {
    if (!shouldFetchCatlinks()) {
    var specialPage = mw.config.get('wgCanonicalSpecialPageName');
        finalizeEmptyCatlinks(cat);
    if (specialPage === 'Preferences') return;
        return;
    }


     var pageName = normalizePageName(mw.config.get('wgPageName'));
     if (cat.getAttribute('data-clbi-catlinks-fetching') === '1' && cat.getAttribute('data-clbi-catlinks-page') === pageTitle) return;
    var namespaceNumber = mw.config.get('wgNamespaceNumber');
    var isMainPage = (pageName === '대문');
    var isUserProfilePage = (namespaceNumber === 2);
    var isScreenDoc = ($('.screen-header').length > 0);
    var hideTools = (isMainPage || isUserProfilePage || !canShowContentTools());


     $('body').toggleClass('user-profile-page', isUserProfilePage);
     requestToken = ++CLBI_CATLINKS_FETCH_TOKEN;
     $('body').toggleClass('clbi-main-page', isMainPage);
     markCatlinksPending(cat, pageTitle);


     // 모든 문서에서 분류 바를 본문 컨테이너 아래로 이동
     fetchCatlinksForPage(pageTitle, function (categories, requestedPage) {
    moveCatlinksToBottom();
        if (requestToken !== CLBI_CATLINKS_FETCH_TOKEN) return;
        if (requestedPage !== getCurrentPageTitleForCatlinks()) return;
        if (!cat || !cat.isConnected) return;
        if (cat.getAttribute('data-clbi-catlinks-page') !== requestedPage) return;


    if (isMainPage) {
        if (!renderFetchedCatlinks(cat, categories, requestedPage)) {
        $('.liberty-content-header').css('display', 'none');
            finalizeEmptyCatlinks(cat);
        $('.mw-page-title-main').addClass('clbi-hide');
         }
         setNativePageTitleHiddenHard(true);
    });
        $('.catlinks').css('display', 'none');
}
        $('.liberty-content-main').css('border-radius', '0');


        // 새 대문은 .main-portal 본문 구조가 로고/히어로를 담당한다.
function normalizeCatlinksPanel(cat) {
        // Common.js의 구식 바깥 로고/CRT 재배치 루틴은 사용하지 않는다.
    if (!cat) return;
        removeLegacyMainPageHero();
        $('#clbi-tools-box').remove();


        $('.content-tools').css('display', 'none');
    cat.classList.add('catlinks');
    clearCatlinksInlineHiding(cat);
    exposeHiddenCatlinks(cat);


         initCategoryNavIfAvailable(document);
    if (hasRenderedCatlinkItems(cat)) {
         markCatlinksReady(cat, getCurrentPageTitleForCatlinks());
        return;
    }


     } else if (isUserProfilePage) {
     fetchCatlinksIfNeeded(cat);
        $('.liberty-content-header').css('display', 'none');
}
        $('.mw-page-title-main, .mw-page-title-namespace, .mw-page-title-separator').addClass('clbi-hide');
        $('.catlinks').css('display', 'none');
        $('.liberty-content-main').css('border-radius', '0');


        $('#clbi-main-logo').remove();
function createCatlinksPanel(target, className) {
        $('#clbi-main-crt-hero-wrap').remove();
    var cat;
        $('#clbi-main-crt-hero').remove();
        $('#clbi-tools-box').remove();


        $('.content-tools').css('display', 'none');
    if (!target || !target.length) return null;


     } else if (isScreenDoc) {
     cat = document.createElement('div');
        $('.liberty-content-header').css('display', 'none');
    cat.id = 'catlinks';
        $('.mw-page-title-main').addClass('clbi-hide');
    cat.className = className || 'catlinks clbi-catlinks-created clbi-catlinks-pending';
        $('.catlinks').css('display', '');
    target.append(cat);
        $('.liberty-content-main').css('border-radius', '0');
    return cat;
}


        $('#clbi-main-logo').remove();
function prepareSpaCatlinksBeforeInsert(root) {
        $('#clbi-main-crt-hero-wrap').remove();
    var nodes;
    var target;
    var configCategories;
    var pageTitle;


        if ($('#clbi-tools-box').length === 0 && canShowContentTools()) {
    if (!root) return;
            var $toolsBox = $('<div id="clbi-tools-box" class="clbi-left-box"></div>');
            var $toolsTitle = $('<div class="clbi-left-title">관리</div>');
            var $toolsContent = $('<div class="clbi-left-content"></div>');


            $toolsContent.append($('.content-tools .btn-group').clone(true));
    pageTitle = getCurrentPageTitleForCatlinks();
            $toolsBox.append($toolsTitle).append($toolsContent);
    configCategories = getConfigCatlinksCategories();
            $('#clbi-left-sidebar').append($toolsBox);
    nodes = getCatlinkNodes(root);
        }


         $('.content-tools').css('display', 'none');
    nodes.forEach(function (node) {
         node.classList.add('catlinks');
        clearCatlinksInlineHiding(node);
        exposeHiddenCatlinks(node);


    } else {
         if (hasRenderedCatlinkItems(node)) {
         $('.liberty-content-header').css('display', '');
            markCatlinksReady(node, pageTitle);
         $('.mw-page-title-main, .mw-page-title-namespace, .mw-page-title-separator').removeClass('clbi-hide');
         } else if (configCategories.length) {
         $('.catlinks').css('display', '');
            renderFetchedCatlinks(node, configCategories, pageTitle);
         $('.liberty-content-main').css('border-radius', '0');
         } else {
            markCatlinksEmpty(node);
         }
    });


        $('#clbi-main-logo').remove();
    if (nodes.length || !configCategories.length) return;
        $('#clbi-main-crt-hero-wrap').remove();
        $('#clbi-tools-box').remove();
    }


     if (!isUserProfilePage) {
     target = getCatlinksTarget(root);
        $('.profile-card').remove();
    if (!target.length) target = $(root);
        $('.user-profile-portal').removeClass('user-profile-portal');
    }


     $('.content-tools').css('display', 'none');
     renderFetchedCatlinks(createCatlinksPanel(target, 'catlinks catlinks-allhidden clbi-catlinks-created clbi-catlinks-pending'), configCategories, pageTitle);
}


     applyDefaultPageTitleVisibility();
function moveCatlinksToBottom() {
     updateSidebar();
    var main = $('.liberty-content-main').first();
}
     var target = getCatlinksTarget();
     var catlinks = getCatlinkNodes();
    var configCategories;
    var pageTitle;
    var requestToken;


// 본문 기본 목차 제거
     if (!main.length || !target.length) return;
function removeNativeTocFromContent() {
     $('.liberty-content-main #toc, .liberty-content-main .toc').remove();
}


// 왼쪽 목차: MediaWiki 문단 ID 가져오기
     pageTitle = getCurrentPageTitleForCatlinks();
function getHeadingId(heading) {
     if (heading.id) {
        return heading.id;
    }


     var headline = heading.querySelector('.mw-headline[id]');
     catlinks.forEach(function (node) {
    if (headline && headline.id) {
         var catNode = $(node);
         return headline.id;
    }


    return '';
        if (node.parentNode !== target[0]) {
}
            catNode.appendTo(target);
        }


// 왼쪽 목차: MediaWiki 문단 제목 텍스트 가져오기
        normalizeCatlinksPanel(node);
function getHeadingText(heading) {
     });
    var headline = heading.querySelector('.mw-headline');
     var source = headline || heading;
    var clone = source.cloneNode(true);


     $(clone).find('.mw-editsection, .mw-editsection-bracket, .mw-editsection-divider').remove();
     if (catlinks.length) return;


     return (clone.textContent || '')
     configCategories = getConfigCatlinksCategories();
         .replace(/\s+/g, ' ')
    if (configCategories.length) {
         .trim();
         renderFetchedCatlinks(createCatlinksPanel(target, 'catlinks catlinks-allhidden clbi-catlinks-created clbi-catlinks-pending'), configCategories, pageTitle);
}
         return;
    }


// 왼쪽 목차: 긴 제목에 자동 스크롤 적용
     if (!shouldFetchCatlinks()) return;
function initTocTitleScroll(root) {
     var $items = root
        ? $(root).find('.toc-scroll-text')
        : $('#side-toc-box .toc-scroll-text');


     $items.each(function () {
     requestToken = ++CLBI_CATLINKS_FETCH_TOKEN;
        var $text = $(this);
        var $wrap = $text.closest('.toc-scroll-wrap');


        if (!$wrap.length) return;
    fetchCatlinksForPage(pageTitle, function (categories, requestedPage) {
        var cat;


         var wrapW = Math.floor($wrap.width());
         if (requestToken !== CLBI_CATLINKS_FETCH_TOKEN) return;
         var textW = Math.ceil(this.scrollWidth);
        if (requestedPage !== getCurrentPageTitleForCatlinks()) return;
         if (getCatlinkNodes().length) return;
        if (!categories || !categories.length) return;


         // 왼쪽 목차: 레이아웃 계산이 끝나지 않았으면 이번 실행에서는 건드리지 않는다.
         cat = createCatlinksPanel(target, 'catlinks catlinks-allhidden clbi-catlinks-created clbi-catlinks-pending');
         if (!wrapW || !textW) return;
         renderFetchedCatlinks(cat, categories, requestedPage);
    });
}


        if (textW <= wrapW + 12) {
// 대문 스타일
            // 왼쪽 목차: 칸을 넘지 않는 제목은 전체 텍스트를 그대로 보여준다.
function initCategoryNavIfAvailable(root) {
            $wrap.removeClass('is-scrolling');
    /*
    CategoryNav.js는 대문 카테고리 네비를 SVG로 생성한다.


            if ($text.data('toc-scroll-enabled')) {
    Common.js가 SPA로 본문을 갈아끼운 뒤에는 MediaWiki 원래 페이지 로드와 달리
                $text.css({
    CategoryNav.js의 초기 DOMContentLoaded만으로는 새 mount를 다시 잡지 못할 수 있다.
                    animation: '',
    CategoryNav.js 자체도 mw.hook('wikipage.content')를 듣지만, 로드 순서와 SPA 타이밍이
                    'animation-delay': '',
    엇갈릴 수 있으므로 Common.js 쪽에서도 존재 여부를 확인한 뒤 한 번 더 호출한다.
                    '--scroll-dist': ''
                });
                $text.removeData('toc-scroll-enabled');
                $text.removeData('toc-scroll-key');
            }


            return;
    이 함수는 CategoryNav.js가 아직 로드되지 않았으면 아무 것도 하지 않는다.
        }
    */
    if (window.CategoryNav && typeof window.CategoryNav.init === 'function') {
        window.CategoryNav.init(root || document);
        return;
    }


         var scrollDist = '-' + (textW - wrapW + 10) + 'px';
    if (
         var duration = Math.max(7, textW / 38) * 1.25;
        window.CLBI &&
        var scrollKey = scrollDist + '|' + duration;
        window.CLBI.categoryNav &&
         typeof window.CLBI.categoryNav.init === 'function'
    ) {
         window.CLBI.categoryNav.init(root || document);
    }
}


        // 왼쪽 목차: 긴 제목에는 오른쪽 페이드와 스크롤을 적용한다.
function removeLegacyMainPageHero() {
        $wrap.addClass('is-scrolling');
    /*
    기존 대문 전용 레거시 요소 정리
    -----------------------------------------
    이전 대문 구조에서는 Common.js가 본문 바깥에 #clbi-main-logo를 직접 삽입하고,
    본문 안의 #clbi-main-crt-hero를 #clbi-main-crt-hero-wrap으로 감싸서
    .liberty-content-main 위쪽으로 재배치했다.


        // 왼쪽 목차: 같은 값으로 이미 적용된 애니메이션은 다시 초기화하지 않는다.
    새 대문은 본문 내부의 .main-portal이 로고, 알림, 카테고리 네비, 이미지 피드,
        if ($text.data('toc-scroll-key') === scrollKey) {
    방명록, 상태 패널을 모두 담당한다. 따라서 Common.js가 별도 로고나 CRT 래퍼를
            return;
    삽입하면 새 로고/콘텐츠와 중복된다.
        }


        $text.data('toc-scroll-enabled', true);
    여기서는 JS가 만들던 바깥 로고와 CRT 래퍼를 제거하고, 예전 대문 원본이나
        $text.data('toc-scroll-key', scrollKey);
    캐시된 렌더 결과에 남아 있을 수 있는 #clbi-main-crt-hero도 제거한다.
 
    */
        $text.css({
    $('#clbi-main-logo').remove();
            // 왼쪽 목차: 페이지 진입 직후에는 잠시 읽을 시간을 준 뒤 흐르게 한다.
    $('#clbi-main-crt-hero-wrap').remove();
            animation: 'toc-scroll-blink-reset ' + duration + 's linear infinite',
    $('#clbi-main-crt-hero').remove();
            'animation-delay': '1s',
            '--scroll-dist': scrollDist
        });
    });
}
}


// 목차를 왼쪽 사이드바에 새로 생성
function moveTocToLeftSidebar() {
    removeNativeTocFromContent();
    $('#side-toc-box').remove();
    return;


     // 왼쪽 목차: MediaWiki가 만든 원래 목차는 본문에서 제거한다.
function setNativePageTitleHiddenHard(hidden) {
     removeNativeTocFromContent();
     var selectors = [
        '.liberty-content-header',
        '.liberty-content-header .title',
        '.liberty-content-header .title h1',
        '.liberty-content-header h1',
        '#firstHeading',
        '.firstHeading',
        '.mw-first-heading',
        '.page-heading',
        '.page-header',
        '.mw-page-title-main',
        '.mw-page-title-namespace',
        '.mw-page-title-separator'
     ];


     var leftSidebar = document.getElementById('clbi-left-sidebar');
     document.querySelectorAll(selectors.join(',')).forEach(function(node) {
    if (!leftSidebar) return;
        if (!node || !node.style) return;


    var content =
        if (hidden) {
        document.querySelector('.liberty-content-main .mw-parser-output') ||
            node.setAttribute('data-clbi-title-hidden', 'true');
        document.querySelector('.liberty-content-main');
            node.style.setProperty('display', 'none', 'important');
 
            node.style.setProperty('visibility', 'hidden', 'important');
    if (!content) return;
            node.style.setProperty('height', '0', 'important');
 
            node.style.setProperty('min-height', '0', 'important');
    var headings = Array.prototype.slice.call(
            node.style.setProperty('margin', '0', 'important');
        content.querySelectorAll('h2, h3')
            node.style.setProperty('padding', '0', 'important');
    ).filter(function (heading) {
            node.style.setProperty('overflow', 'hidden', 'important');
         if (heading.closest('#toc, .toc, #side-toc-box')) return false;
         } else if (node.getAttribute('data-clbi-title-hidden') === 'true') {
 
            node.removeAttribute('data-clbi-title-hidden');
        var id = getHeadingId(heading);
            node.style.removeProperty('display');
        var text = getHeadingText(heading);
            node.style.removeProperty('visibility');
 
            node.style.removeProperty('height');
        if (!id || !text) return false;
            node.style.removeProperty('min-height');
 
            node.style.removeProperty('margin');
         return true;
            node.style.removeProperty('padding');
            node.style.removeProperty('overflow');
         }
     });
     });
}


    var tocKey = headings.map(function (heading) {
function applyDefaultPageTitleVisibility() {
        return getHeadingId(heading) + '|' + getHeadingText(heading);
    var hideTitle = true;
     }).join('||');
     var isSystemAssetPage = false;


     var existingBox = document.getElementById('side-toc-box');
     if (window.CLBI_PAGE_SHELL && typeof window.CLBI_PAGE_SHELL.isSystemAssetPage === 'function') {
 
         isSystemAssetPage = window.CLBI_PAGE_SHELL.isSystemAssetPage();
    // 왼쪽 목차: 같은 문서에서 같은 목차를 이미 만들었다면 다시 지우고 만들지 않는다.
    if (existingBox && existingBox.getAttribute('data-toc-key') === tocKey) {
         initTocTitleScroll(existingBox);
        return;
     }
     }


     if (existingBox) {
     if (isSystemAssetPage) {
         existingBox.remove();
         hideTitle = true;
    } else if (window.CLBI_PAGE_SHELL && typeof window.CLBI_PAGE_SHELL.isBackendOrSystemPage === 'function') {
        hideTitle = !window.CLBI_PAGE_SHELL.isBackendOrSystemPage();
     }
     }


     if (!headings.length) return;
     $('body')
        .toggleClass('page-title-hidden', hideTitle)
        .toggleClass('page-title-visible', !hideTitle)
        .toggleClass('clbi-system-doc-page', isSystemAssetPage);


     var tocBox = document.createElement('div');
     $('.content-tools').css('display', 'none');
    tocBox.className = 'clbi-left-box';
    tocBox.id = 'side-toc-box';
    tocBox.setAttribute('data-toc-key', tocKey);


     var title = document.createElement('div');
     if (isSystemAssetPage && window.CLBI_PAGE_SHELL && typeof window.CLBI_PAGE_SHELL.renderSystemDocIndicator === 'function') {
     title.className = 'clbi-left-title';
        window.CLBI_PAGE_SHELL.renderSystemDocIndicator();
     } else if (!isSystemAssetPage && window.CLBI_PAGE_SHELL && typeof window.CLBI_PAGE_SHELL.removeSystemDocIndicator === 'function') {
        window.CLBI_PAGE_SHELL.removeSystemDocIndicator();
    }


     // 왼쪽 목차: 박스 제목은 Lang.js의 현재 UI 언어를 따른다.
     if (hideTitle) {
    var currentLang = getCurrentLang();
        $('.liberty-content-header').css('display', 'none');
     var t = (window.LANG && window.LANG[currentLang]) ? window.LANG[currentLang] : window.LANG.ko;
        $('.mw-page-title-main, .mw-page-title-namespace, .mw-page-title-separator').addClass('clbi-hide');
    var tocTitleText = (t && t.toc) ? t.toc : '목차';
        $('#firstHeading, .firstHeading, .mw-first-heading, .page-heading, .page-header').css('display', 'none');
        setNativePageTitleHiddenHard(true);
     } else {
        $('.liberty-content-header').css('display', '');
        $('.mw-page-title-main, .mw-page-title-namespace, .mw-page-title-separator').removeClass('clbi-hide');
        $('#firstHeading, .firstHeading, .mw-first-heading, .page-heading, .page-header').css('display', '');
        setNativePageTitleHiddenHard(false);
    }
}


     title.textContent = tocTitleText;
function applyMainPageStyle() {
     var specialPage = mw.config.get('wgCanonicalSpecialPageName');
    if (specialPage === 'Preferences') return;


     var body = document.createElement('div');
     var pageName = normalizePageName(mw.config.get('wgPageName'));
     body.className = 'clbi-left-content toc-sidebar-content';
     var namespaceNumber = mw.config.get('wgNamespaceNumber');
    var isMainPage = (pageName === '대문');
    var isUserProfilePage = (namespaceNumber === 2);
    var isScreenDoc = ($('.screen-header').length > 0);
    var hideTools = (isMainPage || isUserProfilePage || !canShowContentTools());


     var list = document.createElement('ul');
     $('body').toggleClass('user-profile-page', isUserProfilePage);
     list.className = 'generated-toc';
     $('body').toggleClass('clbi-main-page', isMainPage);


     headings.forEach(function (heading) {
     // 모든 문서에서 분류 바를 본문 컨테이너 아래로 이동
        var id = getHeadingId(heading);
    moveCatlinksToBottom();
        var text = getHeadingText(heading);
        var level = heading.tagName.toLowerCase() === 'h3' ? 3 : 2;


         var item = document.createElement('li');
    if (isMainPage) {
         item.className = 'toc-level-' + level;
         $('.liberty-content-header').css('display', 'none');
         $('.mw-page-title-main').addClass('clbi-hide');
        setNativePageTitleHiddenHard(true);
        $('.catlinks').css('display', 'none');
        $('.liberty-content-main').css('border-radius', '0');


         var link = document.createElement('a');
         // 새 대문은 .main-portal 본문 구조가 로고/히어로를 담당한다.
         link.setAttribute('href', '#' + id);
        // Common.js의 구식 바깥 로고/CRT 재배치 루틴은 사용하지 않는다.
        removeLegacyMainPageHero();
         $('#clbi-tools-box').remove();


         // 왼쪽 목차: 긴 제목 스크롤을 위해 텍스트를 별도 span으로 감싼다.
         $('.content-tools').css('display', 'none');
        var textWrap = document.createElement('span');
        textWrap.className = 'toc-scroll-wrap';


         var textSpan = document.createElement('span');
         initCategoryNavIfAvailable(document);
        textSpan.className = 'toc-scroll-text';
        textSpan.textContent = text;


         textWrap.appendChild(textSpan);
    } else if (isUserProfilePage) {
         link.appendChild(textWrap);
        $('.liberty-content-header').css('display', 'none');
        $('.mw-page-title-main, .mw-page-title-namespace, .mw-page-title-separator').addClass('clbi-hide');
        $('.catlinks').css('display', 'none');
        $('.liberty-content-main').css('border-radius', '0');
 
        $('#clbi-main-logo').remove();
        $('#clbi-main-crt-hero-wrap').remove();
        $('#clbi-main-crt-hero').remove();
        $('#clbi-tools-box').remove();
 
        $('.content-tools').css('display', 'none');
 
    } else if (isScreenDoc) {
        $('.liberty-content-header').css('display', 'none');
        $('.mw-page-title-main').addClass('clbi-hide');
         $('.catlinks').css('display', '');
         $('.liberty-content-main').css('border-radius', '0');


         item.appendChild(link);
         $('#clbi-main-logo').remove();
         list.appendChild(item);
         $('#clbi-main-crt-hero-wrap').remove();
    });


    body.appendChild(list);
        if ($('#clbi-tools-box').length === 0 && canShowContentTools()) {
    tocBox.appendChild(title);
            var $toolsBox = $('<div id="clbi-tools-box" class="clbi-left-box"></div>');
    tocBox.appendChild(body);
            var $toolsTitle = $('<div class="clbi-left-title">관리</div>');
    leftSidebar.appendChild(tocBox);
            var $toolsContent = $('<div class="clbi-left-content"></div>');


    // 왼쪽 목차: DOM 배치가 끝난 뒤 긴 제목 스크롤 여부를 계산한다.
            $toolsContent.append($('.content-tools .btn-group').clone(true));
    requestAnimationFrame(function () {
            $toolsBox.append($toolsTitle).append($toolsContent);
        initTocTitleScroll(tocBox);
            $('#clbi-left-sidebar').append($toolsBox);
        }


         setTimeout(function () {
         $('.content-tools').css('display', 'none');
            initTocTitleScroll(tocBox);
        }, 120);
    });
}


    } else {
        $('.liberty-content-header').css('display', '');
        $('.mw-page-title-main, .mw-page-title-namespace, .mw-page-title-separator').removeClass('clbi-hide');
        $('.catlinks').css('display', '');
        $('.liberty-content-main').css('border-radius', '0');


// 우측 광고판: 이미지/번역 캡션 목록
         $('#clbi-main-logo').remove();
var RIGHT_BILLBOARD_ITEMS = [
         $('#clbi-main-crt-hero-wrap').remove();
    {
         $('#clbi-tools-box').remove();
         file: 'Side-visual-001.png',
        alt: 'PROOF TO THE WORLD / YOU ONCE PART OF IT',
        duration: 3000,
         caption: [
            '"당신이 한때 이 세계의',
            '',
            '일부였다는 것을 증명하십시오"'
        ]
    },
    {
        file: 'Side-visual-002.png',
        alt: 'APPLY NOW',
        duration: 1000,
        caption: [
            '"지금 지원하세요!"'
        ]
    },
    {
        file: 'Side-visual-003.png',
        alt: 'APPLY NOW',
        duration: 1000,
         caption: [
            '"지금 지원하세요!"'
        ]
    },
    {
        file: 'Side-visual-002.png',
        alt: 'APPLY NOW',
        duration: 1000,
        caption: [
            '"지금 지원하세요!"'
        ]
     }
     }
];


function getRightBillboardItem(index) {
    if (!isUserProfilePage) {
    var items = RIGHT_BILLBOARD_ITEMS;
        $('.profile-card').remove();
 
         $('.user-profile-portal').removeClass('user-profile-portal');
    if (!items || !items.length) {
         return {
            file: 'Side-visual-001.png',
            alt: '',
            caption: []
        };
     }
     }


     var normalized = index % items.length;
     $('.content-tools').css('display', 'none');
    if (normalized < 0) normalized += items.length;


     return items[normalized];
     applyDefaultPageTitleVisibility();
    updateSidebar();
}
}


function getRightBillboardImageUrl(fileName) {
// 본문 기본 목차 제거
     return '/index.php?title=특수:Redirect/file/' + encodeURIComponent(fileName || 'Side-visual-001.png');
function removeNativeTocFromContent() {
     $('.liberty-content-main #toc, .liberty-content-main .toc').remove();
}
}


function getRightBillboardCaptionHtml(item) {
// 왼쪽 목차: MediaWiki 문단 ID 가져오기
     var lines = item && item.caption ? item.caption : [];
function getHeadingId(heading) {
     var html = '';
     if (heading.id) {
        return heading.id;
     }


     function escapeCaptionText(value) {
     var headline = heading.querySelector('.mw-headline[id]');
        return String(value == null ? '' : value)
    if (headline && headline.id) {
            .replace(/&/g, '&amp;')
        return headline.id;
            .replace(/</g, '&lt;')
            .replace(/>/g, '&gt;')
            .replace(/\"/g, '&quot;')
            .replace(/'/g, '&#039;');
     }
     }


     lines.forEach(function(line) {
     return '';
        var text = String(line == null ? '' : line);
}
        var isGap = !text.trim();
 
        var className = 'right-billboard-caption-line' + (isGap ? ' is-gap' : '');
// 왼쪽 목차: MediaWiki 문단 제목 텍스트 가져오기
        html += '<span class="' + className + '">' + (isGap ? '&nbsp;' : escapeCaptionText(text)) + '</span>';
function getHeadingText(heading) {
    });
    var headline = heading.querySelector('.mw-headline');
    var source = headline || heading;
    var clone = source.cloneNode(true);
 
    $(clone).find('.mw-editsection, .mw-editsection-bracket, .mw-editsection-divider').remove();


     return html;
     return (clone.textContent || '')
        .replace(/\s+/g, ' ')
        .trim();
}
}


function setRightBillboardItem(index) {
// 왼쪽 목차: 긴 제목에 자동 스크롤 적용
     var box = document.querySelector('.right-billboard-box');
function initTocTitleScroll(root) {
    if (!box) return;
     var $items = root
        ? $(root).find('.toc-scroll-text')
        : $('#side-toc-box .toc-scroll-text');


     var item = getRightBillboardItem(index);
     $items.each(function () {
    var src = getRightBillboardImageUrl(item.file);
        var $text = $(this);
    var images = box.querySelectorAll('.right-billboard-image');
        var $wrap = $text.closest('.toc-scroll-wrap');
    var caption = box.querySelector('#right-billboard-caption');
    var emptySub = box.querySelector('.right-billboard-empty-sub');


    box.setAttribute('data-billboard-index', String(index));
        if (!$wrap.length) return;
    box.classList.remove('is-empty');


    Array.prototype.forEach.call(images, function(img) {
        var wrapW = Math.floor($wrap.width());
        img.style.display = '';
         var textW = Math.ceil(this.scrollWidth);
        img.setAttribute('src', src);
         img.setAttribute('alt', img.classList.contains('right-billboard-image-base') ? (item.alt || '') : '');
    });


    if (caption) {
        // 왼쪽 목차: 레이아웃 계산이 끝나지 않았으면 이번 실행에서는 건드리지 않는다.
         caption.innerHTML = getRightBillboardCaptionHtml(item);
         if (!wrapW || !textW) return;
    }


    if (emptySub) {
        if (textW <= wrapW + 12) {
        emptySub.textContent = item.file || 'Side-visual-001.png';
            // 왼쪽 목차: 칸을 넘지 않는 제목은 전체 텍스트를 그대로 보여준다.
    }
            $wrap.removeClass('is-scrolling');
}


function getRightBillboardItemDuration(item) {
            if ($text.data('toc-scroll-enabled')) {
    var duration = item && item.duration ? parseInt(item.duration, 10) : 3000;
                $text.css({
                    animation: '',
                    'animation-delay': '',
                    '--scroll-dist': ''
                });
                $text.removeData('toc-scroll-enabled');
                $text.removeData('toc-scroll-key');
            }


    if (Number.isNaN(duration) || duration < 500) {
            return;
         duration = 3000;
         }
    }


    return duration;
        var scrollDist = '-' + (textW - wrapW + 10) + 'px';
}
        var duration = Math.max(7, textW / 38) * 1.25;
        var scrollKey = scrollDist + '|' + duration;
 
        // 왼쪽 목차: 긴 제목에는 오른쪽 페이드와 스크롤을 적용한다.
        $wrap.addClass('is-scrolling');


function initRightBillboardCarousel() {
        // 왼쪽 목차: 같은 값으로 이미 적용된 애니메이션은 다시 초기화하지 않는다.
    var box = document.querySelector('.right-billboard-box');
        if ($text.data('toc-scroll-key') === scrollKey) {
    if (!box || box.getAttribute('data-billboard-ready') === '1') return;
            return;
        }


    box.setAttribute('data-billboard-ready', '1');
        $text.data('toc-scroll-enabled', true);
    box.setAttribute('data-billboard-index', '0');
        $text.data('toc-scroll-key', scrollKey);


     setRightBillboardItem(0);
        $text.css({
            // 왼쪽 목차: 페이지 진입 직후에는 잠시 읽을 시간을 준 뒤 흐르게 한다.
            animation: 'toc-scroll-blink-reset ' + duration + 's linear infinite',
            'animation-delay': '1s',
            '--scroll-dist': scrollDist
        });
     });
}


     if (!RIGHT_BILLBOARD_ITEMS || RIGHT_BILLBOARD_ITEMS.length <= 1) return;
// 목차를 왼쪽 사이드바에 새로 생성
function moveTocToLeftSidebar() {
    removeNativeTocFromContent();
     $('#side-toc-box').remove();
    return;


     function scheduleNext() {
     // 왼쪽 목차: MediaWiki가 만든 원래 목차는 본문에서 제거한다.
        var current = parseInt(box.getAttribute('data-billboard-index') || '0', 10);
    removeNativeTocFromContent();
        if (Number.isNaN(current)) current = 0;


        var currentItem = getRightBillboardItem(current);
    var leftSidebar = document.getElementById('clbi-left-sidebar');
        var delay = getRightBillboardItemDuration(currentItem);
    if (!leftSidebar) return;


         window.setTimeout(function() {
    var content =
            if (!document.body.contains(box)) return;
         document.querySelector('.liberty-content-main .mw-parser-output') ||
        document.querySelector('.liberty-content-main');


            if (!document.hidden) {
    if (!content) return;
                setRightBillboardItem(current + 1);
            }


            scheduleNext();
    var headings = Array.prototype.slice.call(
         }, delay);
        content.querySelectorAll('h2, h3')
    }
    ).filter(function (heading) {
         if (heading.closest('#toc, .toc, #side-toc-box')) return false;


    scheduleNext();
        var id = getHeadingId(heading);
}
        var text = getHeadingText(heading);


        if (!id || !text) return false;


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


function buildRightBillboardBox() {
    var tocKey = headings.map(function (heading) {
    var billboardInitial = getRightBillboardItem(0);
        return getHeadingId(heading) + '|' + getHeadingText(heading);
     var billboardSrc = getRightBillboardImageUrl(billboardInitial.file);
     }).join('||');


     return '' +
     var existingBox = document.getElementById('side-toc-box');
        '<div class="clbi-left-box right-billboard-box left-billboard-box left-ad-box" data-billboard-index="0">' +
            '<div class="clbi-left-title right-billboard-title left-ad-title left-ad-title-iconless">' +
                '<span id="clbi-title-left-ad" class="left-ad-title-label">Looking for a job?</span>' +
            '</div>' +
            '<div class="clbi-left-content left-ad-content-shell">' +
                '<div class="right-billboard-body">' +
                    '<div class="right-billboard-recess">' +
                        '<div class="right-billboard-screen">' +
                            '<img id="right-billboard-image" class="right-billboard-image right-billboard-image-base" src="' + escapeRightBillboardAttr(billboardSrc) + '" alt="' + escapeRightBillboardAttr(billboardInitial.alt || '') + '" onload="var b=this.closest(\'.right-billboard-box\'); if(b){b.classList.remove(\'is-empty\');}" onerror="this.onerror=null;this.style.display=\'none\';var b=this.closest(\'.right-billboard-box\'); if(b){b.classList.add(\'is-empty\');}">' +
                            '<img class="right-billboard-image right-billboard-image-bloom" src="' + escapeRightBillboardAttr(billboardSrc) + '" alt="" aria-hidden="true" onerror="this.onerror=null;this.style.display=\'none\';">' +
                            '<img class="right-billboard-image right-billboard-slice right-billboard-slice-a" src="' + escapeRightBillboardAttr(billboardSrc) + '" alt="" aria-hidden="true" onerror="this.onerror=null;this.style.display=\'none\';">' +
                            '<img class="right-billboard-image right-billboard-slice right-billboard-slice-b" src="' + escapeRightBillboardAttr(billboardSrc) + '" alt="" aria-hidden="true" onerror="this.onerror=null;this.style.display=\'none\';">' +
                            '<img class="right-billboard-image right-billboard-slice right-billboard-slice-c" src="' + escapeRightBillboardAttr(billboardSrc) + '" alt="" aria-hidden="true" onerror="this.onerror=null;this.style.display=\'none\';">' +
                            '<div class="right-billboard-glitch" aria-hidden="true"></div>' +
                            '<div class="right-billboard-tear" aria-hidden="true"></div>' +
                            '<div class="right-billboard-empty" aria-hidden="true">' +
                                '<span class="right-billboard-empty-main">SIGNAL EMPTY</span>' +
                                '<span class="right-billboard-empty-sub">' + escapeRightBillboardAttr(billboardInitial.file || 'Side-visual-001.png') + '</span>' +
                            '</div>' +
                        '</div>' +
                    '</div>' +
                    '<div id="right-billboard-caption" class="right-billboard-caption" aria-hidden="true">' + getRightBillboardCaptionHtml(billboardInitial) + '</div>' +
                    '<div class="right-billboard-bottom-finish left-ad-bottom-finish" aria-hidden="true"></div>' +
                '</div>' +
            '</div>' +
        '</div>';
}


    // 왼쪽 목차: 같은 문서에서 같은 목차를 이미 만들었다면 다시 지우고 만들지 않는다.
    if (existingBox && existingBox.getAttribute('data-toc-key') === tocKey) {
        initTocTitleScroll(existingBox);
        return;
    }


var GREAT_WALL_DATA_TITLE = '프로젝트:The_Great_Wall/Data.json';
     if (existingBox) {
var GREAT_WALL_LIST_LIMIT = 0;
        existingBox.remove();
var greatWallState = {
     }
     data: { entries: {} },
    loaded: false,
    loading: false,
    saving: false,
     selectedOwnEntry: false,
    statusText: ''
};


function normalizeGreatWallData(data) {
    if (!headings.length) return;
    var normalized = { entries: {} };
    var entries = data && data.entries && typeof data.entries === 'object' ? data.entries : {};


     Object.keys(entries).forEach(function(key) {
     var tocBox = document.createElement('div');
        var item = entries[key];
    tocBox.className = 'clbi-left-box';
        var user = item && item.user ? String(item.user) : String(key || '');
    tocBox.id = 'side-toc-box';
        var text = item && item.text ? String(item.text) : '';
    tocBox.setAttribute('data-toc-key', tocKey);
        var timestamp = item && item.timestamp ? String(item.timestamp) : '';


        if (!user || !text) return;
    var title = document.createElement('div');
    title.className = 'clbi-left-title';


        normalized.entries[user] = {
    // 왼쪽 목차: 박스 제목은 Lang.js의 현재 UI 언어를 따른다.
            user: user,
    var currentLang = getCurrentLang();
            text: text.slice(0, 140),
    var t = (window.LANG && window.LANG[currentLang]) ? window.LANG[currentLang] : window.LANG.ko;
            timestamp: timestamp || new Date(0).toISOString()
     var tocTitleText = (t && t.toc) ? t.toc : '목차';
        };
     });


     return normalized;
     title.textContent = tocTitleText;
}


function parseGreatWallData(text) {
    var body = document.createElement('div');
     var parsed;
     body.className = 'clbi-left-content toc-sidebar-content';


     try {
     var list = document.createElement('ul');
        parsed = text ? JSON.parse(text) : {};
     list.className = 'generated-toc';
     } catch (err) {
        console.error('The Great Wall data parse failed:', err);
        parsed = {};
    }


     return normalizeGreatWallData(parsed);
     headings.forEach(function (heading) {
}
        var id = getHeadingId(heading);
        var text = getHeadingText(heading);
        var level = heading.tagName.toLowerCase() === 'h3' ? 3 : 2;


function stringifyGreatWallData(data) {
        var item = document.createElement('li');
    return JSON.stringify(normalizeGreatWallData(data), null, 2) + '\n';
        item.className = 'toc-level-' + level;
}


function getGreatWallRevisionText(page) {
        var link = document.createElement('a');
    var rev;
        link.setAttribute('href', '#' + id);
    var slot;


    if (!page || !page.revisions || !page.revisions.length) return '';
        // 왼쪽 목차: 긴 제목 스크롤을 위해 텍스트를 별도 span으로 감싼다.
        var textWrap = document.createElement('span');
        textWrap.className = 'toc-scroll-wrap';


    rev = page.revisions[0];
        var textSpan = document.createElement('span');
        textSpan.className = 'toc-scroll-text';
        textSpan.textContent = text;


    if (rev.slots && rev.slots.main) {
        textWrap.appendChild(textSpan);
        slot = rev.slots.main;
         link.appendChild(textWrap);
         return slot.content || slot['*'] || '';
    }


    return rev.content || rev['*'] || '';
        item.appendChild(link);
}
        list.appendChild(item);
    });


function fetchGreatWallData() {
    body.appendChild(list);
     var api = new mw.Api();
    tocBox.appendChild(title);
    tocBox.appendChild(body);
     leftSidebar.appendChild(tocBox);


     return api.get({
     // 왼쪽 목차: DOM 배치가 끝난 뒤 긴 제목 스크롤 여부를 계산한다.
        action: 'query',
     requestAnimationFrame(function () {
        prop: 'revisions',
         initTocTitleScroll(tocBox);
        titles: GREAT_WALL_DATA_TITLE,
        rvprop: 'content|timestamp',
        rvslots: 'main',
        formatversion: 2
     }).then(function(data) {
         var pages = data && data.query && data.query.pages ? data.query.pages : [];
        var page = pages[0] || null;


         if (!page || page.missing) {
         setTimeout(function () {
             return { entries: {} };
             initTocTitleScroll(tocBox);
        }
        }, 120);
 
        return parseGreatWallData(getGreatWallRevisionText(page));
    }, function(err) {
        console.error('The Great Wall load failed:', err);
        return { entries: {} };
     });
     });
}
}


function getGreatWallEntries(data) {
    var entries = data && data.entries && typeof data.entries === 'object' ? data.entries : {};


     return Object.keys(entries).map(function(key) {
// 우측 광고판: 이미지/번역 캡션 목록
         return entries[key];
var RIGHT_BILLBOARD_ITEMS = [
     }).filter(function(item) {
     {
         return item && item.user && item.text;
        file: 'Side-visual-001.png',
     }).sort(function(a, b) {
        alt: 'PROOF TO THE WORLD / YOU ONCE PART OF IT',
         return String(a.timestamp || '').localeCompare(String(b.timestamp || ''));
        duration: 3000,
     });
         caption: [
}
            '"당신이 한때 이 세계의',
 
            '',
function getGreatWallMessageTime(timestamp) {
            '일부였다는 것을 증명하십시오"'
    var date = timestamp ? new Date(timestamp) : null;
        ]
     var month;
     },
    var day;
    {
    var hour;
         file: 'Side-visual-002.png',
     var minute;
        alt: 'APPLY NOW',
 
        duration: 1000,
     if (!date || isNaN(date.getTime())) return '';
        caption: [
 
            '"지금 지원하세요!"'
    month = String(date.getMonth() + 1).padStart(2, '0');
        ]
     day = String(date.getDate()).padStart(2, '0');
     },
     hour = String(date.getHours()).padStart(2, '0');
    {
    minute = String(date.getMinutes()).padStart(2, '0');
         file: 'Side-visual-003.png',
        alt: 'APPLY NOW',
        duration: 1000,
        caption: [
            '"지금 지원하세요!"'
        ]
     },
    {
        file: 'Side-visual-002.png',
        alt: 'APPLY NOW',
        duration: 1000,
        caption: [
            '"지금 지원하세요!"'
        ]
     }
];
 
function getRightBillboardItem(index) {
     var items = RIGHT_BILLBOARD_ITEMS;
 
     if (!items || !items.length) {
        return {
            file: 'Side-visual-001.png',
            alt: '',
            caption: []
        };
     }
 
    var normalized = index % items.length;
     if (normalized < 0) normalized += items.length;


     return month + '.' + day + ' ' + hour + ':' + minute;
     return items[normalized];
}
}


function getGreatWallAvatarSrc(user) {
function getRightBillboardImageUrl(fileName) {
     return '/index.php?title=특수:Redirect/file/Pfp-' + encodeURIComponent(String(user || '')) + '.png';
     return '/index.php?title=특수:Redirect/file/' + encodeURIComponent(fileName || 'Side-visual-001.png');
}
}


function buildGreatWallEntryHtml(item, currentUser, selectedOwnEntry) {
function getRightBillboardCaptionHtml(item) {
     var isOwn = currentUser && item.user === currentUser;
     var lines = item && item.caption ? item.caption : [];
    var tag = isOwn ? 'button' : 'div';
     var html = '';
    var attrs = isOwn ? ' type="button" data-great-wall-own-entry="1" aria-label="Edit your wall message"' : '';
    var className = 'great-wall-entry' + (isOwn ? ' is-own' : '') + (isOwn && selectedOwnEntry ? ' is-selected' : '');
    var avatarSrc = getGreatWallAvatarSrc(item.user);
     var messageTime = getGreatWallMessageTime(item.timestamp);
    var isoTime = item.timestamp || '';


     return '' +
     function escapeCaptionText(value) {
        '<' + tag + attrs + ' class="' + className + '">' +
        return String(value == null ? '' : value)
             '<img class="great-wall-avatar" src="' + escapeClbiHtml(avatarSrc) + '" alt="" onerror="this.onerror=null;this.src=&quot;/index.php?title=특수:Redirect/file/Pfp-default.png&quot;;">' +
            .replace(/&/g, '&amp;')
            '<div class="great-wall-bubble">' +
            .replace(/</g, '&lt;')
                '<div class="great-wall-entry-head">' +
            .replace(/>/g, '&gt;')
                    '<span class="great-wall-user">@' + escapeClbiHtml(item.user) + '</span>' +
            .replace(/\"/g, '&quot;')
                    '<span class="great-wall-time" title="' + escapeClbiHtml(isoTime) + '">' + escapeClbiHtml(messageTime) + '</span>' +
             .replace(/'/g, '&#039;');
                '</div>' +
    }
                '<div class="great-wall-text">' + escapeClbiHtml(item.text) + '</div>' +
 
            '</div>' +
    lines.forEach(function(line) {
        '</' + tag + '>';
        var text = String(line == null ? '' : line);
        var isGap = !text.trim();
        var className = 'right-billboard-caption-line' + (isGap ? ' is-gap' : '');
        html += '<span class="' + className + '">' + (isGap ? '&nbsp;' : escapeCaptionText(text)) + '</span>';
    });
 
    return html;
}
}


function renderGreatWallBox() {
function setRightBillboardItem(index) {
     var box = document.getElementById('great-wall-sidebar');
     var box = document.querySelector('.right-billboard-box');
    var list = document.getElementById('great-wall-list');
     if (!box) return;
     var input = document.getElementById('great-wall-input');
    var submit = document.getElementById('great-wall-submit');
    var deleteButton = document.getElementById('great-wall-delete');
    var status = document.getElementById('great-wall-status');
    var currentUser = mw.config.get('wgUserName') || '';
    var entries = getGreatWallEntries(greatWallState.data);
    var ownEntry = currentUser && greatWallState.data.entries ? greatWallState.data.entries[currentUser] : null;
    var visibleEntries = [];
    var seen = {};


     if (!box || !list) return;
     var item = getRightBillboardItem(index);
    var src = getRightBillboardImageUrl(item.file);
    var images = box.querySelectorAll('.right-billboard-image');
    var caption = box.querySelector('#right-billboard-caption');
    var emptySub = box.querySelector('.right-billboard-empty-sub');


     box.classList.toggle('is-guest', !currentUser);
     box.setAttribute('data-billboard-index', String(index));
    box.classList.toggle('is-loading', !!greatWallState.loading);
     box.classList.remove('is-empty');
    box.classList.toggle('is-saving', !!greatWallState.saving);
    box.classList.toggle('has-own-entry', !!ownEntry);
     box.classList.toggle('is-own-selected', !!greatWallState.selectedOwnEntry);


     entries.forEach(function(item) {
     Array.prototype.forEach.call(images, function(img) {
         if (seen[item.user]) return;
         img.style.display = '';
         visibleEntries.push(item);
         img.setAttribute('src', src);
         seen[item.user] = true;
         img.setAttribute('alt', img.classList.contains('right-billboard-image-base') ? (item.alt || '') : '');
     });
     });


     if (greatWallState.loading && !greatWallState.loaded) {
     if (caption) {
         list.innerHTML = '<div class="great-wall-empty">SYNCING WALL</div>';
         caption.innerHTML = getRightBillboardCaptionHtml(item);
    } else if (!visibleEntries.length) {
        list.innerHTML = '<div class="great-wall-empty">NO MARKS</div>';
    } else {
        list.innerHTML = visibleEntries.map(function(item) {
            return buildGreatWallEntryHtml(item, currentUser, greatWallState.selectedOwnEntry);
        }).join('');
     }
     }


     if (list && list.scrollHeight > list.clientHeight) {
     if (emptySub) {
         list.scrollTop = list.scrollHeight;
         emptySub.textContent = item.file || 'Side-visual-001.png';
     }
     }
}


    if (status) {
function getRightBillboardItemDuration(item) {
        status.textContent = greatWallState.statusText || (currentUser ? (ownEntry ? 'SELECT YOUR MARK TO UPDATE' : 'LEAVE ONE MARK') : 'ACCOUNT REQUIRED');
    var duration = item && item.duration ? parseInt(item.duration, 10) : 3000;
    }


     if (!input || !submit) return;
     if (Number.isNaN(duration) || duration < 500) {
        duration = 3000;
    }


     input.disabled = false;
     return duration;
    input.readOnly = false;
}
    input.removeAttribute('aria-readonly');


    if (!currentUser) {
function initRightBillboardCarousel() {
        input.disabled = true;
    var box = document.querySelector('.right-billboard-box');
        input.readOnly = false;
    if (!box || box.getAttribute('data-billboard-ready') === '1') return;
        input.value = '';
        input.placeholder = 'Login required';
        submit.textContent = 'LOGIN';
        submit.disabled = false;
        if (deleteButton) {
            deleteButton.disabled = true;
            deleteButton.title = 'Login required';
        }
        return;
    }


     if (greatWallState.saving || greatWallState.loading) {
     box.setAttribute('data-billboard-ready', '1');
        input.disabled = true;
    box.setAttribute('data-billboard-index', '0');
        input.readOnly = false;
        submit.disabled = true;
        submit.textContent = greatWallState.saving ? 'SAVE' : 'SYNC';
        if (deleteButton) {
            deleteButton.disabled = true;
            deleteButton.title = greatWallState.saving ? 'Saving' : 'Syncing';
        }
        return;
    }


     if (deleteButton) {
     setRightBillboardItem(0);
        deleteButton.disabled = !ownEntry;
        deleteButton.title = ownEntry ? 'Delete your mark' : 'No mark to delete';
    }


     if (ownEntry && !greatWallState.selectedOwnEntry) {
     if (!RIGHT_BILLBOARD_ITEMS || RIGHT_BILLBOARD_ITEMS.length <= 1) return;
        input.disabled = false;
        input.readOnly = true;
        input.setAttribute('aria-readonly', 'true');
        input.value = '';
        input.placeholder = 'Select your existing mark';
        submit.disabled = true;
        submit.textContent = 'UPDATE';
        return;
    }


     input.disabled = false;
     function scheduleNext() {
    input.readOnly = false;
        var current = parseInt(box.getAttribute('data-billboard-index') || '0', 10);
    input.removeAttribute('aria-readonly');
        if (Number.isNaN(current)) current = 0;
    input.placeholder = ownEntry ? 'Update your mark' : 'Leave your mark';
    if (ownEntry && greatWallState.selectedOwnEntry && !input.value) {
        input.value = ownEntry.text || '';
    }
    submit.disabled = false;
    submit.textContent = ownEntry ? 'UPDATE' : 'POST';
}


function saveGreatWallEntry() {
        var currentItem = getRightBillboardItem(current);
    var input = document.getElementById('great-wall-input');
        var delay = getRightBillboardItemDuration(currentItem);
    var currentUser = mw.config.get('wgUserName') || '';
 
    var ownEntry = currentUser && greatWallState.data.entries ? greatWallState.data.entries[currentUser] : null;
        window.setTimeout(function() {
    var text = input ? String(input.value || '').trim() : '';
            if (!document.body.contains(box)) return;
    var api;


    if (!currentUser) {
            if (!document.hidden) {
        window.location.href = '/index.php?title=특수:로그인&returnto=' + encodeURIComponent(mw.config.get('wgPageName') || '대문');
                setRightBillboardItem(current + 1);
        return;
            }
    }


    if (ownEntry && !greatWallState.selectedOwnEntry) {
            scheduleNext();
        greatWallState.statusText = 'SELECT YOUR MARK FIRST';
         }, delay);
         renderGreatWallBox();
        return;
     }
     }


     if (!text) {
     scheduleNext();
        greatWallState.statusText = 'EMPTY MARK';
}
        renderGreatWallBox();
        return;
    }


    if (text.length > 140) {
        text = text.slice(0, 140);
    }


     greatWallState.saving = true;
function escapeRightBillboardAttr(value) {
    greatWallState.statusText = 'SAVING';
     return String(value == null ? '' : value)
    renderGreatWallBox();
        .replace(/&/g, '&amp;')
        .replace(/</g, '&lt;')
        .replace(/>/g, '&gt;')
        .replace(/"/g, '&quot;')
        .replace(/'/g, '&#039;');
}


    fetchGreatWallData().then(function(data) {
function buildRightBillboardBox() {
        data = normalizeGreatWallData(data);
    var billboardInitial = getRightBillboardItem(0);
        data.entries[currentUser] = {
    var billboardSrc = getRightBillboardImageUrl(billboardInitial.file);
            user: currentUser,
            text: text,
            timestamp: new Date().toISOString()
        };


        api = new mw.Api();
    return '' +
         return api.postWithToken('csrf', {
         '<div class="clbi-left-box right-billboard-box left-billboard-box left-ad-box" data-billboard-index="0">' +
             action: 'edit',
             '<div class="clbi-left-title right-billboard-title left-ad-title left-ad-title-iconless">' +
            title: GREAT_WALL_DATA_TITLE,
                '<span id="clbi-title-left-ad" class="left-ad-title-label">Looking for a job?</span>' +
             text: stringifyGreatWallData(data),
             '</div>' +
             summary: 'Update The Great Wall entry',
             '<div class="clbi-left-content left-ad-content-shell">' +
            format: 'json'
                '<div class="right-billboard-body">' +
        }).then(function() {
                    '<div class="right-billboard-recess">' +
            greatWallState.data = data;
                        '<div class="right-billboard-screen">' +
            greatWallState.loaded = true;
                            '<img id="right-billboard-image" class="right-billboard-image right-billboard-image-base" src="' + escapeRightBillboardAttr(billboardSrc) + '" alt="' + escapeRightBillboardAttr(billboardInitial.alt || '') + '" onload="var b=this.closest(\'.right-billboard-box\'); if(b){b.classList.remove(\'is-empty\');}" onerror="this.onerror=null;this.style.display=\'none\';var b=this.closest(\'.right-billboard-box\'); if(b){b.classList.add(\'is-empty\');}">' +
            greatWallState.selectedOwnEntry = false;
                            '<img class="right-billboard-image right-billboard-image-bloom" src="' + escapeRightBillboardAttr(billboardSrc) + '" alt="" aria-hidden="true" onerror="this.onerror=null;this.style.display=\'none\';">' +
            greatWallState.statusText = 'MARK UPDATED';
                            '<img class="right-billboard-image right-billboard-slice right-billboard-slice-a" src="' + escapeRightBillboardAttr(billboardSrc) + '" alt="" aria-hidden="true" onerror="this.onerror=null;this.style.display=\'none\';">' +
            if (input) input.value = '';
                            '<img class="right-billboard-image right-billboard-slice right-billboard-slice-b" src="' + escapeRightBillboardAttr(billboardSrc) + '" alt="" aria-hidden="true" onerror="this.onerror=null;this.style.display=\'none\';">' +
        });
                            '<img class="right-billboard-image right-billboard-slice right-billboard-slice-c" src="' + escapeRightBillboardAttr(billboardSrc) + '" alt="" aria-hidden="true" onerror="this.onerror=null;this.style.display=\'none\';">' +
    }).then(function() {
                            '<div class="right-billboard-glitch" aria-hidden="true"></div>' +
        greatWallState.saving = false;
                            '<div class="right-billboard-tear" aria-hidden="true"></div>' +
        renderGreatWallBox();
                            '<div class="right-billboard-empty" aria-hidden="true">' +
    }, function(err) {
                                '<span class="right-billboard-empty-main">SIGNAL EMPTY</span>' +
        console.error('The Great Wall save failed:', err);
                                '<span class="right-billboard-empty-sub">' + escapeRightBillboardAttr(billboardInitial.file || 'Side-visual-001.png') + '</span>' +
        greatWallState.saving = false;
                            '</div>' +
        greatWallState.statusText = 'SAVE FAILED';
                        '</div>' +
        renderGreatWallBox();
                    '</div>' +
    });
                    '<div id="right-billboard-caption" class="right-billboard-caption" aria-hidden="true">' + getRightBillboardCaptionHtml(billboardInitial) + '</div>' +
                    '<div class="right-billboard-bottom-finish left-ad-bottom-finish" aria-hidden="true"></div>' +
                '</div>' +
            '</div>' +
        '</div>';
}
}




function deleteGreatWallEntry() {
var GREAT_WALL_DATA_TITLE = '프로젝트:The_Great_Wall/Data.json';
    var input = document.getElementById('great-wall-input');
var GREAT_WALL_LIST_LIMIT = 0;
    var currentUser = mw.config.get('wgUserName') || '';
var greatWallState = {
    var ownEntry = currentUser && greatWallState.data.entries ? greatWallState.data.entries[currentUser] : null;
    data: { entries: {} },
     var api;
    loaded: false,
    loading: false,
    saving: false,
    selectedOwnEntry: false,
     statusText: ''
};


    if (!currentUser) {
function normalizeGreatWallData(data) {
        window.location.href = '/index.php?title=특수:로그인&returnto=' + encodeURIComponent(mw.config.get('wgPageName') || '대문');
    var normalized = { entries: {} };
        return;
    var entries = data && data.entries && typeof data.entries === 'object' ? data.entries : {};
    }


     if (!ownEntry) {
     Object.keys(entries).forEach(function(key) {
         greatWallState.statusText = 'NO MARK TO DELETE';
         var item = entries[key];
         renderGreatWallBox();
        var user = item && item.user ? String(item.user) : String(key || '');
         return;
         var text = item && item.text ? String(item.text) : '';
    }
         var timestamp = item && item.timestamp ? String(item.timestamp) : '';


    greatWallState.saving = true;
        if (!user || !text) return;
    greatWallState.statusText = 'DELETING';
    renderGreatWallBox();


    fetchGreatWallData().then(function(data) {
        normalized.entries[user] = {
        data = normalizeGreatWallData(data);
            user: user,
        if (data.entries && data.entries[currentUser]) {
            text: text.slice(0, 140),
            delete data.entries[currentUser];
            timestamp: timestamp || new Date(0).toISOString()
        }
        };
    });


        api = new mw.Api();
    return normalized;
        return api.postWithToken('csrf', {
            action: 'edit',
            title: GREAT_WALL_DATA_TITLE,
            text: stringifyGreatWallData(data),
            summary: 'Delete The Great Wall entry',
            format: 'json'
        }).then(function() {
            greatWallState.data = data;
            greatWallState.loaded = true;
            greatWallState.selectedOwnEntry = false;
            greatWallState.statusText = 'MARK DELETED';
            if (input) input.value = '';
        });
    }).then(function() {
        greatWallState.saving = false;
        renderGreatWallBox();
    }, function(err) {
        console.error('The Great Wall delete failed:', err);
        greatWallState.saving = false;
        greatWallState.statusText = 'DELETE FAILED';
        renderGreatWallBox();
    });
}
}


function initGreatWallBoxWhenReady() {
function parseGreatWallData(text) {
     if (!document.getElementById('great-wall-sidebar')) return;
     var parsed;


     if (mw.Api) {
     try {
         initGreatWallBox();
        parsed = text ? JSON.parse(text) : {};
         return;
    } catch (err) {
         console.error('The Great Wall data parse failed:', err);
         parsed = {};
     }
     }


     if (mw.loader && mw.loader.using) {
     return normalizeGreatWallData(parsed);
        mw.loader.using(['mediawiki.api']).then(function() {
}
            initGreatWallBox();
 
        });
function stringifyGreatWallData(data) {
    }
    return JSON.stringify(normalizeGreatWallData(data), null, 2) + '\n';
}
}


function initGreatWallBox() {
function getGreatWallRevisionText(page) {
     var box = document.getElementById('great-wall-sidebar');
     var rev;
    var input = document.getElementById('great-wall-input');
     var slot;
    var submit = document.getElementById('great-wall-submit');
     var deleteButton = document.getElementById('great-wall-delete');


     if (!box || box.getAttribute('data-great-wall-ready') === '1') return;
     if (!page || !page.revisions || !page.revisions.length) return '';


     if (!mw.Api) {
    rev = page.revisions[0];
         initGreatWallBoxWhenReady();
 
         return;
     if (rev.slots && rev.slots.main) {
         slot = rev.slots.main;
         return slot.content || slot['*'] || '';
     }
     }


     box.setAttribute('data-great-wall-ready', '1');
     return rev.content || rev['*'] || '';
}


    box.addEventListener('click', function(e) {
function fetchGreatWallData() {
        var ownButton = e.target.closest ? e.target.closest('[data-great-wall-own-entry="1"]') : null;
    var api = new mw.Api();
        var currentUser = mw.config.get('wgUserName') || '';
        var ownEntry = currentUser && greatWallState.data.entries ? greatWallState.data.entries[currentUser] : null;


         if (!ownButton || !ownEntry) return;
    return api.get({
        action: 'query',
        prop: 'revisions',
        titles: GREAT_WALL_DATA_TITLE,
        rvprop: 'content|timestamp',
         rvslots: 'main',
        formatversion: 2
    }).then(function(data) {
        var pages = data && data.query && data.query.pages ? data.query.pages : [];
        var page = pages[0] || null;


         greatWallState.selectedOwnEntry = true;
         if (!page || page.missing) {
        greatWallState.statusText = 'YOUR MARK SELECTED';
            return { entries: {} };
         renderGreatWallBox();
         }


         if (input) {
         return parseGreatWallData(getGreatWallRevisionText(page));
            input.focus();
    }, function(err) {
            input.setSelectionRange(input.value.length, input.value.length);
        console.error('The Great Wall load failed:', err);
         }
         return { entries: {} };
     });
     });
}


    document.addEventListener('click', function(e) {
function getGreatWallEntries(data) {
        var target = e.target;
    var entries = data && data.entries && typeof data.entries === 'object' ? data.entries : {};
        var keepSelection;


         if (!greatWallState.selectedOwnEntry || !target || !target.closest) return;
    return Object.keys(entries).map(function(key) {
         return entries[key];
    }).filter(function(item) {
        return item && item.user && item.text;
    }).sort(function(a, b) {
        return String(a.timestamp || '').localeCompare(String(b.timestamp || ''));
    });
}


        keepSelection = target.closest('[data-great-wall-own-entry="1"], .great-wall-editor, .great-wall-compose-sector');
function getGreatWallMessageTime(timestamp) {
    var date = timestamp ? new Date(timestamp) : null;
    var month;
    var day;
    var hour;
    var minute;


        if (keepSelection) return;
    if (!date || isNaN(date.getTime())) return '—';


        greatWallState.selectedOwnEntry = false;
    month = String(date.getMonth() + 1).padStart(2, '0');
        greatWallState.statusText = '';
    day = String(date.getDate()).padStart(2, '0');
        if (input) input.value = '';
    hour = String(date.getHours()).padStart(2, '0');
        renderGreatWallBox();
     minute = String(date.getMinutes()).padStart(2, '0');
     });


     if (submit) {
     return month + '.' + day + ' ' + hour + ':' + minute;
        submit.addEventListener('click', function(e) {
}
            e.preventDefault();
            if (!mw.config.get('wgUserName')) {
                window.location.href = '/index.php?title=특수:로그인&returnto=' + encodeURIComponent(mw.config.get('wgPageName') || '대문');
                return;
            }
            saveGreatWallEntry();
        });
    }


    if (deleteButton) {
function getGreatWallAvatarSrc(user) {
        deleteButton.addEventListener('click', function(e) {
    return '/index.php?title=특수:Redirect/file/Pfp-' + encodeURIComponent(String(user || '')) + '.png';
            e.preventDefault();
}
            deleteGreatWallEntry();
        });
    }


    if (input) {
function buildGreatWallEntryHtml(item, currentUser, selectedOwnEntry) {
        input.addEventListener('keydown', function(e) {
    var isOwn = currentUser && item.user === currentUser;
            if (e.key === 'Enter') {
    var tag = isOwn ? 'button' : 'div';
                e.preventDefault();
    var attrs = isOwn ? ' type="button" data-great-wall-own-entry="1" aria-label="Edit your wall message"' : '';
                saveGreatWallEntry();
    var className = 'great-wall-entry' + (isOwn ? ' is-own' : '') + (isOwn && selectedOwnEntry ? ' is-selected' : '');
            }
    var avatarSrc = getGreatWallAvatarSrc(item.user);
        });
    var messageTime = getGreatWallMessageTime(item.timestamp);
    var isoTime = item.timestamp || '';


         input.addEventListener('input', function() {
    return '' +
             if (input.value.length > 140) {
         '<' + tag + attrs + ' class="' + className + '">' +
                 input.value = input.value.slice(0, 140);
            '<img class="great-wall-avatar" src="' + escapeClbiHtml(avatarSrc) + '" alt="" onerror="this.onerror=null;this.src=&quot;/index.php?title=특수:Redirect/file/Pfp-default.png&quot;;">' +
             }
             '<div class="great-wall-bubble">' +
         });
                '<div class="great-wall-entry-head">' +
    }
                    '<span class="great-wall-user">@' + escapeClbiHtml(item.user) + '</span>' +
                    '<span class="great-wall-time" title="' + escapeClbiHtml(isoTime) + '">' + escapeClbiHtml(messageTime) + '</span>' +
                '</div>' +
                 '<div class="great-wall-text">' + escapeClbiHtml(item.text) + '</div>' +
             '</div>' +
         '</' + tag + '>';
}


     greatWallState.loading = true;
function renderGreatWallBox() {
     greatWallState.statusText = 'SYNCING WALL';
     var box = document.getElementById('great-wall-sidebar');
     renderGreatWallBox();
     var list = document.getElementById('great-wall-list');
 
     var input = document.getElementById('great-wall-input');
     fetchGreatWallData().then(function(data) {
     var submit = document.getElementById('great-wall-submit');
        greatWallState.data = normalizeGreatWallData(data);
    var deleteButton = document.getElementById('great-wall-delete');
        greatWallState.loaded = true;
    var status = document.getElementById('great-wall-status');
        greatWallState.loading = false;
    var currentUser = mw.config.get('wgUserName') || '';
        greatWallState.selectedOwnEntry = false;
    var entries = getGreatWallEntries(greatWallState.data);
        greatWallState.statusText = '';
    var ownEntry = currentUser && greatWallState.data.entries ? greatWallState.data.entries[currentUser] : null;
        renderGreatWallBox();
    var visibleEntries = [];
     });
     var seen = {};
}


function buildGreatWallBox() {
    if (!box || !list) return;
    return '' +
        '<div id="great-wall-sidebar" class="clbi-right-box great-wall-sidebar">' +
            '<div class="clbi-right-title great-wall-title">' +
                '<span id="clbi-title-great-wall">The Great Wall</span>' +
            '</div>' +
            '<div class="clbi-right-content great-wall-content">' +
                '<div id="great-wall-list" class="great-wall-list"><div class="great-wall-empty">SYNCING WALL</div></div>' +
            '</div>' +
            '<div class="great-wall-compose-sector" aria-label="The Great Wall editor">' +
                '<div class="great-wall-editor">' +
                    '<input id="great-wall-input" class="great-wall-input" type="text" maxlength="140" autocomplete="off" placeholder="Leave your mark">' +
                    '<button id="great-wall-submit" class="great-wall-submit" type="button">POST</button>' +
                    '<button id="great-wall-delete" class="great-wall-delete" type="button" disabled>DEL</button>' +
                '</div>' +
                '<div id="great-wall-status" class="great-wall-status">SYNCING WALL</div>' +
            '</div>' +
        '</div>';
}


function removeMainPortalGuestbookPreview() {
    box.classList.toggle('is-guest', !currentUser);
     var portal = document.querySelector('.main-portal');
     box.classList.toggle('is-loading', !!greatWallState.loading);
     var guestbook = portal ? portal.querySelector('.guestbook-device') : null;
     box.classList.toggle('is-saving', !!greatWallState.saving);
     var sideScreen;
     box.classList.toggle('has-own-entry', !!ownEntry);
     var grid;
     box.classList.toggle('is-own-selected', !!greatWallState.selectedOwnEntry);


     if (!portal || !guestbook) return;
     entries.forEach(function(item) {
        if (seen[item.user]) return;
        visibleEntries.push(item);
        seen[item.user] = true;
    });


     sideScreen = guestbook.closest ? guestbook.closest('.side-screen') : null;
     if (greatWallState.loading && !greatWallState.loaded) {
     grid = sideScreen && sideScreen.closest ? sideScreen.closest('.console-grid') : null;
        list.innerHTML = '<div class="great-wall-empty">SYNCING WALL</div>';
    } else if (!visibleEntries.length) {
        list.innerHTML = '<div class="great-wall-empty">NO MARKS</div>';
     } else {
        list.innerHTML = visibleEntries.map(function(item) {
            return buildGreatWallEntryHtml(item, currentUser, greatWallState.selectedOwnEntry);
        }).join('');
    }


     if (guestbook.parentNode) {
     if (list && list.scrollHeight > list.clientHeight) {
         guestbook.parentNode.removeChild(guestbook);
         list.scrollTop = list.scrollHeight;
     }
     }


     if (sideScreen && !(sideScreen.textContent || '').trim() && !sideScreen.querySelector('img,svg,video,canvas,form,input,button,a')) {
     if (status) {
         if (sideScreen.parentNode) {
         // Status text is intentionally suppressed in the composer strip; the area is spacing-only UI.
            sideScreen.parentNode.removeChild(sideScreen);
         status.textContent = '';
        }
 
         if (grid) {
            grid.style.gridTemplateColumns = 'minmax(0,1fr)';
        }
     }
     }


     portal.classList.add('is-great-wall-relocated');
     if (!input || !submit) return;
}


function buildSiteInformationBox() {
     input.disabled = false;
     return '' +
    input.readOnly = false;
        '<div class="clbi-right-box site-info-sidebar">' +
    input.removeAttribute('aria-readonly');
            '<div class="clbi-right-title site-info-title">' +
                '<span>정보</span>' +
            '</div>' +
            '<div class="clbi-right-content site-info-content">' +
                '<div class="policy-list">' +
                    '<div class="policy-row"><a href="/index.php/개인정보처리방침" class="site-info-policy-button"><span class="site-info-policy-title">개인정보처리방침</span><span class="site-info-policy-arrow" aria-hidden="true">›</span></a></div>' +
                    '<div class="policy-row"><a href="/index.php/면책_조항" class="site-info-policy-button"><span class="site-info-policy-title">면책 조항</span><span class="site-info-policy-arrow" aria-hidden="true">›</span></a></div>' +
                    '<div class="policy-row"><a href="/index.php/라이선스" class="site-info-policy-button"><span class="site-info-policy-title">라이선스</span><span class="site-info-policy-arrow" aria-hidden="true">›</span></a></div>' +
                    '<div class="policy-row"><a href="/index.php/크레딧" class="site-info-policy-button"><span class="site-info-policy-title">크레딧</span><span class="site-info-policy-arrow" aria-hidden="true">›</span></a></div>' +
                '</div>' +
                '<div class="social-strip">' +
                    '<span class="social-icon"><a href="https://discord.gg/ctaeJ9d3Q5" target="_blank" rel="noopener noreferrer">DC</a></span>' +
                    '<span class="social-icon"><a href="https://www.youtube.com/@nxdsxn" target="_blank" rel="noopener noreferrer">YT</a></span>' +
                    '<span class="social-icon"><a href="https://x.com/nxd_sxn" target="_blank" rel="noopener noreferrer">X</a></span>' +
                    '<span class="social-icon"><a href="/index.php/프로젝트:소개">WIP:</a></span>' +
                '</div>' +
            '</div>' +
        '</div>';
}


    if (!currentUser) {
        input.disabled = true;
        input.readOnly = false;
        input.value = '';
        input.placeholder = '담벼락';
        submit.textContent = 'LOGIN';
        submit.disabled = false;
        if (deleteButton) {
            deleteButton.disabled = true;
            deleteButton.title = 'Login required';
        }
        return;
    }


// 초기화 함수
    if (greatWallState.saving || greatWallState.loading) {
function initSidebars() {
        input.disabled = true;
    var header = $('.liberty-content-header');
        input.readOnly = false;
    var content = $('.liberty-content');
        submit.disabled = true;
        submit.textContent = greatWallState.saving ? 'SAVE' : 'SYNC';
        if (deleteButton) {
            deleteButton.disabled = true;
            deleteButton.title = greatWallState.saving ? 'Saving' : 'Syncing';
        }
        return;
    }


     if (header.length && content.length) {
     if (deleteButton) {
         header.prependTo(content);
         deleteButton.disabled = !ownEntry;
        deleteButton.title = ownEntry ? 'Delete your mark' : 'No mark to delete';
     }
     }


     if ($('#clbi-right-sidebar').length === 0) {
     if (ownEntry && !greatWallState.selectedOwnEntry) {
        var username = mw.config.get('wgUserName');
        input.disabled = false;
        var isLoggedIn = username !== null;
        input.readOnly = true;
        var avatarSrc = isLoggedIn
        input.setAttribute('aria-readonly', 'true');
            ? '/index.php?title=특수:Redirect/file/Pfp-' + username + '.png'
        input.value = '';
            : '/index.php?title=특수:Redirect/file/Pfp-default.png';
        input.placeholder = '담벼락';
        submit.disabled = true;
        submit.textContent = 'UPDATE';
        return;
    }
 
    input.disabled = false;
    input.readOnly = false;
    input.removeAttribute('aria-readonly');
    input.placeholder = '담벼락';
    if (ownEntry && greatWallState.selectedOwnEntry && !input.value) {
        input.value = ownEntry.text || '';
    }
    submit.disabled = false;
    submit.textContent = ownEntry ? 'UPDATE' : 'POST';
}
 
function saveGreatWallEntry() {
    var input = document.getElementById('great-wall-input');
    var currentUser = mw.config.get('wgUserName') || '';
    var ownEntry = currentUser && greatWallState.data.entries ? greatWallState.data.entries[currentUser] : null;
    var text = input ? String(input.value || '').trim() : '';
    var api;
 
    if (!currentUser) {
        window.location.href = '/index.php?title=특수:로그인&returnto=' + encodeURIComponent(mw.config.get('wgPageName') || '대문');
        return;
    }


         var userBox;
    if (ownEntry && !greatWallState.selectedOwnEntry) {
         greatWallState.statusText = '';
        renderGreatWallBox();
        return;
    }


        if (isLoggedIn) {
    if (!text) {
            userBox =
        greatWallState.statusText = 'EMPTY MARK';
                '<div class="clbi-right-box profile-card-box">' +
        renderGreatWallBox();
                    '<div id="clbi-user-avatar-wrap" class="profile-identity-panel">' +
         return;
                        '<div class="profile-avatar-stage">' +
    }
                            '<img id="clbi-user-avatar" src="' + avatarSrc + '" onerror="this.onerror=null;this.src=\'/index.php?title=특수:Redirect/file/Pfp-default.png\';">' +
                        '</div>' +
                        '<div id="clbi-user-name-row" class="profile-name-row">' +
                            '<a href="/index.php/사용자:' + username + '" id="clbi-user-name">' + username + '</a>' +
                        '</div>' +
                    '</div>' +
                    '<div class="clbi-right-content profile-action-box">' +
                        '<div class="profile-quick-actions" aria-label="프로필 빠른 메뉴">' +
                            '<button type="button" class="profile-quick-btn" id="profile-quick-inventory" aria-label="인벤토리"><span class="profile-quick-icon" aria-hidden="true">' + CLBI_SVG_PACKAGE + '</span><span class="profile-quick-tip" aria-hidden="true">인벤토리</span></button>' +
                            '<button type="button" class="profile-quick-btn" id="profile-quick-achievements" aria-label="업적"><span class="profile-quick-icon" aria-hidden="true">' + CLBI_SVG_TROPHY + '</span><span class="profile-quick-tip" aria-hidden="true">업적</span></button>' +
                            '<button type="button" class="profile-quick-btn" id="profile-quick-notifications" aria-label="알림"><span id="profile-quick-notification-icon" class="profile-quick-icon" aria-hidden="true">' + CLBI_SVG_BELL + '</span><span class="profile-quick-tip" aria-hidden="true">알림</span></button>' +
                        '</div>' +
                        '<a href="/index.php/특수:기여/' + username + '" class="clbi-user-btn" id="clbi-btn-contribution"><span class="profile-action-icon" aria-hidden="true">' + CLBI_SVG_SCAN_TEXT + '</span><span class="profile-action-label">기여</span><i class="hn hn-angle-right-solid profile-action-arrow" aria-hidden="true"></i></a>' +
                        '<a href="/index.php/특수:주시문서목록" class="clbi-user-btn" id="clbi-btn-watchlist"><span class="profile-action-icon" aria-hidden="true">' + CLBI_SVG_SCAN_EYE + '</span><span class="profile-action-label">주시문서 목록</span><i class="hn hn-angle-right-solid profile-action-arrow" aria-hidden="true"></i></a>' +
                        '<a href="/index.php/특수:설정" class="clbi-user-btn" id="clbi-btn-preferences"><span class="profile-action-icon" aria-hidden="true">' + CLBI_SVG_SETTINGS + '</span><span class="profile-action-label">설정</span><i class="hn hn-angle-right-solid profile-action-arrow" aria-hidden="true"></i></a>' +
                        '<a href="/index.php?title=특수:로그아웃&returnto=대문" class="clbi-user-btn clbi-user-btn-logout" id="clbi-btn-logout"><span class="profile-action-icon" aria-hidden="true">' + CLBI_SVG_POWER + '</span><span class="profile-action-label">로그아웃</span><i class="hn hn-angle-right-solid profile-action-arrow" aria-hidden="true"></i></a>' +
                    '</div>' +
                '</div>';
         } else {
            userBox =
                '<div class="clbi-right-box profile-card-box">' +
                    '<div id="clbi-user-avatar-wrap" class="profile-identity-panel">' +
                        '<div class="profile-avatar-stage">' +
                            '<img id="clbi-user-avatar" src="/index.php?title=특수:Redirect/file/Pfp-default.png">' +
                        '</div>' +
                        '<div id="clbi-user-name-row" class="profile-name-row profile-name-row-guest">' +
                            '<span id="clbi-user-name">Guest</span>' +
                        '</div>' +
                    '</div>' +
                    '<div class="clbi-right-content profile-action-box">' +
                        '<a href="/index.php?title=특수:로그인&returnto=대문" class="clbi-user-btn" id="clbi-btn-login"><span class="profile-action-icon" aria-hidden="true">' + CLBI_SVG_POWER + '</span><span class="profile-action-label">로그인</span><i class="hn hn-angle-right-solid profile-action-arrow" aria-hidden="true"></i></a>' +
                    '</div>' +
                '</div>';
        }
        var greatWallBox = '';
        var siteInformationBox = '';


        try {
    if (text.length > 140) {
            greatWallBox = buildGreatWallBox();
         text = text.slice(0, 140);
        } catch (err) {
            console.error('The Great Wall build failed:', err);
            greatWallBox = '';
        }
 
         try {
            siteInformationBox = buildSiteInformationBox();
        } catch (err) {
            console.error('Site information build failed:', err);
            siteInformationBox = '';
        }
 
 
        var sidebar = userBox + greatWallBox + siteInformationBox;
 
        $('.content-wrapper').append('<div id="clbi-right-sidebar">' + sidebar + '</div>');
        initGreatWallBoxWhenReady();
        removeMainPortalGuestbookPreview();
     }
     }


     initGreatWallBoxWhenReady();
     greatWallState.saving = true;
     removeMainPortalGuestbookPreview();
     greatWallState.statusText = 'SAVING';
    renderGreatWallBox();


    fetchGreatWallData().then(function(data) {
        data = normalizeGreatWallData(data);
        data.entries[currentUser] = {
            user: currentUser,
            text: text,
            timestamp: new Date().toISOString()
        };


    if ($('#clbi-left-sidebar').length === 0) {
        api = new mw.Api();
var leftBillboardBox = '';
        return api.postWithToken('csrf', {
 
            action: 'edit',
         try {
            title: GREAT_WALL_DATA_TITLE,
            leftBillboardBox = buildRightBillboardBox();
            text: stringifyGreatWallData(data),
        } catch (err) {
            summary: 'Update The Great Wall entry',
            console.error('Left billboard build failed:', err);
            format: 'json'
            leftBillboardBox = '';
        }).then(function() {
         }
            greatWallState.data = data;
            greatWallState.loaded = true;
            greatWallState.selectedOwnEntry = false;
            greatWallState.statusText = 'MARK UPDATED';
            if (input) input.value = '';
         });
    }).then(function() {
        greatWallState.saving = false;
        renderGreatWallBox();
    }, function(err) {
        console.error('The Great Wall save failed:', err);
        greatWallState.saving = false;
        greatWallState.statusText = 'SAVE FAILED';
         renderGreatWallBox();
    });
}


var leftSidebar =
    '<div id="clbi-left-sidebar">' +
        '<div class="clbi-left-box clbi-left-lang-box">' +
            '<div class="clbi-left-title">' +
                '<span id="clbi-title-left-language">언어</span>' +
            '</div>' +
            '<div class="clbi-left-content sidebar-lang-box">' +
                '<div id="clbi-sidebar-lang-selector" class="sidebar-lang-selector sidebar-lang-dial" tabindex="0" role="group" aria-label="언어 선택">' +
                    '<div id="clbi-sidebar-lang-dial-stage" class="sidebar-lang-dial-stage">' +
                        '<div id="clbi-sidebar-lang-fan" class="sidebar-lang-fan" aria-hidden="true"></div>' +
                        '<div id="clbi-sidebar-lang-selected-panel" class="sidebar-lang-status-panel sidebar-lang-status-left" aria-hidden="true">' +
                            '<span id="clbi-sidebar-lang-selected-value" class="sidebar-lang-status-value">한국어</span>' +
                        '</div>' +
                        '<div id="clbi-sidebar-lang-availability-panel" class="sidebar-lang-status-panel sidebar-lang-status-right is-current" aria-hidden="true">' +
                            '<span id="clbi-sidebar-lang-availability-value" class="sidebar-lang-status-value">CURRENT</span>' +
                        '</div>' +
                        '<button type="button" id="clbi-sidebar-lang-apply" class="sidebar-lang-apply" aria-label="언어 적용">' +
                            '<span class="sidebar-lang-apply-mark" aria-hidden="true">✓</span>' +
                        '</button>' +
                    '</div>' +
                '</div>' +
            '</div>' +
        '</div>' +
        '<div class="clbi-left-box clbi-left-news-box">' +
            '<div class="clbi-left-title">' +
                '<span id="clbi-title-left-news">뉴스</span>' +
            '</div>' +
            '<div class="clbi-left-content clbi-news-box">' +


                '<div class="news-feed-title" id="clbi-left-news-changelog-title">CHANGELOG</div>' +
function deleteGreatWallEntry() {
                '<div class="news-left-changelog-feed">' +
    var input = document.getElementById('great-wall-input');
                    '<a href="/index.php/체인지로그" class="news-post-item">' +
    var currentUser = mw.config.get('wgUserName') || '';
                        '<div class="news-post-title-wrap">' +
    var ownEntry = currentUser && greatWallState.data.entries ? greatWallState.data.entries[currentUser] : null;
                            '<span class="news-post-title" id="clbi-left-news-changelog-main">체인지로그</span>' +
    var api;
                        '</div>' +
                        '<span class="news-post-jump" aria-hidden="true">›</span>' +
                    '</a>' +
                '</div>' +


                '<div class="news-divider"></div>' +
    if (!currentUser) {
        window.location.href = '/index.php?title=특수:로그인&returnto=' + encodeURIComponent(mw.config.get('wgPageName') || '대문');
        return;
    }


                '<div class="news-feed-title" id="clbi-left-news-recent-title">RECENT CHANGES</div>' +
    if (!ownEntry) {
                '<div class="news-left-recent-feed" id="clbi-left-recent-list">불러오는 중...</div>' +
        greatWallState.statusText = 'NO MARK TO DELETE';
        renderGreatWallBox();
        return;
    }
 
    greatWallState.saving = true;
    greatWallState.statusText = 'DELETING';
    renderGreatWallBox();


                '<a class="news-fill-image-slot" id="clbi-left-news-fill-image" href="/index.php/특수:최근바뀜" aria-label="최근 바뀜으로 이동">' +
    fetchGreatWallData().then(function(data) {
                    '<div class="news-fill-image-frame">' +
        data = normalizeGreatWallData(data);
                        '<span class="news-fill-image" style="--news-fill-image-url:url(\'/index.php?title=특수:Redirect/file/Side-news-fill-001.png\');" aria-hidden="true"></span><img class="news-fill-image-probe" src="/index.php?title=특수:Redirect/file/Side-news-fill-001.png" alt="" aria-hidden="true" onerror="this.onerror=null;this.closest(\'.news-fill-image-slot\').classList.add(\'is-empty\');this.remove();">' +
        if (data.entries && data.entries[currentUser]) {
                    '</div>' +
            delete data.entries[currentUser];
                '</a>' +
        }


             '</div>' +
        api = new mw.Api();
         '</div>' +
        return api.postWithToken('csrf', {
         leftBillboardBox +
            action: 'edit',
     '</div>';
            title: GREAT_WALL_DATA_TITLE,
            text: stringifyGreatWallData(data),
             summary: 'Delete The Great Wall entry',
            format: 'json'
         }).then(function() {
            greatWallState.data = data;
            greatWallState.loaded = true;
            greatWallState.selectedOwnEntry = false;
            greatWallState.statusText = 'MARK DELETED';
            if (input) input.value = '';
        });
    }).then(function() {
        greatWallState.saving = false;
         renderGreatWallBox();
     }, function(err) {
        console.error('The Great Wall delete failed:', err);
        greatWallState.saving = false;
        greatWallState.statusText = 'DELETE FAILED';
        renderGreatWallBox();
    });
}


        $('.content-wrapper').prepend(leftSidebar);
function initGreatWallBoxWhenReady() {
    if (!document.getElementById('great-wall-sidebar')) return;


        renderSidebarLanguageBox();
    if (mw.Api) {
        loadRecentChangesList('#clbi-left-recent-list', 10);
         initGreatWallBox();
         scheduleAdaptiveLeftRecentItems();
         return;
         scheduleLeftBillboardAdaptive();
        scheduleClbiContentBottomGap();
        updateLeftSidebarNationsImage();
     }
     }


     try {
     if (mw.loader && mw.loader.using) {
         initRightBillboardCarousel();
         mw.loader.using(['mediawiki.api']).then(function() {
    } catch (err) {
            initGreatWallBox();
         console.error('Right billboard carousel failed:', err);
         });
     }
     }
}


     if (typeof window.normalizeClbiShellDomOrder === 'function') window.normalizeClbiShellDomOrder();
function initGreatWallBox() {
     applyMainPageStyle();
     var box = document.getElementById('great-wall-sidebar');
     initClbiCustomDocumentScrollbars();
    var input = document.getElementById('great-wall-input');
     initCategoryNavIfAvailable(document);
     var submit = document.getElementById('great-wall-submit');
     var deleteButton = document.getElementById('great-wall-delete');
 
     if (!box || box.getAttribute('data-great-wall-ready') === '1') return;


     if (window.ProgressSystemWebUi && typeof window.ProgressSystemWebUi.boot === 'function') {
     if (!mw.Api) {
         window.ProgressSystemWebUi.boot('initSidebars');
         initGreatWallBoxWhenReady();
        return;
     }
     }


     $('#side-toc-box').remove();
     box.setAttribute('data-great-wall-ready', '1');
 
    box.addEventListener('click', function(e) {
        var ownButton = e.target.closest ? e.target.closest('[data-great-wall-own-entry="1"]') : null;
        var currentUser = mw.config.get('wgUserName') || '';
        var ownEntry = currentUser && greatWallState.data.entries ? greatWallState.data.entries[currentUser] : null;


    mw.loader.using(['mediawiki.api']).then(function() {
         if (!ownButton || !ownEntry) return;
         setTimeout(function() {
            initNotifications();
            initProfile();
            moveTocToLeftSidebar();
        }, 300);


         setTimeout(moveTocToLeftSidebar, 800);
         greatWallState.selectedOwnEntry = true;
         setTimeout(moveTocToLeftSidebar, 1500);
         greatWallState.statusText = '';
    });
        renderGreatWallBox();
}


$(function() {
        if (input) {
    loadLangScript(function() {
            input.focus();
        setTimeout(function() {
             input.setSelectionRange(input.value.length, input.value.length);
             initSidebars();
         }
         }, 100);
     });
     });
});


$(document).on('click.profileQuickPlaceholder', '#profile-quick-inventory, #profile-quick-achievements', function(e) {
    document.addEventListener('click', function(e) {
    e.preventDefault();
        var target = e.target;
    e.stopPropagation();
        var keepSelection;
});


function extractJsonArrayAfterMwConfigKey(text, key) {
        if (!greatWallState.selectedOwnEntry || !target || !target.closest) return;
    var needle = '"' + key + '"';
    var keyIndex = String(text || '').indexOf(needle);
    var start;
    var i;
    var depth = 0;
    var inString = false;
    var escaped = false;


    if (keyIndex === -1) return null;
        keepSelection = target.closest('[data-great-wall-own-entry="1"], .great-wall-editor, .great-wall-compose-sector');


    start = String(text || '').indexOf('[', keyIndex + needle.length);
        if (keepSelection) return;
    if (start === -1) return null;


    for (i = start; i < text.length; i += 1) {
        greatWallState.selectedOwnEntry = false;
         var ch = text.charAt(i);
        greatWallState.statusText = '';
        if (input) input.value = '';
         renderGreatWallBox();
    });


        if (inString) {
    if (submit) {
            if (escaped) {
        submit.addEventListener('click', function(e) {
                escaped = false;
            e.preventDefault();
             } else if (ch === '\\') {
             if (!mw.config.get('wgUserName')) {
                 escaped = true;
                 window.location.href = '/index.php?title=특수:로그인&returnto=' + encodeURIComponent(mw.config.get('wgPageName') || '대문');
            } else if (ch === '"') {
                 return;
                 inString = false;
             }
             }
             continue;
             saveGreatWallEntry();
         }
         });
    }
 
    if (deleteButton) {
        deleteButton.addEventListener('click', function(e) {
            e.preventDefault();
            deleteGreatWallEntry();
        });
    }


         if (ch === '"') {
    if (input) {
            inString = true;
         input.addEventListener('keydown', function(e) {
             continue;
            if (e.key === 'Enter') {
         }
                e.preventDefault();
                saveGreatWallEntry();
             }
         });


         if (ch === '[') depth += 1;
         input.addEventListener('input', function() {
        if (ch === ']') {
             if (input.value.length > 140) {
            depth -= 1;
                 input.value = input.value.slice(0, 140);
             if (depth === 0) {
                 try {
                    return JSON.parse(text.slice(start, i + 1));
                } catch (err) {
                    return null;
                }
             }
             }
         }
         });
     }
     }


     return null;
     greatWallState.loading = true;
}
    greatWallState.statusText = 'SYNCING WALL';
    renderGreatWallBox();


function extractJsonStringAfterMwConfigKey(text, key) {
    fetchGreatWallData().then(function(data) {
    var needle = '"' + key + '"';
        greatWallState.data = normalizeGreatWallData(data);
    var keyIndex = String(text || '').indexOf(needle);
        greatWallState.loaded = true;
    var colon;
        greatWallState.loading = false;
    var start;
        greatWallState.selectedOwnEntry = false;
    var i;
        greatWallState.statusText = '';
     var escaped = false;
        renderGreatWallBox();
     });
}


     if (keyIndex === -1) return null;
function buildGreatWallBox() {
     return '' +
        '<div id="great-wall-sidebar" class="clbi-right-box great-wall-sidebar">' +
            '<div class="clbi-right-title great-wall-title">' +
                '<span id="clbi-title-great-wall">The Great Wall</span>' +
            '</div>' +
            '<div class="clbi-right-content great-wall-content">' +
                '<div id="great-wall-list" class="great-wall-list"><div class="great-wall-empty">SYNCING WALL</div></div>' +
            '</div>' +
            '<div class="great-wall-compose-sector" aria-label="The Great Wall editor">' +
                '<div class="great-wall-editor">' +
                    '<input id="great-wall-input" class="great-wall-input" type="text" maxlength="140" autocomplete="off" placeholder="담벼락">' +
                    '<button id="great-wall-submit" class="great-wall-submit" type="button">POST</button>' +
                    '<button id="great-wall-delete" class="great-wall-delete" type="button" disabled>DEL</button>' +
                '</div>' +
                '<div id="great-wall-status" class="great-wall-status">SYNCING WALL</div>' +
            '</div>' +
        '</div>';
}


     colon = text.indexOf(':', keyIndex + needle.length);
function removeMainPortalGuestbookPreview() {
     if (colon === -1) return null;
     var portal = document.querySelector('.main-portal');
     var guestbook = portal ? portal.querySelector('.guestbook-device') : null;
    var sideScreen;
    var grid;


    start = text.indexOf('"', colon + 1);
     if (!portal || !guestbook) return;
     if (start === -1) return null;


     for (i = start + 1; i < text.length; i += 1) {
     sideScreen = guestbook.closest ? guestbook.closest('.side-screen') : null;
        var ch = text.charAt(i);
    grid = sideScreen && sideScreen.closest ? sideScreen.closest('.console-grid') : null;


        if (escaped) {
    if (guestbook.parentNode) {
            escaped = false;
        guestbook.parentNode.removeChild(guestbook);
            continue;
    }
        }


        if (ch === '\\') {
    if (sideScreen && !(sideScreen.textContent || '').trim() && !sideScreen.querySelector('img,svg,video,canvas,form,input,button,a')) {
            escaped = true;
        if (sideScreen.parentNode) {
             continue;
             sideScreen.parentNode.removeChild(sideScreen);
         }
         }


         if (ch === '"') {
         if (grid) {
             try {
             grid.style.gridTemplateColumns = 'minmax(0,1fr)';
                return JSON.parse(text.slice(start, i + 1));
            } catch (err) {
                return text.slice(start + 1, i);
            }
         }
         }
     }
     }


     return null;
     portal.classList.add('is-great-wall-relocated');
}
}


function syncCatlinksConfigFromSpaDocument(doc) {
function buildSiteInformationBox() {
     var scripts = doc ? doc.querySelectorAll('script') : [];
     return '' +
    var categories = null;
        '<div class="clbi-right-box site-info-sidebar">' +
    var hiddenCategories = null;
            '<div class="clbi-right-title site-info-title">' +
    var relevantPageName = null;
                '<span>정보</span>' +
    var pageName = null;
            '</div>' +
    var i;
            '<div class="clbi-right-content site-info-content">' +
    var text;
                '<div class="policy-list">' +
    var value;
                    '<div class="policy-row"><a href="/index.php/개인정보처리방침" class="site-info-policy-button"><span class="site-info-policy-title">개인정보처리방침</span><span class="site-info-policy-arrow" aria-hidden="true">›</span></a></div>' +
                    '<div class="policy-row"><a href="/index.php/면책_조항" class="site-info-policy-button"><span class="site-info-policy-title">면책 조항</span><span class="site-info-policy-arrow" aria-hidden="true">›</span></a></div>' +
                    '<div class="policy-row"><a href="/index.php/라이선스" class="site-info-policy-button"><span class="site-info-policy-title">라이선스</span><span class="site-info-policy-arrow" aria-hidden="true">›</span></a></div>' +
                    '<div class="policy-row"><a href="/index.php/크레딧" class="site-info-policy-button"><span class="site-info-policy-title">크레딧</span><span class="site-info-policy-arrow" aria-hidden="true">›</span></a></div>' +
                '</div>' +
                '<div class="social-strip">' +
                    '<span class="social-icon"><a href="https://discord.gg/ctaeJ9d3Q5" target="_blank" rel="noopener noreferrer">DC</a></span>' +
                    '<span class="social-icon"><a href="https://www.youtube.com/@nxdsxn" target="_blank" rel="noopener noreferrer">YT</a></span>' +
                    '<span class="social-icon"><a href="https://x.com/nxd_sxn" target="_blank" rel="noopener noreferrer">X</a></span>' +
                    '<span class="social-icon"><a href="/index.php/프로젝트:소개">WIP:</a></span>' +
                '</div>' +
            '</div>' +
        '</div>';
}


    for (i = 0; i < scripts.length; i += 1) {
        text = scripts[i].textContent || '';


        if (categories === null) {
// 초기화 함수
            value = extractJsonArrayAfterMwConfigKey(text, 'wgCategories');
function initSidebars() {
            if (Array.isArray(value)) categories = value;
    var header = $('.liberty-content-header');
        }
    var content = $('.liberty-content');


        if (hiddenCategories === null) {
    if (header.length && content.length) {
            value = extractJsonArrayAfterMwConfigKey(text, 'wgHiddenCategories');
        header.prependTo(content);
            if (Array.isArray(value)) hiddenCategories = value;
    }
        }


        if (relevantPageName === null) {
    if ($('#clbi-right-sidebar').length === 0) {
            value = extractJsonStringAfterMwConfigKey(text, 'wgRelevantPageName');
        var username = mw.config.get('wgUserName');
            if (value !== null) relevantPageName = value;
        var isLoggedIn = username !== null;
         }
         var avatarSrc = isLoggedIn
            ? '/index.php?title=특수:Redirect/file/Pfp-' + username + '.png'
            : '/index.php?title=특수:Redirect/file/Pfp-default.png';


         if (pageName === null) {
         var userBox;
            value = extractJsonStringAfterMwConfigKey(text, 'wgPageName');
            if (value !== null) pageName = value;
        }
    }


    mw.config.set('wgCategories', Array.isArray(categories) ? categories : []);
        if (isLoggedIn) {
    mw.config.set('wgHiddenCategories', Array.isArray(hiddenCategories) ? hiddenCategories : []);
            userBox =
 
                '<div class="clbi-right-box profile-card-box">' +
    if (relevantPageName !== null) {
                    '<div id="clbi-user-avatar-wrap" class="profile-identity-panel">' +
        mw.config.set('wgRelevantPageName', relevantPageName);
                        '<div class="profile-avatar-stage">' +
    } else if (pageName !== null) {
                            '<img id="clbi-user-avatar" src="' + avatarSrc + '" onerror="this.onerror=null;this.src=\'/index.php?title=특수:Redirect/file/Pfp-default.png\';">' +
        mw.config.set('wgRelevantPageName', pageName);
                        '</div>' +
    }
                        '<div id="clbi-user-name-row" class="profile-name-row">' +
                            '<a href="/index.php/사용자:' + username + '" id="clbi-user-name">' + username + '</a>' +
                        '</div>' +
                    '</div>' +
                    '<div class="clbi-right-content profile-action-box">' +
                        '<div class="profile-quick-actions" aria-label="프로필 빠른 메뉴">' +
                            '<button type="button" class="profile-quick-btn" id="profile-quick-inventory" aria-label="인벤토리"><span class="profile-quick-icon" aria-hidden="true">' + CLBI_SVG_PACKAGE + '</span><span class="profile-quick-tip" aria-hidden="true">인벤토리</span></button>' +
                            '<button type="button" class="profile-quick-btn" id="profile-quick-achievements" aria-label="업적"><span class="profile-quick-icon" aria-hidden="true">' + CLBI_SVG_TROPHY + '</span><span class="profile-quick-tip" aria-hidden="true">업적</span></button>' +
                            '<button type="button" class="profile-quick-btn" id="profile-quick-notifications" aria-label="알림"><span id="profile-quick-notification-icon" class="profile-quick-icon" aria-hidden="true">' + CLBI_SVG_BELL + '</span><span class="profile-quick-tip" aria-hidden="true">알림</span></button>' +
                        '</div>' +
                        '<a href="/index.php/특수:기여/' + username + '" class="clbi-user-btn" id="clbi-btn-contribution"><span class="profile-action-icon" aria-hidden="true">' + CLBI_SVG_SCAN_TEXT + '</span><span class="profile-action-label">기여</span><i class="hn hn-angle-right-solid profile-action-arrow" aria-hidden="true"></i></a>' +
                        '<a href="/index.php/특수:주시문서목록" class="clbi-user-btn" id="clbi-btn-watchlist"><span class="profile-action-icon" aria-hidden="true">' + CLBI_SVG_SCAN_EYE + '</span><span class="profile-action-label">주시문서 목록</span><i class="hn hn-angle-right-solid profile-action-arrow" aria-hidden="true"></i></a>' +
                        '<a href="/index.php/특수:설정" class="clbi-user-btn" id="clbi-btn-preferences"><span class="profile-action-icon" aria-hidden="true">' + CLBI_SVG_SETTINGS + '</span><span class="profile-action-label">설정</span><i class="hn hn-angle-right-solid profile-action-arrow" aria-hidden="true"></i></a>' +
                        '<a href="/index.php?title=특수:로그아웃&returnto=대문" class="clbi-user-btn clbi-user-btn-logout" id="clbi-btn-logout"><span class="profile-action-icon" aria-hidden="true">' + CLBI_SVG_POWER + '</span><span class="profile-action-label">로그아웃</span><i class="hn hn-angle-right-solid profile-action-arrow" aria-hidden="true"></i></a>' +
                    '</div>' +
                '</div>';
        } else {
            userBox =
                '<div class="clbi-right-box profile-card-box">' +
                    '<div id="clbi-user-avatar-wrap" class="profile-identity-panel">' +
                        '<div class="profile-avatar-stage">' +
                            '<img id="clbi-user-avatar" src="/index.php?title=특수:Redirect/file/Pfp-default.png">' +
                        '</div>' +
                        '<div id="clbi-user-name-row" class="profile-name-row profile-name-row-guest">' +
                            '<span id="clbi-user-name">Guest</span>' +
                        '</div>' +
                    '</div>' +
                    '<div class="clbi-right-content profile-action-box">' +
                        '<a href="/index.php?title=특수:로그인&returnto=대문" class="clbi-user-btn" id="clbi-btn-login"><span class="profile-action-icon" aria-hidden="true">' + CLBI_SVG_POWER + '</span><span class="profile-action-label">로그인</span><i class="hn hn-angle-right-solid profile-action-arrow" aria-hidden="true"></i></a>' +
                    '</div>' +
                '</div>';
        }
        var greatWallBox = '';
        var siteInformationBox = '';
 
        try {
            greatWallBox = buildGreatWallBox();
        } catch (err) {
            console.error('The Great Wall build failed:', err);
            greatWallBox = '';
        }


    CLBI_CATLINKS_FETCH_TOKEN += 1;
        try {
}
            siteInformationBox = buildSiteInformationBox();
        } catch (err) {
            console.error('Site information build failed:', err);
            siteInformationBox = '';
        }


// SPA 네비게이션
function shouldSkip(url) {
    return url.match(/action=edit|action=submit|action=history|action=delete|action=protect|action=purge|특수:로그인|특수:로그아웃|Special:UserLogin|Special:UserLogout|특수:사용자정보|특수:비밀번호바꾸기|uselang=/);
}


$(function() {
        var sidebar = userBox + greatWallBox + siteInformationBox;
    if (window._spaInitialized) return;
    window._spaInitialized = true;


    function isInternal(url) {
        $('.content-wrapper').append('<div id="clbi-right-sidebar">' + sidebar + '</div>');
        var a = document.createElement('a');
         initGreatWallBoxWhenReady();
         a.href = url;
         removeMainPortalGuestbookPreview();
         return a.hostname === window.location.hostname;
     }
     }


     function getCachedSpaPageHtml(url) {
     initGreatWallBoxWhenReady();
        if (!window.EntryStore || typeof window.EntryStore.getTextSync !== 'function') return '';
    removeMainPortalGuestbookPreview();
        return window.EntryStore.getTextSync(url) || window.EntryStore.getTextSync(String(url || '').replace(/^https?:\/\/[^/]+/i, '')) || '';
 
    }


     function fetchSpaPageHtml(url) {
     if ($('#clbi-left-sidebar').length === 0) {
        var cached = getCachedSpaPageHtml(url);
var leftBillboardBox = '';
        if (cached) return Promise.resolve(cached);
        return fetch(url, { credentials: 'same-origin', cache: 'force-cache' }).then(function(res) {
            return res.text();
        });
    }


    function prepareDetachedEntryContent(newContent) {
        /*
        Initial boot prepares entry artifacts; SPA is only allowed to consume them.
        This hook runs while the fetched page is still detached, before the user sees it.
        It must stay synchronous or already-resolved: if a subsystem cannot prepare from
        EntryStore immediately, it should leave the old fallback path in place instead of
        opening BootGate during SPA.
        */
         try {
         try {
             if (window.NationsPanel && typeof window.NationsPanel.prepareContentForEntry === 'function') {
             leftBillboardBox = buildRightBillboardBox();
                window.NationsPanel.prepareContentForEntry(newContent);
            }
         } catch (err) {
         } catch (err) {
             console.warn('entry content preparation failed:', err);
             console.error('Left billboard build failed:', err);
            leftBillboardBox = '';
         }
         }
    }


     function loadPage(url) {
var leftSidebar =
         invalidateProfileRender();
     '<div id="clbi-left-sidebar">' +
        '<div class="clbi-left-box clbi-left-lang-box">' +
            '<div class="clbi-left-title">' +
                '<span id="clbi-title-left-language">언어</span>' +
            '</div>' +
            '<div class="clbi-left-content sidebar-lang-box">' +
                '<div id="clbi-sidebar-lang-selector" class="sidebar-lang-selector sidebar-lang-dial" tabindex="0" role="group" aria-label="언어 선택">' +
                    '<div id="clbi-sidebar-lang-dial-stage" class="sidebar-lang-dial-stage">' +
                        '<div id="clbi-sidebar-lang-fan" class="sidebar-lang-fan" aria-hidden="true"></div>' +
                        '<div id="clbi-sidebar-lang-selected-panel" class="sidebar-lang-status-panel sidebar-lang-status-left" aria-hidden="true">' +
                            '<span id="clbi-sidebar-lang-selected-value" class="sidebar-lang-status-value">한국어</span>' +
                        '</div>' +
                        '<div id="clbi-sidebar-lang-availability-panel" class="sidebar-lang-status-panel sidebar-lang-status-right is-current" aria-hidden="true">' +
                            '<span id="clbi-sidebar-lang-availability-value" class="sidebar-lang-status-value">CURRENT</span>' +
                        '</div>' +
                        '<button type="button" id="clbi-sidebar-lang-apply" class="sidebar-lang-apply" aria-label="언어 적용">' +
                            '<span class="sidebar-lang-apply-mark" aria-hidden="true">✓</span>' +
                        '</button>' +
                    '</div>' +
                '</div>' +
            '</div>' +
        '</div>' +
         '<div class="clbi-left-box clbi-left-news-box">' +
            '<div class="clbi-left-title">' +
                '<span id="clbi-title-left-news">뉴스</span>' +
            '</div>' +
            '<div class="clbi-left-content clbi-news-box">' +


        return fetchSpaPageHtml(url)
                '<div class="news-feed-title" id="clbi-left-news-changelog-title">CHANGELOG</div>' +
            .then(function(html) {
                '<div class="news-left-changelog-feed">' +
                var parser = new DOMParser();
                    '<a href="/index.php/체인지로그" class="news-post-item">' +
                 var doc = parser.parseFromString(html, 'text/html');
                        '<div class="news-post-title-wrap">' +
                            '<span class="news-post-title" id="clbi-left-news-changelog-main">체인지로그</span>' +
                        '</div>' +
                        '<span class="news-post-jump" aria-hidden="true">›</span>' +
                    '</a>' +
                 '</div>' +


                 var scripts = doc.querySelectorAll('script');
                 '<div class="news-divider"></div>' +
                for (var i = 0; i < scripts.length; i++) {
                    var src = scripts[i].textContent;


                    if (src.indexOf('wgNamespaceNumber') !== -1) {
                '<div class="news-feed-title" id="clbi-left-news-recent-title">RECENT CHANGES</div>' +
                        var match = src.match(/"wgNamespaceNumber":(-?\d+)/);
                '<div class="news-left-recent-feed" id="clbi-left-recent-list">불러오는 중...</div>' +
                        if (match) mw.config.set('wgNamespaceNumber', parseInt(match[1], 10));


                        var matchTitle = src.match(/"wgTitle":"([^"]+)"/);
                '<a class="news-fill-image-slot" id="clbi-left-news-fill-image" href="/index.php/특수:최근바뀜" aria-label="최근 바뀜으로 이동">' +
                        if (matchTitle) mw.config.set('wgTitle', matchTitle[1]);
                    '<div class="news-fill-image-frame">' +
                        '<span class="news-fill-image" style="--news-fill-image-url:url(\'/index.php?title=특수:Redirect/file/Side-news-fill-001.png\');" aria-hidden="true"></span><img class="news-fill-image-probe" src="/index.php?title=특수:Redirect/file/Side-news-fill-001.png" alt="" aria-hidden="true" onerror="this.onerror=null;this.closest(\'.news-fill-image-slot\').classList.add(\'is-empty\');this.remove();">' +
                    '</div>' +
                '</a>' +


                        var matchPage = src.match(/"wgPageName":"([^"]+)"/);
            '</div>' +
                        if (matchPage) mw.config.set('wgPageName', matchPage[1]);
        '</div>' +
        leftBillboardBox +
    '</div>';


                        var matchArticle = src.match(/"wgArticleId":(\d+)/);
        $('.content-wrapper').prepend(leftSidebar);
                        if (matchArticle) {
                            mw.config.set('wgArticleId', parseInt(matchArticle[1], 10));
                        } else {
                            mw.config.set('wgArticleId', 0);
                        }


                        var matchIsMainPage = src.match(/"wgIsMainPage":(true|false)/);
        renderSidebarLanguageBox();
                        if (matchIsMainPage) {
        loadRecentChangesList('#clbi-left-recent-list', 10);
                            mw.config.set('wgIsMainPage', matchIsMainPage[1] === 'true');
        scheduleAdaptiveLeftRecentItems();
                        } else {
        scheduleLeftBillboardAdaptive();
                            mw.config.set('wgIsMainPage', false);
        scheduleClbiContentBottomGap();
                        }
        updateLeftSidebarNationsImage();
    }


                        var matchSpecial = src.match(/"wgCanonicalSpecialPageName":"([^"]+)"/);
    try {
                        if (matchSpecial) {
        initRightBillboardCarousel();
                            mw.config.set('wgCanonicalSpecialPageName', matchSpecial[1]);
    } catch (err) {
                        } else {
        console.error('Right billboard carousel failed:', err);
                            mw.config.set('wgCanonicalSpecialPageName', false);
    }
                        }
                        break;
                    }
                }


                syncCatlinksConfigFromSpaDocument(doc);
    if (typeof window.normalizeClbiShellDomOrder === 'function') window.normalizeClbiShellDomOrder();
    applyMainPageStyle();
    initClbiCustomDocumentScrollbars();
    initCategoryNavIfAvailable(document);


                var newContent = doc.querySelector('.liberty-content-main');
    if (window.ProgressSystemWebUi && typeof window.ProgressSystemWebUi.boot === 'function') {
                var newTitle = doc.querySelector('.mw-page-title-main');
        window.ProgressSystemWebUi.boot('initSidebars');
                var newHead = doc.querySelector('title');
    }
                var newHeader = doc.querySelector('.liberty-content-header');


                if (newContent) {
    $('#side-toc-box').remove();
                    prepareDetachedEntryContent(newContent);
                    prepareSpaCatlinksBeforeInsert(newContent);
                    $('#side-toc-box').remove();
                    $('.profile-card').remove();
                    $('.user-profile-portal').removeClass('user-profile-portal');
                    $('.liberty-content-main').html(newContent.innerHTML);
                    $('.profile-card').remove();
                    try {
                        if (window.Decorations && typeof window.Decorations.renderPrepared === 'function') window.Decorations.renderPrepared();
                        else if (window.CLBI_DECORATIONS && typeof window.CLBI_DECORATIONS.renderPrepared === 'function') window.CLBI_DECORATIONS.renderPrepared();
                    } catch (err) {}
                    $('body').removeClass('page-loading');
                }


                if (newTitle) {
    mw.loader.using(['mediawiki.api']).then(function() {
                    $('.mw-page-title-main').html(newTitle.innerHTML);
        setTimeout(function() {
                }
            initNotifications();
            initProfile();
            moveTocToLeftSidebar();
        }, 300);


                if (newHead) {
        setTimeout(moveTocToLeftSidebar, 800);
                    document.title = newHead.textContent;
        setTimeout(moveTocToLeftSidebar, 1500);
                }
    });
}


                if (newHeader) {
$(function() {
                    $('.liberty-content-header').html(newHeader.innerHTML);
    loadLangScript(function() {
                }
        setTimeout(function() {
            initSidebars();
        }, 100);
    });
});


                if (typeof window.normalizeClbiShellDomOrder === 'function') window.normalizeClbiShellDomOrder();
$(document).on('click.profileQuickPlaceholder', '#profile-quick-inventory, #profile-quick-achievements', function(e) {
                window.scrollTo(0, 0);
    e.preventDefault();
                mw.hook('wikipage.content').fire($('.liberty-content-main'));
    e.stopPropagation();
                applyMainPageStyle();
});
                initClbiCustomDocumentScrollbars();
                initCategoryNavIfAvailable(document);


                if (window.ProgressSystemWebUi && typeof window.ProgressSystemWebUi.handleSpaPageView === 'function') {
function extractJsonArrayAfterMwConfigKey(text, key) {
                    window.ProgressSystemWebUi.handleSpaPageView();
    var needle = '"' + key + '"';
                } else if (window.ProgressSystemWebUi && typeof window.ProgressSystemWebUi.boot === 'function') {
    var keyIndex = String(text || '').indexOf(needle);
                    window.ProgressSystemWebUi.boot('spa');
    var start;
                }
    var i;
    var depth = 0;
    var inString = false;
    var escaped = false;


                $('#side-toc-box').remove();
    if (keyIndex === -1) return null;
                setTimeout(moveTocToLeftSidebar, 100);
                setTimeout(moveTocToLeftSidebar, 500);
                setTimeout(moveTocToLeftSidebar, 1200);


                mw.loader.using(['mediawiki.api']).then(function() {
    start = String(text || '').indexOf('[', keyIndex + needle.length);
                    initProfile();
    if (start === -1) return null;
                    moveTocToLeftSidebar();
                });
            })
            .catch(function (err) {
                console.error('SPA page load failed:', err);
                $('body').removeClass('page-loading');
            });
    }


// 목차 링크는 전용 처리
    for (i = start; i < text.length; i += 1) {
$(document).on('click', '#side-toc-box a, #toc a, .toc a', function(e) {
        var ch = text.charAt(i);
    var href = $(this).attr('href');
    if (!href || href.charAt(0) !== '#') return;


    var rawId = href.slice(1);
        if (inString) {
    if (!rawId) return;
            if (escaped) {
                escaped = false;
            } else if (ch === '\\') {
                escaped = true;
            } else if (ch === '"') {
                inString = false;
            }
            continue;
        }


    var decodedId = rawId;
        if (ch === '"') {
            inString = true;
            continue;
        }


    try {
        if (ch === '[') depth += 1;
        decodedId = decodeURIComponent(rawId);
        if (ch === ']') {
    } catch (err) {
            depth -= 1;
         decodedId = rawId;
            if (depth === 0) {
                try {
                    return JSON.parse(text.slice(start, i + 1));
                } catch (err) {
                    return null;
                }
            }
         }
     }
     }


     var target = document.getElementById(decodedId);
     return null;
}


    if (!target && window.CSS && CSS.escape) {
function extractJsonStringAfterMwConfigKey(text, key) {
        target = document.querySelector('#' + CSS.escape(decodedId));
    var needle = '"' + key + '"';
     }
    var keyIndex = String(text || '').indexOf(needle);
     var colon;
    var start;
    var i;
    var escaped = false;


     if (!target) return;
     if (keyIndex === -1) return null;


     e.preventDefault();
     colon = text.indexOf(':', keyIndex + needle.length);
     e.stopPropagation();
     if (colon === -1) return null;


     var scrollTarget = target.closest('h2, h3') || target;
     start = text.indexOf('"', colon + 1);
    if (start === -1) return null;


     scrollTarget.scrollIntoView({
     for (i = start + 1; i < text.length; i += 1) {
         behavior: 'auto',
         var ch = text.charAt(i);
        block: 'start'
    });


    history.replaceState(null, '', '#' + rawId);
        if (escaped) {
});
            escaped = false;
            continue;
        }


    $(document).on('click', 'a', function(e) {
        if (ch === '\\') {
        // 휠 클릭, 새 탭 열기, 보조키 이동은 브라우저 기본 동작을 유지한다.
            escaped = true;
        if (e.which && e.which !== 1) return;
            continue;
        if (e.button && e.button !== 0) return;
         }
         if (e.ctrlKey || e.metaKey || e.shiftKey || e.altKey) return;


         var href = $(this).attr('href');
         if (ch === '"') {
        if (!href) return;
            try {
                return JSON.parse(text.slice(start, i + 1));
            } catch (err) {
                return text.slice(start + 1, i);
            }
        }
    }


        // 목차 링크는 별도 핸들러에서 처리
    return null;
        if ($(this).closest('#side-toc-box, #toc, .toc').length) return;
}


        // 단순 해시 링크는 SPA 가로채기 제외
function syncCatlinksConfigFromSpaDocument(doc) {
        if (href.startsWith('#')) return;
    var scripts = doc ? doc.querySelectorAll('script') : [];
    var categories = null;
    var hiddenCategories = null;
    var relevantPageName = null;
    var pageName = null;
    var i;
    var text;
    var value;


        var link = document.createElement('a');
    for (i = 0; i < scripts.length; i += 1) {
         link.href = href;
         text = scripts[i].textContent || '';


         var samePath = decodeURIComponent(link.pathname) === decodeURIComponent(window.location.pathname);
         if (categories === null) {
        var sameSearch = (link.search || '') === (window.location.search || '');
            value = extractJsonArrayAfterMwConfigKey(text, 'wgCategories');
            if (Array.isArray(value)) categories = value;
        }


         if (link.hash && samePath && sameSearch) return;
         if (hiddenCategories === null) {
            value = extractJsonArrayAfterMwConfigKey(text, 'wgHiddenCategories');
            if (Array.isArray(value)) hiddenCategories = value;
        }


         var currentBase = window.location.href.split('#')[0];
         if (relevantPageName === null) {
        var targetBase = link.href.split('#')[0];
            value = extractJsonStringAfterMwConfigKey(text, 'wgRelevantPageName');
            if (value !== null) relevantPageName = value;
        }


         if (link.hash && currentBase === targetBase) return;
         if (pageName === null) {
 
             value = extractJsonStringAfterMwConfigKey(text, 'wgPageName');
        if (!isInternal(href)) return;
             if (value !== null) pageName = value;
        if (shouldSkip(href)) return;
 
        e.preventDefault();
        playStaticSound();
        /*
        SPA must remain a consumer phase.  If the target page HTML was prepared by
        the first-load boot pack, do not show the legacy page-loading veil.  The
        route will consume cached HTML and detached entry artifacts before insertion.
        */
        if (getCachedSpaPageHtml(href)) {
             $('body').removeClass('page-loading');
        } else {
             $('body').addClass('page-loading');
         }
         }
        history.pushState(null, '', href);
     }
        loadPage(href);
     });


     window.addEventListener('popstate', function() {
     mw.config.set('wgCategories', Array.isArray(categories) ? categories : []);
        loadPage(window.location.href);
     mw.config.set('wgHiddenCategories', Array.isArray(hiddenCategories) ? hiddenCategories : []);
     });
});


    if (relevantPageName !== null) {
        mw.config.set('wgRelevantPageName', relevantPageName);
    } else if (pageName !== null) {
        mw.config.set('wgRelevantPageName', pageName);
    }


    CLBI_CATLINKS_FETCH_TOKEN += 1;
}


/* ========== CLBI Custom Document Scrollbar ========== */
// SPA 네비게이션
function isGeneralDocumentView() {
function shouldSkip(url) {
     var body = document.body;
     return url.match(/action=edit|action=submit|action=history|action=delete|action=protect|action=purge|특수:로그인|특수:로그아웃|Special:UserLogin|Special:UserLogout|특수:사용자정보|특수:비밀번호바꾸기|uselang=/);
    if (!body) return false;
 
    return body.classList.contains('action-view') &&
        !body.classList.contains('clbi-main-page') &&
        !body.classList.contains('clbi-system-doc-page') &&
        !body.classList.contains('backend-system-page') &&
        !body.classList.contains('user-profile-page') &&
        !body.classList.contains('user-profile-settings-page');
}
}


function getClbiDocumentScrollTargets() {
$(function() {
     if (!isGeneralDocumentView()) return [];
     if (window._spaInitialized) return;
    window._spaInitialized = true;


     return Array.prototype.slice.call(document.querySelectorAll(
     function isInternal(url) {
        '.liberty-content-main > #mw-content-text .mw-parser-output, ' +
        var a = document.createElement('a');
         '.liberty-content-main > .mw-body-content .mw-parser-output'
         a.href = url;
    )).filter(function (el, index, list) {
         return a.hostname === window.location.hostname;
         return el && list.indexOf(el) === index;
     }
     });
}


function getClbiOuterWellForScroll(scrollEl) {
    function getCachedSpaPageHtml(url) {
    var main = scrollEl ? scrollEl.closest('.liberty-content-main') : null;
        if (!window.EntryStore || typeof window.EntryStore.getTextSync !== 'function') return '';
    var children;
        return window.EntryStore.getTextSync(url) || window.EntryStore.getTextSync(String(url || '').replace(/^https?:\/\/[^/]+/i, '')) || '';
    var i;
     }
     var child;


     if (!main) return null;
     function fetchSpaPageHtml(url) {
        var cached = getCachedSpaPageHtml(url);
        if (cached) return Promise.resolve(cached);
        return fetch(url, { credentials: 'same-origin', cache: 'force-cache' }).then(function(res) {
            return res.text();
        });
    }


     children = Array.prototype.slice.call(main.children || []);
     function prepareDetachedEntryContent(newContent) {
    for (i = 0; i < children.length; i += 1) {
        /*
         child = children[i];
        Initial boot prepares entry artifacts; SPA is only allowed to consume them.
         if (
         This hook runs while the fetched page is still detached, before the user sees it.
             child &&
         It must stay synchronous or already-resolved: if a subsystem cannot prepare from
            (child.id === 'mw-content-text' || child.classList.contains('mw-body-content')) &&
        EntryStore immediately, it should leave the old fallback path in place instead of
             child.contains(scrollEl)
        opening BootGate during SPA.
         ) {
        */
             return child;
        try {
             if (window.NationsPanel && typeof window.NationsPanel.prepareContentForEntry === 'function') {
                window.NationsPanel.prepareContentForEntry(newContent);
             }
         } catch (err) {
             console.warn('entry content preparation failed:', err);
         }
         }
     }
     }


     return scrollEl.parentElement || null;
     function loadPage(url) {
}
        invalidateProfileRender();
 
        return fetchSpaPageHtml(url)
            .then(function(html) {
                var parser = new DOMParser();
                var doc = parser.parseFromString(html, 'text/html');


function buildClbiCustomScrollbar(well, scrollEl) {
                var scripts = doc.querySelectorAll('script');
    var bar = well.querySelector(':scope > .clbi-custom-scrollbar');
                for (var i = 0; i < scripts.length; i++) {
    var up;
                    var src = scripts[i].textContent;
    var track;
    var thumb;
    var down;


    if (!bar) {
                    if (src.indexOf('wgNamespaceNumber') !== -1) {
        bar = document.createElement('div');
                        var match = src.match(/"wgNamespaceNumber":(-?\d+)/);
        bar.className = 'clbi-custom-scrollbar';
                        if (match) mw.config.set('wgNamespaceNumber', parseInt(match[1], 10));
        bar.setAttribute('aria-hidden', 'true');
        bar.innerHTML =
            '<div class="clbi-custom-scroll-arrow clbi-custom-scroll-arrow-up" data-scroll-arrow="up"></div>' +
            '<div class="clbi-custom-scroll-track"><div class="clbi-custom-scroll-thumb"></div></div>' +
            '<div class="clbi-custom-scroll-arrow clbi-custom-scroll-arrow-down" data-scroll-arrow="down"></div>';
        well.appendChild(bar);
    }


    bar.__clbiScrollTarget = scrollEl;
                        var matchTitle = src.match(/"wgTitle":"([^"]+)"/);
    up = bar.querySelector('.clbi-custom-scroll-arrow-up');
                        if (matchTitle) mw.config.set('wgTitle', matchTitle[1]);
    track = bar.querySelector('.clbi-custom-scroll-track');
    thumb = bar.querySelector('.clbi-custom-scroll-thumb');
    down = bar.querySelector('.clbi-custom-scroll-arrow-down');


    if (up && !up.__clbiBound) {
                        var matchPage = src.match(/"wgPageName":"([^"]+)"/);
        up.__clbiBound = true;
                        if (matchPage) mw.config.set('wgPageName', matchPage[1]);
        up.addEventListener('mousedown', function (e) {
            e.preventDefault();
            e.stopPropagation();
            if (bar.__clbiScrollTarget) bar.__clbiScrollTarget.scrollTop -= 48;
            updateClbiCustomScrollbar(bar);
        });
    }


    if (down && !down.__clbiBound) {
                        var matchArticle = src.match(/"wgArticleId":(\d+)/);
        down.__clbiBound = true;
                        if (matchArticle) {
        down.addEventListener('mousedown', function (e) {
                            mw.config.set('wgArticleId', parseInt(matchArticle[1], 10));
            e.preventDefault();
                        } else {
            e.stopPropagation();
                            mw.config.set('wgArticleId', 0);
            if (bar.__clbiScrollTarget) bar.__clbiScrollTarget.scrollTop += 48;
                        }
            updateClbiCustomScrollbar(bar);
        });
    }


    if (track && !track.__clbiBound) {
                        var matchIsMainPage = src.match(/"wgIsMainPage":(true|false)/);
        track.__clbiBound = true;
                        if (matchIsMainPage) {
        track.addEventListener('mousedown', function (e) {
                            mw.config.set('wgIsMainPage', matchIsMainPage[1] === 'true');
            var rect;
                        } else {
            var thumbRect;
                            mw.config.set('wgIsMainPage', false);
            var target;
                        }
            var direction;


            if (e.target === thumb) return;
                        var matchSpecial = src.match(/"wgCanonicalSpecialPageName":"([^"]+)"/);
            e.preventDefault();
                        if (matchSpecial) {
            e.stopPropagation();
                            mw.config.set('wgCanonicalSpecialPageName', matchSpecial[1]);
                        } else {
                            mw.config.set('wgCanonicalSpecialPageName', false);
                        }
                        break;
                    }
                }


            target = bar.__clbiScrollTarget;
                syncCatlinksConfigFromSpaDocument(doc);
            if (!target) return;


            rect = track.getBoundingClientRect();
                var newContent = doc.querySelector('.liberty-content-main');
            thumbRect = thumb.getBoundingClientRect();
                var newTitle = doc.querySelector('.mw-page-title-main');
            direction = e.clientY < thumbRect.top ? -1 : 1;
                var newHead = doc.querySelector('title');
            target.scrollTop += direction * Math.max(60, Math.floor(target.clientHeight * 0.82));
                var newHeader = doc.querySelector('.liberty-content-header');
            updateClbiCustomScrollbar(bar);
        });
    }


    if (thumb && !thumb.__clbiBound) {
                if (newContent) {
        thumb.__clbiBound = true;
                    prepareDetachedEntryContent(newContent);
        thumb.addEventListener('mousedown', function (e) {
                    prepareSpaCatlinksBeforeInsert(newContent);
            var target = bar.__clbiScrollTarget;
                    $('#side-toc-box').remove();
            var startY;
                    $('.profile-card').remove();
            var startScroll;
                    $('.user-profile-portal').removeClass('user-profile-portal');
            var maxScroll;
                    $('.liberty-content-main').html(newContent.innerHTML);
            var maxThumbTop;
                    $('.profile-card').remove();
            var trackHeight;
                    try {
            var thumbHeight;
                        if (window.Decorations && typeof window.Decorations.renderPrepared === 'function') window.Decorations.renderPrepared();
                        else if (window.CLBI_DECORATIONS && typeof window.CLBI_DECORATIONS.renderPrepared === 'function') window.CLBI_DECORATIONS.renderPrepared();
                    } catch (err) {}
                    $('body').removeClass('page-loading');
                }


            if (!target) return;
                if (newTitle) {
                    $('.mw-page-title-main').html(newTitle.innerHTML);
                }


            e.preventDefault();
                if (newHead) {
            e.stopPropagation();
                    document.title = newHead.textContent;
                }


            startY = e.clientY;
                if (newHeader) {
            startScroll = target.scrollTop;
                    $('.liberty-content-header').html(newHeader.innerHTML);
            maxScroll = Math.max(1, target.scrollHeight - target.clientHeight);
                }
            trackHeight = track ? track.clientHeight : 0;
            thumbHeight = thumb.offsetHeight || 0;
            maxThumbTop = Math.max(1, trackHeight - thumbHeight);


            bar.classList.add('is-dragging');
                if (typeof window.normalizeClbiShellDomOrder === 'function') window.normalizeClbiShellDomOrder();
 
                window.scrollTo(0, 0);
            function onMove(moveEvent) {
                 mw.hook('wikipage.content').fire($('.liberty-content-main'));
                 var dy = moveEvent.clientY - startY;
                 applyMainPageStyle();
                 target.scrollTop = startScroll + (dy / maxThumbTop) * maxScroll;
                 initClbiCustomDocumentScrollbars();
                 updateClbiCustomScrollbar(bar);
                 initCategoryNavIfAvailable(document);
                 moveEvent.preventDefault();
            }


            function onUp() {
                if (window.ProgressSystemWebUi && typeof window.ProgressSystemWebUi.handleSpaPageView === 'function') {
                bar.classList.remove('is-dragging');
                    window.ProgressSystemWebUi.handleSpaPageView();
                 document.removeEventListener('mousemove', onMove);
                 } else if (window.ProgressSystemWebUi && typeof window.ProgressSystemWebUi.boot === 'function') {
                document.removeEventListener('mouseup', onUp);
                    window.ProgressSystemWebUi.boot('spa');
            }
                }


            document.addEventListener('mousemove', onMove);
                $('#side-toc-box').remove();
            document.addEventListener('mouseup', onUp);
                setTimeout(moveTocToLeftSidebar, 100);
        });
                setTimeout(moveTocToLeftSidebar, 500);
    }
                setTimeout(moveTocToLeftSidebar, 1200);


    if (!scrollEl.__clbiCustomScrollbarBound) {
                mw.loader.using(['mediawiki.api']).then(function() {
        scrollEl.__clbiCustomScrollbarBound = true;
                    initProfile();
        scrollEl.addEventListener('scroll', function () {
                    moveTocToLeftSidebar();
            if (scrollEl.__clbiCustomScrollbar) {
                });
                 updateClbiCustomScrollbar(scrollEl.__clbiCustomScrollbar);
            })
             }
            .catch(function (err) {
        }, { passive: true });
                console.error('SPA page load failed:', err);
                 $('body').removeClass('page-loading');
             });
     }
     }


     scrollEl.__clbiCustomScrollbar = bar;
// 목차 링크는 전용 처리
     updateClbiCustomScrollbar(bar);
$(document).on('click', '#side-toc-box a, #toc a, .toc a', function(e) {
     var href = $(this).attr('href');
     if (!href || href.charAt(0) !== '#') return;


     return bar;
     var rawId = href.slice(1);
}
    if (!rawId) return;


function updateClbiCustomScrollbar(bar) {
     var decodedId = rawId;
     var scrollEl = bar && bar.__clbiScrollTarget;
    var track = bar ? bar.querySelector('.clbi-custom-scroll-track') : null;
    var thumb = bar ? bar.querySelector('.clbi-custom-scroll-thumb') : null;
    var maxScroll;
    var trackHeight;
    var thumbHeight;
    var maxTop;
    var top;


     if (!bar || !scrollEl || !track || !thumb) return;
     try {
        decodedId = decodeURIComponent(rawId);
    } catch (err) {
        decodedId = rawId;
    }
 
    var target = document.getElementById(decodedId);


     maxScroll = scrollEl.scrollHeight - scrollEl.clientHeight;
     if (!target && window.CSS && CSS.escape) {
    if (maxScroll <= 1) {
         target = document.querySelector('#' + CSS.escape(decodedId));
         bar.classList.add('is-hidden');
        return;
     }
     }


     bar.classList.remove('is-hidden');
     if (!target) return;


     trackHeight = Math.max(1, track.clientHeight || 1);
     e.preventDefault();
    thumbHeight = Math.max(12, Math.floor((scrollEl.clientHeight / Math.max(scrollEl.scrollHeight, 1)) * trackHeight));
     e.stopPropagation();
    thumbHeight = Math.min(trackHeight, thumbHeight);
    maxTop = Math.max(0, trackHeight - thumbHeight);
     top = maxScroll > 0 ? Math.round((scrollEl.scrollTop / maxScroll) * maxTop) : 0;


     thumb.style.height = thumbHeight + 'px';
     var scrollTarget = target.closest('h2, h3') || target;
    thumb.style.transform = 'translateY(' + top + 'px)';
}


function initClbiCustomDocumentScrollbars() {
    scrollTarget.scrollIntoView({
    var existing = Array.prototype.slice.call(document.querySelectorAll('.clbi-custom-scrollbar'));
        behavior: 'auto',
     var targets = getClbiDocumentScrollTargets();
        block: 'start'
    var liveBars = [];
     });


     if (!targets.length) {
     history.replaceState(null, '', '#' + rawId);
        existing.forEach(function (bar) { bar.remove(); });
});
        return;
    }


     targets.forEach(function (scrollEl) {
     /* 길라잡이는 문서 링크가 아니라 대문 본문 화면 탭이다. */
         var well = getClbiOuterWellForScroll(scrollEl);
    $(document).on('click', '.portal-guide-anchor[data-category-key="guide"]', function(e) {
         var bar;
         if (e.which && e.which !== 1) return;
        if (e.button && e.button !== 0) return;
         if (e.ctrlKey || e.metaKey || e.shiftKey || e.altKey) return;


         if (!well) return;
         if (window.BottomGuideNav && typeof window.BottomGuideNav.toggle === 'function') {
        bar = buildClbiCustomScrollbar(well, scrollEl);
            e.preventDefault();
        liveBars.push(bar);
            e.stopImmediatePropagation();
            window.BottomGuideNav.toggle();
        }
     });
     });


     existing.forEach(function (bar) {
     $(document).on('click', 'a', function(e) {
         if (liveBars.indexOf(bar) === -1) bar.remove();
        // 휠 클릭, 새 탭 열기, 보조키 이동은 브라우저 기본 동작을 유지한다.
    });
         if (e.which && e.which !== 1) return;
        if (e.button && e.button !== 0) return;
        if (e.ctrlKey || e.metaKey || e.shiftKey || e.altKey) return;


    window.requestAnimationFrame(function () {
        /* 길라잡이 화면 탭은 위 전용 처리 외에는 SPA 문서 이동 대상으로 삼지 않는다. */
        liveBars.forEach(updateClbiCustomScrollbar);
        if ($(this).is('.portal-guide-anchor[data-category-key="guide"]')) return;
    });


    setTimeout(function () {
        var href = $(this).attr('href');
        liveBars.forEach(updateClbiCustomScrollbar);
        if (!href) return;
    }, 120);
}


if (!window.__clbiCustomScrollbarResizeBound) {
        // 목차 링크는 별도 핸들러에서 처리
    window.__clbiCustomScrollbarResizeBound = true;
        if ($(this).closest('#side-toc-box, #toc, .toc').length) return;
    window.addEventListener('resize', function () {
        setTimeout(initClbiCustomDocumentScrollbars, 60);
    });
}


        // 단순 해시 링크는 SPA 가로채기 제외
        if (href.startsWith('#')) return;


// 시간 계산 함수
        var link = document.createElement('a');
function timeAgo(timestamp) {
        link.href = href;
    var now = new Date();
    var date = new Date(timestamp);
    var diff = Math.floor((now - date) / 1000);


    if (diff < 60) return diff + '초 전';
        var samePath = decodeURIComponent(link.pathname) === decodeURIComponent(window.location.pathname);
    if (diff < 3600) return Math.floor(diff / 60) + '분 전';
        var sameSearch = (link.search || '') === (window.location.search || '');
    if (diff < 86400) return Math.floor(diff / 3600) + '시간 전';
    return Math.floor(diff / 86400) + '일 전';
}


// 펼접 토글
        if (link.hash && samePath && sameSearch) return;
// 펼접 토글
function getFoldTexts() {
    var lang = getCurrentLang();
    return (window.LANG && window.LANG[lang])
        ? window.LANG[lang]
        : (window.LANG ? window.LANG.ko : { expand: '펼치기', collapse: '접기' });
}


function refreshOpenAncestors($start) {
        var currentBase = window.location.href.split('#')[0];
    $start.parents('[id^="collapsible"]').each(function () {
         var targetBase = link.href.split('#')[0];
         var $parent = $(this);
        if (!$parent.hasClass('folding-open')) return;


        // 이미 fully open 상태면 굳이 다시 잠그지 않음
         if (link.hash && currentBase === targetBase) return;
         if ($parent.data('fold-state') === 'open') {
            return;
        }


         $parent.css('max-height', this.scrollHeight + 'px');
         if (!isInternal(href)) return;
    });
        if (shouldSkip(href)) return;
}


function bindInnerResizeUpdates($target) {
        e.preventDefault();
    // 이미지 늦게 로드될 때 높이 갱신
        playStaticSound();
    $target.find('img').off('.foldimg').on('load.foldimg', function () {
        /*
         if ($target.hasClass('folding-open')) {
        SPA must remain a consumer phase.  If the target page HTML was prepared by
             if ($target.data('fold-state') !== 'open') {
        the first-load boot pack, do not show the legacy page-loading veil. The
                $target.css('max-height', $target[0].scrollHeight + 'px');
        route will consume cached HTML and detached entry artifacts before insertion.
            }
        */
            refreshOpenAncestors($target);
         if (getCachedSpaPageHtml(href)) {
             $('body').removeClass('page-loading');
        } else {
            $('body').addClass('page-loading');
         }
         }
        history.pushState(null, '', href);
        loadPage(href);
     });
     });
}


function openFold($target, $btn) {
    window.addEventListener('popstate', function() {
     var t = getFoldTexts();
        loadPage(window.location.href);
     });
});


    $target.data('fold-state', 'opening');
    $target.addClass('folding-open');


    // 열린 뒤 자연 확장 가능하게 만들기 위해 먼저 px로 열기
    $target.css('max-height', '0px');
    $target[0].offsetHeight;
    $target.css('max-height', $target[0].scrollHeight + 'px');


     $btn.text(t.collapse);
/* ========== CLBI Custom Document Scrollbar ========== */
function isGeneralDocumentView() {
     var body = document.body;
    if (!body) return false;


     bindInnerResizeUpdates($target);
     return body.classList.contains('action-view') &&
        !body.classList.contains('clbi-main-page') &&
        !body.classList.contains('clbi-system-doc-page') &&
        !body.classList.contains('backend-system-page') &&
        !body.classList.contains('user-profile-page') &&
        !body.classList.contains('user-profile-settings-page');
}


    // 바깥 펼접 즉시 갱신
function getClbiDocumentScrollTargets() {
     refreshOpenAncestors($target);
     if (!isGeneralDocumentView()) return [];


     // 전환 끝나면 none으로 풀어서 중첩 펼접/동적 내용 증가를 자연스럽게 허용
     return Array.prototype.slice.call(document.querySelectorAll(
    $target.off('transitionend.foldopen').on('transitionend.foldopen', function (e) {
        '.liberty-content-main > #mw-content-text .mw-parser-output, ' +
         if (e.target !== this) return;
        '.liberty-content-main > .mw-body-content .mw-parser-output'
        if (!$target.hasClass('folding-open')) return;
    )).filter(function (el, index, list) {
         return el && list.indexOf(el) === index;
    });
}


        $target.css('max-height', 'none');
function getClbiOuterWellForScroll(scrollEl) {
        $target.data('fold-state', 'open');
    var main = scrollEl ? scrollEl.closest('.liberty-content-main') : null;
    var children;
    var i;
    var child;


        refreshOpenAncestors($target);
    if (!main) return null;
    });


     // 늦게 렌더되는 콘텐츠 대응
     children = Array.prototype.slice.call(main.children || []);
     requestAnimationFrame(function () {
     for (i = 0; i < children.length; i += 1) {
         if ($target.hasClass('folding-open') && $target.data('fold-state') !== 'open') {
        child = children[i];
             $target.css('max-height', $target[0].scrollHeight + 'px');
         if (
             refreshOpenAncestors($target);
            child &&
            (child.id === 'mw-content-text' || child.classList.contains('mw-body-content')) &&
             child.contains(scrollEl)
        ) {
             return child;
         }
         }
     });
     }


     setTimeout(function () {
     return scrollEl.parentElement || null;
        if ($target.hasClass('folding-open') && $target.data('fold-state') !== 'open') {
            $target.css('max-height', $target[0].scrollHeight + 'px');
            refreshOpenAncestors($target);
        }
    }, 80);
 
    setTimeout(function () {
        if ($target.hasClass('folding-open') && $target.data('fold-state') !== 'open') {
            $target.css('max-height', $target[0].scrollHeight + 'px');
            refreshOpenAncestors($target);
        }
    }, 220);
}
}


function closeFold($target, $btn) {
function buildClbiCustomScrollbar(well, scrollEl) {
     var t = getFoldTexts();
     var bar = well.querySelector(':scope > .clbi-custom-scrollbar');
    var up;
    var track;
    var thumb;
    var down;


    // none 상태에서 닫으면 transition이 안 되므로 실제 높이로 고정
     if (!bar) {
     if ($target.css('max-height') === 'none' || $target.data('fold-state') === 'open') {
        bar = document.createElement('div');
         $target.css('max-height', $target[0].scrollHeight + 'px');
        bar.className = 'clbi-custom-scrollbar';
    } else {
         bar.setAttribute('aria-hidden', 'true');
         $target.css('max-height', $target[0].scrollHeight + 'px');
         bar.innerHTML =
            '<div class="clbi-custom-scroll-arrow clbi-custom-scroll-arrow-up" data-scroll-arrow="up"></div>' +
            '<div class="clbi-custom-scroll-track"><div class="clbi-custom-scroll-thumb"></div></div>' +
            '<div class="clbi-custom-scroll-arrow clbi-custom-scroll-arrow-down" data-scroll-arrow="down"></div>';
        well.appendChild(bar);
     }
     }


     $target.data('fold-state', 'closing');
     bar.__clbiScrollTarget = scrollEl;
     $target[0].offsetHeight;
    up = bar.querySelector('.clbi-custom-scroll-arrow-up');
     $target.css('max-height', '0px');
     track = bar.querySelector('.clbi-custom-scroll-track');
     $target.removeClass('folding-open');
     thumb = bar.querySelector('.clbi-custom-scroll-thumb');
     down = bar.querySelector('.clbi-custom-scroll-arrow-down');


     $btn.text(t.expand);
     if (up && !up.__clbiBound) {
        up.__clbiBound = true;
        up.addEventListener('mousedown', function (e) {
            e.preventDefault();
            e.stopPropagation();
            if (bar.__clbiScrollTarget) bar.__clbiScrollTarget.scrollTop -= 48;
            updateClbiCustomScrollbar(bar);
        });
    }


     refreshOpenAncestors($target);
     if (down && !down.__clbiBound) {
        down.__clbiBound = true;
        down.addEventListener('mousedown', function (e) {
            e.preventDefault();
            e.stopPropagation();
            if (bar.__clbiScrollTarget) bar.__clbiScrollTarget.scrollTop += 48;
            updateClbiCustomScrollbar(bar);
        });
    }


     setTimeout(function () {
     if (track && !track.__clbiBound) {
         refreshOpenAncestors($target);
         track.__clbiBound = true;
         $target.data('fold-state', 'closed');
         track.addEventListener('mousedown', function (e) {
    }, 250);
            var rect;
}
            var thumbRect;
            var target;
            var direction;


$(function () {
            if (e.target === thumb) return;
    $(document)
             e.preventDefault();
        .off('click.clbiToggle')
             e.stopPropagation();
        .on('click.clbiToggle', '.toggleBtn', function () {
            var $btn = $(this);
             var targetId = $btn.data('target');
             var $target = $('#' + targetId);
            if (!$target.length) return;


             var scrollY = window.scrollY;
             target = bar.__clbiScrollTarget;
            if (!target) return;


             if ($target.hasClass('folding-open')) {
             rect = track.getBoundingClientRect();
                closeFold($target, $btn);
            thumbRect = thumb.getBoundingClientRect();
             } else {
             direction = e.clientY < thumbRect.top ? -1 : 1;
                openFold($target, $btn);
            target.scrollTop += direction * Math.max(60, Math.floor(target.clientHeight * 0.82));
             }
             updateClbiCustomScrollbar(bar);
 
            window.scrollTo(0, scrollY);
         });
         });
});
    }


// ========== 프로필 시스템 ==========
    if (thumb && !thumb.__clbiBound) {
function initProfile() {
        thumb.__clbiBound = true;
    $('.profile-card').remove();
        thumb.addEventListener('mousedown', function (e) {
    $('.user-profile-portal').removeClass('user-profile-portal');
            var target = bar.__clbiScrollTarget;
            var startY;
            var startScroll;
            var maxScroll;
            var maxThumbTop;
            var trackHeight;
            var thumbHeight;


    var token = ++PROFILE_RENDER_TOKEN;
            if (!target) return;
    var ns = mw.config.get('wgNamespaceNumber');
    var title = mw.config.get('wgTitle');
    var specialPage = mw.config.get('wgCanonicalSpecialPageName');
    var isProfileSettings = specialPage === '사용자정보';


    $('body').toggleClass('user-profile-page', ns === 2);
            e.preventDefault();
    $('body').toggleClass('user-profile-settings-page', isProfileSettings);
            e.stopPropagation();


    if (ns === 2) {
            startY = e.clientY;
        var profileUser = title.split('/')[0];
            startScroll = target.scrollTop;
        renderProfile(profileUser, token);
            maxScroll = Math.max(1, target.scrollHeight - target.clientHeight);
    }
            trackHeight = track ? track.clientHeight : 0;
            thumbHeight = thumb.offsetHeight || 0;
            maxThumbTop = Math.max(1, trackHeight - thumbHeight);


    if (isProfileSettings) {
            bar.classList.add('is-dragging');
        initUserProfilePage();
    }
}


function renderProfile(username, token) {
            function onMove(moveEvent) {
    var api = new mw.Api();
                var dy = moveEvent.clientY - startY;
    api.get({
                target.scrollTop = startScroll + (dy / maxThumbTop) * maxScroll;
        action: 'query',
                updateClbiCustomScrollbar(bar);
        list: 'users',
                moveEvent.preventDefault();
        ususers: username,
            }
        usprop: 'editcount'
    }).then(function(data) {
        if (token !== PROFILE_RENDER_TOKEN) return;
        if (mw.config.get('wgNamespaceNumber') !== 2) return;


        var currentTitle = String(mw.config.get('wgTitle') || '').split('/')[0];
            function onUp() {
        if (currentTitle !== username) return;
                bar.classList.remove('is-dragging');
                document.removeEventListener('mousemove', onMove);
                document.removeEventListener('mouseup', onUp);
            }


        var user = data.query.users[0];
            document.addEventListener('mousemove', onMove);
        var contentEl = document.getElementById('mw-content-text');
            document.addEventListener('mouseup', onUp);
         if (!contentEl) return;
         });
    }


         var pageContent = contentEl.querySelector('.mw-parser-output') || contentEl;
    if (!scrollEl.__clbiCustomScrollbarBound) {
        injectProfileCard(username, user, pageContent);
         scrollEl.__clbiCustomScrollbarBound = true;
    });
        scrollEl.addEventListener('scroll', function () {
}
            if (scrollEl.__clbiCustomScrollbar) {
                updateClbiCustomScrollbar(scrollEl.__clbiCustomScrollbar);
            }
        }, { passive: true });
    }


function injectProfileCard(username, userData, container) {
     scrollEl.__clbiCustomScrollbar = bar;
     var isOwnPage = mw.config.get('wgUserName') === username;
     updateClbiCustomScrollbar(bar);
     var editCount = (userData && userData.editcount) ? userData.editcount : 0;


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


     container.classList.add('user-profile-portal');
function updateClbiCustomScrollbar(bar) {
    var scrollEl = bar && bar.__clbiScrollTarget;
     var track = bar ? bar.querySelector('.clbi-custom-scroll-track') : null;
    var thumb = bar ? bar.querySelector('.clbi-custom-scroll-thumb') : null;
    var maxScroll;
    var trackHeight;
    var thumbHeight;
    var maxTop;
    var top;
 
    if (!bar || !scrollEl || !track || !thumb) return;
 
    maxScroll = scrollEl.scrollHeight - scrollEl.clientHeight;
    if (maxScroll <= 1) {
        bar.classList.add('is-hidden');
        return;
    }
 
    bar.classList.remove('is-hidden');
 
    trackHeight = Math.max(1, track.clientHeight || 1);
    thumbHeight = Math.max(12, Math.floor((scrollEl.clientHeight / Math.max(scrollEl.scrollHeight, 1)) * trackHeight));
    thumbHeight = Math.min(trackHeight, thumbHeight);
    maxTop = Math.max(0, trackHeight - thumbHeight);
    top = maxScroll > 0 ? Math.round((scrollEl.scrollTop / maxScroll) * maxTop) : 0;


     var safeUsername = escapeHtml(username);
     thumb.style.height = thumbHeight + 'px';
    var avatarSrc = '/index.php?title=특수:Redirect/file/Pfp-' + encodeURIComponent(username) + '.png&width=220';
     thumb.style.transform = 'translateY(' + top + 'px)';
     var fallbackSrc = '/index.php?title=특수:Redirect/file/Pfp-default.png&width=220';
}
    var editBtn = isOwnPage
        ? '<a href="/index.php/특수:사용자정보" class="profile-edit-btn"><span class="profile-edit-label">프로필 수정</span><span class="profile-edit-arrow">›</span></a>'
        : '';


     var progressHtml = isOwnPage
function initClbiCustomDocumentScrollbars() {
        ? '<div class="profile-page-progress is-syncing" data-profile-progress>' +
     var existing = Array.prototype.slice.call(document.querySelectorAll('.clbi-custom-scrollbar'));
            '<div class="profile-section-title">LEVEL RECORD</div>' +
    var targets = getClbiDocumentScrollTargets();
            '<div class="profile-page-progress-body">' +
    var liveBars = [];
                '<div class="profile-page-progress-row">' +
                    '<span class="profile-page-level">SYNC</span>' +
                    '<span class="profile-page-total-xp">— XP</span>' +
                '</div>' +
                '<div class="profile-page-xp-bar" aria-hidden="true"><div class="profile-page-xp-fill"></div></div>' +
                '<div class="profile-page-progress-sub">SYNCING</div>' +
                '<div class="profile-page-progress-meta">TODAY — · DISCOVERED —</div>' +
            '</div>' +
        '</div>'
        : '';


     var card = document.createElement('div');
     if (!targets.length) {
    card.className = 'profile-card profile-page-console';
        existing.forEach(function (bar) { bar.remove(); });
    card.innerHTML =
         return;
        '<div class="profile-card-titlebar">' +
    }
            '<span>USER PROFILE</span>' +
            '<span>OFFICIAL ARCHIVE</span>' +
        '</div>' +
        '<div class="profile-card-body">' +
            '<div class="profile-identity-row">' +
                '<div class="profile-avatar-bay">' +
                    '<img src="' + avatarSrc + '" onerror="this.onerror=null;this.src=\'' + fallbackSrc + '\';" alt="' + safeUsername + '">' +
                '</div>' +
                '<div class="profile-info-panel">' +
                    '<div class="profile-nameplate">' +
                        '<h2 class="profile-username">' + safeUsername + '</h2>' +
                        editBtn +
                    '</div>' +
                    '<div class="profile-name" data-field="name"></div>' +
                    '<div class="profile-role" data-field="role"></div>' +
                    '<div class="profile-discord" data-field="discord"></div>' +
                '</div>' +
            '</div>' +
            '<div class="profile-lower-grid">' +
                '<div class="profile-bio-panel">' +
                    '<div class="profile-section-title">BIOGRAPHY</div>' +
                    '<div class="profile-bio" data-field="bio"></div>' +
                '</div>' +
                '<div class="profile-stats-panel">' +
                    '<div class="profile-section-title">RECORD</div>' +
                    '<div class="profile-stats">' +
                        progressHtml +
                        '<div class="profile-stat-grid">' +
                            '<div class="profile-stat">' +
                                '<span class="clbi-stat-value">' + editCount + '</span>' +
                                '<span class="clbi-stat-label">수정 횟수</span>' +
                            '</div>' +
                            '<div class="profile-stat" data-contrib-pages-stat>' +
                                '<span class="clbi-stat-value" data-contrib-pages-value>SYNC</span>' +
                                '<span class="clbi-stat-label">기여 문서</span>' +
                            '</div>' +
                        '</div>' +
                        '<div class="profile-info-grid">' +
                            '<div class="profile-info-stat">' +
                                '<span class="profile-info-label">TIME</span>' +
                                '<span class="profile-info-value" data-profile-time>UTC --:--</span>' +
                            '</div>' +
                            '<div class="profile-info-stat">' +
                                '<span class="profile-info-label">LANGUAGE</span>' +
                                '<span class="profile-info-value" data-profile-language>—</span>' +
                            '</div>' +
                        '</div>' +
                    '</div>' +
                '</div>' +
            '</div>' +
         '</div>';


     $('.profile-card').remove();
     targets.forEach(function (scrollEl) {
    container.insertBefore(card, container.firstChild);
        var well = getClbiOuterWellForScroll(scrollEl);
    loadProfileFields(username, card);
        var bar;
    loadProfileContributionPages(username, card);
    updateProfilePageEnvironment(card, null);


    if (isOwnPage) {
        if (!well) return;
         loadProfileProgressForUserPage(card);
        bar = buildClbiCustomScrollbar(well, scrollEl);
     }
         liveBars.push(bar);
}
     });


function getProfileLanguageLabel() {
    existing.forEach(function (bar) {
    var lang = getCurrentLang();
        if (liveBars.indexOf(bar) === -1) bar.remove();
     return SIDEBAR_LANGUAGE_LABELS[lang] || (lang ? lang.toUpperCase() : '—');
     });
}


function updateProfilePageEnvironment(card, summary) {
    window.requestAnimationFrame(function () {
     if (!card) return;
        liveBars.forEach(updateClbiCustomScrollbar);
     });


     var timezone = summary && summary.timezone ? summary.timezone : 'UTC';
     setTimeout(function () {
    var timeEl = card.querySelector('[data-profile-time]');
        liveBars.forEach(updateClbiCustomScrollbar);
     var langEl = card.querySelector('[data-profile-language]');
     }, 120);
}


    if (timeEl) {
if (!window.__clbiCustomScrollbarResizeBound) {
        try {
    window.__clbiCustomScrollbarResizeBound = true;
            timeEl.textContent = timezone + ' ' + new Intl.DateTimeFormat('ko-KR', {
    window.addEventListener('resize', function () {
                hour: '2-digit',
         setTimeout(initClbiCustomDocumentScrollbars, 60);
                minute: '2-digit',
     });
                hour12: false,
                timeZone: timezone
            }).format(new Date());
         } catch (err) {
            timeEl.textContent = 'UTC ' + new Intl.DateTimeFormat('ko-KR', {
                hour: '2-digit',
                minute: '2-digit',
                hour12: false,
                timeZone: 'UTC'
            }).format(new Date());
        }
     }
 
    if (langEl) {
        langEl.textContent = getProfileLanguageLabel();
    }
}
}


function loadProfileContributionPages(username, card) {
    if (!username || !card) return;
    if (!mw.loader || typeof mw.loader.using !== 'function') return;


     var valueEl = card.querySelector('[data-contrib-pages-value]');
// 시간 계산 함수
     if (valueEl) valueEl.textContent = 'SYNC';
function timeAgo(timestamp) {
     var now = new Date();
     var date = new Date(timestamp);
    var diff = Math.floor((now - date) / 1000);


     mw.loader.using(['mediawiki.api']).then(function () {
     if (diff < 60) return diff + '초 전';
        var api = new mw.Api();
    if (diff < 3600) return Math.floor(diff / 60) + '분 전';
        var pages = Object.create(null);
    if (diff < 86400) return Math.floor(diff / 3600) + '시간 전';
        var cont = {};
    return Math.floor(diff / 86400) + '일 전';
        var guard = 0;
}


        function requestNext() {
// 펼접 토글
            guard++;
// 펼접 토글
function getFoldTexts() {
    var lang = getCurrentLang();
    return (window.LANG && window.LANG[lang])
        ? window.LANG[lang]
        : (window.LANG ? window.LANG.ko : { expand: '펼치기', collapse: '접기' });
}


            var params = Object.assign({
function refreshOpenAncestors($start) {
                action: 'query',
    $start.parents('[id^="collapsible"]').each(function () {
                list: 'usercontribs',
        var $parent = $(this);
                ucuser: username,
        if (!$parent.hasClass('folding-open')) return;
                ucnamespace: 0,
                ucprop: 'title',
                uclimit: 'max',
                format: 'json',
                formatversion: 2
            }, cont);


            return api.get(params).then(function (data) {
        // 이미 fully open 상태면 굳이 다시 잠그지 않음
                var rows = data && data.query && data.query.usercontribs ? data.query.usercontribs : [];
        if ($parent.data('fold-state') === 'open') {
 
             return;
                rows.forEach(function (row) {
                    if (row && row.title) pages[row.title] = true;
                });
 
                if (data && data.continue && data.continue.uccontinue && guard < 40) {
                    cont = data.continue;
                    return requestNext();
                }
 
                if (valueEl) valueEl.textContent = Object.keys(pages).length;
             });
         }
         }


         requestNext().fail(function () {
         $parent.css('max-height', this.scrollHeight + 'px');
            if (valueEl) valueEl.textContent = '';
        });
     });
     });
}
}


function loadProfileProgressForUserPage(card) {
function bindInnerResizeUpdates($target) {
     if (!mw.config.get('wgUserName')) return;
     // 이미지 늦게 로드될 때 높이 갱신
    if (!mw.loader || typeof mw.loader.using !== 'function') return;
    $target.find('img').off('.foldimg').on('load.foldimg', function () {
 
         if ($target.hasClass('folding-open')) {
    mw.loader.using(['mediawiki.api']).then(function () {
             if ($target.data('fold-state') !== 'open') {
         var api = new mw.Api();
                $target.css('max-height', $target[0].scrollHeight + 'px');
        api.get({
             }
            action: 'progress_summary',
            refreshOpenAncestors($target);
            format: 'json',
         }
            formatversion: 2
        }).then(function (data) {
            var payload = data && data.progress_summary;
             if (!payload || !payload.available || !payload.summary) return;
            updateUserPageProgress(card, payload.summary);
             updateProfilePageEnvironment(card, payload.summary);
         });
     });
     });
}
}


function updateUserPageProgress(card, summary) {
function openFold($target, $btn) {
     var panel = card.querySelector('[data-profile-progress]');
     var t = getFoldTexts();
     if (!panel || !summary) return;
 
    $target.data('fold-state', 'opening');
     $target.addClass('folding-open');


     var level = summary.level || 1;
     // 열린 뒤 자연 확장 가능하게 만들기 위해 먼저 px로 열기
     var totalXp = summary.totalXp || 0;
     $target.css('max-height', '0px');
     var xpIntoLevel = summary.xpIntoLevel || 0;
     $target[0].offsetHeight;
    var xpForNext = summary.xpForNextLevel || 1;
     $target.css('max-height', $target[0].scrollHeight + 'px');
     var percent = Math.max(0, Math.min(100, summary.progressPercent || 0));
    var isMaxLevel = !!summary.isMaxLevel;
    var dailyXp = summary.dailyXp || 0;
    var discoveries = summary.discoveryCount || 0;


     panel.classList.remove('is-syncing');
     $btn.text(t.collapse);
    panel.classList.toggle('is-max-level', isMaxLevel);


     var levelEl = panel.querySelector('.profile-page-level');
     bindInnerResizeUpdates($target);
    var totalEl = panel.querySelector('.profile-page-total-xp');
    var fillEl = panel.querySelector('.profile-page-xp-fill');
    var subEl = panel.querySelector('.profile-page-progress-sub');
    var metaEl = panel.querySelector('.profile-page-progress-meta');


     if (levelEl) levelEl.textContent = (isMaxLevel ? 'MAX ' : 'LVL ') + level;
     // 바깥 펼접 즉시 갱신
    if (totalEl) totalEl.textContent = totalXp + ' XP';
     refreshOpenAncestors($target);
    if (fillEl) fillEl.style.width = percent + '%';
    if (subEl) subEl.textContent = isMaxLevel ? 'MAX LEVEL' : (xpIntoLevel + ' / ' + xpForNext + ' TO NEXT');
     if (metaEl) metaEl.textContent = 'TODAY ' + dailyXp + ' XP · DISCOVERED ' + discoveries;
}


function loadProfileFields(username, card) {
    // 전환 끝나면 none으로 풀어서 중첩 펼접/동적 내용 증가를 자연스럽게 허용
     var api = new mw.Api();
     $target.off('transitionend.foldopen').on('transitionend.foldopen', function (e) {
    api.get({
         if (e.target !== this) return;
        action: 'userprofile',
         if (!$target.hasClass('folding-open')) return;
        user: username
    }).then(function(data) {
         var profile = data.userprofile;
         updateProfileFields(card, {
            name: profile.name || '',
            discord: profile.discord || '',
            role: profile.role || '',
            bio: profile.bio || ''
        });
    }).fail(function() {
        updateProfileFields(card, {
            name: '',
            discord: '',
            role: '',
            bio: ''
        });
    });
}


function updateProfileFields(card, data) {
        $target.css('max-height', 'none');
    var nameEl = card.querySelector('[data-field="name"]');
        $target.data('fold-state', 'open');
    var roleEl = card.querySelector('[data-field="role"]');
    var discordEl = card.querySelector('[data-field="discord"]');
    var bioEl = card.querySelector('[data-field="bio"]');
    if (nameEl) nameEl.textContent = data.name || '';
    if (roleEl) roleEl.textContent = data.role || '';
    if (discordEl) discordEl.textContent = data.discord ? ('디스코드: ' + data.discord) : '';
    if (bioEl) bioEl.textContent = data.bio || '';
}
// ========== 프로필 시스템 끝 ==========


// ========== 알림 시스템 ==========
        refreshOpenAncestors($target);
function ensureNotificationPopup() {
     });
     if (document.getElementById('clbi-notification-popup')) return;


     var popup = document.createElement('div');
     // 늦게 렌더되는 콘텐츠 대응
    popup.id = 'clbi-notification-popup';
    requestAnimationFrame(function () {
    popup.style.cssText =
        if ($target.hasClass('folding-open') && $target.data('fold-state') !== 'open') {
        'display:none;position:fixed;z-index:99999;width:320px;max-height:420px;' +
            $target.css('max-height', $target[0].scrollHeight + 'px');
        'background:#0a0909;border:2px solid #854369;border-radius:5px;' +
            refreshOpenAncestors($target);
         'box-shadow:0 0 0 1px #1a1a1a, 0 8px 24px rgba(0,0,0,0.55);overflow:hidden;';
         }
    });


     popup.innerHTML =
     setTimeout(function () {
         '<div style="padding:10px 12px;border-bottom:2px solid #854369;background:linear-gradient(to bottom, #171114 0%, #0a0909 100%);color:#E2E2E2;font-size:13px;font-weight:700;display:flex;align-items:center;justify-content:space-between;gap:8px;">' +
         if ($target.hasClass('folding-open') && $target.data('fold-state') !== 'open') {
            '<span>알림</span>' +
            $target.css('max-height', $target[0].scrollHeight + 'px');
            '<button type="button" id="clbi-notification-readall" style="background:#171717;border:1px solid #854369;border-radius:6px;color:#E2E2E2;font-size:11px;font-weight:700;padding:4px 8px;cursor:pointer;">전체 읽음</button>' +
             refreshOpenAncestors($target);
        '</div>' +
         }
        '<div id="clbi-notification-list" style="max-height:320px;overflow-y:auto;padding:8px 0;color:#E2E2E2;font-size:12px;">불러오는 중...</div>' +
    }, 80);
        '<div style="padding:8px;border-top:1px solid #2a2a2a;background:#111;">' +
             '<a href="/index.php?title=Special:Notifications" id="clbi-notification-more" style="display:block;width:100%;text-align:center;padding:8px 10px;border-radius:6px;background:#171717;border:1px solid #854369;color:#E2E2E2 !important;text-decoration:none !important;font-size:12px;font-weight:700;">더보기</a>' +
         '</div>';


     document.body.appendChild(popup);
     setTimeout(function () {
        if ($target.hasClass('folding-open') && $target.data('fold-state') !== 'open') {
            $target.css('max-height', $target[0].scrollHeight + 'px');
            refreshOpenAncestors($target);
        }
    }, 220);
}
}


function positionNotificationPopup() {
function closeFold($target, $btn) {
     var btn = document.getElementById('profile-quick-notifications');
     var t = getFoldTexts();
    var popup = document.getElementById('clbi-notification-popup');
    if (!btn || !popup) return;


     var rect = btn.getBoundingClientRect();
     // none 상태에서 닫으면 transition이 안 되므로 실제 높이로 고정
    var top = rect.bottom + 6;
    if ($target.css('max-height') === 'none' || $target.data('fold-state') === 'open') {
    var left = rect.left + (rect.width / 2) - (popup.offsetWidth / 2);
         $target.css('max-height', $target[0].scrollHeight + 'px');
 
     } else {
    if (left < 8) left = 8;
         $target.css('max-height', $target[0].scrollHeight + 'px');
    if (left + popup.offsetWidth > window.innerWidth - 8) {
         left = window.innerWidth - popup.offsetWidth - 8;
     }
    if (top + popup.offsetHeight > window.innerHeight - 8) {
         top = Math.max(8, rect.top - popup.offsetHeight - 6);
     }
     }


     popup.style.top = top + 'px';
     $target.data('fold-state', 'closing');
     popup.style.left = left + 'px';
     $target[0].offsetHeight;
}
    $target.css('max-height', '0px');
    $target.removeClass('folding-open');


function parseNotificationItemsFromHtml(html) {
     $btn.text(t.expand);
     var parser = new DOMParser();
    var doc = parser.parseFromString(html, 'text/html');


     var selectors = [
     refreshOpenAncestors($target);
        '.mw-echo-ui-notificationItemWidget',
        '.mw-echo-ui-notificationsInboxWidgetRow',
        '.echo-ui-notificationItemWidget',
        'li[data-notification-id]',
        '.mw-echo-notifications-list li'
    ];


     var items = [];
     setTimeout(function () {
    for (var i = 0; i < selectors.length; i++) {
         refreshOpenAncestors($target);
         items = Array.prototype.slice.call(doc.querySelectorAll(selectors[i]));
         $target.data('fold-state', 'closed');
         if (items.length) break;
     }, 250);
     }
}


     return items.slice(0, 5).map(function(item) {
$(function () {
        var link = item.querySelector('a[href]');
     $(document)
        var href = link ? link.getAttribute('href') : '/index.php?title=Special:Notifications';
        .off('click.clbiToggle')
        var text = (item.textContent || '').replace(/\s+/g, ' ').trim();
        .on('click.clbiToggle', '.toggleBtn', function () {
            var $btn = $(this);
            var targetId = $btn.data('target');
            var $target = $('#' + targetId);
            if (!$target.length) return;


        var notificationId =
            var scrollY = window.scrollY;
            item.getAttribute('data-notification-id') ||
            item.getAttribute('data-id') ||
            item.getAttribute('data-notification') ||
            '';


        if (!notificationId) {
            if ($target.hasClass('folding-open')) {
            var anyWithId = item.querySelector('[data-notification-id], [data-id], [data-notification]');
                 closeFold($target, $btn);
            if (anyWithId) {
            } else {
                 notificationId =
                openFold($target, $btn);
                    anyWithId.getAttribute('data-notification-id') ||
                    anyWithId.getAttribute('data-id') ||
                    anyWithId.getAttribute('data-notification') ||
                    '';
             }
             }
        }


        if (href && href.indexOf('http') !== 0) {
             window.scrollTo(0, scrollY);
             href = href.charAt(0) === '/'
        });
                ? href
});
                : '/index.php' + (href.charAt(0) === '?' ? href : '/' + href);
        }


        return {
// ========== 프로필 시스템 ==========
            id: notificationId,
function initProfile() {
            href: href,
    $('.profile-card').remove();
            text: text || '알림'
     $('.user-profile-portal').removeClass('user-profile-portal');
        };
     });
}


function setNotificationIcon(hasItems) {
    var token = ++PROFILE_RENDER_TOKEN;
     var quickIcon = document.getElementById('profile-quick-notification-icon');
    var ns = mw.config.get('wgNamespaceNumber');
     var svg = hasItems ? CLBI_SVG_BELL_DOT : CLBI_SVG_BELL;
    var title = mw.config.get('wgTitle');
    var specialPage = mw.config.get('wgCanonicalSpecialPageName');
     var isProfileSettings = specialPage === '사용자정보';
 
    $('body').toggleClass('user-profile-page', ns === 2);
    $('body').toggleClass('user-profile-settings-page', isProfileSettings);
 
     if (ns === 2) {
        var profileUser = title.split('/')[0];
        renderProfile(profileUser, token);
    }


     if (quickIcon) {
     if (isProfileSettings) {
         quickIcon.innerHTML = svg;
         initUserProfilePage();
        quickIcon.classList.toggle('has-notifications', !!hasItems);
     }
     }
}
}


function renderNotificationPopup(items) {
function renderProfile(username, token) {
     var list = document.getElementById('clbi-notification-list');
     var api = new mw.Api();
     var badge = document.getElementById('clbi-notification-badge');
     api.get({
    if (!list) return;
        action: 'query',
        list: 'users',
        ususers: username,
        usprop: 'editcount'
    }).then(function(data) {
        if (token !== PROFILE_RENDER_TOKEN) return;
        if (mw.config.get('wgNamespaceNumber') !== 2) return;


    if (!items || !items.length) {
        var currentTitle = String(mw.config.get('wgTitle') || '').split('/')[0];
        list.innerHTML = '<div style="padding:14px 12px;color:#999;">표시할 알림이 없습니다.</div>';
         if (currentTitle !== username) return;
         if (badge) badge.style.display = 'none';
        setNotificationIcon(false);
        return;
    }


    var html = '';
        var user = data.query.users[0];
    for (var i = 0; i < items.length; i++) {
         var contentEl = document.getElementById('mw-content-text');
         html +=
        if (!contentEl) return;
            '<a href="' + items[i].href + '" class="clbi-notification-item" data-notification-id="' + (items[i].id || '') + '" style="display:block;padding:10px 12px;color:#E2E2E2 !important;text-decoration:none !important;border-bottom:1px solid #1f1f1f;line-height:1.5;">' +
                items[i].text +
            '</a>';
    }
    list.innerHTML = html;


    if (badge) {
         var pageContent = contentEl.querySelector('.mw-parser-output') || contentEl;
         badge.textContent = items.length;
         injectProfileCard(username, user, pageContent);
         badge.style.display = 'block';
     });
     }
    setNotificationIcon(true);
}
}


function loadNotificationsIntoPopup() {
function injectProfileCard(username, userData, container) {
     var list = document.getElementById('clbi-notification-list');
     var isOwnPage = mw.config.get('wgUserName') === username;
     if (list) {
     var editCount = (userData && userData.editcount) ? userData.editcount : 0;
        list.innerHTML = '<div style="padding:14px 12px;color:#999;">불러오는 중...</div>';
    }


     fetch('/index.php?title=Special:Notifications', { credentials: 'same-origin' })
     function escapeHtml(value) {
        .then(function(res) {
        return String(value == null ? '' : value)
            return res.text();
             .replace(/&/g, '&amp;')
        })
            .replace(/</g, '&lt;')
        .then(function(html) {
             .replace(/>/g, '&gt;')
            var items = parseNotificationItemsFromHtml(html);
             .replace(/"/g, '&quot;')
             renderNotificationPopup(items);
             .replace(/'/g, '&#039;');
        })
        .catch(function(err) {
             console.error(err);
             if (list) {
                list.innerHTML = '<div style="padding:14px 12px;color:#999;">알림을 불러오지 못했습니다.</div>';
             }
        });
}
 
function markAllNotificationsRead() {
    return new mw.Api().postWithToken('csrf', {
        action: 'echomarkread',
        list: 'all'
    });
}
 
function markNotificationReadById(notificationId) {
    if (!notificationId) {
        return $.Deferred().resolve().promise();
     }
     }


     return new mw.Api().postWithToken('csrf', {
     container.classList.add('user-profile-portal');
        action: 'echomarkread',
        list: notificationId
    });
}


function initNotifications() {
    var safeUsername = escapeHtml(username);
     var quickBtn = document.getElementById('profile-quick-notifications');
     var avatarSrc = '/index.php?title=특수:Redirect/file/Pfp-' + encodeURIComponent(username) + '.png&width=220';
    var fallbackSrc = '/index.php?title=특수:Redirect/file/Pfp-default.png&width=220';
    var editBtn = isOwnPage
        ? '<a href="/index.php/특수:사용자정보" class="profile-edit-btn"><span class="profile-edit-label">프로필 수정</span><span class="profile-edit-arrow">›</span></a>'
        : '';


     if (!quickBtn) return;
     var progressHtml = isOwnPage
        ? '<div class="profile-page-progress is-syncing" data-profile-progress>' +
            '<div class="profile-section-title">LEVEL RECORD</div>' +
            '<div class="profile-page-progress-body">' +
                '<div class="profile-page-progress-row">' +
                    '<span class="profile-page-level">SYNC</span>' +
                    '<span class="profile-page-total-xp">— XP</span>' +
                '</div>' +
                '<div class="profile-page-xp-bar" aria-hidden="true"><div class="profile-page-xp-fill"></div></div>' +
                '<div class="profile-page-progress-sub">SYNCING</div>' +
                '<div class="profile-page-progress-meta">TODAY — · DISCOVERED —</div>' +
            '</div>' +
        '</div>'
        : '';


     ensureNotificationPopup();
     var card = document.createElement('div');
     loadNotificationsIntoPopup();
     card.className = 'profile-card profile-page-console';
 
     card.innerHTML =
     $(document)
         '<div class="profile-card-titlebar">' +
         .off('click.clbiNotificationToggle')
            '<span>USER PROFILE</span>' +
         .on('click.clbiNotificationToggle', '#profile-quick-notifications', function(e) {
            '<span>OFFICIAL ARCHIVE</span>' +
             e.preventDefault();
         '</div>' +
             e.stopPropagation();
        '<div class="profile-card-body">' +
             '<div class="profile-identity-row">' +
                '<div class="profile-avatar-bay">' +
                    '<img src="' + avatarSrc + '" onerror="this.onerror=null;this.src=\'' + fallbackSrc + '\';" alt="' + safeUsername + '">' +
                '</div>' +
                '<div class="profile-info-panel">' +
                    '<div class="profile-nameplate">' +
                        '<h2 class="profile-username">' + safeUsername + '</h2>' +
                        editBtn +
                    '</div>' +
                    '<div class="profile-name" data-field="name"></div>' +
                    '<div class="profile-role" data-field="role"></div>' +
                    '<div class="profile-discord" data-field="discord"></div>' +
                '</div>' +
            '</div>' +
            '<div class="profile-lower-grid">' +
                '<div class="profile-bio-panel">' +
                    '<div class="profile-section-title">BIOGRAPHY</div>' +
                    '<div class="profile-bio" data-field="bio"></div>' +
                '</div>' +
                '<div class="profile-stats-panel">' +
                    '<div class="profile-section-title">RECORD</div>' +
                    '<div class="profile-stats">' +
                        progressHtml +
                        '<div class="profile-stat-grid">' +
                            '<div class="profile-stat">' +
                                '<span class="clbi-stat-value">' + editCount + '</span>' +
                                '<span class="clbi-stat-label">수정 횟수</span>' +
                            '</div>' +
                            '<div class="profile-stat" data-contrib-pages-stat>' +
                                '<span class="clbi-stat-value" data-contrib-pages-value>SYNC</span>' +
                                '<span class="clbi-stat-label">기여 문서</span>' +
                            '</div>' +
                        '</div>' +
                        '<div class="profile-info-grid">' +
                            '<div class="profile-info-stat">' +
                                '<span class="profile-info-label">TIME</span>' +
                                '<span class="profile-info-value" data-profile-time>UTC --:--</span>' +
                            '</div>' +
                            '<div class="profile-info-stat">' +
                                '<span class="profile-info-label">LANGUAGE</span>' +
                                '<span class="profile-info-value" data-profile-language>—</span>' +
                            '</div>' +
                        '</div>' +
                    '</div>' +
                '</div>' +
             '</div>' +
        '</div>';


            var popup = document.getElementById('clbi-notification-popup');
    $('.profile-card').remove();
            if (!popup) return;
    container.insertBefore(card, container.firstChild);
    loadProfileFields(username, card);
    loadProfileContributionPages(username, card);
    updateProfilePageEnvironment(card, null);


            if (popup.style.display === 'none' || popup.style.display === '') {
    if (isOwnPage) {
                popup.style.display = 'block';
        loadProfileProgressForUserPage(card);
                positionNotificationPopup();
    }
                loadNotificationsIntoPopup();
}
            } else {
                popup.style.display = 'none';
            }
        });


    $(document)
function getProfileLanguageLabel() {
        .off('click.clbiNotificationOutside')
    var lang = getCurrentLang();
        .on('click.clbiNotificationOutside', function(e) {
    return SIDEBAR_LANGUAGE_LABELS[lang] || (lang ? lang.toUpperCase() : '');
            var popup = document.getElementById('clbi-notification-popup');
}
            var quickToggle = document.getElementById('profile-quick-notifications');
            if (!popup) return;


            if (!popup.contains(e.target) && (!quickToggle || !quickToggle.contains(e.target))) {
function updateProfilePageEnvironment(card, summary) {
                popup.style.display = 'none';
    if (!card) return;
            }
        });


     $(document)
     var timezone = summary && summary.timezone ? summary.timezone : 'UTC';
        .off('click.clbiNotificationReadAll')
    var timeEl = card.querySelector('[data-profile-time]');
        .on('click.clbiNotificationReadAll', '#clbi-notification-readall', function(e) {
    var langEl = card.querySelector('[data-profile-language]');
            e.preventDefault();
            e.stopPropagation();


             var button = this;
    if (timeEl) {
             button.disabled = true;
        try {
             button.textContent = '처리 중...';
             timeEl.textContent = timezone + ' ' + new Intl.DateTimeFormat('ko-KR', {
                hour: '2-digit',
                minute: '2-digit',
                hour12: false,
                timeZone: timezone
             }).format(new Date());
        } catch (err) {
             timeEl.textContent = 'UTC ' + new Intl.DateTimeFormat('ko-KR', {
                hour: '2-digit',
                minute: '2-digit',
                hour12: false,
                timeZone: 'UTC'
            }).format(new Date());
        }
    }


            markAllNotificationsRead()
    if (langEl) {
                .then(function() {
        langEl.textContent = getProfileLanguageLabel();
                    loadNotificationsIntoPopup();
    }
                })
}
                .always(function() {
                    button.disabled = false;
                    button.textContent = '전체 읽음';
                });
        });


     $(document)
function loadProfileContributionPages(username, card) {
        .off('click.clbiNotificationItem')
     if (!username || !card) return;
        .on('click.clbiNotificationItem', '.clbi-notification-item', function(e) {
    if (!mw.loader || typeof mw.loader.using !== 'function') return;
             e.preventDefault();
 
             e.stopPropagation();
    var valueEl = card.querySelector('[data-contrib-pages-value]');
    if (valueEl) valueEl.textContent = 'SYNC';
 
    mw.loader.using(['mediawiki.api']).then(function () {
        var api = new mw.Api();
        var pages = Object.create(null);
        var cont = {};
        var guard = 0;
 
        function requestNext() {
             guard++;
 
            var params = Object.assign({
                action: 'query',
                list: 'usercontribs',
                ucuser: username,
                ucnamespace: 0,
                ucprop: 'title',
                uclimit: 'max',
                format: 'json',
                formatversion: 2
            }, cont);
 
             return api.get(params).then(function (data) {
                var rows = data && data.query && data.query.usercontribs ? data.query.usercontribs : [];


            var href = this.getAttribute('href');
                rows.forEach(function (row) {
            var notificationId = this.getAttribute('data-notification-id') || '';
                    if (row && row.title) pages[row.title] = true;
                });


            markNotificationReadById(notificationId).always(function() {
                if (data && data.continue && data.continue.uccontinue && guard < 40) {
                loadNotificationsIntoPopup();
                    cont = data.continue;
                if (href) {
                    return requestNext();
                    window.location.href = href;
                 }
                 }
                if (valueEl) valueEl.textContent = Object.keys(pages).length;
             });
             });
         });
         }


    $(window)
         requestNext().fail(function () {
         .off('resize.clbiNotification')
             if (valueEl) valueEl.textContent = '';
        .on('resize.clbiNotification', function() {
             var popup = document.getElementById('clbi-notification-popup');
            if (popup && popup.style.display === 'block') {
                positionNotificationPopup();
            }
         });
         });
    });
}
}
// ========== 알림 시스템 끝 ==========


function initUserProfilePage() {
function loadProfileProgressForUserPage(card) {
     $('body').addClass('user-profile-settings-page');
     if (!mw.config.get('wgUserName')) return;
    if (!mw.loader || typeof mw.loader.using !== 'function') return;


     var saveBtn = document.getElementById('pref-save');
     mw.loader.using(['mediawiki.api']).then(function () {
    if (!saveBtn) return;
        var api = new mw.Api();
        api.get({
            action: 'progress_summary',
            format: 'json',
            formatversion: 2
        }).then(function (data) {
            var payload = data && data.progress_summary;
            if (!payload || !payload.available || !payload.summary) return;
            updateUserPageProgress(card, payload.summary);
            updateProfilePageEnvironment(card, payload.summary);
        });
    });
}


    function getPrefRow(id) {
function updateUserPageProgress(card, summary) {
        var el = document.getElementById(id);
    var panel = card.querySelector('[data-profile-progress]');
        if (!el) return null;
    if (!panel || !summary) return;
        return el.closest('.clbi-pref-row') || el.parentNode;
    }


     function removePrefRow(id) {
     var level = summary.level || 1;
        var row = getPrefRow(id);
    var totalXp = summary.totalXp || 0;
        if (row && row.parentNode) {
    var xpIntoLevel = summary.xpIntoLevel || 0;
            row.parentNode.removeChild(row);
    var xpForNext = summary.xpForNextLevel || 1;
        }
    var percent = Math.max(0, Math.min(100, summary.progressPercent || 0));
     }
    var isMaxLevel = !!summary.isMaxLevel;
     var dailyXp = summary.dailyXp || 0;
    var discoveries = summary.discoveryCount || 0;


     function createPrefSection(className, titleText) {
     panel.classList.remove('is-syncing');
        var section = document.createElement('div');
    panel.classList.toggle('is-max-level', isMaxLevel);
        section.className = 'clbi-pref-section ' + className;


        var title = document.createElement('div');
    var levelEl = panel.querySelector('.profile-page-level');
        title.className = 'clbi-pref-section-title';
    var totalEl = panel.querySelector('.profile-page-total-xp');
        title.textContent = titleText;
    var fillEl = panel.querySelector('.profile-page-xp-fill');
    var subEl = panel.querySelector('.profile-page-progress-sub');
    var metaEl = panel.querySelector('.profile-page-progress-meta');


        var body = document.createElement('div');
    if (levelEl) levelEl.textContent = (isMaxLevel ? 'MAX ' : 'LVL ') + level;
        body.className = 'clbi-pref-section-body';
    if (totalEl) totalEl.textContent = totalXp + ' XP';
    if (fillEl) fillEl.style.width = percent + '%';
    if (subEl) subEl.textContent = isMaxLevel ? 'MAX LEVEL' : (xpIntoLevel + ' / ' + xpForNext + ' TO NEXT');
    if (metaEl) metaEl.textContent = 'TODAY ' + dailyXp + ' XP · DISCOVERED ' + discoveries;
}


         section.appendChild(title);
function loadProfileFields(username, card) {
         section.appendChild(body);
    var api = new mw.Api();
 
    api.get({
         return {
         action: 'userprofile',
             section: section,
        user: username
             body: body
    }).then(function(data) {
         };
        var profile = data.userprofile;
     }
         updateProfileFields(card, {
            name: profile.name || '',
            discord: profile.discord || '',
            role: profile.role || '',
            bio: profile.bio || ''
        });
    }).fail(function() {
         updateProfileFields(card, {
             name: '',
             discord: '',
            role: '',
            bio: ''
         });
     });
}


    function moveRowToSection(id, targetBody, className) {
function updateProfileFields(card, data) {
        var row = getPrefRow(id);
    var nameEl = card.querySelector('[data-field="name"]');
        if (!row || !targetBody) return false;
    var roleEl = card.querySelector('[data-field="role"]');
    var discordEl = card.querySelector('[data-field="discord"]');
    var bioEl = card.querySelector('[data-field="bio"]');
    if (nameEl) nameEl.textContent = data.name || '';
    if (roleEl) roleEl.textContent = data.role || '';
    if (discordEl) discordEl.textContent = data.discord ? ('디스코드: ' + data.discord) : '';
    if (bioEl) bioEl.textContent = data.bio || '';
}
// ========== 프로필 시스템 끝 ==========


        row.classList.add('clbi-pref-row-key-' + className);
// ========== 알림 시스템 ==========
        targetBody.appendChild(row);
function ensureNotificationPopup() {
        return true;
    if (document.getElementById('clbi-notification-popup')) return;
    }


     function rebuildProfileSettingsLayout() {
     var popup = document.createElement('div');
        var root = document.querySelector('.clbi-prefs-profile');
    popup.id = 'clbi-notification-popup';
        if (!root || root.dataset.profileSettingsReworked === '1') return;
    popup.style.cssText =
        'display:none;position:fixed;z-index:99999;width:320px;max-height:420px;' +
        'background:#0a0909;border:2px solid #854369;border-radius:5px;' +
        'box-shadow:0 0 0 1px #1a1a1a, 0 8px 24px rgba(0,0,0,0.55);overflow:hidden;';


         root.dataset.profileSettingsReworked = '1';
    popup.innerHTML =
         root.classList.add('profile-settings-console');
         '<div style="padding:10px 12px;border-bottom:2px solid #854369;background:linear-gradient(to bottom, #171114 0%, #0a0909 100%);color:#E2E2E2;font-size:13px;font-weight:700;display:flex;align-items:center;justify-content:space-between;gap:8px;">' +
            '<span>알림</span>' +
            '<button type="button" id="clbi-notification-readall" style="background:#171717;border:1px solid #854369;border-radius:6px;color:#E2E2E2;font-size:11px;font-weight:700;padding:4px 8px;cursor:pointer;">전체 읽음</button>' +
        '</div>' +
         '<div id="clbi-notification-list" style="max-height:320px;overflow-y:auto;padding:8px 0;color:#E2E2E2;font-size:12px;">불러오는 중...</div>' +
        '<div style="padding:8px;border-top:1px solid #2a2a2a;background:#111;">' +
            '<a href="/index.php?title=Special:Notifications" id="clbi-notification-more" style="display:block;width:100%;text-align:center;padding:8px 10px;border-radius:6px;background:#171717;border:1px solid #854369;color:#E2E2E2 !important;text-decoration:none !important;font-size:12px;font-weight:700;">더보기</a>' +
        '</div>';


        removePrefRow('pref-badges');
    document.body.appendChild(popup);
}


        var originalRows = Array.prototype.slice.call(root.querySelectorAll('.clbi-pref-row'));
function positionNotificationPopup() {
        var actionNodes = [];
    var btn = document.getElementById('profile-quick-notifications');
    var popup = document.getElementById('clbi-notification-popup');
    if (!btn || !popup) return;


        if (saveBtn.parentNode === root || saveBtn.closest('.clbi-prefs-profile') === root) {
    var rect = btn.getBoundingClientRect();
            actionNodes.push(saveBtn);
    var top = rect.bottom + 6;
        }
    var left = rect.left + (rect.width / 2) - (popup.offsetWidth / 2);


         var statusNode = document.getElementById('pref-status');
    if (left < 8) left = 8;
        if (statusNode && statusNode.closest('.clbi-prefs-profile') === root) {
    if (left + popup.offsetWidth > window.innerWidth - 8) {
            actionNodes.push(statusNode);
         left = window.innerWidth - popup.offsetWidth - 8;
        }
    }
    if (top + popup.offsetHeight > window.innerHeight - 8) {
        top = Math.max(8, rect.top - popup.offsetHeight - 6);
    }


        var main = document.createElement('div');
    popup.style.top = top + 'px';
        main.className = 'clbi-pref-main-grid';
    popup.style.left = left + 'px';
}


        var media = createPrefSection('clbi-pref-section-media', 'PROFILE IMAGE');
function parseNotificationItemsFromHtml(html) {
        var identity = createPrefSection('clbi-pref-section-identity', 'IDENTITY RECORD');
    var parser = new DOMParser();
        var bio = createPrefSection('clbi-pref-section-bio', 'BIOGRAPHY');
    var doc = parser.parseFromString(html, 'text/html');
        var account = createPrefSection('clbi-pref-section-account', 'ACCOUNT CONTACT');
        var misc = createPrefSection('clbi-pref-section-misc', 'OTHER OPTIONS');


         main.appendChild(media.section);
    var selectors = [
         main.appendChild(identity.section);
         '.mw-echo-ui-notificationItemWidget',
         main.appendChild(bio.section);
         '.mw-echo-ui-notificationsInboxWidgetRow',
         main.appendChild(account.section);
         '.echo-ui-notificationItemWidget',
         main.appendChild(misc.section);
         'li[data-notification-id]',
         '.mw-echo-notifications-list li'
    ];


         root.innerHTML = '';
    var items = [];
         root.appendChild(main);
    for (var i = 0; i < selectors.length; i++) {
         items = Array.prototype.slice.call(doc.querySelectorAll(selectors[i]));
         if (items.length) break;
    }


        moveRowToSection('pref-pfp-preview', media.body, 'pfp');
    return items.slice(0, 5).map(function(item) {
         moveRowToSection('pref-pfp-btn', media.body, 'pfp');
        var link = item.querySelector('a[href]');
         moveRowToSection('pref-pfp-input', media.body, 'pfp');
         var href = link ? link.getAttribute('href') : '/index.php?title=Special:Notifications';
         var text = (item.textContent || '').replace(/\s+/g, ' ').trim();


         moveRowToSection('pref-name', identity.body, 'name');
         var notificationId =
        moveRowToSection('pref-role', identity.body, 'role');
            item.getAttribute('data-notification-id') ||
        moveRowToSection('pref-discord', identity.body, 'discord');
            item.getAttribute('data-id') ||
            item.getAttribute('data-notification') ||
            '';


         moveRowToSection('pref-bio', bio.body, 'bio');
         if (!notificationId) {
 
            var anyWithId = item.querySelector('[data-notification-id], [data-id], [data-notification]');
        moveRowToSection('pref-new-email', account.body, 'email');
            if (anyWithId) {
        moveRowToSection('pref-email-password', account.body, 'email');
                notificationId =
        moveRowToSection('pref-email-save', account.body, 'email');
                    anyWithId.getAttribute('data-notification-id') ||
 
                    anyWithId.getAttribute('data-id') ||
        originalRows.forEach(function (row) {
                    anyWithId.getAttribute('data-notification') ||
            if (!row.parentNode && !row.className.match(/clbi-pref-row-key-/)) {
                    '';
                misc.body.appendChild(row);
             }
             }
         });
         }


         if (!misc.body.children.length) {
         if (href && href.indexOf('http') !== 0) {
             misc.section.parentNode.removeChild(misc.section);
             href = href.charAt(0) === '/'
                ? href
                : '/index.php' + (href.charAt(0) === '?' ? href : '/' + href);
         }
         }


         var actions = document.createElement('div');
         return {
        actions.className = 'clbi-pref-actions';
            id: notificationId,
            href: href,
            text: text || '알림'
        };
    });
}


        if (saveBtn) actions.appendChild(saveBtn);
function setNotificationIcon(hasItems) {
        if (statusNode) actions.appendChild(statusNode);
    var quickIcon = document.getElementById('profile-quick-notification-icon');
    var svg = hasItems ? CLBI_SVG_BELL_DOT : CLBI_SVG_BELL;


         root.appendChild(actions);
    if (quickIcon) {
        quickIcon.innerHTML = svg;
         quickIcon.classList.toggle('has-notifications', !!hasItems);
     }
     }
}


     rebuildProfileSettingsLayout();
function renderNotificationPopup(items) {
    var list = document.getElementById('clbi-notification-list');
    var badge = document.getElementById('clbi-notification-badge');
     if (!list) return;


     var api = new mw.Api();
     if (!items || !items.length) {
    var selectedFile = null;
        list.innerHTML = '<div style="padding:14px 12px;color:#999;">표시할 알림이 없습니다.</div>';
     var cropper = null;
        if (badge) badge.style.display = 'none';
        setNotificationIcon(false);
        return;
     }


     if (!document.getElementById('clbi-gallery-modal')) {
     var html = '';
         var gModal = document.createElement('div');
    for (var i = 0; i < items.length; i++) {
        gModal.id = 'clbi-gallery-modal';
         html +=
        gModal.style.cssText =
            '<a href="' + items[i].href + '" class="clbi-notification-item" data-notification-id="' + (items[i].id || '') + '" style="display:block;padding:10px 12px;color:#E2E2E2 !important;text-decoration:none !important;border-bottom:1px solid #1f1f1f;line-height:1.5;">' +
            'display:none;position:fixed;top:0;left:0;width:100%;height:100%;background:rgba(0,0,0,0.85);z-index:99999;align-items:center;justify-content:center;';
                items[i].text +
            '</a>';
    }
    list.innerHTML = html;


         gModal.innerHTML =
    if (badge) {
            '<div style="background:#1e1e1e;border:2px solid #854369;border-radius:12px;padding:24px;max-width:480px;width:90%;display:flex;flex-direction:column;gap:16px;">' +
         badge.textContent = items.length;
                '<div style="display:flex;justify-content:space-between;align-items:center;">' +
        badge.style.display = 'block';
                    '<span style="font-size:14px;font-weight:700;color:#e2e2e2;">프로필 사진 선택</span>' +
    }
                    '<button type="button" id="clbi-gallery-close" style="background:none;border:none;color:#aaa;font-size:18px;cursor:pointer;">✕</button>' +
    setNotificationIcon(true);
                '</div>' +
}
                '<button type="button" id="clbi-gallery-upload-btn" style="background:#2a2a2a;border:2px dashed #854369;border-radius:8px;padding:32px;color:#e2e2e2;cursor:pointer;display:flex;flex-direction:column;align-items:center;gap:8px;font-size:13px;width:100%;">' +
                    '<span style="font-size:32px;">🖼️</span>새 사진 업로드' +
                '</button>' +
                '<div id="clbi-gallery-history-section" style="display:none;">' +
                    '<div style="font-size:11px;color:#888;margin-bottom:8px;">이전 사진 — 클릭하면 바로 적용</div>' +
                    '<div id="clbi-gallery-history" style="display:flex;gap:8px;flex-wrap:wrap;"></div>' +
                '</div>' +
            '</div>';


        document.body.appendChild(gModal);
function loadNotificationsIntoPopup() {
    var list = document.getElementById('clbi-notification-list');
    if (list) {
        list.innerHTML = '<div style="padding:14px 12px;color:#999;">불러오는 중...</div>';
     }
     }


     if (!document.getElementById('clbi-crop-modal')) {
     fetch('/index.php?title=Special:Notifications', { credentials: 'same-origin' })
        var cModal = document.createElement('div');
        .then(function(res) {
         cModal.id = 'clbi-crop-modal';
            return res.text();
         cModal.style.cssText =
         })
             'display:none;position:fixed;top:0;left:0;width:100%;height:100%;background:rgba(0,0,0,0.85);z-index:99999;align-items:center;justify-content:center;';
        .then(function(html) {
            var items = parseNotificationItemsFromHtml(html);
            renderNotificationPopup(items);
         })
        .catch(function(err) {
            console.error(err);
             if (list) {
                list.innerHTML = '<div style="padding:14px 12px;color:#999;">알림을 불러오지 못했습니다.</div>';
            }
        });
}


        cModal.innerHTML =
function markAllNotificationsRead() {
            '<div style="background:#1e1e1e;border:2px solid #854369;border-radius:12px;padding:24px;max-width:500px;width:90%;display:flex;flex-direction:column;gap:16px;">' +
    return new mw.Api().postWithToken('csrf', {
                '<div style="font-size:14px;font-weight:700;color:#e2e2e2;">사진 조정</div>' +
        action: 'echomarkread',
                '<div style="width:100%;max-height:380px;overflow:hidden;border-radius:8px;">' +
        list: 'all'
                    '<img id="clbi-crop-image" style="max-width:100%;">' +
    });
                '</div>' +
}
                '<div style="display:flex;gap:8px;justify-content:flex-end;">' +
                    '<button type="button" id="clbi-crop-cancel" style="background:#2a2a2a;color:#e2e2e2;border:1px solid #444;padding:8px 16px;border-radius:6px;cursor:pointer;">취소</button>' +
                    '<button type="button" id="clbi-crop-confirm" style="background:#854369;color:#fff;border:none;padding:8px 16px;border-radius:6px;cursor:pointer;">확정</button>' +
                '</div>' +
            '</div>';


         document.body.appendChild(cModal);
function markNotificationReadById(notificationId) {
    if (!notificationId) {
         return $.Deferred().resolve().promise();
     }
     }


     var gModal = document.getElementById('clbi-gallery-modal');
     return new mw.Api().postWithToken('csrf', {
    var cModal = document.getElementById('clbi-crop-modal');
        action: 'echomarkread',
    var cropImage = document.getElementById('clbi-crop-image');
        list: notificationId
     var pfpInput = document.getElementById('pref-pfp-input');
     });
}


    function openGallery() {
function initNotifications() {
        gModal.style.display = 'flex';
    var quickBtn = document.getElementById('profile-quick-notifications');


        var username = mw.config.get('wgUserName');
    if (!quickBtn) return;
        api.get({
            action: 'query',
            titles: '파일:Pfp-' + username + '.png',
            prop: 'imageinfo',
            iiprop: 'url|timestamp',
            iilimit: 6
        }).then(function(data) {
            var pages = data.query.pages;
            var page = pages[Object.keys(pages)[0]];
            if (!page.imageinfo || page.imageinfo.length === 0) return;


            var historyEl = document.getElementById('clbi-gallery-history');
    ensureNotificationPopup();
            var sectionEl = document.getElementById('clbi-gallery-history-section');
    loadNotificationsIntoPopup();
            historyEl.innerHTML = '';


            page.imageinfo.forEach(function(info, idx) {
    $(document)
                var wrap = document.createElement('div');
        .off('click.clbiNotificationToggle')
                wrap.style.cssText = 'position:relative;cursor:pointer;';
        .on('click.clbiNotificationToggle', '#profile-quick-notifications', function(e) {
            e.preventDefault();
            e.stopPropagation();


                var img = document.createElement('img');
            var popup = document.getElementById('clbi-notification-popup');
                img.src = info.url;
            if (!popup) return;
                img.style.cssText =
                    'width:72px;height:72px;object-fit:cover;border-radius:8px;border:2px solid #444;flex-shrink:0;';


                if (idx === 0) {
            if (popup.style.display === 'none' || popup.style.display === '') {
                    img.style.borderColor = '#854369';
                popup.style.display = 'block';
                    var badge = document.createElement('div');
                positionNotificationPopup();
                    badge.textContent = '현재';
                loadNotificationsIntoPopup();
                    badge.style.cssText =
            } else {
                        'position:absolute;bottom:4px;left:50%;transform:translateX(-50%);background:#854369;color:#fff;font-size:9px;padding:1px 6px;border-radius:10px;';
                popup.style.display = 'none';
                    wrap.appendChild(badge);
            }
                }
        });


                img.addEventListener('mouseenter', function() {
    $(document)
                    if (idx !== 0) img.style.borderColor = '#854369';
        .off('click.clbiNotificationOutside')
                });
        .on('click.clbiNotificationOutside', function(e) {
            var popup = document.getElementById('clbi-notification-popup');
            var quickToggle = document.getElementById('profile-quick-notifications');
            if (!popup) return;


                img.addEventListener('mouseleave', function() {
            if (!popup.contains(e.target) && (!quickToggle || !quickToggle.contains(e.target))) {
                    if (idx !== 0) img.style.borderColor = '#444';
                popup.style.display = 'none';
                });
            }
        });


                img.addEventListener('click', function() {
    $(document)
                    fetch(info.url)
        .off('click.clbiNotificationReadAll')
                        .then(function(r) {
        .on('click.clbiNotificationReadAll', '#clbi-notification-readall', function(e) {
                            return r.blob();
            e.preventDefault();
                        })
            e.stopPropagation();
                        .then(function(blob) {
                            selectedFile = new File([blob], 'profile.png', { type: 'image/png' });
                            document.getElementById('pref-pfp-preview').src = URL.createObjectURL(blob);
                            gModal.style.display = 'none';
                            document.getElementById('pref-pfp-btn').textContent = '✓ 사진 선택됨';
                        });
                });


                wrap.appendChild(img);
            var button = this;
                historyEl.appendChild(wrap);
            button.disabled = true;
             });
             button.textContent = '처리 중...';


             sectionEl.style.display = 'block';
             markAllNotificationsRead()
                .then(function() {
                    loadNotificationsIntoPopup();
                })
                .always(function() {
                    button.disabled = false;
                    button.textContent = '전체 읽음';
                });
         });
         });
    }


     function openCrop(src) {
     $(document)
         cropImage.src = src;
         .off('click.clbiNotificationItem')
         cModal.style.display = 'flex';
         .on('click.clbiNotificationItem', '.clbi-notification-item', function(e) {
            e.preventDefault();
            e.stopPropagation();


        if (cropper) {
             var href = this.getAttribute('href');
             cropper.destroy();
             var notificationId = this.getAttribute('data-notification-id') || '';
             cropper = null;
        }


        setTimeout(function() {
            markNotificationReadById(notificationId).always(function() {
            cropper = new Cropper(cropImage, {
                loadNotificationsIntoPopup();
                 aspectRatio: 1,
                 if (href) {
                viewMode: 1,
                    window.location.href = href;
                dragMode: 'move',
                 }
                autoCropArea: 0.8,
                cropBoxResizable: true,
                 cropBoxMovable: true
             });
             });
         }, 150);
         });
    }


     document.getElementById('pref-pfp-btn').addEventListener('click', function() {
     $(window)
        openGallery();
        .off('resize.clbiNotification')
    });
        .on('resize.clbiNotification', function() {
            var popup = document.getElementById('clbi-notification-popup');
            if (popup && popup.style.display === 'block') {
                positionNotificationPopup();
            }
        });
}
// ========== 알림 시스템 끝 ==========


     document.getElementById('clbi-gallery-upload-btn').addEventListener('click', function() {
function initUserProfilePage() {
        pfpInput.click();
     $('body').addClass('user-profile-settings-page');
    });


     document.getElementById('clbi-gallery-close').addEventListener('click', function() {
     var saveBtn = document.getElementById('pref-save');
        gModal.style.display = 'none';
     if (!saveBtn) return;
     });


     pfpInput.addEventListener('change', function() {
     function getPrefRow(id) {
         var file = this.files[0];
         var el = document.getElementById(id);
         if (!file) return;
         if (!el) return null;
        return el.closest('.clbi-pref-row') || el.parentNode;
    }


         gModal.style.display = 'none';
    function removePrefRow(id) {
         var row = getPrefRow(id);
        if (row && row.parentNode) {
            row.parentNode.removeChild(row);
        }
    }


        var reader = new FileReader();
    function createPrefSection(className, titleText) {
         reader.onload = function(e) {
         var section = document.createElement('div');
            openCrop(e.target.result);
         section.className = 'clbi-pref-section ' + className;
         };
        reader.readAsDataURL(file);
    });


    document.getElementById('clbi-crop-cancel').addEventListener('click', function() {
        var title = document.createElement('div');
         cModal.style.display = 'none';
         title.className = 'clbi-pref-section-title';
         if (cropper) {
         title.textContent = titleText;
            cropper.destroy();
            cropper = null;
        }
        pfpInput.value = '';
    });


    document.getElementById('clbi-crop-confirm').addEventListener('click', function() {
        var body = document.createElement('div');
        if (!cropper) return;
        body.className = 'clbi-pref-section-body';


         var canvas = cropper.getCroppedCanvas({ width: 256, height: 256 });
         section.appendChild(title);
         if (!canvas) return;
         section.appendChild(body);


         canvas.toBlob(function(blob) {
         return {
             selectedFile = new File([blob], 'profile.png', { type: 'image/png' });
             section: section,
             document.getElementById('pref-pfp-preview').src = URL.createObjectURL(blob);
             body: body
            cModal.style.display = 'none';
         };
            cropper.destroy();
     }
            cropper = null;
            document.getElementById('pref-pfp-btn').textContent = '✓ 사진 선택됨';
         }, 'image/png');
     });


     var emailSaveBtn = document.getElementById('pref-email-save');
     function moveRowToSection(id, targetBody, className) {
    if (emailSaveBtn) {
         var row = getPrefRow(id);
         emailSaveBtn.addEventListener('click', function() {
        if (!row || !targetBody) return false;
            var statusEl = document.getElementById('pref-email-status');
            var newEmail = document.getElementById('pref-new-email').value;
            var password = document.getElementById('pref-email-password').value;


            if (!newEmail || !password) {
        row.classList.add('clbi-pref-row-key-' + className);
                statusEl.textContent = '이메일과 비밀번호를 입력해주세요.';
        targetBody.appendChild(row);
                return;
        return true;
            }
    }


            statusEl.textContent = '변경 중...';
    function rebuildProfileSettingsLayout() {
        var root = document.querySelector('.clbi-prefs-profile');
        if (!root || root.dataset.profileSettingsReworked === '1') return;


            api.postWithToken('csrf', {
        root.dataset.profileSettingsReworked = '1';
                action: 'changeemail',
        root.classList.add('profile-settings-console');
                email: newEmail,
                password: password
            }).then(function() {
                statusEl.textContent = '✓ 이메일 변경됨';
                document.getElementById('pref-new-email').value = '';
                document.getElementById('pref-email-password').value = '';


                setTimeout(function() {
        removePrefRow('pref-badges');
                    statusEl.textContent = '';
                }, 3000);
            }).fail(function(code, data) {
                var msg = data && data.error && data.error.info ? data.error.info : '변경 실패';
                statusEl.textContent = msg;
            });
        });
    }


    saveBtn.addEventListener('click', function() {
         var originalRows = Array.prototype.slice.call(root.querySelectorAll('.clbi-pref-row'));
         var statusEl = document.getElementById('pref-status');
         var actionNodes = [];
         statusEl.textContent = '저장 중...';


         var promises = [];
         if (saveBtn.parentNode === root || saveBtn.closest('.clbi-prefs-profile') === root) {
            actionNodes.push(saveBtn);
        }


         if (selectedFile) {
         var statusNode = document.getElementById('pref-status');
            var username = mw.config.get('wgUserName');
        if (statusNode && statusNode.closest('.clbi-prefs-profile') === root) {
            promises.push(
             actionNodes.push(statusNode);
                api.postWithToken('csrf', {
         }
                    action: 'upload',
                    filename: 'Pfp-' + username + '.png',
                    ignorewarnings: true,
                    file: selectedFile,
                    format: 'json'
                }, {
                    contentType: 'multipart/form-data'
                })
             );
         }


         var fields = ['name', 'discord', 'role', 'bio'];
         var main = document.createElement('div');
        main.className = 'clbi-pref-main-grid';


         for (var i = 0; i < fields.length; i++) {
         var media = createPrefSection('clbi-pref-section-media', 'PROFILE IMAGE');
            var el = document.getElementById('pref-' + fields[i]);
        var identity = createPrefSection('clbi-pref-section-identity', 'IDENTITY RECORD');
            if (!el) continue;
        var bio = createPrefSection('clbi-pref-section-bio', 'BIOGRAPHY');
        var account = createPrefSection('clbi-pref-section-account', 'ACCOUNT CONTACT');
        var misc = createPrefSection('clbi-pref-section-misc', 'OTHER OPTIONS');


            promises.push(
        main.appendChild(media.section);
                api.postWithToken('csrf', {
        main.appendChild(identity.section);
                    action: 'options',
        main.appendChild(bio.section);
                    optionname: 'profile-' + fields[i],
        main.appendChild(account.section);
                    optionvalue: el.value
         main.appendChild(misc.section);
                })
            );
         }


         $.when.apply($, promises)
         root.innerHTML = '';
            .then(function() {
        root.appendChild(main);
                statusEl.textContent = '✓ 저장됨';
                selectedFile = null;
                document.getElementById('pref-pfp-btn').textContent = '사진 선택';


                setTimeout(function() {
        moveRowToSection('pref-pfp-preview', media.body, 'pfp');
                    statusEl.textContent = '';
        moveRowToSection('pref-pfp-btn', media.body, 'pfp');
                }, 2000);
        moveRowToSection('pref-pfp-input', media.body, 'pfp');
            })
            .fail(function() {
                statusEl.textContent = '저장 실패';
            });
    });
}


/* =========================================
        moveRowToSection('pref-name', identity.body, 'name');
  Banner / CRT Page Monitor thumbnail slices
        moveRowToSection('pref-role', identity.body, 'role');
  - base 이미지는 틀에 들어간 파일 문법 그대로 사용
        moveRowToSection('pref-discord', identity.body, 'discord');
  - slice 레이어에는 300px MediaWiki 썸네일만 삽입
  ========================================= */


(function ($, mw) {
        moveRowToSection('pref-bio', bio.body, 'bio');
    var thumbCache = {};


    function parseSliceWidth(value) {
        moveRowToSection('pref-new-email', account.body, 'email');
         var parsed = parseInt(value, 10);
        moveRowToSection('pref-email-password', account.body, 'email');
         moveRowToSection('pref-email-save', account.body, 'email');


         if (!isFinite(parsed) || parsed < 120) {
         originalRows.forEach(function (row) {
             return 300;
            if (!row.parentNode && !row.className.match(/clbi-pref-row-key-/)) {
                misc.body.appendChild(row);
             }
        });
 
        if (!misc.body.children.length) {
            misc.section.parentNode.removeChild(misc.section);
         }
         }


         return parsed;
         var actions = document.createElement('div');
    }
        actions.className = 'clbi-pref-actions';
 
        if (saveBtn) actions.appendChild(saveBtn);
        if (statusNode) actions.appendChild(statusNode);


    function getImageSrc(img) {
         root.appendChild(actions);
         return img ? (img.currentSrc || img.getAttribute('src') || img.src || '') : '';
     }
     }


     function getFileNameFromSrc(src) {
     rebuildProfileSettingsLayout();
        var a;
        var parts;
        var fileName;


        if (!src) return '';
    var api = new mw.Api();
    var selectedFile = null;
    var cropper = null;


         a = document.createElement('a');
    if (!document.getElementById('clbi-gallery-modal')) {
         a.href = src;
         var gModal = document.createElement('div');
         gModal.id = 'clbi-gallery-modal';
        gModal.style.cssText =
            'display:none;position:fixed;top:0;left:0;width:100%;height:100%;background:rgba(0,0,0,0.85);z-index:99999;align-items:center;justify-content:center;';


         parts = (a.pathname || '').split('/').filter(function (part) {
         gModal.innerHTML =
            return !!part;
            '<div style="background:#1e1e1e;border:2px solid #854369;border-radius:12px;padding:24px;max-width:480px;width:90%;display:flex;flex-direction:column;gap:16px;">' +
        });
                '<div style="display:flex;justify-content:space-between;align-items:center;">' +
                    '<span style="font-size:14px;font-weight:700;color:#e2e2e2;">프로필 사진 선택</span>' +
                    '<button type="button" id="clbi-gallery-close" style="background:none;border:none;color:#aaa;font-size:18px;cursor:pointer;">✕</button>' +
                '</div>' +
                '<button type="button" id="clbi-gallery-upload-btn" style="background:#2a2a2a;border:2px dashed #854369;border-radius:8px;padding:32px;color:#e2e2e2;cursor:pointer;display:flex;flex-direction:column;align-items:center;gap:8px;font-size:13px;width:100%;">' +
                    '<span style="font-size:32px;">🖼️</span>새 사진 업로드' +
                '</button>' +
                '<div id="clbi-gallery-history-section" style="display:none;">' +
                    '<div style="font-size:11px;color:#888;margin-bottom:8px;">이전 사진 — 클릭하면 바로 적용</div>' +
                    '<div id="clbi-gallery-history" style="display:flex;gap:8px;flex-wrap:wrap;"></div>' +
                '</div>' +
            '</div>';


         if (!parts.length) return '';
         document.body.appendChild(gModal);
    }


         fileName = parts.pop();
    if (!document.getElementById('clbi-crop-modal')) {
         var cModal = document.createElement('div');
        cModal.id = 'clbi-crop-modal';
        cModal.style.cssText =
            'display:none;position:fixed;top:0;left:0;width:100%;height:100%;background:rgba(0,0,0,0.85);z-index:99999;align-items:center;justify-content:center;';


         /*
         cModal.innerHTML =
        * MediaWiki thumb URL 예시:
            '<div style="background:#1e1e1e;border:2px solid #854369;border-radius:12px;padding:24px;max-width:500px;width:90%;display:flex;flex-direction:column;gap:16px;">' +
        * /images/thumb/a/ab/File.png/1000px-File.png
                '<div style="font-size:14px;font-weight:700;color:#e2e2e2;">사진 조정</div>' +
        * /images/thumb/a/ab/File.svg/1000px-File.svg.png
                '<div style="width:100%;max-height:380px;overflow:hidden;border-radius:8px;">' +
        *
                    '<img id="clbi-crop-image" style="max-width:100%;">' +
        * 이 경우 실제 파일명은 마지막 조각이 아니라 그 앞 조각이다.
                '</div>' +
        */
                '<div style="display:flex;gap:8px;justify-content:flex-end;">' +
        if (/^\d+px-/.test(fileName) && parts.length) {
                    '<button type="button" id="clbi-crop-cancel" style="background:#2a2a2a;color:#e2e2e2;border:1px solid #444;padding:8px 16px;border-radius:6px;cursor:pointer;">취소</button>' +
             fileName = parts.pop();
                    '<button type="button" id="clbi-crop-confirm" style="background:#854369;color:#fff;border:none;padding:8px 16px;border-radius:6px;cursor:pointer;">확정</button>' +
        }
                '</div>' +
             '</div>';


         fileName = fileName.replace(/^\d+px-/, '');
         document.body.appendChild(cModal);
    }


        try {
    var gModal = document.getElementById('clbi-gallery-modal');
            fileName = decodeURIComponent(fileName);
    var cModal = document.getElementById('clbi-crop-modal');
        } catch (e) {}
    var cropImage = document.getElementById('clbi-crop-image');
    var pfpInput = document.getElementById('pref-pfp-input');


         return fileName;
    function openGallery() {
    }
         gModal.style.display = 'flex';


    function resolveThumbUrl(img, width, callback) {
        var username = mw.config.get('wgUserName');
        var src = getImageSrc(img);
        api.get({
        var fileName = getFileNameFromSrc(src);
            action: 'query',
        var cacheKey;
            titles: '파일:Pfp-' + username + '.png',
        var entry;
            prop: 'imageinfo',
            iiprop: 'url|timestamp',
            iilimit: 6
        }).then(function(data) {
            var pages = data.query.pages;
            var page = pages[Object.keys(pages)[0]];
            if (!page.imageinfo || page.imageinfo.length === 0) return;


        if (!src) return;
            var historyEl = document.getElementById('clbi-gallery-history');
            var sectionEl = document.getElementById('clbi-gallery-history-section');
            historyEl.innerHTML = '';


        if (!fileName || !mw || !mw.loader) {
            page.imageinfo.forEach(function(info, idx) {
            callback(src);
                var wrap = document.createElement('div');
            return;
                wrap.style.cssText = 'position:relative;cursor:pointer;';
        }


        cacheKey = fileName + '|' + width;
                var img = document.createElement('img');
        entry = thumbCache[cacheKey];
                img.src = info.url;
                img.style.cssText =
                    'width:72px;height:72px;object-fit:cover;border-radius:8px;border:2px solid #444;flex-shrink:0;';


        if (entry) {
                if (idx === 0) {
            if (entry.resolved) {
                    img.style.borderColor = '#854369';
                callback(entry.url || src);
                    var badge = document.createElement('div');
            } else {
                    badge.textContent = '현재';
                 entry.callbacks.push(callback);
                    badge.style.cssText =
            }
                        'position:absolute;bottom:4px;left:50%;transform:translateX(-50%);background:#854369;color:#fff;font-size:9px;padding:1px 6px;border-radius:10px;';
            return;
                    wrap.appendChild(badge);
        }
                }
 
                 img.addEventListener('mouseenter', function() {
                    if (idx !== 0) img.style.borderColor = '#854369';
                });
 
                img.addEventListener('mouseleave', function() {
                    if (idx !== 0) img.style.borderColor = '#444';
                });


        entry = {
                img.addEventListener('click', function() {
            resolved: false,
                    fetch(info.url)
            url: '',
                        .then(function(r) {
            callbacks: [callback]
                            return r.blob();
        };
                        })
                        .then(function(blob) {
                            selectedFile = new File([blob], 'profile.png', { type: 'image/png' });
                            document.getElementById('pref-pfp-preview').src = URL.createObjectURL(blob);
                            gModal.style.display = 'none';
                            document.getElementById('pref-pfp-btn').textContent = '✓ 사진 선택됨';
                        });
                });


        thumbCache[cacheKey] = entry;
                wrap.appendChild(img);
                historyEl.appendChild(wrap);
            });


        function finish(url) {
             sectionEl.style.display = 'block';
             var callbacks = entry.callbacks.slice();
        });
            var i;
    }


            entry.resolved = true;
    function openCrop(src) {
            entry.url = url || src;
        cropImage.src = src;
            entry.callbacks = [];
        cModal.style.display = 'flex';


            for (i = 0; i < callbacks.length; i++) {
        if (cropper) {
                callbacks[i](entry.url);
            cropper.destroy();
             }
             cropper = null;
         }
         }


         mw.loader.using('mediawiki.api').done(function () {
         setTimeout(function() {
             var api = new mw.Api();
             cropper = new Cropper(cropImage, {
 
                 aspectRatio: 1,
            api.get({
                 viewMode: 1,
                 action: 'query',
                 dragMode: 'move',
                 titles: 'File:' + fileName,
                 autoCropArea: 0.8,
                 prop: 'imageinfo',
                 cropBoxResizable: true,
                 iiprop: 'url',
                 cropBoxMovable: true
                 iiurlwidth: width,
                 formatversion: 2
            }).done(function (data) {
                var page;
                var info;
 
                if (
                    data &&
                    data.query &&
                    data.query.pages &&
                    data.query.pages.length
                ) {
                    page = data.query.pages[0];
 
                    if (
                        page &&
                        page.imageinfo &&
                        page.imageinfo.length
                    ) {
                        info = page.imageinfo[0];
                    }
                }
 
                finish((info && (info.thumburl || info.url)) || src);
            }).fail(function () {
                finish(src);
             });
             });
         }).fail(function () {
         }, 150);
            finish(src);
        });
     }
     }


     function applySliceImages(frame, thumbUrl) {
     document.getElementById('pref-pfp-btn').addEventListener('click', function() {
         var slices;
         openGallery();
        var i;
    });
        var img;


         if (!frame || !thumbUrl) return;
    document.getElementById('clbi-gallery-upload-btn').addEventListener('click', function() {
         pfpInput.click();
    });


        slices = frame.querySelectorAll('.crt-page-monitor-slice');
    document.getElementById('clbi-gallery-close').addEventListener('click', function() {
        gModal.style.display = 'none';
    });


        for (i = 0; i < slices.length; i++) {
    pfpInput.addEventListener('change', function() {
            slices[i].innerHTML = '';
        var file = this.files[0];
        if (!file) return;


            img = document.createElement('img');
        gModal.style.display = 'none';
            img.className = 'crt-page-monitor-slice-img';
            img.src = thumbUrl;
            img.alt = '';
            img.decoding = 'async';
            img.loading = 'eager';
            img.setAttribute('aria-hidden', 'true');


             slices[i].appendChild(img);
        var reader = new FileReader();
        }
        reader.onload = function(e) {
             openCrop(e.target.result);
        };
        reader.readAsDataURL(file);
    });


        frame.setAttribute('data-crt-slices-ready', '1');
    document.getElementById('clbi-crop-cancel').addEventListener('click', function() {
     }
        cModal.style.display = 'none';
        if (cropper) {
            cropper.destroy();
            cropper = null;
        }
        pfpInput.value = '';
     });
 
    document.getElementById('clbi-crop-confirm').addEventListener('click', function() {
        if (!cropper) return;
 
        var canvas = cropper.getCroppedCanvas({ width: 256, height: 256 });
        if (!canvas) return;


    function initBannerFrame(frame) {
        canvas.toBlob(function(blob) {
         var baseImg;
            selectedFile = new File([blob], 'profile.png', { type: 'image/png' });
        var width;
            document.getElementById('pref-pfp-preview').src = URL.createObjectURL(blob);
            cModal.style.display = 'none';
            cropper.destroy();
            cropper = null;
            document.getElementById('pref-pfp-btn').textContent = '✓ 사진 선택됨';
         }, 'image/png');
    });


        if (!frame) return;
    var emailSaveBtn = document.getElementById('pref-email-save');
         if (frame.getAttribute('data-crt-slices-ready') === '1') return;
    if (emailSaveBtn) {
         emailSaveBtn.addEventListener('click', function() {
            var statusEl = document.getElementById('pref-email-status');
            var newEmail = document.getElementById('pref-new-email').value;
            var password = document.getElementById('pref-email-password').value;


        baseImg = frame.querySelector('.crt-page-monitor-image-base img');
            if (!newEmail || !password) {
                statusEl.textContent = '이메일과 비밀번호를 입력해주세요.';
                return;
            }


        if (!baseImg) return;
            statusEl.textContent = '변경 중...';


        width = parseSliceWidth(frame.getAttribute('data-crt-slice-width'));
            api.postWithToken('csrf', {
                action: 'changeemail',
                email: newEmail,
                password: password
            }).then(function() {
                statusEl.textContent = '✓ 이메일 변경됨';
                document.getElementById('pref-new-email').value = '';
                document.getElementById('pref-email-password').value = '';


        resolveThumbUrl(baseImg, width, function (thumbUrl) {
                setTimeout(function() {
            if (!frame || !frame.parentNode) return;
                    statusEl.textContent = '';
             applySliceImages(frame, thumbUrl);
                }, 3000);
             }).fail(function(code, data) {
                var msg = data && data.error && data.error.info ? data.error.info : '변경 실패';
                statusEl.textContent = msg;
            });
         });
         });
     }
     }


     function initBannerFrames(root) {
     saveBtn.addEventListener('click', function() {
         var scope = root && root.querySelectorAll ? root : document;
         var statusEl = document.getElementById('pref-status');
         var frames = scope.querySelectorAll('.crt-page-monitor-frame');
        statusEl.textContent = '저장 중...';
         var i;
 
         var promises = [];
 
        if (selectedFile) {
            var username = mw.config.get('wgUserName');
            promises.push(
                api.postWithToken('csrf', {
                    action: 'upload',
                    filename: 'Pfp-' + username + '.png',
                    ignorewarnings: true,
                    file: selectedFile,
                    format: 'json'
                }, {
                    contentType: 'multipart/form-data'
                })
            );
        }
 
         var fields = ['name', 'discord', 'role', 'bio'];
 
        for (var i = 0; i < fields.length; i++) {
            var el = document.getElementById('pref-' + fields[i]);
            if (!el) continue;


        for (i = 0; i < frames.length; i++) {
            promises.push(
            initBannerFrame(frames[i]);
                api.postWithToken('csrf', {
                    action: 'options',
                    optionname: 'profile-' + fields[i],
                    optionvalue: el.value
                })
            );
         }
         }
    }


    $(function () {
        $.when.apply($, promises)
        initBannerFrames(document);
            .then(function() {
                statusEl.textContent = '✓ 저장됨';
                selectedFile = null;
                document.getElementById('pref-pfp-btn').textContent = '사진 선택';
 
                setTimeout(function() {
                    statusEl.textContent = '';
                }, 2000);
            })
            .fail(function() {
                statusEl.textContent = '저장 실패';
            });
     });
     });
 
}
    if (mw && mw.hook) {
        mw.hook('wikipage.content').add(function ($content) {
            initBannerFrames($content && $content[0] ? $content[0] : document);
        });
    }
})(jQuery, window.mw);


/* =========================================
/* =========================================
   Doc Tab System — tab switching UI
   Banner / CRT Page Monitor thumbnail slices
   글리치 플리커 + RGB split + 방향 슬라이드
   - base 이미지는 틀에 들어간 파일 문법 그대로 사용
  - slice 레이어에는 300px MediaWiki 썸네일만 삽입
   ========================================= */
   ========================================= */


(function () {
(function ($, mw) {
     'use strict';
     var thumbCache = {};


     function initDocTabs() {
     function parseSliceWidth(value) {
         var tabBars = document.querySelectorAll('.doc-tab-bar');
         var parsed = parseInt(value, 10);
        if (!tabBars.length) return;


         tabBars.forEach(function (bar) {
         if (!isFinite(parsed) || parsed < 120) {
             if (bar.getAttribute('data-tabs-init')) return;
             return 300;
            bar.setAttribute('data-tabs-init', '1');
        }


            var tabs = Array.from(bar.querySelectorAll('.doc-tab'));
        return parsed;
            if (!tabs.length) return;
    }


            var panel = bar.closest('.doc-panel');
    function getImageSrc(img) {
            var display = panel ? panel.querySelector('.doc-display') : null;
        return img ? (img.currentSrc || img.getAttribute('src') || img.src || '') : '';
            if (!display) display = document.getElementById('doc-main-display');
    }
            if (!display) return;


            tabs.forEach(function (tab, i) {
    function getFileNameFromSrc(src) {
                tab.addEventListener('click', function () {
        var a;
                    var currentIdx = tabs.findIndex(function (t) {
        var parts;
                        return t.classList.contains('active');
        var fileName;
                    });
                    if (currentIdx === i) return;
                    switchTab(tabs, display, i, i > currentIdx ? 1 : -1);
                });
            });


            var initIdx = tabs.findIndex(function (t) { return t.classList.contains('active'); });
        if (!src) return '';
            if (initIdx !== -1) {
                var initRef = tabs[initIdx].dataset.ref;
                var initEl = initRef ? document.getElementById(initRef) : null;
                display.innerHTML = initEl ? initEl.innerHTML : (tabs[initIdx].dataset.content || '');
            }
        });


    }
        a = document.createElement('a');
        a.href = src;


    var isAnimating = false;
        parts = (a.pathname || '').split('/').filter(function (part) {
            return !!part;
        });


    function switchTab(tabs, display, nextIdx, dir) {
         if (!parts.length) return '';
         if (isAnimating) return;
        isAnimating = true;


         tabs.forEach(function (t) { t.classList.remove('active'); });
         fileName = parts.pop();
        tabs[nextIdx].classList.add('active');


         var ref = tabs[nextIdx].dataset.ref;
         /*
        var nextContent;
        * MediaWiki thumb URL 예시:
         if (ref) {
        * /images/thumb/a/ab/File.png/1000px-File.png
            var refEl = document.getElementById(ref);
        * /images/thumb/a/ab/File.svg/1000px-File.svg.png
            nextContent = refEl ? refEl.innerHTML : '';
        *
        } else {
        * 이 경우 실제 파일명은 마지막 조각이 아니라 그 앞 조각이다.
             nextContent = tabs[nextIdx].dataset.content || '';
        */
         if (/^\d+px-/.test(fileName) && parts.length) {
             fileName = parts.pop();
         }
         }


         glitchOut(display, dir, function () {
         fileName = fileName.replace(/^\d+px-/, '');
             display.innerHTML = nextContent;
 
            glitchIn(display, dir, function () {
        try {
                isAnimating = false;
             fileName = decodeURIComponent(fileName);
            });
        } catch (e) {}
         });
 
         return fileName;
     }
     }


     function glitchOut(el, dir, cb) {
     function resolveThumbUrl(img, width, callback) {
         var duration = 160;
         var src = getImageSrc(img);
         var start = null;
         var fileName = getFileNameFromSrc(src);
         var slideX = dir * 16;
         var cacheKey;
        var entry;


         function step(ts) {
         if (!src) return;
            if (!start) start = ts;
            var p = Math.min((ts - start) / duration, 1);
            var ease = p * p;


            var tx = slideX * ease;
        if (!fileName || !mw || !mw.loader) {
             var skew = dir * ease * 1.0;
             callback(src);
             var opacity = 1 - ease;
             return;
            var rgb = ease * 5;
        }


            el.style.transform = 'translateX(' + tx + 'px) skewX(' + skew + 'deg)';
        cacheKey = fileName + '|' + width;
            el.style.opacity = opacity;
        entry = thumbCache[cacheKey];
            el.style.filter =
                'drop-shadow(' + (-rgb) + 'px 0 0 rgba(80,160,255,0.75)) ' +
                'drop-shadow(' + rgb + 'px 0 0 rgba(255,55,90,0.65)) ' +
                'brightness(' + (1 + ease * 0.25) + ')';


             if (p < 1) {
        if (entry) {
                 requestAnimationFrame(step);
             if (entry.resolved) {
                 callback(entry.url || src);
             } else {
             } else {
                 el.style.opacity = '0';
                 entry.callbacks.push(callback);
                cb();
             }
             }
            return;
         }
         }
        requestAnimationFrame(step);
    }


    function glitchIn(el, dir, cb) {
        entry = {
        var duration = 200;
            resolved: false,
        var start = null;
            url: '',
         var startX = -dir * 16;
            callbacks: [callback]
         };


         el.style.transform = 'translateX(' + startX + 'px) skewX(' + (-dir * 1.0) + 'deg)';
         thumbCache[cacheKey] = entry;
        el.style.opacity = '0';


         function step(ts) {
         function finish(url) {
            if (!start) start = ts;
             var callbacks = entry.callbacks.slice();
             var p = Math.min((ts - start) / duration, 1);
             var i;
             var ease = 1 - Math.pow(1 - p, 3);


             var tx = startX * (1 - ease);
             entry.resolved = true;
             var skew = -dir * 1.0 * (1 - ease);
             entry.url = url || src;
            var opacity = ease;
             entry.callbacks = [];
             var rgb = (1 - ease) * 3;
            var brightness = 1 + (1 - ease) * 0.35;


             el.style.transform = 'translateX(' + tx + 'px) skewX(' + skew + 'deg)';
             for (i = 0; i < callbacks.length; i++) {
            el.style.opacity = opacity;
                 callbacks[i](entry.url);
            el.style.filter =
                'drop-shadow(' + (-rgb) + 'px 0 0 rgba(80,160,255,0.65)) ' +
                'drop-shadow(' + rgb + 'px 0 0 rgba(255,55,90,0.55)) ' +
                'brightness(' + brightness + ')';
 
            if (p < 1) {
                 requestAnimationFrame(step);
            } else {
                el.style.transform = '';
                el.style.opacity = '';
                el.style.filter = '';
                cb();
             }
             }
         }
         }
        requestAnimationFrame(step);
    }


if (document.readyState === 'loading') {
        mw.loader.using('mediawiki.api').done(function () {
    document.addEventListener('DOMContentLoaded', initDocTabs);
            var api = new mw.Api();
} else {
    initDocTabs();
}


if (typeof mw !== 'undefined' && mw.hook) {
            api.get({
    mw.hook('wikipage.content').add(function () {
                action: 'query',
        initDocTabs();
                titles: 'File:' + fileName,
    });
                prop: 'imageinfo',
}
                iiprop: 'url',
                iiurlwidth: width,
                formatversion: 2
            }).done(function (data) {
                var page;
                var info;


})();
                if (
                    data &&
                    data.query &&
                    data.query.pages &&
                    data.query.pages.length
                ) {
                    page = data.query.pages[0];


/* =========================================
                    if (
  Doc Section Switch — 좌측 섹션 전환
                        page &&
  ========================================= */
                        page.imageinfo &&
                        page.imageinfo.length
                    ) {
                        info = page.imageinfo[0];
                    }
                }
 
                finish((info && (info.thumburl || info.url)) || src);
            }).fail(function () {
                finish(src);
            });
        }).fail(function () {
            finish(src);
        });
    }
 
    function applySliceImages(frame, thumbUrl) {
        var slices;
        var i;
        var img;
 
        if (!frame || !thumbUrl) return;


$(document).on('click', '.doc-nav-item[data-section]', function () {
        slices = frame.querySelectorAll('.crt-page-monitor-slice');
    var name = $(this).attr('data-section');
    var display = document.getElementById('doc-main-display');
    var titleEl = document.getElementById('doc-center-title');
    var tabBar = document.getElementById('doc-tab-bar-text');


    if (!display) return;
        for (i = 0; i < slices.length; i++) {
            slices[i].innerHTML = '';


    $('.doc-nav-item[data-section]').removeClass('active');
            img = document.createElement('img');
    $('.doc-nav-item[data-section="' + name + '"]').addClass('active');
            img.className = 'crt-page-monitor-slice-img';
            img.src = thumbUrl;
            img.alt = '';
            img.decoding = 'async';
            img.loading = 'eager';
            img.setAttribute('aria-hidden', 'true');


    if (name === 'text') {
             slices[i].appendChild(img);
        if (titleEl) titleEl.textContent = '개요';
        if (tabBar) $(tabBar).show();
        var activeTab = tabBar ? tabBar.querySelector('.doc-tab.active') : null;
        if (!activeTab && tabBar) activeTab = tabBar.querySelector('.doc-tab');
        if (activeTab) {
            var ref = activeTab.dataset.ref;
            var refEl = ref ? document.getElementById(ref) : null;
             display.innerHTML = refEl ? refEl.innerHTML : (activeTab.dataset.content || '');
         }
         }
    } else {
 
         if (titleEl) titleEl.textContent = name === 'factions' ? '세력' : name === 'people' ? '인물' : name;
         frame.setAttribute('data-crt-slices-ready', '1');
        if (tabBar) $(tabBar).hide();
        var refEl = document.getElementById('doc-content-' + name);
        display.innerHTML = refEl ? refEl.innerHTML : '';
     }
     }
});


/* =========================================
    function initBannerFrame(frame) {
  CRT WebGL Renderer — cool-retro-term IBM DOS style
        var baseImg;
  ========================================= */
        var width;
(function () {
 
    'use strict';
        if (!frame) return;
        if (frame.getAttribute('data-crt-slices-ready') === '1') return;


    function createNoiseTexture(gl) {
         baseImg = frame.querySelector('.crt-page-monitor-image-base img');
        var size = 512;
        var data = new Uint8Array(size * size * 4);
        var s = 12345;
         function rand() {
            s = (s * 1664525 + 1013904223) & 0xffffffff;
            return (s >>> 0) / 0xffffffff;
        }
        for (var i = 0; i < data.length; i++) {
            data[i] = (rand() * 255) | 0;
        }
        var tex = gl.createTexture();
        gl.bindTexture(gl.TEXTURE_2D, tex);
        gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, size, size, 0, gl.RGBA, gl.UNSIGNED_BYTE, data);
        gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.REPEAT);
        gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.REPEAT);
        gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
        gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR);
        return tex;
    }


    var VERT = [
         if (!baseImg) return;
         'attribute vec2 a_pos;',
        'varying vec2 v_uv;',
        'void main() {',
        '  v_uv = vec2(a_pos.x * 0.5 + 0.5, 0.5 - a_pos.y * 0.5);',
        '  gl_Position = vec4(a_pos, 0.0, 1.0);',
        '}'
    ].join('\n');


    var FRAG = [
        width = parseSliceWidth(frame.getAttribute('data-crt-slice-width'));
        'precision mediump float;',
        'uniform sampler2D u_tex;',
        'uniform sampler2D u_noise;',
        'uniform vec2 u_res;',
        'uniform vec2 u_imgSize;',
        'uniform float u_time;',
        'uniform vec2 u_noiseScale;',
        'varying vec2 v_uv;',


         'float sum2(vec2 v) { return v.x + v.y; }',
         resolveThumbUrl(baseImg, width, function (thumbUrl) {
        'float min2(vec2 v) { return min(v.x, v.y); }',
            if (!frame || !frame.parentNode) return;
         'float rgb2grey(vec3 v) { return dot(v, vec3(0.21, 0.72, 0.04)); }',
            applySliceImages(frame, thumbUrl);
         });
    }


        'vec2 coverUV(vec2 uv) {',
    function initBannerFrames(root) {
         '  float imgAR = u_imgSize.x / u_imgSize.y;',
         var scope = root && root.querySelectorAll ? root : document;
         '  float scrAR = u_res.x / u_res.y;',
         var frames = scope.querySelectorAll('.crt-page-monitor-frame');
        '  float scale = imgAR / scrAR;',
         var i;
        ' float offsetY = (1.0 - scale) * 0.5;',
         '  return vec2(uv.x, uv.y * scale + offsetY);',
        '}',


         'vec2 barrel(vec2 v, vec2 cc, float k) {',
         for (i = 0; i < frames.length; i++) {
        '  float ar = u_res.x / u_res.y;',
            initBannerFrame(frames[i]);
        '  vec2 c2 = cc;',
         }
        '  if (ar > 1.0) c2.x /= ar; else c2.y *= ar;',
    }
        '  float dist = dot(c2, c2) * k;',
         '  return v - cc * (1.0 + dist) * dist;',
        '}',


        'vec4 sampleInitialNoise(float t) {',
    $(function () {
         '  return texture2D(u_noise, vec2(fract(t/2048.0), fract(t/1048576.0)));',
         initBannerFrames(document);
        '}',
    });


         'vec4 sampleScreenNoise(vec2 uv) {',
    if (mw && mw.hook) {
        '  return texture2D(u_noise, u_noiseScale * uv);',
         mw.hook('wikipage.content').add(function ($content) {
         '}',
            initBannerFrames($content && $content[0] ? $content[0] : document);
         });
    }
})(jQuery, window.mw);


        'vec3 applyRgbShift(vec2 texUV, float shift) {',
/* =========================================
        '  vec2 d = vec2(shift, 0.0);',
   Doc Tab System — tab switching UI
        '  vec3 r = texture2D(u_tex, clamp(texUV + d, 0.0, 1.0)).rgb;',
   글리치 플리커 + RGB split + 방향 슬라이드
        '  vec3 c = texture2D(u_tex, texUV).rgb;',
   ========================================= */
        '  vec3 l = texture2D(u_tex, clamp(texUV - d, 0.0, 1.0)).rgb;',
        '  return vec3(',
        '   l.r*0.10 + r.r*0.30 + c.r*0.60,',
        '   l.g*0.20 + r.g*0.20 + c.g*0.60,',
        '   l.b*0.30 + r.b*0.10 + c.b*0.60',
        '  );',
        '}',


        'vec3 applyBloom(vec2 texUV, float strength) {',
(function () {
        '  vec2 px = 2.0 / u_res;',
    'use strict';
        '  vec3 acc = vec3(0.0);',
        '  acc += texture2D(u_tex, clamp(texUV + vec2( px.x,  0.0), 0.0, 1.0)).rgb;',
        '  acc += texture2D(u_tex, clamp(texUV + vec2(-px.x,  0.0), 0.0, 1.0)).rgb;',
        '  acc += texture2D(u_tex, clamp(texUV + vec2( 0.0,  px.y), 0.0, 1.0)).rgb;',
        '  acc += texture2D(u_tex, clamp(texUV + vec2( 0.0, -px.y), 0.0, 1.0)).rgb;',
        '  acc += texture2D(u_tex, clamp(texUV + vec2( px.x,  px.y), 0.0, 1.0)).rgb * 0.5;',
        '  acc += texture2D(u_tex, clamp(texUV + vec2(-px.x,  px.y), 0.0, 1.0)).rgb * 0.5;',
        '  acc += texture2D(u_tex, clamp(texUV + vec2( px.x, -px.y), 0.0, 1.0)).rgb * 0.5;',
        '  acc += texture2D(u_tex, clamp(texUV + vec2(-px.x, -px.y), 0.0, 1.0)).rgb * 0.5;',
        ' return acc / 6.0 * strength;',
        '}',


        'vec3 applyScanlines(vec2 uv, vec3 col) {',
    function initDocTabs() {
         '  float line = mod(uv.y * u_res.y, 2.0);',
         var tabBars = document.querySelectorAll('.doc-tab-bar');
        '  vec3 hi = ((1.0 + 0.30) - 0.2 * col) * col;',
         if (!tabBars.length) return;
         '  vec3 lo = ((1.0 - 0.30) + 0.1 * col) * col;',
        '  return line < 1.0 ? lo : hi;',
        '}',


'vec3 applyRasterization(vec2 uv, vec3 col) {',
        tabBars.forEach(function (bar) {
'  float t = u_time;',
            if (bar.getAttribute('data-tabs-init')) return;
'  vec2 noiseUV = uv + vec2(fract(t * 0.030), fract(t * 0.060));',
            bar.setAttribute('data-tabs-init', '1');
' float wobbleX = (texture2D(u_noise, noiseUV * 0.8).r - 0.5) * 0.0018;',
' float wobbleY = (texture2D(u_noise, noiseUV * 0.8 + 0.5).r - 0.5) * 0.0008;',
' vec2 wobbledUV = clamp(uv + vec2(wobbleX, wobbleY), 0.0, 1.0);',
'  vec3 wobbled = texture2D(u_tex, wobbledUV).rgb;',
'  return mix(col, wobbled, 0.35);',
'}',


        'float glowingLine(vec2 uv, float t) {',
            var tabs = Array.from(bar.querySelectorAll('.doc-tab'));
'  float pos = fract(t * 0.2);',
            if (!tabs.length) return;
'  float lineY = pos * (u_res.y + 330.0) - 120.0;',
        '  float y = uv.y * u_res.y;',
        '  return fract(smoothstep(-300.0, 0.0, y - lineY));',
        '}',


        'vec2 applyHSync(vec2 uv, vec4 noise, float strength) {',
            var panel = bar.closest('.doc-panel');
        ' float randval = strength - noise.r;',
            var display = panel ? panel.querySelector('.doc-display') : null;
        '  float scale = step(0.0, randval) * randval * strength;',
            if (!display) display = document.getElementById('doc-main-display');
        '  float freq = mix(4.0, 40.0, noise.g);',
            if (!display) return;
        '  uv.x += sin((uv.y + u_time * 0.001) * freq) * scale;',
        '  return uv;',
        '}',


        'void main() {',
            tabs.forEach(function (tab, i) {
        '  vec2 cc = vec2(0.5) - v_uv;',
                tab.addEventListener('click', function () {
                    var currentIdx = tabs.findIndex(function (t) {
                        return t.classList.contains('active');
                    });
                    if (currentIdx === i) return;
                    switchTab(tabs, display, i, i > currentIdx ? 1 : -1);
                });
            });


        ' float curvature = 0.18;',
            var initIdx = tabs.findIndex(function (t) { return t.classList.contains('active'); });
        '  vec2 uv = barrel(v_uv, cc, curvature);',
            if (initIdx !== -1) {
                var initRef = tabs[initIdx].dataset.ref;
                var initEl = initRef ? document.getElementById(initRef) : null;
                display.innerHTML = initEl ? initEl.innerHTML : (tabs[initIdx].dataset.content || '');
            }
        });


        '  float inScreen = min2(step(vec2(0.0), uv) - step(vec2(1.0), uv));',
    }
        '  if (inScreen < 0.5) { gl_FragColor = vec4(0.0, 0.0, 0.0, 1.0); return; }',


        '  vec2 texUV = clamp(coverUV(uv), 0.0, 1.0);',
    var isAnimating = false;


         '  vec4 initNoise = sampleInitialNoise(u_time);',
    function switchTab(tabs, display, nextIdx, dir) {
         '  vec4 screenNoise = sampleScreenNoise(uv);',
         if (isAnimating) return;
         isAnimating = true;


         '  texUV = applyHSync(texUV, initNoise, 0.006);',
         tabs.forEach(function (t) { t.classList.remove('active'); });
         '  texUV = clamp(texUV, 0.0, 1.0);',
         tabs[nextIdx].classList.add('active');


         '  texUV += (vec2(screenNoise.b, screenNoise.a) - 0.5) * 0.0006;',
         var ref = tabs[nextIdx].dataset.ref;
         '  texUV = clamp(texUV, 0.0, 1.0);',
        var nextContent;
 
         if (ref) {
        '  vec3 col = applyRgbShift(texUV, 0.003);',
            var refEl = document.getElementById(ref);
         '  col += applyBloom(texUV, 0.22);',
            nextContent = refEl ? refEl.innerHTML : '';
         } else {
            nextContent = tabs[nextIdx].dataset.content || '';
        }


         '  vec2 bpx = 1.5 / u_res;',
         glitchOut(display, dir, function () {
        '  vec3 blurCol = vec3(0.0);',
            display.innerHTML = nextContent;
        '  blurCol += texture2D(u_tex, clamp(texUV + vec2(-bpx.x, -bpx.y), 0.0, 1.0)).rgb;',
            glitchIn(display, dir, function () {
        '  blurCol += texture2D(u_tex, clamp(texUV + vec2( 0.0,  -bpx.y), 0.0, 1.0)).rgb;',
                isAnimating = false;
        '  blurCol += texture2D(u_tex, clamp(texUV + vec2( bpx.x, -bpx.y), 0.0, 1.0)).rgb;',
            });
        '  blurCol += texture2D(u_tex, clamp(texUV + vec2(-bpx.x, 0.0  ), 0.0, 1.0)).rgb;',
         });
        '  blurCol += texture2D(u_tex, clamp(texUV + vec2( bpx.x,  0.0  ), 0.0, 1.0)).rgb;',
    }
        '  blurCol += texture2D(u_tex, clamp(texUV + vec2(-bpx.x,  bpx.y), 0.0, 1.0)).rgb;',
        '  blurCol += texture2D(u_tex, clamp(texUV + vec2( 0.0,    bpx.y), 0.0, 1.0)).rgb;',
         '  blurCol += texture2D(u_tex, clamp(texUV + vec2( bpx.x,  bpx.y), 0.0, 1.0)).rgb;',
        '  col = mix(col, blurCol / 8.0, 0.40);',


        '  col = applyScanlines(uv, col);',
    function glitchOut(el, dir, cb) {
         '  col = applyRasterization(texUV, col);',
        var duration = 160;
        var start = null;
         var slideX = dir * 16;


         '  float glow = glowingLine(uv, u_time);',
         function step(ts) {
'  col += glow * 0.08 * vec3(0.85, 0.95, 1.0);',
            if (!start) start = ts;
            var p = Math.min((ts - start) / duration, 1);
            var ease = p * p;


        '  float dist = length(cc);',
            var tx = slideX * ease;
        '  col += screenNoise.a * 0.07 * (1.0 - dist * 1.3);',
            var skew = dir * ease * 1.0;
            var opacity = 1 - ease;
            var rgb = ease * 5;


        ' float grey = rgb2grey(col);',
            el.style.transform = 'translateX(' + tx + 'px) skewX(' + skew + 'deg)';
        ' vec3 phosphor = vec3(0.75, 0.88, 1.0);',
            el.style.opacity = opacity;
        ' col = mix(col, grey * phosphor, 0.35);',
            el.style.filter =
                'drop-shadow(' + (-rgb) + 'px 0 0 rgba(80,160,255,0.75)) ' +
                'drop-shadow(' + rgb + 'px 0 0 rgba(255,55,90,0.65)) ' +
                'brightness(' + (1 + ease * 0.25) + ')';


        '  vec2 vig = v_uv * (1.0 - v_uv);',
            if (p < 1) {
         '  col *= pow(vig.x * vig.y * 15.0, 0.25);',
                requestAnimationFrame(step);
            } else {
                el.style.opacity = '0';
                cb();
            }
        }
         requestAnimationFrame(step);
    }


         '  col *= 1.0 + (initNoise.g - 0.5) * 0.06;',
    function glitchIn(el, dir, cb) {
        var duration = 200;
        var start = null;
         var startX = -dir * 16;


         ' col += vec3(0.012) * (1.0 - dist) * (1.0 - dist);',
         el.style.transform = 'translateX(' + startX + 'px) skewX(' + (-dir * 1.0) + 'deg)';
        el.style.opacity = '0';


         '  col = pow(clamp(col, 0.0, 1.0), vec3(0.90));',
         function step(ts) {
            if (!start) start = ts;
            var p = Math.min((ts - start) / duration, 1);
            var ease = 1 - Math.pow(1 - p, 3);


        '  gl_FragColor = vec4(col, 1.0);',
            var tx = startX * (1 - ease);
        '}'
            var skew = -dir * 1.0 * (1 - ease);
    ].join('\n');
            var opacity = ease;
            var rgb = (1 - ease) * 3;
            var brightness = 1 + (1 - ease) * 0.35;


    function initCRTCanvas(screen, imgEl) {
            el.style.transform = 'translateX(' + tx + 'px) skewX(' + skew + 'deg)';
        var existing = screen.querySelector('.crt-webgl-canvas');
            el.style.opacity = opacity;
        if (existing) existing.remove();
            el.style.filter =
                'drop-shadow(' + (-rgb) + 'px 0 0 rgba(80,160,255,0.65)) ' +
                'drop-shadow(' + rgb + 'px 0 0 rgba(255,55,90,0.55)) ' +
                'brightness(' + brightness + ')';


        var canvas = document.createElement('canvas');
            if (p < 1) {
        canvas.className = 'crt-webgl-canvas';
                requestAnimationFrame(step);
        canvas.style.cssText = 'position:absolute;inset:0;width:100%;height:100%;z-index:19;pointer-events:none;display:block;';
            } else {
        screen.appendChild(canvas);
                el.style.transform = '';
 
                el.style.opacity = '';
        var gl = canvas.getContext('webgl') || canvas.getContext('experimental-webgl');
                el.style.filter = '';
        if (!gl) return;
                 cb();
 
        function compile(type, src) {
            var s = gl.createShader(type);
            gl.shaderSource(s, src);
            gl.compileShader(s);
            if (!gl.getShaderParameter(s, gl.COMPILE_STATUS)) {
                 console.error('[CRT shader]', gl.getShaderInfoLog(s));
             }
             }
            return s;
         }
         }
        requestAnimationFrame(step);
    }


        var prog = gl.createProgram();
if (document.readyState === 'loading') {
        gl.attachShader(prog, compile(gl.VERTEX_SHADER, VERT));
    document.addEventListener('DOMContentLoaded', initDocTabs);
        gl.attachShader(prog, compile(gl.FRAGMENT_SHADER, FRAG));
} else {
        gl.linkProgram(prog);
    initDocTabs();
        gl.useProgram(prog);
}


        var buf = gl.createBuffer();
if (typeof mw !== 'undefined' && mw.hook) {
        gl.bindBuffer(gl.ARRAY_BUFFER, buf);
    mw.hook('wikipage.content').add(function () {
        gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([-1,-1, 1,-1, -1,1, 1,1]), gl.STATIC_DRAW);
         initDocTabs();
         var aPos = gl.getAttribLocation(prog, 'a_pos');
    });
        gl.enableVertexAttribArray(aPos);
}
        gl.vertexAttribPointer(aPos, 2, gl.FLOAT, false, 0, 0);


        var uTex    = gl.getUniformLocation(prog, 'u_tex');
})();
        var uNoise  = gl.getUniformLocation(prog, 'u_noise');
 
        var uRes    = gl.getUniformLocation(prog, 'u_res');
/* =========================================
        var uImgSize = gl.getUniformLocation(prog, 'u_imgSize');
  Doc Section Switch — 좌측 섹션 전환
        var uTime   = gl.getUniformLocation(prog, 'u_time');
   ========================================= */
        var uNoiseSc = gl.getUniformLocation(prog, 'u_noiseScale');


        var imgTex = gl.createTexture();
$(document).on('click', '.doc-nav-item[data-section]', function () {
        gl.activeTexture(gl.TEXTURE0);
    var name = $(this).attr('data-section');
        gl.bindTexture(gl.TEXTURE_2D, imgTex);
    var display = document.getElementById('doc-main-display');
        gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
    var titleEl = document.getElementById('doc-center-title');
        gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
    var tabBar = document.getElementById('doc-tab-bar-text');
        gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
        gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR);


        gl.activeTexture(gl.TEXTURE1);
    if (!display) return;
        createNoiseTexture(gl);


        var texReady = false;
    $('.doc-nav-item[data-section]').removeClass('active');
        function uploadImg() {
    $('.doc-nav-item[data-section="' + name + '"]').addClass('active');
            if (!imgEl || !imgEl.complete || !imgEl.naturalWidth) return;
            try {
                gl.activeTexture(gl.TEXTURE0);
                gl.bindTexture(gl.TEXTURE_2D, imgTex);
                gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, imgEl);
                texReady = true;
            } catch(e) { console.error('[CRT] tex:', e); }
        }


         var lastW = 0, lastH = 0;
    if (name === 'text') {
         function resize() {
         if (titleEl) titleEl.textContent = '개요';
            var w = screen.offsetWidth, h = screen.offsetHeight;
         if (tabBar) $(tabBar).show();
            if (w === lastW && h === lastH) return;
        var activeTab = tabBar ? tabBar.querySelector('.doc-tab.active') : null;
             lastW = w; lastH = h;
        if (!activeTab && tabBar) activeTab = tabBar.querySelector('.doc-tab');
             canvas.width = w; canvas.height = h;
        if (activeTab) {
             gl.viewport(0, 0, w, h);
             var ref = activeTab.dataset.ref;
             var refEl = ref ? document.getElementById(ref) : null;
             display.innerHTML = refEl ? refEl.innerHTML : (activeTab.dataset.content || '');
         }
         }
    } else {
        if (titleEl) titleEl.textContent = name === 'factions' ? '세력' : name === 'people' ? '인물' : name;
        if (tabBar) $(tabBar).hide();
        var refEl = document.getElementById('doc-content-' + name);
        display.innerHTML = refEl ? refEl.innerHTML : '';
    }
});


        var raf;
/* =========================================
        var t0 = performance.now();
  CRT WebGL Renderer — cool-retro-term IBM DOS style
  ========================================= */
(function () {
    'use strict';


        function render() {
    function createNoiseTexture(gl) {
            raf = requestAnimationFrame(render);
        var size = 512;
            if (!texReady) { uploadImg(); return; }
        var data = new Uint8Array(size * size * 4);
            resize();
        var s = 12345;
             var t = (performance.now() - t0) / 1000;
        function rand() {
             gl.uniform1i(uTex, 0);
             s = (s * 1664525 + 1013904223) & 0xffffffff;
            gl.uniform1i(uNoise, 1);
             return (s >>> 0) / 0xffffffff;
            gl.uniform2f(uRes, canvas.width, canvas.height);
        }
             gl.uniform2f(uImgSize, imgEl.naturalWidth, imgEl.naturalHeight);
        for (var i = 0; i < data.length; i++) {
            gl.uniform1f(uTime, t);
             data[i] = (rand() * 255) | 0;
            gl.uniform2f(uNoiseSc, canvas.width / 512.0, canvas.height / 512.0);
            gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4);
         }
         }
        var tex = gl.createTexture();
        gl.bindTexture(gl.TEXTURE_2D, tex);
        gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, size, size, 0, gl.RGBA, gl.UNSIGNED_BYTE, data);
        gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.REPEAT);
        gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.REPEAT);
        gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
        gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR);
        return tex;
    }
    var VERT = [
        'attribute vec2 a_pos;',
        'varying vec2 v_uv;',
        'void main() {',
        '  v_uv = vec2(a_pos.x * 0.5 + 0.5, 0.5 - a_pos.y * 0.5);',
        '  gl_Position = vec4(a_pos, 0.0, 1.0);',
        '}'
    ].join('\n');


         if (imgEl.complete && imgEl.naturalWidth) { uploadImg(); }
    var FRAG = [
         else { imgEl.addEventListener('load', uploadImg); }
        'precision mediump float;',
        'uniform sampler2D u_tex;',
        'uniform sampler2D u_noise;',
        'uniform vec2 u_res;',
        'uniform vec2 u_imgSize;',
         'uniform float u_time;',
         'uniform vec2 u_noiseScale;',
        'varying vec2 v_uv;',


         render();
         'float sum2(vec2 v) { return v.x + v.y; }',
         screen._crtCleanup = function () { cancelAnimationFrame(raf); };
         'float min2(vec2 v) { return min(v.x, v.y); }',
    }
        'float rgb2grey(vec3 v) { return dot(v, vec3(0.21, 0.72, 0.04)); }',


    function initAllCRTScreens(root) {
        'vec2 coverUV(vec2 uv) {',
         var scope = root && root.querySelectorAll ? root : document;
         '  float imgAR = u_imgSize.x / u_imgSize.y;',
         scope.querySelectorAll('.crt-page-monitor-screen').forEach(function (screen) {
         '  float scrAR = u_res.x / u_res.y;',
            if (screen.getAttribute('data-crt-webgl') === '1') return;
        ' float scale = imgAR / scrAR;',
            screen.setAttribute('data-crt-webgl', '1');
        ' float offsetY = (1.0 - scale) * 0.5;',
            var frame = screen.closest('.crt-page-monitor-frame');
        ' return vec2(uv.x, uv.y * scale + offsetY);',
            if (!frame) return;
        '}',
            var imgEl = frame.querySelector('.crt-page-monitor-slice-img, .crt-page-monitor-image-base img');
            if (imgEl && imgEl.complete && imgEl.naturalWidth) {
                initCRTCanvas(screen, imgEl);
            } else if (imgEl) {
                imgEl.addEventListener('load', function () { initCRTCanvas(screen, imgEl); });
            } else {
                var obs = new MutationObserver(function () {
                    var img = frame.querySelector('.crt-page-monitor-slice-img');
                    if (!img) return;
                    obs.disconnect();
                    if (img.complete && img.naturalWidth) {
                        initCRTCanvas(screen, img);
                    } else {
                        img.addEventListener('load', function () { initCRTCanvas(screen, img); });
                    }
                });
                obs.observe(frame, { childList: true, subtree: true });
            }
        });
    }


    $(function () { initAllCRTScreens(document); });
        'vec2 barrel(vec2 v, vec2 cc, float k) {',
        '  float ar = u_res.x / u_res.y;',
        '  vec2 c2 = cc;',
        '  if (ar > 1.0) c2.x /= ar; else c2.y *= ar;',
        '  float dist = dot(c2, c2) * k;',
        '  return v - cc * (1.0 + dist) * dist;',
        '}',


    if (typeof mw !== 'undefined' && mw.hook) {
         'vec4 sampleInitialNoise(float t) {',
         mw.hook('wikipage.content').add(function ($c) {
        ' return texture2D(u_noise, vec2(fract(t/2048.0), fract(t/1048576.0)));',
            document.querySelectorAll('.crt-page-monitor-screen').forEach(function (s) {
         '}',
                if (s._crtCleanup) s._crtCleanup();
                s.removeAttribute('data-crt-webgl');
            });
            initAllCRTScreens($c && $c[0] ? $c[0] : document);
         });
    }
})();


/* =========================================
        'vec4 sampleScreenNoise(vec2 uv) {',
Progress System UI
        '  return texture2D(u_noise, u_noiseScale * uv);',
MediaWiki:Common.js controlled frontend
        '}',
========================================= */
(function (mw, $) {
    'use strict';


    if (window.ProgressSystemWebUiInitialized) return;
        'vec3 applyRgbShift(vec2 texUV, float shift) {',
    window.ProgressSystemWebUiInitialized = true;
        '  vec2 d = vec2(shift, 0.0);',
        '  vec3 r = texture2D(u_tex, clamp(texUV + d, 0.0, 1.0)).rgb;',
        '  vec3 c = texture2D(u_tex, texUV).rgb;',
        '  vec3 l = texture2D(u_tex, clamp(texUV - d, 0.0, 1.0)).rgb;',
        '  return vec3(',
        '    l.r*0.10 + r.r*0.30 + c.r*0.60,',
        '    l.g*0.20 + r.g*0.20 + c.g*0.60,',
        '    l.b*0.30 + r.b*0.10 + c.b*0.60',
        '  );',
        '}',


    var api = null;
        'vec3 applyBloom(vec2 texUV, float strength) {',
 
        '  vec2 px = 2.0 / u_res;',
    function withApi(done, fail) {
        '  vec3 acc = vec3(0.0);',
         if (api) {
        '  acc += texture2D(u_tex, clamp(texUV + vec2( px.x,  0.0), 0.0, 1.0)).rgb;',
            done(api);
        '  acc += texture2D(u_tex, clamp(texUV + vec2(-px.x,  0.0), 0.0, 1.0)).rgb;',
            return;
        '  acc += texture2D(u_tex, clamp(texUV + vec2( 0.0,  px.y), 0.0, 1.0)).rgb;',
         }
        '  acc += texture2D(u_tex, clamp(texUV + vec2( 0.0, -px.y), 0.0, 1.0)).rgb;',
        '  acc += texture2D(u_tex, clamp(texUV + vec2( px.x,  px.y), 0.0, 1.0)).rgb * 0.5;',
        '  acc += texture2D(u_tex, clamp(texUV + vec2(-px.x,  px.y), 0.0, 1.0)).rgb * 0.5;',
         '  acc += texture2D(u_tex, clamp(texUV + vec2( px.x, -px.y), 0.0, 1.0)).rgb * 0.5;',
        '  acc += texture2D(u_tex, clamp(texUV + vec2(-px.x, -px.y), 0.0, 1.0)).rgb * 0.5;',
        '  return acc / 6.0 * strength;',
         '}',


         if (!mw.loader || typeof mw.loader.using !== 'function') {
         'vec3 applyScanlines(vec2 uv, vec3 col) {',
            if (typeof fail === 'function') fail();
        '  float line = mod(uv.y * u_res.y, 2.0);',
            return;
        ' vec3 hi = ((1.0 + 0.30) - 0.2 * col) * col;',
         }
        '  vec3 lo = ((1.0 - 0.30) + 0.1 * col) * col;',
        '  return line < 1.0 ? lo : hi;',
         '}',


        mw.loader.using(['mediawiki.api']).then(function () {
'vec3 applyRasterization(vec2 uv, vec3 col) {',
            api = new mw.Api();
'  float t = u_time;',
            done(api);
'  vec2 noiseUV = uv + vec2(fract(t * 0.030), fract(t * 0.060));',
        }, function () {
'  float wobbleX = (texture2D(u_noise, noiseUV * 0.8).r - 0.5) * 0.0018;',
            if (typeof fail === 'function') fail();
'  float wobbleY = (texture2D(u_noise, noiseUV * 0.8 + 0.5).r - 0.5) * 0.0008;',
        });
'  vec2 wobbledUV = clamp(uv + vec2(wobbleX, wobbleY), 0.0, 1.0);',
    }
' vec3 wobbled = texture2D(u_tex, wobbledUV).rgb;',
'  return mix(col, wobbled, 0.35);',
'}',


    var inFlightPageIds = new Set();
        'float glowingLine(vec2 uv, float t) {',
    var handledPageIds = new Set();
'  float pos = fract(t * 0.2);',
    var notificationQueue = [];
'  float lineY = pos * (u_res.y + 330.0) - 120.0;',
    var notificationActive = false;
        '  float y = uv.y * u_res.y;',
    var summaryRequested = false;
        '  return fract(smoothstep(-300.0, 0.0, y - lineY));',
    var currentSummary = null;
        '}',
    var pendingSummary = null;
    var pendingOptions = null;
    var visibilityBound = false;
    var barTimerA = null;
    var barTimerB = null;
    var barTimerC = null;
    var summaryRetryTimer = null;
    var summaryRetryAttempts = 0;


    function isLoggedIn() {
        'vec2 applyHSync(vec2 uv, vec4 noise, float strength) {',
         return !!mw.config.get('wgUserName');
         '  float randval = strength - noise.r;',
    }
        ' float scale = step(0.0, randval) * randval * strength;',
 
        '  float freq = mix(4.0, 40.0, noise.g);',
    function getPageId() {
         '  uv.x += sin((uv.y + u_time * 0.001) * freq) * scale;',
         var id = parseInt(mw.config.get('wgArticleId') || 0, 10);
         return uv;',
         return Number.isFinite(id) ? id : 0;
        '}',
    }


    function isRewardableClientSide() {
        'void main() {',
        if (!isLoggedIn()) return false;
         ' vec2 cc = vec2(0.5) - v_uv;',
        if (parseInt(mw.config.get('wgNamespaceNumber'), 10) !== 0) return false;
         if (mw.config.get('wgIsMainPage')) return false;
        if (getPageId() <= 0) return false;
        return true;
    }


    function getPanelHtml() {
         ' float curvature = 0.18;',
         return '' +
        ' vec2 uv = barrel(v_uv, cc, curvature);',
            '<div id="progress-panel" class="profile-progress-block is-syncing" aria-live="polite" data-progress-state="syncing">' +
                '<div class="progress-title-row" hidden></div>' +
                '<div class="progress-level-row">' +
                    '<span class="progress-level-label">SYNC</span>' +
                    '<span class="progress-total-xp">— XP</span>' +
                '</div>' +
                '<div class="progress-xp-bar" aria-hidden="true">' +
                    '<div class="progress-xp-gain"></div>' +
                    '<div class="progress-xp-fill"></div>' +
                '</div>' +
                '<div class="progress-sub-row">' +
                    '<span class="progress-xp-next">SYNCING</span>' +
                    '<span class="progress-daily-xp">TODAY —</span>' +
                '</div>' +
                '<div class="progress-discovery-row">DISCOVERED —</div>' +
            '</div>';
    }


    function getDividerHtml() {
        '  float inScreen = min2(step(vec2(0.0), uv) - step(vec2(1.0), uv));',
         /* 프로필 패널 최신 규칙: 레벨 패널과 버튼 영역 사이에 별도 나눔선은 만들지 않는다. */
         '  if (inScreen < 0.5) { gl_FragColor = vec4(0.0, 0.0, 0.0, 1.0); return; }',
        return '';
    }


    function setPanelSync($panel) {
        '  vec2 texUV = clamp(coverUV(uv), 0.0, 1.0);',
        if (!$panel || !$panel.length) return;


         $panel.addClass('is-syncing').removeClass('is-max-level').attr('data-progress-state', 'syncing');
         ' vec4 initNoise = sampleInitialNoise(u_time);',
        $panel.find('.progress-title-row').text('').prop('hidden', true);
         ' vec4 screenNoise = sampleScreenNoise(uv);',
         $panel.find('.progress-level-label').text('SYNC');
        $panel.find('.progress-total-xp').text('— XP');
        $panel.find('.progress-xp-next').text('SYNCING');
        $panel.find('.progress-daily-xp').text('TODAY —');
        $panel.find('.progress-discovery-row').text('DISCOVERED —');
        $panel.find('.progress-xp-fill').css({ transition: 'none', width: '0%' });
        $panel.find('.progress-xp-gain').css({ transition: 'none', width: '0%', opacity: 0 });
    }


    function placePanel($panel) {
         '  texUV = applyHSync(texUV, initNoise, 0.006);',
         var $right = $('#clbi-right-sidebar');
         '  texUV = clamp(texUV, 0.0, 1.0);',
         if (!$right.length) return false;


         var $userBox = $right.children('.clbi-right-box').first();
         '  texUV += (vec2(screenNoise.b, screenNoise.a) - 0.5) * 0.0006;',
         if (!$userBox.length) return false;
         '  texUV = clamp(texUV, 0.0, 1.0);',


         var $buttonArea = $userBox.children('.clbi-right-content').first();
         '  vec3 col = applyRgbShift(texUV, 0.003);',
         var $oldFallback = $panel.closest('.progress-panel-fallback');
         '  col += applyBloom(texUV, 0.22);',


         if ($buttonArea.length) {
         '  vec2 bpx = 1.5 / u_res;',
            var $divider = $('#profile-progress-divider');
        '  vec3 blurCol = vec3(0.0);',
        '  blurCol += texture2D(u_tex, clamp(texUV + vec2(-bpx.x, -bpx.y), 0.0, 1.0)).rgb;',
        '  blurCol += texture2D(u_tex, clamp(texUV + vec2( 0.0,  -bpx.y), 0.0, 1.0)).rgb;',
        '  blurCol += texture2D(u_tex, clamp(texUV + vec2( bpx.x, -bpx.y), 0.0, 1.0)).rgb;',
        '  blurCol += texture2D(u_tex, clamp(texUV + vec2(-bpx.x,  0.0  ), 0.0, 1.0)).rgb;',
        '  blurCol += texture2D(u_tex, clamp(texUV + vec2( bpx.x,  0.0  ), 0.0, 1.0)).rgb;',
        '  blurCol += texture2D(u_tex, clamp(texUV + vec2(-bpx.x,  bpx.y), 0.0, 1.0)).rgb;',
        '  blurCol += texture2D(u_tex, clamp(texUV + vec2( 0.0,    bpx.y), 0.0, 1.0)).rgb;',
        '  blurCol += texture2D(u_tex, clamp(texUV + vec2( bpx.x,  bpx.y), 0.0, 1.0)).rgb;',
        '  col = mix(col, blurCol / 8.0, 0.40);',


            $panel.insertBefore($buttonArea);
        '  col = applyScanlines(uv, col);',
        '  col = applyRasterization(texUV, col);',


            if (!$divider.length) {
        '  float glow = glowingLine(uv, u_time);',
                $divider = $(getDividerHtml());
'  col += glow * 0.08 * vec3(0.85, 0.95, 1.0);',
            }


            $divider.insertAfter($panel);
        '  float dist = length(cc);',
         } else {
         ' col += screenNoise.a * 0.07 * (1.0 - dist * 1.3);',
            $('#profile-progress-divider').remove();
            $userBox.append($panel);
        }


         if ($oldFallback.length && !$oldFallback.find('#progress-panel').length) {
         '  float grey = rgb2grey(col);',
            $oldFallback.remove();
        '  vec3 phosphor = vec3(0.75, 0.88, 1.0);',
        }
        '  col = mix(col, grey * phosphor, 0.35);',


         return true;
         '  vec2 vig = v_uv * (1.0 - v_uv);',
    }
        '  col *= pow(vig.x * vig.y * 15.0, 0.25);',


    function ensurePanel() {
         '  col *= 1.0 + (initNoise.g - 0.5) * 0.06;',
         if (!isLoggedIn()) return $();


         var $right = $('#clbi-right-sidebar');
         '  col += vec3(0.012) * (1.0 - dist) * (1.0 - dist);',
        if (!$right.length) return $();


         var $panel = $('#progress-panel');
         '  col = pow(clamp(col, 0.0, 1.0), vec3(0.90));',


         if (!$panel.length) {
         '  gl_FragColor = vec4(col, 1.0);',
            $panel = $(getPanelHtml());
         '}'
            if (!placePanel($panel)) return $();
    ].join('\n');
            setPanelSync($panel);
         } else {
            $panel.addClass('profile-progress-block');
            placePanel($panel);


            if (!currentSummary && $panel.attr('data-progress-state') !== 'syncing') {
    function initCRTCanvas(screen, imgEl) {
                setPanelSync($panel);
        var existing = screen.querySelector('.crt-webgl-canvas');
            }
        if (existing) existing.remove();
        }


         return $('#progress-panel');
         var canvas = document.createElement('canvas');
    }
        canvas.className = 'crt-webgl-canvas';
        canvas.style.cssText = 'position:absolute;inset:0;width:100%;height:100%;z-index:19;pointer-events:none;display:block;';
        screen.appendChild(canvas);


    function clampPercent(value) {
         var gl = canvas.getContext('webgl') || canvas.getContext('experimental-webgl');
         return Math.max(0, Math.min(100, value || 0));
        if (!gl) return;
    }


    function hasXpNotification(items) {
        function compile(type, src) {
        if (!items || !items.length) return false;
            var s = gl.createShader(type);
        return items.some(function (item) {
            gl.shaderSource(s, src);
            return item && item.type === 'xp' && parseInt(item.amount || 0, 10) > 0;
            gl.compileShader(s);
        });
            if (!gl.getShaderParameter(s, gl.COMPILE_STATUS)) {
    }
                console.error('[CRT shader]', gl.getShaderInfoLog(s));
            }
            return s;
        }


    function clearBarTimers() {
        var prog = gl.createProgram();
         [barTimerA, barTimerB, barTimerC].forEach(function (timer) {
         gl.attachShader(prog, compile(gl.VERTEX_SHADER, VERT));
            if (timer) clearTimeout(timer);
        gl.attachShader(prog, compile(gl.FRAGMENT_SHADER, FRAG));
         });
         gl.linkProgram(prog);
         barTimerA = null;
         gl.useProgram(prog);
        barTimerB = null;
        barTimerC = null;
    }


    function setBarInstant($fill, $gain, percent) {
         var buf = gl.createBuffer();
        clearBarTimers();
         gl.bindBuffer(gl.ARRAY_BUFFER, buf);
         percent = clampPercent(percent);
         gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([-1,-1, 1,-1, -1,1, 1,1]), gl.STATIC_DRAW);
         $fill.css({ transition: 'none', width: percent + '%' });
         var aPos = gl.getAttribLocation(prog, 'a_pos');
         $gain.css({ transition: 'none', left: '0%', width: '0%', opacity: 0 });
         gl.enableVertexAttribArray(aPos);
         if ($fill[0]) $fill[0].offsetHeight;
         gl.vertexAttribPointer(aPos, 2, gl.FLOAT, false, 0, 0);
         $fill.css({ transition: '' });
         $gain.css({ transition: '' });
    }


     function animateGain($fill, $gain, fromPercent, toPercent, levelChanged) {
        var uTex     = gl.getUniformLocation(prog, 'u_tex');
         clearBarTimers();
        var uNoise  = gl.getUniformLocation(prog, 'u_noise');
        var uRes    = gl.getUniformLocation(prog, 'u_res');
        var uImgSize = gl.getUniformLocation(prog, 'u_imgSize');
        var uTime    = gl.getUniformLocation(prog, 'u_time');
         var uNoiseSc = gl.getUniformLocation(prog, 'u_noiseScale');


         fromPercent = clampPercent(fromPercent);
         var imgTex = gl.createTexture();
         toPercent = clampPercent(toPercent);
        gl.activeTexture(gl.TEXTURE0);
 
        gl.bindTexture(gl.TEXTURE_2D, imgTex);
         $fill.css({ transition: 'none', width: fromPercent + '%' });
        gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
         gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
        gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
         gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR);


         if (levelChanged) {
         gl.activeTexture(gl.TEXTURE1);
            var firstDelta = Math.max(0, 100 - fromPercent);
        createNoiseTexture(gl);


             $gain.css({
        var texReady = false;
                 transition: 'none',
        function uploadImg() {
                 opacity: firstDelta > 0 ? 1 : 0,
            if (!imgEl || !imgEl.complete || !imgEl.naturalWidth) return;
                left: fromPercent + '%',
             try {
                 width: firstDelta + '%'
                gl.activeTexture(gl.TEXTURE0);
             });
                 gl.bindTexture(gl.TEXTURE_2D, imgTex);
                 gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, imgEl);
                 texReady = true;
             } catch(e) { console.error('[CRT] tex:', e); }
        }


             if ($fill[0]) $fill[0].offsetHeight;
        var lastW = 0, lastH = 0;
        function resize() {
            var w = screen.offsetWidth, h = screen.offsetHeight;
             if (w === lastW && h === lastH) return;
            lastW = w; lastH = h;
            canvas.width = w; canvas.height = h;
            gl.viewport(0, 0, w, h);
        }


            barTimerA = setTimeout(function () {
        var raf;
                $fill.css({
        var visualTime = 0;
                    transition: 'width 540ms cubic-bezier(0.22, 0.7, 0.18, 1)',
        var lastVisualNow = 0;
                    width: '100%'
                });
            }, 260);


            barTimerB = setTimeout(function () {
        function render(now) {
                $fill.css({ transition: 'none', width: '0%' });
            var delta;
                $gain.css({ transition: 'none', opacity: toPercent > 0 ? 1 : 0, left: '0%', width: toPercent + '%' });
            raf = requestAnimationFrame(render);


                if ($fill[0]) $fill[0].offsetHeight;
            if (!lastVisualNow) lastVisualNow = now;
            delta = Math.max(0, Math.min(100, now - lastVisualNow));
            lastVisualNow = now;


                $fill.css({
            if (document.hidden || isClbiCompositorBusy()) return;
                    transition: 'width 460ms cubic-bezier(0.22, 0.7, 0.18, 1)',
             if (!texReady) { uploadImg(); return; }
                    width: toPercent + '%'
                });
             }, 860);


             barTimerC = setTimeout(function () {
             visualTime += delta;
                $gain.css({ transition: 'opacity 180ms ease', opacity: 0 });
            resize();
             }, 1380);
            var t = visualTime / 1000;
 
            gl.uniform1i(uTex, 0);
             return;
            gl.uniform1i(uNoise, 1);
            gl.uniform2f(uRes, canvas.width, canvas.height);
            gl.uniform2f(uImgSize, imgEl.naturalWidth, imgEl.naturalHeight);
             gl.uniform1f(uTime, t);
            gl.uniform2f(uNoiseSc, canvas.width / 512.0, canvas.height / 512.0);
             gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4);
         }
         }


         var delta = Math.max(0, toPercent - fromPercent);
         if (imgEl.complete && imgEl.naturalWidth) { uploadImg(); }
        else { imgEl.addEventListener('load', uploadImg); }


         if (delta <= 0.15) {
         render();
            setBarInstant($fill, $gain, toPercent);
        screen._crtCleanup = function () { cancelAnimationFrame(raf); };
            return;
    }
        }


         $gain.css({
    function initAllCRTScreens(root) {
             transition: 'none',
        var scope = root && root.querySelectorAll ? root : document;
             opacity: 1,
         scope.querySelectorAll('.crt-page-monitor-screen').forEach(function (screen) {
             left: fromPercent + '%',
             if (screen.getAttribute('data-crt-webgl') === '1') return;
             width: delta + '%'
            screen.setAttribute('data-crt-webgl', '1');
            var frame = screen.closest('.crt-page-monitor-frame');
            if (!frame) return;
            var imgEl = frame.querySelector('.crt-page-monitor-slice-img, .crt-page-monitor-image-base img');
             if (imgEl && imgEl.complete && imgEl.naturalWidth) {
                initCRTCanvas(screen, imgEl);
             } else if (imgEl) {
                imgEl.addEventListener('load', function () { initCRTCanvas(screen, imgEl); });
             } else {
                var obs = new MutationObserver(function () {
                    var img = frame.querySelector('.crt-page-monitor-slice-img');
                    if (!img) return;
                    obs.disconnect();
                    if (img.complete && img.naturalWidth) {
                        initCRTCanvas(screen, img);
                    } else {
                        img.addEventListener('load', function () { initCRTCanvas(screen, img); });
                    }
                });
                obs.observe(frame, { childList: true, subtree: true });
            }
         });
         });
    }


        if ($fill[0]) $fill[0].offsetHeight;
    $(function () { initAllCRTScreens(document); });


         barTimerA = setTimeout(function () {
    if (typeof mw !== 'undefined' && mw.hook) {
             $fill.css({
         mw.hook('wikipage.content').add(function ($c) {
                 transition: 'width 560ms cubic-bezier(0.22, 0.7, 0.18, 1)',
             document.querySelectorAll('.crt-page-monitor-screen').forEach(function (s) {
                 width: toPercent + '%'
                 if (s._crtCleanup) s._crtCleanup();
                 s.removeAttribute('data-crt-webgl');
             });
             });
        }, 260);
            initAllCRTScreens($c && $c[0] ? $c[0] : document);
 
         });
        barTimerB = setTimeout(function () {
            $gain.css({ transition: 'opacity 180ms ease', opacity: 0 });
         }, 940);
     }
     }
})();


    function updatePanel(summary, options) {
/* =========================================
        if (!summary) return;
Progress System UI
MediaWiki:Common.js controlled frontend
========================================= */
(function (mw, $) {
    'use strict';


        options = options || {};
    if (window.ProgressSystemWebUiInitialized) return;
    window.ProgressSystemWebUiInitialized = true;


        var $panel = ensurePanel();
    var api = null;
         if (!$panel.length) {
 
             pendingSummary = $.extend({}, summary);
    function withApi(done, fail) {
            pendingOptions = $.extend({}, options);
         if (api) {
             done(api);
             return;
             return;
         }
         }


         var level = summary.level || 1;
         if (!mw.loader || typeof mw.loader.using !== 'function') {
        var totalXp = summary.totalXp || 0;
            if (typeof fail === 'function') fail();
        var xpIntoLevel = summary.xpIntoLevel || 0;
            return;
        var xpForNext = summary.xpForNextLevel || 1;
         }
        var percent = clampPercent(summary.progressPercent);
        var isMaxLevel = !!summary.isMaxLevel;
        var dailyXp = summary.dailyXp || 0;
        var discoveries = summary.discoveryCount || 0;
         var title = summary.equippedTitle || summary.title || '';


         $panel.removeClass('is-syncing').toggleClass('is-max-level', isMaxLevel).attr('data-progress-state', 'ready');
         mw.loader.using(['mediawiki.api']).then(function () {
        $panel.find('.progress-level-label').text((isMaxLevel ? 'MAX ' : 'LVL ') + level);
            api = new mw.Api();
        $panel.find('.progress-total-xp').text(totalXp + ' XP');
            done(api);
         $panel.find('.progress-xp-next').text(isMaxLevel ? 'MAX LEVEL' : (xpIntoLevel + ' / ' + xpForNext + ' TO NEXT'));
         }, function () {
        $panel.find('.progress-daily-xp').text('TODAY ' + dailyXp + ' XP');
            if (typeof fail === 'function') fail();
         $panel.find('.progress-discovery-row').text('DISCOVERED ' + discoveries);
         });
    }


        var $title = $panel.find('.progress-title-row');
    var inFlightPageIds = new Set();
        if (title) {
    var handledPageIds = new Set();
            $title.text(title).prop('hidden', false);
    var notificationQueue = [];
        } else {
    var notificationActive = false;
            $title.text('').prop('hidden', true);
    var summaryRequested = false;
        }
    var currentSummary = null;
    var pendingSummary = null;
    var pendingOptions = null;
    var visibilityBound = false;
    var barTimerA = null;
    var barTimerB = null;
    var barTimerC = null;
    var summaryRetryTimer = null;
    var summaryRetryAttempts = 0;


        var $fill = $panel.find('.progress-xp-fill');
    function isLoggedIn() {
         var $gain = $panel.find('.progress-xp-gain');
         return !!mw.config.get('wgUserName');
        var animate = !!options.animateGain && currentSummary && totalXp > (currentSummary.totalXp || 0);
    }


        if (animate) {
    function getPageId() {
            animateGain(
        var id = parseInt(mw.config.get('wgArticleId') || 0, 10);
                $fill,
         return Number.isFinite(id) ? id : 0;
                $gain,
    }
                clampPercent(currentSummary.progressPercent),
                percent,
                level !== (currentSummary.level || 1)
            );
         } else {
            setBarInstant($fill, $gain, percent);
        }


         currentSummary = $.extend({}, summary);
    function isRewardableClientSide() {
         pendingSummary = null;
         if (!isLoggedIn()) return false;
         pendingOptions = null;
         if (parseInt(mw.config.get('wgNamespaceNumber'), 10) !== 0) return false;
         if (summaryRetryTimer) {
         if (mw.config.get('wgIsMainPage')) return false;
            clearTimeout(summaryRetryTimer);
         if (getPageId() <= 0) return false;
            summaryRetryTimer = null;
         return true;
         }
        summaryRetryAttempts = 0;
     }
     }


     function clearSummaryRetry() {
     function getPanelHtml() {
         if (summaryRetryTimer) clearTimeout(summaryRetryTimer);
         return '' +
        summaryRetryTimer = null;
            '<div id="progress-panel" class="profile-progress-block is-syncing" aria-live="polite" data-progress-state="syncing">' +
        summaryRetryAttempts = 0;
                '<div class="progress-title-row" hidden></div>' +
                '<div class="progress-level-row">' +
                    '<span class="progress-level-label">SYNC</span>' +
                    '<span class="progress-total-xp">— XP</span>' +
                '</div>' +
                '<div class="progress-xp-bar" aria-hidden="true">' +
                    '<div class="progress-xp-gain"></div>' +
                    '<div class="progress-xp-fill"></div>' +
                '</div>' +
                '<div class="progress-sub-row">' +
                    '<span class="progress-xp-next">SYNCING</span>' +
                    '<span class="progress-daily-xp">TODAY —</span>' +
                '</div>' +
                '<div class="progress-discovery-row">DISCOVERED —</div>' +
            '</div>';
     }
     }


     function scheduleSummaryRetry(delay) {
     function getDividerHtml() {
         if (!isLoggedIn()) return;
         /* 프로필 패널 최신 규칙: 레벨 패널과 버튼 영역 사이에 별도 나눔선은 만들지 않는다. */
        if (summaryRetryTimer) return;
         return '';
         if (summaryRetryAttempts >= 12) return;
 
        summaryRetryAttempts += 1;
        summaryRetryTimer = setTimeout(function () {
            summaryRetryTimer = null;
            requestSummary();
        }, delay || 1800);
     }
     }


     function requestSummary() {
     function setPanelSync($panel) {
         if (!isLoggedIn()) return;
         if (!$panel || !$panel.length) return;
        if (summaryRequested) return;


         summaryRequested = true;
         $panel.addClass('is-syncing').removeClass('is-max-level').attr('data-progress-state', 'syncing');
 
        $panel.find('.progress-title-row').text('').prop('hidden', true);
        withApi(function (api) {
        $panel.find('.progress-level-label').text('SYNC');
            api.get({
        $panel.find('.progress-total-xp').text('— XP');
                action: 'progress_summary',
        $panel.find('.progress-xp-next').text('SYNCING');
                format: 'json',
        $panel.find('.progress-daily-xp').text('TODAY —');
                formatversion: 2
        $panel.find('.progress-discovery-row').text('DISCOVERED —');
            }).then(function (data) {
         $panel.find('.progress-xp-fill').css({ transition: 'none', width: '0%' });
                var payload = data && data.progress_summary;
        $panel.find('.progress-xp-gain').css({ transition: 'none', width: '0%', opacity: 0 });
                if (payload && payload.available && payload.summary) {
                    clearSummaryRetry();
                    updatePanel(payload.summary, { animateGain: false });
                } else {
                    scheduleSummaryRetry(2200);
                }
            }).catch(function () {
                scheduleSummaryRetry(2200);
            }).always(function () {
                summaryRequested = false;
            });
         }, function () {
            summaryRequested = false;
            scheduleSummaryRetry(2200);
        });
     }
     }


     function queueNotifications(items) {
     function placePanel($panel) {
         if (!items || !items.length) return;
        var $right = $('#clbi-right-sidebar');
         if (!$right.length) return false;
 
        var $userBox = $right.children('.clbi-right-box').first();
        if (!$userBox.length) return false;
 
        var $buttonArea = $userBox.children('.clbi-right-content').first();
        var $oldFallback = $panel.closest('.progress-panel-fallback');


         items.forEach(function (item) {
         if ($buttonArea.length) {
             if (!item) return;
             var $divider = $('#profile-progress-divider');
            notificationQueue.push(item);
        });


        showNextNotification();
            $panel.insertBefore($buttonArea);
    }


    function notificationText(item) {
            if (!$divider.length) {
        if (item.type === 'xp') {
                $divider = $(getDividerHtml());
            return '+' + (item.amount || 0) + ' XP · ' + (item.label || '문서 열람');
            }
        }


        if (item.type === 'achievement') {
            $divider.insertAfter($panel);
             var xp = item.amount ? ' · +' + item.amount + ' XP' : '';
        } else {
             return '업적 달성 · ' + (item.label || '새 업적') + xp;
             $('#profile-progress-divider').remove();
             $userBox.append($panel);
         }
         }


         if (item.type === 'level') {
         if ($oldFallback.length && !$oldFallback.find('#progress-panel').length) {
             return item.label || '레벨 상승';
             $oldFallback.remove();
         }
         }


         return item.label || '보상 획득';
         return true;
     }
     }


     function showNextNotification() {
     function ensurePanel() {
         if (notificationActive) return;
         if (!isLoggedIn()) return $();
        if (!notificationQueue.length) return;


        notificationActive = true;
         var $right = $('#clbi-right-sidebar');
         var item = notificationQueue.shift();
         if (!$right.length) return $();
         var $root = $('#progress-toast-root');


         if (!$root.length) {
         var $panel = $('#progress-panel');
            $('body').append('<div id="progress-toast-root"></div>');
            $root = $('#progress-toast-root');
        }


         var $toast = $('<div class="progress-toast"></div>');
         if (!$panel.length) {
        $toast.text(notificationText(item));
            $panel = $(getPanelHtml());
         $root.append($toast);
            if (!placePanel($panel)) return $();
            setPanelSync($panel);
         } else {
            $panel.addClass('profile-progress-block');
            placePanel($panel);


        requestAnimationFrame(function () {
            if (!currentSummary && $panel.attr('data-progress-state') !== 'syncing') {
            $toast.addClass('is-visible');
                setPanelSync($panel);
         });
            }
         }


         setTimeout(function () {
         return $('#progress-panel');
            $toast.removeClass('is-visible');
            setTimeout(function () {
                $toast.remove();
                notificationActive = false;
                showNextNotification();
            }, 220);
        }, 2600);
     }
     }


     function applyPendingSummaryIfPossible() {
     function clampPercent(value) {
         if (!pendingSummary) return;
         return Math.max(0, Math.min(100, value || 0));
        updatePanel(pendingSummary, pendingOptions || { animateGain: false });
     }
     }


     function handlePageView() {
     function hasXpNotification(items) {
         ensurePanel();
         if (!items || !items.length) return false;
         applyPendingSummaryIfPossible();
         return items.some(function (item) {
            return item && item.type === 'xp' && parseInt(item.amount || 0, 10) > 0;
        });
    }


         if (!isRewardableClientSide()) {
    function clearBarTimers() {
             requestSummary();
         [barTimerA, barTimerB, barTimerC].forEach(function (timer) {
            return;
             if (timer) clearTimeout(timer);
         }
        });
        barTimerA = null;
        barTimerB = null;
         barTimerC = null;
    }


         var pageId = getPageId();
    function setBarInstant($fill, $gain, percent) {
         if (handledPageIds.has(pageId)) {
         clearBarTimers();
            requestSummary();
        percent = clampPercent(percent);
            return;
         $fill.css({ transition: 'none', width: percent + '%' });
        }
        $gain.css({ transition: 'none', left: '0%', width: '0%', opacity: 0 });
        if ($fill[0]) $fill[0].offsetHeight;
        $fill.css({ transition: '' });
        $gain.css({ transition: '' });
    }


        if (inFlightPageIds.has(pageId)) {
    function animateGain($fill, $gain, fromPercent, toPercent, levelChanged) {
            requestSummary();
        clearBarTimers();
            return;
        }


         inFlightPageIds.add(pageId);
         fromPercent = clampPercent(fromPercent);
        toPercent = clampPercent(toPercent);


         withApi(function (api) {
         $fill.css({ transition: 'none', width: fromPercent + '%' });
            api.postWithToken('csrf', {
                action: 'progress_view',
                format: 'json',
                formatversion: 2,
                errorformat: 'plaintext',
                pageid: pageId
            }).then(function (data) {
                var payload = data && data.progress_view;
                if (!payload) return;


                handledPageIds.add(pageId);
        if (levelChanged) {
            var firstDelta = Math.max(0, 100 - fromPercent);


                var animate = hasXpNotification(payload.notifications);
            $gain.css({
 
                transition: 'none',
                if (payload.summary) {
                 opacity: firstDelta > 0 ? 1 : 0,
                    updatePanel(payload.summary, { animateGain: animate });
                 left: fromPercent + '%',
                 }
                 width: firstDelta + '%'
 
                 if (payload.notifications && payload.notifications.length) {
                    queueNotifications(payload.notifications);
                 }
            }).catch(function () {
                requestSummary();
            }).always(function () {
                inFlightPageIds.delete(pageId);
             });
             });
        }, function () {
            inFlightPageIds.delete(pageId);
            requestSummary();
        });
    }


    function bindVisibilitySync() {
            if ($fill[0]) $fill[0].offsetHeight;
        if (visibilityBound) return;
        visibilityBound = true;


        document.addEventListener('visibilitychange', function () {
            barTimerA = setTimeout(function () {
            if (document.visibilityState === 'visible') {
                $fill.css({
                 requestSummary();
                    transition: 'width 540ms cubic-bezier(0.22, 0.7, 0.18, 1)',
            }
                    width: '100%'
        });
                });
    }
            }, 260);
 
            barTimerB = setTimeout(function () {
                $fill.css({ transition: 'none', width: '0%' });
                $gain.css({ transition: 'none', opacity: toPercent > 0 ? 1 : 0, left: '0%', width: toPercent + '%' });
 
                 if ($fill[0]) $fill[0].offsetHeight;
 
                $fill.css({
                    transition: 'width 460ms cubic-bezier(0.22, 0.7, 0.18, 1)',
                    width: toPercent + '%'
                });
            }, 860);


    function bootProgressSystem(reason) {
            barTimerC = setTimeout(function () {
        ensurePanel();
                $gain.css({ transition: 'opacity 180ms ease', opacity: 0 });
        applyPendingSummaryIfPossible();
            }, 1380);


        if (isRewardableClientSide()) {
             return;
             handlePageView();
        } else {
            requestSummary();
         }
         }


         setTimeout(function () {
         var delta = Math.max(0, toPercent - fromPercent);
            ensurePanel();
            applyPendingSummaryIfPossible();
            requestSummary();
        }, 350);


         setTimeout(function () {
         if (delta <= 0.15) {
             ensurePanel();
             setBarInstant($fill, $gain, toPercent);
            applyPendingSummaryIfPossible();
             return;
             requestSummary();
         }
         }, 1500);
    }


    function handleSpaPageView() {
         $gain.css({
        ensurePanel();
             transition: 'none',
        applyPendingSummaryIfPossible();
            opacity: 1,
 
             left: fromPercent + '%',
         requestAnimationFrame(function () {
            width: delta + '%'
             setTimeout(function () {
                handlePageView();
             }, 80);
         });
         });
    }


    function applySummary(summary, options) {
         if ($fill[0]) $fill[0].offsetHeight;
         updatePanel(summary, options || { animateGain: false });
    }


    window.ProgressSystemWebUi = {
        barTimerA = setTimeout(function () {
        boot: bootProgressSystem,
            $fill.css({
        requestSummary: requestSummary,
                transition: 'width 560ms cubic-bezier(0.22, 0.7, 0.18, 1)',
        applySummary: applySummary,
                width: toPercent + '%'
        handlePageView: handlePageView,
            });
        handleSpaPageView: handleSpaPageView,
         }, 260);
         ensurePanel: ensurePanel
    };


    $(function () {
        barTimerB = setTimeout(function () {
        bindVisibilitySync();
            $gain.css({ transition: 'opacity 180ms ease', opacity: 0 });
         bootProgressSystem('documentReady');
         }, 940);
     });
     }


     mw.hook('wikipage.content').add(function () {
     function updatePanel(summary, options) {
         ensurePanel();
         if (!summary) return;
        applyPendingSummaryIfPossible();
        setTimeout(function () {
            ensurePanel();
            applyPendingSummaryIfPossible();
            requestSummary();
        }, 120);
    });
})(mediaWiki, jQuery);


        options = options || {};


/* CLBI Nations / Historical Events year tabs
        var $panel = ensurePanel();
* Mirrors the country information panel model:
        if (!$panel.length) {
* active tab uses .is-active/aria-selected and inactive pages use hidden.
            pendingSummary = $.extend({}, summary);
*/
            pendingOptions = $.extend({}, options);
(function (mw, $) {
            return;
    'use strict';
        }


    function activateClbiNationsHistoryYear(panel, targetYear) {
        var level = summary.level || 1;
         var tabs;
        var totalXp = summary.totalXp || 0;
         var pages;
        var xpIntoLevel = summary.xpIntoLevel || 0;
        var xpForNext = summary.xpForNextLevel || 1;
        var percent = clampPercent(summary.progressPercent);
        var isMaxLevel = !!summary.isMaxLevel;
        var dailyXp = summary.dailyXp || 0;
         var discoveries = summary.discoveryCount || 0;
         var title = summary.equippedTitle || summary.title || '';


         if (!panel || !targetYear) return false;
         $panel.removeClass('is-syncing').toggleClass('is-max-level', isMaxLevel).attr('data-progress-state', 'ready');
        $panel.find('.progress-level-label').text((isMaxLevel ? 'MAX ' : 'LVL ') + level);
        $panel.find('.progress-total-xp').text(totalXp + ' XP');
        $panel.find('.progress-xp-next').text(isMaxLevel ? 'MAX LEVEL' : (xpIntoLevel + ' / ' + xpForNext + ' TO NEXT'));
        $panel.find('.progress-daily-xp').text('TODAY ' + dailyXp + ' XP');
        $panel.find('.progress-discovery-row').text('DISCOVERED ' + discoveries);


         tabs = Array.prototype.slice.call(panel.querySelectorAll('.clbi-nations-history-year-button[data-year]'));
         var $title = $panel.find('.progress-title-row');
         pages = Array.prototype.slice.call(panel.querySelectorAll('.clbi-nations-history-page[data-year-panel]'));
         if (title) {
            $title.text(title).prop('hidden', false);
        } else {
            $title.text('').prop('hidden', true);
        }


         if (!tabs.length || !pages.length) return false;
         var $fill = $panel.find('.progress-xp-fill');
        var $gain = $panel.find('.progress-xp-gain');
        var animate = !!options.animateGain && currentSummary && totalXp > (currentSummary.totalXp || 0);
 
        if (animate) {
            animateGain(
                $fill,
                $gain,
                clampPercent(currentSummary.progressPercent),
                percent,
                level !== (currentSummary.level || 1)
            );
        } else {
            setBarInstant($fill, $gain, percent);
        }


         tabs.forEach(function (tab) {
         currentSummary = $.extend({}, summary);
            var active = tab.getAttribute('data-year') === targetYear;
        pendingSummary = null;
            tab.classList.toggle('is-active', active);
        pendingOptions = null;
             tab.setAttribute('aria-selected', active ? 'true' : 'false');
        if (summaryRetryTimer) {
             tab.setAttribute('tabindex', active ? '0' : '-1');
             clearTimeout(summaryRetryTimer);
         });
             summaryRetryTimer = null;
         }
        summaryRetryAttempts = 0;
    }


        pages.forEach(function (page) {
    function clearSummaryRetry() {
            var active = page.getAttribute('data-year-panel') === targetYear;
        if (summaryRetryTimer) clearTimeout(summaryRetryTimer);
            page.classList.toggle('is-active', active);
        summaryRetryTimer = null;
        summaryRetryAttempts = 0;
    }


            if (active) {
    function scheduleSummaryRetry(delay) {
                page.removeAttribute('hidden');
        if (!isLoggedIn()) return;
            } else {
        if (summaryRetryTimer) return;
                page.setAttribute('hidden', 'hidden');
         if (summaryRetryAttempts >= 12) return;
            }
         });


         return true;
         summaryRetryAttempts += 1;
        summaryRetryTimer = setTimeout(function () {
            summaryRetryTimer = null;
            requestSummary();
        }, delay || 1800);
     }
     }


     function moveClbiNationsHistoryYear(panel, direction) {
     function requestSummary() {
         var tabs;
         if (!isLoggedIn()) return;
         var activeIndex;
         if (summaryRequested) return;
        var nextIndex;
        var target;


         if (!panel) return false;
         summaryRequested = true;


         tabs = Array.prototype.slice.call(panel.querySelectorAll('.clbi-nations-history-year-button[data-year]'));
         withApi(function (api) {
        if (!tabs.length) return false;
            api.get({
 
                action: 'progress_summary',
        activeIndex = tabs.findIndex(function (tab) {
                format: 'json',
             return tab.classList.contains('is-active') || tab.getAttribute('aria-selected') === 'true';
                formatversion: 2
            }).then(function (data) {
                var payload = data && data.progress_summary;
                if (payload && payload.available && payload.summary) {
                    clearSummaryRetry();
                    updatePanel(payload.summary, { animateGain: false });
                } else {
                    scheduleSummaryRetry(2200);
                }
            }).catch(function () {
                scheduleSummaryRetry(2200);
             }).always(function () {
                summaryRequested = false;
            });
        }, function () {
            summaryRequested = false;
            scheduleSummaryRetry(2200);
         });
         });
    }
    function queueNotifications(items) {
        if (!items || !items.length) return;


         if (activeIndex < 0) activeIndex = 0;
         items.forEach(function (item) {
            if (!item) return;
            notificationQueue.push(item);
        });


         nextIndex = (activeIndex + direction + tabs.length) % tabs.length;
         showNextNotification();
        target = tabs[nextIndex].getAttribute('data-year');
    }


         if (activateClbiNationsHistoryYear(panel, target)) {
    function notificationText(item) {
             tabs[nextIndex].focus();
         if (item.type === 'xp') {
            return true;
             return '+' + (item.amount || 0) + ' XP · ' + (item.label || '문서 열람');
         }
         }


         return false;
         if (item.type === 'achievement') {
    }
            var xp = item.amount ? ' · +' + item.amount + ' XP' : '';
            return '업적 달성 · ' + (item.label || '새 업적') + xp;
        }


    function initClbiNationsHistoryYearTabs(root) {
        if (item.type === 'level') {
        var scope = root && root.querySelectorAll ? root : document;
            return item.label || '레벨 상승';
         var panels = scope.querySelectorAll('.clbi-nations-history-panel');
         }


         Array.prototype.forEach.call(panels, function (panel) {
         return item.label || '보상 획득';
            if (panel.getAttribute('data-clbi-history-tabs-ready') === '1') return;
    }


            panel.setAttribute('data-clbi-history-tabs-ready', '1');
    function showNextNotification() {
        if (notificationActive) return;
        if (!notificationQueue.length) return;


            panel.addEventListener('click', function (event) {
        notificationActive = true;
                var tab = event.target.closest ? event.target.closest('.clbi-nations-history-year-button[data-year]') : null;
        var item = notificationQueue.shift();
 
        var $root = $('#progress-toast-root');
                if (!tab || !panel.contains(tab)) return;


                if (activateClbiNationsHistoryYear(panel, tab.getAttribute('data-year'))) {
        if (!$root.length) {
                    event.preventDefault();
            $('body').append('<div id="progress-toast-root"></div>');
                }
            $root = $('#progress-toast-root');
            });
        }


            panel.addEventListener('keydown', function (event) {
        var $toast = $('<div class="progress-toast"></div>');
                var tab = event.target.closest ? event.target.closest('.clbi-nations-history-year-button[data-year]') : null;
        $toast.text(notificationText(item));
                var handled = false;
        $root.append($toast);


                if (!tab || !panel.contains(tab)) return;
        requestAnimationFrame(function () {
            $toast.addClass('is-visible');
        });


                if (event.key === 'ArrowLeft') handled = moveClbiNationsHistoryYear(panel, -1);
        setTimeout(function () {
                else if (event.key === 'ArrowRight') handled = moveClbiNationsHistoryYear(panel, 1);
            $toast.removeClass('is-visible');
                else if (event.key === 'Home') handled = activateClbiNationsHistoryYear(panel, (panel.querySelector('.clbi-nations-history-year-button[data-year]') || {}).getAttribute && panel.querySelector('.clbi-nations-history-year-button[data-year]').getAttribute('data-year'));
            setTimeout(function () {
                 else if (event.key === 'End') {
                $toast.remove();
                    var tabs = panel.querySelectorAll('.clbi-nations-history-year-button[data-year]');
                 notificationActive = false;
                    var last = tabs[tabs.length - 1];
                showNextNotification();
                    handled = last ? activateClbiNationsHistoryYear(panel, last.getAttribute('data-year')) : false;
            }, 220);
                    if (handled) last.focus();
        }, 2600);
                }
    }


                if (handled) {
    function applyPendingSummaryIfPossible() {
                    event.preventDefault();
        if (!pendingSummary) return;
                    event.stopPropagation();
        updatePanel(pendingSummary, pendingOptions || { animateGain: false });
                }
            });
        });
     }
     }


     window.initClbiNationsHistoryYearTabs = initClbiNationsHistoryYearTabs;
     function handlePageView() {
        ensurePanel();
        applyPendingSummaryIfPossible();


    $(function () {
        if (!isRewardableClientSide()) {
        initClbiNationsHistoryYearTabs(document);
            requestSummary();
    });
            return;
        }


    if (mw && mw.hook) {
        var pageId = getPageId();
         mw.hook('wikipage.content').add(function ($content) {
         if (handledPageIds.has(pageId)) {
             initClbiNationsHistoryYearTabs($content && $content[0] ? $content[0] : document);
             requestSummary();
        });
            return;
    }
        }
})(mediaWiki, jQuery);


        if (inFlightPageIds.has(pageId)) {
            requestSummary();
            return;
        }
        inFlightPageIds.add(pageId);


/* =========================================
        withApi(function (api) {
  Decoration runtime renderer
            api.postWithToken('csrf', {
  ========================================= */
                action: 'progress_view',
(function (mw) {
                format: 'json',
    'use strict';
                formatversion: 2,
 
                errorformat: 'plaintext',
    var REGISTRY_TITLE = 'MediaWiki:Decorations.json';
                pageid: pageId
    var RENDERED_ATTR = 'data-wiki-decoration-rendered';
            }).then(function (data) {
    var HOST_ATTR = 'data-wiki-decoration-host';
                var payload = data && data.progress_view;
    var runtimeToken = 0;
                if (!payload) return;
    var lastRegistry = null;
    var lastRenderedPageKey = '';
    var scheduledRender = 0;
    var nationsPlacementObserver = null;
    var observedNationsStack = null;
    var pixelAssetCache = {};
    var pixelCanvasCache = {};


    function normalizePageName(value) {
                handledPageIds.add(pageId);
        return String(value || '')
            .split('?')[0]
            .replace(/^\/index\.php\//, '')
            .replace(/_/g, ' ')
            .trim();
    }


    function currentPageKey() {
                var animate = hasXpNotification(payload.notifications);
        var raw = mw && mw.config ? String(mw.config.get('wgPageName') || '') : '';
        return normalizePageName(raw) || raw || '대문';
    }


                if (payload.summary) {
                    updatePanel(payload.summary, { animateGain: animate });
                }


    function cssAttrEscape(value) {
                if (payload.notifications && payload.notifications.length) {
        return String(value || '').replace(/\\/g, '\\\\').replace(/"/g, '\\"');
                    queueNotifications(payload.notifications);
                }
            }).catch(function () {
                requestSummary();
            }).always(function () {
                inFlightPageIds.delete(pageId);
            });
        }, function () {
            inFlightPageIds.delete(pageId);
            requestSummary();
        });
     }
     }


     function getActiveNationsEra() {
     function bindVisibilitySync() {
         var content = document.querySelector('.clbi-nations-era-content.is-active[data-era-content]:not([hidden])');
         if (visibilityBound) return;
         var title;
         visibilityBound = true;
        var globe;


         if (content) return content.getAttribute('data-era-content') || '';
         document.addEventListener('visibilitychange', function () {
            if (document.visibilityState === 'visible') {
                requestSummary();
            }
        });
    }


         title = document.querySelector('.clbi-nations-era-title-plate.is-active[data-era]');
    function bootProgressSystem(reason) {
         if (title) return title.getAttribute('data-era') || '';
         ensurePanel();
         applyPendingSummaryIfPossible();


         globe = document.querySelector('.clbi-nations-globe-window[data-nations-globe]');
         if (isRewardableClientSide()) {
         if (globe) {
            handlePageView();
             return globe.getAttribute('data-current-era') || globe.getAttribute('data-nations-current-era') || globe.getAttribute('data-era-year') || '';
         } else {
             requestSummary();
         }
         }


         return '';
         setTimeout(function () {
    }
            ensurePanel();
            applyPendingSummaryIfPossible();
            requestSummary();
        }, 350);


    function getActiveNationsEraPanel() {
        setTimeout(function () {
        var era = getActiveNationsEra();
            ensurePanel();
        var selector;
            applyPendingSummaryIfPossible();
        if (!era) return document.querySelector('.clbi-nations-era-content.is-active[data-era-content]:not([hidden])');
            requestSummary();
        selector = '.clbi-nations-era-content[data-era-content="' + cssAttrEscape(era) + '"]';
         }, 1500);
         return document.querySelector(selector) || document.querySelector('.clbi-nations-era-content.is-active[data-era-content]:not([hidden])');
     }
     }


     function getActiveNationsContinent() {
     function handleSpaPageView() {
         var eraPanel = getActiveNationsEraPanel();
         ensurePanel();
         var root = eraPanel || document;
         applyPendingSummaryIfPossible();
        var tab = root.querySelector('.clbi-nations-tabpanel-tab.is-active[data-continent], .clbi-nations-tabpanel-tab[aria-selected="true"][data-continent]');
        var panel;


         if (tab) return tab.getAttribute('data-continent') || '';
         requestAnimationFrame(function () {
 
            setTimeout(function () {
        panel = root.querySelector('.clbi-nations-tabpanel-continent.is-active[data-continent-panel]');
                handlePageView();
        if (panel) return panel.getAttribute('data-continent-panel') || '';
            }, 80);
 
         });
         return '';
     }
     }


     function getDecorationNationsBodySelector(era) {
     function applySummary(summary, options) {
         if (era) {
         updatePanel(summary, options || { animateGain: false });
            return '.clbi-nations-era-content[data-era-content="' + cssAttrEscape(era) + '"] .clbi-nations-tabpanel-body';
        }
        return '.clbi-nations-era-content.is-active[data-era-content]:not([hidden]) .clbi-nations-tabpanel-body, .clbi-nations-tabpanel-body';
     }
     }


     /*
     window.ProgressSystemWebUi = {
     Decoration semantic placement resolver
        boot: bootProgressSystem,
     -----------------------------------------
        requestSummary: requestSummary,
    장식 저장 데이터의 placement 값은 "사용자가 고른 의미상 위치"를 나타낸다.
        applySummary: applySummary,
     resolver는 그 의미값을 실제 DOM 부착 위치와 표시 조건으로 번역한다.
        handlePageView: handlePageView,
        handleSpaPageView: handleSpaPageView,
        ensurePanel: ensurePanel
     };
 
     $(function () {
        bindVisibilitySync();
        bootProgressSystem('documentReady');
     });


     예: 국가 및 조합에서 사용자가 1950년 / 아메리카를 지정하면 의미상 scope는
     mw.hook('wikipage.content').add(function () {
    그 조합이지만, 이미지를 붙일 기준면은 대륙 패널 자체가 아니라
        ensurePanel();
     .clbi-nations-tabpanel-body이다. 따라서 placement=nations-continent-body는
        applyPendingSummaryIfPossible();
    .clbi-nations-tabpanel-body에 이미지를 붙이고, 현재 활성 연도와 대륙이 저장값과
        setTimeout(function () {
    일치할 때만 렌더링한다.
            ensurePanel();
            applyPendingSummaryIfPossible();
            requestSummary();
        }, 120);
     });
})(mediaWiki, jQuery);


    유지보수 규칙:
    - 새 조합형 문서가 생기면 entry.target을 매번 특수하게 저장하지 말고 placement를 추가한다.
    - DevTools.js 에디터 미리보기와 Common.js 런타임 렌더러의 resolver는 같은 의미를 가져야 한다.
    - target은 물리적 기준면, era/continent 같은 필드는 표시 조건으로 다룬다.
    */
    function shouldUseNationsBodyPlacement(entry) {
        var placement = String(entry && entry.placement || '').trim();
        var target = String(entry && entry.target || '').trim();
        var era = String(entry && entry.era || '').trim();
        var continent = String(entry && entry.continent || '').trim();


        if (!document.querySelector('.clbi-nations-panel-stack')) return false;
/* CLBI Nations / Historical Events year tabs
        if (placement === 'nations-continent-body') return true;
* Mirrors the country information panel model:
        if (era || continent) return true;
* active tab uses .is-active/aria-selected and inactive pages use hidden.
        if (target.indexOf('clbi-nations-tabpanel-continent') !== -1) return true;
*/
        return false;
(function (mw, $) {
    }
    'use strict';


     function resolveDecorationPlacement(entry) {
     function activateClbiNationsHistoryYear(panel, targetYear) {
         var placement = String(entry && entry.placement || '').trim();
         var tabs;
        var selector = String(entry && entry.target || '').trim() || '.liberty-content-main';
         var pages;
        var era = String(entry && entry.era || '').trim();
        var continent = String(entry && entry.continent || '').trim();
         var target;


         if (!placement && shouldUseNationsBodyPlacement(entry)) {
         if (!panel || !targetYear) return false;
            placement = 'nations-continent-body';
        }


         if (placement === 'boot-gate' || placement === 'loading-screen') {
         tabs = Array.prototype.slice.call(panel.querySelectorAll('.clbi-nations-history-year-button[data-year]'));
            selector = '#boot-gate-screen .boot-gate-decoration-layer, #boot-gate-screen';
        pages = Array.prototype.slice.call(panel.querySelectorAll('.clbi-nations-history-page[data-year-panel]'));
            target = document.querySelector(selector);
            return {
                placement: placement,
                target: target,
                targetSelector: selector,
                visible: !!document.getElementById('boot-gate-screen')
            };
        }


         if (placement === 'nations-continent-body') {
         if (!tabs.length || !pages.length) return false;
            selector = getDecorationNationsBodySelector(era);
            target = document.querySelector(selector) || document.querySelector('.clbi-nations-tabpanel-body') || document.querySelector(String(entry && entry.target || '').trim());
            return {
                placement: placement,
                target: target,
                targetSelector: selector,
                visible: (!era || era === getActiveNationsEra()) && (!continent || continent === getActiveNationsContinent())
            };
        }


         return {
         tabs.forEach(function (tab) {
             placement: placement,
             var active = tab.getAttribute('data-year') === targetYear;
             target: document.querySelector(selector),
             tab.classList.toggle('is-active', active);
             targetSelector: selector,
             tab.setAttribute('aria-selected', active ? 'true' : 'false');
             visible: true
             tab.setAttribute('tabindex', active ? '0' : '-1');
         };
         });
    }


    function normalizeNumber(value, fallback) {
        pages.forEach(function (page) {
        var n = parseFloat(value);
            var active = page.getAttribute('data-year-panel') === targetYear;
        return Number.isFinite(n) ? n : fallback;
            page.classList.toggle('is-active', active);
    }


    function normalizeBool(value, fallback) {
            if (active) {
        if (value === true || value === 'true' || value === '1' || value === 1) return true;
                page.removeAttribute('hidden');
        if (value === false || value === 'false' || value === '0' || value === 0) return false;
            } else {
         return fallback;
                page.setAttribute('hidden', 'hidden');
    }
            }
         });


    function normalizeSrc(src) {
         return true;
        src = String(src || '').trim();
        if (!src) return '';
        if (/^(?:https?:)?\/\//i.test(src) || src.charAt(0) === '/' || src.indexOf('data:') === 0 || src.indexOf('blob:') === 0) return src;
         return '/index.php/Special:Redirect/file/' + encodeURIComponent(src);
     }
     }


    function moveClbiNationsHistoryYear(panel, direction) {
        var tabs;
        var activeIndex;
        var nextIndex;
        var target;


    function normalizeAssetType(entry) {
         if (!panel) return false;
         var type = String(entry && entry.assetType || '').trim().toLowerCase();
        var ref = String(entry && (entry.asset || entry.src) || '').trim();


         if (type === 'clbi-pixel-json' || type === 'pixel-rle' || type === 'pixel-json') return 'pixel-json';
         tabs = Array.prototype.slice.call(panel.querySelectorAll('.clbi-nations-history-year-button[data-year]'));
         if (!type && /\.json(?:[?#].*)?$/i.test(ref)) return 'pixel-json';
         if (!tabs.length) return false;
        return type || 'image';
 
    }
        activeIndex = tabs.findIndex(function (tab) {
            return tab.classList.contains('is-active') || tab.getAttribute('aria-selected') === 'true';
        });


    function isPixelJsonDecoration(entry) {
         if (activeIndex < 0) activeIndex = 0;
         return normalizeAssetType(entry) === 'pixel-json';
    }


    function getPixelJsonRef(entry) {
        nextIndex = (activeIndex + direction + tabs.length) % tabs.length;
         return String(entry && (entry.asset || entry.src) || '').trim();
         target = tabs[nextIndex].getAttribute('data-year');
    }


    function normalizePixelJsonUrl(ref) {
         if (activateClbiNationsHistoryYear(panel, target)) {
        ref = String(ref || '').trim();
             tabs[nextIndex].focus();
        if (!ref) return '';
             return true;
        if (/^(?:https?:)?\/\//i.test(ref) || ref.charAt(0) === '/' || ref.indexOf('data:') === 0 || ref.indexOf('blob:') === 0) return ref;
         if (/^(?:file|파일):/i.test(ref)) return '/index.php/Special:Redirect/file/' + encodeURIComponent(ref.replace(/^(?:file|파일):/i, ''));
        if (mw && mw.util && typeof mw.util.getUrl === 'function') {
             if (ref.indexOf(':') !== -1) {
                return mw.util.getUrl(ref, { action: 'raw', ctype: 'application/json' });
            }
             return mw.util.getUrl('MediaWiki:' + ref, { action: 'raw', ctype: 'application/json' });
         }
         }
         return ref;
 
         return false;
     }
     }


     function parsePixelColor(value) {
     function initClbiNationsHistoryYearTabs(root) {
         var text;
         var scope = root && root.querySelectorAll ? root : document;
        var m;
         var panels = scope.querySelectorAll('.clbi-nations-history-panel');
        if (Array.isArray(value)) {
            return [
                Math.max(0, Math.min(255, Number(value[0]) || 0)),
                Math.max(0, Math.min(255, Number(value[1]) || 0)),
                Math.max(0, Math.min(255, Number(value[2]) || 0)),
                value.length > 3 ? Math.max(0, Math.min(255, Number(value[3]) || 0)) : 255
            ];
         }
        text = String(value || '').trim();
        m = text.match(/^#([0-9a-f]{3}|[0-9a-f]{4}|[0-9a-f]{6}|[0-9a-f]{8})$/i);
        if (!m) return [0, 0, 0, 0];
        text = m[1];
        if (text.length === 3 || text.length === 4) {
            return [
                parseInt(text.charAt(0) + text.charAt(0), 16),
                parseInt(text.charAt(1) + text.charAt(1), 16),
                parseInt(text.charAt(2) + text.charAt(2), 16),
                text.length === 4 ? parseInt(text.charAt(3) + text.charAt(3), 16) : 255
            ];
        }
        return [
            parseInt(text.slice(0, 2), 16),
            parseInt(text.slice(2, 4), 16),
            parseInt(text.slice(4, 6), 16),
            text.length === 8 ? parseInt(text.slice(6, 8), 16) : 255
        ];
    }


        Array.prototype.forEach.call(panels, function (panel) {
            if (panel.getAttribute('data-clbi-history-tabs-ready') === '1') return;


    function decodePixelRle36(value) {
            panel.setAttribute('data-clbi-history-tabs-ready', '1');
        var text = String(value || '').trim();
 
        var parts;
            panel.addEventListener('click', function (event) {
        var runs = [];
                var tab = event.target.closest ? event.target.closest('.clbi-nations-history-year-button[data-year]') : null;
        var i;
        var x;
        var y;
        var len;
        var colorIndex;


        if (!text) return runs;
                if (!tab || !panel.contains(tab)) return;
        parts = text.split(',');
        for (i = 0; i + 3 < parts.length; i += 4) {
            x = parseInt(parts[i], 36);
            y = parseInt(parts[i + 1], 36);
            len = parseInt(parts[i + 2], 36);
            colorIndex = parseInt(parts[i + 3], 36);
            if (!Number.isFinite(x) || !Number.isFinite(y) || !Number.isFinite(len) || !Number.isFinite(colorIndex)) continue;
            runs.push([x, y, len, colorIndex]);
        }
        return runs;
    }


    function normalizePixelAsset(doc) {
                if (activateClbiNationsHistoryYear(panel, tab.getAttribute('data-year'))) {
        var encoding = String(doc && (doc.encoding || doc.e) || '').trim().toLowerCase();
                    event.preventDefault();
        var width = Math.round(Number(doc && (doc.width || doc.w)) || 0);
                }
        var height = Math.round(Number(doc && (doc.height || doc.h)) || 0);
            });
        var paletteSource = Array.isArray(doc && doc.palette) ? doc.palette : (Array.isArray(doc && doc.p) ? doc.p : []);
        var palette = paletteSource.map(parsePixelColor);
        var runs;


        /*
            panel.addEventListener('keydown', function (event) {
        CLBI Pixel Forge v0.2.3 compact format:
                var tab = event.target.closest ? event.target.closest('.clbi-nations-history-year-button[data-year]') : null;
        - MediaWiki 단일 문서 크기 제한을 피하기 위해 사람이 읽기 쉬운 [[x,y,len,c], ...] 배열 대신
                var handled = false;
          base36 토큰 문자열을 쓴다.
        - 형식은 x,y,len,colorIndex를 4개 토큰 단위로 반복한 rle36이다.
        - 복원 후 좌표계는 기존 runs 배열과 완전히 같으며, 최종 CSS px에 1:1로 찍는다.
        */
        if ((encoding === 'rle36' || encoding === 'clbi-rle36') && typeof (doc && doc.r) === 'string') {
            runs = decodePixelRle36(doc.r);
        } else {
            runs = Array.isArray(doc && doc.runs) ? doc.runs : [];
        }


        if (!width || !height || width < 1 || height < 1) throw new Error('invalid pixel decoration size');
                if (!tab || !panel.contains(tab)) return;
        if (width > 8192 || height > 8192) throw new Error('pixel decoration too large');
        return {
            type: String(doc && (doc.type || doc.t) || 'clbi-pixel-decoration'),
            version: Number(doc && (doc.version || doc.v)) || 1,
            encoding: encoding || 'runs',
            width: width,
            height: height,
            palette: palette,
            runs: runs
        };
    }


    function fetchPixelAsset(ref) {
                if (event.key === 'ArrowLeft') handled = moveClbiNationsHistoryYear(panel, -1);
        var url = normalizePixelJsonUrl(ref);
                else if (event.key === 'ArrowRight') handled = moveClbiNationsHistoryYear(panel, 1);
        var cached;
                else if (event.key === 'Home') handled = activateClbiNationsHistoryYear(panel, (panel.querySelector('.clbi-nations-history-year-button[data-year]') || {}).getAttribute && panel.querySelector('.clbi-nations-history-year-button[data-year]').getAttribute('data-year'));
        if (!url) return Promise.reject(new Error('pixel json ref is empty'));
                else if (event.key === 'End') {
        cached = pixelAssetCache[url];
                    var tabs = panel.querySelectorAll('.clbi-nations-history-year-button[data-year]');
        if (cached) return cached.promise;
                    var last = tabs[tabs.length - 1];
        cached = {
                     handled = last ? activateClbiNationsHistoryYear(panel, last.getAttribute('data-year')) : false;
            promise: fetch(url, { credentials: 'same-origin', cache: 'force-cache' })
                     if (handled) last.focus();
                .then(function (response) {
                 }
                     if (!response.ok) throw new Error('HTTP ' + response.status);
 
                     return response.json();
                if (handled) {
                 })
                    event.preventDefault();
                .then(normalizePixelAsset)
                    event.stopPropagation();
        };
                }
        pixelAssetCache[url] = cached;
            });
         return cached.promise;
         });
     }
     }


     function drawPixelAssetToCanvas(canvas, asset) {
     window.initClbiNationsHistoryYearTabs = initClbiNationsHistoryYearTabs;
         var ctx;
 
        var image;
    $(function () {
        var data;
         initClbiNationsHistoryYearTabs(document);
        var i;
    });
         var run;
 
        var x;
    if (mw && mw.hook) {
         var y;
         mw.hook('wikipage.content').add(function ($content) {
        var len;
            initClbiNationsHistoryYearTabs($content && $content[0] ? $content[0] : document);
        var color;
         });
        var colorIndex;
    }
        var p;
})(mediaWiki, jQuery);
        var k;


        canvas.width = asset.width;
        canvas.height = asset.height;
        ctx = canvas.getContext('2d');
        ctx.imageSmoothingEnabled = false;
        image = ctx.createImageData(asset.width, asset.height);
        data = image.data;


        for (i = 0; i < asset.runs.length; i += 1) {
/* =========================================
            run = asset.runs[i];
  Decoration runtime renderer
            if (!Array.isArray(run) || run.length < 4) continue;
  ========================================= */
            x = Math.round(Number(run[0]) || 0);
(function (mw) {
            y = Math.round(Number(run[1]) || 0);
    'use strict';
            len = Math.round(Number(run[2]) || 0);
 
            colorIndex = Math.round(Number(run[3]) || 0);
    var REGISTRY_TITLE = 'MediaWiki:Decorations.json';
            color = asset.palette[colorIndex];
    var RENDERED_ATTR = 'data-wiki-decoration-rendered';
            if (!color || len <= 0 || y < 0 || y >= asset.height || x >= asset.width) continue;
    var HOST_ATTR = 'data-wiki-decoration-host';
            if (x < 0) {
    var MAIN_PAGE_PLACEMENT = 'main-body-well';
                len += x;
    var MAIN_PAGE_TARGET = '.main-portal .main-body-well';
                x = 0;
    var runtimeToken = 0;
            }
    var lastRegistry = null;
            len = Math.min(len, asset.width - x);
    var lastRenderedPageKey = '';
            for (k = 0; k < len; k += 1) {
    var scheduledRender = 0;
                p = ((y * asset.width) + x + k) * 4;
    var nationsPlacementObserver = null;
                data[p] = color[0];
    var observedNationsStack = null;
                data[p + 1] = color[1];
    var mainPagePlacementObserver = null;
                data[p + 2] = color[2];
    var mainPagePlacementMutationObserver = null;
                data[p + 3] = color[3];
    var observedMainPagePortal = null;
            }
    var scheduledMainPageRebase = 0;
        }
    var pixelAssetCache = {};
    var pixelCanvasCache = {};


         ctx.putImageData(image, 0, 0);
    function normalizePageName(value) {
         return String(value || '')
            .split('?')[0]
            .replace(/^\/index\.php\//, '')
            .replace(/_/g, ' ')
            .trim();
     }
     }


     function preparePixelCanvas(ref) {
     function currentPageKey() {
         var url = normalizePixelJsonUrl(ref);
         var raw = mw && mw.config ? String(mw.config.get('wgPageName') || '') : '';
        var cached;
        return normalizePageName(raw) || raw || '대문';
        if (!url) return Promise.reject(new Error('pixel json ref is empty'));
        cached = pixelCanvasCache[url];
        if (cached) return cached.promise;
        cached = {};
        cached.promise = fetchPixelAsset(ref).then(function (asset) {
            var canvas = document.createElement('canvas');
            drawPixelAssetToCanvas(canvas, asset);
            canvas.style.imageRendering = 'pixelated';
            canvas.setAttribute('data-decoration-asset-size', asset.width + 'x' + asset.height);
            cached.canvas = canvas;
            cached.width = asset.width;
            cached.height = asset.height;
            return canvas;
        });
        pixelCanvasCache[url] = cached;
        return cached.promise;
     }
     }


     function getPreparedPixelCanvasSync(ref) {
     function ensureMainPageBodyWell() {
         var url = normalizePixelJsonUrl(ref);
         var portal;
         var cached = url ? pixelCanvasCache[url] : null;
        var topMount;
         return cached && cached.canvas ? cached.canvas : null;
        var panel;
    }
        var well;
         var manifesto;
 
         if (currentPageKey() !== '대문') return null;


    function applyDecorationBaseStyle(node, entry) {
        portal = document.querySelector('.main-portal');
         node.style.left = normalizeNumber(entry.x, 0) + 'px';
        if (!portal) return null;
         node.style.top = normalizeNumber(entry.y, 0) + 'px';
 
         node.style.opacity = String(normalizeNumber(entry.opacity, 1));
         topMount = portal.querySelector('[data-component="category-nav"]');
        node.style.zIndex = String(Math.round(normalizeNumber(entry.zIndex, 0)));
         panel = portal.querySelector('.main-body-panel');
        node.style.pointerEvents = String(entry.pointerEvents || 'none');
        well = portal.querySelector('.main-body-well');
         if (entry.blendMode) node.style.mixBlendMode = String(entry.blendMode);
        manifesto = portal.querySelector('.main-manifesto');
         if (entry.filter) node.style.filter = String(entry.filter);
 
         if (entry.transform) node.style.transform = String(entry.transform);
         if (!panel) {
            panel = document.createElement('div');
            panel.className = 'main-body-panel';
            panel.setAttribute('data-main-body-panel-generated', '1');
 
            if (topMount && topMount.parentNode) {
                if (topMount.nextSibling) {
                    topMount.parentNode.insertBefore(panel, topMount.nextSibling);
                } else {
                    topMount.parentNode.appendChild(panel);
                }
            } else {
                portal.appendChild(panel);
            }
        }
 
         if (!well) {
            well = document.createElement('div');
            well.className = 'main-body-well';
            well.setAttribute('data-main-body-well-generated', '1');
            panel.appendChild(well);
         } else if (well.parentNode !== panel) {
            panel.appendChild(well);
        }
 
         if (manifesto && manifesto.parentNode !== well) {
            well.insertBefore(manifesto, well.firstChild);
        }
 
        return well;
     }
     }


     function isDecorationNodeActiveForNationsState(node) {
 
         var era = node ? String(node.getAttribute('data-decoration-era') || '').trim() : '';
     function cssAttrEscape(value) {
        var continent = node ? String(node.getAttribute('data-decoration-continent') || '').trim() : '';
         return String(value || '').replace(/\\/g, '\\\\').replace(/"/g, '\\"');
        if (era && era !== getActiveNationsEra()) return false;
        if (continent && continent !== getActiveNationsContinent()) return false;
        return true;
     }
     }


     function setDecorationNodeVisibility(node, visible) {
     function getActiveNationsEra() {
         if (!node) return;
         var content = document.querySelector('.clbi-nations-era-content.is-active[data-era-content]:not([hidden])');
         visible = visible !== false;
         var title;
         /* aria-hidden stays true because wiki decorations are purely visual. */
         var globe;
         if (node.hidden !== !visible) node.hidden = !visible;
 
         if (node.style.display !== (visible ? '' : 'none')) node.style.display = visible ? '' : 'none';
         if (content) return content.getAttribute('data-era-content') || '';
         node.setAttribute('data-decoration-visible', visible ? '1' : '0');
 
    }
        title = document.querySelector('.clbi-nations-era-title-plate.is-active[data-era]');
         if (title) return title.getAttribute('data-era') || '';
 
        globe = document.querySelector('.clbi-nations-globe-window[data-nations-globe]');
         if (globe) {
            return globe.getAttribute('data-current-era') || globe.getAttribute('data-nations-current-era') || globe.getAttribute('data-era-year') || '';
        }


    function updateDecorationVisibility(root) {
         return '';
         var scope = root && root.querySelectorAll ? root : document;
        var count = 0;
        Array.prototype.forEach.call(scope.querySelectorAll('[' + RENDERED_ATTR + '="1"]'), function (node) {
            setDecorationNodeVisibility(node, isDecorationNodeActiveForNationsState(node));
            count += 1;
        });
        return count;
     }
     }


     function hasRenderedDecorations(root) {
     function getActiveNationsEraPanel() {
         var scope = root && root.querySelector ? root : document;
         var era = getActiveNationsEra();
         return !!(scope && scope.querySelector && scope.querySelector('[' + RENDERED_ATTR + '="1"]'));
        var selector;
         if (!era) return document.querySelector('.clbi-nations-era-content.is-active[data-era-content]:not([hidden])');
        selector = '.clbi-nations-era-content[data-era-content="' + cssAttrEscape(era) + '"]';
        return document.querySelector(selector) || document.querySelector('.clbi-nations-era-content.is-active[data-era-content]:not([hidden])');
     }
     }


     function applyPixelJsonDecoration(entry, target, visible) {
     function getActiveNationsContinent() {
         var ref = getPixelJsonRef(entry);
         var eraPanel = getActiveNationsEraPanel();
         var template;
         var root = eraPanel || document;
         var canvas;
         var tab = root.querySelector('.clbi-nations-tabpanel-tab.is-active[data-continent], .clbi-nations-tabpanel-tab[aria-selected="true"][data-continent]');
         var ctx;
         var panel;
        if (!ref) return false;


         function makeCanvasFromTemplate(source) {
         if (tab) return tab.getAttribute('data-continent') || '';
            var out = document.createElement('canvas');
            var outCtx;
            out.className = 'wiki-decoration wiki-decoration-pixel-json';
            out.setAttribute(RENDERED_ATTR, '1');
            out.setAttribute('aria-hidden', 'true');
            out.setAttribute('data-decoration-id', String(entry.id || ''));
            out.setAttribute('data-decoration-asset-type', 'pixel-json');
            if (entry.placement) out.setAttribute('data-decoration-placement', String(entry.placement));
            if (entry.era) out.setAttribute('data-decoration-era', String(entry.era));
            if (entry.continent) out.setAttribute('data-decoration-continent', String(entry.continent));
            out.width = source.width;
            out.height = source.height;
            out.style.imageRendering = 'pixelated';
            out.style.width = source.width + 'px';
            out.style.height = source.height + 'px';
            out.setAttribute('data-decoration-asset-size', source.width + 'x' + source.height);
            outCtx = out.getContext('2d');
            outCtx.imageSmoothingEnabled = false;
            outCtx.drawImage(source, 0, 0);
            applyDecorationBaseStyle(out, entry);
            setDecorationNodeVisibility(out, visible);
            return out;
        }


         template = getPreparedPixelCanvasSync(ref);
         panel = root.querySelector('.clbi-nations-tabpanel-continent.is-active[data-continent-panel]');
         if (template) {
         if (panel) return panel.getAttribute('data-continent-panel') || '';
            target.appendChild(makeCanvasFromTemplate(template));
            return true;
        }


         /* Fallback path only.  A full entry pack should prepare the template before the
         return '';
          normal UI is released, so users should not see a blank decoration canvas. */
        preparePixelCanvas(ref).then(function (source) {
            if (!target || !target.parentNode || !source) return;
            target.appendChild(makeCanvasFromTemplate(source));
        }).catch(function () {});
 
        return true;
     }
     }


     function preloadPixelAssetsForRegistry(registry) {
     function getDecorationNationsBodySelector(era) {
         var promises = [];
         if (era) {
        decorationList(registry).forEach(function (entry) {
             return '.clbi-nations-era-content[data-era-content="' + cssAttrEscape(era) + '"] .clbi-nations-tabpanel-body';
             var ref;
         }
            if (!entry || !matchesPage(entry) || !isPixelJsonDecoration(entry)) return;
         return '.clbi-nations-era-content.is-active[data-era-content]:not([hidden]) .clbi-nations-tabpanel-body, .clbi-nations-tabpanel-body';
            ref = getPixelJsonRef(entry);
            if (!ref) return;
            promises.push(preparePixelCanvas(ref).catch(function () { return null; }));
         });
         return Promise.all(promises);
     }
     }


     function decorationList(registry) {
    /*
         if (!registry || typeof registry !== 'object') return [];
    Decoration semantic placement resolver
         if (Array.isArray(registry)) return registry;
    -----------------------------------------
         if (Array.isArray(registry.decorations)) return registry.decorations;
    장식 저장 데이터의 placement 값은 "사용자가 고른 의미상 위치"를 나타낸다.
         return [];
    resolver는 그 의미값을 실제 DOM 부착 위치와 표시 조건으로 번역한다.
 
    예: 시대 문서에서 사용자가 1950년 / 아메리카를 지정하면 의미상 scope는
    그 조합이지만, 이미지를 붙일 기준면은 대륙 패널 자체가 아니라
    .clbi-nations-tabpanel-body이다. 따라서 placement=nations-continent-body는
    .clbi-nations-tabpanel-body에 이미지를 붙이고, 현재 활성 연도와 대륙이 저장값과
    일치할 때만 렌더링한다.
 
    유지보수 규칙:
    - 새 조합형 문서가 생기면 entry.target을 매번 특수하게 저장하지 말고 placement를 추가한다.
    - DevTools.js 에디터 미리보기와 Common.js 런타임 렌더러의 resolver는 같은 의미를 가져야 한다.
    - target은 물리적 기준면, era/continent 같은 필드는 표시 조건으로 다룬다.
    */
     function shouldUseNationsBodyPlacement(entry) {
         var placement = String(entry && entry.placement || '').trim();
        var target = String(entry && entry.target || '').trim();
        var era = String(entry && entry.era || '').trim();
        var continent = String(entry && entry.continent || '').trim();
 
         if (!document.querySelector('.clbi-nations-panel-stack')) return false;
        if (placement === 'nations-continent-body') return true;
        if (era || continent) return true;
         if (target.indexOf('clbi-nations-tabpanel-continent') !== -1) return true;
         return false;
     }
     }


     function matchesPage(entry) {
     function resolveDecorationPlacement(entry) {
         var page = currentPageKey();
         var placement = String(entry && entry.placement || '').trim();
         var underscored = page.replace(/ /g, '_');
         var selector = String(entry && entry.target || '').trim() || '.liberty-content-main';
         var pages = entry && entry.pages;
         var era = String(entry && entry.era || '').trim();
         var target = entry && entry.page;
         var continent = String(entry && entry.continent || '').trim();
         var i;
         var target;


         if (normalizeBool(entry && entry.global, false)) return true;
         if (!placement && shouldUseNationsBodyPlacement(entry)) {
        if (String(entry && entry.placement || '').trim() === 'boot-gate' || String(entry && entry.placement || '').trim() === 'loading-screen') {
            placement = 'nations-continent-body';
            return true;
         }
         }
        if (normalizePageName(target).toLowerCase() === '__boot__' || normalizePageName(target).toLowerCase() === 'loading-screen') return true;
        if (!target && !pages) return true;


         if (Array.isArray(pages)) {
         if (placement === 'boot-gate' || placement === 'loading-screen') {
             for (i = 0; i < pages.length; i += 1) {
             selector = '#boot-gate-screen .boot-gate-decoration-layer, #boot-gate-screen';
                 if (normalizePageName(pages[i]) === page || String(pages[i] || '') === underscored) return true;
            target = document.querySelector(selector);
             }
            return {
                 placement: placement,
                target: target,
                targetSelector: selector,
                visible: !!document.getElementById('boot-gate-screen')
             };
         }
         }


         target = normalizePageName(target);
         if (placement === 'nations-continent-body') {
        return target === page || target === underscored;
            selector = getDecorationNationsBodySelector(era);
    }
            target = document.querySelector(selector) || document.querySelector('.clbi-nations-tabpanel-body') || document.querySelector(String(entry && entry.target || '').trim());
 
            return {
    function clearRendered(root) {
                placement: placement,
         var scope = root && root.querySelectorAll ? root : document;
                target: target,
         Array.prototype.forEach.call(scope.querySelectorAll('[' + RENDERED_ATTR + '="1"]'), function (node) {
                targetSelector: selector,
             if (node.parentNode) node.parentNode.removeChild(node);
                visible: (!era || era === getActiveNationsEra()) && (!continent || continent === getActiveNationsContinent())
         });
            };
        }
 
        /*
         대문의 일반 장식은 저장된 target과 관계없이 본문 우물 안에서 렌더링한다.
        우물이 overflow:hidden이므로 #1d1d1d 프레임 밖으로 나갈 수 없다.
         boot/loading 및 국가 패널 전용 placement는 위 분기에서 기존 동작을 유지한다.
        */
        var mainPageWell = ensureMainPageBodyWell();
        if (mainPageWell) {
            return {
                placement: MAIN_PAGE_PLACEMENT,
                target: mainPageWell,
                targetSelector: MAIN_PAGE_TARGET,
                visible: true
            };
        }
 
        return {
             placement: placement,
            target: document.querySelector(selector),
            targetSelector: selector,
            visible: true
         };
     }
     }


     function ensureHost(target) {
     function normalizeNumber(value, fallback) {
         var style;
         var n = parseFloat(value);
        if (!target) return;
         return Number.isFinite(n) ? n : fallback;
         target.setAttribute(HOST_ATTR, '1');
        style = window.getComputedStyle ? window.getComputedStyle(target) : null;
        if (style && style.position === 'static') target.style.position = 'relative';
     }
     }


     function applyDecoration(entry) {
     function normalizeBool(value, fallback) {
         var resolved;
         if (value === true || value === 'true' || value === '1' || value === 1) return true;
         var target;
         if (value === false || value === 'false' || value === '0' || value === 0) return false;
        var src;
         return fallback;
        var img;
    }
         var width;
        var height;


         if (!entry || typeof entry !== 'object' || normalizeBool(entry.enabled, true) === false) return false;
    function normalizeSrc(src) {
         resolved = resolveDecorationPlacement(entry);
        src = String(src || '').trim();
        if (!resolved) return false;
         if (!src) return '';
        target = resolved.target;
        if (/^(?:https?:)?\/\//i.test(src) || src.charAt(0) === '/' || src.indexOf('data:') === 0 || src.indexOf('blob:') === 0) return src;
        if (!target) return false;
         return '/index.php/Special:Redirect/file/' + encodeURIComponent(src);
    }


        ensureHost(target);


        if (isPixelJsonDecoration(entry)) {
    function normalizeAssetType(entry) {
            return applyPixelJsonDecoration(entry, target, resolved.visible !== false);
        var type = String(entry && entry.assetType || '').trim().toLowerCase();
         }
         var ref = String(entry && (entry.asset || entry.src) || '').trim();


         src = normalizeSrc(entry.src);
         if (type === 'clbi-pixel-json' || type === 'pixel-rle' || type === 'pixel-json') return 'pixel-json';
         if (!src) return false;
         if (!type && /\.json(?:[?#].*)?$/i.test(ref)) return 'pixel-json';
        return type || 'image';
    }


        img = document.createElement('img');
    function isPixelJsonDecoration(entry) {
        img.className = 'wiki-decoration';
         return normalizeAssetType(entry) === 'pixel-json';
        img.setAttribute(RENDERED_ATTR, '1');
        img.setAttribute('aria-hidden', 'true');
        img.setAttribute('alt', '');
        img.setAttribute('decoding', 'async');
        img.setAttribute('loading', 'eager');
        img.setAttribute('data-decoration-id', String(entry.id || ''));
         if (entry.placement) img.setAttribute('data-decoration-placement', String(entry.placement));
        if (entry.era) img.setAttribute('data-decoration-era', String(entry.era));
        if (entry.continent) img.setAttribute('data-decoration-continent', String(entry.continent));
        img.src = src;
 
        applyDecorationBaseStyle(img, entry);
        width = normalizeNumber(entry.width, NaN);
        height = normalizeNumber(entry.height, NaN);
        if (Number.isFinite(width) && width > 0) img.style.width = width + 'px';
        if (Number.isFinite(height) && height > 0) img.style.height = height + 'px';
        if (entry.objectFit) img.style.objectFit = String(entry.objectFit);
        setDecorationNodeVisibility(img, resolved.visible !== false);
 
        target.appendChild(img);
        return true;
     }
     }


     function render(registry) {
     function getPixelJsonRef(entry) {
         var token = runtimeToken;
         return String(entry && (entry.asset || entry.src) || '').trim();
        lastRegistry = registry || { decorations: [] };
        lastRenderedPageKey = currentPageKey();
        bindNationsPlacementRefresh();
        clearRendered(document);
        decorationList(lastRegistry).forEach(function (entry) {
            if (token === runtimeToken && matchesPage(entry)) applyDecoration(entry);
        });
     }
     }


     function renderPrepared() {
     function normalizePixelJsonUrl(ref) {
         var registry = getPreparedRegistrySync() || lastRegistry;
         ref = String(ref || '').trim();
         if (!registry) return false;
         if (!ref) return '';
         render(registry);
         if (/^(?:https?:)?\/\//i.test(ref) || ref.charAt(0) === '/' || ref.indexOf('data:') === 0 || ref.indexOf('blob:') === 0) return ref;
         return true;
         if (/^(?:file|파일):/i.test(ref)) return '/index.php/Special:Redirect/file/' + encodeURIComponent(ref.replace(/^(?:file|파일):/i, ''));
    }
         if (mw && mw.util && typeof mw.util.getUrl === 'function') {
 
            if (ref.indexOf(':') !== -1) {
    function syncDecorationState() {
                return mw.util.getUrl(ref, { action: 'raw', ctype: 'application/json' });
         if (lastRenderedPageKey === currentPageKey() && hasRenderedDecorations(document)) {
            }
            updateDecorationVisibility(document);
             return mw.util.getUrl('MediaWiki:' + ref, { action: 'raw', ctype: 'application/json' });
             return true;
         }
         }
         if (lastRegistry) {
         return ref;
            render(lastRegistry);
            return true;
        }
        return renderPrepared();
     }
     }


     function scheduleRenderFromCache() {
     function parsePixelColor(value) {
         if (scheduledRender) return;
        var text;
        scheduledRender = window.requestAnimationFrame ? window.requestAnimationFrame(function () {
        var m;
            scheduledRender = 0;
         if (Array.isArray(value)) {
            if (!syncDecorationState()) reload();
            return [
        }) : window.setTimeout(function () {
                Math.max(0, Math.min(255, Number(value[0]) || 0)),
             scheduledRender = 0;
                Math.max(0, Math.min(255, Number(value[1]) || 0)),
            if (!syncDecorationState()) reload();
                Math.max(0, Math.min(255, Number(value[2]) || 0)),
         }, 0);
                value.length > 3 ? Math.max(0, Math.min(255, Number(value[3]) || 0)) : 255
    }
             ];
 
        }
    function scheduleDecorationVisibilityUpdate() {
        text = String(value || '').trim();
         if (scheduledRender) return;
         m = text.match(/^#([0-9a-f]{3}|[0-9a-f]{4}|[0-9a-f]{6}|[0-9a-f]{8})$/i);
         scheduledRender = window.requestAnimationFrame ? window.requestAnimationFrame(function () {
         if (!m) return [0, 0, 0, 0];
             scheduledRender = 0;
         text = m[1];
            if (lastRenderedPageKey !== currentPageKey() || !hasRenderedDecorations(document)) {
        if (text.length === 3 || text.length === 4) {
                 if (!syncDecorationState()) reload();
             return [
                 return;
                parseInt(text.charAt(0) + text.charAt(0), 16),
            }
                 parseInt(text.charAt(1) + text.charAt(1), 16),
            updateDecorationVisibility(document);
                 parseInt(text.charAt(2) + text.charAt(2), 16),
        }) : window.setTimeout(function () {
                text.length === 4 ? parseInt(text.charAt(3) + text.charAt(3), 16) : 255
             scheduledRender = 0;
             ];
             if (lastRenderedPageKey !== currentPageKey() || !hasRenderedDecorations(document)) {
        }
                if (!syncDecorationState()) reload();
        return [
                return;
             parseInt(text.slice(0, 2), 16),
             }
            parseInt(text.slice(2, 4), 16),
            updateDecorationVisibility(document);
            parseInt(text.slice(4, 6), 16),
         }, 0);
             text.length === 8 ? parseInt(text.slice(6, 8), 16) : 255
         ];
     }
     }


    function bindNationsPlacementRefresh() {
        var stack = document.querySelector('.clbi-nations-panel-stack');


         if (!stack || observedNationsStack === stack) return;
    function decodePixelRle36(value) {
         observedNationsStack = stack;
         var text = String(value || '').trim();
         var parts;
        var runs = [];
        var i;
        var x;
        var y;
        var len;
        var colorIndex;


         if (nationsPlacementObserver) {
         if (!text) return runs;
             nationsPlacementObserver.disconnect();
        parts = text.split(',');
         }
        for (i = 0; i + 3 < parts.length; i += 4) {
             x = parseInt(parts[i], 36);
            y = parseInt(parts[i + 1], 36);
            len = parseInt(parts[i + 2], 36);
            colorIndex = parseInt(parts[i + 3], 36);
            if (!Number.isFinite(x) || !Number.isFinite(y) || !Number.isFinite(len) || !Number.isFinite(colorIndex)) continue;
            runs.push([x, y, len, colorIndex]);
         }
        return runs;
    }
 
    function normalizePixelAsset(doc) {
        var encoding = String(doc && (doc.encoding || doc.e) || '').trim().toLowerCase();
        var width = Math.round(Number(doc && (doc.width || doc.w)) || 0);
        var height = Math.round(Number(doc && (doc.height || doc.h)) || 0);
        var paletteSource = Array.isArray(doc && doc.palette) ? doc.palette : (Array.isArray(doc && doc.p) ? doc.p : []);
        var palette = paletteSource.map(parsePixelColor);
        var runs;


         if (typeof MutationObserver === 'function') {
        /*
             nationsPlacementObserver = new MutationObserver(scheduleDecorationVisibilityUpdate);
        CLBI Pixel Forge v0.2.3 compact format:
             nationsPlacementObserver.observe(stack, {
        - MediaWiki 단일 문서 크기 제한을 피하기 위해 사람이 읽기 쉬운 [[x,y,len,c], ...] 배열 대신
                subtree: true,
          base36 토큰 문자열을 쓴다.
                attributes: true,
        - 형식은 x,y,len,colorIndex를 4개 토큰 단위로 반복한 rle36이다.
                attributeFilter: ['class', 'hidden', 'aria-selected', 'data-current-era', 'data-nations-current-era', 'data-era-year']
        - 복원 후 좌표계는 기존 runs 배열과 완전히 같으며, 최종 CSS px에 1:1로 찍는다.
            });
        */
         if ((encoding === 'rle36' || encoding === 'clbi-rle36') && typeof (doc && doc.r) === 'string') {
             runs = decodePixelRle36(doc.r);
        } else {
             runs = Array.isArray(doc && doc.runs) ? doc.runs : [];
         }
         }
    }


    function getPreparedRegistrySync() {
        if (!width || !height || width < 1 || height < 1) throw new Error('invalid pixel decoration size');
        var registry = null;
         if (width > 8192 || height > 8192) throw new Error('pixel decoration too large');
         var url;
         return {
         try {
             type: String(doc && (doc.type || doc.t) || 'clbi-pixel-decoration'),
             if (window.EntryStore && typeof window.EntryStore.getJsonSync === 'function') {
            version: Number(doc && (doc.version || doc.v)) || 1,
                registry = window.EntryStore.getJsonSync(REGISTRY_TITLE);
            encoding: encoding || 'runs',
                if (!registry && mw && mw.util && typeof mw.util.getUrl === 'function') {
            width: width,
                    url = mw.util.getUrl(REGISTRY_TITLE, { action: 'raw', ctype: 'application/json' });
            height: height,
                    registry = window.EntryStore.getJsonSync(url);
            palette: palette,
                }
             runs: runs
             }
         };
         } catch (err) {}
        return registry && typeof registry === 'object' ? registry : null;
     }
     }


     function fetchRegistry() {
     function fetchPixelAsset(ref) {
         var url;
         var url = normalizePixelJsonUrl(ref);
         var prepared = getPreparedRegistrySync();
         var cached;
         if (prepared) return Promise.resolve(prepared);
         if (!url) return Promise.reject(new Error('pixel json ref is empty'));
         if (!mw || !mw.util || typeof fetch !== 'function') return Promise.resolve({ decorations: [] });
        cached = pixelAssetCache[url];
         url = mw.util.getUrl(REGISTRY_TITLE, {
         if (cached) return cached.promise;
             action: 'raw',
         cached = {
            ctype: 'application/json'
             promise: fetch(url, { credentials: 'same-origin', cache: 'force-cache' })
        });
                .then(function (response) {
        return fetch(url, { credentials: 'same-origin', cache: 'force-cache' })
                    if (!response.ok) throw new Error('HTTP ' + response.status);
            .then(function (response) {
                    return response.json();
                if (!response.ok) throw new Error('HTTP ' + response.status);
                })
                return response.text();
                .then(normalizePixelAsset)
            })
        };
            .then(function (text) {
        pixelAssetCache[url] = cached;
                if (!text || !text.trim()) return { decorations: [] };
        return cached.promise;
                return JSON.parse(text);
            })
            .catch(function () {
                return { decorations: [] };
            });
     }
     }


     function reload() {
     function drawPixelAssetToCanvas(canvas, asset) {
         var token;
         var ctx;
         runtimeToken += 1;
         var image;
         token = runtimeToken;
        var data;
         return fetchRegistry().then(function (registry) {
         var i;
            return preloadPixelAssetsForRegistry(registry).then(function () {
         var run;
                if (token === runtimeToken) render(registry);
        var x;
                return registry;
        var y;
            });
        var len;
         });
        var color;
    }
        var colorIndex;
         var p;
        var k;


    document.addEventListener('click', function (event) {
        canvas.width = asset.width;
         var target = event.target && event.target.closest ? event.target.closest('.clbi-nations-tabpanel-tab[data-continent], [data-nations-era-move]') : null;
         canvas.height = asset.height;
         if (target) window.setTimeout(scheduleDecorationVisibilityUpdate, 0);
        ctx = canvas.getContext('2d');
    }, true);
         ctx.imageSmoothingEnabled = false;
        image = ctx.createImageData(asset.width, asset.height);
        data = image.data;


    function decorationDiagnostics() {
        for (i = 0; i < asset.runs.length; i += 1) {
        return {
             run = asset.runs[i];
             build: '20260708-nations-mouse-click-live-cache-guard-001',
             if (!Array.isArray(run) || run.length < 4) continue;
             hasRegistry: !!lastRegistry,
             x = Math.round(Number(run[0]) || 0);
            entries: decorationList(lastRegistry).length,
             y = Math.round(Number(run[1]) || 0);
             rendered: document.querySelectorAll('[' + RENDERED_ATTR + '="1"]').length,
             len = Math.round(Number(run[2]) || 0);
            preparedRegistry: !!getPreparedRegistrySync(),
             colorIndex = Math.round(Number(run[3]) || 0);
             pixelAssets: Object.keys(pixelAssetCache || {}).length,
             color = asset.palette[colorIndex];
             pixelCanvases: Object.keys(pixelCanvasCache || {}).length,
             if (!color || len <= 0 || y < 0 || y >= asset.height || x >= asset.width) continue;
             visibilityOnlySync: true,
             if (x < 0) {
             lastRenderedPage: lastRenderedPageKey,
                len += x;
             visibleRendered: document.querySelectorAll('[' + RENDERED_ATTR + '="1"][data-decoration-visible="1"]').length,
                x = 0;
             scheduled: !!scheduledRender
            }
        };
            len = Math.min(len, asset.width - x);
    }
            for (k = 0; k < len; k += 1) {
 
                p = ((y * asset.width) + x + k) * 4;
    window.Decorations = window.Decorations || {};
                data[p] = color[0];
    window.Decorations.reload = reload;
                data[p + 1] = color[1];
    window.Decorations.render = render;
                data[p + 2] = color[2];
    window.Decorations.renderPrepared = renderPrepared;
                data[p + 3] = color[3];
    window.Decorations.sync = syncDecorationState;
            }
    window.Decorations.updateVisibility = updateDecorationVisibility;
        }
    window.Decorations.diagnostics = decorationDiagnostics;
 
    window.Decorations.clear = clearRendered;
        ctx.putImageData(image, 0, 0);
     window.Decorations.apply = applyDecoration;
     }
     window.Decorations.pageKey = currentPageKey;
 
    window.Decorations.loadPixelAsset = fetchPixelAsset;
     function preparePixelCanvas(ref) {
    window.Decorations.preparePixelCanvas = preparePixelCanvas;
        var url = normalizePixelJsonUrl(ref);
    window.Decorations.getPreparedPixelCanvasSync = getPreparedPixelCanvasSync;
        var cached;
    window.Decorations.drawPixelAssetToCanvas = drawPixelAssetToCanvas;
        if (!url) return Promise.reject(new Error('pixel json ref is empty'));
 
        cached = pixelCanvasCache[url];
    window.CLBI_DECORATIONS = window.CLBI_DECORATIONS || {};
        if (cached) return cached.promise;
    window.CLBI_DECORATIONS.reload = reload;
        cached = {};
    window.CLBI_DECORATIONS.render = render;
        cached.promise = fetchPixelAsset(ref).then(function (asset) {
    window.CLBI_DECORATIONS.renderPrepared = renderPrepared;
            var canvas = document.createElement('canvas');
    window.CLBI_DECORATIONS.sync = syncDecorationState;
            drawPixelAssetToCanvas(canvas, asset);
    window.CLBI_DECORATIONS.updateVisibility = updateDecorationVisibility;
            canvas.style.imageRendering = 'pixelated';
    window.CLBI_DECORATIONS.diagnostics = decorationDiagnostics;
            canvas.setAttribute('data-decoration-asset-size', asset.width + 'x' + asset.height);
    window.CLBI_DECORATIONS.clear = clearRendered;
            cached.canvas = canvas;
    window.CLBI_DECORATIONS.apply = applyDecoration;
            cached.width = asset.width;
    window.CLBI_DECORATIONS.pageKey = currentPageKey;
            cached.height = asset.height;
    window.CLBI_DECORATIONS.loadPixelAsset = fetchPixelAsset;
            return canvas;
    window.CLBI_DECORATIONS.preparePixelCanvas = preparePixelCanvas;
        });
    window.CLBI_DECORATIONS.getPreparedPixelCanvasSync = getPreparedPixelCanvasSync;
        pixelCanvasCache[url] = cached;
    window.CLBI_DECORATIONS.drawPixelAssetToCanvas = drawPixelAssetToCanvas;
        return cached.promise;
 
    }
     if (document.readyState === 'loading') {
 
         document.addEventListener('DOMContentLoaded', reload);
     function getPreparedPixelCanvasSync(ref) {
    } else {
         var url = normalizePixelJsonUrl(ref);
         reload();
         var cached = url ? pixelCanvasCache[url] : null;
     }
        return cached && cached.canvas ? cached.canvas : null;
 
     }
     if (mw && mw.hook) {
 
         mw.hook('wikipage.content').add(function () {
     function queryDecorationTarget(selector) {
             reload();
         selector = String(selector || '').trim();
         });
        if (!selector) return null;
     }
        try {
})(mediaWiki);
             return document.querySelector(selector);
         } catch (err) {
            return null;
        }
     }
 
    function usesCanonicalMainPageCoordinates(entry, actualTarget) {
        var placement = String(entry && entry.placement || '').trim();
        var selector = String(entry && entry.target || '').trim();
        var declaredTarget;
 
        if (placement === MAIN_PAGE_PLACEMENT) return true;
        if (!actualTarget || !selector) return false;
 
        declaredTarget = queryDecorationTarget(selector);
        return declaredTarget === actualTarget;
    }
 
    function getDecorationLocalPosition(entry, actualTarget) {
        var x = normalizeNumber(entry && entry.x, 0);
        var y = normalizeNumber(entry && entry.y, 0);
        var sourceX = x;
        var sourceY = y;
        var sourceSelector;
        var sourceTarget;
        var sourceRect;
        var actualRect;
        var sourceOriginX;
        var sourceOriginY;
        var actualOriginX;
        var actualOriginY;
 
        /*
        대문 데코 좌표 보존
        -----------------------------------------
        저장된 x/y는 기존 entry.target 좌표계를 기준으로 만들어졌다.
        데코를 main-body-well 안으로 옮길 때 x/y를 그대로 적용하면
        더 아래에서 시작하는 새 좌표계 때문에 장식이 바닥으로 밀린다.
 
        실제 화면상의 위치는 유지하고 부모만 우물로 바꾼다.
        기존 기준면의 padding-box 원점과 새 우물의 padding-box 원점 차이를
        정수 px로 더해 새 로컬 좌표를 만든다.
        */
        if (
            currentPageKey() === '대문' &&
            actualTarget &&
            actualTarget.classList &&
            actualTarget.classList.contains('main-body-well') &&
            !usesCanonicalMainPageCoordinates(entry, actualTarget)
        ) {
            sourceSelector = String(entry && entry.target || '').trim() ||
                '.liberty-content-main';
            sourceTarget = queryDecorationTarget(sourceSelector) ||
                document.querySelector('.liberty-content-main') ||
                document.querySelector('.main-portal');
 
            if (sourceTarget && sourceTarget !== actualTarget) {
                sourceRect = sourceTarget.getBoundingClientRect();
                actualRect = actualTarget.getBoundingClientRect();
 
                sourceOriginX = sourceRect.left + normalizeNumber(sourceTarget.clientLeft, 0);
                sourceOriginY = sourceRect.top + normalizeNumber(sourceTarget.clientTop, 0);
                actualOriginX = actualRect.left + normalizeNumber(actualTarget.clientLeft, 0);
                actualOriginY = actualRect.top + normalizeNumber(actualTarget.clientTop, 0);
 
                x += Math.round(sourceOriginX - actualOriginX);
                y += Math.round(sourceOriginY - actualOriginY);
            }
        }
 
        return {
            x: Math.round(x),
            y: Math.round(y),
            sourceX: Math.round(sourceX),
            sourceY: Math.round(sourceY),
            sourceSelector: sourceSelector || ''
        };
    }
 
    function applyDecorationBaseStyle(node, entry, actualTarget) {
        var position = getDecorationLocalPosition(entry, actualTarget);
 
        node.style.left = position.x + 'px';
        node.style.top = position.y + 'px';
        node.style.opacity = String(normalizeNumber(entry.opacity, 1));
        node.style.zIndex = String(Math.round(normalizeNumber(entry.zIndex, 0)));
        node.style.pointerEvents = String(entry.pointerEvents || 'none');
 
        if (position.sourceSelector) {
            node.setAttribute('data-decoration-coordinate-source', position.sourceSelector);
            node.setAttribute('data-decoration-source-x', String(position.sourceX));
            node.setAttribute('data-decoration-source-y', String(position.sourceY));
            node.setAttribute('data-decoration-local-x', String(position.x));
            node.setAttribute('data-decoration-local-y', String(position.y));
        }
 
        if (entry.blendMode) node.style.mixBlendMode = String(entry.blendMode);
        if (entry.filter) node.style.filter = String(entry.filter);
        if (entry.transform) node.style.transform = String(entry.transform);
    }
 
    function isDecorationNodeActiveForNationsState(node) {
        var era = node ? String(node.getAttribute('data-decoration-era') || '').trim() : '';
        var continent = node ? String(node.getAttribute('data-decoration-continent') || '').trim() : '';
        if (era && era !== getActiveNationsEra()) return false;
        if (continent && continent !== getActiveNationsContinent()) return false;
        return true;
    }
 
    function setDecorationNodeVisibility(node, visible) {
        if (!node) return;
        visible = visible !== false;
        /* aria-hidden stays true because wiki decorations are purely visual. */
        if (node.hidden !== !visible) node.hidden = !visible;
        if (node.style.display !== (visible ? '' : 'none')) node.style.display = visible ? '' : 'none';
        node.setAttribute('data-decoration-visible', visible ? '1' : '0');
    }
 
    function updateDecorationVisibility(root) {
        var scope = root && root.querySelectorAll ? root : document;
        var count = 0;
        Array.prototype.forEach.call(scope.querySelectorAll('[' + RENDERED_ATTR + '="1"]'), function (node) {
            setDecorationNodeVisibility(node, isDecorationNodeActiveForNationsState(node));
            count += 1;
        });
        return count;
    }
 
    function hasRenderedDecorations(root) {
        var scope = root && root.querySelector ? root : document;
        return !!(scope && scope.querySelector && scope.querySelector('[' + RENDERED_ATTR + '="1"]'));
    }
 
    function applyPixelJsonDecoration(entry, target, visible) {
        var ref = getPixelJsonRef(entry);
        var template;
        var canvas;
        var ctx;
        if (!ref) return false;
 
        function makeCanvasFromTemplate(source) {
            var out = document.createElement('canvas');
            var outCtx;
            out.className = 'wiki-decoration wiki-decoration-pixel-json';
            out.setAttribute(RENDERED_ATTR, '1');
            out.setAttribute('aria-hidden', 'true');
            out.setAttribute('data-decoration-id', String(entry.id || ''));
            out.setAttribute('data-decoration-asset-type', 'pixel-json');
            if (entry.placement) out.setAttribute('data-decoration-placement', String(entry.placement));
            if (entry.era) out.setAttribute('data-decoration-era', String(entry.era));
            if (entry.continent) out.setAttribute('data-decoration-continent', String(entry.continent));
            out.width = source.width;
            out.height = source.height;
            out.style.imageRendering = 'pixelated';
            out.style.width = source.width + 'px';
            out.style.height = source.height + 'px';
            out.setAttribute('data-decoration-asset-size', source.width + 'x' + source.height);
            outCtx = out.getContext('2d');
            outCtx.imageSmoothingEnabled = false;
            outCtx.drawImage(source, 0, 0);
            applyDecorationBaseStyle(out, entry, target);
            setDecorationNodeVisibility(out, visible);
            return out;
        }
 
        template = getPreparedPixelCanvasSync(ref);
        if (template) {
            target.appendChild(makeCanvasFromTemplate(template));
            return true;
        }
 
        /* Fallback path only.  A full entry pack should prepare the template before the
          normal UI is released, so users should not see a blank decoration canvas. */
        preparePixelCanvas(ref).then(function (source) {
            if (!target || !target.parentNode || !source) return;
            target.appendChild(makeCanvasFromTemplate(source));
        }).catch(function () {});
 
        return true;
    }
 
    function preloadPixelAssetsForRegistry(registry) {
        var promises = [];
        decorationList(registry).forEach(function (entry) {
            var ref;
            if (!entry || !matchesPage(entry) || !isPixelJsonDecoration(entry)) return;
            ref = getPixelJsonRef(entry);
            if (!ref) return;
            promises.push(preparePixelCanvas(ref).catch(function () { return null; }));
        });
        return Promise.all(promises);
    }
 
    function decorationList(registry) {
        if (!registry || typeof registry !== 'object') return [];
        if (Array.isArray(registry)) return registry;
        if (Array.isArray(registry.decorations)) return registry.decorations;
        return [];
    }
 
    function matchesPage(entry) {
        var page = currentPageKey();
        var underscored = page.replace(/ /g, '_');
        var pages = entry && entry.pages;
        var target = entry && entry.page;
        var i;
 
        if (normalizeBool(entry && entry.global, false)) return true;
        if (String(entry && entry.placement || '').trim() === 'boot-gate' || String(entry && entry.placement || '').trim() === 'loading-screen') {
            return true;
        }
        if (normalizePageName(target).toLowerCase() === '__boot__' || normalizePageName(target).toLowerCase() === 'loading-screen') return true;
        if (!target && !pages) return true;
 
        if (Array.isArray(pages)) {
            for (i = 0; i < pages.length; i += 1) {
                if (normalizePageName(pages[i]) === page || String(pages[i] || '') === underscored) return true;
            }
        }
 
        target = normalizePageName(target);
        return target === page || target === underscored;
    }
 
    function clearRendered(root) {
        var scope = root && root.querySelectorAll ? root : document;
        Array.prototype.forEach.call(scope.querySelectorAll('[' + RENDERED_ATTR + '="1"]'), function (node) {
            if (node.parentNode) node.parentNode.removeChild(node);
        });
    }
 
    function ensureHost(target) {
        var style;
        if (!target) return;
        target.setAttribute(HOST_ATTR, '1');
        style = window.getComputedStyle ? window.getComputedStyle(target) : null;
        if (style && style.position === 'static') target.style.position = 'relative';
    }
 
    function applyDecoration(entry) {
        var resolved;
        var target;
        var src;
        var img;
        var width;
        var height;
 
        if (!entry || typeof entry !== 'object' || normalizeBool(entry.enabled, true) === false) return false;
        resolved = resolveDecorationPlacement(entry);
        if (!resolved) return false;
        target = resolved.target;
        if (!target) return false;
 
        ensureHost(target);
 
        if (isPixelJsonDecoration(entry)) {
            return applyPixelJsonDecoration(entry, target, resolved.visible !== false);
        }
 
        src = normalizeSrc(entry.src);
        if (!src) return false;
 
        img = document.createElement('img');
        img.className = 'wiki-decoration';
        img.setAttribute(RENDERED_ATTR, '1');
        img.setAttribute('aria-hidden', 'true');
        img.setAttribute('alt', '');
        img.setAttribute('decoding', 'async');
        img.setAttribute('loading', 'eager');
        img.setAttribute('data-decoration-id', String(entry.id || ''));
        if (entry.placement) img.setAttribute('data-decoration-placement', String(entry.placement));
        if (entry.era) img.setAttribute('data-decoration-era', String(entry.era));
        if (entry.continent) img.setAttribute('data-decoration-continent', String(entry.continent));
        img.src = src;
 
        applyDecorationBaseStyle(img, entry, target);
        width = normalizeNumber(entry.width, NaN);
        height = normalizeNumber(entry.height, NaN);
        if (Number.isFinite(width) && width > 0) img.style.width = width + 'px';
        if (Number.isFinite(height) && height > 0) img.style.height = height + 'px';
        if (entry.objectFit) img.style.objectFit = String(entry.objectFit);
        setDecorationNodeVisibility(img, resolved.visible !== false);
 
        target.appendChild(img);
        return true;
    }
 
    function mainPageLayoutSignature() {
        var source = document.querySelector('.liberty-content-main');
        var well = ensureMainPageBodyWell();
        var sourceRect;
        var wellRect;
 
        if (!source || !well) return '';
        sourceRect = source.getBoundingClientRect();
        wellRect = well.getBoundingClientRect();
 
        return [
            Math.round((wellRect.left + normalizeNumber(well.clientLeft, 0)) - (sourceRect.left + normalizeNumber(source.clientLeft, 0))),
            Math.round((wellRect.top + normalizeNumber(well.clientTop, 0)) - (sourceRect.top + normalizeNumber(source.clientTop, 0))),
            Math.round(wellRect.width),
            Math.round(wellRect.height)
        ].join('|');
    }
 
    function isMainPageDecorationLayoutReady() {
        var portal = document.querySelector('.main-portal');
        var panel = portal && portal.querySelector('.main-body-panel');
        var topMount = portal && portal.querySelector('[data-component="category-nav"]');
 
        if (!portal || !panel || !ensureMainPageBodyWell()) return false;
        if (!document.body || !document.body.classList.contains('clbi-main-page')) return false;
        if (topMount && !topMount.querySelector('.portal-category-nav')) return false;
        return true;
    }
 
    function waitForMainPageDecorationLayout() {
        var started;
        var previous = '';
        var stableFrames = 0;
 
        if (currentPageKey() !== '대문') return Promise.resolve(true);
        started = Date.now();
 
        return new Promise(function (resolve) {
            function check() {
                var signature = mainPageLayoutSignature();
 
                if (signature && signature === previous) stableFrames += 1;
                else stableFrames = 0;
                previous = signature;
 
                if ((isMainPageDecorationLayoutReady() && stableFrames >= 2) || Date.now() - started >= 1200) {
                    resolve(true);
                    return;
                }
 
                if (window.requestAnimationFrame) window.requestAnimationFrame(check);
                else window.setTimeout(check, 16);
            }
 
            check();
        });
    }
 
    function rebaseLegacyMainPageDecorations() {
        var well;
 
        if (currentPageKey() !== '대문') return;
        well = ensureMainPageBodyWell();
        if (!well) return;
 
        document.querySelectorAll('[' + RENDERED_ATTR + '="1"][data-decoration-coordinate-source]').forEach(function (node) {
            var position = getDecorationLocalPosition({
                target: node.getAttribute('data-decoration-coordinate-source') || '.liberty-content-main',
                x: normalizeNumber(node.getAttribute('data-decoration-source-x'), 0),
                y: normalizeNumber(node.getAttribute('data-decoration-source-y'), 0)
            }, well);
 
            node.style.left = position.x + 'px';
            node.style.top = position.y + 'px';
            node.setAttribute('data-decoration-local-x', String(position.x));
            node.setAttribute('data-decoration-local-y', String(position.y));
        });
    }
 
    function scheduleMainPageDecorationRebase() {
        if (scheduledMainPageRebase) return;
        scheduledMainPageRebase = window.requestAnimationFrame ? window.requestAnimationFrame(function () {
            scheduledMainPageRebase = 0;
            rebaseLegacyMainPageDecorations();
        }) : window.setTimeout(function () {
            scheduledMainPageRebase = 0;
            rebaseLegacyMainPageDecorations();
        }, 0);
    }
 
    function bindMainPagePlacementRefresh() {
        var portal = document.querySelector('.main-portal');
        var nodes;
 
        if (currentPageKey() !== '대문' || !portal) {
            if (mainPagePlacementObserver) mainPagePlacementObserver.disconnect();
            if (mainPagePlacementMutationObserver) mainPagePlacementMutationObserver.disconnect();
            observedMainPagePortal = null;
            return;
        }
 
        if (observedMainPagePortal !== portal) {
            if (mainPagePlacementObserver) mainPagePlacementObserver.disconnect();
            if (mainPagePlacementMutationObserver) mainPagePlacementMutationObserver.disconnect();
            observedMainPagePortal = portal;
 
            if (typeof ResizeObserver === 'function') {
                mainPagePlacementObserver = new ResizeObserver(scheduleMainPageDecorationRebase);
                nodes = [
                    document.querySelector('.liberty-content-main'),
                    portal,
                    portal.querySelector('[data-component="category-nav"]'),
                    portal.querySelector('.main-body-panel'),
                    portal.querySelector('.main-body-well')
                ];
                nodes.forEach(function (node) {
                    if (node) mainPagePlacementObserver.observe(node);
                });
            }
 
            /*
            장식 좌표는 크기 변화에만 의존한다. portal subtree DOM 변경을 감시하면
            프레임 SVG와 선언문 애니메이션이 장식 재배치를 반복 호출한다.
            ResizeObserver와 명시적 렌더 시점만 사용한다.
            */
            mainPagePlacementMutationObserver = null;
        }
 
        scheduleMainPageDecorationRebase();
    }
 
    function renderForCurrentLayout(registry, token) {
        if (currentPageKey() !== '대문') {
            if (token === runtimeToken) render(registry);
            return Promise.resolve(registry);
        }
 
        return waitForMainPageDecorationLayout().then(function () {
            if (token === runtimeToken) render(registry);
            return registry;
        });
    }
 
    function render(registry) {
        var token = runtimeToken;
        lastRegistry = registry || { decorations: [] };
        lastRenderedPageKey = currentPageKey();
        bindNationsPlacementRefresh();
        clearRendered(document);
        decorationList(lastRegistry).forEach(function (entry) {
            if (token === runtimeToken && matchesPage(entry)) applyDecoration(entry);
        });
        bindMainPagePlacementRefresh();
    }
 
    function renderPrepared() {
        var registry = getPreparedRegistrySync() || lastRegistry;
        var token = runtimeToken;
        if (!registry) return false;
        renderForCurrentLayout(registry, token);
        return true;
    }
 
    function syncDecorationState() {
        if (lastRenderedPageKey === currentPageKey() && hasRenderedDecorations(document)) {
            updateDecorationVisibility(document);
            return true;
        }
        if (lastRegistry) {
            renderForCurrentLayout(lastRegistry, runtimeToken);
            return true;
        }
        return renderPrepared();
    }
 
    function scheduleRenderFromCache() {
        if (scheduledRender) return;
        scheduledRender = window.requestAnimationFrame ? window.requestAnimationFrame(function () {
            scheduledRender = 0;
            if (!syncDecorationState()) reload();
        }) : window.setTimeout(function () {
            scheduledRender = 0;
            if (!syncDecorationState()) reload();
        }, 0);
    }
 
    function scheduleDecorationVisibilityUpdate() {
        if (scheduledRender) return;
        scheduledRender = window.requestAnimationFrame ? window.requestAnimationFrame(function () {
            scheduledRender = 0;
            if (lastRenderedPageKey !== currentPageKey() || !hasRenderedDecorations(document)) {
                if (!syncDecorationState()) reload();
                return;
            }
            updateDecorationVisibility(document);
        }) : window.setTimeout(function () {
            scheduledRender = 0;
            if (lastRenderedPageKey !== currentPageKey() || !hasRenderedDecorations(document)) {
                if (!syncDecorationState()) reload();
                return;
            }
            updateDecorationVisibility(document);
        }, 0);
    }
 
    function bindNationsPlacementRefresh() {
        var stack = document.querySelector('.clbi-nations-panel-stack');
 
        if (!stack || observedNationsStack === stack) return;
        observedNationsStack = stack;
 
        if (nationsPlacementObserver) {
            nationsPlacementObserver.disconnect();
        }
 
        if (typeof MutationObserver === 'function') {
            nationsPlacementObserver = new MutationObserver(scheduleDecorationVisibilityUpdate);
            nationsPlacementObserver.observe(stack, {
                subtree: true,
                attributes: true,
                attributeFilter: ['class', 'hidden', 'aria-selected', 'data-current-era', 'data-nations-current-era', 'data-era-year']
            });
        }
    }
 
    function getPreparedRegistrySync() {
        var registry = null;
        var url;
        try {
            if (window.EntryStore && typeof window.EntryStore.getJsonSync === 'function') {
                registry = window.EntryStore.getJsonSync(REGISTRY_TITLE);
                if (!registry && mw && mw.util && typeof mw.util.getUrl === 'function') {
                    url = mw.util.getUrl(REGISTRY_TITLE, { action: 'raw', ctype: 'application/json' });
                    registry = window.EntryStore.getJsonSync(url);
                }
            }
        } catch (err) {}
        return registry && typeof registry === 'object' ? registry : null;
    }
 
    function fetchRegistry() {
        var url;
        var prepared = getPreparedRegistrySync();
        if (prepared) return Promise.resolve(prepared);
        if (!mw || !mw.util || typeof fetch !== 'function') return Promise.resolve({ decorations: [] });
        url = mw.util.getUrl(REGISTRY_TITLE, {
            action: 'raw',
            ctype: 'application/json'
        });
        return fetch(url, { credentials: 'same-origin', cache: 'force-cache' })
            .then(function (response) {
                if (!response.ok) throw new Error('HTTP ' + response.status);
                return response.text();
            })
            .then(function (text) {
                if (!text || !text.trim()) return { decorations: [] };
                return JSON.parse(text);
            })
            .catch(function () {
                return { decorations: [] };
            });
    }
 
    function reload() {
        var token;
        runtimeToken += 1;
        token = runtimeToken;
        return fetchRegistry().then(function (registry) {
            return preloadPixelAssetsForRegistry(registry).then(function () {
                return renderForCurrentLayout(registry, token);
            });
        });
    }
 
    document.addEventListener('click', function (event) {
        var target = event.target && event.target.closest ? event.target.closest('.clbi-nations-tabpanel-tab[data-continent], [data-nations-era-move]') : null;
        if (target) window.setTimeout(scheduleDecorationVisibilityUpdate, 0);
    }, true);
 
    function decorationDiagnostics() {
        return {
            build: '20260713-main-body-well-coordinate-003',
            hasRegistry: !!lastRegistry,
            entries: decorationList(lastRegistry).length,
            rendered: document.querySelectorAll('[' + RENDERED_ATTR + '="1"]').length,
            preparedRegistry: !!getPreparedRegistrySync(),
            pixelAssets: Object.keys(pixelAssetCache || {}).length,
            pixelCanvases: Object.keys(pixelCanvasCache || {}).length,
            visibilityOnlySync: true,
            mainPageCoordinateSurface: MAIN_PAGE_TARGET,
            mainPagePlacementObserved: !!observedMainPagePortal,
            lastRenderedPage: lastRenderedPageKey,
            visibleRendered: document.querySelectorAll('[' + RENDERED_ATTR + '="1"][data-decoration-visible="1"]').length,
            scheduled: !!scheduledRender
        };
    }
 
    window.Decorations = window.Decorations || {};
    window.Decorations.reload = reload;
    window.Decorations.render = render;
    window.Decorations.renderPrepared = renderPrepared;
    window.Decorations.sync = syncDecorationState;
    window.Decorations.updateVisibility = updateDecorationVisibility;
    window.Decorations.diagnostics = decorationDiagnostics;
    window.Decorations.clear = clearRendered;
    window.Decorations.apply = applyDecoration;
    window.Decorations.pageKey = currentPageKey;
    window.Decorations.loadPixelAsset = fetchPixelAsset;
    window.Decorations.preparePixelCanvas = preparePixelCanvas;
    window.Decorations.getPreparedPixelCanvasSync = getPreparedPixelCanvasSync;
    window.Decorations.drawPixelAssetToCanvas = drawPixelAssetToCanvas;
 
    window.CLBI_DECORATIONS = window.CLBI_DECORATIONS || {};
    window.CLBI_DECORATIONS.reload = reload;
    window.CLBI_DECORATIONS.render = render;
    window.CLBI_DECORATIONS.renderPrepared = renderPrepared;
    window.CLBI_DECORATIONS.sync = syncDecorationState;
    window.CLBI_DECORATIONS.updateVisibility = updateDecorationVisibility;
    window.CLBI_DECORATIONS.diagnostics = decorationDiagnostics;
    window.CLBI_DECORATIONS.clear = clearRendered;
    window.CLBI_DECORATIONS.apply = applyDecoration;
    window.CLBI_DECORATIONS.pageKey = currentPageKey;
    window.CLBI_DECORATIONS.loadPixelAsset = fetchPixelAsset;
    window.CLBI_DECORATIONS.preparePixelCanvas = preparePixelCanvas;
    window.CLBI_DECORATIONS.getPreparedPixelCanvasSync = getPreparedPixelCanvasSync;
    window.CLBI_DECORATIONS.drawPixelAssetToCanvas = drawPixelAssetToCanvas;
 
    if (document.readyState === 'loading') {
        document.addEventListener('DOMContentLoaded', reload);
    } else {
        reload();
    }
 
    if (mw && mw.hook) {
        mw.hook('wikipage.content').add(function () {
            reload();
        });
    }
})(mediaWiki);
 
/* =========================================
  Unified Shortcuts loader
  ========================================= */
(function () {
    'use strict';
 
    if (!window.mw || !mw.loader) return;
    loadClbiRawScript('MediaWiki:Nation_List_Manager.js');
    loadClbiRawScript('MediaWiki:History_Event_Manager.js');
    loadClbiRawScript('MediaWiki:Shortcuts.js');
})();
 
/* =========================================
  Main page manifesto bitmap renderer
  Integrated build: no extra raw-script page, no image/data-URL decoding.
  New shared systems do not use the legacy CLBI prefix.
  ========================================= */
(function (window, document, mw) {
    'use strict';
 
    /*
    Retired compatibility block
    -----------------------------------------
    선언문은 실제 HTML과 ManifestoIntro의 실선 요소만 사용한다.
    이 구식 렌더러는 제목·본문과 완성형 구분선을 canvas에 한 번에 그려
    새 애니메이션의 선 뒤에 트랙이 나타날 수 있었다.
 
    기존 Common.js를 장기 캐시한 뒤 새 파일로 넘어오는 경우를 위해
    남아 있는 canvas와 상태만 제거하고 즉시 종료한다. 새 공개 이름에는
    프로젝트 접두사를 사용하지 않는 규칙을 유지한다.
    */
    function removeRetiredBitmap() {
        Array.prototype.forEach.call(
            document.querySelectorAll('.main-portal .main-manifesto-bitmap'),
            function (canvas) {
                if (canvas.parentNode) canvas.parentNode.removeChild(canvas);
            }
        );
 
        Array.prototype.forEach.call(
            document.querySelectorAll('.main-portal .main-manifesto-inner'),
            function (inner) {
                inner.classList.remove('is-bitmap-ready');
                [
                    'data-bitmap-width',
                    'data-bitmap-optical-shift-x',
                    'data-bitmap-state',
                    'data-bitmap-error'
                ].forEach(function (name) {
                    inner.removeAttribute(name);
                });
            }
        );
    }
 
    removeRetiredBitmap();
 
    try {
        if (mw && mw.hook) {
            mw.hook('wikipage.content').add(removeRetiredBitmap);
        }
    } catch (ignoreRetiredBitmapHook) {}
 
    window.MainPageBitmap = {
        version:'retired-20260713-divider-no-track-001',
        recalculate:removeRetiredBitmap,
        status:function () {
            return {
                integrated:false,
                state:'retired',
                canvas:!!document.querySelector('.main-portal .main-manifesto-bitmap')
            };
        }
    };
 
    return;
 
    var VERSION = '20260712-bitmap18-inline-002';
    var SELECTOR = '.main-portal .main-manifesto-inner';
    var CELL_W = 24;
    var CELL_H = 24;
    var GLYPHS = {"진":[19,0,0,0,0,0,12288,24576,24576,25472,25584,25056,25072,25568,26160,26136,24576,9088,8960,768,256,32512,15872,0,0],"창":[19,0,0,0,0,0,6144,12512,12480,12800,13280,78328,258272,12768,14128,13848,4360,3840,7040,12672,4480,8064,3840,0,0],"바":[19,0,0,0,0,0,6144,12288,12288,12672,12672,14080,13056,13064,258456,127472,12784,12504,12408,12312,12288,12288,4096,0,0],"다":[19,0,0,0,0,0,6144,12288,12288,12288,13056,12792,12400,12320,258096,128528,14328,13048,12304,12288,12288,12288,4096,0,0],"는":[19,0,0,0,0,0,0,96,192,192,14528,16064,960,0,130048,118780,56,448,448,128,128,16256,7936,0,0],"왕":[19,0,0,0,0,0,12288,28768,25568,25456,25392,25392,254944,24960,16064,10236,12344,7680,14080,12672,12672,16128,7936,0,0],"과":[19,0,0,0,0,0,12288,28672,24576,26560,26352,26112,26112,26208,255680,254912,29248,32736,25084,8192,8192,8192,8192,0,0],"노":[19,0,0,0,0,0,0,0,0,96,192,192,4288,15552,16320,7040,7168,3072,50688,131064,124,0,0,0,0],"예":[19,0,0,0,0,0,24576,60416,22528,22528,22544,24048,24504,23320,23320,24504,23024,22528,22528,18432,16384,16384,24576,0,0],"를":[19,0,0,0,0,0,7168,16320,6144,8128,448,15552,1984,122880,262140,16636,8128,8128,3840,8064,1792,960,65408,4096,0],"같":[19,0,0,0,0,0,6144,12288,12288,13248,13304,110976,127168,12384,12344,4104,3584,0,8064,1920,256,14720,8064,0,0],"은":[19,0,0,0,0,0,384,3968,6528,6336,6272,3968,1792,32768,130944,115708,16,448,448,128,128,16256,7936,0,0],"깊":[19,0,0,0,0,0,12288,24576,24576,26496,25592,25472,25024,24800,24624,8216,12288,16256,4864,15872,7680,31232,32640,256,0],"이":[19,0,0,0,0,0,12288,24576,24576,24576,24592,25072,25584,25368,25368,25496,25072,24640,24576,24576,24576,8192,8192,0,0],"로":[19,0,0,0,0,0,0,0,15360,16320,6272,7168,8128,384,14528,16320,7040,3072,50688,131064,124,0,0,0,0],"삼":[19,0,0,0,0,0,6144,12288,12352,12736,12480,78064,258272,13296,14136,13324,4096,16256,16256,4224,4480,8064,8064,0,0],"켰":[19,0,0,0,0,0,12288,24576,26112,26600,29560,32656,25072,31992,26720,8224,8216,13056,15104,5056,15232,28544,58992,0,0],"고":[19,0,0,0,0,0,0,0,0,7680,16352,12416,12288,12288,12544,13056,6912,4864,49920,131064,124,0,0,0,0],",":[8,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,12,12,8,0,0,0],"겨":[19,0,0,0,0,0,12288,24576,24576,24576,25344,26616,32560,31104,24768,31840,31792,24600,24584,24576,24576,24576,8192,0,0],"울":[19,0,0,0,0,0,1792,8064,6528,6272,7552,3840,114688,262140,18428,1536,16320,8128,3840,8064,1920,896,65280,0,0],"사":[19,0,0,0,0,0,6144,12288,12288,12352,12480,12480,12480,12400,258272,127920,14104,13828,13312,12288,12288,12288,4096,0,0],"람":[19,0,0,0,0,0,6144,12288,12288,13296,13240,78208,258552,12304,14224,13296,4096,15744,16256,4288,4480,8064,8064,0,0],"짐":[19,0,0,0,0,0,12288,24576,24576,25472,25584,24992,25072,26592,26160,9756,0,32512,32640,12672,12672,16128,16128,0,0],"승":[19,0,0,0,0,0,512,3584,1536,1792,8064,29056,24800,32768,130816,115708,272,3840,7040,4224,6272,8064,3840,0,0],"의":[19,0,0,0,0,0,28672,57344,24608,25536,26592,26160,26160,26464,25568,24576,24576,32704,25596,24576,24576,24576,8192,0,0],"살":[19,0,0,0,0,0,6144,12288,12352,12736,12480,110832,127456,13232,13848,5132,8064,8064,3840,8064,1792,896,65024,0,0],"을":[19,0,0,0,0,0,1792,8064,6528,6272,7552,3840,32768,130944,115708,0,16320,8128,3840,8064,1920,896,65280,0,0],"빛":[19,0,0,0,0,0,12288,24576,25344,25344,26376,25496,25592,25584,25552,25088,7168,12288,32640,6400,7680,32256,25472,0,0],"으":[19,0,0,0,0,0,0,0,128,3968,8064,4544,4288,6336,7616,3968,0,0,57344,131068,56,0,0,0,0],"얼":[19,0,0,0,0,0,12288,24576,24592,24816,25008,32536,31512,25016,25072,8192,16256,16256,7680,16128,3584,1792,130560,0,0],"렸":[19,0,0,0,0,0,12288,24576,24576,25584,32568,27008,31224,31792,26544,9200,8192,13056,15104,4928,15232,28544,58976,0,0],".":[8,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,12,12,0,0,0,0],"그":[19,0,0,0,0,0,0,0,0,7168,16352,12672,12288,12288,12288,12288,6144,4096,57344,131068,56,0,0,0,0],"들":[19,0,0,0,0,0,6144,16320,896,384,14528,16320,960,130048,118780,120,16320,8128,3840,8064,1920,896,65280,0,0],"돌":[19,0,0,0,0,0,6144,16320,896,384,14528,16320,8128,130048,253948,16504,16320,8128,3840,8064,1920,896,65280,0,0],"벽":[19,0,0,0,0,0,12288,24576,24832,25344,32520,31512,25560,32632,25584,9104,8192,31744,32640,8192,8192,8192,8192,0,0],"세":[19,0,0,0,0,0,24576,60416,22528,22528,22720,22720,22752,24432,22752,22960,23320,23300,22528,18432,16384,16384,24576,0,0],"우":[19,0,0,0,0,0,0,384,7936,6528,12480,6336,7552,3968,0,131008,116732,1552,1536,1536,1536,1536,512,0,0],"땅":[19,0,0,0,0,0,12288,28672,24576,28560,9200,74160,254128,30936,16376,12280,12544,7936,13184,12672,12672,8064,3840,0,0],"찢":[19,0,0,0,0,0,12288,24576,24704,26616,28656,26560,25456,28656,28056,26848,8192,32512,16128,7680,15872,25344,16768,0,0],"어":[19,0,0,0,0,0,12288,24576,24576,24576,24592,24816,25520,32536,27416,25496,25072,24640,24576,24576,24576,8192,8192,0,0],"길":[19,0,0,0,0,0,12288,24576,24576,26496,25592,25472,24768,24672,24624,8216,16256,16128,7680,16128,3584,1792,130560,0,0],"냈":[19,0,0,0,0,0,8192,58368,19456,27672,27704,31792,31792,32560,28656,27120,8704,13824,14080,13248,15232,28544,50272,0,0],"며":[19,0,0,0,0,0,12288,24576,24576,24576,25472,26600,32536,25368,25400,32560,25584,25056,24576,24576,24576,24576,8192,0,0],"쇠":[19,0,0,0,0,0,28672,57344,24704,24960,24960,25056,25536,26208,27888,24960,24960,32704,25596,24576,24576,24576,8192,0,0],"에":[19,0,0,0,0,0,24576,60416,22528,22528,22544,23024,23480,24344,24344,22968,23024,22528,22528,18432,16384,16384,24576,0,0],"제":[19,0,0,0,0,0,24576,60416,22528,22528,23488,23032,22976,24512,24184,23024,23344,23320,23052,18432,16384,16384,24576,0,0],"름":[19,0,0,0,0,0,6144,16320,6272,8128,960,14528,16320,384,130560,116732,56,15872,16320,4288,4544,7552,8064,0,0],"새":[19,0,0,0,0,0,24576,60416,23552,19456,27840,27840,31968,31856,23776,23984,20248,20228,16384,16384,16384,16384,24576,0,0],"겼":[19,0,0,0,0,0,12288,24576,24576,26496,30712,32640,25024,31968,26672,8216,8200,15104,15104,6976,15232,28608,58992,0,0],"감":[19,0,0,0,0,0,6144,12288,12288,13184,13304,78208,258240,12384,12336,4120,4096,16256,16320,4224,4480,8064,8064,0,0],"옥":[19,0,0,0,0,0,1792,7936,6528,6272,6528,8064,3584,1536,130816,116732,56,16128,15296,4096,4096,4096,4096,0,0],"되":[19,0,0,0,0,0,28672,57344,25088,26608,25072,24672,25632,26416,26608,25456,24960,32704,25596,24576,24576,24576,8192,0,0],"군":[19,0,0,0,0,0,0,7680,16352,12288,12288,12288,6144,126976,262140,18172,1536,1728,1472,192,192,16256,8064,0,0],"대":[19,0,0,0,0,0,24576,60416,23552,19456,28032,28144,31856,31776,23600,24344,20440,19960,16400,16384,16384,16384,24576,0,0],"나":[19,0,0,0,0,0,6144,12288,12288,12288,12296,12312,12336,12304,259600,128784,14256,12528,12320,12288,12288,12288,4096,0,0],"르":[19,0,0,0,0,0,0,0,6144,16320,6336,7168,8128,448,12480,16320,3968,0,57344,131068,56,0,0,0,0],"오":[19,0,0,0,0,0,0,0,256,7936,15232,12672,12416,12672,8064,7936,3072,1024,49664,131064,124,0,0,0,0],"래":[19,0,0,0,0,0,24576,60416,23552,19456,28144,27064,32128,32248,23664,23568,20240,20464,16880,16384,16384,16384,24576,0,0],"된":[19,0,0,0,0,0,28672,57344,26368,25584,24800,25696,28656,26608,29440,32736,25080,24832,25344,768,768,65280,15872,0,0],"원":[19,0,0,0,0,0,28672,57440,25536,26464,26160,26160,29664,32640,26592,26620,25360,25024,25536,768,768,65280,15872,0,0],"한":[19,0,0,0,0,0,6144,28704,12512,12800,14328,12784,258528,13088,12592,12784,12512,12416,4544,384,128,16256,7936,0,0],"함":[19,0,0,0,0,0,6144,12512,12480,12800,14328,78032,258528,13104,12720,12784,4192,15744,16256,4288,4480,8064,8064,0,0],"께":[19,0,0,0,0,0,24576,55296,55296,55296,55488,57336,57280,57184,57184,55600,55704,55500,55392,49152,49152,16384,16384,0,0],"음":[19,0,0,0,0,0,1920,8064,6528,6336,6336,3968,1792,32768,131040,16892,0,16128,16320,4288,4544,7552,8064,0,0],"손":[19,0,0,0,0,0,512,1536,1536,1920,7936,14720,26304,1584,130560,249852,120,448,448,128,128,16256,7936,0,0],"넘":[19,0,0,0,0,0,12288,24576,24576,24600,31792,31792,24624,28208,26608,8416,0,32512,32640,12672,12672,16256,15616,0,0],"갔":[19,0,0,0,0,0,6144,12288,12288,13184,13304,78208,258240,12384,12336,12312,4352,15104,6912,6528,7616,30656,25136,0,0],"\n":[8,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],"줄":[19,0,0,0,0,0,6144,16320,3520,3840,15296,12480,41056,130976,116732,1536,16320,8128,3840,8064,1920,896,65280,0,0],"처":[19,0,0,0,0,0,12288,24576,24800,25024,25344,26592,25072,32448,27616,25392,25360,24840,24832,24960,24960,8320,8192,0,0],"부":[19,0,0,0,0,0,6144,12288,28768,14400,15552,13248,8128,7872,0,131008,116732,1552,1536,1536,1536,1536,512,0,0],"터":[19,0,0,0,0,0,12288,24576,24576,25056,24768,25344,25584,31984,31840,25648,28568,26360,24624,24576,24576,8192,8192,0,0],"목":[19,0,0,0,0,0,14336,32512,15328,6336,3264,16064,7296,3072,130816,116732,56,16128,15296,4096,4096,4096,4096,0,0],"둘":[19,0,0,0,0,0,6144,16320,896,384,14528,16320,960,130944,116732,1592,16320,8128,3840,8064,1920,896,65280,0,0],"레":[19,0,0,0,0,0,24576,60416,22528,22528,23024,22968,22912,24568,22640,22544,23312,23536,23024,18432,16384,16384,24576,0,0],"있":[19,0,0,0,0,0,12288,24576,24576,24816,25072,25400,25368,25368,25072,8416,8192,15104,15104,7104,14720,28608,26224,0,0],"었":[19,0,0,0,0,0,12288,24576,24576,24816,25072,32568,32536,25496,25072,8416,8192,15104,15104,7104,14720,28608,26224,0,0],"끝":[19,0,0,0,0,0,0,14720,16368,13152,12672,12672,4480,130560,116732,56,3840,3584,8128,1920,384,16320,4032,0,0],"속":[19,0,0,0,0,0,512,3584,1536,1792,7936,12672,26336,1568,130560,116732,56,16128,15296,4096,4096,4096,4096,0,0],"뿌":[19,0,0,0,0,0,4096,30720,14080,15904,16352,13280,16320,14208,4608,130944,126972,1552,1536,1536,1536,1536,512,0,0],"리":[19,0,0,0,0,0,12288,24576,24576,24576,25568,25528,24960,25040,24688,25648,26160,26608,25056,24576,24576,8192,8192,0,0],"와":[19,0,0,0,0,0,12288,28672,24608,25568,25456,26160,26416,25584,254432,254336,28864,32736,25084,8192,8192,8192,8192,0,0],"빨":[19,0,0,0,0,0,12288,28672,26112,26176,26304,255944,255992,10200,10224,1728,16256,16256,7936,7936,1792,896,65024,0,0],"굶":[19,0,0,0,0,0,0,16128,13248,12288,12288,6144,118784,262140,18428,1536,14080,16368,13792,14272,14272,16352,16352,0,0],"주":[19,0,0,0,0,0,0,14336,16320,3264,1792,7936,12736,24672,32,131008,116732,1552,1536,1536,1536,1536,512,0,0],"림":[19,0,0,0,0,0,12288,24576,24576,25584,25400,25472,25072,24624,26544,9200,8192,15616,32640,12672,12672,16256,16128,0,0],"매":[19,0,0,0,0,0,24576,60416,23552,19456,28544,28644,32156,32024,23832,23992,19952,19504,16384,16384,16384,16384,24576,0,0],"였":[19,0,0,0,0,0,12288,24576,24576,24816,32752,32568,25368,32664,25072,8416,8192,15104,15104,7104,14720,28608,26224,0,0],"른":[19,0,0,0,0,0,4096,16320,6336,7744,4032,14528,16320,896,126976,253948,120,64,448,128,128,16256,7936,0,0],"아":[19,0,0,0,0,0,6144,12288,12288,12288,12304,12528,12720,13080,258840,127896,12784,12352,12288,12288,12288,12288,4096,0,0],"직":[19,0,0,0,0,0,12288,24576,24576,25472,25584,24992,25072,25568,26160,9752,0,31744,32640,8192,8192,12288,12288,0,0],"태":[19,0,0,0,0,0,24576,60416,23552,19680,27840,28032,32240,31856,23600,24336,20440,20472,16432,16384,16384,16384,24576,0,0],"지":[19,0,0,0,0,0,12288,24576,24576,24576,26496,25584,24992,24768,24816,25568,26160,27672,25612,24576,24576,24576,8192,0,0],"않":[19,0,0,0,0,0,6144,12288,12304,12528,12784,111384,258840,13208,12784,4160,7168,6240,32352,15456,13408,14176,8160,0,0],"자":[19,0,0,0,0,0,6144,12288,12288,12288,13184,13304,12720,12480,258296,127984,14128,13848,13324,12288,12288,12288,4096,0,0],"졌":[19,0,0,0,0,0,12288,24576,24576,25472,32752,31136,32240,32736,26160,9756,8704,13056,15104,4928,15232,28544,50784,0,0],"앞":[19,0,0,0,0,0,6144,12288,12304,12528,12784,111384,258840,13208,12784,4160,6144,8128,2432,7936,2816,15616,16320,128,0],"갈":[19,0,0,0,0,0,6144,12288,12288,13248,13304,127360,127168,12384,12344,4104,16256,8064,3840,8064,1792,896,65280,0,0],"때":[19,0,0,0,0,0,24576,52224,55296,55296,57096,56312,63920,63632,63704,56536,57336,57336,49216,49152,49152,16384,16384,0,0],"마":[19,0,0,0,0,0,6144,12288,12288,12288,13184,13284,13084,13080,258840,127792,12784,12464,12288,12288,12288,12288,4096,0,0],"듭":[19,0,0,0,0,0,6144,16320,1984,384,12480,16064,3008,32768,131008,115708,14336,12352,12480,14720,16256,8064,7808,0,0],"파":[19,0,0,0,0,0,6144,12288,12288,12288,13248,12792,12688,13296,258528,127392,16352,12412,12288,12288,12288,12288,4096,0,0],"각":[19,0,0,0,0,0,6144,12288,12288,13184,13304,12672,258240,12384,12336,4120,4096,16128,13248,12288,4096,4096,4096,0,0],"쥔":[19,0,0,0,0,0,28672,57856,26608,25584,25024,26592,28208,27664,28544,28668,25360,24960,25536,768,768,65280,15872,0,0],"당":[19,0,0,0,0,0,6144,12288,12288,13248,12536,77920,258096,15888,14328,12920,4352,7936,7040,12672,4480,8064,3840,0,0],"기":[19,0,0,0,0,0,12288,24576,24576,24576,25344,26616,25392,24960,24768,24672,24624,24600,24584,24576,24576,24576,8192,0,0],"면":[19,0,0,0,0,0,12288,24576,24576,26496,26604,32536,25368,32560,31728,25072,24576,25344,8960,768,768,32512,15872,0,0],"풀":[19,0,0,0,0,0,12288,16352,7296,8064,3328,32704,41440,131008,116732,1536,16320,8128,3840,8064,1920,896,65280,0,0],"려":[19,0,0,0,0,0,12288,24576,24576,24576,25568,32696,31104,25040,31856,27696,26160,26608,25056,24576,24576,8192,8192,0,0],"날":[19,0,0,0,0,0,6144,12288,12288,12312,12344,110640,128560,14256,13296,4192,8064,8064,3840,8064,1792,896,65024,0,0],"것":[19,0,0,0,0,0,12288,24576,24576,26496,25592,24960,32704,26848,24624,24600,11272,3072,1792,3584,15104,29056,24800,0,0],"라":[19,0,0,0,0,0,6144,12288,12288,12288,13280,12728,12672,12792,258168,127504,13840,14320,12784,12288,12288,12288,4096,0,0],"여":[19,0,0,0,0,0,12288,24576,24576,24576,24592,24816,32688,25368,25368,32664,25072,24640,24576,24576,24576,8192,8192,0,0],"므":[19,0,0,0,0,0,0,0,4096,31744,32736,6624,3264,1728,16064,7360,0,0,57344,131068,56,0,0,0,0],"번":[19,0,0,0,0,0,12288,24576,24576,25344,26376,26392,32728,25464,25584,25552,24576,25344,8960,768,768,32512,15872,0,0],"도":[19,0,0,0,0,0,0,0,0,15360,4064,960,384,14528,16064,7136,7360,3072,50688,131064,124,0,0,0,0],"느":[19,0,0,0,0,0,0,0,0,32,192,192,192,14528,16064,7104,0,0,57344,131068,56,0,0,0,0],"슨":[19,0,0,0,0,0,512,1536,1536,1920,7936,14720,24768,48,130048,118780,56,448,448,128,128,16256,7936,0,0],"해":[19,0,0,0,0,0,24576,60416,23552,19680,27968,28664,32240,32224,23984,23984,19952,19568,16384,16384,16384,16384,24576,0,0],"았":[19,0,0,0,0,0,6144,12288,12288,12528,12784,78616,258840,13208,12784,12512,4352,15104,6912,6528,7616,30656,25136,0,0],"침":[19,0,0,0,0,0,12288,24768,25024,25088,26592,25584,24800,25568,26416,9232,8,15616,32640,12672,12672,16256,16128,0,0],"내":[19,0,0,0,0,0,24576,58368,19456,19456,27656,27672,31792,31792,31760,24368,20464,19952,16384,16384,24576,24576,8192,0,0],"멎":[19,0,0,0,0,0,12288,24576,24576,26496,25576,32536,31512,25392,25072,8224,8192,32640,6912,3584,15872,25344,16768,0,0],"서":[19,0,0,0,0,0,12288,24576,24576,24640,24768,25024,24768,32496,24800,25520,26392,26124,25600,24576,24576,8192,8192,0,0],"긴":[19,0,0,0,0,0,12288,24576,24576,26368,25592,25520,25024,24768,24688,24632,24584,25472,8960,768,768,32512,15872,0,0],"붙":[19,0,0,0,0,0,6144,12288,14432,16064,13248,7872,38912,130816,116732,1552,3840,3584,8064,1920,384,15808,4032,0,0],"잡":[19,0,0,0,0,0,6144,12288,12288,13184,13304,111040,258288,13280,13872,5660,4096,12352,12416,14720,16256,8064,7296,0,0],"피":[19,0,0,0,0,0,12288,24576,24576,24576,26496,25592,25392,25568,25440,24992,28640,26744,24576,24576,24576,24576,8192,0,0],"가":[19,0,0,0,0,0,6144,12288,12288,12288,13056,13304,12728,12736,127168,258144,12336,12312,12300,12288,12288,12288,4096,0,0],"굴":[19,0,0,0,0,0,0,16128,13248,12288,12288,6144,126976,262140,18172,1536,16320,8128,3840,8064,1920,896,65280,0,0],"보":[19,0,0,0,0,0,0,0,6144,12288,28768,14528,16064,13248,16320,7360,3072,1024,50688,131064,124,0,0,0,0],"하":[19,0,0,0,0,0,6144,12288,12320,12512,12800,14332,12792,12512,259040,127792,12720,12528,12288,12288,12288,12288,4096,0,0],"입":[19,0,0,0,0,0,12288,24576,24576,24816,25584,25368,25368,25496,25072,8256,4096,28800,24960,14720,16128,16128,15616,0,0],"말":[19,0,0,0,0,0,6144,12288,12288,13184,13308,111384,127768,13240,12784,4096,8064,8064,3840,8064,1792,896,65024,0,0],"했":[19,0,0,0,0,0,24576,58368,23776,19776,28664,27888,32224,23984,19888,18672,96,13824,15872,13184,15232,28544,50272,0,0],"조":[19,0,0,0,0,0,0,0,0,15360,8128,3072,1792,16128,28864,25184,1568,1536,50688,131064,124,0,0,0,0],"올":[19,0,0,0,0,0,1792,8064,6528,6528,8064,3840,1536,128768,253948,16504,16320,8128,3840,8064,1920,896,65280,0,0],"수":[19,0,0,0,0,0,0,1536,3072,1792,1792,6912,29056,24800,32,131008,116732,1552,1536,1536,1536,1536,512,0,0],"록":[19,0,0,0,0,0,6144,16320,6272,8128,960,14528,16320,3968,128512,253948,120,15872,16320,4096,4096,4096,6144,0,0],"열":[19,0,0,0,0,0,12288,24576,24592,24816,32688,31512,25368,32696,25072,8192,16256,16256,7680,16128,3584,1792,130560,0,0],"단":[19,0,0,0,0,0,6144,28672,12288,13184,12792,12400,258096,13872,14232,14328,12336,12672,4480,384,384,16256,7936,0,0],"질":[19,0,0,0,0,0,12288,24576,24576,26496,25584,24992,25072,26464,26160,9240,16128,16128,7680,16128,3584,1792,130560,0,0],"히":[19,0,0,0,0,0,12288,24576,24576,25024,26112,26616,25584,25024,25440,25392,25392,25056,24576,24576,24576,8192,8192,0,0],"운":[19,0,0,0,0,0,384,3968,6528,6336,6272,3968,1792,32768,131040,17916,3072,1984,1472,128,192,16256,7936,0,0],"데":[19,0,0,0,0,0,24576,60416,22528,22528,22912,23024,22640,24352,22576,23320,23512,23032,22544,18432,16384,16384,24576,0,0],"끌":[19,0,0,0,0,0,0,14720,16368,13152,12672,12672,4480,130560,116732,56,16320,8128,3840,8064,1920,896,65280,0,0],"린":[19,0,0,0,0,0,12288,24576,24576,25568,25464,24960,25592,24624,26416,26608,24672,8320,9088,256,256,32512,15872,0,0],"게":[19,0,0,0,0,0,24576,60416,22528,22528,23424,23544,22960,24000,24512,22624,22576,22552,22536,18432,16384,16384,24576,0,0],"누":[19,0,0,0,0,0,0,96,192,192,192,15552,16320,960,0,131008,116732,1552,1536,1536,1536,1536,512,0,0],"인":[19,0,0,0,0,0,12288,24576,24576,24624,25072,25400,25368,25368,25072,24800,24576,9088,8960,256,256,32512,15872,0,0],"쳐":[19,0,0,0,0,0,12288,24576,24800,25024,25344,26592,32752,31936,25568,32560,25360,24840,24832,24960,24960,8320,8192,0,0],"문":[19,0,0,0,0,0,12288,15872,15328,6336,3264,7872,7360,32768,131040,17916,3072,1984,1472,128,192,16256,7936,0,0],"몸":[19,0,0,0,0,0,14336,32512,15328,6336,3264,16064,7296,3072,130816,116732,56,16128,16320,4288,4544,7552,8064,0,0],"식":[19,0,0,0,0,0,12288,24576,24768,24960,25024,24816,24800,25568,26160,9244,0,31744,32640,8192,8192,12288,12288,0,0],"억":[19,0,0,0,0,0,12288,24576,24576,24816,25072,32536,32536,25368,25072,8416,0,31744,32640,12288,12288,12288,12288,0,0],"전":[19,0,0,0,0,0,12288,24576,24576,25472,25592,24992,31984,25568,26160,26136,24576,9088,8960,768,256,32512,15872,0,0],"품":[19,0,0,0,0,0,12288,16352,7360,8064,7040,16128,26592,127040,253948,18040,1536,16128,16320,4288,4544,7552,8064,0,0],"발":[19,0,0,0,0,0,6144,12288,12672,13056,14092,111512,127992,13176,13272,4096,8064,8064,3840,8064,1792,896,65024,0,0],"머":[19,0,0,0,0,0,12288,24576,24576,24576,25472,25576,25368,32536,27448,25392,25584,25056,24576,24576,24576,24576,8192,0,0],"두":[19,0,0,0,0,0,0,14336,8128,960,384,12480,16064,5056,192,131008,116732,1552,1536,1536,1536,1536,512,0,0],"닐":[19,0,0,0,0,0,12288,24576,24576,24600,24624,24624,27696,28464,26608,8288,16256,16256,7680,16128,3584,1792,130560,0,0],"격":[19,0,0,0,0,0,12288,24576,24576,26496,26616,32640,25024,31968,26672,8216,8200,32256,26496,8192,8192,8192,8192,0,0],"없":[19,0,0,0,0,0,12288,24576,24592,24816,25072,32536,31512,25496,25072,8256,8192,13824,15936,4928,15296,28608,51008,0,0],"니":[19,0,0,0,0,0,12288,24576,24576,24576,24584,24624,24624,24624,25648,28208,26544,25328,24576,24576,24576,8192,8192,0,0],"산":[19,0,0,0,0,0,6144,28672,12352,12480,12480,12496,258272,13280,14128,13852,12288,12672,4480,384,384,16256,7936,0,0],"복":[19,0,0,0,0,0,14336,12288,28736,15552,14272,16320,7808,3072,130560,249852,120,15872,16320,4096,4096,4096,6144,0,0],"종":[19,0,0,0,0,0,6144,16320,3520,1792,7936,12736,26208,1568,130816,116732,280,3840,7040,4224,6272,8064,3840,0,0],"죽":[19,0,0,0,0,0,6144,16320,3520,1792,7936,12736,24672,122912,262140,18172,1536,16128,15296,4096,4096,4096,4096,0,0],"망":[19,0,0,0,0,0,6144,12288,12288,13248,13308,78620,258840,13112,12784,12336,4352,7936,7040,12672,4480,8064,3840,0,0],"베":[19,0,0,0,0,0,24576,60416,22528,22912,22912,24448,24448,24456,24536,23024,23024,22776,22648,18456,16384,16384,24576,0,0],"막":[19,0,0,0,0,0,6144,12288,12288,13248,13308,13084,258840,13080,12784,12464,4096,15872,16320,12288,4096,4096,4096,0,0],"무":[19,0,0,0,0,0,0,30720,32608,15296,6336,3520,16320,15488,0,131008,116732,1552,1536,1536,1536,1536,512,0,0],"릎":[19,0,0,0,0,0,6144,16320,6272,8128,960,14528,8128,0,130560,116732,56,16320,3456,8064,3840,16128,14272,0,0],"꿇":[19,0,0,0,0,0,0,14720,16368,12640,12672,12672,118912,262140,18428,1536,7936,7152,32736,14784,11712,16352,7648,0,0],"까":[19,0,0,0,0,0,12288,28672,24576,24576,24768,10232,10176,9824,254752,254768,8600,8388,8288,8192,8192,8192,12288,0,0],"놓":[19,0,0,0,0,0,0,224,192,12480,15552,8128,7552,130048,253948,16760,1792,15872,16320,3584,6912,3328,1792,0,0],"러":[19,0,0,0,0,0,12288,24576,24576,24576,25568,25528,29056,32720,24688,25648,26160,26608,25056,24576,24576,8192,8192,0,0],"재":[19,0,0,0,0,0,24576,60416,23552,19456,28608,28152,32192,31936,23672,24048,20272,20248,16908,16384,16384,16384,24576,0,0],"받":[19,0,0,0,0,0,6144,12288,12672,13056,14092,78744,259064,13176,13304,12672,0,7936,3584,768,24960,32640,1920,0,0],"묵":[19,0,0,0,0,0,14336,32512,15328,6336,3264,16064,3200,130048,118780,1592,1536,16128,15296,4096,4096,4096,4096,0,0],"스":[19,0,0,0,0,0,0,0,512,1536,3584,1920,1792,7936,12672,24800,0,0,57344,131068,56,0,0,0,0],"릴":[19,0,0,0,0,0,12288,24576,24832,25592,25360,25536,25072,25136,26608,8432,14464,16256,7936,16128,3840,1792,130560,0,0],"계":[19,0,0,0,0,0,24576,60416,22528,22528,23424,23544,24496,24256,22720,24416,22576,22552,22536,18432,16384,16384,24576,0,0],"구":[19,0,0,0,0,0,0,6144,16352,12736,12288,12288,12288,6144,4096,131056,67324,1536,1536,1536,1536,1536,512,0,0],"물":[19,0,0,0,0,0,14336,32544,15328,3264,7872,16064,32768,131040,18428,1536,16320,8128,3840,8064,1920,896,65280,0,0],"못":[19,0,0,0,0,0,14336,32512,15328,7360,3776,16064,7296,3072,130816,116732,1048,3584,1536,1792,7936,12480,8304,0,0],"시":[19,0,0,0,0,0,12288,24576,24576,24640,24768,25024,24768,24816,24800,25520,26424,26124,25600,24576,24576,8192,8192,0,0],"육":[19,0,0,0,0,0,1792,8064,6528,6336,6528,3968,1536,130048,118780,6584,6528,16256,15296,4096,4096,4096,4096,0,0],"신":[19,0,0,0,0,0,12288,24576,24640,25024,25024,24768,24800,25568,26416,25628,24576,9088,8960,768,256,32512,15872,0,0],"일":[19,0,0,0,0,0,12288,24576,24592,24816,25584,25368,25368,25528,25072,8192,16256,16256,7680,16128,3584,1792,130560,0,0],"모":[19,0,0,0,0,0,0,0,14336,32256,16352,6368,3264,7872,16064,6144,2048,1024,50688,131064,124,0,0,0,0],"행":[19,0,0,0,0,0,24576,58368,23776,19776,20472,27888,32224,32176,23984,19696,25184,15872,30208,25344,25344,16128,7680,0,0],"절":[19,0,0,0,0,0,12288,24576,24576,25472,25584,32160,32752,26464,26160,9752,16128,16128,7680,16128,3584,1792,130560,0,0],"멸":[19,0,0,0,0,0,12288,24576,24576,26496,32760,31512,25400,32688,25072,8224,16128,16128,7680,16128,3584,1792,130560,0,0],"끊":[19,0,0,0,0,0,0,14592,16368,13280,12672,12672,12672,114688,262140,16636,7168,6240,32352,15456,13408,16224,7648,0,0],"순":[19,0,0,0,0,0,512,3584,1536,1792,8064,29056,24800,32768,131008,116732,3072,1984,1472,128,192,16256,7936,0,0],"간":[19,0,0,0,0,0,6144,28672,12288,13184,13304,12688,258240,12384,12336,12312,12296,12672,4480,384,384,16256,7936,0,0]};
    var BODY_SCALE = 1;
    var TITLE_SCALE = 2;
    var BODY_TRACKING = -2;
    var TITLE_TRACKING = 0;
    var SPACE_ADVANCE = Math.round(CELL_H * 0.5);
    var BODY_LINE_HEIGHT = CELL_H + 8;
    var PARAGRAPH_GAP = 20;
    var DIVIDER_Y = 52;
    var BODY_TOP = 68;
    var BODY_COLOR = '#d8d8d8';
    var TITLE_COLOR = '#ffffff';
    var DIVIDER_COLOR = '#555555';
    var resizeTimer = 0;
    var observed = typeof WeakSet === 'function' ? new WeakSet() : null;
    var resizeObserver = typeof ResizeObserver === 'function' ? new ResizeObserver(function (entries) {
        entries.forEach(function (entry) { schedule(entry.target, false); });
    }) : null;
 
    function glyphFor(character) {
        return Object.prototype.hasOwnProperty.call(GLYPHS, character) ? GLYPHS[character] : null;
    }
 
    function measureText(text, scale, tracking) {
        var width = 0;
        var glyphCount = 0;
        Array.from(String(text || '')).forEach(function (character) {
            var glyph;
            if (character === ' ') {
                width += SPACE_ADVANCE * scale;
                return;
            }
            glyph = glyphFor(character);
            if (!glyph) return;
            width += glyph[0] * scale;
            width += tracking * scale;
            glyphCount += 1;
        });
        if (glyphCount && tracking) width -= tracking * scale;
        return Math.max(0, Math.round(width));
    }
 
    function breakLongToken(token, maxWidth) {
        var pieces = [];
        var current = '';
        Array.from(token).forEach(function (character) {
            var candidate = current + character;
            if (current && measureText(candidate, BODY_SCALE, BODY_TRACKING) > maxWidth) {
                pieces.push(current);
                current = character;
            } else {
                current = candidate;
            }
        });
        if (current) pieces.push(current);
        return pieces;
    }
 
    function wrapParagraph(text, maxWidth) {
        var words = String(text || '').trim().split(/\s+/).filter(Boolean);
        var lines = [];
        var current = '';
 
        words.forEach(function (word) {
            var candidate = current ? current + ' ' + word : word;
            var pieces;
            if (measureText(candidate, BODY_SCALE, BODY_TRACKING) <= maxWidth) {
                current = candidate;
                return;
            }
            if (current) {
                lines.push(current);
                current = '';
            }
            if (measureText(word, BODY_SCALE, BODY_TRACKING) <= maxWidth) {
                current = word;
                return;
            }
            pieces = breakLongToken(word, maxWidth);
            pieces.forEach(function (piece, index) {
                if (index === pieces.length - 1) current = piece;
                else lines.push(piece);
            });
        });
        if (current) lines.push(current);
        return lines.length ? lines : [''];
    }
 
    function calculateOpticalShift(lines, width) {
        var weightedCenter = 0;
        var totalWeight = 0;
        var maxLineWidth = 0;
        var shift;
        lines.forEach(function (line) {
            var lineWidth = line.width;
            var weight = Math.max(1, lineWidth) * BODY_LINE_HEIGHT;
            weightedCenter += (lineWidth / 2) * weight;
            totalWeight += weight;
            maxLineWidth = Math.max(maxLineWidth, lineWidth);
        });
        if (!totalWeight) return 0;
        shift = Math.round((width / 2) - (weightedCenter / totalWeight));
        return Math.max(0, Math.min(Math.max(0, width - maxLineWidth), shift));
    }
 
    function drawGlyph(context, glyph, x, y, scale) {
        var py;
        var px;
        var row;
        for (py = 0; py < CELL_H; py += 1) {
            row = glyph[py + 1] || 0;
            if (!row) continue;
            for (px = 0; px < CELL_W; px += 1) {
                if (row & (1 << px)) {
                    context.fillRect(
                        Math.round(x + px * scale),
                        Math.round(y + py * scale),
                        scale,
                        scale
                    );
                }
            }
        }
    }
 
    function drawText(context, text, x, y, scale, tracking, color) {
        context.fillStyle = color;
        Array.from(String(text || '')).forEach(function (character) {
            var glyph;
            if (character === ' ') {
                x += SPACE_ADVANCE * scale;
                return;
            }
            glyph = glyphFor(character);
            if (!glyph) return;
            drawGlyph(context, glyph, x, y, scale);
            x += (glyph[0] + tracking) * scale;
        });
    }
 
    function sourceContent(inner) {
        var titleNode = inner.querySelector('.main-manifesto-title');
        var paragraphs = [];
        Array.prototype.forEach.call(inner.children || [], function (child) {
            if (child && child.tagName === 'P') {
                var value = String(child.textContent || '').trim();
                if (value) paragraphs.push(value);
            }
        });
        return {
            title: titleNode ? String(titleNode.textContent || '').trim() : '',
            paragraphs: paragraphs
        };
    }
 
    function ensureCanvas(inner) {
        var canvas = inner.querySelector('.main-manifesto-bitmap');
        if (!canvas) {
            canvas = document.createElement('canvas');
            canvas.className = 'main-manifesto-bitmap';
            canvas.setAttribute('aria-hidden', 'true');
            canvas.setAttribute('data-bitmap-version', VERSION);
            inner.insertBefore(canvas, inner.firstChild);
        }
        return canvas;
    }
 
    function render(inner, force) {
        var width;
        var source;
        var paragraphs;
        var flatLines = [];
        var titleWidth;
        var height;
        var canvas;
        var context;
        var y;
        var opticalShift;
 
        if (!inner || !inner.isConnected) return;
        width = Math.max(1, Math.floor(inner.clientWidth || inner.getBoundingClientRect().width || 0));
        if (width <= 1) {
            window.setTimeout(function () { schedule(inner, true); }, 60);
            return;
        }
        if (!force && Number(inner.getAttribute('data-bitmap-width')) === width && inner.classList.contains('is-bitmap-ready')) return;
 
        source = sourceContent(inner);
        if (!source.title || !source.paragraphs.length) {
            fail(inner, new Error('Manifesto source nodes were not found.'));
            return;
        }
 
        paragraphs = source.paragraphs.map(function (paragraph) {
            return wrapParagraph(paragraph, width).map(function (line) {
                var item = { text:line, width:measureText(line, BODY_SCALE, BODY_TRACKING) };
                flatLines.push(item);
                return item;
            });
        });
 
        titleWidth = measureText(source.title, TITLE_SCALE, TITLE_TRACKING);
        height = BODY_TOP;
        paragraphs.forEach(function (lines, index) {
            height += lines.length * BODY_LINE_HEIGHT;
            if (index < paragraphs.length - 1) height += PARAGRAPH_GAP;
        });
        height = Math.max(height, DIVIDER_Y + 2);
 
        canvas = ensureCanvas(inner);
        canvas.width = width;
        canvas.height = Math.ceil(height);
        canvas.style.width = width + 'px';
        canvas.style.height = Math.ceil(height) + 'px';
        context = canvas.getContext('2d', { alpha:true });
        if (!context) {
            fail(inner, new Error('Canvas 2D context is unavailable.'));
            return;
        }
        context.imageSmoothingEnabled = false;
        context.clearRect(0, 0, canvas.width, canvas.height);
 
        drawText(
            context,
            source.title,
            Math.round((width - titleWidth) / 2),
            0,
            TITLE_SCALE,
            TITLE_TRACKING,
            TITLE_COLOR
        );
 
        /* 완성형 구분선은 폐기했다. 실선 애니메이션은 ManifestoIntro만 담당한다. */
 
        opticalShift = calculateOpticalShift(flatLines, width);
        y = BODY_TOP;
        paragraphs.forEach(function (lines, paragraphIndex) {
            lines.forEach(function (line) {
                drawText(context, line.text, opticalShift, y, BODY_SCALE, BODY_TRACKING, BODY_COLOR);
                y += BODY_LINE_HEIGHT;
            });
            if (paragraphIndex < paragraphs.length - 1) y += PARAGRAPH_GAP;
        });
 
        inner.setAttribute('data-bitmap-width', String(width));
        inner.setAttribute('data-bitmap-optical-shift-x', String(opticalShift));
        inner.setAttribute('data-bitmap-state', 'ready');
        inner.classList.add('is-bitmap-ready');
    }
 
    function fail(inner, error) {
        if (!inner) return;
        inner.classList.remove('is-bitmap-ready');
        inner.setAttribute('data-bitmap-state', 'failed');
        inner.setAttribute('data-bitmap-error', String(error && error.message ? error.message : error));
        try { console.error('[MainPageBitmap]', error); } catch (ignore) {}
    }
 
    function schedule(inner, force) {
        if (!inner) return;
        window.requestAnimationFrame(function () {
            try { render(inner, force); }
            catch (error) { fail(inner, error); }
        });
    }
 
    function mount(root) {
        var scope = root && root.querySelectorAll ? root : document;
        var nodes = [];
        if (scope.matches && scope.matches(SELECTOR)) nodes.push(scope);
        nodes = nodes.concat(Array.prototype.slice.call(scope.querySelectorAll(SELECTOR)));
        nodes.forEach(function (inner) {
            if (resizeObserver && (!observed || !observed.has(inner))) {
                resizeObserver.observe(inner);
                if (observed) observed.add(inner);
            }
            schedule(inner, true);
        });
    }
 
    function recalculate() {
        Array.prototype.forEach.call(document.querySelectorAll(SELECTOR), function (inner) {
            schedule(inner, true);
        });
    }
 
    function boot() {
        mount(document);
        window.setTimeout(function () { mount(document); }, 120);
        window.setTimeout(function () { mount(document); }, 500);
    }
 
    if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', boot, { once:true });
    else boot();
 
    window.addEventListener('resize', function () {
        window.clearTimeout(resizeTimer);
        resizeTimer = window.setTimeout(recalculate, 100);
    }, { passive:true });
 
    try {
        if (mw && mw.hook) {
            mw.hook('wikipage.content').add(function (content) {
                mount(content && content[0] ? content[0] : document);
            });
        }
    } catch (ignoreHook) {}
 
    window.MainPageBitmap = {
        version:VERSION,
        recalculate:recalculate,
        status:function () {
            var inner = document.querySelector(SELECTOR);
            return inner ? {
                integrated:true,
                state:inner.getAttribute('data-bitmap-state') || 'pending',
                error:inner.getAttribute('data-bitmap-error') || '',
                width:inner.getAttribute('data-bitmap-width') || '',
                opticalShiftX:inner.getAttribute('data-bitmap-optical-shift-x') || '',
                canvas:!!inner.querySelector('.main-manifesto-bitmap')
            } : { integrated:true, state:'not-mounted' };
        }
    };


/* =========================================
    window.MainPageManifesto = window.MainPageManifesto || {};
  Unified Shortcuts loader
     window.MainPageManifesto.recalculate = recalculate;
  ========================================= */
})(window, document, window.mw);
(function () {
    'use strict';
 
    if (!window.mw || !mw.loader) return;
     loadClbiRawScript('MediaWiki:Nation_List_Manager.js');
    loadClbiRawScript('MediaWiki:History_Event_Manager.js');
    loadClbiRawScript('MediaWiki:Shortcuts.js');
})();

2026년 7월 21일 (화) 12:29 기준 최신판

mw.loader.load('https://cdnjs.cloudflare.com/ajax/libs/cropperjs/1.5.13/cropper.min.js');

/* Raw MediaWiki script cache key.
   This is not a semantic version. It only prevents stale raw JS during active editing. */
window.CLBI_RAW_LOAD_BUST = 'portal-frame-section-fit-20260721-008';

/*
Raw script loads are tracked so the site boot gate can wait for real subsystem scripts
before releasing the normal surface.  The old prefixed load-bust variable remains for
compatibility only; new loader state uses unprefixed EntryScriptLoads/EntryRawScriptPromises.
*/
window.EntryScriptLoads = window.EntryScriptLoads || [];
window.EntryRawScriptPromises = window.EntryRawScriptPromises || {};
function buildEntryRawScriptUrl(key) {
    var url = '/index.php?title=' + encodeURIComponent(key) + '&action=raw&ctype=text/javascript';
    var token;
    var sep;

    try {
        if (window.RevisionManifest && typeof window.RevisionManifest.addRevisionParam === 'function') {
            url = window.RevisionManifest.addRevisionParam(url, key);
        }
        if (/[?&]_entryRev=/.test(url)) return url;
        token = window.RevisionManifest && typeof window.RevisionManifest.manifestToken === 'function' ? window.RevisionManifest.manifestToken('raw-script-fallback') : window.CLBI_RAW_LOAD_BUST;
    } catch (err) {
        token = window.CLBI_RAW_LOAD_BUST;
    }

    sep = url.indexOf('?') === -1 ? '?' : '&';
    return url + sep + 'v=' + encodeURIComponent(String(token || 'entry-raw'));
}

function loadClbiRawScript(title) {
    var key = String(title || '').trim();
    var promise;

    if (!key) return Promise.resolve(false);
    if (window.EntryRawScriptPromises[key]) return window.EntryRawScriptPromises[key];

    promise = Promise.resolve()
        .then(function () {
            if (window.RevisionManifest && typeof window.RevisionManifest.ensureLoaded === 'function') {
                return window.RevisionManifest.ensureLoaded();
            }
            return null;
        })
        .catch(function () { return null; })
        .then(function () {
            return new Promise(function (resolve) {
                var script = document.createElement('script');
                script.src = buildEntryRawScriptUrl(key);
                script.async = true;
                script.setAttribute('data-entry-raw-script', key);
                script.onload = function () { resolve({ title: key, ok: true, src: script.src }); };
                script.onerror = function () { resolve({ title: key, ok: false, src: script.src }); };
                (document.head || document.documentElement).appendChild(script);
            });
        });

    window.EntryRawScriptPromises[key] = promise;
    window.EntryScriptLoads.push(promise);
    return promise;
}


/* =========================================
Main-page fresh assets with fallback
========================================= */

/*
Common.css의 MainPage.css import는 안전망으로 그대로 유지한다.
대문에서는 새로고침마다 timestamp가 붙은 CSS/JS를 추가로 불러와
브라우저·프록시의 오래된 raw 응답을 우회한다.

이 보조 로더가 실패해도 기존 정적 import와 기존 화면은 남는다.
*/
window.MainPageFreshAssets = window.MainPageFreshAssets || (function (window, document, mw) {
    'use strict';

    var sessionToken = String(Date.now());
    var PORTAL_FRAME_BUILD = '20260721-portal-frame-section-fit-008';
    var ASSET_LOAD_TIMEOUT_MS = 5000;
    var scriptPromises = {};
    var styleNode = null;

    function isMainPage() {
        var page = '';
        try {
            page = String(mw && mw.config ? mw.config.get('wgPageName') || '' : '');
        } catch (err) {}
        return page.replace(/_/g, ' ') === '대문';
    }

    function rawUrl(title, type) {
        var ctype = type === 'style' ? 'text/css' : 'text/javascript';
        return '/index.php?title=' + encodeURIComponent(title) +
            '&action=raw&ctype=' + encodeURIComponent(ctype) +
            '&_mainPageFresh=' + encodeURIComponent(sessionToken);
    }

    function loadStyle() {
        if (!isMainPage()) return Promise.resolve(false);

        if (styleNode && styleNode.parentNode) {
            return Promise.resolve(true);
        }

        return new Promise(function (resolve) {
            var link = document.createElement('link');
            var settled = false;
            var timeout = 0;

            function settle(ok) {
                if (settled) return;
                settled = true;
                window.clearTimeout(timeout);
                resolve(ok);
            }

            link.rel = 'stylesheet';
            link.href = rawUrl('MediaWiki:MainPage.css', 'style');
            link.setAttribute('data-main-page-fresh-style', sessionToken);
            link.onload = function () {
                styleNode = link;
                settle(true);
            };
            link.onerror = function () {
                /* Common.css의 정적 import가 그대로 남아 있으므로 화면은 유지된다. */
                settle(false);
            };
            timeout = window.setTimeout(function () { settle(false); }, ASSET_LOAD_TIMEOUT_MS);
            (document.head || document.documentElement).appendChild(link);
        });
    }

    function loadScript(title) {
        var key = String(title || '').trim();

        if (!key || !isMainPage()) return Promise.resolve(false);
        if (scriptPromises[key]) return scriptPromises[key];

        scriptPromises[key] = new Promise(function (resolve) {
            var script = document.createElement('script');
            var settled = false;
            var timeout = 0;

            function settle(ok) {
                if (settled) return;
                settled = true;
                window.clearTimeout(timeout);
                resolve(ok);
            }

            script.src = rawUrl(key, 'script');
            script.async = false;
            script.setAttribute('data-main-page-fresh-script', key);
            script.onload = function () { settle(true); };
            script.onerror = function () { settle(false); };
            timeout = window.setTimeout(function () {
                if (script.parentNode) script.parentNode.removeChild(script);
                settle(false);
            }, ASSET_LOAD_TIMEOUT_MS);
            (document.head || document.documentElement).appendChild(script);
        });

        return scriptPromises[key];
    }

    function validatePortalFrameSet() {
        var modules = [
            ['PortalFrame', window.PortalFrame],
            ['CategoryNav', window.CategoryNav],
            ['CategoryPillar', window.CategoryPillar],
            ['PortalSectionNav', window.PortalSectionNav],
            ['BottomGuideNav', window.BottomGuideNav]
        ];
        var mismatches = modules.filter(function (entry) {
            var module = entry[1];
            var version = module && (module.frameVersion || module.version);
            return version !== PORTAL_FRAME_BUILD;
        });

        document.documentElement.toggleAttribute(
            'data-portal-frame-build-mismatch',
            mismatches.length > 0
        );
        document.documentElement.setAttribute('data-portal-frame-build', PORTAL_FRAME_BUILD);

        if (mismatches.length && window.console && typeof window.console.error === 'function') {
            window.console.error(
                '[PortalFrame] 원자적 배포 세트의 버전이 일치하지 않습니다:',
                mismatches.map(function (entry) { return entry[0]; }).join(', ')
            );
        }
        return mismatches.length === 0;
    }

    function ensure() {
        if (!isMainPage()) return Promise.resolve(false);

        return loadStyle()
            .then(function () {
                return loadScript('MediaWiki:CategoryNav.js');
            })
            .then(function () {
                return loadScript('MediaWiki:CategoryPillar.js');
            })
            .then(function () {
                return loadScript('MediaWiki:PortalSectionNav.js');
            })
            .then(function () {
                return loadScript('MediaWiki:BottomGuideNav.js');
            })
            .then(function () {
                validatePortalFrameSet();
                if (window.CategoryNav && typeof window.CategoryNav.renderAll === 'function') {
                    window.CategoryNav.renderAll(document);
                }
                if (window.CategoryPillar && typeof window.CategoryPillar.renderAll === 'function') {
                    window.CategoryPillar.renderAll(document);
                }
                if (window.PortalSectionNav && typeof window.PortalSectionNav.renderAll === 'function') {
                    window.PortalSectionNav.renderAll(document);
                }
                if (window.BottomGuideNav && typeof window.BottomGuideNav.render === 'function') {
                    window.BottomGuideNav.render();
                }
                return true;
            });
    }

    function status() {
        return {
            mainPage: isMainPage(),
            token: sessionToken,
            style: !!document.querySelector('link[data-main-page-fresh-style]'),
            category: !!document.querySelector(
                'script[data-main-page-fresh-script="MediaWiki:CategoryNav.js"]'
            ),
            pillar: !!document.querySelector(
                'script[data-main-page-fresh-script="MediaWiki:CategoryPillar.js"]'
            ),
            section: !!document.querySelector(
                'script[data-main-page-fresh-script="MediaWiki:PortalSectionNav.js"]'
            ),
            guide: !!document.querySelector(
                'script[data-main-page-fresh-script="MediaWiki:BottomGuideNav.js"]'
            ),
            frameBuild: PORTAL_FRAME_BUILD,
            frameConsistent: validatePortalFrameSet()
        };
    }

    if (mw && mw.hook) {
        mw.hook('wikipage.content').add(function () {
            ensure();
        });
    }

    window.setTimeout(ensure, 0);

    return {
        ensure: ensure,
        status: status
    };
}(window, document, window.mw));


/* =========================================
   Account access page marker
   =========================================
   New shared systems use unprefixed names.  The server body class differs
   between skins/locales, so the canonical special-page name is normalized
   once and exposed as a stable page-state class for the login surface.
*/
(function markAccountLoginPage(window, document) {
    'use strict';

    var canonical = '';
    var pageName = '';
    var isLoginPage = false;

    try {
        canonical = String(window.mw && mw.config ? mw.config.get('wgCanonicalSpecialPageName') || '' : '').toLowerCase();
        pageName = String(window.mw && mw.config ? mw.config.get('wgPageName') || '' : '').replace(/_/g, ' ').toLowerCase();
    } catch (err) {}

    isLoginPage = canonical === 'userlogin' || /^(?:special|특수):(?:userlogin|로그인)$/.test(pageName);
    if (!isLoginPage) return;

    document.documentElement.classList.add('account-login-page-root');

    function isCreateAccountTarget(link) {
        var href = '';
        var title = '';
        var text = '';
        var sample = '';

        if (!link) return false;
        href = String(link.getAttribute('href') || '');
        title = String(link.getAttribute('title') || '');
        text = String(link.textContent || '');
        try { href = decodeURIComponent(href); } catch (err) {}
        sample = (href + ' ' + title + ' ' + text).replace(/_/g, ' ').toLowerCase();
        return /(?:special|특수)\s*[:%]\s*(?:createaccount|계정\s*(?:만들기|생성))|createaccount|계정\s*(?:만들기|생성)/i.test(sample);
    }

    function suppressCreateAccountSurface(root) {
        var scope = root && root.querySelectorAll ? root : document;
        var fixedSelectors = [
            '.mw-createacct-benefits-container',
            '.mw-createacct-benefits-list',
            '.mw-createaccount-cta',
            '.mw-createaccount-join',
            '#mw-createaccount-join',
            '.mw-userlogin-create'
        ];

        fixedSelectors.forEach(function (selector) {
            Array.prototype.forEach.call(scope.querySelectorAll(selector), function (node) {
                node.style.setProperty('display', 'none', 'important');
                node.setAttribute('aria-hidden', 'true');
            });
        });

        Array.prototype.forEach.call(scope.querySelectorAll('a[href]'), function (link) {
            var container;
            if (!isCreateAccountTarget(link)) return;

            link.style.setProperty('display', 'none', 'important');
            link.setAttribute('aria-hidden', 'true');
            link.setAttribute('tabindex', '-1');

            container = link.closest('.mw-ui-vform-field, .oo-ui-fieldLayout, .mw-userlogin-create, p, li');
            if (container && container.querySelectorAll('a').length === 1) {
                container.style.setProperty('display', 'none', 'important');
                container.setAttribute('aria-hidden', 'true');
            }
        });
    }

    function applyMarker() {
        if (!document.body) return;
        document.body.classList.add('account-login-page');
        suppressCreateAccountSurface(document);
    }

    if (document.readyState === 'loading') {
        document.addEventListener('DOMContentLoaded', applyMarker, { once: true });
    } else {
        applyMarker();
    }

    try {
        if (window.mw && mw.hook) {
            mw.hook('wikipage.content').add(function (content) {
                suppressCreateAccountSurface(content && content[0] ? content[0] : document);
            });
        }
    } catch (err) {}
})(window, document);

/* =========================================
   Boot gate prelude
   =========================================
   This tiny prelude runs before the full EntryStore/EntryLoader/BootGate implementation.
   Its job is only to guarantee that the user sees the separate loading surface before the
   normal wiki shell can paint.  The full BootGate later adopts the same DOM node and keeps
   it open until the real full-entry readiness contract is satisfied.

   Maintenance rule:
   - Do not move first-screen hiding into a late subsystem callback.  If the boot gate is
     meant to protect the first impression, the surface must be activated before sidebars,
     navbars, document wells, nations panels, or other entry surfaces can visibly fill in.
   - New names remain unprefixed.  Existing prefixed names elsewhere are legacy aliases only.
*/
(function installBootGatePrelude(window, document) {
    'use strict';

    var SCREEN_ID = 'boot-gate-screen';
    var STYLE_ID = 'boot-gate-prelude-style';
    var START_TIME = Date.now ? Date.now() : new Date().getTime();
    function hasQueryFlag(name, value) {
        var search = String(window.location && window.location.search || '');
        var re = new RegExp('[?&]' + name + '=([^&]+)');
        var match = search.match(re);
        return !!(match && decodeURIComponent(match[1]) === value);
    }

    function getMwConfig(name, fallback) {
        try {
            if (window.mw && window.mw.config && typeof window.mw.config.get === 'function') {
                var value = window.mw.config.get(name);
                return value == null ? fallback : value;
            }
        } catch (err) {}
        return fallback;
    }

    function normalizePageName(value) {
        return String(value || '').split('?')[0].replace(/^\/index\.php\//, '').replace(/_/g, ' ').trim();
    }

    function isDeveloperOrEditingPage() {
        var ns = Number(getMwConfig('wgNamespaceNumber', NaN));
        var action = String(getMwConfig('wgAction', 'view') || 'view').toLowerCase();
        var model = String(getMwConfig('wgPageContentModel', '') || '').toLowerCase();
        var name = normalizePageName(getMwConfig('wgPageName', '') || window.location.pathname || '');
        var systemNamespaces = {
            '-1': true, '4': true, '5': true, '6': true, '7': true, '8': true, '9': true,
            '10': true, '11': true, '12': true, '13': true, '14': true, '15': true,
            '828': true, '829': true
        };

        /*
         * Developer/system pages must stay practical.  Editing MediaWiki:, File:,
         * Project:, Template:, Module:, Category:, or any non-view action already forces
         * a full page load in MediaWiki, so showing the entry boot screen there only slows
         * maintenance work and does not improve the public first impression.
         */
        if (hasQueryFlag('bootGatePreview', '1')) return false;
        if (action && action !== 'view') return true;
        if (systemNamespaces[String(ns)]) return true;
        if (model === 'css' || model === 'javascript' || model === 'json' || model === 'sanitized-css') return true;
        if (/\.(?:css|js|json)$/i.test(name)) return true;
        if (/^(?:mediawiki|미디어위키|file|파일|project|프로젝트|template|틀|module|모듈|category|분류|special|특수):/i.test(name)) return true;
        return false;
    }

    function isAnonymousUser() {
        var userName = getMwConfig('wgUserName', null);
        var userId = Number(getMwConfig('wgUserId', 0) || 0);
        return !userName && !userId;
    }

    function isAuthenticationPage() {
        var canonical = String(getMwConfig('wgCanonicalSpecialPageName', '') || '').toLowerCase();
        var name = normalizePageName(getMwConfig('wgPageName', '') || window.location.pathname || '').toLowerCase();
        var allowed = {
            userlogin: true,
            passwordreset: true,
            resetpass: true,
            confirmemail: true
        };

        if (allowed[canonical]) return true;
        return /^(?:special|특수):(?:userlogin|로그인|passwordreset|비밀번호 ?재설정|resetpass|confirmemail)/i.test(name);
    }

    var anonymousUser = isAnonymousUser();
    var disabled = isAuthenticationPage() || (!anonymousUser && (hasQueryFlag('bootGate', '0') || isDeveloperOrEditingPage()));

    function injectStyle() {
        var style;
        if (disabled || document.getElementById(STYLE_ID)) return;
        style = document.createElement('style');
        style.id = STYLE_ID;
        style.textContent = [
            'html.boot-gate-active,body.boot-gate-active{overflow:hidden!important;}',
            'html.boot-gate-active body{background:#080808!important;}',
            'html.boot-gate-active body>:not(#boot-gate-screen){visibility:hidden!important;pointer-events:none!important;}',
            'html.boot-gate-active #boot-gate-screen,html.boot-gate-active #boot-gate-screen *{visibility:visible!important;}',
            '#boot-gate-screen{position:fixed!important;inset:0!important;z-index:2147483000!important;display:flex!important;align-items:center!important;justify-content:center!important;background:#080808!important;color:#d8d8d8!important;opacity:1;pointer-events:auto!important;}',
            '#boot-gate-screen .boot-gate-decoration-layer{position:absolute;inset:0;z-index:1;pointer-events:none;overflow:hidden;}',
            '#boot-gate-screen .boot-gate-panel{position:relative;z-index:2;}',
            '#boot-gate-screen .boot-gate-close{display:none;position:absolute;right:7px;top:6px;z-index:3;height:18px;min-width:18px;border:1px solid #000;background:#141414;color:#ddd;font-size:11px;line-height:16px;padding:0 5px;cursor:pointer;}',
            '#boot-gate-screen.is-preview{inset:auto!important;left:18px!important;top:18px!important;width:min(720px,calc(100vw - 380px))!important;height:360px!important;z-index:99990!important;overflow:hidden!important;border:1px solid #000!important;box-shadow:0 10px 28px rgba(0,0,0,.55)!important;pointer-events:none!important;}',
            '#boot-gate-screen.is-preview .boot-gate-panel,#boot-gate-screen.is-preview .boot-gate-close{pointer-events:auto!important;}',
            '#boot-gate-screen.is-preview .boot-gate-close{display:block;}'
        ].join('');
        (document.head || document.documentElement).appendChild(style);
    }

    function activate() {
        if (disabled) return;
        injectStyle();
        if (document.documentElement) document.documentElement.classList.add('boot-gate-active');
        if (document.body) document.body.classList.add('boot-gate-active');
    }

    function makeNode() {
        var node;
        var panel;
        var title;
        var status;
        var meter;
        var fill;
        var progress;
        var detail;
        var decoLayer;
        var close;

        if (disabled || !document.body) return null;
        activate();

        node = document.getElementById(SCREEN_ID);
        if (node) return node;

        node = document.createElement('div');
        node.id = SCREEN_ID;
        node.className = 'boot-gate-screen is-active';
        node.setAttribute('role', 'status');
        node.setAttribute('aria-live', 'polite');
        node.setAttribute('data-boot-gate-prelude', '1');

        panel = document.createElement('div');
        panel.className = 'boot-gate-panel';

        title = document.createElement('div');
        title.className = 'boot-gate-title';
        title.textContent = 'ARCHIVE INITIALIZATION';

        status = document.createElement('div');
        status.className = 'boot-gate-status';
        status.textContent = 'Preparing site entry systems';

        meter = document.createElement('div');
        meter.className = 'boot-gate-meter';
        fill = document.createElement('div');
        fill.className = 'boot-gate-meter-fill';
        meter.appendChild(fill);

        progress = document.createElement('div');
        progress.className = 'boot-gate-progress';
        progress.textContent = '0%';

        detail = document.createElement('div');
        detail.className = 'boot-gate-detail';
        detail.textContent = 'boot gate prelude';

        close = document.createElement('button');
        close.type = 'button';
        close.className = 'boot-gate-close';
        close.setAttribute('aria-label', 'Close boot preview');
        close.textContent = '×';
        close.addEventListener('click', function () { release(node); });

        decoLayer = document.createElement('div');
        decoLayer.className = 'boot-gate-decoration-layer';
        decoLayer.setAttribute('data-decoration-target', 'boot-gate');
        decoLayer.setAttribute('aria-hidden', 'true');

        panel.appendChild(title);
        panel.appendChild(status);
        panel.appendChild(meter);
        panel.appendChild(progress);
        panel.appendChild(detail);
        node.appendChild(decoLayer);
        node.appendChild(panel);
        node.appendChild(close);

        document.body.insertBefore(node, document.body.firstChild || null);
        return node;
    }

    function onBody(callback) {
        if (document.body) {
            callback();
            return;
        }
        if (document.readyState === 'loading') {
            document.addEventListener('DOMContentLoaded', callback, { once: true });
        }
        window.setTimeout(function tick() {
            if (document.body) return callback();
            window.setTimeout(tick, 10);
        }, 0);
    }

    function release(node) {
        node = node || document.getElementById(SCREEN_ID);
        if (node) {
            node.classList.add('is-complete');
            node.classList.remove('is-active');
            window.setTimeout(function () {
                if (node.parentNode) node.parentNode.removeChild(node);
            }, 240);
        }
        window.setTimeout(function () {
            if (document.documentElement) document.documentElement.classList.remove('boot-gate-active');
            if (document.body) document.body.classList.remove('boot-gate-active');
        }, 250);
    }

    activate();
    onBody(function () {
        activate();
        makeNode();
    });

    window.__BootGatePrelude = {
        startTime: START_TIME,
        activate: activate,
        ensure: makeNode,
        release: release,
        node: function () { return document.getElementById(SCREEN_ID); },
        disabled: disabled
    };
})(window, document);

/* =========================================
   Site boot gate and entry artifact contract
   =========================================
   This is the initial-load full/half entry artifact system.

   Purpose:
   - The loading screen is not decorative. It exists only during the first site entry in a
     tab, before the user is allowed into the normal wiki surface.
   - A "full" entry must mean that the first view of that system can appear without an
     additional visible data load. For the nations system, the 1950 entry is full only when
     its nation list/link-map data and first-view pixel decorations are ready in this tab.
   - A "half" entry is a predictive warm state for the next likely path. It may fetch and
     parse data, but may skip expensive final work such as image/canvas preparation until it
     is promoted to full.
   - This principle is a site-wide design priority, like SPA continuity. New viewers,
     document systems, and information panels should define their entry full/half packs
     before exposing a first screen that can visibly fill in later.
   - SPA navigation is a consumer phase, not a blocking phase.  Do not add BootGate holds
     to SPA route changes.  If a route needs seamless first paint, prepare its artifacts
     during the initial boot pack and have the subsystem consume EntryStore synchronously
     before inserting or painting visible DOM.

   Naming rule:
   - New public APIs, globals, classes, functions, files, and components must not use a
     project prefix. Old prefixed globals are legacy compatibility surfaces only.
   - New code should use names such as BootGate, EntryLoader, EntryStore, and boot-gate-*.
*/
(function (window, document, mw) {
    'use strict';

    var MANIFEST_TITLE = 'MediaWiki:EntryManifest.json';
    var BUILD_ID = '20260711-existing-account-login-001';
    var deferredEntryWarmups = [];
    var READY_KEY = 'boot-gate-ready-version';
    var DISMISS_PARAM = 'bootGate';
    var bootStarted = false;
    var bootPromise = null;
    var bootNode = null;
    var bootStatusNode = null;
    var bootProgressNode = null;
    var bootDetailNode = null;
    var bootFillNode = null;
    var bootStartTime = 0;
    var earlyBootStyleInjected = false;
    var loginGateLocked = false;
    var loginGateActionNode = null;

    function hasQueryFlag(name, value) {
        var search = String(window.location && window.location.search || '');
        var re = new RegExp('[?&]' + name + '=([^&]+)');
        var match = search.match(re);
        return !!(match && decodeURIComponent(match[1]) === value);
    }

    function readConfig(name, fallback) {
        try {
            if (mw && mw.config && typeof mw.config.get === 'function') {
                var value = mw.config.get(name);
                return value == null ? fallback : value;
            }
        } catch (err) {}
        return fallback;
    }

    function normalizeBootPageName(value) {
        return String(value || '').split('?')[0].replace(/^\/index\.php\//, '').replace(/_/g, ' ').trim();
    }

    function isCreateAccountPage() {
        var canonical = String(readConfig('wgCanonicalSpecialPageName', '') || '').toLowerCase();
        var name = normalizeBootPageName(readConfig('wgPageName', '') || window.location.pathname || '').toLowerCase();
        return canonical === 'createaccount' || /^(?:special|특수):(?:createaccount|계정 ?(?:만들기|생성))/i.test(name);
    }

    function isAnonymousUser() {
        var userName = readConfig('wgUserName', null);
        var userId = Number(readConfig('wgUserId', 0) || 0);
        return !userName && !userId;
    }

    function isAuthenticationPage() {
        var canonical = String(readConfig('wgCanonicalSpecialPageName', '') || '').toLowerCase();
        var name = normalizeBootPageName(readConfig('wgPageName', '') || window.location.pathname || '').toLowerCase();
        var allowed = {
            userlogin: true,
            passwordreset: true,
            resetpass: true,
            confirmemail: true
        };

        if (allowed[canonical]) return true;
        return /^(?:special|특수):(?:userlogin|로그인|passwordreset|비밀번호 ?재설정|resetpass|confirmemail)/i.test(name);
    }

    function requiresLoginGate() {
        return isAnonymousUser() && !isAuthenticationPage();
    }

    function isBootExcludedPage() {
        var ns = Number(readConfig('wgNamespaceNumber', NaN));
        var action = String(readConfig('wgAction', 'view') || 'view').toLowerCase();
        var model = String(readConfig('wgPageContentModel', '') || '').toLowerCase();
        var name = normalizeBootPageName(readConfig('wgPageName', '') || window.location.pathname || '');
        var systemNamespaces = {
            '-1': true, '4': true, '5': true, '6': true, '7': true, '8': true, '9': true,
            '10': true, '11': true, '12': true, '13': true, '14': true, '15': true,
            '828': true, '829': true
        };

        /*
         * The public boot gate is for normal reading surfaces.  Developer/system
         * namespaces and edit/diff/history actions already reload outside SPA, so
         * blocking them would turn every maintenance save into another entry boot.
         */
        if (hasQueryFlag('bootGatePreview', '1')) return false;
        if (action && action !== 'view') return true;
        if (systemNamespaces[String(ns)]) return true;
        if (model === 'css' || model === 'javascript' || model === 'json' || model === 'sanitized-css') return true;
        if (/\.(?:css|js|json)$/i.test(name)) return true;
        if (/^(?:mediawiki|미디어위키|file|파일|project|프로젝트|template|틀|module|모듈|category|분류|special|특수):/i.test(name)) return true;
        return false;
    }

    var LOGIN_REQUIRED = requiresLoginGate();
    var BOOT_EXCLUDED_PAGE = isBootExcludedPage() && !LOGIN_REQUIRED;

    function injectEarlyBootStyle() {
        var style;
        if (earlyBootStyleInjected || !document.documentElement) return;
        earlyBootStyleInjected = true;
        style = document.createElement('style');
        style.id = 'boot-gate-early-style';
        style.textContent = [
            'html.boot-gate-active, body.boot-gate-active{overflow:hidden!important;}',
            'html.boot-gate-active body{background:#080808!important;}',
            'html.boot-gate-active body> :not(#boot-gate-screen){visibility:hidden!important;pointer-events:none!important;}',
            'html.boot-gate-active #boot-gate-screen,html.boot-gate-active #boot-gate-screen *{visibility:visible!important;}'
        ].join('');
        (document.head || document.documentElement).appendChild(style);
    }

    function activateBootSurface() {
        injectEarlyBootStyle();
        if (document.documentElement) document.documentElement.classList.add('boot-gate-active');
        if (document.body) document.body.classList.add('boot-gate-active');
    }

    if (!BOOT_EXCLUDED_PAGE) activateBootSurface();
    else if (window.__BootGatePrelude && window.__BootGatePrelude.release) window.__BootGatePrelude.release();

    var defaultManifest = {
        version: '20260710-globe-vhs-restore-entry-001',
        boot: {
            minDisplayMs: 950,
            cachedMinDisplayMs: 350,
            maxBlockingMs: 15000
        },
        initial: {
            full: [
                {
                    id: 'decorations-registry',
                    label: 'DECORATION REGISTRY',
                    type: 'decorations',
                    ref: 'MediaWiki:Decorations.json',
                    page: '시대',
                    era: '1950',
                    preparePixels: true
                },
                {
                    id: 'nations-1950-entry',
                    label: '1950 ERA ENTRY',
                    type: 'nations-era',
                    era: '1950',
                    level: 'full'
                }
            ],
            half: [
                {
                    id: 'nations-1960-half',
                    label: '1960 ERA HALF',
                    type: 'nations-era',
                    era: '1960',
                    level: 'half'
                }
            ]
        }
    };

    function now() {
        return Date.now ? Date.now() : new Date().getTime();
    }


    var BootPerf = window.BootPerf = window.BootPerf || (function () {
        var t0 = now();
        var entries = [];
        var active = {};
        var seq = 0;

        function cloneMeta(meta) {
            var out = {};
            Object.keys(meta || {}).forEach(function (key) {
                var value = meta[key];
                if (value == null) return;
                if (typeof value === 'string' || typeof value === 'number' || typeof value === 'boolean') out[key] = value;
                else {
                    try { out[key] = JSON.parse(JSON.stringify(value)); }
                    catch (err) { out[key] = String(value); }
                }
            });
            return out;
        }

        function start(name, meta) {
            var id = String(++seq);
            active[id] = { id: id, name: String(name || 'entry'), start: now(), meta: cloneMeta(meta) };
            return id;
        }

        function end(id, meta) {
            var item = active[id];
            var ended = now();
            if (!item) return null;
            delete active[id];
            item.end = ended;
            item.ms = ended - item.start;
            item.offset = item.start - t0;
            item.meta = Object.assign({}, item.meta || {}, cloneMeta(meta));
            entries.push(item);
            return item;
        }

        function instant(name, meta) {
            var t = now();
            entries.push({ id: String(++seq), name: String(name || 'mark'), start: t, end: t, ms: 0, offset: t - t0, meta: cloneMeta(meta) });
        }

        function measure(name, meta, fn) {
            var id = start(name, meta);
            try {
                return Promise.resolve(fn()).then(function (value) {
                    end(id, { ok: true });
                    return value;
                }, function (err) {
                    end(id, { ok: false, error: err && (err.message || String(err)) });
                    throw err;
                });
            } catch (err) {
                end(id, { ok: false, error: err && (err.message || String(err)) });
                return Promise.reject(err);
            }
        }

        function rows() {
            return entries.slice().sort(function (a, b) { return a.start - b.start; }).map(function (item) {
                return {
                    offset: item.offset,
                    ms: item.ms,
                    name: item.name,
                    meta: item.meta || {}
                };
            });
        }

        function summary() {
            var list = rows();
            var total = list.reduce(function (max, item) { return Math.max(max, item.offset + item.ms); }, 0);
            return {
                build: BUILD_ID,
                startedAt: t0,
                totalMs: total,
                entries: list,
                active: Object.keys(active).map(function (id) { return active[id]; })
            };
        }

        function print() {
            var list = rows();
            if (!window.console || !console.log) return summary();
            try {
                console.groupCollapsed('[BootPerf] entry loading timeline · ' + BUILD_ID);
                if (console.table) console.table(list.map(function (item) {
                    return {
                        offset: item.offset + 'ms',
                        duration: item.ms + 'ms',
                        name: item.name,
                        detail: JSON.stringify(item.meta || {})
                    };
                }));
                else list.forEach(function (item) { console.log(item.offset + 'ms', item.ms + 'ms', item.name, item.meta || {}); });
                console.groupEnd();
            } catch (err) {}
            return summary();
        }

        return {
            start: start,
            end: end,
            mark: instant,
            measure: measure,
            rows: rows,
            summary: summary,
            print: print
        };
    }());


    var InteractionPerf = window.InteractionPerf = window.InteractionPerf || (function () {
        var BUILD = '20260710-interaction-perf-instrument-001';
        var t0 = (window.performance && performance.now ? performance.now() : now());
        var entries = [];
        var active = {};
        var seq = 0;
        var maxEntries = 1600;

        function perfNow() {
            return window.performance && performance.now ? performance.now() : now();
        }

        function cloneMeta(meta) {
            var out = {};
            Object.keys(meta || {}).forEach(function (key) {
                var value = meta[key];
                if (value == null) return;
                if (typeof value === 'string' || typeof value === 'number' || typeof value === 'boolean') out[key] = value;
                else {
                    try { out[key] = JSON.parse(JSON.stringify(value)); }
                    catch (err) { out[key] = String(value); }
                }
            });
            return out;
        }

        function push(entry) {
            entries.push(entry);
            if (entries.length > maxEntries) entries.splice(0, entries.length - maxEntries);
            return entry;
        }

        function start(name, meta) {
            var id = String(++seq);
            active[id] = { id: id, name: String(name || 'interaction'), start: perfNow(), meta: cloneMeta(meta) };
            return id;
        }

        function end(id, meta) {
            var item = active[id];
            var ended = perfNow();
            if (!item) return null;
            delete active[id];
            item.end = ended;
            item.ms = Math.round((ended - item.start) * 100) / 100;
            item.offset = Math.round((item.start - t0) * 100) / 100;
            item.meta = Object.assign({}, item.meta || {}, cloneMeta(meta));
            return push(item);
        }

        function mark(name, meta) {
            var t = perfNow();
            return push({ id: String(++seq), name: String(name || 'mark'), start: t, end: t, ms: 0, offset: Math.round((t - t0) * 100) / 100, meta: cloneMeta(meta) });
        }

        function measureSync(name, meta, fn) {
            var id = start(name, meta);
            try {
                var value = fn();
                end(id, { ok: true });
                return value;
            } catch (err) {
                end(id, { ok: false, error: err && (err.message || String(err)) || String(err) });
                throw err;
            }
        }

        function measureAsync(name, meta, fn) {
            var id = start(name, meta);
            try {
                return Promise.resolve(fn()).then(function (value) {
                    end(id, { ok: true });
                    return value;
                }, function (err) {
                    end(id, { ok: false, error: err && (err.message || String(err)) || String(err) });
                    throw err;
                });
            } catch (err) {
                end(id, { ok: false, error: err && (err.message || String(err)) || String(err) });
                return Promise.reject(err);
            }
        }

        function summary() {
            return {
                build: BUILD,
                startedAt: t0,
                totalMs: Math.round((perfNow() - t0) * 100) / 100,
                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: Math.round((item.start - t0) * 100) / 100, ms: Math.round((perfNow() - item.start) * 100) / 100, name: item.name, meta: item.meta || {} };
                })
            };
        }

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

        try {
            if (window.PerformanceObserver && !window.CLBI_InteractionLongTaskObserverBound) {
                window.CLBI_InteractionLongTaskObserverBound = true;
                new PerformanceObserver(function (list) {
                    list.getEntries().forEach(function (entry) {
                        mark('browser long task', {
                            ms: Math.round(entry.duration * 100) / 100,
                            start: Math.round(entry.startTime * 100) / 100,
                            attribution: entry.attribution && entry.attribution.length ? entry.attribution.length : 0
                        });
                    });
                }).observe({ entryTypes: ['longtask'] });
            }
        } catch (ignoreLongTaskObserver) {}

        return {
            build: BUILD,
            start: start,
            end: end,
            mark: mark,
            measureSync: measureSync,
            measureAsync: measureAsync,
            summary: summary,
            table: table
        };
    }());

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

    function unique(list) {
        var seen = {};
        var out = [];
        (list || []).forEach(function (item) {
            item = String(item || '').trim();
            if (!item || seen[item]) return;
            seen[item] = true;
            out.push(item);
        });
        return out;
    }

    function hasBootParam(value) {
        var search = String(window.location && window.location.search || '');
        var re = new RegExp('[?&]' + DISMISS_PARAM + '=([^&]+)');
        var match = search.match(re);
        return match && decodeURIComponent(match[1]) === value;
    }

    function normalizeTitle(value) {
        return String(value || '')
            .split('#')[0]
            .replace(/_/g, ' ')
            .trim();
    }

    function extractTitleFromUrl(value) {
        var text = String(value || '');
        var match = text.match(/[?&]title=([^&]+)/i);
        if (match) return normalizeTitle(decodeURIComponent(match[1].replace(/\+/g, ' ')));
        return '';
    }

    function normalizeRefKey(ref) {
        var text = String(ref || '').trim();
        var title;
        if (!text) return '';
        title = extractTitleFromUrl(text);
        if (title) return 'title:' + title.toLowerCase();
        if (text.indexOf('/') === -1 && text.indexOf(':') !== -1) return 'title:' + normalizeTitle(text).toLowerCase();
        return 'url:' + text;
    }

    function rawUrlForRef(ref, ctype) {
        var text = String(ref || '').trim();
        var title;
        if (!text) return '';
        if (/^(?:https?:)?\/\//i.test(text) || text.charAt(0) === '/') return text;
        title = normalizeTitle(text.indexOf(':') !== -1 ? text : ('MediaWiki:' + text));
        if (mw && mw.util && typeof mw.util.getUrl === 'function') {
            return mw.util.getUrl(title, { action: 'raw', ctype: ctype || 'application/json' });
        }
        return '/index.php?title=' + encodeURIComponent(title) + '&action=raw&ctype=' + encodeURIComponent(ctype || 'application/json');
    }

    function getApiEndpoint() {
        return (mw && mw.util && typeof mw.util.wikiScript === 'function') ? mw.util.wikiScript('api') : '/api.php';
    }

    function fetchApi(params) {
        var body = new URLSearchParams();
        Object.keys(params || {}).forEach(function (key) {
            body.append(key, params[key]);
        });
        return fetch(getApiEndpoint(), {
            method: 'POST',
            credentials: 'same-origin',
            headers: { 'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8' },
            body: body.toString()
        }).then(function (res) {
            if (!res.ok) throw new Error('HTTP ' + res.status);
            return res.json();
        });
    }


    /* =========================================
       Revision manifest and persistent entry cache
       =========================================
       The first boot still prepares current-tab artifacts, but freshness is not guessed from
       filenames or old localStorage flags.  The client reads a tiny server-side current-state
       manifest, compares page revisions / file sha1 values, and only keeps cached raw resources
       whose revision token still matches the server.  The manifest is a latest-state table, not
       an append-only client log.
    */
    var REVISION_MANIFEST_ACTION = 'entryrevisionmanifest';
    var REVISION_MANIFEST_LOCAL_KEY = 'entry-revision-manifest-current-v1';
    var REVISION_MANIFEST_PRUNE_LOCAL_KEY = 'entry-revision-manifest-last-client-prune-v1';
    var ENTRY_CACHE_NAME = 'entry-cache-v1';
    var ENTRY_CACHE_INDEX_KEY = 'entry-cache-index-v1';
    var ENTRY_CACHE_REQUEST_PREFIX = '/__entry-cache__/';
    var ENTRY_CACHE_PACK_KEY = 'entry-cache-pack-state-v1';

    function hasCacheStorage() {
        return !!(window.caches && typeof window.caches.open === 'function');
    }

    function safeJsonParse(text, fallback) {
        try { return JSON.parse(text); } catch (err) { return fallback; }
    }

    function readLocalJson(key, fallback) {
        try {
            var text = window.localStorage ? window.localStorage.getItem(key) : null;
            return text ? safeJsonParse(text, fallback) : fallback;
        } catch (err) {
            return fallback;
        }
    }

    function writeLocalJson(key, value) {
        try {
            if (window.localStorage) window.localStorage.setItem(key, JSON.stringify(value));
        } catch (err) {}
    }

    function normalizeManifestTitle(value) {
        var text = String(value || '').trim();
        var match;
        var i;
        if (!text) return '';
        for (i = 0; i < 3; i += 1) {
            try {
                if (/%[0-9a-f]{2}/i.test(text)) text = decodeURIComponent(text);
            } catch (err) { break; }
        }
        match = text.match(/[?&]title=([^&#]+)/i);
        if (match) text = match[1];
        text = text.replace(/^https?:\/\/[^/]+/i, '')
            .replace(/^\/+/, '')
            .replace(/^index\.php\/?/i, '')
            .replace(/^wiki\/?/i, '')
            .trim();
        match = text.match(/(?:^|\/)(?:(?:Special|특수)\s*[:/]\s*)?(?:Redirect|넘겨주기)\s*\/\s*file\s*\/([^?#]+)(?:[?#].*)?$/i);
        if (match) text = 'File:' + match[1];
        text = text.split('#')[0].replace(/_/g, ' ').replace(/\s+/g, ' ').trim();
        text = text.replace(/^(?:파일|Image|이미지)\s*:/i, 'File:');
        if (/^(?:mediawiki|template|module|manage|file)\s*:/i.test(text)) {
            text = text.replace(/^([^:]+)\s*:\s*/, function (_, ns) { return ns.charAt(0).toUpperCase() + ns.slice(1).toLowerCase() + ':'; });
        }
        if (/^File:/i.test(text)) text = 'File:' + text.slice(text.indexOf(':') + 1).trim();
        if (/^Mediawiki:/i.test(text)) text = 'MediaWiki:' + text.slice(text.indexOf(':') + 1).trim();
        return text;
    }

    function resourceToken(resource) {
        if (!resource || typeof resource !== 'object') return '';
        return String(resource.revision || resource.sha1 || resource.hash || resource.timestamp || resource.updatedAt || resource.url || '').trim();
    }

    function createEntryCache() {
        var index = readLocalJson(ENTRY_CACHE_INDEX_KEY, { entries: {} }) || { entries: {} };
        var packs = readLocalJson(ENTRY_CACHE_PACK_KEY, { packs: {} }) || { packs: {} };
        var objectUrls = {};
        var stats = {
            textHits: 0,
            blobHits: 0,
            misses: 0,
            networkStores: 0,
            stores: 0,
            deletes: 0
        };

        function ensureIndex() {
            if (!index || typeof index !== 'object') index = { entries: {} };
            if (!index.entries || typeof index.entries !== 'object') index.entries = {};
        }

        function compactIndexForLocalStorage() {
            var entries;
            var next = {};
            ensureIndex();
            entries = index.entries || {};
            Object.keys(entries).forEach(function (key) {
                var entry = entries[key] || {};
                var kind = String(entry.kind || 'raw').toLowerCase();
                /*
                 * CacheStorage already owns image/blob bodies.  Keeping one localStorage
                 * metadata row per flag/blob competes with MediaWiki ResourceLoader's own
                 * localStorage module store and can trigger QuotaExceededError before our
                 * code even runs on the next page load.  Text/json/file URL rows stay indexed
                 * because they are small and useful for invalidation diagnostics.
                 */
                if (kind === 'image' || kind === 'blob') return;
                next[key] = entry;
            });
            index.entries = next;
        }

        function ensurePacks() {
            if (!packs || typeof packs !== 'object') packs = { packs: {} };
            if (!packs.packs || typeof packs.packs !== 'object') packs.packs = {};
        }

        function saveIndex() {
            ensureIndex();
            compactIndexForLocalStorage();
            writeLocalJson(ENTRY_CACHE_INDEX_KEY, index);
        }

        function savePacks() {
            ensurePacks();
            writeLocalJson(ENTRY_CACHE_PACK_KEY, packs);
        }

        function requestForKey(key) {
            return new Request(ENTRY_CACHE_REQUEST_PREFIX + encodeURIComponent(String(key || '')), { credentials: 'same-origin' });
        }

        function openCache() {
            if (!hasCacheStorage()) return Promise.resolve(null);
            return window.caches.open(ENTRY_CACHE_NAME).catch(function () { return null; });
        }

        function cacheEntry(key, meta) {
            key = String(key || '');
            ensureIndex();
            index.entries[key] = Object.assign({}, index.entries[key] || {}, {
                key: key,
                resourceKey: String(meta && meta.resourceKey || ''),
                token: String(meta && meta.token || ''),
                kind: String(meta && meta.kind || 'raw'),
                contentType: String(meta && meta.contentType || ''),
                size: Number(meta && meta.size || 0) || null,
                createdAt: index.entries[key] && index.entries[key].createdAt ? index.entries[key].createdAt : now(),
                updatedAt: now(),
                lastHitAt: index.entries[key] && index.entries[key].lastHitAt ? index.entries[key].lastHitAt : null,
                hits: Number(index.entries[key] && index.entries[key].hits || 0)
            });
            saveIndex();
        }

        function markHit(key, field) {
            key = String(key || '');
            ensureIndex();
            if (index.entries[key]) {
                index.entries[key].hits = Number(index.entries[key].hits || 0) + 1;
                index.entries[key].lastHitAt = now();
                saveIndex();
            }
            if (field && stats[field] != null) stats[field] += 1;
        }

        function getResponse(key) {
            key = String(key || '');
            if (!key || !hasCacheStorage()) return Promise.resolve(null);
            return openCache().then(function (cache) {
                if (!cache) return null;
                return cache.match(requestForKey(key)).then(function (res) {
                    if (!res) {
                        stats.misses += 1;
                        return null;
                    }
                    return res;
                });
            }).catch(function () { return null; });
        }

        function putResponse(key, response, meta) {
            key = String(key || '');
            if (!key || !response || !hasCacheStorage()) return Promise.resolve(false);
            meta = meta || {};
            return openCache().then(function (cache) {
                var cloned;
                var headers;
                var bodyPromise;
                if (!cache) return false;
                cloned = response.clone();
                bodyPromise = cloned.blob().catch(function () { return null; });
                return bodyPromise.then(function (blob) {
                    if (!blob) return false;
                    headers = new Headers(response.headers || {});
                    if (!headers.get('Content-Type')) headers.set('Content-Type', meta.contentType || blob.type || 'application/octet-stream');
                    headers.set('X-Entry-Cache-Key', key);
                    headers.set('X-Entry-Resource-Key', String(meta.resourceKey || ''));
                    headers.set('X-Entry-Revision-Token', String(meta.token || ''));
                    headers.set('X-Entry-Cached-At', String(now()));
                    return cache.put(requestForKey(key), new Response(blob, { status: 200, headers: headers })).then(function () {
                        stats.stores += 1;
                        cacheEntry(key, {
                            resourceKey: meta.resourceKey,
                            token: meta.token,
                            kind: meta.kind || 'blob',
                            contentType: headers.get('Content-Type') || blob.type || '',
                            size: blob.size
                        });
                        return true;
                    });
                });
            }).catch(function () { return false; });
        }

        function getText(key) {
            return getResponse(key).then(function (res) {
                if (!res) return null;
                markHit(key, 'textHits');
                return res.text();
            }).catch(function () { return null; });
        }

        function putText(key, text, meta) {
            key = String(key || '');
            if (!key || !hasCacheStorage()) return Promise.resolve(false);
            meta = meta || {};
            return openCache().then(function (cache) {
                var headers;
                var body = String(text || '');
                if (!cache) return false;
                headers = new Headers({
                    'Content-Type': meta.contentType || 'text/plain; charset=UTF-8',
                    'X-Entry-Cache-Key': key,
                    'X-Entry-Resource-Key': String(meta.resourceKey || ''),
                    'X-Entry-Revision-Token': String(meta.token || ''),
                    'X-Entry-Cached-At': String(now())
                });
                return cache.put(requestForKey(key), new Response(body, { status: 200, headers: headers })).then(function () {
                    stats.stores += 1;
                    cacheEntry(key, {
                        resourceKey: meta.resourceKey,
                        token: meta.token,
                        kind: meta.kind || 'text',
                        contentType: headers.get('Content-Type'),
                        size: body.length
                    });
                    return true;
                });
            }).catch(function () { return false; });
        }

        function getBlobUrl(key) {
            key = String(key || '');
            if (!key) return Promise.resolve('');
            if (objectUrls[key]) {
                markHit(key, 'blobHits');
                return Promise.resolve(objectUrls[key]);
            }
            return getResponse(key).then(function (res) {
                if (!res) return '';
                return res.blob().then(function (blob) {
                    if (!blob || !blob.size) return '';
                    objectUrls[key] = URL.createObjectURL(blob);
                    markHit(key, 'blobHits');
                    return objectUrls[key];
                });
            }).catch(function () { return ''; });
        }

        function fetchBlobUrl(url, key, meta, options) {
            url = String(url || '').trim();
            key = String(key || '').trim();
            if (!url || !key) return Promise.resolve('');
            return getBlobUrl(key).then(function (cachedUrl) {
                if (cachedUrl) return cachedUrl;
                return fetch(url, {
                    credentials: 'same-origin',
                    cache: options && options.noStore ? 'no-store' : 'force-cache'
                }).then(function (res) {
                    if (!res.ok) throw new Error('HTTP ' + res.status);
                    stats.networkStores += 1;
                    return putResponse(key, res, Object.assign({}, meta || {}, { kind: meta && meta.kind ? meta.kind : 'blob' })).then(function () {
                        return getBlobUrl(key);
                    });
                }).catch(function () {
                    return '';
                });
            });
        }

        function deleteKey(key) {
            key = String(key || '');
            if (!key || !hasCacheStorage()) return Promise.resolve(false);
            if (objectUrls[key]) {
                try { URL.revokeObjectURL(objectUrls[key]); } catch (err) {}
                delete objectUrls[key];
            }
            return openCache().then(function (cache) {
                if (!cache) return false;
                return cache.delete(requestForKey(key)).then(function (ok) {
                    ensureIndex();
                    if (index.entries && index.entries[key]) {
                        delete index.entries[key];
                        stats.deletes += 1;
                        saveIndex();
                    }
                    return ok;
                });
            }).catch(function () { return false; });
        }

        function invalidateResources(resourceKeys) {
            var map = {};
            var entries;
            var keys = [];
            ensureIndex();
            entries = index.entries || {};
            (resourceKeys || []).forEach(function (resourceKey) {
                resourceKey = String(resourceKey || '').toLowerCase();
                if (resourceKey) map[resourceKey] = true;
            });
            Object.keys(entries).forEach(function (cacheKey) {
                var resourceKey = String(entries[cacheKey] && entries[cacheKey].resourceKey || '').toLowerCase();
                if (map[resourceKey]) keys.push(cacheKey);
            });
            return Promise.all(keys.map(deleteKey)).then(function () { return keys.length; });
        }

        function prune(maxAgeDays) {
            var days = Math.max(1, Number(maxAgeDays) || 7);
            var cutoff = now() - days * 24 * 60 * 60 * 1000;
            var entries;
            var keys;
            ensureIndex();
            entries = index.entries || {};
            keys = Object.keys(entries).filter(function (key) {
                return Number(entries[key] && entries[key].createdAt || 0) < cutoff;
            });
            window.localStorage && window.localStorage.setItem(REVISION_MANIFEST_PRUNE_LOCAL_KEY, String(now()));
            return Promise.all(keys.map(deleteKey)).then(function () { return keys.length; });
        }

        function reset() {
            Object.keys(objectUrls).forEach(function (key) {
                try { URL.revokeObjectURL(objectUrls[key]); } catch (err) {}
            });
            objectUrls = {};
            index = { entries: {} };
            packs = { packs: {} };
            saveIndex();
            savePacks();
            if (!hasCacheStorage()) return Promise.resolve(false);
            return window.caches.delete(ENTRY_CACHE_NAME).catch(function () { return false; });
        }

        function packReady(key, token) {
            key = String(key || '');
            token = String(token || '');
            ensurePacks();
            if (!key || !token) return false;
            return !!(packs.packs[key] && String(packs.packs[key].token || '') === token && packs.packs[key].ready);
        }

        function setPackReady(key, token, meta) {
            key = String(key || '');
            token = String(token || '');
            if (!key || !token) return false;
            ensurePacks();
            packs.packs[key] = Object.assign({}, meta || {}, {
                key: key,
                token: token,
                ready: true,
                updatedAt: now()
            });
            savePacks();
            return true;
        }

        function packInfo() {
            ensurePacks();
            return Object.assign({}, packs.packs || {});
        }

        function info() {
            var lastClientPrune = 0;
            var entries;
            var byKind = {};
            try { lastClientPrune = Number(window.localStorage && window.localStorage.getItem(REVISION_MANIFEST_PRUNE_LOCAL_KEY) || 0); } catch (err) {}
            ensureIndex();
            ensurePacks();
            entries = index.entries || {};
            Object.keys(entries).forEach(function (key) {
                var kind = String(entries[key] && entries[key].kind || 'raw');
                byKind[kind] = (byKind[kind] || 0) + 1;
            });
            return {
                supported: hasCacheStorage(),
                name: ENTRY_CACHE_NAME,
                entries: Object.keys(entries).length,
                byKind: byKind,
                packs: Object.keys(packs.packs || {}).length,
                lastClientPruneAt: lastClientPrune || null,
                stats: Object.assign({}, stats)
            };
        }

        /* Trim legacy image/blob metadata rows produced by older builds. */
        try { saveIndex(); } catch (err) {}

        return {
            getResponse: getResponse,
            putResponse: putResponse,
            getText: getText,
            putText: putText,
            getBlobUrl: getBlobUrl,
            fetchBlobUrl: fetchBlobUrl,
            invalidateResources: invalidateResources,
            prune: prune,
            reset: reset,
            packReady: packReady,
            setPackReady: setPackReady,
            packInfo: packInfo,
            info: info
        };
    }

    window.EntryCache = window.EntryCache || createEntryCache();

    function createRevisionManifestService() {
        var current = null;
        var previous = readLocalJson(REVISION_MANIFEST_LOCAL_KEY, null);
        var loadPromise = null;
        var changedResources = [];

        function unwrap(payload) {
            return payload && (payload.entryrevisionmanifest || payload.revisionManifest || payload) || null;
        }

        function resourcesOf(manifest) {
            return manifest && manifest.resources && typeof manifest.resources === 'object' ? manifest.resources : {};
        }

        function compactRevisionManifest(manifest) {
            var out;
            var resources;
            if (!manifest || typeof manifest !== 'object') return null;
            out = {
                manifestVersion: manifest.manifestVersion || manifest.version || '',
                version: manifest.version || manifest.manifestVersion || '',
                generatedAt: manifest.generatedAt || '',
                pruneDays: manifest.pruneDays,
                clientPruneDays: manifest.clientPruneDays,
                resources: {}
            };
            resources = resourcesOf(manifest);
            Object.keys(resources).forEach(function (title) {
                var src = resources[title];
                var dst;
                if (!src || typeof src !== 'object') return;
                dst = {};
                ['revision', 'sha1', 'hash', 'timestamp', 'updatedAt', 'url'].forEach(function (key) {
                    if (src[key] !== undefined && src[key] !== null && String(src[key]) !== '') dst[key] = src[key];
                });
                out.resources[title] = dst;
            });
            return out;
        }

        function buildLookup(manifest) {
            var lookup = {};
            Object.keys(resourcesOf(manifest)).forEach(function (title) {
                lookup[normalizeManifestTitle(title).toLowerCase()] = resourcesOf(manifest)[title];
            });
            return lookup;
        }

        function computeChanged(prev, next) {
            var prevLookup = buildLookup(prev);
            var nextLookup = buildLookup(next);
            var out = [];
            Object.keys(nextLookup).forEach(function (key) {
                if (resourceToken(prevLookup[key]) !== resourceToken(nextLookup[key])) out.push(key);
            });
            Object.keys(prevLookup).forEach(function (key) {
                if (!nextLookup[key]) out.push(key);
            });
            return unique(out);
        }

        function load(options) {
            if (loadPromise && !(options && options.force)) return loadPromise;
            loadPromise = fetchApi({
                action: REVISION_MANIFEST_ACTION,
                format: 'json',
                formatversion: '2'
            }).then(function (payload) {
                var manifest = unwrap(payload);
                if (!manifest || !manifest.resources) throw new Error('invalid revision manifest');
                current = compactRevisionManifest(manifest) || manifest;
                changedResources = computeChanged(previous, current);
                if (changedResources.length && window.EntryCache && typeof window.EntryCache.invalidateResources === 'function') {
                    window.EntryCache.invalidateResources(changedResources);
                }
                writeLocalJson(REVISION_MANIFEST_LOCAL_KEY, current);
                previous = current;
                maybePrune();
                return current;
            }).catch(function () {
                current = previous || null;
                return current;
            });
            return loadPromise;
        }

        function ensureLoaded() {
            return current ? Promise.resolve(current) : load();
        }

        function resourceForRef(ref) {
            var title = normalizeManifestTitle(ref);
            var lookup;
            if (!title || !current) return null;
            lookup = buildLookup(current);
            return lookup[title.toLowerCase()] || null;
        }

        function tokenForRef(ref) {
            return resourceToken(resourceForRef(ref));
        }

        function resourceKeyForRef(ref) {
            var title = normalizeManifestTitle(ref);
            return title ? title.toLowerCase() : '';
        }

        function cacheKeyForRef(ref, type) {
            var resourceKey = resourceKeyForRef(ref);
            var token = tokenForRef(ref);
            if (!resourceKey || !token) return '';
            return String(type || 'raw') + ':' + resourceKey + '@' + token;
        }

        function manifestToken(extra) {
            var base = current && (current.manifestVersion || current.version || current.generatedAt || '') || '';
            return String(base || 'no-manifest') + (extra ? (':' + String(extra)) : '');
        }

        function tokenFromUrl(url) {
            var text = String(url || '');
            var match = text.match(/[?&](?:_entryFileRev|_entryAsset|_entryRev)=([^&]+)/);
            if (!match) return '';
            try { return decodeURIComponent(match[1]); } catch (err) { return match[1]; }
        }

        function cacheKeyForUrl(url, type) {
            var text = String(url || '').trim();
            var token = tokenFromUrl(text) || manifestToken('url');
            var normalized;
            if (!text) return '';
            try {
                normalized = new URL(text, window.location.href);
                text = normalized.pathname + (normalized.search || '');
            } catch (err) {}
            text = text.replace(/[?&](?:_entryFileRev|_entryAsset|_entryRev)=[^&]*/g, '').replace(/[?&]$/, '');
            try { text = decodeURI(text); } catch (err2) {}
            return String(type || 'blob') + ':url:' + text.toLowerCase() + '@' + token;
        }

        function addRevisionParam(url, ref) {
            var token = tokenForRef(ref);
            var text = String(url || '').trim();
            var sep;
            if (!text || !token || /[?&]_entryRev=/.test(text)) return text;
            sep = text.indexOf('?') === -1 ? '?' : '&';
            return text + sep + '_entryRev=' + encodeURIComponent(token);
        }

        function maybePrune() {
            var days = current && Number(current.pruneDays || current.clientPruneDays) || 7;
            var last = 0;
            var due;
            try { last = Number(window.localStorage && window.localStorage.getItem(REVISION_MANIFEST_PRUNE_LOCAL_KEY) || 0); } catch (err) {}
            due = !last || (now() - last >= days * 24 * 60 * 60 * 1000);
            if (due && window.EntryCache && typeof window.EntryCache.prune === 'function') {
                window.EntryCache.prune(days);
            }
        }

        function countdown() {
            var days = current && Number(current.pruneDays || current.clientPruneDays) || 7;
            var last = 0;
            var next;
            var remain;
            try { last = Number(window.localStorage && window.localStorage.getItem(REVISION_MANIFEST_PRUNE_LOCAL_KEY) || 0); } catch (err) {}
            if (!last) return { days: days, lastClientPruneAt: null, nextClientPruneAt: null, remainingMs: 0, remainingDays: 0, due: true };
            next = last + days * 24 * 60 * 60 * 1000;
            remain = Math.max(0, next - now());
            return {
                days: days,
                lastClientPruneAt: last,
                nextClientPruneAt: next,
                remainingMs: remain,
                remainingDays: Math.ceil(remain / (24 * 60 * 60 * 1000)),
                due: remain <= 0
            };
        }

        function status() {
            return {
                available: !!current,
                manifestVersion: current && (current.manifestVersion || current.version || ''),
                generatedAt: current && current.generatedAt || '',
                changedResources: changedResources.slice(0, 50),
                changedCount: changedResources.length,
                prune: countdown(),
                cache: window.EntryCache && typeof window.EntryCache.info === 'function' ? window.EntryCache.info() : null
            };
        }

        return {
            load: load,
            ensureLoaded: ensureLoaded,
            current: function () { return current; },
            resourceForRef: resourceForRef,
            tokenForRef: tokenForRef,
            cacheKeyForRef: cacheKeyForRef,
            cacheKeyForUrl: cacheKeyForUrl,
            manifestToken: manifestToken,
            resourceKeyForRef: resourceKeyForRef,
            addRevisionParam: addRevisionParam,
            maybePrune: maybePrune,
            countdown: countdown,
            status: status
        };
    }

    window.RevisionManifest = window.RevisionManifest || createRevisionManifestService();

    function createEntryStore() {
        var jsonCache = {};
        var flagUrlCache = {};
        var fileUrlCache = {};
        var imageReadyCache = {};
        var imageObjectCache = {};
        var imageDisplayUrlCache = {};
        var imagePromiseCache = {};
        var rawPromiseCache = {};
        var textCache = {};
        var textPromiseCache = {};

        function fetchJsonRef(ref, options) {
            var key = normalizeRefKey(ref);
            var cacheKey;
            var promiseKey;
            var url;
            var resourceKey;
            var token;
            if (!key) return Promise.reject(new Error('empty json ref'));
            cacheKey = window.RevisionManifest && typeof window.RevisionManifest.cacheKeyForRef === 'function' ? window.RevisionManifest.cacheKeyForRef(ref, 'json') : '';
            promiseKey = cacheKey ? (key + '@' + cacheKey) : key;
            if (jsonCache[promiseKey]) return Promise.resolve(jsonCache[promiseKey].data);
            if (rawPromiseCache[promiseKey]) return rawPromiseCache[promiseKey];
            url = rawUrlForRef(ref, 'application/json');
            if (window.RevisionManifest && typeof window.RevisionManifest.addRevisionParam === 'function') {
                url = window.RevisionManifest.addRevisionParam(url, ref);
            }
            resourceKey = window.RevisionManifest && typeof window.RevisionManifest.resourceKeyForRef === 'function' ? window.RevisionManifest.resourceKeyForRef(ref) : key;
            token = window.RevisionManifest && typeof window.RevisionManifest.tokenForRef === 'function' ? window.RevisionManifest.tokenForRef(ref) : '';
            if (cacheKey && !(options && options.noStore) && window.EntryCache && typeof window.EntryCache.getText === 'function') {
                rawPromiseCache[promiseKey] = window.EntryCache.getText(cacheKey).then(function (cachedText) {
                    if (cachedText !== null && cachedText !== undefined) {
                        var cachedData = cachedText && cachedText.trim() ? JSON.parse(cachedText) : {};
                        jsonCache[promiseKey] = { key: promiseKey, ref: ref, url: url, data: cachedData, loadedAt: now(), persistent: true };
                        jsonCache[key] = jsonCache[promiseKey];
                        return cachedData;
                    }
                    return fetch(url, {
                        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 data = text && text.trim() ? JSON.parse(text) : {};
                        jsonCache[promiseKey] = { key: promiseKey, ref: ref, url: url, data: data, loadedAt: now() };
                        jsonCache[key] = jsonCache[promiseKey];
                        window.EntryCache.putText(cacheKey, text, { resourceKey: resourceKey, token: token });
                        return data;
                    });
                });
                return rawPromiseCache[promiseKey];
            }
            rawPromiseCache[promiseKey] = fetch(url, {
                credentials: 'same-origin',
                cache: options && options.noStore ? 'no-store' : 'force-cache'
            }).then(function (res) {
                if (!res.ok) throw new Error('HTTP ' + res.status);
                return res.text();
            }).then(function (text) {
                var data = text && text.trim() ? JSON.parse(text) : {};
                jsonCache[promiseKey] = { key: promiseKey, ref: ref, url: url, data: data, loadedAt: now() };
                jsonCache[key] = jsonCache[promiseKey];
                return data;
            });
            return rawPromiseCache[promiseKey];
        }

        function getJsonSync(ref) {
            var key = normalizeRefKey(ref);
            return key && jsonCache[key] ? jsonCache[key].data : null;
        }

        function setJsonRef(ref, data) {
            var key = normalizeRefKey(ref);
            if (!key) return;
            jsonCache[key] = { key: key, ref: ref, url: rawUrlForRef(ref, 'application/json'), data: data, loadedAt: now() };
        }

        function normalizeFile(value) {
            return String(value || '')
                .replace(/^(?:file|파일):/i, '')
                .trim();
        }

        function fileKey(value) {
            return normalizeFile(value).replace(/_/g, ' ').replace(/\s+/g, ' ').toLowerCase();
        }

        function setFlagUrl(file, url) {
            var key = fileKey(file);
            if (!key) return;
            flagUrlCache[key] = String(url || '');
        }

        function getFlagUrl(file) {
            var key = fileKey(file);
            return key ? (flagUrlCache[key] || '') : '';
        }

        function resolveFlagUrls(files) {
            var clean = unique((files || []).map(normalizeFile).filter(Boolean));

            function cacheKeyForFile(file) {
                return window.RevisionManifest && typeof window.RevisionManifest.cacheKeyForRef === 'function' ? window.RevisionManifest.cacheKeyForRef('File:' + file, 'flagurl') : '';
            }

            function resourceKeyForFile(file) {
                return window.RevisionManifest && typeof window.RevisionManifest.resourceKeyForRef === 'function' ? window.RevisionManifest.resourceKeyForRef('File:' + file) : ('file:' + fileKey(file));
            }

            function tokenForFile(file) {
                return window.RevisionManifest && typeof window.RevisionManifest.tokenForRef === 'function' ? window.RevisionManifest.tokenForRef('File:' + file) : '';
            }

            function hydrateCachedFlagUrl(file) {
                var key = fileKey(file);
                var cacheKey = cacheKeyForFile(file);
                if (!key || flagUrlCache[key] !== undefined || !cacheKey || !window.EntryCache || typeof window.EntryCache.getText !== 'function') {
                    return Promise.resolve(false);
                }
                return window.EntryCache.getText(cacheKey).then(function (cachedUrl) {
                    if (cachedUrl !== null && cachedUrl !== undefined) {
                        flagUrlCache[key] = String(cachedUrl || '');
                        return true;
                    }
                    return false;
                }).catch(function () { return false; });
            }

            return Promise.all(clean.map(hydrateCachedFlagUrl)).then(function () {
                var pending = clean.filter(function (file) {
                    return getFlagUrl(file) === '' && flagUrlCache[fileKey(file)] === undefined;
                });
                var chunks = [];

                while (pending.length) chunks.push(pending.splice(0, 20));
                if (!chunks.length) return flagUrlCache;

                return Promise.all(chunks.map(function (chunk) {
                    var titleToKey = {};
                    var keyToFile = {};
                    var titles = [];
                    chunk.forEach(function (file) {
                        var key = fileKey(file);
                        if (!key) return;
                        titleToKey[fileKey('File:' + file)] = key;
                        titleToKey[fileKey('파일:' + file)] = key;
                        keyToFile[key] = file;
                        titles.push('File:' + file);
                        titles.push('파일:' + file);
                    });
                    return fetchApi({
                        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 key = titleToKey[fileKey(page && page.title)] || fileKey(page && page.title);
                            var file = keyToFile[key] || (page && page.title || '').replace(/^(?:File|파일):/i, '');
                            var imageinfo = page && page.imageinfo && page.imageinfo[0];
                            var url;
                            var rev;
                            var sep;
                            var cacheKey;
                            if (!key) return;
                            seen[key] = true;
                            url = imageinfo && (imageinfo.thumburl || imageinfo.url) ? (imageinfo.thumburl || imageinfo.url) : '';
                            if (url) {
                                rev = imageinfo && (imageinfo.sha1 || imageinfo.timestamp || imageinfo.size) ? (imageinfo.sha1 || imageinfo.timestamp || imageinfo.size) : '';
                                if (rev && !/[?&]_entryFileRev=/.test(url)) {
                                    sep = url.indexOf('?') === -1 ? '?' : '&';
                                    url += sep + '_entryFileRev=' + encodeURIComponent(String(rev));
                                }
                            }
                            flagUrlCache[key] = url;
                            cacheKey = cacheKeyForFile(file);
                            if (cacheKey && window.EntryCache && typeof window.EntryCache.putText === 'function') {
                                window.EntryCache.putText(cacheKey, url || '', {
                                    resourceKey: resourceKeyForFile(file),
                                    token: tokenForFile(file),
                                    kind: 'flagurl',
                                    contentType: 'text/plain; charset=UTF-8'
                                });
                            }
                        });
                        chunk.forEach(function (file) {
                            var key = fileKey(file);
                            if (key && !seen[key] && flagUrlCache[key] === undefined) flagUrlCache[key] = '';
                        });
                    }).catch(function () {
                        chunk.forEach(function (file) {
                            var key = fileKey(file);
                            if (key && flagUrlCache[key] === undefined) flagUrlCache[key] = '';
                        });
                    });
                })).then(function () { return flagUrlCache; });
            });
        }

        function normalizeGenericFileTitle(value) {
            var text = String(value || '').trim();
            var match;
            var i;

            if (!text) return '';
            for (i = 0; i < 4; i += 1) {
                try {
                    if (/%[0-9a-f]{2}/i.test(text)) text = decodeURIComponent(text);
                } catch (err) {
                    break;
                }
            }
            match = text.match(/[?&]title=([^&#]+)/i);
            if (match) text = match[1];
            text = text.replace(/^https?:\/\/[^/]+/i, '').replace(/^\/+/, '').replace(/^index\.php\/?/i, '').replace(/^wiki\/?/i, '').trim();
            match = text.match(/(?:^|\/)(?:(?:Special|특수)\s*[:/]\s*)?(?:Redirect|넘겨주기)\s*\/\s*file\s*\/(.+)$/i);
            if (match) text = match[1];
            text = text.replace(/^(?:File|파일|Image|이미지)\s*:/i, '').replace(/^:+/, '').trim();
            return text;
        }

        function isFileRef(value) {
            var text = String(value || '').trim();
            return /^(?:file|파일)\s*:/i.test(text) || /(?:^|\/)(?:(?:Special|특수)\s*[:/]\s*)?(?:Redirect|넘겨주기)\s*\/\s*file\s*\//i.test(text);
        }

        function fileUrlKey(value) {
            return normalizeGenericFileTitle(value).replace(/_/g, ' ').replace(/\s+/g, ' ').toLowerCase();
        }

        function stableDirectImageUrl(url) {
            var text = String(url || '').trim();
            var separator;
            var bust;
            if (!text) return '';
            if (/[?&]_entryAsset=/.test(text) || /[?&]_=/.test(text)) return text;
            bust = String(BUILD_ID || 'entry');
            separator = text.indexOf('?') === -1 ? '?' : '&';
            return text + separator + '_entryAsset=' + encodeURIComponent(bust);
        }

        function resolveFileUrl(ref) {
            var original = String(ref || '').trim();
            var file;
            var key;
            var cacheKey;
            var resourceKey;
            var token;
            if (!original) return Promise.resolve('');
            if (!isFileRef(original)) return Promise.resolve(stableDirectImageUrl(original));
            file = normalizeGenericFileTitle(original);
            key = fileUrlKey(file);
            if (!file || !key) return Promise.resolve(stableDirectImageUrl(original));
            if (fileUrlCache[key]) return Promise.resolve(fileUrlCache[key]);
            cacheKey = window.RevisionManifest && typeof window.RevisionManifest.cacheKeyForRef === 'function' ? window.RevisionManifest.cacheKeyForRef('File:' + file, 'fileurl') : '';
            resourceKey = window.RevisionManifest && typeof window.RevisionManifest.resourceKeyForRef === 'function' ? window.RevisionManifest.resourceKeyForRef('File:' + file) : ('file:' + key);
            token = window.RevisionManifest && typeof window.RevisionManifest.tokenForRef === 'function' ? window.RevisionManifest.tokenForRef('File:' + file) : '';
            if (cacheKey && window.EntryCache && typeof window.EntryCache.getText === 'function') {
                return window.EntryCache.getText(cacheKey).then(function (cachedUrl) {
                    if (cachedUrl !== null && cachedUrl !== undefined) {
                        fileUrlCache[key] = String(cachedUrl || '');
                        return fileUrlCache[key];
                    }
                    return fetchApi({
                        action: 'query',
                        format: 'json',
                        formatversion: '2',
                        redirects: '1',
                        prop: 'imageinfo',
                        iiprop: 'url|sha1|timestamp|size|mime',
                        titles: 'File:' + file
                    }).then(function (json) {
                        var pages = (json && json.query && json.query.pages) || [];
                        var info = pages[0] && pages[0].imageinfo && pages[0].imageinfo[0] ? pages[0].imageinfo[0] : null;
                        var url = info && info.url ? info.url : original;
                        var rev = info && (info.sha1 || info.timestamp || info.size) ? (info.sha1 || info.timestamp || info.size) : String(BUILD_ID || 'entry-file');
                        var separator = url.indexOf('?') === -1 ? '?' : '&';
                        var resolved = url + separator + '_entryFileRev=' + encodeURIComponent(String(rev));
                        fileUrlCache[key] = resolved;
                        window.EntryCache.putText(cacheKey, resolved, { resourceKey: resourceKey, token: token, kind: 'fileurl', contentType: 'text/plain; charset=UTF-8' });
                        return resolved;
                    });
                }).catch(function () {
                    return stableDirectImageUrl(original);
                });
            }
            return fetchApi({
                action: 'query',
                format: 'json',
                formatversion: '2',
                redirects: '1',
                prop: 'imageinfo',
                iiprop: 'url|sha1|timestamp|size|mime',
                titles: 'File:' + file
            }).then(function (json) {
                var pages = (json && json.query && json.query.pages) || [];
                var info = pages[0] && pages[0].imageinfo && pages[0].imageinfo[0] ? pages[0].imageinfo[0] : null;
                var url = info && info.url ? info.url : original;
                var rev = info && (info.sha1 || info.timestamp || info.size) ? (info.sha1 || info.timestamp || info.size) : String(BUILD_ID || 'entry-file');
                var separator = url.indexOf('?') === -1 ? '?' : '&';
                var resolved = url + separator + '_entryFileRev=' + encodeURIComponent(String(rev));
                fileUrlCache[key] = resolved;
                return resolved;
            }).catch(function () {
                return stableDirectImageUrl(original);
            });
        }

        function preloadImageUrl(url, options) {
            var key = String(url || '').trim();
            var cacheKey;
            var sourcePromise;
            var persistent = !(options && options.persistent === false);
            if (!key) return Promise.resolve(false);
            if (imageReadyCache[key]) return Promise.resolve(true);
            if (imagePromiseCache[key]) return imagePromiseCache[key];

            cacheKey = persistent && window.RevisionManifest && typeof window.RevisionManifest.cacheKeyForUrl === 'function' ? window.RevisionManifest.cacheKeyForUrl(key, 'image') : '';
            sourcePromise = Promise.resolve('');
            if (cacheKey && window.EntryCache && typeof window.EntryCache.fetchBlobUrl === 'function') {
                sourcePromise = window.EntryCache.fetchBlobUrl(key, cacheKey, {
                    resourceKey: '',
                    token: '',
                    kind: 'image'
                }).catch(function () { return ''; });
            }

            imagePromiseCache[key] = sourcePromise.then(function (cachedObjectUrl) {
                return new Promise(function (resolve) {
                    var img = new Image();
                    var settled = false;
                    var src = cachedObjectUrl || key;
                    function finish(ok) {
                        if (settled) return;
                        settled = true;
                        if (ok) {
                            imageReadyCache[key] = true;
                            imageReadyCache[src] = true;
                            imageObjectCache[key] = img;
                            imageObjectCache[src] = img;
                            imageDisplayUrlCache[key] = src;
                            imageDisplayUrlCache[src] = src;
                        }
                        resolve(!!ok);
                    }
                    img.onload = function () {
                        if (img.decode) {
                            img.decode().then(function () { finish(true); }).catch(function () { finish(true); });
                        } else {
                            finish(true);
                        }
                    };
                    img.onerror = function () {
                        if (src !== key) {
                            src = key;
                            img.src = key;
                            return;
                        }
                        finish(false);
                    };
                    img.decoding = 'async';
                    img.loading = 'eager';
                    img.src = src;
                    if (img.complete && img.naturalWidth) {
                        if (img.decode) img.decode().then(function () { finish(true); }).catch(function () { finish(true); });
                        else finish(true);
                    }
                });
            });
            return imagePromiseCache[key];
        }

        function preloadImages(urls, options) {
            var list = unique((urls || []).filter(Boolean));
            var limit = options && Number(options.limit);
            var concurrency = Math.max(1, Math.min(48, Number(options && options.concurrency) || 24));
            var index = 0;
            var ok = 0;
            var fail = 0;
            if (Number.isFinite(limit) && limit > 0) list = list.slice(0, limit);
            if (!list.length) return Promise.resolve({ total: 0, ok: 0, fail: 0 });
            return new Promise(function (resolve) {
                function pump() {
                    while (index < list.length && concurrency > 0) {
                        (function (url) {
                            concurrency -= 1;
                            preloadImageUrl(url, options || {}).then(function (result) {
                                if (result) ok += 1;
                                else fail += 1;
                            }).catch(function () {
                                fail += 1;
                            }).then(function () {
                                concurrency += 1;
                                if (index >= list.length && ok + fail >= list.length) resolve({ total: list.length, ok: ok, fail: fail });
                                else pump();
                            });
                        })(list[index++]);
                    }
                }
                pump();
            });
        }

        function isImageReady(url) {
            var key = String(url || '').trim();
            return !!(key && imageReadyCache[key]);
        }

        function getImageElement(url) {
            var key = String(url || '').trim();
            return key ? (imageObjectCache[key] || null) : null;
        }

        function getImageDisplayUrl(url) {
            var key = String(url || '').trim();
            return key ? (imageDisplayUrlCache[key] || key) : '';
        }

        function normalizeUrlKey(url) {
            var text = String(url || '').trim();
            var a;
            if (!text) return '';
            try {
                a = document.createElement('a');
                a.href = text;
                text = a.pathname + (a.search || '');
            } catch (err) {}
            try {
                text = decodeURI(text);
            } catch (err2) {}
            text = text.replace(/([?&])_=[^&]*/g, '$1').replace(/[?&]$/, '');
            text = text.replace(/_/g, '_');
            return text;
        }

        function fetchTextUrl(url, options) {
            var key = normalizeUrlKey(url);
            var cacheKey;
            var resourceRef;
            var resourceKey;
            var token;
            if (!key) return Promise.reject(new Error('empty text url'));
            if (textCache[key]) return Promise.resolve(textCache[key].text);
            if (textPromiseCache[key]) return textPromiseCache[key];
            resourceRef = options && options.resourceRef ? options.resourceRef : (options && options.ref ? options.ref : '');
            cacheKey = resourceRef && window.RevisionManifest && typeof window.RevisionManifest.cacheKeyForRef === 'function' ? window.RevisionManifest.cacheKeyForRef(resourceRef, 'text') : '';
            if (!cacheKey && window.RevisionManifest && typeof window.RevisionManifest.cacheKeyForUrl === 'function') cacheKey = window.RevisionManifest.cacheKeyForUrl(url, 'text');
            resourceKey = resourceRef && window.RevisionManifest && typeof window.RevisionManifest.resourceKeyForRef === 'function' ? window.RevisionManifest.resourceKeyForRef(resourceRef) : ('url:' + key);
            token = resourceRef && window.RevisionManifest && typeof window.RevisionManifest.tokenForRef === 'function' ? window.RevisionManifest.tokenForRef(resourceRef) : '';
            if (cacheKey && !(options && options.noStore) && window.EntryCache && typeof window.EntryCache.getText === 'function') {
                textPromiseCache[key] = window.EntryCache.getText(cacheKey).then(function (cachedText) {
                    if (cachedText !== null && cachedText !== undefined) {
                        textCache[key] = { key: key, url: url, text: cachedText, loadedAt: now(), persistent: true };
                        return cachedText;
                    }
                    return fetch(url, {
                        credentials: 'same-origin',
                        cache: 'force-cache'
                    }).then(function (res) {
                        if (!res.ok) throw new Error('HTTP ' + res.status);
                        return res.text();
                    }).then(function (text) {
                        textCache[key] = { key: key, url: url, text: text, loadedAt: now() };
                        window.EntryCache.putText(cacheKey, text, { resourceKey: resourceKey, token: token, kind: 'text', contentType: 'text/html; charset=UTF-8' });
                        return text;
                    });
                });
                return textPromiseCache[key];
            }
            textPromiseCache[key] = fetch(url, {
                credentials: 'same-origin',
                cache: options && options.noStore ? 'no-store' : 'force-cache'
            }).then(function (res) {
                if (!res.ok) throw new Error('HTTP ' + res.status);
                return res.text();
            }).then(function (text) {
                textCache[key] = { key: key, url: url, text: text, loadedAt: now() };
                return text;
            });
            return textPromiseCache[key];
        }

        function getTextSync(url) {
            var key = normalizeUrlKey(url);
            return key && textCache[key] ? textCache[key].text : '';
        }

        function setTextUrl(url, text) {
            var key = normalizeUrlKey(url);
            if (!key) return;
            textCache[key] = { key: key, url: url, text: String(text || ''), loadedAt: now() };
        }

        function cacheInfo() {
            return {
                json: Object.keys(jsonCache).length,
                jsonKeys: Object.keys(jsonCache),
                flags: Object.keys(flagUrlCache).length,
                files: Object.keys(fileUrlCache).length,
                images: Object.keys(imageReadyCache).length,
                retainedImages: Object.keys(imageObjectCache).length,
                displayImages: Object.keys(imageDisplayUrlCache).length,
                text: Object.keys(textCache).length,
                textKeys: Object.keys(textCache)
            };
        }

        return {
            fetchJsonRef: fetchJsonRef,
            getJsonSync: getJsonSync,
            setJsonRef: setJsonRef,
            normalizeRefKey: normalizeRefKey,
            rawUrlForRef: rawUrlForRef,
            revisionUrlForRef: function (ref, ctype) {
                var url = rawUrlForRef(ref, ctype);
                if (window.RevisionManifest && typeof window.RevisionManifest.addRevisionParam === 'function') {
                    url = window.RevisionManifest.addRevisionParam(url, ref);
                }
                return url;
            },
            resolveFlagUrls: resolveFlagUrls,
            setFlagUrl: setFlagUrl,
            getFlagUrl: getFlagUrl,
            resolveFileUrl: resolveFileUrl,
            preloadImageUrl: preloadImageUrl,
            preloadImages: preloadImages,
            isImageReady: isImageReady,
            getImageElement: getImageElement,
            getImageDisplayUrl: getImageDisplayUrl,
            fetchTextUrl: fetchTextUrl,
            getTextSync: getTextSync,
            setTextUrl: setTextUrl,
            stableDirectImageUrl: stableDirectImageUrl,
            cacheInfo: cacheInfo
        };
    }

    window.EntryStore = window.EntryStore || createEntryStore();

    function updateBootProgress(done, total, label) {
        var pct = total ? Math.max(0, Math.min(100, Math.round((done / total) * 100))) : 0;
        if (bootProgressNode) bootProgressNode.textContent = pct + '%';
        if (bootFillNode) bootFillNode.style.width = pct + '%';
        if (bootDetailNode && label) bootDetailNode.textContent = label;
    }

    function adoptBootScreen(node) {
        if (!node) return null;
        bootNode = node;
        bootStatusNode = bootNode.querySelector('.boot-gate-status');
        bootProgressNode = bootNode.querySelector('.boot-gate-progress');
        bootDetailNode = bootNode.querySelector('.boot-gate-detail');
        bootFillNode = bootNode.querySelector('.boot-gate-meter-fill');
        bootNode.classList.add('is-active');
        bootNode.classList.remove('is-complete');
        return bootNode;
    }

    function ensureBootScreen(options) {
        var panel;
        var header;
        var meter;
        var existing;
        var decoLayer;
        var close;
        var previewMode;
        options = options || {};
        previewMode = !!options.preview;
        if (bootNode && bootNode.parentNode) return bootNode;
        if (!document.body) return null;

        if (BOOT_EXCLUDED_PAGE && !previewMode) return null;
        if (!previewMode) activateBootSurface();

        existing = document.getElementById('boot-gate-screen') || (!previewMode && window.__BootGatePrelude && window.__BootGatePrelude.ensure ? window.__BootGatePrelude.ensure() : null);
        if (existing) return adoptBootScreen(existing);

        bootNode = document.createElement('div');
        bootNode.id = 'boot-gate-screen';
        bootNode.className = 'boot-gate-screen is-active';
        bootNode.setAttribute('role', 'status');
        bootNode.setAttribute('aria-live', 'polite');

        panel = document.createElement('div');
        panel.className = 'boot-gate-panel';

        header = document.createElement('div');
        header.className = 'boot-gate-title';
        header.textContent = 'ARCHIVE INITIALIZATION';

        bootStatusNode = document.createElement('div');
        bootStatusNode.className = 'boot-gate-status';
        bootStatusNode.textContent = 'Preparing entry systems';

        meter = document.createElement('div');
        meter.className = 'boot-gate-meter';
        bootFillNode = document.createElement('div');
        bootFillNode.className = 'boot-gate-meter-fill';
        meter.appendChild(bootFillNode);

        bootProgressNode = document.createElement('div');
        bootProgressNode.className = 'boot-gate-progress';
        bootProgressNode.textContent = '0%';

        bootDetailNode = document.createElement('div');
        bootDetailNode.className = 'boot-gate-detail';
        bootDetailNode.textContent = 'loading manifest';

        close = document.createElement('button');
        close.type = 'button';
        close.className = 'boot-gate-close';
        close.setAttribute('aria-label', 'Close boot preview');
        close.textContent = '×';
        close.addEventListener('click', function () { hideBootScreen({ force: true }); });

        decoLayer = document.createElement('div');
        decoLayer.className = 'boot-gate-decoration-layer';
        decoLayer.setAttribute('data-decoration-target', 'boot-gate');
        decoLayer.setAttribute('aria-hidden', 'true');

        panel.appendChild(header);
        panel.appendChild(bootStatusNode);
        panel.appendChild(meter);
        panel.appendChild(bootProgressNode);
        panel.appendChild(bootDetailNode);
        bootNode.appendChild(decoLayer);
        bootNode.appendChild(panel);
        bootNode.appendChild(close);
        document.body.appendChild(bootNode);
        return bootNode;
    }

    function buildLoginUrl() {
        var pageName = String(readConfig('wgPageName', '') || '').trim();
        var params = {};

        if (pageName && !isAuthenticationPage() && !isCreateAccountPage()) params.returnto = pageName;
        try {
            if (mw && mw.util && typeof mw.util.getUrl === 'function') {
                return mw.util.getUrl('Special:UserLogin', params);
            }
        } catch (err) {}

        return '/index.php?title=Special%3AUserLogin' + (params.returnto ? '&returnto=' + encodeURIComponent(params.returnto) : '');
    }

    function ensureLoginGateAction(panel) {
        var copy;
        var action;

        if (loginGateActionNode && loginGateActionNode.parentNode) return loginGateActionNode;
        loginGateActionNode = document.createElement('div');
        loginGateActionNode.className = 'boot-gate-login';

        copy = document.createElement('div');
        copy.className = 'boot-gate-login-copy';
        copy.textContent = 'SIGN IN TO ENTER THE WIKI.';

        action = document.createElement('a');
        action.className = 'boot-gate-login-action';
        action.href = buildLoginUrl();
        action.textContent = 'LOGIN';
        action.setAttribute('role', 'button');

        loginGateActionNode.appendChild(copy);
        loginGateActionNode.appendChild(action);
        panel.appendChild(loginGateActionNode);
        return loginGateActionNode;
    }

    function showLoginGate() {
        var node = ensureBootScreen();
        var panel;
        var title;
        var action;

        if (!node || !requiresLoginGate()) return false;
        activateBootSurface();
        loginGateLocked = true;
        node.classList.remove('is-complete');
        node.classList.add('is-active', 'is-login-required');
        node.setAttribute('role', 'dialog');
        node.setAttribute('aria-modal', 'true');
        node.setAttribute('aria-live', 'off');

        panel = node.querySelector('.boot-gate-panel');
        title = node.querySelector('.boot-gate-title');
        if (title) title.textContent = 'ACCOUNT AUTHENTICATION';
        if (bootStatusNode) bootStatusNode.textContent = 'LOGIN REQUIRED';
        if (bootDetailNode) bootDetailNode.textContent = '';
        if (bootProgressNode) bootProgressNode.textContent = '';
        if (bootFillNode) bootFillNode.style.width = '100%';

        action = panel ? ensureLoginGateAction(panel).querySelector('.boot-gate-login-action') : null;
        if (action) {
            action.href = buildLoginUrl();
            window.requestAnimationFrame(function () {
                try { action.focus({ preventScroll: true }); }
                catch (err) { try { action.focus(); } catch (ignore) {} }
            });
        }
        return true;
    }

    function hideBootScreen(options) {
        var node = bootNode || document.getElementById('boot-gate-screen');
        options = options || {};
        if (loginGateLocked && requiresLoginGate() && !options.force) return false;
        if (!node) {
            document.documentElement.classList.remove('boot-gate-active');
            if (document.body) document.body.classList.remove('boot-gate-active');
            return true;
        }
        node.classList.add('is-complete');
        node.classList.remove('is-active');
        window.setTimeout(function () {
            if (node.parentNode) node.parentNode.removeChild(node);
            if (bootNode === node) bootNode = null;
            loginGateActionNode = null;
            document.documentElement.classList.remove('boot-gate-active');
            if (document.body) document.body.classList.remove('boot-gate-active');
        }, 240);
        return true;
    }

    function collectFlagsFromNationPayload(payload) {
        var files = [];
        function add(value) {
            if (!value) return;
            if (typeof value === 'string') {
                files.push(value);
                return;
            }
            if (typeof value === 'object') files.push(value.file || value.flag_file || value.flag || value.flag_title || '');
        }
        function scanItem(item) {
            if (!item || typeof item !== 'object') return;
            if (Array.isArray(item.flags)) item.flags.forEach(add);
            add(item.flag_file || item.flag || item.flag_title || '');
        }
        (payload && payload.continents || []).forEach(function (continent) {
            (continent.regions || []).forEach(function (region) {
                (region.items || []).forEach(scanItem);
            });
        });
        return unique(files);
    }

    function collectFlagsFromLinkMap(payload) {
        var files = [];
        var source = payload && payload.items ? payload.items : {};
        Object.keys(source || {}).forEach(function (key) {
            var item = source[key];
            if (!item || typeof item !== 'object') return;
            files.push(item.flag_file || item.flag || item.flag_title || '');
        });
        return unique(files);
    }

    function prewarmImages(urls, limit) {
        return window.EntryStore.preloadImages(urls, {
            limit: Number(limit) > 0 ? Number(limit) : 0,
            concurrency: 16,
            persistent: false
        });
    }

    function getDecorationRuntime() {
        return window.Decorations || window.CLBI_DECORATIONS || null;
    }

    function waitForDecorationRuntime() {
        return new Promise(function (resolve) {
            var tries = 0;
            function tick() {
                var runtime = getDecorationRuntime();
                if (runtime) return resolve(runtime);
                tries += 1;
                if (tries > 40) return resolve(null);
                window.setTimeout(tick, 25);
            }
            tick();
        });
    }

    function matchesDecorationEntry(entry, filter) {
        var era = String(filter && filter.era || '').trim();
        var page = String(filter && filter.page || '').trim();
        var entryPage = String(entry && entry.page || '').replace(/_/g, ' ').trim();
        if (!entry || typeof entry !== 'object') return false;
        if (page && entryPage && entryPage !== page) return false;
        if (era && String(entry.era || '').trim() && String(entry.era || '').trim() !== era) return false;
        return true;
    }

    function prepareDecorationSet(task, level) {
        var ref = task.ref || 'MediaWiki:Decorations.json';
        return window.EntryStore.fetchJsonRef(ref, { noStore: !!task.noStore }).then(function (registry) {
            var list = registry && Array.isArray(registry.decorations) ? registry.decorations : [];
            var pixelRefs = [];
            list.forEach(function (entry) {
                var type = String(entry && entry.assetType || '').toLowerCase();
                var asset = String(entry && (entry.asset || entry.src) || '').trim();
                if (!asset) return;
                if (type !== 'pixel-json' && !/\.json(?:[?#].*)?$/i.test(asset)) return;
                if (!matchesDecorationEntry(entry, task)) return;
                pixelRefs.push(asset);
            });
            if (level !== 'full' || task.preparePixels === false || !pixelRefs.length) return registry;
            return waitForDecorationRuntime().then(function (runtime) {
                if (!runtime || typeof runtime.preparePixelCanvas !== 'function') {
                    return Promise.all(pixelRefs.map(function (pixelRef) {
                        return window.EntryStore.fetchJsonRef(pixelRef).catch(function () { return null; });
                    })).then(function () { return registry; });
                }
                return Promise.all(pixelRefs.map(function (pixelRef) {
                    return runtime.preparePixelCanvas(pixelRef).catch(function () { return null; });
                })).then(function () { return registry; });
            });
        });
    }

    function prepareNationsEra(task, level) {
        var era = String(task.era || '1950');
        var listRef = task.listRef || ('MediaWiki:' + era + '_Nation_List.json');
        var linkRef = task.linkMapRef || ('MediaWiki:' + era + '_Nation_Link_Map.json');
        var jsonPhaseId = BootPerf.start('nations-era json fetch', { era: era, level: level, listRef: listRef, linkRef: linkRef });
        var listPromise = window.EntryStore.fetchJsonRef(listRef).catch(function () { return null; });
        var linkPromise = window.EntryStore.fetchJsonRef(linkRef).catch(function () { return null; });

        return Promise.all([listPromise, linkPromise]).then(function (results) {
            BootPerf.end(jsonPhaseId, { ok: true });
            var files;
            var flagUrls;
            if (level !== 'full' && level !== 'warm') return results;
            files = unique(collectFlagsFromNationPayload(results[0]).concat(collectFlagsFromLinkMap(results[1])));
            return BootPerf.measure('nations-era flag url resolve', { era: era, count: files.length }, function () {
                return window.EntryStore.resolveFlagUrls(files);
            }).then(function () {
                if (level === 'warm') {
                    /*
                     * Warm tab contract — 20260708.
                     *
                     * A previous tab already stored the same-revision flag URL and image
                     * blobs.  Re-decoding every flag here makes the second tab feel as slow
                     * as the first one.  In warm mode we only hydrate the flag URL table from
                     * EntryCache so NationsPanel can render stable URLs immediately; image
                     * decode remains demand-driven by the live panel instead of blocking the
                     * boot screen.
                     */
                    return results;
                }
                flagUrls = files.map(function (file) { return window.EntryStore.getFlagUrl(file); }).filter(Boolean);
                /*
                 * 20260710: Flag URLs are part of the entry contract, but decoding every
                 * flag image is not.  The 1950 list has more than 150 flags, and waiting
                 * for all image decodes kept the public boot gate open for several seconds.
                 * Keep revision-aware URLs ready for the panel, then let the browser load
                 * the actual images from the live DOM.  A low-priority background warm-up
                 * may run after the boot gate has opened, so it never competes with the
                 * current entry surface or the globe ready contract.
                 */
                BootPerf.mark('nations-era flag image prewarm skipped', {
                    era: era,
                    count: flagUrls.length,
                    limit: task.flagLimit == null ? 0 : Number(task.flagLimit),
                    reason: 'deferred-after-boot-gate'
                });
                if (flagUrls.length && task.deferFlagImages !== false) {
                    deferredEntryWarmups.push(function () {
                        return BootPerf.measure('deferred nations-era flag image prewarm', {
                            era: era,
                            count: flagUrls.length,
                            limit: task.flagLimit == null ? 0 : Number(task.flagLimit)
                        }, function () {
                            return prewarmImages(flagUrls, task.flagLimit == null ? 0 : Number(task.flagLimit));
                        });
                    });
                }
                return results;
            });
        });
    }

    function prepareGlobeSharedAssets(task, level) {
        var refs = Array.isArray(task && task.assets) ? task.assets : [];
        if (!refs.length) return Promise.resolve(null);
        return BootPerf.measure('globe-shared url resolve', { count: refs.length, level: level }, function () {
            return Promise.all(refs.map(function (ref) {
                return window.EntryStore.resolveFileUrl(ref).catch(function () { return ''; });
            }));
        }).then(function (urls) {
            urls = urls.filter(Boolean);
            /*
             * Globe shared assets are heavy 6K texture sources.  The live
             * NationsGlobe ready contract already waits for the actual Three.js
             * texture path, so decoding the same images here only duplicates work
             * and holds the boot screen.  Keep revision-aware URL resolution in the
             * entry ledger, but make the image warm-up non-blocking.
             */
            BootPerf.mark('globe-shared image prewarm skipped', {
                count: urls.length,
                level: level,
                reason: 'deferred-to-nations-globe-ready-contract'
            });
            window.setTimeout(function () {
                if (!window.EntryStore || typeof window.EntryStore.preloadImages !== 'function') return;
                window.EntryStore.preloadImages(urls, {
                    limit: task.imageLimit == null ? 0 : Number(task.imageLimit),
                    concurrency: Math.max(1, Math.min(16, Number(task.concurrency) || 4)),
                    persistent: false
                }).catch(function () {});
            }, 0);
            return urls;
        });
    }

    function prepareImageAssets(task, level) {
        var refs = Array.isArray(task && task.assets) ? task.assets : [];
        var full = level === 'full';
        var exposeAs = String(task && task.exposeAs || '').trim();
        if (!refs.length) return Promise.resolve(null);
        return BootPerf.measure('image-assets url resolve', { count: refs.length, exposeAs: exposeAs, level: level }, function () {
            return Promise.all(refs.map(function (ref) {
                return window.EntryStore.resolveFileUrl(ref).catch(function () { return ''; });
            }));
        }).then(function (urls) {
            urls = urls.filter(Boolean);
            if (exposeAs === 'nationsGlobeLoadingGif') {
                window.NationsGlobeLoadingGifRef = refs[0] || '';
                window.NationsGlobeLoadingGifUrl = urls[0] || '';
                window.NationsGlobeLoadingGifFile = 'Gfx-vhs-glitch-001.gif';
            }
            if (!full) return urls;
            return BootPerf.measure('image-assets image prewarm', { count: urls.length, exposeAs: exposeAs, limit: task.imageLimit == null ? 0 : Number(task.imageLimit), concurrency: Math.max(1, Math.min(16, Number(task.concurrency) || 2)) }, function () {
                return window.EntryStore.preloadImages(urls, {
                    limit: task.imageLimit == null ? 0 : Number(task.imageLimit),
                    concurrency: Math.max(1, Math.min(16, Number(task.concurrency) || 2))
                });
            }).then(function () { return urls; });
        });
    }

    function prepareHtmlEntry(task) {
        var url = String(task && (task.url || task.ref) || '').trim();
        if (!url) return Promise.resolve(null);
        return window.EntryStore.fetchTextUrl(url, { noStore: !!task.noStore, resourceRef: task.resourceRef || task.ref || task.title || '' });
    }

    function prepareTask(task, defaultLevel) {
        var level = String(task && (task.level || defaultLevel) || 'half').toLowerCase();
        var type = String(task && task.type || '').toLowerCase();
        if (!task || typeof task !== 'object') return Promise.resolve(null);
        if (type === 'html') return prepareHtmlEntry(task);
        if (type === 'json') return window.EntryStore.fetchJsonRef(task.ref, { noStore: !!task.noStore });
        if (type === 'pixel-json') {
            return waitForDecorationRuntime().then(function (runtime) {
                if (level === 'full' && runtime && typeof runtime.preparePixelCanvas === 'function') {
                    return runtime.preparePixelCanvas(task.ref || task.asset);
                }
                return window.EntryStore.fetchJsonRef(task.ref || task.asset);
            });
        }
        if (type === 'decorations') return prepareDecorationSet(task, level);
        if (type === 'nations-era') return prepareNationsEra(task, level);
        if (type === 'globe-shared-assets') return prepareGlobeSharedAssets(task, level);
        if (type === 'image-assets') return prepareImageAssets(task, level);
        return Promise.resolve(null);
    }

    function loadManifest() {
        return BootPerf.measure('revision manifest load', {}, function () {
            return (window.RevisionManifest && typeof window.RevisionManifest.load === 'function' ? window.RevisionManifest.load() : Promise.resolve(null));
        }).then(function () {
            return BootPerf.measure('entry manifest json fetch', { ref: MANIFEST_TITLE }, function () {
                return window.EntryStore.fetchJsonRef(MANIFEST_TITLE, { noStore: false });
            });
        }).then(function (manifest) {
            if (!manifest || typeof manifest !== 'object' || !manifest.version) {
                BootPerf.mark('entry manifest fallback', { reason: 'invalid manifest' });
                return defaultManifest;
            }
            BootPerf.mark('entry manifest ready', { version: manifest.version });
            return manifest;
        }).catch(function (err) {
            BootPerf.mark('entry manifest fallback', { reason: err && (err.message || String(err)) || 'load failed' });
            return defaultManifest;
        });
    }

    function flattenInitialTasks(manifest) {
        var initial = manifest && manifest.initial ? manifest.initial : {};
        var full = Array.isArray(initial.full) ? initial.full : [];
        var half = Array.isArray(initial.half) ? initial.half : [];
        var tasks = [];
        full.forEach(function (task) {
            task = Object.assign({}, task);
            task.level = task.level || 'full';
            task.blocking = true;
            tasks.push(task);
        });
        half.forEach(function (task) {
            task = Object.assign({}, task);
            task.level = task.level || 'half';
            task.blocking = false;
            tasks.push(task);
        });
        return tasks;
    }


    function flattenWarmInitialTasks(tasks) {
        var warmed = [];
        (tasks || []).forEach(function (task) {
            var copy;
            var type = String(task && task.type || '').toLowerCase();
            if (!task || task.blocking === false) return;

            /*
             * Warm boot fast path — 20260708.
             *
             * Cold boot intentionally performs the expensive work: resolving every flag,
             * downloading/decoding images, preparing decoration canvases, and warming shared
             * globe textures.  Once a tab has certified the pack against the current revision
             * manifest, later tabs must not replay that full workload.  They only hydrate the
             * small tables needed by the live page and let already-cached image/blob data be
             * consumed on demand.  This is the missing layer that made second tabs feel almost
             * as slow as first tabs even though the string checks were true.
             */
            if (type === 'globe-shared-assets') return;
            if (type === 'image-assets') return;
            copy = Object.assign({}, task);
            if (type === 'nations-era') copy.level = 'warm';
            else if (type === 'decorations') {
                copy.level = 'half';
                copy.preparePixels = false;
            } else if (type === 'pixel-json') copy.level = 'half';
            warmed.push(copy);
        });
        return warmed;
    }

    function currentInitialPackKey(manifest) {
        var version = manifest && manifest.version ? String(manifest.version) : BUILD_ID;
        return 'initial-entry-full:' + version;
    }

    function currentInitialPackToken(manifest) {
        var version = manifest && manifest.version ? String(manifest.version) : BUILD_ID;
        var revToken = window.RevisionManifest && typeof window.RevisionManifest.manifestToken === 'function' ? window.RevisionManifest.manifestToken(version) : version;
        return revToken;
    }

    function isInitialPackWarm(manifest) {
        var key = currentInitialPackKey(manifest);
        var token = currentInitialPackToken(manifest);
        return !!(window.EntryCache && typeof window.EntryCache.packReady === 'function' && window.EntryCache.packReady(key, token));
    }

    function markInitialPackWarm(manifest, meta) {
        var key = currentInitialPackKey(manifest);
        var token = currentInitialPackToken(manifest);
        if (window.EntryCache && typeof window.EntryCache.setPackReady === 'function') {
            window.EntryCache.setPackReady(key, token, Object.assign({ manifestVersion: manifest && manifest.version || BUILD_ID }, meta || {}));
        }
    }


    function waitMs(ms) {
        return new Promise(function (resolve) { window.setTimeout(resolve, Math.max(0, ms || 0)); });
    }

    function waitForBody() {
        if (document.body) return Promise.resolve(document.body);
        return new Promise(function (resolve) {
            function tick() {
                if (document.body) return resolve(document.body);
                window.setTimeout(tick, 10);
            }
            tick();
        });
    }

    function waitForDomReady() {
        if (document.readyState !== 'loading') return Promise.resolve();
        return new Promise(function (resolve) {
            document.addEventListener('DOMContentLoaded', resolve, { once: true });
        });
    }

    function waitForAnimationFrames(count) {
        count = Math.max(1, Math.round(count || 1));
        return new Promise(function (resolve) {
            function next(left) {
                if (left <= 0) return resolve();
                window.requestAnimationFrame(function () { next(left - 1); });
            }
            next(count);
        });
    }

    function waitUntil(predicate, options) {
        var started = now();
        var timeout = options && options.timeoutMs ? options.timeoutMs : 8000;
        var interval = options && options.intervalMs ? options.intervalMs : 60;
        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 (now() - started >= timeout) return resolve({ ok: false, timeout: true });
                window.setTimeout(tick, interval);
            }
            tick();
        });
    }

    function waitForTrackedScripts() {
        var list = (window.EntryScriptLoads || []).slice();
        if (!list.length) return Promise.resolve([]);
        return Promise.all(list.map(function (promise) {
            return Promise.resolve(promise).catch(function (err) { return { ok: false, error: err }; });
        }));
    }

    function waitForDocumentSurface() {
        return waitForDomReady().then(function () {
            return waitForBody();
        }).then(function () {
            return waitUntil(function () {
                return document.querySelector('.content-wrapper') && document.querySelector('.liberty-content-main');
            }, { timeoutMs: 8000, intervalMs: 50 });
        });
    }

    function isNationsPageSurface() {
        var page = String(mw && mw.config ? (mw.config.get('wgPageName') || mw.config.get('wgTitle') || '') : '');
        return !!document.querySelector('.clbi-nations-panel-stack') || /(?:^|[_ ])시대(?:$|[_ ])/.test(page) || /(?:^|[_ ])Era(?:$|[_ ])/i.test(page);
    }

    function getInitialNationsEra(manifest) {
        var tasks = flattenInitialTasks(manifest);
        var i;
        for (i = 0; i < tasks.length; i += 1) {
            if (tasks[i] && tasks[i].type === 'nations-era' && String(tasks[i].level || '').toLowerCase() === 'full') {
                return String(tasks[i].era || '1950');
            }
        }
        return '1950';
    }

    function waitForNationsPanelReady(era) {
        return BootPerf.measure('wait nations panel api', { era: era }, function () {
            return waitUntil(function () { return window.NationsPanel && typeof window.NationsPanel.whenEraReady === 'function'; }, {
                timeoutMs: 8000,
                intervalMs: 50
            });
        }).then(function (result) {
            if (!result.ok || !window.NationsPanel || typeof window.NationsPanel.whenEraReady !== 'function') return null;
            return BootPerf.measure('wait nations panel era ready', { era: era }, function () {
                return window.NationsPanel.whenEraReady(era, { timeoutMs: 15000 }).catch(function () { return null; });
            });
        }).then(function () {
            return BootPerf.measure('wait nations panel dom ready', { era: era }, function () {
                return waitUntil(function () {
                    var panel = document.querySelector('.clbi-nations-era-content[data-era-content="' + era + '"] .clbi-nations-tabpanel[data-nation-list-source="1"]') ||
                        document.querySelector('.clbi-nations-tabpanel[data-nation-list-source="1"]');
                    return panel && panel.classList.contains('clbi-nations-list-json-ready') && panel.getAttribute('data-nation-list-year-loaded') === String(era);
                }, { timeoutMs: 15000, intervalMs: 80 });
            });
        });
    }

    function waitForNationsGlobeReady(options) {
        var globe = document.querySelector('.clbi-nations-globe-window[data-nations-globe], [data-nations-globe="real-world"]');
        var soft;
        var timeout;

        options = options || {};
        soft = !!options.soft;
        timeout = Number(options.timeoutMs || (soft ? 450 : 30000));

        if (!globe) {
            BootPerf.mark('wait nations globe skipped', { reason: 'no globe node', soft: soft });
            return Promise.resolve(null);
        }

        return BootPerf.measure(soft ? 'wait nations globe warm attach' : 'wait nations globe ready contract', { soft: soft, timeoutMs: timeout }, function () {
            if (soft) {
                /*
                Warm-tab policy:
                Raw globe images are already revision-checked and blob-cached by EntryCache,
                but WebGL scene creation, GPU upload, topojson parsing, and per-tab Three.js
                objects cannot be shared across browser tabs.  Waiting for the full is-ready
                contract here makes a warm tab slower than the first tab.  In warm mode the
                initial gate only waits until the globe consumer has been attached, then the
                globe finishes its own per-tab hydrate without holding the whole page hostage.
                */
                return waitUntil(function () {
                    var node = document.querySelector('.clbi-nations-globe-window[data-nations-globe], [data-nations-globe="real-world"]');
                    if (!node) return false;
                    return node.getAttribute('data-nations-globe-ready') === '1' ||
                        !!node.CLBI_NationsGlobeInstance ||
                        !!node.querySelector('.clbi-nations-globe-stage') ||
                        node.classList.contains('is-ready') ||
                        node.classList.contains('has-error');
                }, { timeoutMs: timeout, intervalMs: 40 });
            }

            return waitUntil(function () {
                var node = document.querySelector('.clbi-nations-globe-window[data-nations-globe], [data-nations-globe="real-world"]');
                return node && (node.classList.contains('is-ready') || node.classList.contains('has-error'));
            }, { timeoutMs: timeout, intervalMs: 100 });
        });
    }

    function waitForDecorationsReady(options) {
        options = options || {};
        return waitForDecorationRuntime().then(function (runtime) {
            if (!runtime) return null;
            if (options.warm) {
                /*
                 * Warm tab contract — 20260708.
                 *
                 * The decoration registry and pixel canvases were prepared by the cold tab.
                 * Calling reload() here re-reads and re-syncs the decoration layer during the
                 * boot gate, which is visible as a pointless delay.  A warm tab only needs the
                 * current visibility pass so boot can release quickly and editing/preview tools
                 * remain responsive.
                 */
                try {
                    if (typeof runtime.updateVisibility === 'function') runtime.updateVisibility(document);
                    else if (typeof runtime.sync === 'function') runtime.sync();
                } catch (err) {}
                return waitForAnimationFrames(1);
            }
            if (typeof runtime.reload !== 'function') return null;
            return runtime.reload().catch(function () { return null; }).then(function () {
                return waitForAnimationFrames(2);
            });
        });
    }

    function waitForCurrentEntrySurface(manifest, options) {
        var era = getInitialNationsEra(manifest);
        var warmPack;

        options = options || {};
        warmPack = !!options.warmPack;

        if (!isNationsPageSurface()) {
            return waitForDecorationsReady({ warm: !!warmPack }).then(function () { return waitForAnimationFrames(1); });
        }
        if (bootStatusNode) bootStatusNode.textContent = warmPack ? 'Hydrating current information surface from warm cache' : 'Preparing current information surface';
        if (bootDetailNode) bootDetailNode.textContent = 'waiting for era ' + era + ' ready contract';
        return waitForNationsPanelReady(era)
            .then(function () {
                if (warmPack) {
                    if (bootDetailNode) bootDetailNode.textContent = 'attaching globe warm consumer';
                    return waitForNationsGlobeReady({ soft: true, timeoutMs: 450 });
                }
                if (bootDetailNode) bootDetailNode.textContent = 'waiting for globe ready contract';
                return waitForNationsGlobeReady();
            })
            .then(function () {
                if (bootDetailNode) bootDetailNode.textContent = 'waiting for decoration surface';
                return waitForDecorationsReady({ warm: warmPack });
            })
            .then(function () { return waitForAnimationFrames(warmPack ? 1 : 2); });
    }

    function runDeferredEntryWarmups() {
        var queue = deferredEntryWarmups.splice(0);
        if (!queue.length) return;
        window.setTimeout(function () {
            var chain = Promise.resolve();
            queue.forEach(function (job) {
                chain = chain.then(function () {
                    try { return job(); }
                    catch (err) { return null; }
                }).catch(function () { return null; });
            });
        }, 1200);
    }

    function runInitialLoad(options) {
        var done = 0;
        var manifestRef;
        var tasks;
        var bootOptions;
        var timedOut = false;
        var warmPack = false;

        options = options || {};
        bootStartTime = window.__BootGatePrelude && window.__BootGatePrelude.startTime ? window.__BootGatePrelude.startTime : now();
        activateBootSurface();
        ensureBootScreen();
        updateBootProgress(0, 1, 'loading entry manifest');

        manifestRef = loadManifest().then(function (manifest) {
            var minDisplay;
            var totalSteps;
            var maxBlockingMs;
            var timeoutHandle;
            bootOptions = manifest.boot || {};
            warmPack = isInitialPackWarm(manifest);
            minDisplay = options.minDisplayMs || (warmPack ? (bootOptions.cachedMinDisplayMs || 220) : (bootOptions.minDisplayMs || 950));
            tasks = flattenInitialTasks(manifest);
            if (!tasks.length) tasks = flattenInitialTasks(defaultManifest);
            if (warmPack) tasks = flattenWarmInitialTasks(tasks);

            /*
            Full/half contract:
            These manifest tasks only prepare data assets.  The gate is not allowed to open
            until the current page surface has consumed that data and reported a real ready
            state below.  Do not reintroduce localStorage-only skip logic here; a cached
            version number is not the same as current-tab readiness.
            */
            totalSteps = tasks.length + 4;
            updateBootProgress(0, totalSteps, warmPack ? 'hydrating warm entry cache' : 'starting entry packs');

            return new Promise(function (resolve) {
                maxBlockingMs = Number(options.maxBlockingMs || bootOptions.maxBlockingMs || 30000);
                timeoutHandle = window.setTimeout(function () {
                    timedOut = true;
                    resolve();
                }, maxBlockingMs);

                Promise.all(tasks.map(function (task) {
                    var label = task.label || task.id || task.type || 'entry task';
                    return BootPerf.measure('boot task: ' + label, { id: task.id || '', type: task.type || '', level: task.level || task.level === 0 ? task.level : '' }, function () {
                        return prepareTask(task, task.level);
                    }).catch(function () {
                        return null;
                    }).then(function () {
                        done += 1;
                        updateBootProgress(done, totalSteps, label);
                    });
                })).then(function () {
                    updateBootProgress(++done, totalSteps, 'loading subsystem scripts');
                    return BootPerf.measure('wait tracked subsystem scripts', { count: (window.EntryScriptLoads || []).length }, function () { return waitForTrackedScripts(); });
                }).then(function () {
                    updateBootProgress(++done, totalSteps, 'waiting for document shell');
                    return BootPerf.measure('wait document shell', {}, function () { return waitForDocumentSurface(); });
                }).then(function () {
                    updateBootProgress(++done, totalSteps, 'waiting for current entry surface');
                    return BootPerf.measure('wait current entry surface', { warmPack: warmPack }, function () { return waitForCurrentEntrySurface(manifest, { warmPack: warmPack }); });
                }).then(function () {
                    updateBootProgress(totalSteps, totalSteps, warmPack ? 'warm entry surface complete' : 'entry surface complete');
                    window.clearTimeout(timeoutHandle);
                    resolve();
                }).catch(function () {
                    window.clearTimeout(timeoutHandle);
                    resolve();
                });
            }).then(function () {
                var elapsed = now() - bootStartTime;
                var wait = Math.max(0, minDisplay - elapsed);
                return waitMs(wait);
            }).then(function () {
                localStorage.setItem(READY_KEY, String(manifest.version));
                if (!timedOut) markInitialPackWarm(manifest, { warmSource: warmPack ? 'cache-hit' : 'cold-build' });
                if (bootStatusNode) bootStatusNode.textContent = timedOut ? 'Entry surface ready with deferred items' : (warmPack ? 'Entry surface ready from warm cache' : 'Entry surface ready');
                updateBootProgress(tasks.length + 4, tasks.length + 4, timedOut ? 'timeout: continuing deferred loading' : (warmPack ? 'warm cache complete' : 'complete'));
                BootPerf.mark('boot gate complete', { timedOut: timedOut, warmPack: warmPack, manifestVersion: manifest && manifest.version || '' });
                BootPerf.print();
                runDeferredEntryWarmups();
                window.setTimeout(function () {
                    if (requiresLoginGate()) showLoginGate();
                    else hideBootScreen();
                }, 120);
                return manifest;
            });
        });

        return manifestRef;
    }

    function startBoot(options) {
        if (BOOT_EXCLUDED_PAGE && !(options && options.force)) {
            hideBootScreen();
            return Promise.resolve({ skipped: true, reason: 'developer-or-editing-page' });
        }
        if (hasBootParam('0') && !requiresLoginGate() && !(options && options.force)) return Promise.resolve(null);
        if (bootStarted && bootPromise && !(options && options.force)) return bootPromise;
        bootStarted = true;
        bootPromise = runInitialLoad(options || {});
        return bootPromise;
    }

    function resetBoot() {
        localStorage.removeItem(READY_KEY);
        bootStarted = false;
        bootPromise = null;
        loginGateLocked = false;
    }

    /*
    BootGate intentionally has no SPA hold/release API.
    Initial boot is the only blocking phase; later SPA routes must consume prepared
    EntryStore artifacts without reopening the loading surface.
    */

    function buildBootReport() {
        return {
            build: BUILD_ID,
            state: {
                started: bootStarted,
                excludedPage: BOOT_EXCLUDED_PAGE,
                loginRequired: requiresLoginGate(),
                loginLocked: loginGateLocked,
                hasNode: !!((bootNode && bootNode.parentNode) || document.getElementById('boot-gate-screen')),
                htmlActive: !!(document.documentElement && document.documentElement.classList.contains('boot-gate-active')),
                bodyActive: !!(document.body && document.body.classList.contains('boot-gate-active'))
            },
            store: window.EntryStore && window.EntryStore.cacheInfo ? window.EntryStore.cacheInfo() : null,
            bootPerf: window.BootPerf && typeof window.BootPerf.summary === 'function' ? window.BootPerf.summary() : null,
            revisionManifest: window.RevisionManifest && typeof window.RevisionManifest.status === 'function' ? window.RevisionManifest.status() : null,
            nations: window.NationsPanel && typeof window.NationsPanel.diagnostics === 'function' ? window.NationsPanel.diagnostics() : null,
            decorations: window.Decorations && typeof window.Decorations.diagnostics === 'function' ? window.Decorations.diagnostics() : null
        };
    }

    window.EntryLoader = window.EntryLoader || {
        loadManifest: loadManifest,
        prepareTask: prepareTask,
        prepareNationsEra: prepareNationsEra,
        prepareDecorationSet: prepareDecorationSet,
        prepareGlobeSharedAssets: prepareGlobeSharedAssets,
        prepareImageAssets: prepareImageAssets,
        prepareHtmlEntry: prepareHtmlEntry,
        runInitialLoad: runInitialLoad
    };

    function showBootPreview(options) {
        var node;
        options = options || {};
        /*
         * Boot preview is a design/editing surface, not a real boot gate.
         * The earlier preview reused activateBootSurface(), which applied
         * html.boot-gate-active and hid the entire wiki shell, including
         * DevTools.  That made it impossible to edit loading-screen
         * decorations while previewing them.  Keep the real first-entry gate
         * full-screen, but make preview a small non-blocking surface below the
         * DevTools z-index so the owner can keep using the editor.
         */
        node = ensureBootScreen({ preview: true });
        if (!node) return null;
        node.classList.add('is-preview');
        if (bootStatusNode) bootStatusNode.textContent = options.status || 'Loading screen preview';
        updateBootProgress(Number(options.progress || 64), 100, options.detail || 'preview mode: no entry tasks are running');
        try {
            if (window.Decorations && typeof window.Decorations.sync === 'function') window.Decorations.sync();
        } catch (err) {}
        return node;
    }

    window.BootGate = window.BootGate || {
        version: BUILD_ID,
        start: startBoot,
        show: function (options) {
            options = options || {};
            options.force = true;
            resetBoot();
            return startBoot(options);
        },
        reset: resetBoot,
        hide: hideBootScreen,
        preview: showBootPreview,
        report: buildBootReport,
        state: function () {
            return {
                started: bootStarted,
                loginRequired: requiresLoginGate(),
                loginLocked: loginGateLocked,
                hasNode: !!((bootNode && bootNode.parentNode) || document.getElementById('boot-gate-screen')),
                htmlActive: !!(document.documentElement && document.documentElement.classList.contains('boot-gate-active')),
                bodyActive: !!(document.body && document.body.classList.contains('boot-gate-active')),
                prelude: !!window.__BootGatePrelude,
                store: window.EntryStore && window.EntryStore.cacheInfo ? window.EntryStore.cacheInfo() : null,
                revisionManifest: window.RevisionManifest && typeof window.RevisionManifest.status === 'function' ? window.RevisionManifest.status() : null
            };
        }
    };

    function prime() {
        if (BOOT_EXCLUDED_PAGE) {
            hideBootScreen();
            return;
        }
        activateBootSurface();
        waitForBody().then(function () {
            ensureBootScreen();
            return waitForAnimationFrames(1);
        }).then(function () {
            startBoot();
        });
    }

    waitForBody().then(function () {
        if (!BOOT_EXCLUDED_PAGE) ensureBootScreen();
        else hideBootScreen();
    });
    window.setTimeout(prime, 0);
})(window, document, window.mediaWiki || window.mw);


loadClbiRawScript('MediaWiki:DevTools.js');
loadClbiRawScript('MediaWiki:NationsPanel.js');
loadClbiRawScript('MediaWiki:NationsGlobe.js');

/* CLBI safety guard: adaptive reset functions must exist before shell metric callbacks run. */
function resetLeftRecentAdaptiveState() {
    var list = document.getElementById('clbi-left-recent-list');
    var newsBox = list ? list.closest('.clbi-left-news-box') : null;
    var items = list ? Array.prototype.slice.call(list.querySelectorAll('.news-recent-item')) : [];

    if (newsBox) {
        newsBox.classList.remove('is-adaptive-constrained');
        newsBox.style.removeProperty('--adaptive-news-h');
    }

    if (list) {
        list.classList.remove('is-adaptive-faded');
        list.removeAttribute('data-adaptive-limit');
        list.style.removeProperty('--adaptive-recent-h');
    }

    items.forEach(function (item) {
        item.classList.remove('is-adaptive-hidden');
    });
}

function resetLeftBillboardAdaptiveState() {
    var box = document.querySelector('#clbi-left-sidebar .left-billboard-box');

    if (!box) return;

    box.classList.remove('is-left-ad-title-only', 'is-left-ad-extended');
    box.style.removeProperty('--left-billboard-h');
    box.style.removeProperty('--left-billboard-finish-h');
}

window.resetLeftRecentAdaptiveState = resetLeftRecentAdaptiveState;
window.resetLeftBillboardAdaptiveState = resetLeftBillboardAdaptiveState;

loadClbiRawScript('MediaWiki:AnecdoteViewer.js');


(function () {
    'use strict';

    var SYSTEM_TITLE_NAMESPACES = {
        '-1': true,
        '4': true,
        '5': true,
        '6': true,
        '7': true,
        '8': true,
        '9': true,
        '10': true,
        '11': true,
        '12': true,
        '13': true,
        '14': true,
        '15': true,
        '828': true,
        '829': true
    };

    function normalizePageNameForShell(value) {
        return String(value || '')
            .split('?')[0]
            .replace(/^\/index\.php\//, '')
            .replace(/_/g, ' ')
            .trim();
    }

    function readCurrentPageNameForShell() {
        var pageName = mw.config.get('wgPageName') || '';

        if (pageName) {
            return normalizePageNameForShell(pageName);
        }

        return normalizePageNameForShell(window.location.pathname || '');
    }

    function isAnecdoteNamespaceForShell() {
        var namespaceNumber = Number(mw.config.get('wgNamespaceNumber'));
        var canonicalNamespace = String(mw.config.get('wgCanonicalNamespace') || '').toLowerCase();
        var pageName = readCurrentPageNameForShell();

        return namespaceNumber === 3000 ||
            canonicalNamespace === 'anecdote' ||
            /^(anecdote|에넥도트):/i.test(pageName);
    }

    function isBackendOrSystemPageForShell() {
        var namespaceNumber = Number(mw.config.get('wgNamespaceNumber'));
        var action = String(mw.config.get('wgAction') || 'view').toLowerCase();
        var contentModel = String(mw.config.get('wgPageContentModel') || '').toLowerCase();
        var pageName = readCurrentPageNameForShell();
        var lowerPageName = pageName.toLowerCase();

        if (action && action !== 'view') {
            return true;
        }

        if (pageName === '대문') {
            return false;
        }

        if (SYSTEM_TITLE_NAMESPACES[String(namespaceNumber)]) {
            return true;
        }

        if (contentModel === 'css' || contentModel === 'javascript' || contentModel === 'json' || contentModel === 'sanitized-css') {
            return true;
        }

        if (/\.(css|js|json)$/i.test(pageName)) {
            return true;
        }

        if (/^(mediawiki|미디어위키|special|특수):/i.test(pageName)) {
            return true;
        }

        return false;
    }

    function isMediaWikiSystemAssetPageForShell() {
        var namespaceNumber = Number(mw.config.get('wgNamespaceNumber'));
        var pageName = readCurrentPageNameForShell();
        var contentModel = String(mw.config.get('wgPageContentModel') || '').toLowerCase();

        return namespaceNumber === 8 &&
            (/\.(css|js)$/i.test(pageName) || contentModel === 'css' || contentModel === 'javascript' || contentModel === 'sanitized-css');
    }


    var systemDocRawFetchToken = 0;

    function cleanupLegacySystemDocCodeMutationsForShell() {
        document.querySelectorAll('.clbi-system-doc-codepane').forEach(function (pane) {
            var parent;

            if (!pane || !pane.parentNode) return;

            parent = pane.parentNode;
            while (pane.firstChild) {
                parent.insertBefore(pane.firstChild, pane);
            }
            parent.removeChild(pane);
        });

        document.querySelectorAll('.clbi-system-doc-codebox').forEach(function (node) {
            node.classList.remove('clbi-system-doc-codebox');
            node.removeAttribute('data-clbi-system-doc-codebox');
            node.removeAttribute('style');
        });
    }

    function getSystemDocOutputForShell() {
        return document.querySelector('.liberty-content-main .mw-parser-output');
    }

    function findSystemDocSourceNodeForShell() {
        var output = getSystemDocOutputForShell();
        var children;
        var preferred;

        if (!output) return null;

        children = Array.prototype.slice.call(output.children || [])
            .filter(function (el) {
                return el && el.nodeType === 1 &&
                    el.id !== 'clbi-system-doc-indicator-row' &&
                    el.id !== 'clbi-system-source-viewer' &&
                    !el.classList.contains('catlinks') &&
                    (el.textContent || '').trim().length > 200;
            });

        preferred = children.filter(function (el) {
            return el.matches && el.matches('.mw-highlight, .mw-code, pre');
        })[0];

        return preferred || children.sort(function (a, b) {
            return (b.textContent || '').trim().length - (a.textContent || '').trim().length;
        })[0] || null;
    }

    function getSystemDocRawUrlForShell() {
        var title = mw.config.get('wgPageName') || readCurrentPageNameForShell();
        var url;

        if (window.mw && mw.util && typeof mw.util.getUrl === 'function') {
            url = mw.util.getUrl(title, {
                action: 'raw',
                ctype: 'text/plain'
            });
        } else {
            url = '/index.php?title=' + encodeURIComponent(title) + '&action=raw&ctype=text/plain';
        }

        if (window.RevisionManifest && typeof window.RevisionManifest.addRevisionParam === 'function') {
            url = window.RevisionManifest.addRevisionParam(url, title);
        }
        return url;
    }

    function removeSystemDocSourceViewerForShell() {
        var viewer = document.getElementById('clbi-system-source-viewer');

        if (viewer && viewer.parentNode) {
            viewer.parentNode.removeChild(viewer);
        }

        document.querySelectorAll('.clbi-system-original-source-hidden').forEach(function (node) {
            node.classList.remove('clbi-system-original-source-hidden');
            node.removeAttribute('data-clbi-system-source-hidden');
            node.style.removeProperty('display');
        });

        cleanupLegacySystemDocCodeMutationsForShell();
    }

    function ensureSystemDocSourceViewerForShell() {
        var output = getSystemDocOutputForShell();
        var source;
        var viewer;
        var fallbackText;

        if (!output || !isMediaWikiSystemAssetPageForShell()) return null;

        cleanupLegacySystemDocCodeMutationsForShell();

        source = findSystemDocSourceNodeForShell();
        if (!source) return null;

        viewer = document.getElementById('clbi-system-source-viewer');

        if (!viewer) {
            viewer = document.createElement('pre');
            viewer.id = 'clbi-system-source-viewer';
            viewer.className = 'clbi-system-source-viewer';
            output.appendChild(viewer);
        }

        fallbackText = source.textContent || '';

        if (!viewer.textContent && fallbackText) {
            viewer.textContent = fallbackText;
        }

        source.classList.add('clbi-system-original-source-hidden');
        source.setAttribute('data-clbi-system-source-hidden', 'true');
        source.style.setProperty('display', 'none', 'important');

        return viewer;
    }

    function renderSystemDocSourceViewerForShell() {
        var viewer;
        var pageName;
        var token;
        var currentScrollTop;

        if (!isMediaWikiSystemAssetPageForShell()) return;

        pageName = String(mw.config.get('wgPageName') || readCurrentPageNameForShell());
        viewer = document.getElementById('clbi-system-source-viewer');

        /*
        시스템 문서 뷰어가 이미 만들어져 있고 raw 원문도 로드된 상태라면
        다시 source 탐색/숨김/스타일 재적용을 하지 않는다.
        DevTools Elements 패널에서 body가 계속 파랗게 깜빡이던 원인은
        MutationObserver가 이 재적용을 반복해서 DOM attribute mutation을 만들었기 때문이다.
        */
        if (
            viewer &&
            viewer.getAttribute('data-clbi-raw-title') === pageName &&
            viewer.getAttribute('data-clbi-raw-loaded') === '1'
        ) {
            return;
        }

        viewer = ensureSystemDocSourceViewerForShell();
        if (!viewer) return;

        currentScrollTop = viewer.scrollTop || 0;
        viewer.setAttribute('data-clbi-raw-title', pageName);
        token = ++systemDocRawFetchToken;

        fetch(getSystemDocRawUrlForShell(), { credentials: 'same-origin' })
            .then(function (res) {
                if (!res.ok) throw new Error('raw fetch failed ' + res.status);
                return res.text();
            })
            .then(function (text) {
                if (token !== systemDocRawFetchToken) return;

                currentScrollTop = viewer.scrollTop || currentScrollTop || 0;

                if (text && viewer.textContent !== text) {
                    viewer.textContent = text;
                }

                viewer.setAttribute('data-clbi-raw-loaded', '1');
                viewer.scrollTop = currentScrollTop;
            })
            .catch(function () {
                viewer.setAttribute('data-clbi-raw-loaded', '0');
            });
    }

    function removeSystemDocIndicatorForShell() {
        var existing = document.getElementById('clbi-system-doc-indicator-row');

        if (document.body) {
            document.body.classList.remove('clbi-system-doc-page');
        }

        if (existing && existing.parentNode) {
            existing.parentNode.removeChild(existing);
        }

        removeSystemDocSourceViewerForShell();
    }

    function renderSystemDocIndicatorForShell() {
        var pageName;
        var extMatch;
        var ext;
        var row;
        var box;
        var meta;
        var label;
        var type;
        var title;
        var anchor;
        var main;

        if (!document.body || !isMediaWikiSystemAssetPageForShell()) return;

        pageName = readCurrentPageNameForShell();
        extMatch = pageName.match(/\.(css|js)$/i);
        ext = extMatch ? extMatch[1].toUpperCase() : 'DOC';

        document.body.classList.add('clbi-system-doc-page');

        row = document.getElementById('clbi-system-doc-indicator-row');

        if (!row) {
            row = document.createElement('div');
            row.id = 'clbi-system-doc-indicator-row';
            row.className = 'clbi-system-doc-indicator-row';

            box = document.createElement('div');
            box.className = 'clbi-system-doc-indicator';

            meta = document.createElement('div');
            meta.className = 'clbi-system-doc-meta';

            label = document.createElement('span');
            label.className = 'clbi-system-doc-label';
            label.textContent = 'SYSTEM DOCUMENT';

            type = document.createElement('span');
            type.className = 'clbi-system-doc-type';

            title = document.createElement('div');
            title.className = 'clbi-system-doc-title';

            meta.appendChild(label);
            meta.appendChild(type);
            box.appendChild(meta);
            box.appendChild(title);
            row.appendChild(box);

            anchor = getSystemDocOutputForShell();
            main = document.querySelector('.liberty-content-main');

            if (anchor && anchor.parentNode) {
                anchor.parentNode.insertBefore(row, anchor);
            } else if (main) {
                main.insertBefore(row, main.firstChild);
            }
        }

        type = row.querySelector('.clbi-system-doc-type');
        title = row.querySelector('.clbi-system-doc-title');

        if (type) type.textContent = ext;
        if (title) title.textContent = pageName;

        renderSystemDocSourceViewerForShell();
    }

    var PAGE_TITLE_TARGET_SELECTORS = [
        '.liberty-content-header',
        '.liberty-content-header .title',
        '.liberty-content-header .title h1',
        '.liberty-content-header h1',
        '#firstHeading',
        '.firstHeading',
        '.mw-first-heading',
        '.page-heading',
        '.page-header',
        '.mw-page-title-main',
        '.mw-page-title-namespace',
        '.mw-page-title-separator'
    ];

    var pageShellObserverStarted = false;
    var pageShellObserverTimer = null;

    function setPageTitleDomHidden(hidden) {
        var nodes = document.querySelectorAll(PAGE_TITLE_TARGET_SELECTORS.join(','));

        nodes.forEach(function (node) {
            if (!node || !node.style) return;

            if (hidden) {
                node.setAttribute('data-clbi-title-hidden', 'true');
                node.style.setProperty('display', 'none', 'important');
            } else if (node.getAttribute('data-clbi-title-hidden') === 'true') {
                node.removeAttribute('data-clbi-title-hidden');
                node.style.removeProperty('display');
            }
        });
    }

    function applyPageShellClasses() {
        var body = document.body;
        var isSystemPage;

        if (!body) return;

        isSystemPage = isBackendOrSystemPageForShell();

        body.classList.remove('page-title-hidden', 'page-title-visible', 'backend-system-page', 'anecdote-namespace-page');

        if (!isMediaWikiSystemAssetPageForShell()) {
            body.classList.remove('clbi-system-doc-page');
            removeSystemDocIndicatorForShell();
        }

        if (isAnecdoteNamespaceForShell()) {
            body.classList.add('anecdote-namespace-page');
        }

        if (isMediaWikiSystemAssetPageForShell()) {
            body.classList.add('page-title-hidden', 'backend-system-page', 'clbi-system-doc-page');
            setPageTitleDomHidden(true);
            renderSystemDocIndicatorForShell();
        } else if (isSystemPage) {
            body.classList.add('page-title-visible', 'backend-system-page');
            setPageTitleDomHidden(false);
        } else {
            body.classList.add('page-title-hidden');
            setPageTitleDomHidden(true);
        }
    }

    function applyPageShellClassesDeferred() {
        applyPageShellClasses();
        window.setTimeout(applyPageShellClasses, 0);
        window.setTimeout(applyPageShellClasses, 80);
        window.setTimeout(applyPageShellClasses, 250);
    }

    function startPageShellObserver() {
        var observer;

        if (pageShellObserverStarted || !window.MutationObserver || !document.body) return;

        pageShellObserverStarted = true;
        observer = new MutationObserver(function (mutations) {
            var i;
            var target;

            /*
            시스템 CSS/JS 문서는 applyPageShellClasses()가 초기에 한 번
            인디케이터와 source viewer를 만든 뒤에는 MutationObserver가 다시
            같은 렌더링을 반복할 필요가 없다. 이 반복이 DevTools에서 body/요소가
            계속 플래시되는 직접 원인이다.
            SPA 전환 뒤의 처리는 loadPage()와 wikipage.content hook에서 따로 호출된다.
            */
            if (isMediaWikiSystemAssetPageForShell()) {
                for (i = 0; i < mutations.length; i += 1) {
                    target = mutations[i] && mutations[i].target;

                    if (
                        target &&
                        target.nodeType === 1 &&
                        (
                            target.id === 'clbi-system-source-viewer' ||
                            target.id === 'clbi-system-doc-indicator-row' ||
                            (target.closest && target.closest('#clbi-system-source-viewer, #clbi-system-doc-indicator-row'))
                        )
                    ) {
                        return;
                    }
                }

                if (
                    document.getElementById('clbi-system-doc-indicator-row') &&
                    document.getElementById('clbi-system-source-viewer')
                ) {
                    return;
                }
            }

            if (pageShellObserverTimer) return;

            pageShellObserverTimer = window.setTimeout(function () {
                pageShellObserverTimer = null;
                applyPageShellClasses();
            }, 50);
        });

        /*
        SPA 본문 교체는 wikipage.content 훅이 담당한다.
        body 전체 subtree를 감시하면 대문 SVG·장식·DevTools 내부 변경까지
        페이지 셸 재판정으로 증폭되므로 body 직계 자식 변화만 감시한다.
        */
        observer.observe(document.body, {
            childList: true,
            subtree: false
        });
    }

    if (document.readyState === 'loading') {
        document.addEventListener('DOMContentLoaded', function () {
            applyPageShellClassesDeferred();
            startPageShellObserver();
        });
    } else {
        applyPageShellClassesDeferred();
        startPageShellObserver();
    }

    if (mw.hook) {
        mw.hook('wikipage.content').add(applyPageShellClassesDeferred);
    }

    window.CLBI_PAGE_SHELL = {
        refresh: applyPageShellClasses,
        isBackendOrSystemPage: isBackendOrSystemPageForShell,
        isSystemAssetPage: isMediaWikiSystemAssetPageForShell,
        renderSystemDocIndicator: renderSystemDocIndicatorForShell,
        removeSystemDocIndicator: removeSystemDocIndicatorForShell,
        refreshSystemDocSourceViewer: renderSystemDocSourceViewerForShell
    };
}());

function loadLangScript(done) {
    $.getScript('/index.php?title=미디어위키:Lang.js&action=raw&ctype=text/javascript')
        .done(function() {
            if (typeof done === 'function') done();
        })
        .fail(function(a, b, c) {
            console.error('Lang.js load failed:', b, c);
            if (typeof done === 'function') done();
        });
}


/*
DevTools 같은 내부 스크롤은 셸 레이아웃을 바꾸지 않는다. 이 짧은 상호작용 동안
전체 화면 WebGL/CRT가 새 GPU 프레임을 계속 제출하면, transform 셸과 고정 네비의
합성 타일 갱신이 서로 경합한다. 마지막 정상 프레임은 그대로 유지하고 시각 시계도
정지시켜, 스크롤 종료 후 끊김 없이 이어지게 한다.
*/
var CLBI_COMPOSITOR_BUSY_UNTIL = 0;

function markClbiCompositorBusy(duration) {
    var now = window.performance && performance.now ? performance.now() : Date.now();
    CLBI_COMPOSITOR_BUSY_UNTIL = Math.max(CLBI_COMPOSITOR_BUSY_UNTIL, now + Math.max(80, Number(duration) || 140));
}

function isClbiCompositorBusy() {
    var now = window.performance && performance.now ? performance.now() : Date.now();
    return now < CLBI_COMPOSITOR_BUSY_UNTIL;
}

window.markClbiCompositorBusy = markClbiCompositorBusy;
window.isClbiCompositorBusy = isClbiCompositorBusy;

if (!window.CLBI_COMPOSITOR_ACTIVITY_BOUND) {
    window.CLBI_COMPOSITOR_ACTIVITY_BOUND = true;

    document.addEventListener('scroll', function (event) {
        var target = event.target && event.target.nodeType === 1 ? event.target : null;
        if (target && target.closest && target.closest('#dev-tools-panel')) {
            markClbiCompositorBusy(160);
        }
    }, true);

    document.addEventListener('wheel', function (event) {
        var target = event.target && event.target.nodeType === 1 ? event.target : null;
        if (target && target.closest && target.closest('#dev-tools-panel')) {
            markClbiCompositorBusy(160);
        }
    }, { capture:true, passive:true });
}

function initHalftoneBackground() {
    try {
        initWebGLHalftoneBackground();
    } catch (err) {
        console.error('WebGL halftone background failed:', err);
    }
}

function initWebGLHalftoneBackground() {
    var canvasId = 'site-halftone-bg';
    var existing = document.getElementById(canvasId);
    var canvas = existing || document.createElement('canvas');
    var halftoneState = window.SiteHalftoneBackgroundState || (window.SiteHalftoneBackgroundState = { runId: 0 });
    var runId = halftoneState.runId + 1;

    halftoneState.runId = runId;

    if (!existing) {
        canvas.id = canvasId;
        canvas.setAttribute('aria-hidden', 'true');
        document.body.insertBefore(canvas, document.body.firstChild || null);
    }

    canvas.style.position = 'fixed';
    canvas.style.inset = '0';
    canvas.style.width = '100vw';
    canvas.style.height = '100vh';
    canvas.style.pointerEvents = 'none';
    canvas.style.background = '#000000';
    canvas.style.display = 'block';

    if (!canvas.getAttribute('data-halftone-context-watch')) {
        canvas.setAttribute('data-halftone-context-watch', '1');
        canvas.addEventListener('webglcontextlost', function (event) {
            event.preventDefault();
            canvas.style.display = 'none';
            if (window.SiteHalftoneBackgroundState) {
                window.SiteHalftoneBackgroundState.contextLost = true;
                window.SiteHalftoneBackgroundState.runId += 1;
            }
        }, false);
        canvas.addEventListener('webglcontextrestored', function () {
            if (window.SiteHalftoneBackgroundState) {
                window.SiteHalftoneBackgroundState.contextLost = false;
            }
            window.setTimeout(initWebGLHalftoneBackground, 0);
        }, false);
    }

    var gl = canvas.getContext('webgl', {
        alpha: false,
        antialias: false,
        depth: false,
        stencil: false,
        preserveDrawingBuffer: false,
        powerPreference: 'low-power'
    }) || canvas.getContext('experimental-webgl');

    if (!gl) {
        canvas.style.display = 'none';
        console.warn('WebGL background unavailable.');
        return;
    }

    var vertexSrc = [
        'attribute vec2 a_position;',
        'void main() {',
        '  gl_Position = vec4(a_position, 0.0, 1.0);',
        '}'
    ].join('\n');

    var fragmentSrc = [
        'precision mediump float;',
        'uniform vec2 u_resolution;',
        'uniform float u_time;',
        'const float TAU = 6.28318530718;',
        'float gaussian(float v, float r) {',
        '  return exp(-((v * v) / max(0.0001, r * r)));',
        '}',
        'float hash(vec2 p) {',
        '  return fract(sin(dot(p, vec2(127.1, 311.7))) * 43758.5453123);',
        '}',
        'float bucketAlpha(float a) {',
        '  float i = floor(a * 9.0);',
        '  if (i < 1.0) return 0.040;',
        '  if (i < 2.0) return 0.080;',
        '  if (i < 3.0) return 0.135;',
        '  if (i < 4.0) return 0.210;',
        '  if (i < 5.0) return 0.310;',
        '  if (i < 6.0) return 0.430;',
        '  if (i < 7.0) return 0.580;',
        '  if (i < 8.0) return 0.760;',
        '  return 0.920;',
        '}',
        'void main() {',
        '  vec2 frag = gl_FragCoord.xy;',
        '  float spacing = 5.0;',
        '  float dotSize = 1.08;',
        '  vec2 grid = floor(frag / spacing);',
        '  vec2 inCell = mod(frag, spacing);',
        '  vec2 dotOrigin = vec2(1.0, 1.0);',
        '  vec2 dotCenter = dotOrigin + vec2(dotSize * 0.5);',
        '  vec2 local = abs(inCell - dotCenter);',
        '  float noise = hash(grid);',
        '  float size = dotSize + noise * 0.18;',
        '  float dotMask = 1.0 - smoothstep(size * 0.5, size * 0.5 + 0.22, max(local.x, local.y));',
        '  vec2 uv = frag / u_resolution;',
        '  float centerLine = 0.50 +',
        '    sin((uv.y * 1.32 + 0.08) * TAU) * 0.070 +',
        '    sin((uv.y * 3.18 + 0.34) * TAU) * 0.030;',
        '  float u = uv.x - centerLine;',
        '  float absU = abs(u);',
        '  float sideLift = smoothstep(0.065, 0.44, absU);',
        '  float valley = gaussian(u, 0.150);',
        '  float t = u_time;',
        '  float leftRibbonCenter = -0.28 + sin((uv.y * 3.20 + 0.12) * TAU) * 0.050;',
        '  float rightRibbonCenter = 0.27 + sin((uv.y * 2.85 + 0.56) * TAU) * 0.055;',
        '  float leftRibbon = gaussian(u - leftRibbonCenter, 0.105);',
        '  float rightRibbon = gaussian(u - rightRibbonCenter, 0.110);',
        '  float foldedU = u +',
        '    sin((uv.y * 4.40 + 0.22) * TAU) * 0.050 * (0.3 + sideLift) +',
        '    sin((uv.y * 7.20 + uv.x * 1.10) * TAU) * 0.022;',
        '  float verticalFold = pow(0.5 + 0.5 * cos(((foldedU * 3.05) + (sin(uv.y * TAU * 2.35) * 0.18)) * TAU), 2.5);',
        '  float diagonalFold = pow(0.5 + 0.5 * cos(((foldedU * 1.80) - (uv.y * 1.12) + 0.18) * TAU), 2.1);',
        '  float waist = gaussian(uv.y - 0.50, 0.25) * gaussian(absU - 0.20, 0.19);',
        '  float grain = (noise - 0.5) * 0.050;',
        '  float staticField =',
        '    0.055 +',
        '    sideLift * 0.210 +',
        '    (leftRibbon + rightRibbon) * 0.145 +',
        '    verticalFold * (0.055 + sideLift * 0.115) +',
        '    diagonalFold * 0.045 +',
        '    waist * 0.060 -',
        '    valley * 0.150 +',
        '    grain;',
        '  float alpha = staticField;',
        '  alpha += 0.115 * (leftRibbon + rightRibbon) * sin(t * 0.00030 + ((uv.y * 1.9) + sideLift * 0.4) * TAU);',
        '  alpha += 0.095 * verticalFold * (0.4 + sideLift) * sin(t * 0.00041 + ((uv.y * 2.7) + foldedU * 0.65) * TAU);',
        '  alpha += 0.070 * waist * sin(t * 0.00053 + ((uv.y * 3.1) - absU * 0.8) * TAU);',
        '  alpha += 0.060 * (1.0 - valley) * diagonalFold * sin(t * 0.00067 + ((uv.y * 1.4) + uv.x * 0.6) * TAU);',
        '  alpha += 0.038 * (0.35 + sideLift) * (0.35 + noise) * sin(t * 0.00079 + ((uv.y * 4.6) + noise * 0.8) * TAU);',
        '  alpha = bucketAlpha(clamp(alpha, 0.025, 0.96));',
        '  float value = alpha * dotMask;',
        '  gl_FragColor = vec4(vec3(0.8862745 * value), 1.0);',
        '}'
    ].join('\n');

    function compileShader(type, source) {
        var shader = gl.createShader(type);
        gl.shaderSource(shader, source);
        gl.compileShader(shader);

        if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) {
            console.error('WebGL shader compile error:', gl.getShaderInfoLog(shader));
            gl.deleteShader(shader);
            return null;
        }

        return shader;
    }

    var vertexShader = compileShader(gl.VERTEX_SHADER, vertexSrc);
    var fragmentShader = compileShader(gl.FRAGMENT_SHADER, fragmentSrc);

    if (!vertexShader || !fragmentShader) return;

    var program = gl.createProgram();
    gl.attachShader(program, vertexShader);
    gl.attachShader(program, fragmentShader);
    gl.linkProgram(program);

    if (!gl.getProgramParameter(program, gl.LINK_STATUS)) {
        console.error('WebGL program link error:', gl.getProgramInfoLog(program));
        return;
    }

    var positionLoc = gl.getAttribLocation(program, 'a_position');
    var resolutionLoc = gl.getUniformLocation(program, 'u_resolution');
    var timeLoc = gl.getUniformLocation(program, 'u_time');

    var buffer = gl.createBuffer();
    gl.bindBuffer(gl.ARRAY_BUFFER, buffer);
    gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([
        -1, -1,
         1, -1,
        -1,  1,
        -1,  1,
         1, -1,
         1,  1
    ]), gl.STATIC_DRAW);

    gl.useProgram(program);
    gl.bindBuffer(gl.ARRAY_BUFFER, buffer);
    gl.enableVertexAttribArray(positionLoc);
    gl.vertexAttribPointer(positionLoc, 2, gl.FLOAT, false, 0, 0);

    function resize() {
        var dpr = Math.min(window.devicePixelRatio || 1, 1.5);
        var cssW = Math.max(1, window.innerWidth || document.documentElement.clientWidth || 1);
        var cssH = Math.max(1, window.innerHeight || document.documentElement.clientHeight || 1);
        var w = Math.max(1, Math.floor(cssW * dpr));
        var h = Math.max(1, Math.floor(cssH * dpr));

        if (canvas.width !== w || canvas.height !== h) {
            canvas.width = w;
            canvas.height = h;
            canvas.style.width = cssW + 'px';
            canvas.style.height = cssH + 'px';
            gl.viewport(0, 0, w, h);
        }
    }

    var prefersReducedMotion = false;
    try {
        prefersReducedMotion = window.matchMedia && window.matchMedia('(prefers-reduced-motion: reduce)').matches;
    } catch (err) {}

    function getNationsGlobeHalftoneNode() {
        return document.querySelector('.clbi-nations-globe-window[data-nations-globe], [data-nations-globe="real-world"]');
    }

    function getNationsGlobeHalftoneState() {
        var globe = getNationsGlobeHalftoneNode();
        var instance;

        if (!globe) {
            return 'none';
        }

        if (globe.classList && globe.classList.contains('is-dragging')) {
            return 'active';
        }

        instance = globe.CLBI_NationsGlobeInstance || null;
        if (instance && typeof instance.isHalftoneBackgroundBusy === 'function' && instance.isHalftoneBackgroundBusy()) {
            return 'busy';
        }

        if (globe.getAttribute && globe.getAttribute('data-clbi-globe-bg-busy') === '1') {
            return 'busy';
        }

        return 'present';
    }

    function shouldSkipHalftoneDrawForGlobe() {
        var globe = getNationsGlobeHalftoneNode();
        var instance;

        if (!globe) return false;

        instance = globe.CLBI_NationsGlobeInstance || null;

        /*
         * Background pause/resume — 20260710.
         *
         * While the nations globe is being held, keep the last rendered halftone
         * frame on screen and pause the halftone clock.  The important detail is
         * that this is not a wall-clock catch-up: skipped seconds are not replayed
         * or fast-forwarded when the pointer is released.
         */
        if (globe.classList && globe.classList.contains('is-dragging')) return true;
        if (instance && instance.dragging) return true;

        if (instance && typeof instance.isHalftoneBackgroundHardBusy === 'function') {
            return instance.isHalftoneBackgroundHardBusy();
        }

        if (globe.getAttribute && globe.getAttribute('data-clbi-globe-bg-busy') === '1') return true;

        return false;
    }

    function getFrameInterval() {
        if (prefersReducedMotion) {
            return 1000;
        }

        /*
         * Nations background parity — 20260710.
         *
         * The nations page used to permanently lower the halftone cadence while
         * the globe existed.  Profiling showed that the steady halftone draw is
         * normally cheap; the real contention happens during globe drag, first
         * WebGL texture upload, and long layout/render tasks.  Keep the same
         * full-quality cadence as ordinary pages, and skip only the frames that
         * would directly collide with an active/busy globe moment.
         */
        return 66;
    }

    var lastFrame = 0;
    var startTime = performance.now();
    var visualTime = 0;
    var lastVisualNow = 0;

    function draw(now) {
        var state = getNationsGlobeHalftoneState();
        var interval = getFrameInterval();
        var perf = window.InteractionPerf;
        var delta;

        if (!lastVisualNow) {
            lastVisualNow = now;
        }
        delta = Math.max(0, Math.min(120, now - lastVisualNow));
        lastVisualNow = now;
        visualTime += delta;

        function runDraw() {
            resize();

            gl.clearColor(0, 0, 0, 1);
            gl.clear(gl.COLOR_BUFFER_BIT);
            gl.uniform2f(resolutionLoc, canvas.width, canvas.height);
            gl.uniform1f(timeLoc, visualTime);
            gl.drawArrays(gl.TRIANGLES, 0, 6);
        }

        if (perf && typeof perf.measureSync === 'function') {
            return perf.measureSync('background halftone draw', { globeState: state, interval: interval }, runDraw);
        }
        return runDraw();
    }

    var frameTimer = 0;
    var frameRaf = 0;

    function scheduleNextFrame(delay) {
        window.clearTimeout(frameTimer);
        frameTimer = window.setTimeout(function () {
            frameTimer = 0;
            if (halftoneState.runId !== runId || halftoneState.contextLost) return;
            if (window.requestAnimationFrame) {
                frameRaf = window.requestAnimationFrame(render);
            } else {
                render(performance.now());
            }
        }, Math.max(16, Number(delay) || getFrameInterval()));
    }

    function render(now) {
        var interval = getFrameInterval();

        frameRaf = 0;
        if (halftoneState.runId !== runId || halftoneState.contextLost) return;

        if (document.hidden) {
            lastVisualNow = now;
            scheduleNextFrame(250);
            return;
        }

        if (isClbiCompositorBusy()) {
            lastFrame = now;
            lastVisualNow = now;
            scheduleNextFrame(interval);
            return;
        }

        if (shouldSkipHalftoneDrawForGlobe()) {
            lastFrame = now;
            halftoneState.lastGlobeBusySkipAt = now;
            lastVisualNow = now;
            scheduleNextFrame(interval);
            return;
        }

        lastFrame = now;
        draw(now);
        scheduleNextFrame(interval);
    }

    draw(performance.now());
    scheduleNextFrame(getFrameInterval());

    document.addEventListener('visibilitychange', function () {
        if (!document.hidden && halftoneState.runId === runId && !halftoneState.contextLost) {
            window.clearTimeout(frameTimer);
            if (frameRaf && window.cancelAnimationFrame) window.cancelAnimationFrame(frameRaf);
            frameRaf = 0;
            scheduleNextFrame(16);
        }
    });

}

var CLBI_SVG_BELL = '<svg class="profile-svg profile-svg-bell" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.75" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M10.268 21a2 2 0 0 0 3.464 0"/><path d="M3.262 15.326A1 1 0 0 0 4 17h16a1 1 0 0 0 .74-1.673C19.41 13.956 18 12.499 18 8A6 6 0 0 0 6 8c0 4.499-1.411 5.956-2.738 7.326"/></svg>';
var CLBI_SVG_BELL_DOT = '<svg class="profile-svg profile-svg-bell-dot" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.75" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M10.268 21a2 2 0 0 0 3.464 0"/><path d="M11.68 2.009A6 6 0 0 0 6 8c0 4.499-1.411 5.956-2.738 7.326A1 1 0 0 0 4 17h16a1 1 0 0 0 .74-1.673c-.824-.85-1.678-1.731-2.21-3.348"/><circle cx="18" cy="5" r="3"/></svg>';
var CLBI_SVG_LIST = '<svg class="profile-svg profile-svg-list" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.75" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M3 5h.01"/><path d="M3 12h.01"/><path d="M3 19h.01"/><path d="M8 5h13"/><path d="M8 12h13"/><path d="M8 19h13"/></svg>';
var CLBI_SVG_LANGUAGES = '<svg class="profile-svg profile-svg-languages" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.75" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="m5 8 6 6"/><path d="m4 14 6-6 2-3"/><path d="M2 5h12"/><path d="M7 2h1"/><path d="m22 22-5-10-5 10"/><path d="M14 18h6"/></svg>';
var CLBI_SVG_POWER = '<svg class="profile-svg profile-svg-power" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.75" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M12 2v10"/><path d="M18.4 6.6a9 9 0 1 1-12.77.04"/></svg>';
var CLBI_SVG_SETTINGS = '<svg class="profile-svg profile-svg-settings" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.75" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M9.671 4.136a2.34 2.34 0 0 1 4.659 0 2.34 2.34 0 0 0 3.319 1.915 2.34 2.34 0 0 1 2.33 4.033 2.34 2.34 0 0 0 0 3.831 2.34 2.34 0 0 1-2.33 4.033 2.34 2.34 0 0 0-3.319 1.915 2.34 2.34 0 0 1-4.659 0 2.34 2.34 0 0 0-3.32-1.915 2.34 2.34 0 0 1-2.33-4.033 2.34 2.34 0 0 0 0-3.831A2.34 2.34 0 0 1 6.35 6.051a2.34 2.34 0 0 0 3.319-1.915"/><circle cx="12" cy="12" r="3"/></svg>';
var CLBI_SVG_SCAN_TEXT = '<svg class="profile-svg profile-svg-scan-text" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.75" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M3 7V5a2 2 0 0 1 2-2h2"/><path d="M17 3h2a2 2 0 0 1 2 2v2"/><path d="M21 17v2a2 2 0 0 1-2 2h-2"/><path d="M7 21H5a2 2 0 0 1-2-2v-2"/><path d="M7 8h8"/><path d="M7 12h10"/><path d="M7 16h6"/></svg>';
var CLBI_SVG_SCAN_EYE = '<svg class="profile-svg profile-svg-scan-eye" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.75" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M3 7V5a2 2 0 0 1 2-2h2"/><path d="M17 3h2a2 2 0 0 1 2 2v2"/><path d="M21 17v2a2 2 0 0 1-2 2h-2"/><path d="M7 21H5a2 2 0 0 1-2-2v-2"/><circle cx="12" cy="12" r="1"/><path d="M18.944 12.33a1 1 0 0 0 0-.66 7.5 7.5 0 0 0-13.888 0 1 1 0 0 0 0 .66 7.5 7.5 0 0 0 13.888 0"/></svg>';
var CLBI_SVG_NEWSPAPER = '<svg class="profile-svg profile-svg-newspaper" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.75" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M15 18h-5"/><path d="M18 14h-8"/><path d="M4 22h16a2 2 0 0 0 2-2V4a2 2 0 0 0-2-2H8a2 2 0 0 0-2 2v16a2 2 0 0 1-4 0v-9a2 2 0 0 1 2-2h2"/><rect width="8" height="4" x="10" y="6" rx="1"/></svg>';
var CLBI_SVG_GREAT_WALL = '<svg class="profile-svg profile-svg-great-wall lucide lucide-paint-roller" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect width="16" height="6" x="2" y="2" rx="2"/><path d="M10 16v-2a2 2 0 0 1 2-2h8a2 2 0 0 0 2-2V7a2 2 0 0 0-2-2h-2"/><rect width="4" height="6" x="8" y="16" rx="1"/></svg>';
var CLBI_SVG_TROPHY = '<svg class="profile-svg profile-svg-trophy" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.75" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M10 14.66v1.626a2 2 0 0 1-.976 1.696A5 5 0 0 0 7 21.978"/><path d="M14 14.66v1.626a2 2 0 0 0 .976 1.696A5 5 0 0 1 17 21.978"/><path d="M18 9h1.5a1 1 0 0 0 0-5H18"/><path d="M4 22h16"/><path d="M6 9a6 6 0 0 0 12 0V3a1 1 0 0 0-1-1H7a1 1 0 0 0-1 1z"/><path d="M6 9H4.5a1 1 0 0 1 0-5H6"/></svg>';
var CLBI_SVG_PACKAGE = '<svg class="profile-svg profile-svg-package" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.75" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M12 3v6"/><path d="M16.76 3a2 2 0 0 1 1.8 1.1l2.23 4.479a2 2 0 0 1 .21.891V19a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V9.472a2 2 0 0 1 .211-.894L5.45 4.1A2 2 0 0 1 7.24 3z"/><path d="M3.054 9.013h17.893"/></svg>';

var PROFILE_RENDER_TOKEN = 0;

function invalidateProfileRender() {
    PROFILE_RENDER_TOKEN++;
}

$(function() {
    initHalftoneBackground();

// ── 하단 Plank 단축키 가이드 ──
function escapeClbiBottomGuideHtml(value) {
    return String(value == null ? '' : value)
        .replace(/&/g, '&amp;')
        .replace(/</g, '&lt;')
        .replace(/>/g, '&gt;')
        .replace(/"/g, '&quot;')
        .replace(/'/g, '&#039;');
}

function readClbiBottomGuidePageName() {
    var pageName = window.mw && mw.config ? (mw.config.get('wgPageName') || '') : '';

    if (!pageName) {
        pageName = window.location.pathname || '';
    }

    return String(pageName)
        .split('?')[0]
        .replace(/^\/index\.php\//, '')
        .replace(/_/g, ' ')
        .trim();
}

function isClbiNationsShortcutContext() {
    var pageName = readClbiBottomGuidePageName();

    return pageName === '시대' ||
        pageName === 'Era' ||
        !!document.querySelector('.clbi-nations-panel-stack, .clbi-nations-globe-window, .clbi-nations-tabpanel');
}

function isClbiWikiEditShortcutContext() {
    var action = window.mw && mw.config ? String(mw.config.get('wgAction') || '') : '';
    var search = String(window.location.search || '');

    return action === 'edit' ||
        action === 'submit' ||
        /(?:^|[?&])action=(?:edit|submit)(?:&|$)/.test(search) ||
        !!document.querySelector('#editform, #wpSave, input[name="wpSave"], button[name="wpSave"]');
}


function isClbiNationListManagerShortcutContext() {
    return !!document.querySelector('.nation-list-manager');
}

function getClbiBottomShortcutItems() {
    if (isClbiWikiEditShortcutContext()) {
        return [
            { key: 'Ctrl+S', label: '변경사항 저장' }
        ];
    }


    if (isClbiNationListManagerShortcutContext()) {
        return [
            { key: 'Ctrl+S', label: '국가 목록 저장' }
        ];
    }

    if (isClbiNationsShortcutContext()) {
        return [
            { key: 'Q', label: '이전 시대' },
            { key: 'E', label: '다음 시대' },
            { key: 'Shift+Q', label: '이전 연도' },
            { key: 'Shift+E', label: '다음 연도' },
            { key: 'Ctrl+Q', label: '이전 대륙' },
            { key: 'Ctrl+E', label: '다음 대륙' }
        ];
    }

    return [];
}

function renderClbiBottomShortcutGuide() {
    var guide = document.getElementById('clbi-bottom-shortcut-guide');
    var items;
    var html;

    if (!guide) return;

    items = getClbiBottomShortcutItems();

    if (!items.length) {
        guide.classList.add('is-empty');
        guide.innerHTML = '';
        return;
    }

    guide.classList.remove('is-empty');

    html = '<div class="clbi-bottom-shortcut-list">';

    items.forEach(function (item) {
        html += '<div class="clbi-bottom-shortcut-item">' +
            '<span class="clbi-bottom-shortcut-key">' + escapeClbiBottomGuideHtml(item.key) + '</span>' +
            '<span class="clbi-bottom-shortcut-label">' + escapeClbiBottomGuideHtml(item.label) + '</span>' +
        '</div>';
    });

    html += '</div>';
    guide.innerHTML = html;
}

function buildClbiBottomPlankHtml(wrapId, navId, mainId) {
    return '' +
        '<div id="' + wrapId + '">' +
            '<div id="' + navId + '">' +
                '<div id="' + mainId + '">' +
                    '<div id="clbi-bottom-shortcut-guide" class="is-empty" aria-label="단축키 안내"></div>' +
                '</div>' +
            '</div>' +
        '</div>';
}

var CLBI_NATIONS_LAST_POINTER_X = null;
var CLBI_NATIONS_LAST_POINTER_Y = null;

function getClbiNationsTabAtPointer(tabpanel) {
    var element;
    var tab;

    if (!tabpanel) return null;
    if (CLBI_NATIONS_LAST_POINTER_X === null || CLBI_NATIONS_LAST_POINTER_Y === null) return null;
    if (typeof document.elementFromPoint !== 'function') return null;

    element = document.elementFromPoint(CLBI_NATIONS_LAST_POINTER_X, CLBI_NATIONS_LAST_POINTER_Y);
    if (!element) return null;

    tab = element.closest ? element.closest('.clbi-nations-tabpanel-tab[data-continent]') : null;

    if (!tab || !tabpanel.contains(tab)) return null;

    return tab;
}

function validateClbiNationsPointerHover(tabpanel, forceSuppress) {
    var tab = getClbiNationsTabAtPointer(tabpanel);
    var suppressContinent;
    var tabContinent;
    var tabIsActive;

    if (!tabpanel) return;

    if (!tab) {
        clearClbiNationsKeyboardHoverSuppressed(tabpanel);
        return;
    }

    tabContinent = tab.getAttribute('data-continent') || '';
    tabIsActive = tab.classList.contains('is-active') || tab.getAttribute('aria-selected') === 'true';

    if (tabIsActive) {
        clearClbiNationsKeyboardHoverSuppressed(tabpanel);
        return;
    }

    suppressContinent = tabpanel.getAttribute('data-clbi-hover-suppress-continent') || '';

    if (forceSuppress || !suppressContinent) {
        tabpanel.classList.add('is-keyboard-switching');
        tabpanel.setAttribute('data-clbi-hover-suppress-continent', tabContinent);
        return;
    }

    if (suppressContinent === tabContinent) {
        tabpanel.classList.add('is-keyboard-switching');
        return;
    }

    /*
     * The pointer actually moved onto another tab after the keyboard switch.
     * At that point this is no longer stale browser :hover; let normal hover work.
     */
    clearClbiNationsKeyboardHoverSuppressed(tabpanel);
}

function validateAllClbiNationsPointerHovers() {
    var panels = document.querySelectorAll('.clbi-nations-tabpanel.is-keyboard-switching');

    Array.prototype.forEach.call(panels, function (tabpanel) {
        if (isClbiNationsPanelOwnedTabpanel(tabpanel)) return;
        validateClbiNationsPointerHover(tabpanel, false);
    });
}

function isClbiNationsPanelOwnedTabpanel(tabpanel) {
    /*
     * Mouse continent tab regression guard.
     * -------------------------------------
     * Detached SPA preparation serializes attributes/classes but it cannot
     * serialize DOM event listeners or JS properties.  The NationsPanel fix
     * therefore uses the DOM property CLBI_NationsPanelOwned /
     * CLBI_NationsTabPanelBound as the real ownership marker.
     *
     * Do NOT treat data-nations-tabpanel-ready or clbi-nations-continent-cache
     * as ownership here.  Those can survive innerHTML insertion while the real
     * click listeners were lost, causing Common.js to delegate to a panel that
     * NationsPanel has not rebound yet.  The fallback below must remain able to
     * handle mouse clicks until NationsPanel reclaims the live DOM node.
     */
    return !!(tabpanel && tabpanel.CLBI_NationsPanelOwned);
}

function isClbiNationsLiveContinentActive(tabpanel, continent) {
    var tab;
    var panel;

    if (!tabpanel || !continent) return false;

    tab = Array.prototype.slice.call(tabpanel.querySelectorAll('.clbi-nations-tabpanel-tab[data-continent]')).find(function (candidate) {
        return candidate.getAttribute('data-continent') === continent;
    });

    panel = Array.prototype.slice.call(tabpanel.querySelectorAll('.clbi-nations-tabpanel-continent[data-continent-panel]')).find(function (candidate) {
        return candidate.getAttribute('data-continent-panel') === continent;
    });

    return !!(
        tab &&
        panel &&
        (tab.classList.contains('is-active') || tab.getAttribute('aria-selected') === 'true') &&
        panel.classList.contains('is-active') &&
        panel.getAttribute('aria-hidden') !== 'true' &&
        !panel.hasAttribute('hidden')
    );
}

function activateClbiNationsContinent(tabpanel, targetContinent, options) {
    var tabs;
    var panels;
    var skipOwner = !!(options && options.skipOwner);

    if (!tabpanel || !targetContinent) return false;

    if (!skipOwner && isClbiNationsPanelOwnedTabpanel(tabpanel)) {
        var owner = window.NationsPanel || window.CLBI_NATIONS_PANEL || null;

        if (owner && typeof owner.activateContinent === 'function') {
            try {
                if (owner.activateContinent(targetContinent, {
                    source: 'common-legacy-click',
                    panel: tabpanel
                })) {
                    return true;
                }
            } catch (err) {}
        }

        /*
         * Ownership markers can be stale during SPA/hydration edge cases.  If
         * the owner API is missing or refuses the live panel, do not drop the
         * mouse click.  Fall through to the local fallback so pointer users are
         * never left with keyboard-only continent tabs.
         */
    }

    tabs = Array.prototype.slice.call(tabpanel.querySelectorAll('.clbi-nations-tabpanel-tab[data-continent]'));
    panels = Array.prototype.slice.call(tabpanel.querySelectorAll('.clbi-nations-tabpanel-continent[data-continent-panel]'));

    if (!tabs.length || !panels.length) return false;

    tabs.forEach(function (tab) {
        var active = tab.getAttribute('data-continent') === targetContinent;
        tab.classList.toggle('is-active', active);
        tab.setAttribute('aria-selected', active ? 'true' : 'false');
        tab.setAttribute('tabindex', active ? '0' : '-1');
    });

    panels.forEach(function (panel) {
        var active = panel.getAttribute('data-continent-panel') === targetContinent;
        panel.classList.toggle('is-active', active);

        if (active) {
            panel.removeAttribute('hidden');
        } else {
            panel.setAttribute('hidden', 'hidden');
        }
    });

    try {
        if (window.Decorations && typeof window.Decorations.sync === 'function') window.Decorations.sync();
        else if (window.CLBI_DECORATIONS && typeof window.CLBI_DECORATIONS.sync === 'function') window.CLBI_DECORATIONS.sync();
    } catch (err) {}

    return true;
}

function setClbiNationsKeyboardHoverSuppressed(tabpanel) {
    validateClbiNationsPointerHover(tabpanel, true);
}

function clearClbiNationsKeyboardHoverSuppressed(tabpanel) {
    if (!tabpanel) return;
    tabpanel.classList.remove('is-keyboard-switching');
    tabpanel.removeAttribute('data-clbi-hover-suppress-continent');
}

function moveClbiNationsContinent(direction) {
    var tabpanel = document.querySelector('.clbi-nations-tabpanel');
    var tabs;
    var activeIndex;
    var nextIndex;
    var target;

    if (!tabpanel) return false;

    if (isClbiNationsPanelOwnedTabpanel(tabpanel) &&
        window.CLBI_NATIONS_PANEL &&
        typeof window.CLBI_NATIONS_PANEL.moveContinent === 'function') {
        return !!window.CLBI_NATIONS_PANEL.moveContinent(direction, {
            source: 'common-legacy-keyboard',
            panel: tabpanel
        });
    }

    tabs = Array.prototype.slice.call(tabpanel.querySelectorAll('.clbi-nations-tabpanel-tab[data-continent]'));
    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;
    target = tabs[nextIndex].getAttribute('data-continent');

    if (activateClbiNationsContinent(tabpanel, target)) {
        setClbiNationsKeyboardHoverSuppressed(tabpanel);
        window.setTimeout(function () {
            validateClbiNationsPointerHover(tabpanel, false);
        }, 0);
        return true;
    }

    return false;
}

function initClbiNationsTabpanelControls(root) {
    var scope = root && root.querySelectorAll ? root : document;
    var panels = scope.querySelectorAll('.clbi-nations-tabpanel');

    Array.prototype.forEach.call(panels, function (tabpanel) {
        /*
         * Common.js is only a safety fallback for continent tabs; NationsPanel
         * is the primary owner.  This guard must be a DOM property, not a
         * serialized attribute.  SPA warm prepaint can preserve
         * data-clbi-nations-tabs-ready="1" while losing the actual event
         * listeners, which was the root cause of mouse clicks no longer moving
         * continents while keyboard shortcuts still worked.
         */
        if (tabpanel.CLBI_NationsCommonTabsBound) return;
        tabpanel.CLBI_NationsCommonTabsBound = true;
        tabpanel.setAttribute('data-clbi-nations-tabs-ready', '1');

        tabpanel.addEventListener('pointerdown', function () {
            clearClbiNationsKeyboardHoverSuppressed(tabpanel);
        });

        tabpanel.addEventListener('pointerleave', function () {
            clearClbiNationsKeyboardHoverSuppressed(tabpanel);
        });

        tabpanel.addEventListener('click', function (event) {
            var tab = event.target.closest ? event.target.closest('.clbi-nations-tabpanel-tab[data-continent]') : null;

            if (!tab || !tabpanel.contains(tab)) return;

            clearClbiNationsKeyboardHoverSuppressed(tabpanel);

            if (activateClbiNationsContinent(tabpanel, tab.getAttribute('data-continent'))) {
                event.preventDefault();
            }
        });

        tabpanel.addEventListener('keydown', function (event) {
            var tab = event.target.closest ? event.target.closest('.clbi-nations-tabpanel-tab[data-continent]') : null;
            var handled = false;

            if (!tab || !tabpanel.contains(tab)) return;

            if (event.key === 'ArrowLeft') handled = moveClbiNationsContinent(-1);
            else if (event.key === 'ArrowRight') handled = moveClbiNationsContinent(1);

            if (handled) {
                event.preventDefault();
                event.stopPropagation();
            }
        });
    });
}

function handleClbiNationsContinentDocumentClick(event) {
    var target;
    var tab;
    var tabpanel;
    var continent;
    var owner;
    var handled = false;

    if (!event || event.__clbiNationsContinentHandled) return;
    target = event.target;
    if (!target || !target.closest) return;

    tab = target.closest('.clbi-nations-tabpanel-tab[data-continent]');
    if (!tab) return;

    tabpanel = tab.closest ? tab.closest('.clbi-nations-tabpanel') : null;
    if (!tabpanel || !tabpanel.contains(tab)) return;

    continent = tab.getAttribute('data-continent') || '';
    if (!continent) return;

    /*
     * Final live-DOM mouse delegate.
     * --------------------------------
     * The continent tab UI has two initialization phases: detached preparation
     * for seamless SPA entry, then live DOM hydration.  Detached preparation can
     * leave serialized "ready" attributes behind while losing event listeners.
     * Keyboard shortcuts still work because they call the public NationsPanel
     * API directly, but mouse clicks depend on live listeners.  This capturing
     * delegate is intentionally independent of per-panel ready attributes: it
     * always resolves the clicked live tab and hands it to NationsPanel first,
     * then falls back to the local Common.js switcher if needed.
     */
    event.__clbiNationsContinentHandled = true;
    clearClbiNationsKeyboardHoverSuppressed(tabpanel);

    owner = window.NationsPanel || window.CLBI_NATIONS_PANEL || null;
    if (owner && typeof owner.activateContinent === 'function') {
        try {
            handled = !!owner.activateContinent(continent, {
                source: 'mouse-click-live-delegate',
                panel: tabpanel
            });
        } catch (err) {
            handled = false;
        }

        /*
         * Owner API stale-cache guard.
         *
         * A previous regression came from NationsPanel returning true after it
         * updated cached/detached tab nodes instead of the clicked live tab. A
         * true return value alone is therefore not enough for mouse input. Verify
         * the live DOM that received the click. If it did not become active,
         * bypass the owner and run the Common.js live query fallback directly.
         */
        if (handled && !isClbiNationsLiveContinentActive(tabpanel, continent)) {
            handled = false;
        }
    }

    if (!handled) {
        handled = activateClbiNationsContinent(tabpanel, continent, { skipOwner: true });
    }

    if (handled) {
        try { playStaticSound(); } catch (err2) {}
        event.preventDefault();
        event.stopPropagation();
        if (typeof event.stopImmediatePropagation === 'function') event.stopImmediatePropagation();
    }
}

function initClbiNationsGlobalClickDelegate() {
    if (document.body.getAttribute('data-clbi-nations-global-click-ready') === '1') return;
    document.body.setAttribute('data-clbi-nations-global-click-ready', '1');
    document.addEventListener('click', handleClbiNationsContinentDocumentClick, true);
}


function initClbiNationsPointerHoverValidation() {
    if (document.body.getAttribute('data-clbi-nations-pointer-hover-ready') === '1') return;

    document.body.setAttribute('data-clbi-nations-pointer-hover-ready', '1');

    document.addEventListener('pointermove', function (event) {
        CLBI_NATIONS_LAST_POINTER_X = event.clientX;
        CLBI_NATIONS_LAST_POINTER_Y = event.clientY;
        validateAllClbiNationsPointerHovers();
    }, true);

    document.addEventListener('pointerleave', function () {
        CLBI_NATIONS_LAST_POINTER_X = null;
        CLBI_NATIONS_LAST_POINTER_Y = null;
        validateAllClbiNationsPointerHovers();
    }, true);

    window.addEventListener('blur', function () {
        CLBI_NATIONS_LAST_POINTER_X = null;
        CLBI_NATIONS_LAST_POINTER_Y = null;
        validateAllClbiNationsPointerHovers();
    });
}


function initClbiBottomShortcutSystem(root) {
    renderClbiBottomShortcutGuide();
    initClbiNationsGlobalClickDelegate();
    initClbiNationsTabpanelControls(root || document);
    initClbiNationsPointerHoverValidation();
}

// ── 상·하단 네비게이션 바 ──
function buildClbiNavHtml(position) {
    var isBottom = position === 'bottom';
    var base = isBottom ? 'clbi-bottom' : 'clbi-top';
    var shortBase = isBottom ? 'clbi-bnav' : 'clbi-tnav';
    var wrapId = base + '-nav-wrap';
    var navId = base + '-nav';
    var mainId = base + '-nav-main';
    var tabsId = base + '-nav-tabs';
    var searchId = base + '-nav-search';
    var inputId = isBottom ? 'clbi-bottom-search-input' : 'clbi-top-search-input';
    var worldId = shortBase + '-worldbuilding';
    var subId = isBottom ? 'clbi-bottom-sub-worldbuilding' : 'clbi-sub-worldbuilding';
    var subInnerId = subId + '-inner';

    if (isBottom) {
        return buildClbiBottomPlankHtml(wrapId, navId, mainId);
    }

    return '' +
        '<div id="' + wrapId + '">' +
            '<div id="' + navId + '">' +
                '<div id="' + mainId + '">' +
                    '<div id="' + tabsId + '">' +
                        '<a class="clbi-top-nav-item" href="/index.php/대문">' +
                            '<img class="clbi-tnav-icon" src="/index.php?title=특수:Redirect/file/Ic-main-menu-001.png" alt="">' +
                            '<span class="clbi-tnav-label">메인 메뉴</span>' +
                        '</a>' +
                        '<a class="clbi-top-nav-item" href="/index.php/프로젝트:소개">' +
                            '<img class="clbi-tnav-icon" src="/index.php?title=특수:Redirect/file/Ic-project-001.png" alt="">' +
                            '<span class="clbi-tnav-label">프로젝트</span>' +
                        '</a>' +
                        '<div class="clbi-top-nav-item" id="' + worldId + '">' +
                            '<img class="clbi-tnav-icon" src="/index.php?title=특수:Redirect/file/Ic-worldbuilding-001.png" alt="">' +
                            '<span class="clbi-tnav-label">세계관</span>' +
                            '<span class="clbi-tnav-arrow">▾</span>' +
                        '</div>' +
                    '</div>' +
                    (isBottom ? '' : (
                        '<div id="' + searchId + '">' +
                            '<input type="text" id="' + inputId + '" placeholder="검색...">' +
                        '</div>'
                    )) +
                '</div>' +
                '<div id="' + subId + '">' +
                    '<div id="' + subInnerId + '">' +
                        '<div class="clbi-tnav-sub-list">' +
                            '<a class="clbi-tnav-sub-item" href="/index.php/시대">시대</a>' +
                            '<a class="clbi-tnav-sub-item" href="/index.php/설정">설정</a>' +
                            '<a class="clbi-tnav-sub-item" href="/index.php/기업_및_공동체">기업 및 공동체</a>' +
                            '<a class="clbi-tnav-sub-item" href="/index.php/군_정치집단">군, 정치집단</a>' +
                            '<a class="clbi-tnav-sub-item" href="/index.php/인물">인물</a>' +
                        '</div>' +
                    '</div>' +
                '</div>' +
            '</div>' +
        '</div>';
}

function normalizeClbiShellDomOrder() {
    var contentWrapper = document.querySelector('.content-wrapper');
    var topNav = document.getElementById('clbi-top-nav-wrap');
    var bottomNav = document.getElementById('clbi-bottom-nav-wrap');
    var canvas = document.getElementById('site-halftone-bg');
    var anchor;
    var viewportH;
    var topRect;
    var wrapperRect;
    var needsRecovery;
    var expectedWrapperTop;
    var host;

    if (!contentWrapper || !topNav || !bottomNav || !document.body) return;

    /*
    CLBI shell can live inside a Liberty <section>.  Some skins/layouts give that
    section a flow context that lets the top nav visually overlap the content
    wrapper even when DOM sibling order is correct.  Mark the common parent and
    let Layout.css force a simple vertical flow for the shell.
    */
    host = topNav.parentElement === contentWrapper.parentElement &&
        contentWrapper.parentElement === bottomNav.parentElement
        ? contentWrapper.parentElement
        : null;

    if (host) {
        host.classList.add('clbi-shell-host');
    }

    viewportH = window.innerHeight || document.documentElement.clientHeight || 0;
    topRect = topNav.getBoundingClientRect();
    wrapperRect = contentWrapper.getBoundingClientRect();
    expectedWrapperTop = topRect.bottom + 8;

    needsRecovery = false;

    if (viewportH > 0) {
        if (topRect.top >= viewportH * 0.55) needsRecovery = true;
        if (wrapperRect.top >= viewportH * 0.60) needsRecovery = true;
    }

    if (topRect.top > 240 || wrapperRect.top > 320) {
        needsRecovery = true;
    }

    if (wrapperRect.top < expectedWrapperTop - 1) {
        needsRecovery = true;
    }

    if (!needsRecovery) {
        document.body.classList.add('clbi-shell-ready');
        return;
    }

    anchor = canvas && canvas.parentNode === document.body
        ? canvas.nextSibling
        : document.body.firstChild;

    document.body.insertBefore(topNav, anchor);
    document.body.insertBefore(contentWrapper, topNav.nextSibling);
    document.body.insertBefore(bottomNav, contentWrapper.nextSibling);

    document.body.classList.add('clbi-shell-ready');
}

window.normalizeClbiShellDomOrder = normalizeClbiShellDomOrder;


function ensureClbiVerticalScaleHost() {
    var topNav = document.getElementById('clbi-top-nav-wrap');
    var contentWrapper = document.querySelector('.content-wrapper');
    var bottomNav = document.getElementById('clbi-bottom-nav-wrap');
    var host;
    var existing;
    var parent;

    if (!topNav || !contentWrapper || !bottomNav || !document.body) return null;

    existing = document.getElementById('clbi-shell-scale-host');

    if (existing && existing.contains(topNav) && existing.contains(contentWrapper) && existing.contains(bottomNav)) {
        existing.classList.add('clbi-shell-host');
        return existing;
    }

    parent = topNav.parentElement === contentWrapper.parentElement && contentWrapper.parentElement === bottomNav.parentElement
        ? topNav.parentElement
        : null;

    if (parent && parent !== document.body) {
        parent.classList.add('clbi-shell-host');
        parent.id = parent.id || 'clbi-shell-scale-host';
        return parent;
    }

    host = existing || document.createElement('div');
    host.id = 'clbi-shell-scale-host';
    host.className = 'clbi-shell-host';

    if (!host.parentNode) {
        document.body.insertBefore(host, topNav);
    }

    host.appendChild(topNav);
    host.appendChild(contentWrapper);
    host.appendChild(bottomNav);

    return host;
}

function readClbiRootPx(name, fallback) {
    var raw = '';
    var value;

    try {
        raw = getComputedStyle(document.documentElement).getPropertyValue(name);
    } catch (err) {}

    value = parseFloat(raw);
    return isFinite(value) && value > 0 ? value : fallback;
}

function resetLeftRecentAdaptiveState() {
    var list = document.getElementById('clbi-left-recent-list');
    var newsBox = list ? list.closest('.clbi-left-news-box') : null;
    var items = list ? Array.prototype.slice.call(list.querySelectorAll('.news-recent-item')) : [];

    if (newsBox) {
        newsBox.classList.remove('is-adaptive-constrained');
        newsBox.style.removeProperty('--adaptive-news-h');
    }

    if (list) {
        list.classList.remove('is-adaptive-faded');
        list.removeAttribute('data-adaptive-limit');
        list.style.removeProperty('--adaptive-recent-h');
    }

    items.forEach(function (item) {
        item.classList.remove('is-adaptive-hidden');
    });
}

function resetLeftBillboardAdaptiveState() {
    var box = document.querySelector('#clbi-left-sidebar .left-billboard-box');

    if (!box) return;

    box.classList.remove('is-left-ad-title-only', 'is-left-ad-extended');
    box.style.removeProperty('--left-billboard-h');
    box.style.removeProperty('--left-billboard-finish-h');
}

function updateClbiShellVerticalScale() {
    var root = document.documentElement;
    var body = document.body;
    var host;
    var topInner;
    var bottomInner;
    var thresholdH;
    var gap;
    var outerGapTotal;
    var baseStageH;
    var stageW;
    var stageH;
    var viewportW;
    var viewportH;
    var availableW;
    var availableH;
    var topH;
    var bottomH;
    var contentH;
    var widthScale;
    var heightScale;
    var scale;
    var shouldScale;

    if (!root || !body) return;

    host = ensureClbiVerticalScaleHost();
    if (!host) return;

    thresholdH = readClbiRootPx('--clbi-vertical-scale-threshold-h', 1080);
    gap = readClbiRootPx('--layout-gap', 8);
    outerGapTotal = gap * 2;
    baseStageH = Math.max(420, thresholdH - outerGapTotal);
    stageW = readClbiRootPx('--layout-shell-w', 1880);

    viewportW = Math.max(320, window.innerWidth || document.documentElement.clientWidth || stageW);
    viewportH = Math.max(320, window.innerHeight || document.documentElement.clientHeight || thresholdH);
    availableW = Math.max(240, viewportW - outerGapTotal);
    availableH = Math.max(240, viewportH - outerGapTotal);

    /*
    평상시에는 기존 세로 채움 레이아웃을 기준으로 삼는다.
    세로가 기준점보다 작아지는 경우에는 기준 높이를 고정점으로 삼고,
    가로만 부족한 경우에는 현재 사용 가능한 세로 높이를 고정점으로 삼는다.
    이렇게 해야 가로 부족으로 scale에 들어갈 때 본문 높이가 갑자기 접히지 않는다.
    */
    stageH = availableH >= baseStageH ? availableH : baseStageH;

    topInner = document.getElementById('clbi-top-nav');
    bottomInner = document.getElementById('clbi-bottom-nav');

    topH = topInner ? Math.ceil(topInner.offsetHeight || topInner.getBoundingClientRect().height || 38) : 38;
    bottomH = bottomInner ? Math.ceil(bottomInner.offsetHeight || bottomInner.getBoundingClientRect().height || 38) : 38;
    contentH = Math.max(360, Math.floor(stageH - topH - bottomH - (gap * 2)));

    widthScale = availableW < stageW ? availableW / stageW : 1;
    heightScale = availableH < baseStageH ? availableH / baseStageH : 1;
    scale = Math.min(1, widthScale, heightScale);
    scale = Math.max(0.50, Math.min(1, Math.floor(scale * 1000) / 1000));
    shouldScale = scale < 0.999;

    setClbiRootMetric(root, '--clbi-stage-design-w', stageW + 'px');
    setClbiRootMetric(root, '--clbi-stage-design-h', Math.floor(stageH) + 'px');
    setClbiRootMetric(root, '--clbi-stage-content-h', contentH + 'px');
    setClbiRootMetric(root, '--clbi-shell-scale', String(scale));

    if (body.classList.contains('clbi-shell-vertical-scale') !== shouldScale) {
        body.classList.toggle('clbi-shell-vertical-scale', shouldScale);
    }

    if (shouldScale) {
        resetLeftRecentAdaptiveState();
        resetLeftBillboardAdaptiveState();
    }
}
window.updateClbiShellVerticalScale = updateClbiShellVerticalScale;

var $contentWrapper = $('.content-wrapper').first();

if ($contentWrapper.length) {
    $('#clbi-top-nav-wrap, #clbi-bottom-nav-wrap').remove();
    $contentWrapper.before(buildClbiNavHtml('top'));
    $contentWrapper.after(buildClbiNavHtml('bottom'));
    renderClbiBottomShortcutGuide();
    initClbiNationsTabpanelControls(document);
    if (typeof window.normalizeClbiShellDomOrder === 'function') window.normalizeClbiShellDomOrder();
}

var CLBI_SHELL_METRICS_RAF = null;
var CLBI_SHELL_METRICS_SETTLE_TIMER = null;
var CLBI_SHELL_METRICS_LAST = { topH:-1, bottomH:-1 };

function setClbiRootMetric(root, name, value) {
    var next = String(value);
    if (root.style.getPropertyValue(name) === next) return false;
    root.style.setProperty(name, next);
    return true;
}

function runClbiShellMetricsBatch() {
    var top = document.getElementById('clbi-top-nav-wrap');
    var bottom = document.getElementById('clbi-bottom-nav-wrap');
    var root = document.documentElement;
    var topH = 0;
    var bottomH = 0;

    CLBI_SHELL_METRICS_RAF = null;
    if (!root) return;

    if (top) topH = Math.ceil(top.offsetHeight || top.getBoundingClientRect().height || 0);
    if (bottom) bottomH = Math.ceil(bottom.offsetHeight || bottom.getBoundingClientRect().height || 0);

    if (topH !== CLBI_SHELL_METRICS_LAST.topH) {
        CLBI_SHELL_METRICS_LAST.topH = topH;
        setClbiRootMetric(root, '--clbi-top-nav-outer-h', topH + 'px');
    }
    if (bottomH !== CLBI_SHELL_METRICS_LAST.bottomH) {
        CLBI_SHELL_METRICS_LAST.bottomH = bottomH;
        setClbiRootMetric(root, '--clbi-bottom-nav-outer-h', bottomH + 'px');
    }

    if (typeof updateClbiShellVerticalScale === 'function') updateClbiShellVerticalScale();
    if (typeof scheduleAdaptiveLeftRecentItems === 'function') scheduleAdaptiveLeftRecentItems();
    if (typeof scheduleClbiContentBottomGap === 'function') scheduleClbiContentBottomGap();
}

function requestClbiShellMetricsFrame() {
    if (CLBI_SHELL_METRICS_RAF !== null) return;
    CLBI_SHELL_METRICS_RAF = window.requestAnimationFrame
        ? window.requestAnimationFrame(runClbiShellMetricsBatch)
        : window.setTimeout(runClbiShellMetricsBatch, 16);
}

function scheduleClbiShellMetrics() {
    requestClbiShellMetricsFrame();
    window.clearTimeout(CLBI_SHELL_METRICS_SETTLE_TIMER);
    CLBI_SHELL_METRICS_SETTLE_TIMER = window.setTimeout(function () {
        CLBI_SHELL_METRICS_SETTLE_TIMER = null;
        requestClbiShellMetricsFrame();
    }, 120);
}
function watchClbiShellMetrics() {
    var top = document.getElementById('clbi-top-nav-wrap');
    var bottom = document.getElementById('clbi-bottom-nav-wrap');
    var observer;

    scheduleClbiShellMetrics();

    $(window).on('resize orientationchange', scheduleClbiShellMetrics);
    $(window).on('pageshow.clbiShellScale focus.clbiShellScale', scheduleClbiShellMetrics);
    document.addEventListener('visibilitychange', function () {
        if (!document.hidden) scheduleClbiShellMetrics();
    });
    $(window).on('resize.clbiLeftBillboard orientationchange.clbiLeftBillboard', scheduleLeftSidebarVerticalFit);
    $(window).on('resize.clbiRecentViewport orientationchange.clbiRecentViewport', function () { scheduleAdaptiveLeftRecentItems(); scheduleClbiContentBottomGap(); });
    $(window).on('resize.clbiContentBottomGap orientationchange.clbiContentBottomGap', scheduleClbiContentBottomGap);

    if (window.ResizeObserver) {
        observer = new ResizeObserver(scheduleClbiShellMetrics);
        if (top) observer.observe(top);
        if (bottom) observer.observe(bottom);
        window.CLBI_SHELL_RESIZE_OBSERVER = observer;
    }
}

function bindClbiWorldbuildingToggle(buttonSelector, menuSelector) {
    $(buttonSelector).on('click', function() {
        var $menu = $(menuSelector);
        var $btn = $(this);

        $menu.toggleClass('worldbuilding-open');
        $btn.toggleClass('clbi-tnav-active', $menu.hasClass('worldbuilding-open'));
        scheduleClbiShellMetrics();
    });
}

bindClbiWorldbuildingToggle('#clbi-tnav-worldbuilding', '#clbi-sub-worldbuilding');
bindClbiWorldbuildingToggle('#clbi-bnav-worldbuilding', '#clbi-bottom-sub-worldbuilding');

$('#clbi-top-search-input, #clbi-bottom-search-input').on('keydown', function(e) {
    if (e.key === 'Enter') {
        var q = $(this).val().trim();
        if (q) window.location.href = '/index.php?search=' + encodeURIComponent(q);
    }
});

if (window.mw && mw.hook) {
    mw.hook('wikipage.content').add(function ($content) {
        initClbiBottomShortcutSystem($content && $content[0] ? $content[0] : document);
    });
}

watchClbiShellMetrics();

});

// 페이지 전환 사운드
var transitionSound = new Audio('/index.php?title=특수:Redirect/file/Sfx-ui-001.mp3');

(function() {
    var master = parseFloat(localStorage.getItem('clbi-audio-master') || 80) / 100;
    var sfx = parseFloat(localStorage.getItem('clbi-audio-sfx') || 60) / 100;
    var sfxOn = localStorage.getItem('clbi-audio-sfxOn') !== 'false';
    transitionSound.volume = sfxOn ? master * sfx : 0;
})();

function playStaticSound() {
    var master = parseFloat(localStorage.getItem('clbi-audio-master') || 80) / 100;
    var sfx = parseFloat(localStorage.getItem('clbi-audio-sfx') || 60) / 100;
    var sfxOn = localStorage.getItem('clbi-audio-sfxOn') !== 'false';

    if (!sfxOn) return;

    transitionSound.volume = master * sfx;
    transitionSound.currentTime = 0;
    transitionSound.play();
}

// 현재 언어 감지
function getCurrentLang() {
    var langData = document.getElementById('clbi-lang-data');
    return langData ? (langData.getAttribute('data-lang') || 'ko') : 'ko';
}

function normalizePageName(value) {
    return String(value || '')
        .split('?')[0]
        .replace(/^\/index\.php\//, '')
        .replace(/_/g, ' ')
        .trim();
}

function buildWikiPath(title) {
    return '/index.php/' + encodeURI(String(title || '').replace(/ /g, '_'));
}

function getLangShortCode(lang) {
    var map = { ko: 'KR', en: 'EN', zh: 'ZH', ja: 'JA', ru: 'RU', es: 'ES' };
    return map[lang] || String(lang || '').toUpperCase();
}

function getLanguageTargetTitle(lang) {
    var data = document.getElementById('clbi-lang-data');
    if (!data || !lang) return '';

    var keys = [
        'data-' + lang,
        'data-page-' + lang,
        'data-title-' + lang,
        'data-target-' + lang,
        'data-lang-' + lang
    ];

    for (var i = 0; i < keys.length; i++) {
        var value = data.getAttribute(keys[i]);
        if (value) return value;
    }

    return '';
}

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


var SIDEBAR_LANG_SVG_NS = 'http://www.w3.org/2000/svg';
var SIDEBAR_LANGUAGE_STATUS_TITLE = 'MediaWiki:LanguageStatus.json';
var SIDEBAR_LANGUAGE_LABELS = {
    ko: '한국어',
    en: 'English',
    zh: '中文',
    ja: '日本語',
    ru: 'Русский',
    es: 'Español'
};
var SIDEBAR_LANGUAGE_DIAL_LABELS = {
    ko: '한국어',
    en: 'ENG',
    zh: '中文',
    ja: '日本語',
    ru: 'РУС',
    es: 'ESP'
};
var SIDEBAR_LANGUAGE_STATUS_VALUES = {
    available: true,
    wip: true,
    unavailable: true
};

var sidebarLanguageStatusRegistry = {};
var sidebarLanguageStatusLoaded = false;
var sidebarLanguageStatusLoading = false;
var sidebarLanguageStatusCallbacks = [];

var sidebarLanguageState = {
    order: ['ko', 'en', 'zh', 'ja', 'ru', 'es'],
    currentLang: 'ko',
    baseIndex: 0,
    selectedIndex: 0,
    rotation: 0,
    dragging: false,
    dragMoved: false,
    dragStartX: 0,
    dragStartY: 0,
    dragStartRotation: 0,
    dragAxis: null,
    pointerCaptured: false,
    lastX: 0,
    lastTime: 0,
    releaseVelocity: 0,
    suppressClickUntil: 0,
    raf: null,
    pendingRotation: 0,
    snapTimer: null,
    inertiaRaf: null,
    navigateTimer: null,
    bound: false,
    boundElement: null,
    rotor: null,
    cx: 101,
    cy: 119,
    outerR: 109,
    innerR: 28,
    sectorAngle: 30,
    halfSector: 15,
    repeats: 8,
    dragSensitivity: 0.58,
    maxSpinVelocity: 1.75,
    minSpinVelocity: 0.055,
    spinDecel: 0.00185
};

function createSidebarLanguageSvgEl(tag) {
    return document.createElementNS(SIDEBAR_LANG_SVG_NS, tag);
}

function normalizeSidebarLanguageIndex(index) {
    var length = sidebarLanguageState.order.length;
    var normalized = index % length;
    return normalized < 0 ? normalized + length : normalized;
}

function getSidebarLanguageName(lang) {
    return SIDEBAR_LANGUAGE_LABELS[lang] || String(lang || '').toUpperCase();
}

function getSidebarLanguageDialName(lang) {
    return SIDEBAR_LANGUAGE_DIAL_LABELS[lang] || getSidebarLanguageName(lang);
}

function normalizeSidebarLanguageStatusValue(value) {
    value = String(value == null ? '' : value).toLowerCase().trim();
    return SIDEBAR_LANGUAGE_STATUS_VALUES[value] ? value : '';
}

function getSidebarLanguageStatusPageKey() {
    var raw = String(mw.config.get('wgPageName') || '').trim();
    var normalized = normalizePageName(raw);

    return normalized || raw || '대문';
}

function getSidebarLanguageStatusEntry() {
    var registry = sidebarLanguageStatusRegistry || {};
    var pages = registry.pages && typeof registry.pages === 'object' ? registry.pages : registry;
    var raw = String(mw.config.get('wgPageName') || '').trim();
    var normalized = normalizePageName(raw);
    var title = String(mw.config.get('wgTitle') || '').trim();
    var keys = [
        normalized,
        raw,
        raw.replace(/_/g, ' '),
        normalized.replace(/ /g, '_'),
        title,
        title.replace(/_/g, ' ')
    ];
    var i;

    for (i = 0; i < keys.length; i += 1) {
        if (keys[i] && pages[keys[i]] && typeof pages[keys[i]] === 'object') {
            return pages[keys[i]];
        }
    }

    return {};
}

function getSidebarLanguageStatusOverride(lang) {
    var entry = getSidebarLanguageStatusEntry();
    return normalizeSidebarLanguageStatusValue(entry[lang]);
}

function flushSidebarLanguageStatusCallbacks() {
    var callbacks = sidebarLanguageStatusCallbacks.slice();
    sidebarLanguageStatusCallbacks.length = 0;

    callbacks.forEach(function(callback) {
        if (typeof callback === 'function') {
            callback(sidebarLanguageStatusRegistry);
        }
    });
}

function loadSidebarLanguageStatusRegistry(callback, force) {
    if (typeof callback === 'function') {
        sidebarLanguageStatusCallbacks.push(callback);
    }

    if (sidebarLanguageStatusLoaded && !force) {
        flushSidebarLanguageStatusCallbacks();
        return;
    }

    if (sidebarLanguageStatusLoading) return;

    sidebarLanguageStatusLoading = true;

    function finishLanguageStatus(parsed) {
        sidebarLanguageStatusRegistry = parsed && typeof parsed === 'object' ? parsed : {};
        sidebarLanguageStatusLoaded = true;
        sidebarLanguageStatusLoading = false;
        flushSidebarLanguageStatusCallbacks();
    }

    if (!force && window.EntryStore && typeof window.EntryStore.fetchJsonRef === 'function') {
        window.EntryStore.fetchJsonRef(SIDEBAR_LANGUAGE_STATUS_TITLE, { noStore: false })
            .then(function (parsed) { finishLanguageStatus(parsed); })
            .catch(function () { finishLanguageStatus({}); });
        return;
    }

    (function () {
        var url = mw.util.getUrl(SIDEBAR_LANGUAGE_STATUS_TITLE, {
            action: 'raw',
            ctype: 'application/json'
        });
        if (window.RevisionManifest && typeof window.RevisionManifest.addRevisionParam === 'function') {
            url = window.RevisionManifest.addRevisionParam(url, SIDEBAR_LANGUAGE_STATUS_TITLE);
        }
        $.ajax({
            url: url,
            dataType: 'text',
            cache: true
        }).done(function(text) {
            var parsed = {};

            try {
                parsed = text ? JSON.parse(text) : {};
            } catch (err) {
                console.error('LanguageStatus.json parse failed:', err);
                parsed = {};
            }

            finishLanguageStatus(parsed);
        }).fail(function() {
            finishLanguageStatus({});
        });
    })();
}

window.CLBI_LANGUAGE_STATUS = {
    title: SIDEBAR_LANGUAGE_STATUS_TITLE,
    languages: sidebarLanguageState.order.slice(),
    labels: SIDEBAR_LANGUAGE_LABELS,
    dialLabels: SIDEBAR_LANGUAGE_DIAL_LABELS,
    getPageKey: getSidebarLanguageStatusPageKey,
    getRegistry: function() {
        return sidebarLanguageStatusRegistry || {};
    },
    reload: function(callback) {
        sidebarLanguageStatusLoaded = false;
        loadSidebarLanguageStatusRegistry(function() {
            renderSidebarLanguageBox();
            if (typeof callback === 'function') callback(sidebarLanguageStatusRegistry);
        }, true);
    },
    refreshDial: function() {
        renderSidebarLanguageBox();
    }
};

function getSidebarLanguageMeta(lang) {
    var currentLang = getCurrentLang();
    var targetTitle = getLanguageTargetTitle(lang);
    var isCurrent = lang === currentLang;

    return {
        lang: lang,
        code: getLangShortCode(lang),
        name: getSidebarLanguageName(lang),
        dialName: getSidebarLanguageDialName(lang),
        targetTitle: targetTitle,
        isCurrent: isCurrent,
        canMove: !!targetTitle && !isCurrent
    };
}

function getSidebarLanguageStatus(meta) {
    var override;

    if (!meta) {
        return {
            className: 'is-locked',
            label: 'UNAVAILABLE',
            canApply: false
        };
    }

    if (meta.isCurrent) {
        return {
            className: 'is-current',
            label: 'CURRENT',
            canApply: false
        };
    }

    override = getSidebarLanguageStatusOverride(meta.lang);

    if (override === 'wip') {
        return {
            className: 'is-locked',
            label: 'WIP',
            canApply: false
        };
    }

    if (override === 'unavailable') {
        return {
            className: 'is-locked',
            label: 'UNAVAILABLE',
            canApply: false
        };
    }

    if (override === 'available' || meta.targetTitle) {
        return {
            className: meta.targetTitle ? 'is-ready' : 'is-locked',
            label: meta.targetTitle ? 'AVAILABLE' : 'UNAVAILABLE',
            canApply: !!meta.targetTitle
        };
    }

    return {
        className: 'is-locked',
        label: 'UNAVAILABLE',
        canApply: false
    };
}

function sidebarLanguageRad(deg) {
    return (deg * Math.PI) / 180;
}

function sidebarLanguagePointAt(radius, deg) {
    var state = sidebarLanguageState;
    var angle = sidebarLanguageRad(deg);

    return {
        x: state.cx + Math.sin(angle) * radius,
        y: state.cy - Math.cos(angle) * radius
    };
}

function getSidebarLanguageSectorPath(start, end) {
    var state = sidebarLanguageState;
    var p1 = sidebarLanguagePointAt(state.outerR, start);
    var p2 = sidebarLanguagePointAt(state.outerR, end);
    var p3 = sidebarLanguagePointAt(state.innerR, end);
    var p4 = sidebarLanguagePointAt(state.innerR, start);
    var largeArc = Math.abs(end - start) > 180 ? 1 : 0;

    return [
        'M', p1.x.toFixed(3), p1.y.toFixed(3),
        'A', state.outerR, state.outerR, 0, largeArc, 1, p2.x.toFixed(3), p2.y.toFixed(3),
        'L', p3.x.toFixed(3), p3.y.toFixed(3),
        'A', state.innerR, state.innerR, 0, largeArc, 0, p4.x.toFixed(3), p4.y.toFixed(3),
        'Z'
    ].join(' ');
}

function getSidebarLanguageShellPath() {
    return getSidebarLanguageSectorPath(-68, 68);
}

function getSidebarLanguageByStep(step) {
    var state = sidebarLanguageState;
    var index = normalizeSidebarLanguageIndex(state.baseIndex + step);

    return {
        index: index,
        meta: getSidebarLanguageMeta(state.order[index])
    };
}

function getSidebarLanguagePreviewIndex() {
    var state = sidebarLanguageState;
    var step = Math.round(-state.rotation / state.sectorAngle);
    return normalizeSidebarLanguageIndex(state.baseIndex + step);
}

function getSidebarLanguagePreviewMeta() {
    var state = sidebarLanguageState;
    return getSidebarLanguageMeta(state.order[getSidebarLanguagePreviewIndex()]);
}

function makeSidebarLanguageSector(step) {
    var state = sidebarLanguageState;
    var item = getSidebarLanguageByStep(step);
    var group = createSidebarLanguageSvgEl('g');
    var path = createSidebarLanguageSvgEl('path');
    var label = createSidebarLanguageSvgEl('text');
    var labelY = state.cy - 78;
    var angle = step * state.sectorAngle;

    group.setAttribute('class', 'sidebar-lang-sector-group');
    group.setAttribute('data-step', String(step));
    group.setAttribute('data-index', String(item.index));
    group.setAttribute('data-lang', item.meta.lang);
    group.setAttribute('transform', 'rotate(' + angle + ' ' + state.cx + ' ' + state.cy + ')');

    path.setAttribute('class', 'sidebar-lang-sector');
    path.setAttribute('d', getSidebarLanguageSectorPath(-state.halfSector, state.halfSector));

    label.setAttribute('class', 'sidebar-lang-sector-label');
    label.setAttribute('x', String(state.cx));
    label.setAttribute('y', String(labelY + 5));
    label.textContent = item.meta.dialName || item.meta.name;

    group.appendChild(path);
    group.appendChild(label);

    group.addEventListener('click', function(e) {
        if (sidebarLanguageState.dragging || performance.now() < sidebarLanguageState.suppressClickUntil) return;

        e.preventDefault();
        e.stopPropagation();

        cancelSidebarLanguageSpin();
        snapSidebarLanguageToStep(parseInt(group.getAttribute('data-step') || '0', 10), true);
    });

    return group;
}

function renderSidebarLanguageWheel() {
    var state = sidebarLanguageState;
    var fan = document.getElementById('clbi-sidebar-lang-fan');
    var svg;
    var defs;
    var clip;
    var clipPath;
    var shadowBlur;
    var blur;
    var fixedDepthGradient;
    var shell;
    var clipped;
    var rotor;
    var fixedDepthPath;
    var fixedFocus;
    var shadowSoft;
    var shadowHard;
    var rim;
    var pointer;
    var tri;
    var line;
    var step;

    if (!fan) return;

    fan.innerHTML = '';

    svg = createSidebarLanguageSvgEl('svg');
    svg.setAttribute('class', 'sidebar-lang-fan-svg');
    svg.setAttribute('viewBox', '0 0 202 150');
    svg.setAttribute('preserveAspectRatio', 'xMidYMid meet');
    svg.setAttribute('role', 'img');
    svg.setAttribute('aria-label', '언어 선택 다이얼');

    defs = createSidebarLanguageSvgEl('defs');

    clip = createSidebarLanguageSvgEl('clipPath');
    clip.setAttribute('id', 'clbi-sidebar-language-fan-clip');
    clipPath = createSidebarLanguageSvgEl('path');
    clipPath.setAttribute('d', getSidebarLanguageShellPath());
    clip.appendChild(clipPath);

    shadowBlur = createSidebarLanguageSvgEl('filter');
    shadowBlur.setAttribute('id', 'clbi-sidebar-language-shadow-blur');
    shadowBlur.setAttribute('x', '-20%');
    shadowBlur.setAttribute('y', '-20%');
    shadowBlur.setAttribute('width', '140%');
    shadowBlur.setAttribute('height', '140%');
    blur = createSidebarLanguageSvgEl('feGaussianBlur');
    blur.setAttribute('stdDeviation', '3');
    shadowBlur.appendChild(blur);

    fixedDepthGradient = createSidebarLanguageSvgEl('linearGradient');
    fixedDepthGradient.setAttribute('id', 'clbi-sidebar-language-fixed-depth');
    fixedDepthGradient.setAttribute('x1', '0');
    fixedDepthGradient.setAttribute('y1', '0');
    fixedDepthGradient.setAttribute('x2', '0');
    fixedDepthGradient.setAttribute('y2', '1');

    [
        ['0%', '#ffffff', '0.030'],
        ['34%', '#ffffff', '0.006'],
        ['58%', '#000000', '0.030'],
        ['100%', '#000000', '0.250']
    ].forEach(function(item) {
        var stop = createSidebarLanguageSvgEl('stop');
        stop.setAttribute('offset', item[0]);
        stop.setAttribute('stop-color', item[1]);
        stop.setAttribute('stop-opacity', item[2]);
        fixedDepthGradient.appendChild(stop);
    });

    defs.appendChild(clip);
    defs.appendChild(shadowBlur);
    defs.appendChild(fixedDepthGradient);
    svg.appendChild(defs);

    shell = createSidebarLanguageSvgEl('path');
    shell.setAttribute('class', 'sidebar-lang-shell');
    shell.setAttribute('d', getSidebarLanguageShellPath());
    svg.appendChild(shell);

    clipped = createSidebarLanguageSvgEl('g');
    clipped.setAttribute('clip-path', 'url(#clbi-sidebar-language-fan-clip)');

    rotor = createSidebarLanguageSvgEl('g');
    rotor.setAttribute('id', 'clbi-sidebar-lang-wheel-rotor');
    rotor.setAttribute('class', 'sidebar-lang-wheel-rotor');

    for (step = -state.repeats; step <= state.repeats; step += 1) {
        rotor.appendChild(makeSidebarLanguageSector(step));
    }

    clipped.appendChild(rotor);
    svg.appendChild(clipped);

    fixedDepthPath = createSidebarLanguageSvgEl('path');
    fixedDepthPath.setAttribute('class', 'sidebar-lang-fixed-depth');
    fixedDepthPath.setAttribute('d', getSidebarLanguageShellPath());
    svg.appendChild(fixedDepthPath);

    fixedFocus = createSidebarLanguageSvgEl('path');
    fixedFocus.setAttribute('class', 'sidebar-lang-fixed-focus');
    fixedFocus.setAttribute('d', getSidebarLanguageSectorPath(-state.halfSector, state.halfSector));
    svg.appendChild(fixedFocus);

    shadowSoft = createSidebarLanguageSvgEl('path');
    shadowSoft.setAttribute('class', 'sidebar-lang-inner-shadow-soft');
    shadowSoft.setAttribute('d', getSidebarLanguageShellPath());
    svg.appendChild(shadowSoft);

    shadowHard = createSidebarLanguageSvgEl('path');
    shadowHard.setAttribute('class', 'sidebar-lang-inner-shadow-hard');
    shadowHard.setAttribute('d', getSidebarLanguageShellPath());
    svg.appendChild(shadowHard);

    rim = createSidebarLanguageSvgEl('path');
    rim.setAttribute('class', 'sidebar-lang-rim');
    rim.setAttribute('d', getSidebarLanguageShellPath());
    svg.appendChild(rim);

    pointer = createSidebarLanguageSvgEl('g');
    pointer.setAttribute('class', 'sidebar-lang-fixed-pointer');
    pointer.setAttribute('clip-path', 'url(#clbi-sidebar-language-fan-clip)');

    tri = createSidebarLanguageSvgEl('path');
    tri.setAttribute('class', 'sidebar-lang-pointer-triangle');
    tri.setAttribute('d', 'M ' + (state.cx - 10) + ' 10 L ' + (state.cx + 10) + ' 10 L ' + state.cx + ' 26 Z');
    pointer.appendChild(tri);

    line = createSidebarLanguageSvgEl('line');
    line.setAttribute('class', 'sidebar-lang-pointer-line');
    line.setAttribute('x1', String(state.cx));
    line.setAttribute('x2', String(state.cx));
    line.setAttribute('y1', '24');
    line.setAttribute('y2', '112');
    pointer.appendChild(line);

    svg.appendChild(pointer);
    fan.appendChild(svg);

    state.rotor = rotor;
    setSidebarLanguageRotation(state.rotation, false);
}

function updateSidebarLanguageDial() {
    var state = sidebarLanguageState;
    var meta = getSidebarLanguagePreviewMeta();
    var status = getSidebarLanguageStatus(meta);
    var selector = document.getElementById('clbi-sidebar-lang-selector');
    var apply = document.getElementById('clbi-sidebar-lang-apply');
    var selectedValue = document.getElementById('clbi-sidebar-lang-selected-value');
    var availabilityPanel = document.getElementById('clbi-sidebar-lang-availability-panel');
    var availabilityValue = document.getElementById('clbi-sidebar-lang-availability-value');

    if (selectedValue) {
        selectedValue.textContent = meta.name;
    }

    if (availabilityPanel) {
        availabilityPanel.classList.remove('is-ready', 'is-current', 'is-locked');
        availabilityPanel.classList.add(status.className);
    }

    if (availabilityValue) {
        availabilityValue.textContent = status.label;
    }

    if (apply) {
        apply.classList.toggle('is-disabled', !status.canApply);
        apply.setAttribute('aria-disabled', status.canApply ? 'false' : 'true');
        apply.setAttribute('aria-label', status.canApply ? (meta.name + ' 적용') : (meta.isCurrent ? '현재 언어' : '사용할 수 없는 언어'));
    }

    if (selector) {
        selector.setAttribute('data-selected-lang', meta.lang);
        selector.setAttribute('data-selected-code', meta.code);
        selector.classList.toggle('is-current', meta.isCurrent);
        selector.classList.toggle('is-ready', status.canApply);
        selector.classList.toggle('is-locked', !status.canApply && !meta.isCurrent);
        selector.classList.toggle('is-dragging', !!state.dragging);
        selector.classList.toggle('is-spinning', !!state.inertiaRaf);
    }

    return {
        meta: meta,
        status: status
    };
}

function setSidebarLanguageRotation(value, animate) {
    var state = sidebarLanguageState;
    state.rotation = value;
    updateSidebarLanguageDial();

    if (!state.rotor) return;

    if (animate) {
        $('#clbi-sidebar-lang-selector').addClass('is-snapping');
    } else {
        $('#clbi-sidebar-lang-selector').removeClass('is-snapping');
    }

    state.rotor.style.transform = 'rotate(' + state.rotation.toFixed(3) + 'deg)';
}

function requestSidebarLanguageRotation(value) {
    var state = sidebarLanguageState;
    state.pendingRotation = value;

    if (state.raf) return;

    state.raf = requestAnimationFrame(function() {
        state.raf = null;
        setSidebarLanguageRotation(state.pendingRotation, false);
    });
}

function cancelSidebarLanguageSpin() {
    var state = sidebarLanguageState;

    if (state.inertiaRaf) {
        cancelAnimationFrame(state.inertiaRaf);
        state.inertiaRaf = null;
    }

    $('#clbi-sidebar-lang-selector').removeClass('is-spinning');
}

function finishSidebarLanguageSnap(nearestIndex, callback) {
    var state = sidebarLanguageState;

    state.baseIndex = normalizeSidebarLanguageIndex(nearestIndex);
    state.selectedIndex = state.baseIndex;
    state.rotation = 0;
    state.dragging = false;

    $('#clbi-sidebar-lang-selector').removeClass('is-snapping is-dragging is-spinning');

    renderSidebarLanguageWheel();
    updateSidebarLanguageDial();

    if (typeof callback === 'function') {
        callback(getSidebarLanguageMeta(state.order[state.selectedIndex]));
    }
}

function snapSidebarLanguageToStep(step, animate, callback) {
    var state = sidebarLanguageState;
    var targetRotation = -step * state.sectorAngle;
    var nearestIndex = normalizeSidebarLanguageIndex(state.baseIndex + step);

    cancelSidebarLanguageSpin();
    clearTimeout(state.snapTimer);

    state.selectedIndex = nearestIndex;
    setSidebarLanguageRotation(targetRotation, !!animate);

    state.snapTimer = setTimeout(function() {
        finishSidebarLanguageSnap(nearestIndex, callback);
    }, animate ? 230 : 0);
}

function snapSidebarLanguageNearest(callback) {
    var state = sidebarLanguageState;
    var step = Math.round(-state.rotation / state.sectorAngle);
    snapSidebarLanguageToStep(step, true, callback);
}

function startSidebarLanguageInertiaSpin(initialVelocity) {
    var state = sidebarLanguageState;
    var velocity;
    var lastFrame;

    cancelSidebarLanguageSpin();

    velocity = Math.max(-state.maxSpinVelocity, Math.min(state.maxSpinVelocity, initialVelocity));

    if (Math.abs(velocity) < state.minSpinVelocity) {
        snapSidebarLanguageNearest();
        return;
    }

    $('#clbi-sidebar-lang-selector').addClass('is-spinning');
    lastFrame = performance.now();

    function frame(now) {
        var dt = Math.min(34, Math.max(1, now - lastFrame));
        var sign = velocity < 0 ? -1 : 1;
        var nextSpeed;

        lastFrame = now;
        state.rotation += velocity * dt;
        setSidebarLanguageRotation(state.rotation, false);

        nextSpeed = Math.max(0, Math.abs(velocity) - (state.spinDecel * dt));
        velocity = sign * nextSpeed;

        if (nextSpeed <= state.minSpinVelocity) {
            state.inertiaRaf = null;
            $('#clbi-sidebar-lang-selector').removeClass('is-spinning');
            snapSidebarLanguageNearest();
            return;
        }

        state.inertiaRaf = requestAnimationFrame(frame);
    }

    state.inertiaRaf = requestAnimationFrame(frame);
}

function scheduleSidebarLanguageNavigation(meta) {
    var status = getSidebarLanguageStatus(meta);

    if (!meta || !status.canApply) return;

    clearTimeout(sidebarLanguageState.navigateTimer);
    sidebarLanguageState.navigateTimer = setTimeout(function() {
        var title = getLanguageTargetTitle(meta.lang);

        if (!title || meta.lang === getCurrentLang()) return;

        window.location.href = buildWikiPath(title);
    }, 70);
}

function setSidebarLanguageSelection(lang) {
    var state = sidebarLanguageState;
    var index = state.order.indexOf(lang);

    if (index < 0) index = state.order.indexOf(getCurrentLang());
    if (index < 0) index = 0;

    if (state.raf) {
        cancelAnimationFrame(state.raf);
        state.raf = null;
    }

    cancelSidebarLanguageSpin();
    clearTimeout(state.snapTimer);

    state.currentLang = lang;
    state.baseIndex = index;
    state.selectedIndex = index;
    state.rotation = 0;
    state.dragging = false;
    state.dragMoved = false;
    state.releaseVelocity = 0;

    renderSidebarLanguageWheel();
    updateSidebarLanguageDial();
}

function moveSidebarLanguageSelection(delta) {
    snapSidebarLanguageToStep(-delta, true);
}

function bindSidebarLanguageSelector() {
    var state = sidebarLanguageState;
    var selector = document.getElementById('clbi-sidebar-lang-selector');
    var fan = document.getElementById('clbi-sidebar-lang-fan');
    var apply = document.getElementById('clbi-sidebar-lang-apply');

    if (!selector || !fan || !apply) return;

    if (state.bound && state.boundElement === selector) return;

    state.bound = true;
    state.boundElement = selector;

    fan.addEventListener('pointerdown', function(e) {
        cancelSidebarLanguageSpin();
        clearTimeout(state.snapTimer);

        state.dragging = true;
        state.dragMoved = false;
        state.dragStartX = e.clientX;
        state.dragStartY = e.clientY || 0;
        state.dragStartRotation = state.rotation;
        state.dragAxis = null;
        state.pointerCaptured = false;
        state.lastX = e.clientX;
        state.lastTime = performance.now();
        state.releaseVelocity = 0;

        selector.classList.add('is-dragging');
        selector.classList.remove('is-snapping');

        /*
        Vertical page scrolling must stay available when the pointer starts on
        the language dial. Capture and preventDefault are delayed until a
        horizontal drag is confirmed.
        */
    });

    fan.addEventListener('pointermove', function(e) {
        var now;
        var totalDx;
        var totalDy;
        var frameDx;
        var dt;
        var instantVelocity;

        if (!state.dragging) return;

        totalDx = e.clientX - state.dragStartX;
        totalDy = (e.clientY || 0) - state.dragStartY;

        if (!state.dragAxis && (Math.abs(totalDx) > 4 || Math.abs(totalDy) > 4)) {
            state.dragAxis = Math.abs(totalDx) >= Math.abs(totalDy) ? 'x' : 'y';

            if (state.dragAxis === 'y') {
                state.dragging = false;
                state.dragMoved = false;
                state.dragAxis = null;
                state.pointerCaptured = false;
                selector.classList.remove('is-dragging');
                return;
            }

            if (fan.setPointerCapture && e.pointerId != null) {
                try {
                    fan.setPointerCapture(e.pointerId);
                    state.pointerCaptured = true;
                } catch (err) {
                    state.pointerCaptured = false;
                }
            }
        }

        if (state.dragAxis !== 'x') return;

        now = performance.now();
        frameDx = e.clientX - state.lastX;
        dt = Math.max(1, now - state.lastTime);

        if (Math.abs(totalDx) > 3) state.dragMoved = true;

        instantVelocity = (frameDx * state.dragSensitivity) / dt;
        state.releaseVelocity = (state.releaseVelocity * 0.62) + (instantVelocity * 0.38);
        state.lastX = e.clientX;
        state.lastTime = now;

        requestSidebarLanguageRotation(state.dragStartRotation + totalDx * state.dragSensitivity);
        e.preventDefault();
        e.stopPropagation();
    });

    function finishDrag(e) {
        var velocityAge;
        var throwVelocity;
        var wasHorizontal;

        if (!state.dragging) return;

        wasHorizontal = state.dragAxis === 'x';
        state.dragging = false;
        selector.classList.remove('is-dragging');

        if (fan.releasePointerCapture && state.pointerCaptured && e && e.pointerId != null) {
            try { fan.releasePointerCapture(e.pointerId); } catch (err) {}
        }

        state.pointerCaptured = false;
        state.dragAxis = null;

        if (!wasHorizontal && !state.dragMoved) {
            return;
        }

        velocityAge = performance.now() - state.lastTime;
        throwVelocity = velocityAge > 120 ? 0 : state.releaseVelocity;

        if (state.dragMoved) {
            state.suppressClickUntil = performance.now() + 180;
        }

        if (state.dragMoved && Math.abs(throwVelocity) >= state.minSpinVelocity) {
            startSidebarLanguageInertiaSpin(throwVelocity);
        } else {
            snapSidebarLanguageNearest();
        }

        if (e) {
            e.preventDefault();
            e.stopPropagation();
        }
    }

    fan.addEventListener('pointerup', finishDrag);
    fan.addEventListener('pointercancel', finishDrag);
    fan.addEventListener('lostpointercapture', function() {
        if (!state.dragging) return;

        state.dragging = false;
        state.pointerCaptured = false;
        state.dragAxis = null;
        selector.classList.remove('is-dragging');

        if (state.dragMoved && Math.abs(state.releaseVelocity) >= state.minSpinVelocity) {
            state.suppressClickUntil = performance.now() + 180;
            startSidebarLanguageInertiaSpin(state.releaseVelocity);
        } else {
            snapSidebarLanguageNearest();
        }
    });

    apply.addEventListener('click', function(e) {
        e.preventDefault();
        e.stopPropagation();

        snapSidebarLanguageNearest(function(meta) {
            scheduleSidebarLanguageNavigation(meta);
        });
    });

    selector.addEventListener('keydown', function(e) {
        if (e.key === 'ArrowLeft') {
            moveSidebarLanguageSelection(-1);
            e.preventDefault();
        }

        if (e.key === 'ArrowRight') {
            moveSidebarLanguageSelection(1);
            e.preventDefault();
        }

        if (e.key === 'Enter' || e.key === ' ') {
            apply.click();
            e.preventDefault();
        }
    });
}

function renderSidebarLanguageBox() {
    bindSidebarLanguageSelector();
    setSidebarLanguageSelection(getCurrentLang());

    if (!sidebarLanguageStatusLoaded) {
        loadSidebarLanguageStatusRegistry(function() {
            setSidebarLanguageSelection(getCurrentLang());
        });
    }
}

function loadRecentChangesList(targetSelector, limit) {
    var $target = $(targetSelector);

    if (!$target.length) return;

    var lang = getCurrentLang();
    var t = (window.LANG && window.LANG[lang]) ? window.LANG[lang] : (window.LANG ? window.LANG.ko : null);
    var isNewsList = $target.closest('.clbi-left-news-box').length > 0;

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

    $target.html((t && t.loading) ? t.loading : '불러오는 중...');

    $.getJSON(
        '/api.php?action=query&list=recentchanges&rclimit=' + encodeURIComponent(limit || 5) + '&rcprop=title|timestamp|user&format=json&rcnamespace=0&rctype=edit|new',
        function(data) {
            var items = data && data.query ? data.query.recentchanges : [];
            var html = '';

            if (!items || !items.length) {
                $target.html('표시할 변경 사항이 없습니다.');
                return;
            }

            $.each(items, function(i, item) {
                var label = timeAgo(item.timestamp);
                var title = item.title || '';
                var userName = item.user || 'Unknown';
                var pageHref = buildWikiPath(title);
                var avatarSrc = '/index.php?title=특수:Redirect/file/Pfp-' + encodeURIComponent(userName) + '.png';

                if (isNewsList) {
                    html +=
                        '<a href="' + escapeHtml(pageHref) + '" class="news-recent-item">' +
                            '<img class="news-recent-avatar" src="' + escapeHtml(avatarSrc) + '" alt="" onerror="this.onerror=null;this.src=\'/index.php?title=특수:Redirect/file/Pfp-default.png\';">' +
                            '<div class="news-recent-main">' +
                                '<div class="news-recent-title-wrap">' +
                                    '<span class="news-recent-title">' + escapeHtml(title) + '</span>' +
                                '</div>' +
                                '<div class="news-recent-meta">' +
                                    '<span class="news-recent-user">@' + escapeHtml(userName) + '</span>' +
                                '</div>' +
                            '</div>' +
                            '<span class="news-recent-time">' + escapeHtml(label) + '</span>' +
                        '</a>';
                } else {
                    html +=
                        '<div class="clbi-recent-item">' +
                            '<div class="clbi-recent-title-wrap">' +
                                '<a href="' + escapeHtml(pageHref) + '" class="clbi-recent-title">' + escapeHtml(title) + '</a>' +
                            '</div>' +
                            '<span class="clbi-recent-time">' + escapeHtml(label) + '</span>' +
                        '</div>';
                }
            });

            if (isNewsList) {
                $target.html(
                    '<div class="news-recent-viewport">' +
                        '<div class="news-recent-stack">' + html + '</div>' +
                    '</div>'
                );

                if (typeof ensureNewsBottomFinish === 'function') {
                    ensureNewsBottomFinish();
                }
            } else {
                $target.html(html);
            }

            if (isNewsList && typeof scheduleAdaptiveLeftRecentItems === 'function') {
                scheduleAdaptiveLeftRecentItems();
            }

            $target.find(isNewsList ? '.news-recent-item' : '.clbi-recent-item').each(function() {
                var wrap = $(this).find(isNewsList ? '.news-recent-title-wrap' : '.clbi-recent-title-wrap');
                var title = $(this).find(isNewsList ? '.news-recent-title' : '.clbi-recent-title');

                if (!wrap.length || !title.length) return;

                var wrapW = wrap.width();
                var titleW = title[0].scrollWidth;

                if (titleW > wrapW + 20) {
                    var duration = titleW / 40;

                    title.css({
                        animation: 'clbi-scroll ' + duration + 's linear infinite',
                        '--scroll-dist': '-' + (titleW - wrapW + 8) + 'px'
                    });
                }
            });
        }
    ).fail(function() {
        var lang = getCurrentLang();
        var t = (window.LANG && window.LANG[lang]) ? window.LANG[lang] : (window.LANG ? window.LANG.ko : null);

        $target.html((t && t.loadFail) ? t.loadFail : '불러오기 실패');
    });
}


function ensureRecentViewport() {
    var list = document.getElementById('clbi-left-recent-list');
    var viewport;
    var stack;
    var children;

    if (!list) return null;

    viewport = list.querySelector(':scope > .news-recent-viewport');
    stack = viewport ? viewport.querySelector(':scope > .news-recent-stack') : null;

    if (viewport && stack) return viewport;

    children = Array.prototype.slice.call(list.children || []);

    viewport = document.createElement('div');
    viewport.className = 'news-recent-viewport';

    stack = document.createElement('div');
    stack.className = 'news-recent-stack';

    children.forEach(function (child) {
        if (child.classList && child.classList.contains('news-recent-viewport')) return;
        stack.appendChild(child);
    });

    viewport.appendChild(stack);
    list.appendChild(viewport);

    return viewport;
}

function ensureNewsBottomFinish() {
    var newsBox = document.querySelector('#clbi-left-sidebar .clbi-left-news-box');
    var content = newsBox ? newsBox.querySelector('.clbi-news-box') : null;
    var finish;

    if (!content) return null;

    finish = content.querySelector(':scope > .news-bottom-finish');

    if (!finish) {
        finish = document.createElement('div');
        finish.className = 'news-bottom-finish';
        finish.setAttribute('aria-hidden', 'true');
        content.appendChild(finish);
    }

    return finish;
}

function updateAdaptiveLeftRecentItems() {
    /*
    134 기준: 좌측 사이드는 뉴스 확장형 flex 레이아웃이 높이를 담당한다.
    이전 adaptive 코드는 항목을 숨기거나 mask/fade를 걸기 위한 것이었으므로
    여기서는 잔여 상태만 정리하고 DOM 래퍼만 보장한다.
    */
    resetLeftRecentAdaptiveState();

    if (typeof ensureRecentViewport === 'function') {
        ensureRecentViewport();
    }

    if (typeof ensureNewsBottomFinish === 'function') {
        ensureNewsBottomFinish();
    }

    if (typeof scheduleClbiContentBottomGap === 'function') {
        scheduleClbiContentBottomGap();
    }
}

function scheduleAdaptiveLeftRecentItems() {
    window.requestAnimationFrame(function () {
        updateAdaptiveLeftRecentItems();
    });

    window.setTimeout(updateAdaptiveLeftRecentItems, 80);
    window.setTimeout(updateAdaptiveLeftRecentItems, 240);
}



function updateClbiContentBottomGap(iteration) {
    var content = document.querySelector('.container-fluid.liberty-content');
    var main = document.querySelector('.liberty-content-main');
    var bottomNav = document.getElementById('clbi-bottom-nav-wrap');
    var desiredGap = 8;
    var rootStyle;
    var scale = 1;
    var contentRect;
    var bottomTop;
    var targetHeight;
    var currentHeight;
    var visualGap;

    iteration = iteration || 0;

    if (!content || !main || !bottomNav) return;

    /*
    하단 간격은 scale 모드에서도 같은 기준으로 계산한다.
    transform:scale()이 걸리면 getBoundingClientRect()는 축소된 화면 좌표를 반환하므로,
    목표 간격 8px도 scale을 곱한 화면 좌표로 비교하고 다시 design px로 환산한다.

    목표:
    .liberty-content-main.bottom === #clbi-bottom-nav-wrap.top - 8px
    */
    if (document.body && document.body.classList && document.body.classList.contains('clbi-shell-vertical-scale')) {
        rootStyle = window.getComputedStyle(document.documentElement);
        scale = parseFloat(rootStyle.getPropertyValue('--clbi-shell-scale')) || 1;
        scale = Math.max(0.25, scale);
    }

    contentRect = content.getBoundingClientRect();
    bottomTop = bottomNav.getBoundingClientRect().top;
    visualGap = desiredGap * scale;
    targetHeight = Math.floor((bottomTop - contentRect.top - visualGap) / scale);
    targetHeight = Math.max(120, targetHeight);

    currentHeight = Math.round(content.getBoundingClientRect().height / scale);

    content.style.setProperty('--clbi-content-extra', '0px');
    content.style.setProperty('height', targetHeight + 'px', 'important');
    content.style.setProperty('max-height', targetHeight + 'px', 'important');

    if (Math.abs(currentHeight - targetHeight) >= 1 && iteration < 4) {
        window.requestAnimationFrame(function () {
            updateClbiContentBottomGap(iteration + 1);
        });
    }
}

function scheduleClbiContentBottomGap() {
    window.requestAnimationFrame(function () {
        updateClbiContentBottomGap(0);
    });
    window.setTimeout(function () {
        updateClbiContentBottomGap(0);
    }, 40);
    window.setTimeout(function () {
        updateClbiContentBottomGap(0);
    }, 120);
    window.setTimeout(function () {
        updateClbiContentBottomGap(0);
    }, 280);
    window.setTimeout(function () {
        updateClbiContentBottomGap(0);
    }, 520);
}


function updateLeftBillboardAdaptive() {
    /*
    134 기준: Ad는 이미지/CRT 비율을 유지하는 고정 슬롯이다.
    남는 세로 공간은 뉴스 박스가 흡수하므로, Ad에 하단 finish를 늘리거나
    title-only 상태로 접는 adaptive 보정은 사용하지 않는다.
    */
    resetLeftBillboardAdaptiveState();
}

function scheduleLeftBillboardAdaptive() {
    window.requestAnimationFrame(updateLeftBillboardAdaptive);
    window.setTimeout(updateLeftBillboardAdaptive, 80);
    window.setTimeout(updateLeftBillboardAdaptive, 240);
}

function scheduleLeftSidebarVerticalFit() {
    if (typeof scheduleAdaptiveLeftRecentItems === 'function') {
        scheduleAdaptiveLeftRecentItems();
    }

    if (typeof scheduleLeftBillboardAdaptive === 'function') {
        scheduleLeftBillboardAdaptive();
    }

    window.setTimeout(function () {
        if (typeof scheduleAdaptiveLeftRecentItems === 'function') {
            scheduleAdaptiveLeftRecentItems();
        }

        if (typeof scheduleLeftBillboardAdaptive === 'function') {
            scheduleLeftBillboardAdaptive();
        }
    }, 120);
}


// 시대 문서 전용 왼쪽 사이드바 이미지
function updateLeftSidebarNationsImage() {
    $('#clbi-left-nations-image').remove();
}

function setProfileActionLabel(selector, text) {
    var target = $(selector);
    var label = target.find('.profile-action-label');

    if (label.length) {
        label.text(text);
    } else {
        target.text(text);
    }
}

// 사이드바 업데이트
function updateSidebar() {
    if (!window.LANG) {
        setTimeout(updateSidebar, 100);
        return;
    }

    var currentLang = getCurrentLang();
    var t = (window.LANG && window.LANG[currentLang]) ? window.LANG[currentLang] : window.LANG.ko;

    var newsTitle = t.news || '뉴스';
    var changelogTitle = t.changelog || '체인지로그';
    var recentTitle = t.recentChanges || '최근 변경';
    var languageTitle = t.language || '언어';

    $('#clbi-title-left-language').text(languageTitle);
    renderSidebarLanguageBox();

    $('#clbi-title-left-news').text(newsTitle);
    $('#clbi-left-news-changelog-main').text(changelogTitle);
    $('#clbi-left-news-recent-main').text(recentTitle);

    $('#clbi-title-search a').text(t.search);
    $('#clbi-search-input').attr('placeholder', t.search + '...');
    $('#clbi-title-recent a').text(recentTitle);
    $('#clbi-title-guide-label').text(t.guide);
    $('#clbi-guide-link').text(t.getStarted);
    $('#clbi-title-links-label').text(t.links);

    setProfileActionLabel('#clbi-btn-contribution', t.contribution);
    setProfileActionLabel('#clbi-btn-watchlist', t.watchlist);
    setProfileActionLabel('#clbi-btn-preferences', t.preferences);
    setProfileActionLabel('#clbi-btn-logout', t.logout);
    setProfileActionLabel('#clbi-btn-login', t.login);

    var pageName = normalizePageName(mw.config.get('wgPageName'));
    var specialPage = String(mw.config.get('wgCanonicalSpecialPageName') || '');

$('#clbi-left-news-changelog-main').text(changelogTitle);
$('#clbi-left-news-recent-title').text('RECENT CHANGES');

    $('.clbi-user-btn').removeClass('clbi-user-btn-active');

    if (
        specialPage === 'Contributions' ||
        specialPage === '기여' ||
        pageName.indexOf('특수:기여') === 0 ||
        pageName.indexOf('Special:Contributions') === 0
    ) {
        $('#clbi-btn-contribution').addClass('clbi-user-btn-active');
    }

    if (specialPage === 'Watchlist') {
        $('#clbi-btn-watchlist').addClass('clbi-user-btn-active');
    }

    if (
        specialPage === '설정' ||
        pageName === '특수:설정' ||
        pageName === 'Special:설정'
    ) {
        $('#clbi-btn-preferences').addClass('clbi-user-btn-active');
    }

    $('.toggleBtn').each(function() {
        var btn = $(this);

        if (!$('#' + btn.data('target')).hasClass('folding-open')) {
            btn.text(t.expand);
        } else {
            btn.text(t.collapse);
        }
    });

    updateLeftSidebarNationsImage();
}

function canShowContentTools() {
    // 비로그인 사용자는 편집/역사/공유 버튼을 숨김
    if (!mw.config.get('wgUserName')) {
        return false;
    }

    // MediaWiki가 현재 문서를 편집 가능하지 않다고 판단하면 숨김
    var isEditable = mw.config.get('wgIsProbablyEditable');
    if (isEditable === false) {
        return false;
    }

    var relevantEditable = mw.config.get('wgRelevantPageIsProbablyEditable');
    if (relevantEditable === false) {
        return false;
    }

    return true;
}

function getCatlinkNodes(root) {
    var seen = [];
    var nodes = [];
    var $root = root ? $(root) : $(document);

    $root.find('#catlinks, .catlinks').add($root.filter('#catlinks, .catlinks')).each(function () {
        if (seen.indexOf(this) !== -1) return;
        seen.push(this);
        nodes.push(this);
    });

    return nodes;
}

function getCatlinksTarget(root) {
    var $root = root ? $(root) : $(document);
    var parserOutput = $root.find('.liberty-content-main .mw-parser-output').first();
    var main = $root.find('.liberty-content-main').first();

    if (!parserOutput.length && root && $(root).is('.liberty-content-main')) {
        parserOutput = $(root).find('.mw-parser-output').first();
        main = $(root);
    }

    if (!parserOutput.length && root && $(root).is('.mw-parser-output')) {
        parserOutput = $(root);
    }

    if (parserOutput.length) return parserOutput;
    if (main.length) return main;

    if (!root) {
        parserOutput = $('.liberty-content-main .mw-parser-output').first();
        main = $('.liberty-content-main').first();
        if (parserOutput.length) return parserOutput;
        if (main.length) return main;
    }

    return $();
}

var CLBI_CATLINKS_FETCH_TOKEN = 0;

function getCurrentPageTitleForCatlinks() {
    return String(
        mw.config.get('wgPageName') ||
        mw.config.get('wgRelevantPageName') ||
        ''
    ).trim();
}

function shouldFetchCatlinks() {
    var pageName = getCurrentPageTitleForCatlinks();
    var specialPage = mw.config.get('wgCanonicalSpecialPageName');

    if (!pageName) return false;
    if (specialPage) return false;

    return true;
}

function clearCatlinksInlineHiding(cat) {
    if (!cat || !cat.style) return;

    cat.style.removeProperty('display');
    cat.style.removeProperty('visibility');
    cat.style.removeProperty('height');
    cat.style.removeProperty('max-height');
    cat.style.removeProperty('overflow');

    $(cat).find('.mw-normal-catlinks, #mw-normal-catlinks, .mw-hidden-catlinks, #mw-hidden-catlinks, .mw-hidden-cats-hidden, .mw-hidden-cats-user-shown').each(function () {
        if (!this.style) return;
        this.style.removeProperty('display');
        this.style.removeProperty('visibility');
        this.style.removeProperty('height');
        this.style.removeProperty('max-height');
        this.style.removeProperty('overflow');
    });
}

function exposeHiddenCatlinks(cat) {
    if (!cat) return;

    $(cat).find('.mw-hidden-catlinks, #mw-hidden-catlinks, .mw-hidden-cats-hidden, .mw-hidden-cats-user-shown').each(function () {
        this.classList.remove('mw-hidden-cats-hidden');
        this.classList.add('mw-hidden-cats-user-shown');

        if (this.style) {
            this.style.removeProperty('display');
            this.style.removeProperty('visibility');
            this.style.removeProperty('height');
            this.style.removeProperty('max-height');
            this.style.removeProperty('overflow');
        }
    });
}

function getCatlinkTextContent(cat) {
    var clone;
    var text;

    if (!cat) return '';

    clone = cat.cloneNode(true);
    $(clone).find('script, style').remove();

    text = String(clone.textContent || '')
        .replace(/\s+/g, ' ')
        .replace(/^(분류|숨은 분류|Category|Hidden categories)\s*:\s*/i, '')
        .replace(/^(분류|숨은 분류|Category|Hidden categories)\s*$/i, '')
        .trim();

    return text;
}

function hasRenderedCatlinkItems(cat) {
    var hasLink;
    var hasListText;

    if (!cat) return false;

    hasLink = false;
    $(cat).find('a').each(function () {
        var text = String($(this).text() || '').trim();
        var href = String(this.getAttribute('href') || '').trim();
        if (text || href) hasLink = true;
    });
    if (hasLink) return true;

    hasListText = false;
    $(cat).find('li').each(function () {
        if (String($(this).text() || '').trim()) hasListText = true;
    });
    if (hasListText) return true;

    return !!getCatlinkTextContent(cat);
}

function normalizeCategoryTitle(rawTitle) {
    var title = String(rawTitle == null ? '' : rawTitle).trim();

    if (!title) return '';

    title = title.replace(/_/g, ' ');

    if (/^(Category|분류):/i.test(title)) {
        return title;
    }

    return '분류:' + title;
}

function makeCategoryLinkTitle(rawTitle) {
    return String(rawTitle || '')
        .replace(/^Category:/i, '')
        .replace(/^분류:/, '')
        .replace(/_/g, ' ')
        .trim();
}

function dedupeCatlinkCategories(categories) {
    var seen = {};
    var result = [];

    (categories || []).forEach(function (item) {
        var title = '';
        var hidden = false;

        if (typeof item === 'string') {
            title = normalizeCategoryTitle(item);
        } else if (item && typeof item === 'object') {
            title = normalizeCategoryTitle(item.title || item.name || item.category || '');
            hidden = item.hidden !== undefined || item.isHidden === true;
        }

        if (!title) return;
        if (seen[title]) return;

        seen[title] = true;
        result.push({ title: title, hidden: hidden });
    });

    return result;
}

function getConfigCatlinksCategories() {
    var normal = mw.config.get('wgCategories') || [];
    var hidden = mw.config.get('wgHiddenCategories') || [];
    var categories = [];

    if (!Array.isArray(normal)) normal = [];
    if (!Array.isArray(hidden)) hidden = [];

    normal.forEach(function (name) {
        categories.push({ title: normalizeCategoryTitle(name), hidden: false });
    });

    hidden.forEach(function (name) {
        categories.push({ title: normalizeCategoryTitle(name), hidden: true });
    });

    return dedupeCatlinkCategories(categories);
}

function markCatlinksReady(cat, pageTitle) {
    if (!cat) return;

    cat.classList.add('catlinks');
    cat.classList.add('clbi-catlinks-ready');
    cat.classList.remove('clbi-catlinks-empty');
    cat.classList.remove('clbi-catlinks-pending');
    cat.classList.remove('clbi-catlinks-loading');
    cat.removeAttribute('data-clbi-catlinks-fetching');
    cat.setAttribute('data-clbi-catlinks-page', pageTitle || getCurrentPageTitleForCatlinks());
    clearCatlinksInlineHiding(cat);
    exposeHiddenCatlinks(cat);
}

function markCatlinksEmpty(cat) {
    if (!cat) return;

    cat.classList.add('catlinks');
    cat.classList.add('clbi-catlinks-empty');
    cat.classList.remove('clbi-catlinks-ready');
    cat.classList.remove('clbi-catlinks-pending');
    cat.classList.remove('clbi-catlinks-loading');
    cat.removeAttribute('data-clbi-catlinks-fetching');
    cat.removeAttribute('data-clbi-catlinks-page');
}

function markCatlinksPending(cat, pageTitle) {
    if (!cat) return;

    cat.classList.add('catlinks');
    cat.classList.remove('clbi-catlinks-ready');
    cat.classList.remove('clbi-catlinks-empty');
    cat.classList.add('clbi-catlinks-pending');
    cat.classList.add('clbi-catlinks-loading');
    cat.setAttribute('data-clbi-catlinks-fetching', '1');
    cat.setAttribute('data-clbi-catlinks-page', pageTitle || getCurrentPageTitleForCatlinks());
}

function renderFetchedCatlinks(cat, categories, pageTitle) {
    var container;
    var ul;
    var normalized;

    if (!cat) return false;

    normalized = dedupeCatlinkCategories(categories);

    if (!normalized.length) {
        markCatlinksEmpty(cat);
        return false;
    }

    cat.innerHTML = '';
    cat.classList.add('catlinks');
    cat.classList.add('clbi-catlinks-api-populated');

    container = document.createElement('div');
    container.className = 'mw-normal-catlinks';
    container.appendChild(document.createTextNode('분류: '));

    ul = document.createElement('ul');

    normalized.forEach(function (item) {
        var title = String(item && item.title ? item.title : '').trim();
        var li;
        var a;

        if (!title) return;

        li = document.createElement('li');
        a = document.createElement('a');
        a.href = mw.util.getUrl(title);
        a.title = title;
        a.textContent = makeCategoryLinkTitle(title);

        if (item.hidden) {
            li.className = 'clbi-hidden-category-item';
        }

        li.appendChild(a);
        ul.appendChild(li);
    });

    if (!ul.children.length) {
        markCatlinksEmpty(cat);
        return false;
    }

    container.appendChild(ul);
    cat.appendChild(container);
    markCatlinksReady(cat, pageTitle);
    return true;
}

function fetchCatlinksForPage(pageTitle, callback) {
    var api;

    if (typeof pageTitle === 'function') {
        callback = pageTitle;
        pageTitle = getCurrentPageTitleForCatlinks();
    }

    pageTitle = String(pageTitle || '').trim();

    if (!pageTitle || !shouldFetchCatlinks()) {
        if (typeof callback === 'function') callback([], pageTitle);
        return;
    }

    if (!mw.Api) {
        if (typeof callback === 'function') callback([], pageTitle);
        return;
    }

    api = new mw.Api();
    api.get({
        action: 'query',
        prop: 'categories',
        titles: pageTitle,
        cllimit: 'max',
        clprop: 'hidden',
        formatversion: 2
    }).done(function (data) {
        var pages = data && data.query && data.query.pages ? data.query.pages : [];
        var page = pages && pages.length ? pages[0] : null;
        var categories = page && page.categories ? page.categories : [];

        if (typeof callback === 'function') callback(categories || [], pageTitle);
    }).fail(function () {
        if (typeof callback === 'function') callback([], pageTitle);
    });
}

function finalizeEmptyCatlinks(cat) {
    if (!cat) return;
    if (!cat.isConnected) return;

    clearCatlinksInlineHiding(cat);
    exposeHiddenCatlinks(cat);

    if (hasRenderedCatlinkItems(cat)) {
        markCatlinksReady(cat, getCurrentPageTitleForCatlinks());
        return;
    }

    markCatlinksEmpty(cat);
}

function fetchCatlinksIfNeeded(cat) {
    var configCategories;
    var pageTitle;
    var requestToken;

    if (!cat) return;
    if (!cat.isConnected) return;

    pageTitle = getCurrentPageTitleForCatlinks();

    clearCatlinksInlineHiding(cat);
    exposeHiddenCatlinks(cat);

    if (hasRenderedCatlinkItems(cat)) {
        markCatlinksReady(cat, pageTitle);
        return;
    }

    configCategories = getConfigCatlinksCategories();
    if (configCategories.length) {
        renderFetchedCatlinks(cat, configCategories, pageTitle);
        return;
    }

    if (!shouldFetchCatlinks()) {
        finalizeEmptyCatlinks(cat);
        return;
    }

    if (cat.getAttribute('data-clbi-catlinks-fetching') === '1' && cat.getAttribute('data-clbi-catlinks-page') === pageTitle) return;

    requestToken = ++CLBI_CATLINKS_FETCH_TOKEN;
    markCatlinksPending(cat, pageTitle);

    fetchCatlinksForPage(pageTitle, function (categories, requestedPage) {
        if (requestToken !== CLBI_CATLINKS_FETCH_TOKEN) return;
        if (requestedPage !== getCurrentPageTitleForCatlinks()) return;
        if (!cat || !cat.isConnected) return;
        if (cat.getAttribute('data-clbi-catlinks-page') !== requestedPage) return;

        if (!renderFetchedCatlinks(cat, categories, requestedPage)) {
            finalizeEmptyCatlinks(cat);
        }
    });
}

function normalizeCatlinksPanel(cat) {
    if (!cat) return;

    cat.classList.add('catlinks');
    clearCatlinksInlineHiding(cat);
    exposeHiddenCatlinks(cat);

    if (hasRenderedCatlinkItems(cat)) {
        markCatlinksReady(cat, getCurrentPageTitleForCatlinks());
        return;
    }

    fetchCatlinksIfNeeded(cat);
}

function createCatlinksPanel(target, className) {
    var cat;

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

    cat = document.createElement('div');
    cat.id = 'catlinks';
    cat.className = className || 'catlinks clbi-catlinks-created clbi-catlinks-pending';
    target.append(cat);
    return cat;
}

function prepareSpaCatlinksBeforeInsert(root) {
    var nodes;
    var target;
    var configCategories;
    var pageTitle;

    if (!root) return;

    pageTitle = getCurrentPageTitleForCatlinks();
    configCategories = getConfigCatlinksCategories();
    nodes = getCatlinkNodes(root);

    nodes.forEach(function (node) {
        node.classList.add('catlinks');
        clearCatlinksInlineHiding(node);
        exposeHiddenCatlinks(node);

        if (hasRenderedCatlinkItems(node)) {
            markCatlinksReady(node, pageTitle);
        } else if (configCategories.length) {
            renderFetchedCatlinks(node, configCategories, pageTitle);
        } else {
            markCatlinksEmpty(node);
        }
    });

    if (nodes.length || !configCategories.length) return;

    target = getCatlinksTarget(root);
    if (!target.length) target = $(root);

    renderFetchedCatlinks(createCatlinksPanel(target, 'catlinks catlinks-allhidden clbi-catlinks-created clbi-catlinks-pending'), configCategories, pageTitle);
}

function moveCatlinksToBottom() {
    var main = $('.liberty-content-main').first();
    var target = getCatlinksTarget();
    var catlinks = getCatlinkNodes();
    var configCategories;
    var pageTitle;
    var requestToken;

    if (!main.length || !target.length) return;

    pageTitle = getCurrentPageTitleForCatlinks();

    catlinks.forEach(function (node) {
        var catNode = $(node);

        if (node.parentNode !== target[0]) {
            catNode.appendTo(target);
        }

        normalizeCatlinksPanel(node);
    });

    if (catlinks.length) return;

    configCategories = getConfigCatlinksCategories();
    if (configCategories.length) {
        renderFetchedCatlinks(createCatlinksPanel(target, 'catlinks catlinks-allhidden clbi-catlinks-created clbi-catlinks-pending'), configCategories, pageTitle);
        return;
    }

    if (!shouldFetchCatlinks()) return;

    requestToken = ++CLBI_CATLINKS_FETCH_TOKEN;

    fetchCatlinksForPage(pageTitle, function (categories, requestedPage) {
        var cat;

        if (requestToken !== CLBI_CATLINKS_FETCH_TOKEN) return;
        if (requestedPage !== getCurrentPageTitleForCatlinks()) return;
        if (getCatlinkNodes().length) return;
        if (!categories || !categories.length) return;

        cat = createCatlinksPanel(target, 'catlinks catlinks-allhidden clbi-catlinks-created clbi-catlinks-pending');
        renderFetchedCatlinks(cat, categories, requestedPage);
    });
}

// 대문 스타일
function initCategoryNavIfAvailable(root) {
    /*
    CategoryNav.js는 대문 카테고리 네비를 SVG로 생성한다.

    Common.js가 SPA로 본문을 갈아끼운 뒤에는 MediaWiki 원래 페이지 로드와 달리
    CategoryNav.js의 초기 DOMContentLoaded만으로는 새 mount를 다시 잡지 못할 수 있다.
    CategoryNav.js 자체도 mw.hook('wikipage.content')를 듣지만, 로드 순서와 SPA 타이밍이
    엇갈릴 수 있으므로 Common.js 쪽에서도 존재 여부를 확인한 뒤 한 번 더 호출한다.

    이 함수는 CategoryNav.js가 아직 로드되지 않았으면 아무 것도 하지 않는다.
    */
    if (window.CategoryNav && typeof window.CategoryNav.init === 'function') {
        window.CategoryNav.init(root || document);
        return;
    }

    if (
        window.CLBI &&
        window.CLBI.categoryNav &&
        typeof window.CLBI.categoryNav.init === 'function'
    ) {
        window.CLBI.categoryNav.init(root || document);
    }
}

function removeLegacyMainPageHero() {
    /*
    기존 대문 전용 레거시 요소 정리
    -----------------------------------------
    이전 대문 구조에서는 Common.js가 본문 바깥에 #clbi-main-logo를 직접 삽입하고,
    본문 안의 #clbi-main-crt-hero를 #clbi-main-crt-hero-wrap으로 감싸서
    .liberty-content-main 위쪽으로 재배치했다.

    새 대문은 본문 내부의 .main-portal이 로고, 알림, 카테고리 네비, 이미지 피드,
    방명록, 상태 패널을 모두 담당한다. 따라서 Common.js가 별도 로고나 CRT 래퍼를
    삽입하면 새 로고/콘텐츠와 중복된다.

    여기서는 JS가 만들던 바깥 로고와 CRT 래퍼를 제거하고, 예전 대문 원본이나
    캐시된 렌더 결과에 남아 있을 수 있는 #clbi-main-crt-hero도 제거한다.
    */
    $('#clbi-main-logo').remove();
    $('#clbi-main-crt-hero-wrap').remove();
    $('#clbi-main-crt-hero').remove();
}


function setNativePageTitleHiddenHard(hidden) {
    var selectors = [
        '.liberty-content-header',
        '.liberty-content-header .title',
        '.liberty-content-header .title h1',
        '.liberty-content-header h1',
        '#firstHeading',
        '.firstHeading',
        '.mw-first-heading',
        '.page-heading',
        '.page-header',
        '.mw-page-title-main',
        '.mw-page-title-namespace',
        '.mw-page-title-separator'
    ];

    document.querySelectorAll(selectors.join(',')).forEach(function(node) {
        if (!node || !node.style) return;

        if (hidden) {
            node.setAttribute('data-clbi-title-hidden', 'true');
            node.style.setProperty('display', 'none', 'important');
            node.style.setProperty('visibility', 'hidden', 'important');
            node.style.setProperty('height', '0', 'important');
            node.style.setProperty('min-height', '0', 'important');
            node.style.setProperty('margin', '0', 'important');
            node.style.setProperty('padding', '0', 'important');
            node.style.setProperty('overflow', 'hidden', 'important');
        } else if (node.getAttribute('data-clbi-title-hidden') === 'true') {
            node.removeAttribute('data-clbi-title-hidden');
            node.style.removeProperty('display');
            node.style.removeProperty('visibility');
            node.style.removeProperty('height');
            node.style.removeProperty('min-height');
            node.style.removeProperty('margin');
            node.style.removeProperty('padding');
            node.style.removeProperty('overflow');
        }
    });
}

function applyDefaultPageTitleVisibility() {
    var hideTitle = true;
    var isSystemAssetPage = false;

    if (window.CLBI_PAGE_SHELL && typeof window.CLBI_PAGE_SHELL.isSystemAssetPage === 'function') {
        isSystemAssetPage = window.CLBI_PAGE_SHELL.isSystemAssetPage();
    }

    if (isSystemAssetPage) {
        hideTitle = true;
    } else if (window.CLBI_PAGE_SHELL && typeof window.CLBI_PAGE_SHELL.isBackendOrSystemPage === 'function') {
        hideTitle = !window.CLBI_PAGE_SHELL.isBackendOrSystemPage();
    }

    $('body')
        .toggleClass('page-title-hidden', hideTitle)
        .toggleClass('page-title-visible', !hideTitle)
        .toggleClass('clbi-system-doc-page', isSystemAssetPage);

    $('.content-tools').css('display', 'none');

    if (isSystemAssetPage && window.CLBI_PAGE_SHELL && typeof window.CLBI_PAGE_SHELL.renderSystemDocIndicator === 'function') {
        window.CLBI_PAGE_SHELL.renderSystemDocIndicator();
    } else if (!isSystemAssetPage && window.CLBI_PAGE_SHELL && typeof window.CLBI_PAGE_SHELL.removeSystemDocIndicator === 'function') {
        window.CLBI_PAGE_SHELL.removeSystemDocIndicator();
    }

    if (hideTitle) {
        $('.liberty-content-header').css('display', 'none');
        $('.mw-page-title-main, .mw-page-title-namespace, .mw-page-title-separator').addClass('clbi-hide');
        $('#firstHeading, .firstHeading, .mw-first-heading, .page-heading, .page-header').css('display', 'none');
        setNativePageTitleHiddenHard(true);
    } else {
        $('.liberty-content-header').css('display', '');
        $('.mw-page-title-main, .mw-page-title-namespace, .mw-page-title-separator').removeClass('clbi-hide');
        $('#firstHeading, .firstHeading, .mw-first-heading, .page-heading, .page-header').css('display', '');
        setNativePageTitleHiddenHard(false);
    }
}

function applyMainPageStyle() {
    var specialPage = mw.config.get('wgCanonicalSpecialPageName');
    if (specialPage === 'Preferences') return;

    var pageName = normalizePageName(mw.config.get('wgPageName'));
    var namespaceNumber = mw.config.get('wgNamespaceNumber');
    var isMainPage = (pageName === '대문');
    var isUserProfilePage = (namespaceNumber === 2);
    var isScreenDoc = ($('.screen-header').length > 0);
    var hideTools = (isMainPage || isUserProfilePage || !canShowContentTools());

    $('body').toggleClass('user-profile-page', isUserProfilePage);
    $('body').toggleClass('clbi-main-page', isMainPage);

    // 모든 문서에서 분류 바를 본문 컨테이너 아래로 이동
    moveCatlinksToBottom();

    if (isMainPage) {
        $('.liberty-content-header').css('display', 'none');
        $('.mw-page-title-main').addClass('clbi-hide');
        setNativePageTitleHiddenHard(true);
        $('.catlinks').css('display', 'none');
        $('.liberty-content-main').css('border-radius', '0');

        // 새 대문은 .main-portal 본문 구조가 로고/히어로를 담당한다.
        // Common.js의 구식 바깥 로고/CRT 재배치 루틴은 사용하지 않는다.
        removeLegacyMainPageHero();
        $('#clbi-tools-box').remove();

        $('.content-tools').css('display', 'none');

        initCategoryNavIfAvailable(document);

    } else if (isUserProfilePage) {
        $('.liberty-content-header').css('display', 'none');
        $('.mw-page-title-main, .mw-page-title-namespace, .mw-page-title-separator').addClass('clbi-hide');
        $('.catlinks').css('display', 'none');
        $('.liberty-content-main').css('border-radius', '0');

        $('#clbi-main-logo').remove();
        $('#clbi-main-crt-hero-wrap').remove();
        $('#clbi-main-crt-hero').remove();
        $('#clbi-tools-box').remove();

        $('.content-tools').css('display', 'none');

    } else if (isScreenDoc) {
        $('.liberty-content-header').css('display', 'none');
        $('.mw-page-title-main').addClass('clbi-hide');
        $('.catlinks').css('display', '');
        $('.liberty-content-main').css('border-radius', '0');

        $('#clbi-main-logo').remove();
        $('#clbi-main-crt-hero-wrap').remove();

        if ($('#clbi-tools-box').length === 0 && canShowContentTools()) {
            var $toolsBox = $('<div id="clbi-tools-box" class="clbi-left-box"></div>');
            var $toolsTitle = $('<div class="clbi-left-title">관리</div>');
            var $toolsContent = $('<div class="clbi-left-content"></div>');

            $toolsContent.append($('.content-tools .btn-group').clone(true));
            $toolsBox.append($toolsTitle).append($toolsContent);
            $('#clbi-left-sidebar').append($toolsBox);
        }

        $('.content-tools').css('display', 'none');

    } else {
        $('.liberty-content-header').css('display', '');
        $('.mw-page-title-main, .mw-page-title-namespace, .mw-page-title-separator').removeClass('clbi-hide');
        $('.catlinks').css('display', '');
        $('.liberty-content-main').css('border-radius', '0');

        $('#clbi-main-logo').remove();
        $('#clbi-main-crt-hero-wrap').remove();
        $('#clbi-tools-box').remove();
    }

    if (!isUserProfilePage) {
        $('.profile-card').remove();
        $('.user-profile-portal').removeClass('user-profile-portal');
    }

    $('.content-tools').css('display', 'none');

    applyDefaultPageTitleVisibility();
    updateSidebar();
}

// 본문 기본 목차 제거
function removeNativeTocFromContent() {
    $('.liberty-content-main #toc, .liberty-content-main .toc').remove();
}

// 왼쪽 목차: MediaWiki 문단 ID 가져오기
function getHeadingId(heading) {
    if (heading.id) {
        return heading.id;
    }

    var headline = heading.querySelector('.mw-headline[id]');
    if (headline && headline.id) {
        return headline.id;
    }

    return '';
}

// 왼쪽 목차: MediaWiki 문단 제목 텍스트 가져오기
function getHeadingText(heading) {
    var headline = heading.querySelector('.mw-headline');
    var source = headline || heading;
    var clone = source.cloneNode(true);

    $(clone).find('.mw-editsection, .mw-editsection-bracket, .mw-editsection-divider').remove();

    return (clone.textContent || '')
        .replace(/\s+/g, ' ')
        .trim();
}

// 왼쪽 목차: 긴 제목에 자동 스크롤 적용
function initTocTitleScroll(root) {
    var $items = root
        ? $(root).find('.toc-scroll-text')
        : $('#side-toc-box .toc-scroll-text');

    $items.each(function () {
        var $text = $(this);
        var $wrap = $text.closest('.toc-scroll-wrap');

        if (!$wrap.length) return;

        var wrapW = Math.floor($wrap.width());
        var textW = Math.ceil(this.scrollWidth);

        // 왼쪽 목차: 레이아웃 계산이 끝나지 않았으면 이번 실행에서는 건드리지 않는다.
        if (!wrapW || !textW) return;

        if (textW <= wrapW + 12) {
            // 왼쪽 목차: 칸을 넘지 않는 제목은 전체 텍스트를 그대로 보여준다.
            $wrap.removeClass('is-scrolling');

            if ($text.data('toc-scroll-enabled')) {
                $text.css({
                    animation: '',
                    'animation-delay': '',
                    '--scroll-dist': ''
                });
                $text.removeData('toc-scroll-enabled');
                $text.removeData('toc-scroll-key');
            }

            return;
        }

        var scrollDist = '-' + (textW - wrapW + 10) + 'px';
        var duration = Math.max(7, textW / 38) * 1.25;
        var scrollKey = scrollDist + '|' + duration;

        // 왼쪽 목차: 긴 제목에는 오른쪽 페이드와 스크롤을 적용한다.
        $wrap.addClass('is-scrolling');

        // 왼쪽 목차: 같은 값으로 이미 적용된 애니메이션은 다시 초기화하지 않는다.
        if ($text.data('toc-scroll-key') === scrollKey) {
            return;
        }

        $text.data('toc-scroll-enabled', true);
        $text.data('toc-scroll-key', scrollKey);

        $text.css({
            // 왼쪽 목차: 페이지 진입 직후에는 잠시 읽을 시간을 준 뒤 흐르게 한다.
            animation: 'toc-scroll-blink-reset ' + duration + 's linear infinite',
            'animation-delay': '1s',
            '--scroll-dist': scrollDist
        });
    });
}

// 목차를 왼쪽 사이드바에 새로 생성
function moveTocToLeftSidebar() {
    removeNativeTocFromContent();
    $('#side-toc-box').remove();
    return;

    // 왼쪽 목차: MediaWiki가 만든 원래 목차는 본문에서 제거한다.
    removeNativeTocFromContent();

    var leftSidebar = document.getElementById('clbi-left-sidebar');
    if (!leftSidebar) return;

    var content =
        document.querySelector('.liberty-content-main .mw-parser-output') ||
        document.querySelector('.liberty-content-main');

    if (!content) return;

    var headings = Array.prototype.slice.call(
        content.querySelectorAll('h2, h3')
    ).filter(function (heading) {
        if (heading.closest('#toc, .toc, #side-toc-box')) return false;

        var id = getHeadingId(heading);
        var text = getHeadingText(heading);

        if (!id || !text) return false;

        return true;
    });

    var tocKey = headings.map(function (heading) {
        return getHeadingId(heading) + '|' + getHeadingText(heading);
    }).join('||');

    var existingBox = document.getElementById('side-toc-box');

    // 왼쪽 목차: 같은 문서에서 같은 목차를 이미 만들었다면 다시 지우고 만들지 않는다.
    if (existingBox && existingBox.getAttribute('data-toc-key') === tocKey) {
        initTocTitleScroll(existingBox);
        return;
    }

    if (existingBox) {
        existingBox.remove();
    }

    if (!headings.length) return;

    var tocBox = document.createElement('div');
    tocBox.className = 'clbi-left-box';
    tocBox.id = 'side-toc-box';
    tocBox.setAttribute('data-toc-key', tocKey);

    var title = document.createElement('div');
    title.className = 'clbi-left-title';

    // 왼쪽 목차: 박스 제목은 Lang.js의 현재 UI 언어를 따른다.
    var currentLang = getCurrentLang();
    var t = (window.LANG && window.LANG[currentLang]) ? window.LANG[currentLang] : window.LANG.ko;
    var tocTitleText = (t && t.toc) ? t.toc : '목차';

    title.textContent = tocTitleText;

    var body = document.createElement('div');
    body.className = 'clbi-left-content toc-sidebar-content';

    var list = document.createElement('ul');
    list.className = 'generated-toc';

    headings.forEach(function (heading) {
        var id = getHeadingId(heading);
        var text = getHeadingText(heading);
        var level = heading.tagName.toLowerCase() === 'h3' ? 3 : 2;

        var item = document.createElement('li');
        item.className = 'toc-level-' + level;

        var link = document.createElement('a');
        link.setAttribute('href', '#' + id);

        // 왼쪽 목차: 긴 제목 스크롤을 위해 텍스트를 별도 span으로 감싼다.
        var textWrap = document.createElement('span');
        textWrap.className = 'toc-scroll-wrap';

        var textSpan = document.createElement('span');
        textSpan.className = 'toc-scroll-text';
        textSpan.textContent = text;

        textWrap.appendChild(textSpan);
        link.appendChild(textWrap);

        item.appendChild(link);
        list.appendChild(item);
    });

    body.appendChild(list);
    tocBox.appendChild(title);
    tocBox.appendChild(body);
    leftSidebar.appendChild(tocBox);

    // 왼쪽 목차: DOM 배치가 끝난 뒤 긴 제목 스크롤 여부를 계산한다.
    requestAnimationFrame(function () {
        initTocTitleScroll(tocBox);

        setTimeout(function () {
            initTocTitleScroll(tocBox);
        }, 120);
    });
}


// 우측 광고판: 이미지/번역 캡션 목록
var RIGHT_BILLBOARD_ITEMS = [
    {
        file: 'Side-visual-001.png',
        alt: 'PROOF TO THE WORLD / YOU ONCE PART OF IT',
        duration: 3000,
        caption: [
            '"당신이 한때 이 세계의',
            '',
            '일부였다는 것을 증명하십시오"'
        ]
    },
    {
        file: 'Side-visual-002.png',
        alt: 'APPLY NOW',
        duration: 1000,
        caption: [
            '"지금 지원하세요!"'
        ]
    },
    {
        file: 'Side-visual-003.png',
        alt: 'APPLY NOW',
        duration: 1000,
        caption: [
            '"지금 지원하세요!"'
        ]
    },
    {
        file: 'Side-visual-002.png',
        alt: 'APPLY NOW',
        duration: 1000,
        caption: [
            '"지금 지원하세요!"'
        ]
    }
];

function getRightBillboardItem(index) {
    var items = RIGHT_BILLBOARD_ITEMS;

    if (!items || !items.length) {
        return {
            file: 'Side-visual-001.png',
            alt: '',
            caption: []
        };
    }

    var normalized = index % items.length;
    if (normalized < 0) normalized += items.length;

    return items[normalized];
}

function getRightBillboardImageUrl(fileName) {
    return '/index.php?title=특수:Redirect/file/' + encodeURIComponent(fileName || 'Side-visual-001.png');
}

function getRightBillboardCaptionHtml(item) {
    var lines = item && item.caption ? item.caption : [];
    var html = '';

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

    lines.forEach(function(line) {
        var text = String(line == null ? '' : line);
        var isGap = !text.trim();
        var className = 'right-billboard-caption-line' + (isGap ? ' is-gap' : '');
        html += '<span class="' + className + '">' + (isGap ? '&nbsp;' : escapeCaptionText(text)) + '</span>';
    });

    return html;
}

function setRightBillboardItem(index) {
    var box = document.querySelector('.right-billboard-box');
    if (!box) return;

    var item = getRightBillboardItem(index);
    var src = getRightBillboardImageUrl(item.file);
    var images = box.querySelectorAll('.right-billboard-image');
    var caption = box.querySelector('#right-billboard-caption');
    var emptySub = box.querySelector('.right-billboard-empty-sub');

    box.setAttribute('data-billboard-index', String(index));
    box.classList.remove('is-empty');

    Array.prototype.forEach.call(images, function(img) {
        img.style.display = '';
        img.setAttribute('src', src);
        img.setAttribute('alt', img.classList.contains('right-billboard-image-base') ? (item.alt || '') : '');
    });

    if (caption) {
        caption.innerHTML = getRightBillboardCaptionHtml(item);
    }

    if (emptySub) {
        emptySub.textContent = item.file || 'Side-visual-001.png';
    }
}

function getRightBillboardItemDuration(item) {
    var duration = item && item.duration ? parseInt(item.duration, 10) : 3000;

    if (Number.isNaN(duration) || duration < 500) {
        duration = 3000;
    }

    return duration;
}

function initRightBillboardCarousel() {
    var box = document.querySelector('.right-billboard-box');
    if (!box || box.getAttribute('data-billboard-ready') === '1') return;

    box.setAttribute('data-billboard-ready', '1');
    box.setAttribute('data-billboard-index', '0');

    setRightBillboardItem(0);

    if (!RIGHT_BILLBOARD_ITEMS || RIGHT_BILLBOARD_ITEMS.length <= 1) return;

    function scheduleNext() {
        var current = parseInt(box.getAttribute('data-billboard-index') || '0', 10);
        if (Number.isNaN(current)) current = 0;

        var currentItem = getRightBillboardItem(current);
        var delay = getRightBillboardItemDuration(currentItem);

        window.setTimeout(function() {
            if (!document.body.contains(box)) return;

            if (!document.hidden) {
                setRightBillboardItem(current + 1);
            }

            scheduleNext();
        }, delay);
    }

    scheduleNext();
}


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

function buildRightBillboardBox() {
    var billboardInitial = getRightBillboardItem(0);
    var billboardSrc = getRightBillboardImageUrl(billboardInitial.file);

    return '' +
        '<div class="clbi-left-box right-billboard-box left-billboard-box left-ad-box" data-billboard-index="0">' +
            '<div class="clbi-left-title right-billboard-title left-ad-title left-ad-title-iconless">' +
                '<span id="clbi-title-left-ad" class="left-ad-title-label">Looking for a job?</span>' +
            '</div>' +
            '<div class="clbi-left-content left-ad-content-shell">' +
                '<div class="right-billboard-body">' +
                    '<div class="right-billboard-recess">' +
                        '<div class="right-billboard-screen">' +
                            '<img id="right-billboard-image" class="right-billboard-image right-billboard-image-base" src="' + escapeRightBillboardAttr(billboardSrc) + '" alt="' + escapeRightBillboardAttr(billboardInitial.alt || '') + '" onload="var b=this.closest(\'.right-billboard-box\'); if(b){b.classList.remove(\'is-empty\');}" onerror="this.onerror=null;this.style.display=\'none\';var b=this.closest(\'.right-billboard-box\'); if(b){b.classList.add(\'is-empty\');}">' +
                            '<img class="right-billboard-image right-billboard-image-bloom" src="' + escapeRightBillboardAttr(billboardSrc) + '" alt="" aria-hidden="true" onerror="this.onerror=null;this.style.display=\'none\';">' +
                            '<img class="right-billboard-image right-billboard-slice right-billboard-slice-a" src="' + escapeRightBillboardAttr(billboardSrc) + '" alt="" aria-hidden="true" onerror="this.onerror=null;this.style.display=\'none\';">' +
                            '<img class="right-billboard-image right-billboard-slice right-billboard-slice-b" src="' + escapeRightBillboardAttr(billboardSrc) + '" alt="" aria-hidden="true" onerror="this.onerror=null;this.style.display=\'none\';">' +
                            '<img class="right-billboard-image right-billboard-slice right-billboard-slice-c" src="' + escapeRightBillboardAttr(billboardSrc) + '" alt="" aria-hidden="true" onerror="this.onerror=null;this.style.display=\'none\';">' +
                            '<div class="right-billboard-glitch" aria-hidden="true"></div>' +
                            '<div class="right-billboard-tear" aria-hidden="true"></div>' +
                            '<div class="right-billboard-empty" aria-hidden="true">' +
                                '<span class="right-billboard-empty-main">SIGNAL EMPTY</span>' +
                                '<span class="right-billboard-empty-sub">' + escapeRightBillboardAttr(billboardInitial.file || 'Side-visual-001.png') + '</span>' +
                            '</div>' +
                        '</div>' +
                    '</div>' +
                    '<div id="right-billboard-caption" class="right-billboard-caption" aria-hidden="true">' + getRightBillboardCaptionHtml(billboardInitial) + '</div>' +
                    '<div class="right-billboard-bottom-finish left-ad-bottom-finish" aria-hidden="true"></div>' +
                '</div>' +
            '</div>' +
        '</div>';
}


var GREAT_WALL_DATA_TITLE = '프로젝트:The_Great_Wall/Data.json';
var GREAT_WALL_LIST_LIMIT = 0;
var greatWallState = {
    data: { entries: {} },
    loaded: false,
    loading: false,
    saving: false,
    selectedOwnEntry: false,
    statusText: ''
};

function normalizeGreatWallData(data) {
    var normalized = { entries: {} };
    var entries = data && data.entries && typeof data.entries === 'object' ? data.entries : {};

    Object.keys(entries).forEach(function(key) {
        var item = entries[key];
        var user = item && item.user ? String(item.user) : String(key || '');
        var text = item && item.text ? String(item.text) : '';
        var timestamp = item && item.timestamp ? String(item.timestamp) : '';

        if (!user || !text) return;

        normalized.entries[user] = {
            user: user,
            text: text.slice(0, 140),
            timestamp: timestamp || new Date(0).toISOString()
        };
    });

    return normalized;
}

function parseGreatWallData(text) {
    var parsed;

    try {
        parsed = text ? JSON.parse(text) : {};
    } catch (err) {
        console.error('The Great Wall data parse failed:', err);
        parsed = {};
    }

    return normalizeGreatWallData(parsed);
}

function stringifyGreatWallData(data) {
    return JSON.stringify(normalizeGreatWallData(data), null, 2) + '\n';
}

function getGreatWallRevisionText(page) {
    var rev;
    var slot;

    if (!page || !page.revisions || !page.revisions.length) return '';

    rev = page.revisions[0];

    if (rev.slots && rev.slots.main) {
        slot = rev.slots.main;
        return slot.content || slot['*'] || '';
    }

    return rev.content || rev['*'] || '';
}

function fetchGreatWallData() {
    var api = new mw.Api();

    return api.get({
        action: 'query',
        prop: 'revisions',
        titles: GREAT_WALL_DATA_TITLE,
        rvprop: 'content|timestamp',
        rvslots: 'main',
        formatversion: 2
    }).then(function(data) {
        var pages = data && data.query && data.query.pages ? data.query.pages : [];
        var page = pages[0] || null;

        if (!page || page.missing) {
            return { entries: {} };
        }

        return parseGreatWallData(getGreatWallRevisionText(page));
    }, function(err) {
        console.error('The Great Wall load failed:', err);
        return { entries: {} };
    });
}

function getGreatWallEntries(data) {
    var entries = data && data.entries && typeof data.entries === 'object' ? data.entries : {};

    return Object.keys(entries).map(function(key) {
        return entries[key];
    }).filter(function(item) {
        return item && item.user && item.text;
    }).sort(function(a, b) {
        return String(a.timestamp || '').localeCompare(String(b.timestamp || ''));
    });
}

function getGreatWallMessageTime(timestamp) {
    var date = timestamp ? new Date(timestamp) : null;
    var month;
    var day;
    var hour;
    var minute;

    if (!date || isNaN(date.getTime())) return '—';

    month = String(date.getMonth() + 1).padStart(2, '0');
    day = String(date.getDate()).padStart(2, '0');
    hour = String(date.getHours()).padStart(2, '0');
    minute = String(date.getMinutes()).padStart(2, '0');

    return month + '.' + day + ' ' + hour + ':' + minute;
}

function getGreatWallAvatarSrc(user) {
    return '/index.php?title=특수:Redirect/file/Pfp-' + encodeURIComponent(String(user || '')) + '.png';
}

function buildGreatWallEntryHtml(item, currentUser, selectedOwnEntry) {
    var isOwn = currentUser && item.user === currentUser;
    var tag = isOwn ? 'button' : 'div';
    var attrs = isOwn ? ' type="button" data-great-wall-own-entry="1" aria-label="Edit your wall message"' : '';
    var className = 'great-wall-entry' + (isOwn ? ' is-own' : '') + (isOwn && selectedOwnEntry ? ' is-selected' : '');
    var avatarSrc = getGreatWallAvatarSrc(item.user);
    var messageTime = getGreatWallMessageTime(item.timestamp);
    var isoTime = item.timestamp || '';

    return '' +
        '<' + tag + attrs + ' class="' + className + '">' +
            '<img class="great-wall-avatar" src="' + escapeClbiHtml(avatarSrc) + '" alt="" onerror="this.onerror=null;this.src=&quot;/index.php?title=특수:Redirect/file/Pfp-default.png&quot;;">' +
            '<div class="great-wall-bubble">' +
                '<div class="great-wall-entry-head">' +
                    '<span class="great-wall-user">@' + escapeClbiHtml(item.user) + '</span>' +
                    '<span class="great-wall-time" title="' + escapeClbiHtml(isoTime) + '">' + escapeClbiHtml(messageTime) + '</span>' +
                '</div>' +
                '<div class="great-wall-text">' + escapeClbiHtml(item.text) + '</div>' +
            '</div>' +
        '</' + tag + '>';
}

function renderGreatWallBox() {
    var box = document.getElementById('great-wall-sidebar');
    var list = document.getElementById('great-wall-list');
    var input = document.getElementById('great-wall-input');
    var submit = document.getElementById('great-wall-submit');
    var deleteButton = document.getElementById('great-wall-delete');
    var status = document.getElementById('great-wall-status');
    var currentUser = mw.config.get('wgUserName') || '';
    var entries = getGreatWallEntries(greatWallState.data);
    var ownEntry = currentUser && greatWallState.data.entries ? greatWallState.data.entries[currentUser] : null;
    var visibleEntries = [];
    var seen = {};

    if (!box || !list) return;

    box.classList.toggle('is-guest', !currentUser);
    box.classList.toggle('is-loading', !!greatWallState.loading);
    box.classList.toggle('is-saving', !!greatWallState.saving);
    box.classList.toggle('has-own-entry', !!ownEntry);
    box.classList.toggle('is-own-selected', !!greatWallState.selectedOwnEntry);

    entries.forEach(function(item) {
        if (seen[item.user]) return;
        visibleEntries.push(item);
        seen[item.user] = true;
    });

    if (greatWallState.loading && !greatWallState.loaded) {
        list.innerHTML = '<div class="great-wall-empty">SYNCING WALL</div>';
    } else if (!visibleEntries.length) {
        list.innerHTML = '<div class="great-wall-empty">NO MARKS</div>';
    } else {
        list.innerHTML = visibleEntries.map(function(item) {
            return buildGreatWallEntryHtml(item, currentUser, greatWallState.selectedOwnEntry);
        }).join('');
    }

    if (list && list.scrollHeight > list.clientHeight) {
        list.scrollTop = list.scrollHeight;
    }

    if (status) {
        // Status text is intentionally suppressed in the composer strip; the area is spacing-only UI.
        status.textContent = '';
    }

    if (!input || !submit) return;

    input.disabled = false;
    input.readOnly = false;
    input.removeAttribute('aria-readonly');

    if (!currentUser) {
        input.disabled = true;
        input.readOnly = false;
        input.value = '';
        input.placeholder = '담벼락';
        submit.textContent = 'LOGIN';
        submit.disabled = false;
        if (deleteButton) {
            deleteButton.disabled = true;
            deleteButton.title = 'Login required';
        }
        return;
    }

    if (greatWallState.saving || greatWallState.loading) {
        input.disabled = true;
        input.readOnly = false;
        submit.disabled = true;
        submit.textContent = greatWallState.saving ? 'SAVE' : 'SYNC';
        if (deleteButton) {
            deleteButton.disabled = true;
            deleteButton.title = greatWallState.saving ? 'Saving' : 'Syncing';
        }
        return;
    }

    if (deleteButton) {
        deleteButton.disabled = !ownEntry;
        deleteButton.title = ownEntry ? 'Delete your mark' : 'No mark to delete';
    }

    if (ownEntry && !greatWallState.selectedOwnEntry) {
        input.disabled = false;
        input.readOnly = true;
        input.setAttribute('aria-readonly', 'true');
        input.value = '';
        input.placeholder = '담벼락';
        submit.disabled = true;
        submit.textContent = 'UPDATE';
        return;
    }

    input.disabled = false;
    input.readOnly = false;
    input.removeAttribute('aria-readonly');
    input.placeholder = '담벼락';
    if (ownEntry && greatWallState.selectedOwnEntry && !input.value) {
        input.value = ownEntry.text || '';
    }
    submit.disabled = false;
    submit.textContent = ownEntry ? 'UPDATE' : 'POST';
}

function saveGreatWallEntry() {
    var input = document.getElementById('great-wall-input');
    var currentUser = mw.config.get('wgUserName') || '';
    var ownEntry = currentUser && greatWallState.data.entries ? greatWallState.data.entries[currentUser] : null;
    var text = input ? String(input.value || '').trim() : '';
    var api;

    if (!currentUser) {
        window.location.href = '/index.php?title=특수:로그인&returnto=' + encodeURIComponent(mw.config.get('wgPageName') || '대문');
        return;
    }

    if (ownEntry && !greatWallState.selectedOwnEntry) {
        greatWallState.statusText = '';
        renderGreatWallBox();
        return;
    }

    if (!text) {
        greatWallState.statusText = 'EMPTY MARK';
        renderGreatWallBox();
        return;
    }

    if (text.length > 140) {
        text = text.slice(0, 140);
    }

    greatWallState.saving = true;
    greatWallState.statusText = 'SAVING';
    renderGreatWallBox();

    fetchGreatWallData().then(function(data) {
        data = normalizeGreatWallData(data);
        data.entries[currentUser] = {
            user: currentUser,
            text: text,
            timestamp: new Date().toISOString()
        };

        api = new mw.Api();
        return api.postWithToken('csrf', {
            action: 'edit',
            title: GREAT_WALL_DATA_TITLE,
            text: stringifyGreatWallData(data),
            summary: 'Update The Great Wall entry',
            format: 'json'
        }).then(function() {
            greatWallState.data = data;
            greatWallState.loaded = true;
            greatWallState.selectedOwnEntry = false;
            greatWallState.statusText = 'MARK UPDATED';
            if (input) input.value = '';
        });
    }).then(function() {
        greatWallState.saving = false;
        renderGreatWallBox();
    }, function(err) {
        console.error('The Great Wall save failed:', err);
        greatWallState.saving = false;
        greatWallState.statusText = 'SAVE FAILED';
        renderGreatWallBox();
    });
}


function deleteGreatWallEntry() {
    var input = document.getElementById('great-wall-input');
    var currentUser = mw.config.get('wgUserName') || '';
    var ownEntry = currentUser && greatWallState.data.entries ? greatWallState.data.entries[currentUser] : null;
    var api;

    if (!currentUser) {
        window.location.href = '/index.php?title=특수:로그인&returnto=' + encodeURIComponent(mw.config.get('wgPageName') || '대문');
        return;
    }

    if (!ownEntry) {
        greatWallState.statusText = 'NO MARK TO DELETE';
        renderGreatWallBox();
        return;
    }

    greatWallState.saving = true;
    greatWallState.statusText = 'DELETING';
    renderGreatWallBox();

    fetchGreatWallData().then(function(data) {
        data = normalizeGreatWallData(data);
        if (data.entries && data.entries[currentUser]) {
            delete data.entries[currentUser];
        }

        api = new mw.Api();
        return api.postWithToken('csrf', {
            action: 'edit',
            title: GREAT_WALL_DATA_TITLE,
            text: stringifyGreatWallData(data),
            summary: 'Delete The Great Wall entry',
            format: 'json'
        }).then(function() {
            greatWallState.data = data;
            greatWallState.loaded = true;
            greatWallState.selectedOwnEntry = false;
            greatWallState.statusText = 'MARK DELETED';
            if (input) input.value = '';
        });
    }).then(function() {
        greatWallState.saving = false;
        renderGreatWallBox();
    }, function(err) {
        console.error('The Great Wall delete failed:', err);
        greatWallState.saving = false;
        greatWallState.statusText = 'DELETE FAILED';
        renderGreatWallBox();
    });
}

function initGreatWallBoxWhenReady() {
    if (!document.getElementById('great-wall-sidebar')) return;

    if (mw.Api) {
        initGreatWallBox();
        return;
    }

    if (mw.loader && mw.loader.using) {
        mw.loader.using(['mediawiki.api']).then(function() {
            initGreatWallBox();
        });
    }
}

function initGreatWallBox() {
    var box = document.getElementById('great-wall-sidebar');
    var input = document.getElementById('great-wall-input');
    var submit = document.getElementById('great-wall-submit');
    var deleteButton = document.getElementById('great-wall-delete');

    if (!box || box.getAttribute('data-great-wall-ready') === '1') return;

    if (!mw.Api) {
        initGreatWallBoxWhenReady();
        return;
    }

    box.setAttribute('data-great-wall-ready', '1');

    box.addEventListener('click', function(e) {
        var ownButton = e.target.closest ? e.target.closest('[data-great-wall-own-entry="1"]') : null;
        var currentUser = mw.config.get('wgUserName') || '';
        var ownEntry = currentUser && greatWallState.data.entries ? greatWallState.data.entries[currentUser] : null;

        if (!ownButton || !ownEntry) return;

        greatWallState.selectedOwnEntry = true;
        greatWallState.statusText = '';
        renderGreatWallBox();

        if (input) {
            input.focus();
            input.setSelectionRange(input.value.length, input.value.length);
        }
    });

    document.addEventListener('click', function(e) {
        var target = e.target;
        var keepSelection;

        if (!greatWallState.selectedOwnEntry || !target || !target.closest) return;

        keepSelection = target.closest('[data-great-wall-own-entry="1"], .great-wall-editor, .great-wall-compose-sector');

        if (keepSelection) return;

        greatWallState.selectedOwnEntry = false;
        greatWallState.statusText = '';
        if (input) input.value = '';
        renderGreatWallBox();
    });

    if (submit) {
        submit.addEventListener('click', function(e) {
            e.preventDefault();
            if (!mw.config.get('wgUserName')) {
                window.location.href = '/index.php?title=특수:로그인&returnto=' + encodeURIComponent(mw.config.get('wgPageName') || '대문');
                return;
            }
            saveGreatWallEntry();
        });
    }

    if (deleteButton) {
        deleteButton.addEventListener('click', function(e) {
            e.preventDefault();
            deleteGreatWallEntry();
        });
    }

    if (input) {
        input.addEventListener('keydown', function(e) {
            if (e.key === 'Enter') {
                e.preventDefault();
                saveGreatWallEntry();
            }
        });

        input.addEventListener('input', function() {
            if (input.value.length > 140) {
                input.value = input.value.slice(0, 140);
            }
        });
    }

    greatWallState.loading = true;
    greatWallState.statusText = 'SYNCING WALL';
    renderGreatWallBox();

    fetchGreatWallData().then(function(data) {
        greatWallState.data = normalizeGreatWallData(data);
        greatWallState.loaded = true;
        greatWallState.loading = false;
        greatWallState.selectedOwnEntry = false;
        greatWallState.statusText = '';
        renderGreatWallBox();
    });
}

function buildGreatWallBox() {
    return '' +
        '<div id="great-wall-sidebar" class="clbi-right-box great-wall-sidebar">' +
            '<div class="clbi-right-title great-wall-title">' +
                '<span id="clbi-title-great-wall">The Great Wall</span>' +
            '</div>' +
            '<div class="clbi-right-content great-wall-content">' +
                '<div id="great-wall-list" class="great-wall-list"><div class="great-wall-empty">SYNCING WALL</div></div>' +
            '</div>' +
            '<div class="great-wall-compose-sector" aria-label="The Great Wall editor">' +
                '<div class="great-wall-editor">' +
                    '<input id="great-wall-input" class="great-wall-input" type="text" maxlength="140" autocomplete="off" placeholder="담벼락">' +
                    '<button id="great-wall-submit" class="great-wall-submit" type="button">POST</button>' +
                    '<button id="great-wall-delete" class="great-wall-delete" type="button" disabled>DEL</button>' +
                '</div>' +
                '<div id="great-wall-status" class="great-wall-status">SYNCING WALL</div>' +
            '</div>' +
        '</div>';
}

function removeMainPortalGuestbookPreview() {
    var portal = document.querySelector('.main-portal');
    var guestbook = portal ? portal.querySelector('.guestbook-device') : null;
    var sideScreen;
    var grid;

    if (!portal || !guestbook) return;

    sideScreen = guestbook.closest ? guestbook.closest('.side-screen') : null;
    grid = sideScreen && sideScreen.closest ? sideScreen.closest('.console-grid') : null;

    if (guestbook.parentNode) {
        guestbook.parentNode.removeChild(guestbook);
    }

    if (sideScreen && !(sideScreen.textContent || '').trim() && !sideScreen.querySelector('img,svg,video,canvas,form,input,button,a')) {
        if (sideScreen.parentNode) {
            sideScreen.parentNode.removeChild(sideScreen);
        }

        if (grid) {
            grid.style.gridTemplateColumns = 'minmax(0,1fr)';
        }
    }

    portal.classList.add('is-great-wall-relocated');
}

function buildSiteInformationBox() {
    return '' +
        '<div class="clbi-right-box site-info-sidebar">' +
            '<div class="clbi-right-title site-info-title">' +
                '<span>정보</span>' +
            '</div>' +
            '<div class="clbi-right-content site-info-content">' +
                '<div class="policy-list">' +
                    '<div class="policy-row"><a href="/index.php/개인정보처리방침" class="site-info-policy-button"><span class="site-info-policy-title">개인정보처리방침</span><span class="site-info-policy-arrow" aria-hidden="true">›</span></a></div>' +
                    '<div class="policy-row"><a href="/index.php/면책_조항" class="site-info-policy-button"><span class="site-info-policy-title">면책 조항</span><span class="site-info-policy-arrow" aria-hidden="true">›</span></a></div>' +
                    '<div class="policy-row"><a href="/index.php/라이선스" class="site-info-policy-button"><span class="site-info-policy-title">라이선스</span><span class="site-info-policy-arrow" aria-hidden="true">›</span></a></div>' +
                    '<div class="policy-row"><a href="/index.php/크레딧" class="site-info-policy-button"><span class="site-info-policy-title">크레딧</span><span class="site-info-policy-arrow" aria-hidden="true">›</span></a></div>' +
                '</div>' +
                '<div class="social-strip">' +
                    '<span class="social-icon"><a href="https://discord.gg/ctaeJ9d3Q5" target="_blank" rel="noopener noreferrer">DC</a></span>' +
                    '<span class="social-icon"><a href="https://www.youtube.com/@nxdsxn" target="_blank" rel="noopener noreferrer">YT</a></span>' +
                    '<span class="social-icon"><a href="https://x.com/nxd_sxn" target="_blank" rel="noopener noreferrer">X</a></span>' +
                    '<span class="social-icon"><a href="/index.php/프로젝트:소개">WIP:</a></span>' +
                '</div>' +
            '</div>' +
        '</div>';
}


// 초기화 함수
function initSidebars() {
    var header = $('.liberty-content-header');
    var content = $('.liberty-content');

    if (header.length && content.length) {
        header.prependTo(content);
    }

    if ($('#clbi-right-sidebar').length === 0) {
        var username = mw.config.get('wgUserName');
        var isLoggedIn = username !== null;
        var avatarSrc = isLoggedIn
            ? '/index.php?title=특수:Redirect/file/Pfp-' + username + '.png'
            : '/index.php?title=특수:Redirect/file/Pfp-default.png';

        var userBox;

        if (isLoggedIn) {
            userBox =
                '<div class="clbi-right-box profile-card-box">' +
                    '<div id="clbi-user-avatar-wrap" class="profile-identity-panel">' +
                        '<div class="profile-avatar-stage">' +
                            '<img id="clbi-user-avatar" src="' + avatarSrc + '" onerror="this.onerror=null;this.src=\'/index.php?title=특수:Redirect/file/Pfp-default.png\';">' +
                        '</div>' +
                        '<div id="clbi-user-name-row" class="profile-name-row">' +
                            '<a href="/index.php/사용자:' + username + '" id="clbi-user-name">' + username + '</a>' +
                        '</div>' +
                    '</div>' +
                    '<div class="clbi-right-content profile-action-box">' +
                        '<div class="profile-quick-actions" aria-label="프로필 빠른 메뉴">' +
                            '<button type="button" class="profile-quick-btn" id="profile-quick-inventory" aria-label="인벤토리"><span class="profile-quick-icon" aria-hidden="true">' + CLBI_SVG_PACKAGE + '</span><span class="profile-quick-tip" aria-hidden="true">인벤토리</span></button>' +
                            '<button type="button" class="profile-quick-btn" id="profile-quick-achievements" aria-label="업적"><span class="profile-quick-icon" aria-hidden="true">' + CLBI_SVG_TROPHY + '</span><span class="profile-quick-tip" aria-hidden="true">업적</span></button>' +
                            '<button type="button" class="profile-quick-btn" id="profile-quick-notifications" aria-label="알림"><span id="profile-quick-notification-icon" class="profile-quick-icon" aria-hidden="true">' + CLBI_SVG_BELL + '</span><span class="profile-quick-tip" aria-hidden="true">알림</span></button>' +
                        '</div>' +
                        '<a href="/index.php/특수:기여/' + username + '" class="clbi-user-btn" id="clbi-btn-contribution"><span class="profile-action-icon" aria-hidden="true">' + CLBI_SVG_SCAN_TEXT + '</span><span class="profile-action-label">기여</span><i class="hn hn-angle-right-solid profile-action-arrow" aria-hidden="true"></i></a>' +
                        '<a href="/index.php/특수:주시문서목록" class="clbi-user-btn" id="clbi-btn-watchlist"><span class="profile-action-icon" aria-hidden="true">' + CLBI_SVG_SCAN_EYE + '</span><span class="profile-action-label">주시문서 목록</span><i class="hn hn-angle-right-solid profile-action-arrow" aria-hidden="true"></i></a>' +
                        '<a href="/index.php/특수:설정" class="clbi-user-btn" id="clbi-btn-preferences"><span class="profile-action-icon" aria-hidden="true">' + CLBI_SVG_SETTINGS + '</span><span class="profile-action-label">설정</span><i class="hn hn-angle-right-solid profile-action-arrow" aria-hidden="true"></i></a>' +
                        '<a href="/index.php?title=특수:로그아웃&returnto=대문" class="clbi-user-btn clbi-user-btn-logout" id="clbi-btn-logout"><span class="profile-action-icon" aria-hidden="true">' + CLBI_SVG_POWER + '</span><span class="profile-action-label">로그아웃</span><i class="hn hn-angle-right-solid profile-action-arrow" aria-hidden="true"></i></a>' +
                    '</div>' +
                '</div>';
        } else {
            userBox =
                '<div class="clbi-right-box profile-card-box">' +
                    '<div id="clbi-user-avatar-wrap" class="profile-identity-panel">' +
                        '<div class="profile-avatar-stage">' +
                            '<img id="clbi-user-avatar" src="/index.php?title=특수:Redirect/file/Pfp-default.png">' +
                        '</div>' +
                        '<div id="clbi-user-name-row" class="profile-name-row profile-name-row-guest">' +
                            '<span id="clbi-user-name">Guest</span>' +
                        '</div>' +
                    '</div>' +
                    '<div class="clbi-right-content profile-action-box">' +
                        '<a href="/index.php?title=특수:로그인&returnto=대문" class="clbi-user-btn" id="clbi-btn-login"><span class="profile-action-icon" aria-hidden="true">' + CLBI_SVG_POWER + '</span><span class="profile-action-label">로그인</span><i class="hn hn-angle-right-solid profile-action-arrow" aria-hidden="true"></i></a>' +
                    '</div>' +
                '</div>';
        }
        var greatWallBox = '';
        var siteInformationBox = '';

        try {
            greatWallBox = buildGreatWallBox();
        } catch (err) {
            console.error('The Great Wall build failed:', err);
            greatWallBox = '';
        }

        try {
            siteInformationBox = buildSiteInformationBox();
        } catch (err) {
            console.error('Site information build failed:', err);
            siteInformationBox = '';
        }


        var sidebar = userBox + greatWallBox + siteInformationBox;

        $('.content-wrapper').append('<div id="clbi-right-sidebar">' + sidebar + '</div>');
        initGreatWallBoxWhenReady();
        removeMainPortalGuestbookPreview();
    }

    initGreatWallBoxWhenReady();
    removeMainPortalGuestbookPreview();


    if ($('#clbi-left-sidebar').length === 0) {
var leftBillboardBox = '';

        try {
            leftBillboardBox = buildRightBillboardBox();
        } catch (err) {
            console.error('Left billboard build failed:', err);
            leftBillboardBox = '';
        }

var leftSidebar =
    '<div id="clbi-left-sidebar">' +
        '<div class="clbi-left-box clbi-left-lang-box">' +
            '<div class="clbi-left-title">' +
                '<span id="clbi-title-left-language">언어</span>' +
            '</div>' +
            '<div class="clbi-left-content sidebar-lang-box">' +
                '<div id="clbi-sidebar-lang-selector" class="sidebar-lang-selector sidebar-lang-dial" tabindex="0" role="group" aria-label="언어 선택">' +
                    '<div id="clbi-sidebar-lang-dial-stage" class="sidebar-lang-dial-stage">' +
                        '<div id="clbi-sidebar-lang-fan" class="sidebar-lang-fan" aria-hidden="true"></div>' +
                        '<div id="clbi-sidebar-lang-selected-panel" class="sidebar-lang-status-panel sidebar-lang-status-left" aria-hidden="true">' +
                            '<span id="clbi-sidebar-lang-selected-value" class="sidebar-lang-status-value">한국어</span>' +
                        '</div>' +
                        '<div id="clbi-sidebar-lang-availability-panel" class="sidebar-lang-status-panel sidebar-lang-status-right is-current" aria-hidden="true">' +
                            '<span id="clbi-sidebar-lang-availability-value" class="sidebar-lang-status-value">CURRENT</span>' +
                        '</div>' +
                        '<button type="button" id="clbi-sidebar-lang-apply" class="sidebar-lang-apply" aria-label="언어 적용">' +
                            '<span class="sidebar-lang-apply-mark" aria-hidden="true">✓</span>' +
                        '</button>' +
                    '</div>' +
                '</div>' +
            '</div>' +
        '</div>' +
        '<div class="clbi-left-box clbi-left-news-box">' +
            '<div class="clbi-left-title">' +
                '<span id="clbi-title-left-news">뉴스</span>' +
            '</div>' +
            '<div class="clbi-left-content clbi-news-box">' +

                '<div class="news-feed-title" id="clbi-left-news-changelog-title">CHANGELOG</div>' +
                '<div class="news-left-changelog-feed">' +
                    '<a href="/index.php/체인지로그" class="news-post-item">' +
                        '<div class="news-post-title-wrap">' +
                            '<span class="news-post-title" id="clbi-left-news-changelog-main">체인지로그</span>' +
                        '</div>' +
                        '<span class="news-post-jump" aria-hidden="true">›</span>' +
                    '</a>' +
                '</div>' +

                '<div class="news-divider"></div>' +

                '<div class="news-feed-title" id="clbi-left-news-recent-title">RECENT CHANGES</div>' +
                '<div class="news-left-recent-feed" id="clbi-left-recent-list">불러오는 중...</div>' +

                '<a class="news-fill-image-slot" id="clbi-left-news-fill-image" href="/index.php/특수:최근바뀜" aria-label="최근 바뀜으로 이동">' +
                    '<div class="news-fill-image-frame">' +
                        '<span class="news-fill-image" style="--news-fill-image-url:url(\'/index.php?title=특수:Redirect/file/Side-news-fill-001.png\');" aria-hidden="true"></span><img class="news-fill-image-probe" src="/index.php?title=특수:Redirect/file/Side-news-fill-001.png" alt="" aria-hidden="true" onerror="this.onerror=null;this.closest(\'.news-fill-image-slot\').classList.add(\'is-empty\');this.remove();">' +
                    '</div>' +
                '</a>' +

            '</div>' +
        '</div>' +
        leftBillboardBox +
    '</div>';

        $('.content-wrapper').prepend(leftSidebar);

        renderSidebarLanguageBox();
        loadRecentChangesList('#clbi-left-recent-list', 10);
        scheduleAdaptiveLeftRecentItems();
        scheduleLeftBillboardAdaptive();
        scheduleClbiContentBottomGap();
        updateLeftSidebarNationsImage();
    }

    try {
        initRightBillboardCarousel();
    } catch (err) {
        console.error('Right billboard carousel failed:', err);
    }

    if (typeof window.normalizeClbiShellDomOrder === 'function') window.normalizeClbiShellDomOrder();
    applyMainPageStyle();
    initClbiCustomDocumentScrollbars();
    initCategoryNavIfAvailable(document);

    if (window.ProgressSystemWebUi && typeof window.ProgressSystemWebUi.boot === 'function') {
        window.ProgressSystemWebUi.boot('initSidebars');
    }

    $('#side-toc-box').remove();

    mw.loader.using(['mediawiki.api']).then(function() {
        setTimeout(function() {
            initNotifications();
            initProfile();
            moveTocToLeftSidebar();
        }, 300);

        setTimeout(moveTocToLeftSidebar, 800);
        setTimeout(moveTocToLeftSidebar, 1500);
    });
}

$(function() {
    loadLangScript(function() {
        setTimeout(function() {
            initSidebars();
        }, 100);
    });
});

$(document).on('click.profileQuickPlaceholder', '#profile-quick-inventory, #profile-quick-achievements', function(e) {
    e.preventDefault();
    e.stopPropagation();
});

function extractJsonArrayAfterMwConfigKey(text, key) {
    var needle = '"' + key + '"';
    var keyIndex = String(text || '').indexOf(needle);
    var start;
    var i;
    var depth = 0;
    var inString = false;
    var escaped = false;

    if (keyIndex === -1) return null;

    start = String(text || '').indexOf('[', keyIndex + needle.length);
    if (start === -1) return null;

    for (i = start; i < text.length; i += 1) {
        var ch = text.charAt(i);

        if (inString) {
            if (escaped) {
                escaped = false;
            } else if (ch === '\\') {
                escaped = true;
            } else if (ch === '"') {
                inString = false;
            }
            continue;
        }

        if (ch === '"') {
            inString = true;
            continue;
        }

        if (ch === '[') depth += 1;
        if (ch === ']') {
            depth -= 1;
            if (depth === 0) {
                try {
                    return JSON.parse(text.slice(start, i + 1));
                } catch (err) {
                    return null;
                }
            }
        }
    }

    return null;
}

function extractJsonStringAfterMwConfigKey(text, key) {
    var needle = '"' + key + '"';
    var keyIndex = String(text || '').indexOf(needle);
    var colon;
    var start;
    var i;
    var escaped = false;

    if (keyIndex === -1) return null;

    colon = text.indexOf(':', keyIndex + needle.length);
    if (colon === -1) return null;

    start = text.indexOf('"', colon + 1);
    if (start === -1) return null;

    for (i = start + 1; i < text.length; i += 1) {
        var ch = text.charAt(i);

        if (escaped) {
            escaped = false;
            continue;
        }

        if (ch === '\\') {
            escaped = true;
            continue;
        }

        if (ch === '"') {
            try {
                return JSON.parse(text.slice(start, i + 1));
            } catch (err) {
                return text.slice(start + 1, i);
            }
        }
    }

    return null;
}

function syncCatlinksConfigFromSpaDocument(doc) {
    var scripts = doc ? doc.querySelectorAll('script') : [];
    var categories = null;
    var hiddenCategories = null;
    var relevantPageName = null;
    var pageName = null;
    var i;
    var text;
    var value;

    for (i = 0; i < scripts.length; i += 1) {
        text = scripts[i].textContent || '';

        if (categories === null) {
            value = extractJsonArrayAfterMwConfigKey(text, 'wgCategories');
            if (Array.isArray(value)) categories = value;
        }

        if (hiddenCategories === null) {
            value = extractJsonArrayAfterMwConfigKey(text, 'wgHiddenCategories');
            if (Array.isArray(value)) hiddenCategories = value;
        }

        if (relevantPageName === null) {
            value = extractJsonStringAfterMwConfigKey(text, 'wgRelevantPageName');
            if (value !== null) relevantPageName = value;
        }

        if (pageName === null) {
            value = extractJsonStringAfterMwConfigKey(text, 'wgPageName');
            if (value !== null) pageName = value;
        }
    }

    mw.config.set('wgCategories', Array.isArray(categories) ? categories : []);
    mw.config.set('wgHiddenCategories', Array.isArray(hiddenCategories) ? hiddenCategories : []);

    if (relevantPageName !== null) {
        mw.config.set('wgRelevantPageName', relevantPageName);
    } else if (pageName !== null) {
        mw.config.set('wgRelevantPageName', pageName);
    }

    CLBI_CATLINKS_FETCH_TOKEN += 1;
}

// SPA 네비게이션
function shouldSkip(url) {
    return url.match(/action=edit|action=submit|action=history|action=delete|action=protect|action=purge|특수:로그인|특수:로그아웃|Special:UserLogin|Special:UserLogout|특수:사용자정보|특수:비밀번호바꾸기|uselang=/);
}

$(function() {
    if (window._spaInitialized) return;
    window._spaInitialized = true;

    function isInternal(url) {
        var a = document.createElement('a');
        a.href = url;
        return a.hostname === window.location.hostname;
    }

    function getCachedSpaPageHtml(url) {
        if (!window.EntryStore || typeof window.EntryStore.getTextSync !== 'function') return '';
        return window.EntryStore.getTextSync(url) || window.EntryStore.getTextSync(String(url || '').replace(/^https?:\/\/[^/]+/i, '')) || '';
    }

    function fetchSpaPageHtml(url) {
        var cached = getCachedSpaPageHtml(url);
        if (cached) return Promise.resolve(cached);
        return fetch(url, { credentials: 'same-origin', cache: 'force-cache' }).then(function(res) {
            return res.text();
        });
    }

    function prepareDetachedEntryContent(newContent) {
        /*
        Initial boot prepares entry artifacts; SPA is only allowed to consume them.
        This hook runs while the fetched page is still detached, before the user sees it.
        It must stay synchronous or already-resolved: if a subsystem cannot prepare from
        EntryStore immediately, it should leave the old fallback path in place instead of
        opening BootGate during SPA.
        */
        try {
            if (window.NationsPanel && typeof window.NationsPanel.prepareContentForEntry === 'function') {
                window.NationsPanel.prepareContentForEntry(newContent);
            }
        } catch (err) {
            console.warn('entry content preparation failed:', err);
        }
    }

    function loadPage(url) {
        invalidateProfileRender();

        return fetchSpaPageHtml(url)
            .then(function(html) {
                var parser = new DOMParser();
                var doc = parser.parseFromString(html, 'text/html');

                var scripts = doc.querySelectorAll('script');
                for (var i = 0; i < scripts.length; i++) {
                    var src = scripts[i].textContent;

                    if (src.indexOf('wgNamespaceNumber') !== -1) {
                        var match = src.match(/"wgNamespaceNumber":(-?\d+)/);
                        if (match) mw.config.set('wgNamespaceNumber', parseInt(match[1], 10));

                        var matchTitle = src.match(/"wgTitle":"([^"]+)"/);
                        if (matchTitle) mw.config.set('wgTitle', matchTitle[1]);

                        var matchPage = src.match(/"wgPageName":"([^"]+)"/);
                        if (matchPage) mw.config.set('wgPageName', matchPage[1]);

                        var matchArticle = src.match(/"wgArticleId":(\d+)/);
                        if (matchArticle) {
                            mw.config.set('wgArticleId', parseInt(matchArticle[1], 10));
                        } else {
                            mw.config.set('wgArticleId', 0);
                        }

                        var matchIsMainPage = src.match(/"wgIsMainPage":(true|false)/);
                        if (matchIsMainPage) {
                            mw.config.set('wgIsMainPage', matchIsMainPage[1] === 'true');
                        } else {
                            mw.config.set('wgIsMainPage', false);
                        }

                        var matchSpecial = src.match(/"wgCanonicalSpecialPageName":"([^"]+)"/);
                        if (matchSpecial) {
                            mw.config.set('wgCanonicalSpecialPageName', matchSpecial[1]);
                        } else {
                            mw.config.set('wgCanonicalSpecialPageName', false);
                        }
                        break;
                    }
                }

                syncCatlinksConfigFromSpaDocument(doc);

                var newContent = doc.querySelector('.liberty-content-main');
                var newTitle = doc.querySelector('.mw-page-title-main');
                var newHead = doc.querySelector('title');
                var newHeader = doc.querySelector('.liberty-content-header');

                if (newContent) {
                    prepareDetachedEntryContent(newContent);
                    prepareSpaCatlinksBeforeInsert(newContent);
                    $('#side-toc-box').remove();
                    $('.profile-card').remove();
                    $('.user-profile-portal').removeClass('user-profile-portal');
                    $('.liberty-content-main').html(newContent.innerHTML);
                    $('.profile-card').remove();
                    try {
                        if (window.Decorations && typeof window.Decorations.renderPrepared === 'function') window.Decorations.renderPrepared();
                        else if (window.CLBI_DECORATIONS && typeof window.CLBI_DECORATIONS.renderPrepared === 'function') window.CLBI_DECORATIONS.renderPrepared();
                    } catch (err) {}
                    $('body').removeClass('page-loading');
                }

                if (newTitle) {
                    $('.mw-page-title-main').html(newTitle.innerHTML);
                }

                if (newHead) {
                    document.title = newHead.textContent;
                }

                if (newHeader) {
                    $('.liberty-content-header').html(newHeader.innerHTML);
                }

                if (typeof window.normalizeClbiShellDomOrder === 'function') window.normalizeClbiShellDomOrder();
                window.scrollTo(0, 0);
                mw.hook('wikipage.content').fire($('.liberty-content-main'));
                applyMainPageStyle();
                initClbiCustomDocumentScrollbars();
                initCategoryNavIfAvailable(document);

                if (window.ProgressSystemWebUi && typeof window.ProgressSystemWebUi.handleSpaPageView === 'function') {
                    window.ProgressSystemWebUi.handleSpaPageView();
                } else if (window.ProgressSystemWebUi && typeof window.ProgressSystemWebUi.boot === 'function') {
                    window.ProgressSystemWebUi.boot('spa');
                }

                $('#side-toc-box').remove();
                setTimeout(moveTocToLeftSidebar, 100);
                setTimeout(moveTocToLeftSidebar, 500);
                setTimeout(moveTocToLeftSidebar, 1200);

                mw.loader.using(['mediawiki.api']).then(function() {
                    initProfile();
                    moveTocToLeftSidebar();
                });
            })
            .catch(function (err) {
                console.error('SPA page load failed:', err);
                $('body').removeClass('page-loading');
            });
    }

// 목차 링크는 전용 처리
$(document).on('click', '#side-toc-box a, #toc a, .toc a', function(e) {
    var href = $(this).attr('href');
    if (!href || href.charAt(0) !== '#') return;

    var rawId = href.slice(1);
    if (!rawId) return;

    var decodedId = rawId;

    try {
        decodedId = decodeURIComponent(rawId);
    } catch (err) {
        decodedId = rawId;
    }

    var target = document.getElementById(decodedId);

    if (!target && window.CSS && CSS.escape) {
        target = document.querySelector('#' + CSS.escape(decodedId));
    }

    if (!target) return;

    e.preventDefault();
    e.stopPropagation();

    var scrollTarget = target.closest('h2, h3') || target;

    scrollTarget.scrollIntoView({
        behavior: 'auto',
        block: 'start'
    });

    history.replaceState(null, '', '#' + rawId);
});

    /* 길라잡이는 문서 링크가 아니라 대문 본문 화면 탭이다. */
    $(document).on('click', '.portal-guide-anchor[data-category-key="guide"]', function(e) {
        if (e.which && e.which !== 1) return;
        if (e.button && e.button !== 0) return;
        if (e.ctrlKey || e.metaKey || e.shiftKey || e.altKey) return;

        if (window.BottomGuideNav && typeof window.BottomGuideNav.toggle === 'function') {
            e.preventDefault();
            e.stopImmediatePropagation();
            window.BottomGuideNav.toggle();
        }
    });

    $(document).on('click', 'a', function(e) {
        // 휠 클릭, 새 탭 열기, 보조키 이동은 브라우저 기본 동작을 유지한다.
        if (e.which && e.which !== 1) return;
        if (e.button && e.button !== 0) return;
        if (e.ctrlKey || e.metaKey || e.shiftKey || e.altKey) return;

        /* 길라잡이 화면 탭은 위 전용 처리 외에는 SPA 문서 이동 대상으로 삼지 않는다. */
        if ($(this).is('.portal-guide-anchor[data-category-key="guide"]')) return;

        var href = $(this).attr('href');
        if (!href) return;

        // 목차 링크는 별도 핸들러에서 처리
        if ($(this).closest('#side-toc-box, #toc, .toc').length) return;

        // 단순 해시 링크는 SPA 가로채기 제외
        if (href.startsWith('#')) return;

        var link = document.createElement('a');
        link.href = href;

        var samePath = decodeURIComponent(link.pathname) === decodeURIComponent(window.location.pathname);
        var sameSearch = (link.search || '') === (window.location.search || '');

        if (link.hash && samePath && sameSearch) return;

        var currentBase = window.location.href.split('#')[0];
        var targetBase = link.href.split('#')[0];

        if (link.hash && currentBase === targetBase) return;

        if (!isInternal(href)) return;
        if (shouldSkip(href)) return;

        e.preventDefault();
        playStaticSound();
        /*
        SPA must remain a consumer phase.  If the target page HTML was prepared by
        the first-load boot pack, do not show the legacy page-loading veil.  The
        route will consume cached HTML and detached entry artifacts before insertion.
        */
        if (getCachedSpaPageHtml(href)) {
            $('body').removeClass('page-loading');
        } else {
            $('body').addClass('page-loading');
        }
        history.pushState(null, '', href);
        loadPage(href);
    });

    window.addEventListener('popstate', function() {
        loadPage(window.location.href);
    });
});



/* ========== CLBI Custom Document Scrollbar ========== */
function isGeneralDocumentView() {
    var body = document.body;
    if (!body) return false;

    return body.classList.contains('action-view') &&
        !body.classList.contains('clbi-main-page') &&
        !body.classList.contains('clbi-system-doc-page') &&
        !body.classList.contains('backend-system-page') &&
        !body.classList.contains('user-profile-page') &&
        !body.classList.contains('user-profile-settings-page');
}

function getClbiDocumentScrollTargets() {
    if (!isGeneralDocumentView()) return [];

    return Array.prototype.slice.call(document.querySelectorAll(
        '.liberty-content-main > #mw-content-text .mw-parser-output, ' +
        '.liberty-content-main > .mw-body-content .mw-parser-output'
    )).filter(function (el, index, list) {
        return el && list.indexOf(el) === index;
    });
}

function getClbiOuterWellForScroll(scrollEl) {
    var main = scrollEl ? scrollEl.closest('.liberty-content-main') : null;
    var children;
    var i;
    var child;

    if (!main) return null;

    children = Array.prototype.slice.call(main.children || []);
    for (i = 0; i < children.length; i += 1) {
        child = children[i];
        if (
            child &&
            (child.id === 'mw-content-text' || child.classList.contains('mw-body-content')) &&
            child.contains(scrollEl)
        ) {
            return child;
        }
    }

    return scrollEl.parentElement || null;
}

function buildClbiCustomScrollbar(well, scrollEl) {
    var bar = well.querySelector(':scope > .clbi-custom-scrollbar');
    var up;
    var track;
    var thumb;
    var down;

    if (!bar) {
        bar = document.createElement('div');
        bar.className = 'clbi-custom-scrollbar';
        bar.setAttribute('aria-hidden', 'true');
        bar.innerHTML =
            '<div class="clbi-custom-scroll-arrow clbi-custom-scroll-arrow-up" data-scroll-arrow="up"></div>' +
            '<div class="clbi-custom-scroll-track"><div class="clbi-custom-scroll-thumb"></div></div>' +
            '<div class="clbi-custom-scroll-arrow clbi-custom-scroll-arrow-down" data-scroll-arrow="down"></div>';
        well.appendChild(bar);
    }

    bar.__clbiScrollTarget = scrollEl;
    up = bar.querySelector('.clbi-custom-scroll-arrow-up');
    track = bar.querySelector('.clbi-custom-scroll-track');
    thumb = bar.querySelector('.clbi-custom-scroll-thumb');
    down = bar.querySelector('.clbi-custom-scroll-arrow-down');

    if (up && !up.__clbiBound) {
        up.__clbiBound = true;
        up.addEventListener('mousedown', function (e) {
            e.preventDefault();
            e.stopPropagation();
            if (bar.__clbiScrollTarget) bar.__clbiScrollTarget.scrollTop -= 48;
            updateClbiCustomScrollbar(bar);
        });
    }

    if (down && !down.__clbiBound) {
        down.__clbiBound = true;
        down.addEventListener('mousedown', function (e) {
            e.preventDefault();
            e.stopPropagation();
            if (bar.__clbiScrollTarget) bar.__clbiScrollTarget.scrollTop += 48;
            updateClbiCustomScrollbar(bar);
        });
    }

    if (track && !track.__clbiBound) {
        track.__clbiBound = true;
        track.addEventListener('mousedown', function (e) {
            var rect;
            var thumbRect;
            var target;
            var direction;

            if (e.target === thumb) return;
            e.preventDefault();
            e.stopPropagation();

            target = bar.__clbiScrollTarget;
            if (!target) return;

            rect = track.getBoundingClientRect();
            thumbRect = thumb.getBoundingClientRect();
            direction = e.clientY < thumbRect.top ? -1 : 1;
            target.scrollTop += direction * Math.max(60, Math.floor(target.clientHeight * 0.82));
            updateClbiCustomScrollbar(bar);
        });
    }

    if (thumb && !thumb.__clbiBound) {
        thumb.__clbiBound = true;
        thumb.addEventListener('mousedown', function (e) {
            var target = bar.__clbiScrollTarget;
            var startY;
            var startScroll;
            var maxScroll;
            var maxThumbTop;
            var trackHeight;
            var thumbHeight;

            if (!target) return;

            e.preventDefault();
            e.stopPropagation();

            startY = e.clientY;
            startScroll = target.scrollTop;
            maxScroll = Math.max(1, target.scrollHeight - target.clientHeight);
            trackHeight = track ? track.clientHeight : 0;
            thumbHeight = thumb.offsetHeight || 0;
            maxThumbTop = Math.max(1, trackHeight - thumbHeight);

            bar.classList.add('is-dragging');

            function onMove(moveEvent) {
                var dy = moveEvent.clientY - startY;
                target.scrollTop = startScroll + (dy / maxThumbTop) * maxScroll;
                updateClbiCustomScrollbar(bar);
                moveEvent.preventDefault();
            }

            function onUp() {
                bar.classList.remove('is-dragging');
                document.removeEventListener('mousemove', onMove);
                document.removeEventListener('mouseup', onUp);
            }

            document.addEventListener('mousemove', onMove);
            document.addEventListener('mouseup', onUp);
        });
    }

    if (!scrollEl.__clbiCustomScrollbarBound) {
        scrollEl.__clbiCustomScrollbarBound = true;
        scrollEl.addEventListener('scroll', function () {
            if (scrollEl.__clbiCustomScrollbar) {
                updateClbiCustomScrollbar(scrollEl.__clbiCustomScrollbar);
            }
        }, { passive: true });
    }

    scrollEl.__clbiCustomScrollbar = bar;
    updateClbiCustomScrollbar(bar);

    return bar;
}

function updateClbiCustomScrollbar(bar) {
    var scrollEl = bar && bar.__clbiScrollTarget;
    var track = bar ? bar.querySelector('.clbi-custom-scroll-track') : null;
    var thumb = bar ? bar.querySelector('.clbi-custom-scroll-thumb') : null;
    var maxScroll;
    var trackHeight;
    var thumbHeight;
    var maxTop;
    var top;

    if (!bar || !scrollEl || !track || !thumb) return;

    maxScroll = scrollEl.scrollHeight - scrollEl.clientHeight;
    if (maxScroll <= 1) {
        bar.classList.add('is-hidden');
        return;
    }

    bar.classList.remove('is-hidden');

    trackHeight = Math.max(1, track.clientHeight || 1);
    thumbHeight = Math.max(12, Math.floor((scrollEl.clientHeight / Math.max(scrollEl.scrollHeight, 1)) * trackHeight));
    thumbHeight = Math.min(trackHeight, thumbHeight);
    maxTop = Math.max(0, trackHeight - thumbHeight);
    top = maxScroll > 0 ? Math.round((scrollEl.scrollTop / maxScroll) * maxTop) : 0;

    thumb.style.height = thumbHeight + 'px';
    thumb.style.transform = 'translateY(' + top + 'px)';
}

function initClbiCustomDocumentScrollbars() {
    var existing = Array.prototype.slice.call(document.querySelectorAll('.clbi-custom-scrollbar'));
    var targets = getClbiDocumentScrollTargets();
    var liveBars = [];

    if (!targets.length) {
        existing.forEach(function (bar) { bar.remove(); });
        return;
    }

    targets.forEach(function (scrollEl) {
        var well = getClbiOuterWellForScroll(scrollEl);
        var bar;

        if (!well) return;
        bar = buildClbiCustomScrollbar(well, scrollEl);
        liveBars.push(bar);
    });

    existing.forEach(function (bar) {
        if (liveBars.indexOf(bar) === -1) bar.remove();
    });

    window.requestAnimationFrame(function () {
        liveBars.forEach(updateClbiCustomScrollbar);
    });

    setTimeout(function () {
        liveBars.forEach(updateClbiCustomScrollbar);
    }, 120);
}

if (!window.__clbiCustomScrollbarResizeBound) {
    window.__clbiCustomScrollbarResizeBound = true;
    window.addEventListener('resize', function () {
        setTimeout(initClbiCustomDocumentScrollbars, 60);
    });
}


// 시간 계산 함수
function timeAgo(timestamp) {
    var now = new Date();
    var date = new Date(timestamp);
    var diff = Math.floor((now - date) / 1000);

    if (diff < 60) return diff + '초 전';
    if (diff < 3600) return Math.floor(diff / 60) + '분 전';
    if (diff < 86400) return Math.floor(diff / 3600) + '시간 전';
    return Math.floor(diff / 86400) + '일 전';
}

// 펼접 토글
// 펼접 토글
function getFoldTexts() {
    var lang = getCurrentLang();
    return (window.LANG && window.LANG[lang])
        ? window.LANG[lang]
        : (window.LANG ? window.LANG.ko : { expand: '펼치기', collapse: '접기' });
}

function refreshOpenAncestors($start) {
    $start.parents('[id^="collapsible"]').each(function () {
        var $parent = $(this);
        if (!$parent.hasClass('folding-open')) return;

        // 이미 fully open 상태면 굳이 다시 잠그지 않음
        if ($parent.data('fold-state') === 'open') {
            return;
        }

        $parent.css('max-height', this.scrollHeight + 'px');
    });
}

function bindInnerResizeUpdates($target) {
    // 이미지 늦게 로드될 때 높이 갱신
    $target.find('img').off('.foldimg').on('load.foldimg', function () {
        if ($target.hasClass('folding-open')) {
            if ($target.data('fold-state') !== 'open') {
                $target.css('max-height', $target[0].scrollHeight + 'px');
            }
            refreshOpenAncestors($target);
        }
    });
}

function openFold($target, $btn) {
    var t = getFoldTexts();

    $target.data('fold-state', 'opening');
    $target.addClass('folding-open');

    // 열린 뒤 자연 확장 가능하게 만들기 위해 먼저 px로 열기
    $target.css('max-height', '0px');
    $target[0].offsetHeight;
    $target.css('max-height', $target[0].scrollHeight + 'px');

    $btn.text(t.collapse);

    bindInnerResizeUpdates($target);

    // 바깥 펼접 즉시 갱신
    refreshOpenAncestors($target);

    // 전환 끝나면 none으로 풀어서 중첩 펼접/동적 내용 증가를 자연스럽게 허용
    $target.off('transitionend.foldopen').on('transitionend.foldopen', function (e) {
        if (e.target !== this) return;
        if (!$target.hasClass('folding-open')) return;

        $target.css('max-height', 'none');
        $target.data('fold-state', 'open');

        refreshOpenAncestors($target);
    });

    // 늦게 렌더되는 콘텐츠 대응
    requestAnimationFrame(function () {
        if ($target.hasClass('folding-open') && $target.data('fold-state') !== 'open') {
            $target.css('max-height', $target[0].scrollHeight + 'px');
            refreshOpenAncestors($target);
        }
    });

    setTimeout(function () {
        if ($target.hasClass('folding-open') && $target.data('fold-state') !== 'open') {
            $target.css('max-height', $target[0].scrollHeight + 'px');
            refreshOpenAncestors($target);
        }
    }, 80);

    setTimeout(function () {
        if ($target.hasClass('folding-open') && $target.data('fold-state') !== 'open') {
            $target.css('max-height', $target[0].scrollHeight + 'px');
            refreshOpenAncestors($target);
        }
    }, 220);
}

function closeFold($target, $btn) {
    var t = getFoldTexts();

    // none 상태에서 닫으면 transition이 안 되므로 실제 높이로 고정
    if ($target.css('max-height') === 'none' || $target.data('fold-state') === 'open') {
        $target.css('max-height', $target[0].scrollHeight + 'px');
    } else {
        $target.css('max-height', $target[0].scrollHeight + 'px');
    }

    $target.data('fold-state', 'closing');
    $target[0].offsetHeight;
    $target.css('max-height', '0px');
    $target.removeClass('folding-open');

    $btn.text(t.expand);

    refreshOpenAncestors($target);

    setTimeout(function () {
        refreshOpenAncestors($target);
        $target.data('fold-state', 'closed');
    }, 250);
}

$(function () {
    $(document)
        .off('click.clbiToggle')
        .on('click.clbiToggle', '.toggleBtn', function () {
            var $btn = $(this);
            var targetId = $btn.data('target');
            var $target = $('#' + targetId);
            if (!$target.length) return;

            var scrollY = window.scrollY;

            if ($target.hasClass('folding-open')) {
                closeFold($target, $btn);
            } else {
                openFold($target, $btn);
            }

            window.scrollTo(0, scrollY);
        });
});

// ========== 프로필 시스템 ==========
function initProfile() {
    $('.profile-card').remove();
    $('.user-profile-portal').removeClass('user-profile-portal');

    var token = ++PROFILE_RENDER_TOKEN;
    var ns = mw.config.get('wgNamespaceNumber');
    var title = mw.config.get('wgTitle');
    var specialPage = mw.config.get('wgCanonicalSpecialPageName');
    var isProfileSettings = specialPage === '사용자정보';

    $('body').toggleClass('user-profile-page', ns === 2);
    $('body').toggleClass('user-profile-settings-page', isProfileSettings);

    if (ns === 2) {
        var profileUser = title.split('/')[0];
        renderProfile(profileUser, token);
    }

    if (isProfileSettings) {
        initUserProfilePage();
    }
}

function renderProfile(username, token) {
    var api = new mw.Api();
    api.get({
        action: 'query',
        list: 'users',
        ususers: username,
        usprop: 'editcount'
    }).then(function(data) {
        if (token !== PROFILE_RENDER_TOKEN) return;
        if (mw.config.get('wgNamespaceNumber') !== 2) return;

        var currentTitle = String(mw.config.get('wgTitle') || '').split('/')[0];
        if (currentTitle !== username) return;

        var user = data.query.users[0];
        var contentEl = document.getElementById('mw-content-text');
        if (!contentEl) return;

        var pageContent = contentEl.querySelector('.mw-parser-output') || contentEl;
        injectProfileCard(username, user, pageContent);
    });
}

function injectProfileCard(username, userData, container) {
    var isOwnPage = mw.config.get('wgUserName') === username;
    var editCount = (userData && userData.editcount) ? userData.editcount : 0;

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

    container.classList.add('user-profile-portal');

    var safeUsername = escapeHtml(username);
    var avatarSrc = '/index.php?title=특수:Redirect/file/Pfp-' + encodeURIComponent(username) + '.png&width=220';
    var fallbackSrc = '/index.php?title=특수:Redirect/file/Pfp-default.png&width=220';
    var editBtn = isOwnPage
        ? '<a href="/index.php/특수:사용자정보" class="profile-edit-btn"><span class="profile-edit-label">프로필 수정</span><span class="profile-edit-arrow">›</span></a>'
        : '';

    var progressHtml = isOwnPage
        ? '<div class="profile-page-progress is-syncing" data-profile-progress>' +
            '<div class="profile-section-title">LEVEL RECORD</div>' +
            '<div class="profile-page-progress-body">' +
                '<div class="profile-page-progress-row">' +
                    '<span class="profile-page-level">SYNC</span>' +
                    '<span class="profile-page-total-xp">— XP</span>' +
                '</div>' +
                '<div class="profile-page-xp-bar" aria-hidden="true"><div class="profile-page-xp-fill"></div></div>' +
                '<div class="profile-page-progress-sub">SYNCING</div>' +
                '<div class="profile-page-progress-meta">TODAY — · DISCOVERED —</div>' +
            '</div>' +
        '</div>'
        : '';

    var card = document.createElement('div');
    card.className = 'profile-card profile-page-console';
    card.innerHTML =
        '<div class="profile-card-titlebar">' +
            '<span>USER PROFILE</span>' +
            '<span>OFFICIAL ARCHIVE</span>' +
        '</div>' +
        '<div class="profile-card-body">' +
            '<div class="profile-identity-row">' +
                '<div class="profile-avatar-bay">' +
                    '<img src="' + avatarSrc + '" onerror="this.onerror=null;this.src=\'' + fallbackSrc + '\';" alt="' + safeUsername + '">' +
                '</div>' +
                '<div class="profile-info-panel">' +
                    '<div class="profile-nameplate">' +
                        '<h2 class="profile-username">' + safeUsername + '</h2>' +
                        editBtn +
                    '</div>' +
                    '<div class="profile-name" data-field="name"></div>' +
                    '<div class="profile-role" data-field="role"></div>' +
                    '<div class="profile-discord" data-field="discord"></div>' +
                '</div>' +
            '</div>' +
            '<div class="profile-lower-grid">' +
                '<div class="profile-bio-panel">' +
                    '<div class="profile-section-title">BIOGRAPHY</div>' +
                    '<div class="profile-bio" data-field="bio"></div>' +
                '</div>' +
                '<div class="profile-stats-panel">' +
                    '<div class="profile-section-title">RECORD</div>' +
                    '<div class="profile-stats">' +
                        progressHtml +
                        '<div class="profile-stat-grid">' +
                            '<div class="profile-stat">' +
                                '<span class="clbi-stat-value">' + editCount + '</span>' +
                                '<span class="clbi-stat-label">수정 횟수</span>' +
                            '</div>' +
                            '<div class="profile-stat" data-contrib-pages-stat>' +
                                '<span class="clbi-stat-value" data-contrib-pages-value>SYNC</span>' +
                                '<span class="clbi-stat-label">기여 문서</span>' +
                            '</div>' +
                        '</div>' +
                        '<div class="profile-info-grid">' +
                            '<div class="profile-info-stat">' +
                                '<span class="profile-info-label">TIME</span>' +
                                '<span class="profile-info-value" data-profile-time>UTC --:--</span>' +
                            '</div>' +
                            '<div class="profile-info-stat">' +
                                '<span class="profile-info-label">LANGUAGE</span>' +
                                '<span class="profile-info-value" data-profile-language>—</span>' +
                            '</div>' +
                        '</div>' +
                    '</div>' +
                '</div>' +
            '</div>' +
        '</div>';

    $('.profile-card').remove();
    container.insertBefore(card, container.firstChild);
    loadProfileFields(username, card);
    loadProfileContributionPages(username, card);
    updateProfilePageEnvironment(card, null);

    if (isOwnPage) {
        loadProfileProgressForUserPage(card);
    }
}

function getProfileLanguageLabel() {
    var lang = getCurrentLang();
    return SIDEBAR_LANGUAGE_LABELS[lang] || (lang ? lang.toUpperCase() : '—');
}

function updateProfilePageEnvironment(card, summary) {
    if (!card) return;

    var timezone = summary && summary.timezone ? summary.timezone : 'UTC';
    var timeEl = card.querySelector('[data-profile-time]');
    var langEl = card.querySelector('[data-profile-language]');

    if (timeEl) {
        try {
            timeEl.textContent = timezone + ' ' + new Intl.DateTimeFormat('ko-KR', {
                hour: '2-digit',
                minute: '2-digit',
                hour12: false,
                timeZone: timezone
            }).format(new Date());
        } catch (err) {
            timeEl.textContent = 'UTC ' + new Intl.DateTimeFormat('ko-KR', {
                hour: '2-digit',
                minute: '2-digit',
                hour12: false,
                timeZone: 'UTC'
            }).format(new Date());
        }
    }

    if (langEl) {
        langEl.textContent = getProfileLanguageLabel();
    }
}

function loadProfileContributionPages(username, card) {
    if (!username || !card) return;
    if (!mw.loader || typeof mw.loader.using !== 'function') return;

    var valueEl = card.querySelector('[data-contrib-pages-value]');
    if (valueEl) valueEl.textContent = 'SYNC';

    mw.loader.using(['mediawiki.api']).then(function () {
        var api = new mw.Api();
        var pages = Object.create(null);
        var cont = {};
        var guard = 0;

        function requestNext() {
            guard++;

            var params = Object.assign({
                action: 'query',
                list: 'usercontribs',
                ucuser: username,
                ucnamespace: 0,
                ucprop: 'title',
                uclimit: 'max',
                format: 'json',
                formatversion: 2
            }, cont);

            return api.get(params).then(function (data) {
                var rows = data && data.query && data.query.usercontribs ? data.query.usercontribs : [];

                rows.forEach(function (row) {
                    if (row && row.title) pages[row.title] = true;
                });

                if (data && data.continue && data.continue.uccontinue && guard < 40) {
                    cont = data.continue;
                    return requestNext();
                }

                if (valueEl) valueEl.textContent = Object.keys(pages).length;
            });
        }

        requestNext().fail(function () {
            if (valueEl) valueEl.textContent = '—';
        });
    });
}

function loadProfileProgressForUserPage(card) {
    if (!mw.config.get('wgUserName')) return;
    if (!mw.loader || typeof mw.loader.using !== 'function') return;

    mw.loader.using(['mediawiki.api']).then(function () {
        var api = new mw.Api();
        api.get({
            action: 'progress_summary',
            format: 'json',
            formatversion: 2
        }).then(function (data) {
            var payload = data && data.progress_summary;
            if (!payload || !payload.available || !payload.summary) return;
            updateUserPageProgress(card, payload.summary);
            updateProfilePageEnvironment(card, payload.summary);
        });
    });
}

function updateUserPageProgress(card, summary) {
    var panel = card.querySelector('[data-profile-progress]');
    if (!panel || !summary) return;

    var level = summary.level || 1;
    var totalXp = summary.totalXp || 0;
    var xpIntoLevel = summary.xpIntoLevel || 0;
    var xpForNext = summary.xpForNextLevel || 1;
    var percent = Math.max(0, Math.min(100, summary.progressPercent || 0));
    var isMaxLevel = !!summary.isMaxLevel;
    var dailyXp = summary.dailyXp || 0;
    var discoveries = summary.discoveryCount || 0;

    panel.classList.remove('is-syncing');
    panel.classList.toggle('is-max-level', isMaxLevel);

    var levelEl = panel.querySelector('.profile-page-level');
    var totalEl = panel.querySelector('.profile-page-total-xp');
    var fillEl = panel.querySelector('.profile-page-xp-fill');
    var subEl = panel.querySelector('.profile-page-progress-sub');
    var metaEl = panel.querySelector('.profile-page-progress-meta');

    if (levelEl) levelEl.textContent = (isMaxLevel ? 'MAX ' : 'LVL ') + level;
    if (totalEl) totalEl.textContent = totalXp + ' XP';
    if (fillEl) fillEl.style.width = percent + '%';
    if (subEl) subEl.textContent = isMaxLevel ? 'MAX LEVEL' : (xpIntoLevel + ' / ' + xpForNext + ' TO NEXT');
    if (metaEl) metaEl.textContent = 'TODAY ' + dailyXp + ' XP · DISCOVERED ' + discoveries;
}

function loadProfileFields(username, card) {
    var api = new mw.Api();
    api.get({
        action: 'userprofile',
        user: username
    }).then(function(data) {
        var profile = data.userprofile;
        updateProfileFields(card, {
            name: profile.name || '',
            discord: profile.discord || '',
            role: profile.role || '',
            bio: profile.bio || ''
        });
    }).fail(function() {
        updateProfileFields(card, {
            name: '',
            discord: '',
            role: '',
            bio: ''
        });
    });
}

function updateProfileFields(card, data) {
    var nameEl = card.querySelector('[data-field="name"]');
    var roleEl = card.querySelector('[data-field="role"]');
    var discordEl = card.querySelector('[data-field="discord"]');
    var bioEl = card.querySelector('[data-field="bio"]');
    if (nameEl) nameEl.textContent = data.name || '';
    if (roleEl) roleEl.textContent = data.role || '';
    if (discordEl) discordEl.textContent = data.discord ? ('디스코드: ' + data.discord) : '';
    if (bioEl) bioEl.textContent = data.bio || '';
}
// ========== 프로필 시스템 끝 ==========

// ========== 알림 시스템 ==========
function ensureNotificationPopup() {
    if (document.getElementById('clbi-notification-popup')) return;

    var popup = document.createElement('div');
    popup.id = 'clbi-notification-popup';
    popup.style.cssText =
        'display:none;position:fixed;z-index:99999;width:320px;max-height:420px;' +
        'background:#0a0909;border:2px solid #854369;border-radius:5px;' +
        'box-shadow:0 0 0 1px #1a1a1a, 0 8px 24px rgba(0,0,0,0.55);overflow:hidden;';

    popup.innerHTML =
        '<div style="padding:10px 12px;border-bottom:2px solid #854369;background:linear-gradient(to bottom, #171114 0%, #0a0909 100%);color:#E2E2E2;font-size:13px;font-weight:700;display:flex;align-items:center;justify-content:space-between;gap:8px;">' +
            '<span>알림</span>' +
            '<button type="button" id="clbi-notification-readall" style="background:#171717;border:1px solid #854369;border-radius:6px;color:#E2E2E2;font-size:11px;font-weight:700;padding:4px 8px;cursor:pointer;">전체 읽음</button>' +
        '</div>' +
        '<div id="clbi-notification-list" style="max-height:320px;overflow-y:auto;padding:8px 0;color:#E2E2E2;font-size:12px;">불러오는 중...</div>' +
        '<div style="padding:8px;border-top:1px solid #2a2a2a;background:#111;">' +
            '<a href="/index.php?title=Special:Notifications" id="clbi-notification-more" style="display:block;width:100%;text-align:center;padding:8px 10px;border-radius:6px;background:#171717;border:1px solid #854369;color:#E2E2E2 !important;text-decoration:none !important;font-size:12px;font-weight:700;">더보기</a>' +
        '</div>';

    document.body.appendChild(popup);
}

function positionNotificationPopup() {
    var btn = document.getElementById('profile-quick-notifications');
    var popup = document.getElementById('clbi-notification-popup');
    if (!btn || !popup) return;

    var rect = btn.getBoundingClientRect();
    var top = rect.bottom + 6;
    var left = rect.left + (rect.width / 2) - (popup.offsetWidth / 2);

    if (left < 8) left = 8;
    if (left + popup.offsetWidth > window.innerWidth - 8) {
        left = window.innerWidth - popup.offsetWidth - 8;
    }
    if (top + popup.offsetHeight > window.innerHeight - 8) {
        top = Math.max(8, rect.top - popup.offsetHeight - 6);
    }

    popup.style.top = top + 'px';
    popup.style.left = left + 'px';
}

function parseNotificationItemsFromHtml(html) {
    var parser = new DOMParser();
    var doc = parser.parseFromString(html, 'text/html');

    var selectors = [
        '.mw-echo-ui-notificationItemWidget',
        '.mw-echo-ui-notificationsInboxWidgetRow',
        '.echo-ui-notificationItemWidget',
        'li[data-notification-id]',
        '.mw-echo-notifications-list li'
    ];

    var items = [];
    for (var i = 0; i < selectors.length; i++) {
        items = Array.prototype.slice.call(doc.querySelectorAll(selectors[i]));
        if (items.length) break;
    }

    return items.slice(0, 5).map(function(item) {
        var link = item.querySelector('a[href]');
        var href = link ? link.getAttribute('href') : '/index.php?title=Special:Notifications';
        var text = (item.textContent || '').replace(/\s+/g, ' ').trim();

        var notificationId =
            item.getAttribute('data-notification-id') ||
            item.getAttribute('data-id') ||
            item.getAttribute('data-notification') ||
            '';

        if (!notificationId) {
            var anyWithId = item.querySelector('[data-notification-id], [data-id], [data-notification]');
            if (anyWithId) {
                notificationId =
                    anyWithId.getAttribute('data-notification-id') ||
                    anyWithId.getAttribute('data-id') ||
                    anyWithId.getAttribute('data-notification') ||
                    '';
            }
        }

        if (href && href.indexOf('http') !== 0) {
            href = href.charAt(0) === '/'
                ? href
                : '/index.php' + (href.charAt(0) === '?' ? href : '/' + href);
        }

        return {
            id: notificationId,
            href: href,
            text: text || '알림'
        };
    });
}

function setNotificationIcon(hasItems) {
    var quickIcon = document.getElementById('profile-quick-notification-icon');
    var svg = hasItems ? CLBI_SVG_BELL_DOT : CLBI_SVG_BELL;

    if (quickIcon) {
        quickIcon.innerHTML = svg;
        quickIcon.classList.toggle('has-notifications', !!hasItems);
    }
}

function renderNotificationPopup(items) {
    var list = document.getElementById('clbi-notification-list');
    var badge = document.getElementById('clbi-notification-badge');
    if (!list) return;

    if (!items || !items.length) {
        list.innerHTML = '<div style="padding:14px 12px;color:#999;">표시할 알림이 없습니다.</div>';
        if (badge) badge.style.display = 'none';
        setNotificationIcon(false);
        return;
    }

    var html = '';
    for (var i = 0; i < items.length; i++) {
        html +=
            '<a href="' + items[i].href + '" class="clbi-notification-item" data-notification-id="' + (items[i].id || '') + '" style="display:block;padding:10px 12px;color:#E2E2E2 !important;text-decoration:none !important;border-bottom:1px solid #1f1f1f;line-height:1.5;">' +
                items[i].text +
            '</a>';
    }
    list.innerHTML = html;

    if (badge) {
        badge.textContent = items.length;
        badge.style.display = 'block';
    }
    setNotificationIcon(true);
}

function loadNotificationsIntoPopup() {
    var list = document.getElementById('clbi-notification-list');
    if (list) {
        list.innerHTML = '<div style="padding:14px 12px;color:#999;">불러오는 중...</div>';
    }

    fetch('/index.php?title=Special:Notifications', { credentials: 'same-origin' })
        .then(function(res) {
            return res.text();
        })
        .then(function(html) {
            var items = parseNotificationItemsFromHtml(html);
            renderNotificationPopup(items);
        })
        .catch(function(err) {
            console.error(err);
            if (list) {
                list.innerHTML = '<div style="padding:14px 12px;color:#999;">알림을 불러오지 못했습니다.</div>';
            }
        });
}

function markAllNotificationsRead() {
    return new mw.Api().postWithToken('csrf', {
        action: 'echomarkread',
        list: 'all'
    });
}

function markNotificationReadById(notificationId) {
    if (!notificationId) {
        return $.Deferred().resolve().promise();
    }

    return new mw.Api().postWithToken('csrf', {
        action: 'echomarkread',
        list: notificationId
    });
}

function initNotifications() {
    var quickBtn = document.getElementById('profile-quick-notifications');

    if (!quickBtn) return;

    ensureNotificationPopup();
    loadNotificationsIntoPopup();

    $(document)
        .off('click.clbiNotificationToggle')
        .on('click.clbiNotificationToggle', '#profile-quick-notifications', function(e) {
            e.preventDefault();
            e.stopPropagation();

            var popup = document.getElementById('clbi-notification-popup');
            if (!popup) return;

            if (popup.style.display === 'none' || popup.style.display === '') {
                popup.style.display = 'block';
                positionNotificationPopup();
                loadNotificationsIntoPopup();
            } else {
                popup.style.display = 'none';
            }
        });

    $(document)
        .off('click.clbiNotificationOutside')
        .on('click.clbiNotificationOutside', function(e) {
            var popup = document.getElementById('clbi-notification-popup');
            var quickToggle = document.getElementById('profile-quick-notifications');
            if (!popup) return;

            if (!popup.contains(e.target) && (!quickToggle || !quickToggle.contains(e.target))) {
                popup.style.display = 'none';
            }
        });

    $(document)
        .off('click.clbiNotificationReadAll')
        .on('click.clbiNotificationReadAll', '#clbi-notification-readall', function(e) {
            e.preventDefault();
            e.stopPropagation();

            var button = this;
            button.disabled = true;
            button.textContent = '처리 중...';

            markAllNotificationsRead()
                .then(function() {
                    loadNotificationsIntoPopup();
                })
                .always(function() {
                    button.disabled = false;
                    button.textContent = '전체 읽음';
                });
        });

    $(document)
        .off('click.clbiNotificationItem')
        .on('click.clbiNotificationItem', '.clbi-notification-item', function(e) {
            e.preventDefault();
            e.stopPropagation();

            var href = this.getAttribute('href');
            var notificationId = this.getAttribute('data-notification-id') || '';

            markNotificationReadById(notificationId).always(function() {
                loadNotificationsIntoPopup();
                if (href) {
                    window.location.href = href;
                }
            });
        });

    $(window)
        .off('resize.clbiNotification')
        .on('resize.clbiNotification', function() {
            var popup = document.getElementById('clbi-notification-popup');
            if (popup && popup.style.display === 'block') {
                positionNotificationPopup();
            }
        });
}
// ========== 알림 시스템 끝 ==========

function initUserProfilePage() {
    $('body').addClass('user-profile-settings-page');

    var saveBtn = document.getElementById('pref-save');
    if (!saveBtn) return;

    function getPrefRow(id) {
        var el = document.getElementById(id);
        if (!el) return null;
        return el.closest('.clbi-pref-row') || el.parentNode;
    }

    function removePrefRow(id) {
        var row = getPrefRow(id);
        if (row && row.parentNode) {
            row.parentNode.removeChild(row);
        }
    }

    function createPrefSection(className, titleText) {
        var section = document.createElement('div');
        section.className = 'clbi-pref-section ' + className;

        var title = document.createElement('div');
        title.className = 'clbi-pref-section-title';
        title.textContent = titleText;

        var body = document.createElement('div');
        body.className = 'clbi-pref-section-body';

        section.appendChild(title);
        section.appendChild(body);

        return {
            section: section,
            body: body
        };
    }

    function moveRowToSection(id, targetBody, className) {
        var row = getPrefRow(id);
        if (!row || !targetBody) return false;

        row.classList.add('clbi-pref-row-key-' + className);
        targetBody.appendChild(row);
        return true;
    }

    function rebuildProfileSettingsLayout() {
        var root = document.querySelector('.clbi-prefs-profile');
        if (!root || root.dataset.profileSettingsReworked === '1') return;

        root.dataset.profileSettingsReworked = '1';
        root.classList.add('profile-settings-console');

        removePrefRow('pref-badges');

        var originalRows = Array.prototype.slice.call(root.querySelectorAll('.clbi-pref-row'));
        var actionNodes = [];

        if (saveBtn.parentNode === root || saveBtn.closest('.clbi-prefs-profile') === root) {
            actionNodes.push(saveBtn);
        }

        var statusNode = document.getElementById('pref-status');
        if (statusNode && statusNode.closest('.clbi-prefs-profile') === root) {
            actionNodes.push(statusNode);
        }

        var main = document.createElement('div');
        main.className = 'clbi-pref-main-grid';

        var media = createPrefSection('clbi-pref-section-media', 'PROFILE IMAGE');
        var identity = createPrefSection('clbi-pref-section-identity', 'IDENTITY RECORD');
        var bio = createPrefSection('clbi-pref-section-bio', 'BIOGRAPHY');
        var account = createPrefSection('clbi-pref-section-account', 'ACCOUNT CONTACT');
        var misc = createPrefSection('clbi-pref-section-misc', 'OTHER OPTIONS');

        main.appendChild(media.section);
        main.appendChild(identity.section);
        main.appendChild(bio.section);
        main.appendChild(account.section);
        main.appendChild(misc.section);

        root.innerHTML = '';
        root.appendChild(main);

        moveRowToSection('pref-pfp-preview', media.body, 'pfp');
        moveRowToSection('pref-pfp-btn', media.body, 'pfp');
        moveRowToSection('pref-pfp-input', media.body, 'pfp');

        moveRowToSection('pref-name', identity.body, 'name');
        moveRowToSection('pref-role', identity.body, 'role');
        moveRowToSection('pref-discord', identity.body, 'discord');

        moveRowToSection('pref-bio', bio.body, 'bio');

        moveRowToSection('pref-new-email', account.body, 'email');
        moveRowToSection('pref-email-password', account.body, 'email');
        moveRowToSection('pref-email-save', account.body, 'email');

        originalRows.forEach(function (row) {
            if (!row.parentNode && !row.className.match(/clbi-pref-row-key-/)) {
                misc.body.appendChild(row);
            }
        });

        if (!misc.body.children.length) {
            misc.section.parentNode.removeChild(misc.section);
        }

        var actions = document.createElement('div');
        actions.className = 'clbi-pref-actions';

        if (saveBtn) actions.appendChild(saveBtn);
        if (statusNode) actions.appendChild(statusNode);

        root.appendChild(actions);
    }

    rebuildProfileSettingsLayout();

    var api = new mw.Api();
    var selectedFile = null;
    var cropper = null;

    if (!document.getElementById('clbi-gallery-modal')) {
        var gModal = document.createElement('div');
        gModal.id = 'clbi-gallery-modal';
        gModal.style.cssText =
            'display:none;position:fixed;top:0;left:0;width:100%;height:100%;background:rgba(0,0,0,0.85);z-index:99999;align-items:center;justify-content:center;';

        gModal.innerHTML =
            '<div style="background:#1e1e1e;border:2px solid #854369;border-radius:12px;padding:24px;max-width:480px;width:90%;display:flex;flex-direction:column;gap:16px;">' +
                '<div style="display:flex;justify-content:space-between;align-items:center;">' +
                    '<span style="font-size:14px;font-weight:700;color:#e2e2e2;">프로필 사진 선택</span>' +
                    '<button type="button" id="clbi-gallery-close" style="background:none;border:none;color:#aaa;font-size:18px;cursor:pointer;">✕</button>' +
                '</div>' +
                '<button type="button" id="clbi-gallery-upload-btn" style="background:#2a2a2a;border:2px dashed #854369;border-radius:8px;padding:32px;color:#e2e2e2;cursor:pointer;display:flex;flex-direction:column;align-items:center;gap:8px;font-size:13px;width:100%;">' +
                    '<span style="font-size:32px;">🖼️</span>새 사진 업로드' +
                '</button>' +
                '<div id="clbi-gallery-history-section" style="display:none;">' +
                    '<div style="font-size:11px;color:#888;margin-bottom:8px;">이전 사진 — 클릭하면 바로 적용</div>' +
                    '<div id="clbi-gallery-history" style="display:flex;gap:8px;flex-wrap:wrap;"></div>' +
                '</div>' +
            '</div>';

        document.body.appendChild(gModal);
    }

    if (!document.getElementById('clbi-crop-modal')) {
        var cModal = document.createElement('div');
        cModal.id = 'clbi-crop-modal';
        cModal.style.cssText =
            'display:none;position:fixed;top:0;left:0;width:100%;height:100%;background:rgba(0,0,0,0.85);z-index:99999;align-items:center;justify-content:center;';

        cModal.innerHTML =
            '<div style="background:#1e1e1e;border:2px solid #854369;border-radius:12px;padding:24px;max-width:500px;width:90%;display:flex;flex-direction:column;gap:16px;">' +
                '<div style="font-size:14px;font-weight:700;color:#e2e2e2;">사진 조정</div>' +
                '<div style="width:100%;max-height:380px;overflow:hidden;border-radius:8px;">' +
                    '<img id="clbi-crop-image" style="max-width:100%;">' +
                '</div>' +
                '<div style="display:flex;gap:8px;justify-content:flex-end;">' +
                    '<button type="button" id="clbi-crop-cancel" style="background:#2a2a2a;color:#e2e2e2;border:1px solid #444;padding:8px 16px;border-radius:6px;cursor:pointer;">취소</button>' +
                    '<button type="button" id="clbi-crop-confirm" style="background:#854369;color:#fff;border:none;padding:8px 16px;border-radius:6px;cursor:pointer;">확정</button>' +
                '</div>' +
            '</div>';

        document.body.appendChild(cModal);
    }

    var gModal = document.getElementById('clbi-gallery-modal');
    var cModal = document.getElementById('clbi-crop-modal');
    var cropImage = document.getElementById('clbi-crop-image');
    var pfpInput = document.getElementById('pref-pfp-input');

    function openGallery() {
        gModal.style.display = 'flex';

        var username = mw.config.get('wgUserName');
        api.get({
            action: 'query',
            titles: '파일:Pfp-' + username + '.png',
            prop: 'imageinfo',
            iiprop: 'url|timestamp',
            iilimit: 6
        }).then(function(data) {
            var pages = data.query.pages;
            var page = pages[Object.keys(pages)[0]];
            if (!page.imageinfo || page.imageinfo.length === 0) return;

            var historyEl = document.getElementById('clbi-gallery-history');
            var sectionEl = document.getElementById('clbi-gallery-history-section');
            historyEl.innerHTML = '';

            page.imageinfo.forEach(function(info, idx) {
                var wrap = document.createElement('div');
                wrap.style.cssText = 'position:relative;cursor:pointer;';

                var img = document.createElement('img');
                img.src = info.url;
                img.style.cssText =
                    'width:72px;height:72px;object-fit:cover;border-radius:8px;border:2px solid #444;flex-shrink:0;';

                if (idx === 0) {
                    img.style.borderColor = '#854369';
                    var badge = document.createElement('div');
                    badge.textContent = '현재';
                    badge.style.cssText =
                        'position:absolute;bottom:4px;left:50%;transform:translateX(-50%);background:#854369;color:#fff;font-size:9px;padding:1px 6px;border-radius:10px;';
                    wrap.appendChild(badge);
                }

                img.addEventListener('mouseenter', function() {
                    if (idx !== 0) img.style.borderColor = '#854369';
                });

                img.addEventListener('mouseleave', function() {
                    if (idx !== 0) img.style.borderColor = '#444';
                });

                img.addEventListener('click', function() {
                    fetch(info.url)
                        .then(function(r) {
                            return r.blob();
                        })
                        .then(function(blob) {
                            selectedFile = new File([blob], 'profile.png', { type: 'image/png' });
                            document.getElementById('pref-pfp-preview').src = URL.createObjectURL(blob);
                            gModal.style.display = 'none';
                            document.getElementById('pref-pfp-btn').textContent = '✓ 사진 선택됨';
                        });
                });

                wrap.appendChild(img);
                historyEl.appendChild(wrap);
            });

            sectionEl.style.display = 'block';
        });
    }

    function openCrop(src) {
        cropImage.src = src;
        cModal.style.display = 'flex';

        if (cropper) {
            cropper.destroy();
            cropper = null;
        }

        setTimeout(function() {
            cropper = new Cropper(cropImage, {
                aspectRatio: 1,
                viewMode: 1,
                dragMode: 'move',
                autoCropArea: 0.8,
                cropBoxResizable: true,
                cropBoxMovable: true
            });
        }, 150);
    }

    document.getElementById('pref-pfp-btn').addEventListener('click', function() {
        openGallery();
    });

    document.getElementById('clbi-gallery-upload-btn').addEventListener('click', function() {
        pfpInput.click();
    });

    document.getElementById('clbi-gallery-close').addEventListener('click', function() {
        gModal.style.display = 'none';
    });

    pfpInput.addEventListener('change', function() {
        var file = this.files[0];
        if (!file) return;

        gModal.style.display = 'none';

        var reader = new FileReader();
        reader.onload = function(e) {
            openCrop(e.target.result);
        };
        reader.readAsDataURL(file);
    });

    document.getElementById('clbi-crop-cancel').addEventListener('click', function() {
        cModal.style.display = 'none';
        if (cropper) {
            cropper.destroy();
            cropper = null;
        }
        pfpInput.value = '';
    });

    document.getElementById('clbi-crop-confirm').addEventListener('click', function() {
        if (!cropper) return;

        var canvas = cropper.getCroppedCanvas({ width: 256, height: 256 });
        if (!canvas) return;

        canvas.toBlob(function(blob) {
            selectedFile = new File([blob], 'profile.png', { type: 'image/png' });
            document.getElementById('pref-pfp-preview').src = URL.createObjectURL(blob);
            cModal.style.display = 'none';
            cropper.destroy();
            cropper = null;
            document.getElementById('pref-pfp-btn').textContent = '✓ 사진 선택됨';
        }, 'image/png');
    });

    var emailSaveBtn = document.getElementById('pref-email-save');
    if (emailSaveBtn) {
        emailSaveBtn.addEventListener('click', function() {
            var statusEl = document.getElementById('pref-email-status');
            var newEmail = document.getElementById('pref-new-email').value;
            var password = document.getElementById('pref-email-password').value;

            if (!newEmail || !password) {
                statusEl.textContent = '이메일과 비밀번호를 입력해주세요.';
                return;
            }

            statusEl.textContent = '변경 중...';

            api.postWithToken('csrf', {
                action: 'changeemail',
                email: newEmail,
                password: password
            }).then(function() {
                statusEl.textContent = '✓ 이메일 변경됨';
                document.getElementById('pref-new-email').value = '';
                document.getElementById('pref-email-password').value = '';

                setTimeout(function() {
                    statusEl.textContent = '';
                }, 3000);
            }).fail(function(code, data) {
                var msg = data && data.error && data.error.info ? data.error.info : '변경 실패';
                statusEl.textContent = msg;
            });
        });
    }

    saveBtn.addEventListener('click', function() {
        var statusEl = document.getElementById('pref-status');
        statusEl.textContent = '저장 중...';

        var promises = [];

        if (selectedFile) {
            var username = mw.config.get('wgUserName');
            promises.push(
                api.postWithToken('csrf', {
                    action: 'upload',
                    filename: 'Pfp-' + username + '.png',
                    ignorewarnings: true,
                    file: selectedFile,
                    format: 'json'
                }, {
                    contentType: 'multipart/form-data'
                })
            );
        }

        var fields = ['name', 'discord', 'role', 'bio'];

        for (var i = 0; i < fields.length; i++) {
            var el = document.getElementById('pref-' + fields[i]);
            if (!el) continue;

            promises.push(
                api.postWithToken('csrf', {
                    action: 'options',
                    optionname: 'profile-' + fields[i],
                    optionvalue: el.value
                })
            );
        }

        $.when.apply($, promises)
            .then(function() {
                statusEl.textContent = '✓ 저장됨';
                selectedFile = null;
                document.getElementById('pref-pfp-btn').textContent = '사진 선택';

                setTimeout(function() {
                    statusEl.textContent = '';
                }, 2000);
            })
            .fail(function() {
                statusEl.textContent = '저장 실패';
            });
    });
}

/* =========================================
   Banner / CRT Page Monitor thumbnail slices
   - base 이미지는 틀에 들어간 파일 문법 그대로 사용
   - slice 레이어에는 300px MediaWiki 썸네일만 삽입
   ========================================= */

(function ($, mw) {
    var thumbCache = {};

    function parseSliceWidth(value) {
        var parsed = parseInt(value, 10);

        if (!isFinite(parsed) || parsed < 120) {
            return 300;
        }

        return parsed;
    }

    function getImageSrc(img) {
        return img ? (img.currentSrc || img.getAttribute('src') || img.src || '') : '';
    }

    function getFileNameFromSrc(src) {
        var a;
        var parts;
        var fileName;

        if (!src) return '';

        a = document.createElement('a');
        a.href = src;

        parts = (a.pathname || '').split('/').filter(function (part) {
            return !!part;
        });

        if (!parts.length) return '';

        fileName = parts.pop();

        /*
         * MediaWiki thumb URL 예시:
         * /images/thumb/a/ab/File.png/1000px-File.png
         * /images/thumb/a/ab/File.svg/1000px-File.svg.png
         *
         * 이 경우 실제 파일명은 마지막 조각이 아니라 그 앞 조각이다.
         */
        if (/^\d+px-/.test(fileName) && parts.length) {
            fileName = parts.pop();
        }

        fileName = fileName.replace(/^\d+px-/, '');

        try {
            fileName = decodeURIComponent(fileName);
        } catch (e) {}

        return fileName;
    }

    function resolveThumbUrl(img, width, callback) {
        var src = getImageSrc(img);
        var fileName = getFileNameFromSrc(src);
        var cacheKey;
        var entry;

        if (!src) return;

        if (!fileName || !mw || !mw.loader) {
            callback(src);
            return;
        }

        cacheKey = fileName + '|' + width;
        entry = thumbCache[cacheKey];

        if (entry) {
            if (entry.resolved) {
                callback(entry.url || src);
            } else {
                entry.callbacks.push(callback);
            }
            return;
        }

        entry = {
            resolved: false,
            url: '',
            callbacks: [callback]
        };

        thumbCache[cacheKey] = entry;

        function finish(url) {
            var callbacks = entry.callbacks.slice();
            var i;

            entry.resolved = true;
            entry.url = url || src;
            entry.callbacks = [];

            for (i = 0; i < callbacks.length; i++) {
                callbacks[i](entry.url);
            }
        }

        mw.loader.using('mediawiki.api').done(function () {
            var api = new mw.Api();

            api.get({
                action: 'query',
                titles: 'File:' + fileName,
                prop: 'imageinfo',
                iiprop: 'url',
                iiurlwidth: width,
                formatversion: 2
            }).done(function (data) {
                var page;
                var info;

                if (
                    data &&
                    data.query &&
                    data.query.pages &&
                    data.query.pages.length
                ) {
                    page = data.query.pages[0];

                    if (
                        page &&
                        page.imageinfo &&
                        page.imageinfo.length
                    ) {
                        info = page.imageinfo[0];
                    }
                }

                finish((info && (info.thumburl || info.url)) || src);
            }).fail(function () {
                finish(src);
            });
        }).fail(function () {
            finish(src);
        });
    }

    function applySliceImages(frame, thumbUrl) {
        var slices;
        var i;
        var img;

        if (!frame || !thumbUrl) return;

        slices = frame.querySelectorAll('.crt-page-monitor-slice');

        for (i = 0; i < slices.length; i++) {
            slices[i].innerHTML = '';

            img = document.createElement('img');
            img.className = 'crt-page-monitor-slice-img';
            img.src = thumbUrl;
            img.alt = '';
            img.decoding = 'async';
            img.loading = 'eager';
            img.setAttribute('aria-hidden', 'true');

            slices[i].appendChild(img);
        }

        frame.setAttribute('data-crt-slices-ready', '1');
    }

    function initBannerFrame(frame) {
        var baseImg;
        var width;

        if (!frame) return;
        if (frame.getAttribute('data-crt-slices-ready') === '1') return;

        baseImg = frame.querySelector('.crt-page-monitor-image-base img');

        if (!baseImg) return;

        width = parseSliceWidth(frame.getAttribute('data-crt-slice-width'));

        resolveThumbUrl(baseImg, width, function (thumbUrl) {
            if (!frame || !frame.parentNode) return;
            applySliceImages(frame, thumbUrl);
        });
    }

    function initBannerFrames(root) {
        var scope = root && root.querySelectorAll ? root : document;
        var frames = scope.querySelectorAll('.crt-page-monitor-frame');
        var i;

        for (i = 0; i < frames.length; i++) {
            initBannerFrame(frames[i]);
        }
    }

    $(function () {
        initBannerFrames(document);
    });

    if (mw && mw.hook) {
        mw.hook('wikipage.content').add(function ($content) {
            initBannerFrames($content && $content[0] ? $content[0] : document);
        });
    }
})(jQuery, window.mw);

/* =========================================
   Doc Tab System — tab switching UI
   글리치 플리커 + RGB split + 방향 슬라이드
   ========================================= */

(function () {
    'use strict';

    function initDocTabs() {
        var tabBars = document.querySelectorAll('.doc-tab-bar');
        if (!tabBars.length) return;

        tabBars.forEach(function (bar) {
            if (bar.getAttribute('data-tabs-init')) return;
            bar.setAttribute('data-tabs-init', '1');

            var tabs = Array.from(bar.querySelectorAll('.doc-tab'));
            if (!tabs.length) return;

            var panel = bar.closest('.doc-panel');
            var display = panel ? panel.querySelector('.doc-display') : null;
            if (!display) display = document.getElementById('doc-main-display');
            if (!display) return;

            tabs.forEach(function (tab, i) {
                tab.addEventListener('click', function () {
                    var currentIdx = tabs.findIndex(function (t) {
                        return t.classList.contains('active');
                    });
                    if (currentIdx === i) return;
                    switchTab(tabs, display, i, i > currentIdx ? 1 : -1);
                });
            });

            var initIdx = tabs.findIndex(function (t) { return t.classList.contains('active'); });
            if (initIdx !== -1) {
                var initRef = tabs[initIdx].dataset.ref;
                var initEl = initRef ? document.getElementById(initRef) : null;
                display.innerHTML = initEl ? initEl.innerHTML : (tabs[initIdx].dataset.content || '');
            }
        });

    }

    var isAnimating = false;

    function switchTab(tabs, display, nextIdx, dir) {
        if (isAnimating) return;
        isAnimating = true;

        tabs.forEach(function (t) { t.classList.remove('active'); });
        tabs[nextIdx].classList.add('active');

        var ref = tabs[nextIdx].dataset.ref;
        var nextContent;
        if (ref) {
            var refEl = document.getElementById(ref);
            nextContent = refEl ? refEl.innerHTML : '';
        } else {
            nextContent = tabs[nextIdx].dataset.content || '';
        }

        glitchOut(display, dir, function () {
            display.innerHTML = nextContent;
            glitchIn(display, dir, function () {
                isAnimating = false;
            });
        });
    }

    function glitchOut(el, dir, cb) {
        var duration = 160;
        var start = null;
        var slideX = dir * 16;

        function step(ts) {
            if (!start) start = ts;
            var p = Math.min((ts - start) / duration, 1);
            var ease = p * p;

            var tx = slideX * ease;
            var skew = dir * ease * 1.0;
            var opacity = 1 - ease;
            var rgb = ease * 5;

            el.style.transform = 'translateX(' + tx + 'px) skewX(' + skew + 'deg)';
            el.style.opacity = opacity;
            el.style.filter =
                'drop-shadow(' + (-rgb) + 'px 0 0 rgba(80,160,255,0.75)) ' +
                'drop-shadow(' + rgb + 'px 0 0 rgba(255,55,90,0.65)) ' +
                'brightness(' + (1 + ease * 0.25) + ')';

            if (p < 1) {
                requestAnimationFrame(step);
            } else {
                el.style.opacity = '0';
                cb();
            }
        }
        requestAnimationFrame(step);
    }

    function glitchIn(el, dir, cb) {
        var duration = 200;
        var start = null;
        var startX = -dir * 16;

        el.style.transform = 'translateX(' + startX + 'px) skewX(' + (-dir * 1.0) + 'deg)';
        el.style.opacity = '0';

        function step(ts) {
            if (!start) start = ts;
            var p = Math.min((ts - start) / duration, 1);
            var ease = 1 - Math.pow(1 - p, 3);

            var tx = startX * (1 - ease);
            var skew = -dir * 1.0 * (1 - ease);
            var opacity = ease;
            var rgb = (1 - ease) * 3;
            var brightness = 1 + (1 - ease) * 0.35;

            el.style.transform = 'translateX(' + tx + 'px) skewX(' + skew + 'deg)';
            el.style.opacity = opacity;
            el.style.filter =
                'drop-shadow(' + (-rgb) + 'px 0 0 rgba(80,160,255,0.65)) ' +
                'drop-shadow(' + rgb + 'px 0 0 rgba(255,55,90,0.55)) ' +
                'brightness(' + brightness + ')';

            if (p < 1) {
                requestAnimationFrame(step);
            } else {
                el.style.transform = '';
                el.style.opacity = '';
                el.style.filter = '';
                cb();
            }
        }
        requestAnimationFrame(step);
    }

if (document.readyState === 'loading') {
    document.addEventListener('DOMContentLoaded', initDocTabs);
} else {
    initDocTabs();
}

if (typeof mw !== 'undefined' && mw.hook) {
    mw.hook('wikipage.content').add(function () {
        initDocTabs();
    });
}

})();

/* =========================================
   Doc Section Switch — 좌측 섹션 전환
   ========================================= */

$(document).on('click', '.doc-nav-item[data-section]', function () {
    var name = $(this).attr('data-section');
    var display = document.getElementById('doc-main-display');
    var titleEl = document.getElementById('doc-center-title');
    var tabBar = document.getElementById('doc-tab-bar-text');

    if (!display) return;

    $('.doc-nav-item[data-section]').removeClass('active');
    $('.doc-nav-item[data-section="' + name + '"]').addClass('active');

    if (name === 'text') {
        if (titleEl) titleEl.textContent = '개요';
        if (tabBar) $(tabBar).show();
        var activeTab = tabBar ? tabBar.querySelector('.doc-tab.active') : null;
        if (!activeTab && tabBar) activeTab = tabBar.querySelector('.doc-tab');
        if (activeTab) {
            var ref = activeTab.dataset.ref;
            var refEl = ref ? document.getElementById(ref) : null;
            display.innerHTML = refEl ? refEl.innerHTML : (activeTab.dataset.content || '');
        }
    } else {
        if (titleEl) titleEl.textContent = name === 'factions' ? '세력' : name === 'people' ? '인물' : name;
        if (tabBar) $(tabBar).hide();
        var refEl = document.getElementById('doc-content-' + name);
        display.innerHTML = refEl ? refEl.innerHTML : '';
    }
});

/* =========================================
   CRT WebGL Renderer — cool-retro-term IBM DOS style
   ========================================= */
(function () {
    'use strict';

    function createNoiseTexture(gl) {
        var size = 512;
        var data = new Uint8Array(size * size * 4);
        var s = 12345;
        function rand() {
            s = (s * 1664525 + 1013904223) & 0xffffffff;
            return (s >>> 0) / 0xffffffff;
        }
        for (var i = 0; i < data.length; i++) {
            data[i] = (rand() * 255) | 0;
        }
        var tex = gl.createTexture();
        gl.bindTexture(gl.TEXTURE_2D, tex);
        gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, size, size, 0, gl.RGBA, gl.UNSIGNED_BYTE, data);
        gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.REPEAT);
        gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.REPEAT);
        gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
        gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR);
        return tex;
    }

    var VERT = [
        'attribute vec2 a_pos;',
        'varying vec2 v_uv;',
        'void main() {',
        '  v_uv = vec2(a_pos.x * 0.5 + 0.5, 0.5 - a_pos.y * 0.5);',
        '  gl_Position = vec4(a_pos, 0.0, 1.0);',
        '}'
    ].join('\n');

    var FRAG = [
        'precision mediump float;',
        'uniform sampler2D u_tex;',
        'uniform sampler2D u_noise;',
        'uniform vec2 u_res;',
        'uniform vec2 u_imgSize;',
        'uniform float u_time;',
        'uniform vec2 u_noiseScale;',
        'varying vec2 v_uv;',

        'float sum2(vec2 v) { return v.x + v.y; }',
        'float min2(vec2 v) { return min(v.x, v.y); }',
        'float rgb2grey(vec3 v) { return dot(v, vec3(0.21, 0.72, 0.04)); }',

        'vec2 coverUV(vec2 uv) {',
        '  float imgAR = u_imgSize.x / u_imgSize.y;',
        '  float scrAR = u_res.x / u_res.y;',
        '  float scale = imgAR / scrAR;',
        '  float offsetY = (1.0 - scale) * 0.5;',
        '  return vec2(uv.x, uv.y * scale + offsetY);',
        '}',

        'vec2 barrel(vec2 v, vec2 cc, float k) {',
        '  float ar = u_res.x / u_res.y;',
        '  vec2 c2 = cc;',
        '  if (ar > 1.0) c2.x /= ar; else c2.y *= ar;',
        '  float dist = dot(c2, c2) * k;',
        '  return v - cc * (1.0 + dist) * dist;',
        '}',

        'vec4 sampleInitialNoise(float t) {',
        '  return texture2D(u_noise, vec2(fract(t/2048.0), fract(t/1048576.0)));',
        '}',

        'vec4 sampleScreenNoise(vec2 uv) {',
        '  return texture2D(u_noise, u_noiseScale * uv);',
        '}',

        'vec3 applyRgbShift(vec2 texUV, float shift) {',
        '  vec2 d = vec2(shift, 0.0);',
        '  vec3 r = texture2D(u_tex, clamp(texUV + d, 0.0, 1.0)).rgb;',
        '  vec3 c = texture2D(u_tex, texUV).rgb;',
        '  vec3 l = texture2D(u_tex, clamp(texUV - d, 0.0, 1.0)).rgb;',
        '  return vec3(',
        '    l.r*0.10 + r.r*0.30 + c.r*0.60,',
        '    l.g*0.20 + r.g*0.20 + c.g*0.60,',
        '    l.b*0.30 + r.b*0.10 + c.b*0.60',
        '  );',
        '}',

        'vec3 applyBloom(vec2 texUV, float strength) {',
        '  vec2 px = 2.0 / u_res;',
        '  vec3 acc = vec3(0.0);',
        '  acc += texture2D(u_tex, clamp(texUV + vec2( px.x,  0.0), 0.0, 1.0)).rgb;',
        '  acc += texture2D(u_tex, clamp(texUV + vec2(-px.x,  0.0), 0.0, 1.0)).rgb;',
        '  acc += texture2D(u_tex, clamp(texUV + vec2( 0.0,  px.y), 0.0, 1.0)).rgb;',
        '  acc += texture2D(u_tex, clamp(texUV + vec2( 0.0, -px.y), 0.0, 1.0)).rgb;',
        '  acc += texture2D(u_tex, clamp(texUV + vec2( px.x,  px.y), 0.0, 1.0)).rgb * 0.5;',
        '  acc += texture2D(u_tex, clamp(texUV + vec2(-px.x,  px.y), 0.0, 1.0)).rgb * 0.5;',
        '  acc += texture2D(u_tex, clamp(texUV + vec2( px.x, -px.y), 0.0, 1.0)).rgb * 0.5;',
        '  acc += texture2D(u_tex, clamp(texUV + vec2(-px.x, -px.y), 0.0, 1.0)).rgb * 0.5;',
        '  return acc / 6.0 * strength;',
        '}',

        'vec3 applyScanlines(vec2 uv, vec3 col) {',
        '  float line = mod(uv.y * u_res.y, 2.0);',
        '  vec3 hi = ((1.0 + 0.30) - 0.2 * col) * col;',
        '  vec3 lo = ((1.0 - 0.30) + 0.1 * col) * col;',
        '  return line < 1.0 ? lo : hi;',
        '}',

'vec3 applyRasterization(vec2 uv, vec3 col) {',
'  float t = u_time;',
'  vec2 noiseUV = uv + vec2(fract(t * 0.030), fract(t * 0.060));',
'  float wobbleX = (texture2D(u_noise, noiseUV * 0.8).r - 0.5) * 0.0018;',
'  float wobbleY = (texture2D(u_noise, noiseUV * 0.8 + 0.5).r - 0.5) * 0.0008;',
'  vec2 wobbledUV = clamp(uv + vec2(wobbleX, wobbleY), 0.0, 1.0);',
'  vec3 wobbled = texture2D(u_tex, wobbledUV).rgb;',
'  return mix(col, wobbled, 0.35);',
'}',

        'float glowingLine(vec2 uv, float t) {',
'  float pos = fract(t * 0.2);',
'  float lineY = pos * (u_res.y + 330.0) - 120.0;',
        '  float y = uv.y * u_res.y;',
        '  return fract(smoothstep(-300.0, 0.0, y - lineY));',
        '}',

        'vec2 applyHSync(vec2 uv, vec4 noise, float strength) {',
        '  float randval = strength - noise.r;',
        '  float scale = step(0.0, randval) * randval * strength;',
        '  float freq = mix(4.0, 40.0, noise.g);',
        '  uv.x += sin((uv.y + u_time * 0.001) * freq) * scale;',
        '  return uv;',
        '}',

        'void main() {',
        '  vec2 cc = vec2(0.5) - v_uv;',

        '  float curvature = 0.18;',
        '  vec2 uv = barrel(v_uv, cc, curvature);',

        '  float inScreen = min2(step(vec2(0.0), uv) - step(vec2(1.0), uv));',
        '  if (inScreen < 0.5) { gl_FragColor = vec4(0.0, 0.0, 0.0, 1.0); return; }',

        '  vec2 texUV = clamp(coverUV(uv), 0.0, 1.0);',

        '  vec4 initNoise = sampleInitialNoise(u_time);',
        '  vec4 screenNoise = sampleScreenNoise(uv);',

        '  texUV = applyHSync(texUV, initNoise, 0.006);',
        '  texUV = clamp(texUV, 0.0, 1.0);',

        '  texUV += (vec2(screenNoise.b, screenNoise.a) - 0.5) * 0.0006;',
        '  texUV = clamp(texUV, 0.0, 1.0);',

        '  vec3 col = applyRgbShift(texUV, 0.003);',
        '  col += applyBloom(texUV, 0.22);',

        '  vec2 bpx = 1.5 / u_res;',
        '  vec3 blurCol = vec3(0.0);',
        '  blurCol += texture2D(u_tex, clamp(texUV + vec2(-bpx.x, -bpx.y), 0.0, 1.0)).rgb;',
        '  blurCol += texture2D(u_tex, clamp(texUV + vec2( 0.0,   -bpx.y), 0.0, 1.0)).rgb;',
        '  blurCol += texture2D(u_tex, clamp(texUV + vec2( bpx.x, -bpx.y), 0.0, 1.0)).rgb;',
        '  blurCol += texture2D(u_tex, clamp(texUV + vec2(-bpx.x,  0.0  ), 0.0, 1.0)).rgb;',
        '  blurCol += texture2D(u_tex, clamp(texUV + vec2( bpx.x,  0.0  ), 0.0, 1.0)).rgb;',
        '  blurCol += texture2D(u_tex, clamp(texUV + vec2(-bpx.x,  bpx.y), 0.0, 1.0)).rgb;',
        '  blurCol += texture2D(u_tex, clamp(texUV + vec2( 0.0,    bpx.y), 0.0, 1.0)).rgb;',
        '  blurCol += texture2D(u_tex, clamp(texUV + vec2( bpx.x,  bpx.y), 0.0, 1.0)).rgb;',
        '  col = mix(col, blurCol / 8.0, 0.40);',

        '  col = applyScanlines(uv, col);',
        '  col = applyRasterization(texUV, col);',

        '  float glow = glowingLine(uv, u_time);',
'  col += glow * 0.08 * vec3(0.85, 0.95, 1.0);',

        '  float dist = length(cc);',
        '  col += screenNoise.a * 0.07 * (1.0 - dist * 1.3);',

        '  float grey = rgb2grey(col);',
        '  vec3 phosphor = vec3(0.75, 0.88, 1.0);',
        '  col = mix(col, grey * phosphor, 0.35);',

        '  vec2 vig = v_uv * (1.0 - v_uv);',
        '  col *= pow(vig.x * vig.y * 15.0, 0.25);',

        '  col *= 1.0 + (initNoise.g - 0.5) * 0.06;',

        '  col += vec3(0.012) * (1.0 - dist) * (1.0 - dist);',

        '  col = pow(clamp(col, 0.0, 1.0), vec3(0.90));',

        '  gl_FragColor = vec4(col, 1.0);',
        '}'
    ].join('\n');

    function initCRTCanvas(screen, imgEl) {
        var existing = screen.querySelector('.crt-webgl-canvas');
        if (existing) existing.remove();

        var canvas = document.createElement('canvas');
        canvas.className = 'crt-webgl-canvas';
        canvas.style.cssText = 'position:absolute;inset:0;width:100%;height:100%;z-index:19;pointer-events:none;display:block;';
        screen.appendChild(canvas);

        var gl = canvas.getContext('webgl') || canvas.getContext('experimental-webgl');
        if (!gl) return;

        function compile(type, src) {
            var s = gl.createShader(type);
            gl.shaderSource(s, src);
            gl.compileShader(s);
            if (!gl.getShaderParameter(s, gl.COMPILE_STATUS)) {
                console.error('[CRT shader]', gl.getShaderInfoLog(s));
            }
            return s;
        }

        var prog = gl.createProgram();
        gl.attachShader(prog, compile(gl.VERTEX_SHADER, VERT));
        gl.attachShader(prog, compile(gl.FRAGMENT_SHADER, FRAG));
        gl.linkProgram(prog);
        gl.useProgram(prog);

        var buf = gl.createBuffer();
        gl.bindBuffer(gl.ARRAY_BUFFER, buf);
        gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([-1,-1, 1,-1, -1,1, 1,1]), gl.STATIC_DRAW);
        var aPos = gl.getAttribLocation(prog, 'a_pos');
        gl.enableVertexAttribArray(aPos);
        gl.vertexAttribPointer(aPos, 2, gl.FLOAT, false, 0, 0);

        var uTex     = gl.getUniformLocation(prog, 'u_tex');
        var uNoise   = gl.getUniformLocation(prog, 'u_noise');
        var uRes     = gl.getUniformLocation(prog, 'u_res');
        var uImgSize = gl.getUniformLocation(prog, 'u_imgSize');
        var uTime    = gl.getUniformLocation(prog, 'u_time');
        var uNoiseSc = gl.getUniformLocation(prog, 'u_noiseScale');

        var imgTex = gl.createTexture();
        gl.activeTexture(gl.TEXTURE0);
        gl.bindTexture(gl.TEXTURE_2D, imgTex);
        gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
        gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
        gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
        gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR);

        gl.activeTexture(gl.TEXTURE1);
        createNoiseTexture(gl);

        var texReady = false;
        function uploadImg() {
            if (!imgEl || !imgEl.complete || !imgEl.naturalWidth) return;
            try {
                gl.activeTexture(gl.TEXTURE0);
                gl.bindTexture(gl.TEXTURE_2D, imgTex);
                gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, imgEl);
                texReady = true;
            } catch(e) { console.error('[CRT] tex:', e); }
        }

        var lastW = 0, lastH = 0;
        function resize() {
            var w = screen.offsetWidth, h = screen.offsetHeight;
            if (w === lastW && h === lastH) return;
            lastW = w; lastH = h;
            canvas.width = w; canvas.height = h;
            gl.viewport(0, 0, w, h);
        }

        var raf;
        var visualTime = 0;
        var lastVisualNow = 0;

        function render(now) {
            var delta;
            raf = requestAnimationFrame(render);

            if (!lastVisualNow) lastVisualNow = now;
            delta = Math.max(0, Math.min(100, now - lastVisualNow));
            lastVisualNow = now;

            if (document.hidden || isClbiCompositorBusy()) return;
            if (!texReady) { uploadImg(); return; }

            visualTime += delta;
            resize();
            var t = visualTime / 1000;
            gl.uniform1i(uTex, 0);
            gl.uniform1i(uNoise, 1);
            gl.uniform2f(uRes, canvas.width, canvas.height);
            gl.uniform2f(uImgSize, imgEl.naturalWidth, imgEl.naturalHeight);
            gl.uniform1f(uTime, t);
            gl.uniform2f(uNoiseSc, canvas.width / 512.0, canvas.height / 512.0);
            gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4);
        }

        if (imgEl.complete && imgEl.naturalWidth) { uploadImg(); }
        else { imgEl.addEventListener('load', uploadImg); }

        render();
        screen._crtCleanup = function () { cancelAnimationFrame(raf); };
    }

    function initAllCRTScreens(root) {
        var scope = root && root.querySelectorAll ? root : document;
        scope.querySelectorAll('.crt-page-monitor-screen').forEach(function (screen) {
            if (screen.getAttribute('data-crt-webgl') === '1') return;
            screen.setAttribute('data-crt-webgl', '1');
            var frame = screen.closest('.crt-page-monitor-frame');
            if (!frame) return;
            var imgEl = frame.querySelector('.crt-page-monitor-slice-img, .crt-page-monitor-image-base img');
            if (imgEl && imgEl.complete && imgEl.naturalWidth) {
                initCRTCanvas(screen, imgEl);
            } else if (imgEl) {
                imgEl.addEventListener('load', function () { initCRTCanvas(screen, imgEl); });
            } else {
                var obs = new MutationObserver(function () {
                    var img = frame.querySelector('.crt-page-monitor-slice-img');
                    if (!img) return;
                    obs.disconnect();
                    if (img.complete && img.naturalWidth) {
                        initCRTCanvas(screen, img);
                    } else {
                        img.addEventListener('load', function () { initCRTCanvas(screen, img); });
                    }
                });
                obs.observe(frame, { childList: true, subtree: true });
            }
        });
    }

    $(function () { initAllCRTScreens(document); });

    if (typeof mw !== 'undefined' && mw.hook) {
        mw.hook('wikipage.content').add(function ($c) {
            document.querySelectorAll('.crt-page-monitor-screen').forEach(function (s) {
                if (s._crtCleanup) s._crtCleanup();
                s.removeAttribute('data-crt-webgl');
            });
            initAllCRTScreens($c && $c[0] ? $c[0] : document);
        });
    }
})();

/* =========================================
Progress System UI
MediaWiki:Common.js controlled frontend
========================================= */
(function (mw, $) {
    'use strict';

    if (window.ProgressSystemWebUiInitialized) return;
    window.ProgressSystemWebUiInitialized = true;

    var api = null;

    function withApi(done, fail) {
        if (api) {
            done(api);
            return;
        }

        if (!mw.loader || typeof mw.loader.using !== 'function') {
            if (typeof fail === 'function') fail();
            return;
        }

        mw.loader.using(['mediawiki.api']).then(function () {
            api = new mw.Api();
            done(api);
        }, function () {
            if (typeof fail === 'function') fail();
        });
    }

    var inFlightPageIds = new Set();
    var handledPageIds = new Set();
    var notificationQueue = [];
    var notificationActive = false;
    var summaryRequested = false;
    var currentSummary = null;
    var pendingSummary = null;
    var pendingOptions = null;
    var visibilityBound = false;
    var barTimerA = null;
    var barTimerB = null;
    var barTimerC = null;
    var summaryRetryTimer = null;
    var summaryRetryAttempts = 0;

    function isLoggedIn() {
        return !!mw.config.get('wgUserName');
    }

    function getPageId() {
        var id = parseInt(mw.config.get('wgArticleId') || 0, 10);
        return Number.isFinite(id) ? id : 0;
    }

    function isRewardableClientSide() {
        if (!isLoggedIn()) return false;
        if (parseInt(mw.config.get('wgNamespaceNumber'), 10) !== 0) return false;
        if (mw.config.get('wgIsMainPage')) return false;
        if (getPageId() <= 0) return false;
        return true;
    }

    function getPanelHtml() {
        return '' +
            '<div id="progress-panel" class="profile-progress-block is-syncing" aria-live="polite" data-progress-state="syncing">' +
                '<div class="progress-title-row" hidden></div>' +
                '<div class="progress-level-row">' +
                    '<span class="progress-level-label">SYNC</span>' +
                    '<span class="progress-total-xp">— XP</span>' +
                '</div>' +
                '<div class="progress-xp-bar" aria-hidden="true">' +
                    '<div class="progress-xp-gain"></div>' +
                    '<div class="progress-xp-fill"></div>' +
                '</div>' +
                '<div class="progress-sub-row">' +
                    '<span class="progress-xp-next">SYNCING</span>' +
                    '<span class="progress-daily-xp">TODAY —</span>' +
                '</div>' +
                '<div class="progress-discovery-row">DISCOVERED —</div>' +
            '</div>';
    }

    function getDividerHtml() {
        /* 프로필 패널 최신 규칙: 레벨 패널과 버튼 영역 사이에 별도 나눔선은 만들지 않는다. */
        return '';
    }

    function setPanelSync($panel) {
        if (!$panel || !$panel.length) return;

        $panel.addClass('is-syncing').removeClass('is-max-level').attr('data-progress-state', 'syncing');
        $panel.find('.progress-title-row').text('').prop('hidden', true);
        $panel.find('.progress-level-label').text('SYNC');
        $panel.find('.progress-total-xp').text('— XP');
        $panel.find('.progress-xp-next').text('SYNCING');
        $panel.find('.progress-daily-xp').text('TODAY —');
        $panel.find('.progress-discovery-row').text('DISCOVERED —');
        $panel.find('.progress-xp-fill').css({ transition: 'none', width: '0%' });
        $panel.find('.progress-xp-gain').css({ transition: 'none', width: '0%', opacity: 0 });
    }

    function placePanel($panel) {
        var $right = $('#clbi-right-sidebar');
        if (!$right.length) return false;

        var $userBox = $right.children('.clbi-right-box').first();
        if (!$userBox.length) return false;

        var $buttonArea = $userBox.children('.clbi-right-content').first();
        var $oldFallback = $panel.closest('.progress-panel-fallback');

        if ($buttonArea.length) {
            var $divider = $('#profile-progress-divider');

            $panel.insertBefore($buttonArea);

            if (!$divider.length) {
                $divider = $(getDividerHtml());
            }

            $divider.insertAfter($panel);
        } else {
            $('#profile-progress-divider').remove();
            $userBox.append($panel);
        }

        if ($oldFallback.length && !$oldFallback.find('#progress-panel').length) {
            $oldFallback.remove();
        }

        return true;
    }

    function ensurePanel() {
        if (!isLoggedIn()) return $();

        var $right = $('#clbi-right-sidebar');
        if (!$right.length) return $();

        var $panel = $('#progress-panel');

        if (!$panel.length) {
            $panel = $(getPanelHtml());
            if (!placePanel($panel)) return $();
            setPanelSync($panel);
        } else {
            $panel.addClass('profile-progress-block');
            placePanel($panel);

            if (!currentSummary && $panel.attr('data-progress-state') !== 'syncing') {
                setPanelSync($panel);
            }
        }

        return $('#progress-panel');
    }

    function clampPercent(value) {
        return Math.max(0, Math.min(100, value || 0));
    }

    function hasXpNotification(items) {
        if (!items || !items.length) return false;
        return items.some(function (item) {
            return item && item.type === 'xp' && parseInt(item.amount || 0, 10) > 0;
        });
    }

    function clearBarTimers() {
        [barTimerA, barTimerB, barTimerC].forEach(function (timer) {
            if (timer) clearTimeout(timer);
        });
        barTimerA = null;
        barTimerB = null;
        barTimerC = null;
    }

    function setBarInstant($fill, $gain, percent) {
        clearBarTimers();
        percent = clampPercent(percent);
        $fill.css({ transition: 'none', width: percent + '%' });
        $gain.css({ transition: 'none', left: '0%', width: '0%', opacity: 0 });
        if ($fill[0]) $fill[0].offsetHeight;
        $fill.css({ transition: '' });
        $gain.css({ transition: '' });
    }

    function animateGain($fill, $gain, fromPercent, toPercent, levelChanged) {
        clearBarTimers();

        fromPercent = clampPercent(fromPercent);
        toPercent = clampPercent(toPercent);

        $fill.css({ transition: 'none', width: fromPercent + '%' });

        if (levelChanged) {
            var firstDelta = Math.max(0, 100 - fromPercent);

            $gain.css({
                transition: 'none',
                opacity: firstDelta > 0 ? 1 : 0,
                left: fromPercent + '%',
                width: firstDelta + '%'
            });

            if ($fill[0]) $fill[0].offsetHeight;

            barTimerA = setTimeout(function () {
                $fill.css({
                    transition: 'width 540ms cubic-bezier(0.22, 0.7, 0.18, 1)',
                    width: '100%'
                });
            }, 260);

            barTimerB = setTimeout(function () {
                $fill.css({ transition: 'none', width: '0%' });
                $gain.css({ transition: 'none', opacity: toPercent > 0 ? 1 : 0, left: '0%', width: toPercent + '%' });

                if ($fill[0]) $fill[0].offsetHeight;

                $fill.css({
                    transition: 'width 460ms cubic-bezier(0.22, 0.7, 0.18, 1)',
                    width: toPercent + '%'
                });
            }, 860);

            barTimerC = setTimeout(function () {
                $gain.css({ transition: 'opacity 180ms ease', opacity: 0 });
            }, 1380);

            return;
        }

        var delta = Math.max(0, toPercent - fromPercent);

        if (delta <= 0.15) {
            setBarInstant($fill, $gain, toPercent);
            return;
        }

        $gain.css({
            transition: 'none',
            opacity: 1,
            left: fromPercent + '%',
            width: delta + '%'
        });

        if ($fill[0]) $fill[0].offsetHeight;

        barTimerA = setTimeout(function () {
            $fill.css({
                transition: 'width 560ms cubic-bezier(0.22, 0.7, 0.18, 1)',
                width: toPercent + '%'
            });
        }, 260);

        barTimerB = setTimeout(function () {
            $gain.css({ transition: 'opacity 180ms ease', opacity: 0 });
        }, 940);
    }

    function updatePanel(summary, options) {
        if (!summary) return;

        options = options || {};

        var $panel = ensurePanel();
        if (!$panel.length) {
            pendingSummary = $.extend({}, summary);
            pendingOptions = $.extend({}, options);
            return;
        }

        var level = summary.level || 1;
        var totalXp = summary.totalXp || 0;
        var xpIntoLevel = summary.xpIntoLevel || 0;
        var xpForNext = summary.xpForNextLevel || 1;
        var percent = clampPercent(summary.progressPercent);
        var isMaxLevel = !!summary.isMaxLevel;
        var dailyXp = summary.dailyXp || 0;
        var discoveries = summary.discoveryCount || 0;
        var title = summary.equippedTitle || summary.title || '';

        $panel.removeClass('is-syncing').toggleClass('is-max-level', isMaxLevel).attr('data-progress-state', 'ready');
        $panel.find('.progress-level-label').text((isMaxLevel ? 'MAX ' : 'LVL ') + level);
        $panel.find('.progress-total-xp').text(totalXp + ' XP');
        $panel.find('.progress-xp-next').text(isMaxLevel ? 'MAX LEVEL' : (xpIntoLevel + ' / ' + xpForNext + ' TO NEXT'));
        $panel.find('.progress-daily-xp').text('TODAY ' + dailyXp + ' XP');
        $panel.find('.progress-discovery-row').text('DISCOVERED ' + discoveries);

        var $title = $panel.find('.progress-title-row');
        if (title) {
            $title.text(title).prop('hidden', false);
        } else {
            $title.text('').prop('hidden', true);
        }

        var $fill = $panel.find('.progress-xp-fill');
        var $gain = $panel.find('.progress-xp-gain');
        var animate = !!options.animateGain && currentSummary && totalXp > (currentSummary.totalXp || 0);

        if (animate) {
            animateGain(
                $fill,
                $gain,
                clampPercent(currentSummary.progressPercent),
                percent,
                level !== (currentSummary.level || 1)
            );
        } else {
            setBarInstant($fill, $gain, percent);
        }

        currentSummary = $.extend({}, summary);
        pendingSummary = null;
        pendingOptions = null;
        if (summaryRetryTimer) {
            clearTimeout(summaryRetryTimer);
            summaryRetryTimer = null;
        }
        summaryRetryAttempts = 0;
    }

    function clearSummaryRetry() {
        if (summaryRetryTimer) clearTimeout(summaryRetryTimer);
        summaryRetryTimer = null;
        summaryRetryAttempts = 0;
    }

    function scheduleSummaryRetry(delay) {
        if (!isLoggedIn()) return;
        if (summaryRetryTimer) return;
        if (summaryRetryAttempts >= 12) return;

        summaryRetryAttempts += 1;
        summaryRetryTimer = setTimeout(function () {
            summaryRetryTimer = null;
            requestSummary();
        }, delay || 1800);
    }

    function requestSummary() {
        if (!isLoggedIn()) return;
        if (summaryRequested) return;

        summaryRequested = true;

        withApi(function (api) {
            api.get({
                action: 'progress_summary',
                format: 'json',
                formatversion: 2
            }).then(function (data) {
                var payload = data && data.progress_summary;
                if (payload && payload.available && payload.summary) {
                    clearSummaryRetry();
                    updatePanel(payload.summary, { animateGain: false });
                } else {
                    scheduleSummaryRetry(2200);
                }
            }).catch(function () {
                scheduleSummaryRetry(2200);
            }).always(function () {
                summaryRequested = false;
            });
        }, function () {
            summaryRequested = false;
            scheduleSummaryRetry(2200);
        });
    }

    function queueNotifications(items) {
        if (!items || !items.length) return;

        items.forEach(function (item) {
            if (!item) return;
            notificationQueue.push(item);
        });

        showNextNotification();
    }

    function notificationText(item) {
        if (item.type === 'xp') {
            return '+' + (item.amount || 0) + ' XP · ' + (item.label || '문서 열람');
        }

        if (item.type === 'achievement') {
            var xp = item.amount ? ' · +' + item.amount + ' XP' : '';
            return '업적 달성 · ' + (item.label || '새 업적') + xp;
        }

        if (item.type === 'level') {
            return item.label || '레벨 상승';
        }

        return item.label || '보상 획득';
    }

    function showNextNotification() {
        if (notificationActive) return;
        if (!notificationQueue.length) return;

        notificationActive = true;
        var item = notificationQueue.shift();
        var $root = $('#progress-toast-root');

        if (!$root.length) {
            $('body').append('<div id="progress-toast-root"></div>');
            $root = $('#progress-toast-root');
        }

        var $toast = $('<div class="progress-toast"></div>');
        $toast.text(notificationText(item));
        $root.append($toast);

        requestAnimationFrame(function () {
            $toast.addClass('is-visible');
        });

        setTimeout(function () {
            $toast.removeClass('is-visible');
            setTimeout(function () {
                $toast.remove();
                notificationActive = false;
                showNextNotification();
            }, 220);
        }, 2600);
    }

    function applyPendingSummaryIfPossible() {
        if (!pendingSummary) return;
        updatePanel(pendingSummary, pendingOptions || { animateGain: false });
    }

    function handlePageView() {
        ensurePanel();
        applyPendingSummaryIfPossible();

        if (!isRewardableClientSide()) {
            requestSummary();
            return;
        }

        var pageId = getPageId();
        if (handledPageIds.has(pageId)) {
            requestSummary();
            return;
        }

        if (inFlightPageIds.has(pageId)) {
            requestSummary();
            return;
        }

        inFlightPageIds.add(pageId);

        withApi(function (api) {
            api.postWithToken('csrf', {
                action: 'progress_view',
                format: 'json',
                formatversion: 2,
                errorformat: 'plaintext',
                pageid: pageId
            }).then(function (data) {
                var payload = data && data.progress_view;
                if (!payload) return;

                handledPageIds.add(pageId);

                var animate = hasXpNotification(payload.notifications);

                if (payload.summary) {
                    updatePanel(payload.summary, { animateGain: animate });
                }

                if (payload.notifications && payload.notifications.length) {
                    queueNotifications(payload.notifications);
                }
            }).catch(function () {
                requestSummary();
            }).always(function () {
                inFlightPageIds.delete(pageId);
            });
        }, function () {
            inFlightPageIds.delete(pageId);
            requestSummary();
        });
    }

    function bindVisibilitySync() {
        if (visibilityBound) return;
        visibilityBound = true;

        document.addEventListener('visibilitychange', function () {
            if (document.visibilityState === 'visible') {
                requestSummary();
            }
        });
    }

    function bootProgressSystem(reason) {
        ensurePanel();
        applyPendingSummaryIfPossible();

        if (isRewardableClientSide()) {
            handlePageView();
        } else {
            requestSummary();
        }

        setTimeout(function () {
            ensurePanel();
            applyPendingSummaryIfPossible();
            requestSummary();
        }, 350);

        setTimeout(function () {
            ensurePanel();
            applyPendingSummaryIfPossible();
            requestSummary();
        }, 1500);
    }

    function handleSpaPageView() {
        ensurePanel();
        applyPendingSummaryIfPossible();

        requestAnimationFrame(function () {
            setTimeout(function () {
                handlePageView();
            }, 80);
        });
    }

    function applySummary(summary, options) {
        updatePanel(summary, options || { animateGain: false });
    }

    window.ProgressSystemWebUi = {
        boot: bootProgressSystem,
        requestSummary: requestSummary,
        applySummary: applySummary,
        handlePageView: handlePageView,
        handleSpaPageView: handleSpaPageView,
        ensurePanel: ensurePanel
    };

    $(function () {
        bindVisibilitySync();
        bootProgressSystem('documentReady');
    });

    mw.hook('wikipage.content').add(function () {
        ensurePanel();
        applyPendingSummaryIfPossible();
        setTimeout(function () {
            ensurePanel();
            applyPendingSummaryIfPossible();
            requestSummary();
        }, 120);
    });
})(mediaWiki, jQuery);


/* CLBI Nations / Historical Events year tabs
 * Mirrors the country information panel model:
 * active tab uses .is-active/aria-selected and inactive pages use hidden.
 */
(function (mw, $) {
    'use strict';

    function activateClbiNationsHistoryYear(panel, targetYear) {
        var tabs;
        var pages;

        if (!panel || !targetYear) return false;

        tabs = Array.prototype.slice.call(panel.querySelectorAll('.clbi-nations-history-year-button[data-year]'));
        pages = Array.prototype.slice.call(panel.querySelectorAll('.clbi-nations-history-page[data-year-panel]'));

        if (!tabs.length || !pages.length) return false;

        tabs.forEach(function (tab) {
            var active = tab.getAttribute('data-year') === targetYear;
            tab.classList.toggle('is-active', active);
            tab.setAttribute('aria-selected', active ? 'true' : 'false');
            tab.setAttribute('tabindex', active ? '0' : '-1');
        });

        pages.forEach(function (page) {
            var active = page.getAttribute('data-year-panel') === targetYear;
            page.classList.toggle('is-active', active);

            if (active) {
                page.removeAttribute('hidden');
            } else {
                page.setAttribute('hidden', 'hidden');
            }
        });

        return true;
    }

    function moveClbiNationsHistoryYear(panel, direction) {
        var tabs;
        var activeIndex;
        var nextIndex;
        var target;

        if (!panel) return false;

        tabs = Array.prototype.slice.call(panel.querySelectorAll('.clbi-nations-history-year-button[data-year]'));
        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;
        target = tabs[nextIndex].getAttribute('data-year');

        if (activateClbiNationsHistoryYear(panel, target)) {
            tabs[nextIndex].focus();
            return true;
        }

        return false;
    }

    function initClbiNationsHistoryYearTabs(root) {
        var scope = root && root.querySelectorAll ? root : document;
        var panels = scope.querySelectorAll('.clbi-nations-history-panel');

        Array.prototype.forEach.call(panels, function (panel) {
            if (panel.getAttribute('data-clbi-history-tabs-ready') === '1') return;

            panel.setAttribute('data-clbi-history-tabs-ready', '1');

            panel.addEventListener('click', function (event) {
                var tab = event.target.closest ? event.target.closest('.clbi-nations-history-year-button[data-year]') : null;

                if (!tab || !panel.contains(tab)) return;

                if (activateClbiNationsHistoryYear(panel, tab.getAttribute('data-year'))) {
                    event.preventDefault();
                }
            });

            panel.addEventListener('keydown', function (event) {
                var tab = event.target.closest ? event.target.closest('.clbi-nations-history-year-button[data-year]') : null;
                var handled = false;

                if (!tab || !panel.contains(tab)) return;

                if (event.key === 'ArrowLeft') handled = moveClbiNationsHistoryYear(panel, -1);
                else if (event.key === 'ArrowRight') handled = moveClbiNationsHistoryYear(panel, 1);
                else if (event.key === 'Home') handled = activateClbiNationsHistoryYear(panel, (panel.querySelector('.clbi-nations-history-year-button[data-year]') || {}).getAttribute && panel.querySelector('.clbi-nations-history-year-button[data-year]').getAttribute('data-year'));
                else if (event.key === 'End') {
                    var tabs = panel.querySelectorAll('.clbi-nations-history-year-button[data-year]');
                    var last = tabs[tabs.length - 1];
                    handled = last ? activateClbiNationsHistoryYear(panel, last.getAttribute('data-year')) : false;
                    if (handled) last.focus();
                }

                if (handled) {
                    event.preventDefault();
                    event.stopPropagation();
                }
            });
        });
    }

    window.initClbiNationsHistoryYearTabs = initClbiNationsHistoryYearTabs;

    $(function () {
        initClbiNationsHistoryYearTabs(document);
    });

    if (mw && mw.hook) {
        mw.hook('wikipage.content').add(function ($content) {
            initClbiNationsHistoryYearTabs($content && $content[0] ? $content[0] : document);
        });
    }
})(mediaWiki, jQuery);


/* =========================================
   Decoration runtime renderer
   ========================================= */
(function (mw) {
    'use strict';

    var REGISTRY_TITLE = 'MediaWiki:Decorations.json';
    var RENDERED_ATTR = 'data-wiki-decoration-rendered';
    var HOST_ATTR = 'data-wiki-decoration-host';
    var MAIN_PAGE_PLACEMENT = 'main-body-well';
    var MAIN_PAGE_TARGET = '.main-portal .main-body-well';
    var runtimeToken = 0;
    var lastRegistry = null;
    var lastRenderedPageKey = '';
    var scheduledRender = 0;
    var nationsPlacementObserver = null;
    var observedNationsStack = null;
    var mainPagePlacementObserver = null;
    var mainPagePlacementMutationObserver = null;
    var observedMainPagePortal = null;
    var scheduledMainPageRebase = 0;
    var pixelAssetCache = {};
    var pixelCanvasCache = {};

    function normalizePageName(value) {
        return String(value || '')
            .split('?')[0]
            .replace(/^\/index\.php\//, '')
            .replace(/_/g, ' ')
            .trim();
    }

    function currentPageKey() {
        var raw = mw && mw.config ? String(mw.config.get('wgPageName') || '') : '';
        return normalizePageName(raw) || raw || '대문';
    }

    function ensureMainPageBodyWell() {
        var portal;
        var topMount;
        var panel;
        var well;
        var manifesto;

        if (currentPageKey() !== '대문') return null;

        portal = document.querySelector('.main-portal');
        if (!portal) return null;

        topMount = portal.querySelector('[data-component="category-nav"]');
        panel = portal.querySelector('.main-body-panel');
        well = portal.querySelector('.main-body-well');
        manifesto = portal.querySelector('.main-manifesto');

        if (!panel) {
            panel = document.createElement('div');
            panel.className = 'main-body-panel';
            panel.setAttribute('data-main-body-panel-generated', '1');

            if (topMount && topMount.parentNode) {
                if (topMount.nextSibling) {
                    topMount.parentNode.insertBefore(panel, topMount.nextSibling);
                } else {
                    topMount.parentNode.appendChild(panel);
                }
            } else {
                portal.appendChild(panel);
            }
        }

        if (!well) {
            well = document.createElement('div');
            well.className = 'main-body-well';
            well.setAttribute('data-main-body-well-generated', '1');
            panel.appendChild(well);
        } else if (well.parentNode !== panel) {
            panel.appendChild(well);
        }

        if (manifesto && manifesto.parentNode !== well) {
            well.insertBefore(manifesto, well.firstChild);
        }

        return well;
    }


    function cssAttrEscape(value) {
        return String(value || '').replace(/\\/g, '\\\\').replace(/"/g, '\\"');
    }

    function getActiveNationsEra() {
        var content = document.querySelector('.clbi-nations-era-content.is-active[data-era-content]:not([hidden])');
        var title;
        var globe;

        if (content) return content.getAttribute('data-era-content') || '';

        title = document.querySelector('.clbi-nations-era-title-plate.is-active[data-era]');
        if (title) return title.getAttribute('data-era') || '';

        globe = document.querySelector('.clbi-nations-globe-window[data-nations-globe]');
        if (globe) {
            return globe.getAttribute('data-current-era') || globe.getAttribute('data-nations-current-era') || globe.getAttribute('data-era-year') || '';
        }

        return '';
    }

    function getActiveNationsEraPanel() {
        var era = getActiveNationsEra();
        var selector;
        if (!era) return document.querySelector('.clbi-nations-era-content.is-active[data-era-content]:not([hidden])');
        selector = '.clbi-nations-era-content[data-era-content="' + cssAttrEscape(era) + '"]';
        return document.querySelector(selector) || document.querySelector('.clbi-nations-era-content.is-active[data-era-content]:not([hidden])');
    }

    function getActiveNationsContinent() {
        var eraPanel = getActiveNationsEraPanel();
        var root = eraPanel || document;
        var tab = root.querySelector('.clbi-nations-tabpanel-tab.is-active[data-continent], .clbi-nations-tabpanel-tab[aria-selected="true"][data-continent]');
        var panel;

        if (tab) return tab.getAttribute('data-continent') || '';

        panel = root.querySelector('.clbi-nations-tabpanel-continent.is-active[data-continent-panel]');
        if (panel) return panel.getAttribute('data-continent-panel') || '';

        return '';
    }

    function getDecorationNationsBodySelector(era) {
        if (era) {
            return '.clbi-nations-era-content[data-era-content="' + cssAttrEscape(era) + '"] .clbi-nations-tabpanel-body';
        }
        return '.clbi-nations-era-content.is-active[data-era-content]:not([hidden]) .clbi-nations-tabpanel-body, .clbi-nations-tabpanel-body';
    }

    /*
    Decoration semantic placement resolver
    -----------------------------------------
    장식 저장 데이터의 placement 값은 "사용자가 고른 의미상 위치"를 나타낸다.
    resolver는 그 의미값을 실제 DOM 부착 위치와 표시 조건으로 번역한다.

    예: 시대 문서에서 사용자가 1950년 / 아메리카를 지정하면 의미상 scope는
    그 조합이지만, 이미지를 붙일 기준면은 대륙 패널 자체가 아니라
    .clbi-nations-tabpanel-body이다. 따라서 placement=nations-continent-body는
    .clbi-nations-tabpanel-body에 이미지를 붙이고, 현재 활성 연도와 대륙이 저장값과
    일치할 때만 렌더링한다.

    유지보수 규칙:
    - 새 조합형 문서가 생기면 entry.target을 매번 특수하게 저장하지 말고 placement를 추가한다.
    - DevTools.js 에디터 미리보기와 Common.js 런타임 렌더러의 resolver는 같은 의미를 가져야 한다.
    - target은 물리적 기준면, era/continent 같은 필드는 표시 조건으로 다룬다.
    */
    function shouldUseNationsBodyPlacement(entry) {
        var placement = String(entry && entry.placement || '').trim();
        var target = String(entry && entry.target || '').trim();
        var era = String(entry && entry.era || '').trim();
        var continent = String(entry && entry.continent || '').trim();

        if (!document.querySelector('.clbi-nations-panel-stack')) return false;
        if (placement === 'nations-continent-body') return true;
        if (era || continent) return true;
        if (target.indexOf('clbi-nations-tabpanel-continent') !== -1) return true;
        return false;
    }

    function resolveDecorationPlacement(entry) {
        var placement = String(entry && entry.placement || '').trim();
        var selector = String(entry && entry.target || '').trim() || '.liberty-content-main';
        var era = String(entry && entry.era || '').trim();
        var continent = String(entry && entry.continent || '').trim();
        var target;

        if (!placement && shouldUseNationsBodyPlacement(entry)) {
            placement = 'nations-continent-body';
        }

        if (placement === 'boot-gate' || placement === 'loading-screen') {
            selector = '#boot-gate-screen .boot-gate-decoration-layer, #boot-gate-screen';
            target = document.querySelector(selector);
            return {
                placement: placement,
                target: target,
                targetSelector: selector,
                visible: !!document.getElementById('boot-gate-screen')
            };
        }

        if (placement === 'nations-continent-body') {
            selector = getDecorationNationsBodySelector(era);
            target = document.querySelector(selector) || document.querySelector('.clbi-nations-tabpanel-body') || document.querySelector(String(entry && entry.target || '').trim());
            return {
                placement: placement,
                target: target,
                targetSelector: selector,
                visible: (!era || era === getActiveNationsEra()) && (!continent || continent === getActiveNationsContinent())
            };
        }

        /*
        대문의 일반 장식은 저장된 target과 관계없이 본문 우물 안에서 렌더링한다.
        우물이 overflow:hidden이므로 #1d1d1d 프레임 밖으로 나갈 수 없다.
        boot/loading 및 국가 패널 전용 placement는 위 분기에서 기존 동작을 유지한다.
        */
        var mainPageWell = ensureMainPageBodyWell();
        if (mainPageWell) {
            return {
                placement: MAIN_PAGE_PLACEMENT,
                target: mainPageWell,
                targetSelector: MAIN_PAGE_TARGET,
                visible: true
            };
        }

        return {
            placement: placement,
            target: document.querySelector(selector),
            targetSelector: selector,
            visible: true
        };
    }

    function normalizeNumber(value, fallback) {
        var n = parseFloat(value);
        return Number.isFinite(n) ? n : fallback;
    }

    function normalizeBool(value, fallback) {
        if (value === true || value === 'true' || value === '1' || value === 1) return true;
        if (value === false || value === 'false' || value === '0' || value === 0) return false;
        return fallback;
    }

    function normalizeSrc(src) {
        src = String(src || '').trim();
        if (!src) return '';
        if (/^(?:https?:)?\/\//i.test(src) || src.charAt(0) === '/' || src.indexOf('data:') === 0 || src.indexOf('blob:') === 0) return src;
        return '/index.php/Special:Redirect/file/' + encodeURIComponent(src);
    }


    function normalizeAssetType(entry) {
        var type = String(entry && entry.assetType || '').trim().toLowerCase();
        var ref = String(entry && (entry.asset || entry.src) || '').trim();

        if (type === 'clbi-pixel-json' || type === 'pixel-rle' || type === 'pixel-json') return 'pixel-json';
        if (!type && /\.json(?:[?#].*)?$/i.test(ref)) return 'pixel-json';
        return type || 'image';
    }

    function isPixelJsonDecoration(entry) {
        return normalizeAssetType(entry) === 'pixel-json';
    }

    function getPixelJsonRef(entry) {
        return String(entry && (entry.asset || entry.src) || '').trim();
    }

    function normalizePixelJsonUrl(ref) {
        ref = String(ref || '').trim();
        if (!ref) return '';
        if (/^(?:https?:)?\/\//i.test(ref) || ref.charAt(0) === '/' || ref.indexOf('data:') === 0 || ref.indexOf('blob:') === 0) return ref;
        if (/^(?:file|파일):/i.test(ref)) return '/index.php/Special:Redirect/file/' + encodeURIComponent(ref.replace(/^(?:file|파일):/i, ''));
        if (mw && mw.util && typeof mw.util.getUrl === 'function') {
            if (ref.indexOf(':') !== -1) {
                return mw.util.getUrl(ref, { action: 'raw', ctype: 'application/json' });
            }
            return mw.util.getUrl('MediaWiki:' + ref, { action: 'raw', ctype: 'application/json' });
        }
        return ref;
    }

    function parsePixelColor(value) {
        var text;
        var m;
        if (Array.isArray(value)) {
            return [
                Math.max(0, Math.min(255, Number(value[0]) || 0)),
                Math.max(0, Math.min(255, Number(value[1]) || 0)),
                Math.max(0, Math.min(255, Number(value[2]) || 0)),
                value.length > 3 ? Math.max(0, Math.min(255, Number(value[3]) || 0)) : 255
            ];
        }
        text = String(value || '').trim();
        m = text.match(/^#([0-9a-f]{3}|[0-9a-f]{4}|[0-9a-f]{6}|[0-9a-f]{8})$/i);
        if (!m) return [0, 0, 0, 0];
        text = m[1];
        if (text.length === 3 || text.length === 4) {
            return [
                parseInt(text.charAt(0) + text.charAt(0), 16),
                parseInt(text.charAt(1) + text.charAt(1), 16),
                parseInt(text.charAt(2) + text.charAt(2), 16),
                text.length === 4 ? parseInt(text.charAt(3) + text.charAt(3), 16) : 255
            ];
        }
        return [
            parseInt(text.slice(0, 2), 16),
            parseInt(text.slice(2, 4), 16),
            parseInt(text.slice(4, 6), 16),
            text.length === 8 ? parseInt(text.slice(6, 8), 16) : 255
        ];
    }


    function decodePixelRle36(value) {
        var text = String(value || '').trim();
        var parts;
        var runs = [];
        var i;
        var x;
        var y;
        var len;
        var colorIndex;

        if (!text) return runs;
        parts = text.split(',');
        for (i = 0; i + 3 < parts.length; i += 4) {
            x = parseInt(parts[i], 36);
            y = parseInt(parts[i + 1], 36);
            len = parseInt(parts[i + 2], 36);
            colorIndex = parseInt(parts[i + 3], 36);
            if (!Number.isFinite(x) || !Number.isFinite(y) || !Number.isFinite(len) || !Number.isFinite(colorIndex)) continue;
            runs.push([x, y, len, colorIndex]);
        }
        return runs;
    }

    function normalizePixelAsset(doc) {
        var encoding = String(doc && (doc.encoding || doc.e) || '').trim().toLowerCase();
        var width = Math.round(Number(doc && (doc.width || doc.w)) || 0);
        var height = Math.round(Number(doc && (doc.height || doc.h)) || 0);
        var paletteSource = Array.isArray(doc && doc.palette) ? doc.palette : (Array.isArray(doc && doc.p) ? doc.p : []);
        var palette = paletteSource.map(parsePixelColor);
        var runs;

        /*
        CLBI Pixel Forge v0.2.3 compact format:
        - MediaWiki 단일 문서 크기 제한을 피하기 위해 사람이 읽기 쉬운 [[x,y,len,c], ...] 배열 대신
          base36 토큰 문자열을 쓴다.
        - 형식은 x,y,len,colorIndex를 4개 토큰 단위로 반복한 rle36이다.
        - 복원 후 좌표계는 기존 runs 배열과 완전히 같으며, 최종 CSS px에 1:1로 찍는다.
        */
        if ((encoding === 'rle36' || encoding === 'clbi-rle36') && typeof (doc && doc.r) === 'string') {
            runs = decodePixelRle36(doc.r);
        } else {
            runs = Array.isArray(doc && doc.runs) ? doc.runs : [];
        }

        if (!width || !height || width < 1 || height < 1) throw new Error('invalid pixel decoration size');
        if (width > 8192 || height > 8192) throw new Error('pixel decoration too large');
        return {
            type: String(doc && (doc.type || doc.t) || 'clbi-pixel-decoration'),
            version: Number(doc && (doc.version || doc.v)) || 1,
            encoding: encoding || 'runs',
            width: width,
            height: height,
            palette: palette,
            runs: runs
        };
    }

    function fetchPixelAsset(ref) {
        var url = normalizePixelJsonUrl(ref);
        var cached;
        if (!url) return Promise.reject(new Error('pixel json ref is empty'));
        cached = pixelAssetCache[url];
        if (cached) return cached.promise;
        cached = {
            promise: fetch(url, { credentials: 'same-origin', cache: 'force-cache' })
                .then(function (response) {
                    if (!response.ok) throw new Error('HTTP ' + response.status);
                    return response.json();
                })
                .then(normalizePixelAsset)
        };
        pixelAssetCache[url] = cached;
        return cached.promise;
    }

    function drawPixelAssetToCanvas(canvas, asset) {
        var ctx;
        var image;
        var data;
        var i;
        var run;
        var x;
        var y;
        var len;
        var color;
        var colorIndex;
        var p;
        var k;

        canvas.width = asset.width;
        canvas.height = asset.height;
        ctx = canvas.getContext('2d');
        ctx.imageSmoothingEnabled = false;
        image = ctx.createImageData(asset.width, asset.height);
        data = image.data;

        for (i = 0; i < asset.runs.length; i += 1) {
            run = asset.runs[i];
            if (!Array.isArray(run) || run.length < 4) continue;
            x = Math.round(Number(run[0]) || 0);
            y = Math.round(Number(run[1]) || 0);
            len = Math.round(Number(run[2]) || 0);
            colorIndex = Math.round(Number(run[3]) || 0);
            color = asset.palette[colorIndex];
            if (!color || len <= 0 || y < 0 || y >= asset.height || x >= asset.width) continue;
            if (x < 0) {
                len += x;
                x = 0;
            }
            len = Math.min(len, asset.width - x);
            for (k = 0; k < len; k += 1) {
                p = ((y * asset.width) + x + k) * 4;
                data[p] = color[0];
                data[p + 1] = color[1];
                data[p + 2] = color[2];
                data[p + 3] = color[3];
            }
        }

        ctx.putImageData(image, 0, 0);
    }

    function preparePixelCanvas(ref) {
        var url = normalizePixelJsonUrl(ref);
        var cached;
        if (!url) return Promise.reject(new Error('pixel json ref is empty'));
        cached = pixelCanvasCache[url];
        if (cached) return cached.promise;
        cached = {};
        cached.promise = fetchPixelAsset(ref).then(function (asset) {
            var canvas = document.createElement('canvas');
            drawPixelAssetToCanvas(canvas, asset);
            canvas.style.imageRendering = 'pixelated';
            canvas.setAttribute('data-decoration-asset-size', asset.width + 'x' + asset.height);
            cached.canvas = canvas;
            cached.width = asset.width;
            cached.height = asset.height;
            return canvas;
        });
        pixelCanvasCache[url] = cached;
        return cached.promise;
    }

    function getPreparedPixelCanvasSync(ref) {
        var url = normalizePixelJsonUrl(ref);
        var cached = url ? pixelCanvasCache[url] : null;
        return cached && cached.canvas ? cached.canvas : null;
    }

    function queryDecorationTarget(selector) {
        selector = String(selector || '').trim();
        if (!selector) return null;
        try {
            return document.querySelector(selector);
        } catch (err) {
            return null;
        }
    }

    function usesCanonicalMainPageCoordinates(entry, actualTarget) {
        var placement = String(entry && entry.placement || '').trim();
        var selector = String(entry && entry.target || '').trim();
        var declaredTarget;

        if (placement === MAIN_PAGE_PLACEMENT) return true;
        if (!actualTarget || !selector) return false;

        declaredTarget = queryDecorationTarget(selector);
        return declaredTarget === actualTarget;
    }

    function getDecorationLocalPosition(entry, actualTarget) {
        var x = normalizeNumber(entry && entry.x, 0);
        var y = normalizeNumber(entry && entry.y, 0);
        var sourceX = x;
        var sourceY = y;
        var sourceSelector;
        var sourceTarget;
        var sourceRect;
        var actualRect;
        var sourceOriginX;
        var sourceOriginY;
        var actualOriginX;
        var actualOriginY;

        /*
        대문 데코 좌표 보존
        -----------------------------------------
        저장된 x/y는 기존 entry.target 좌표계를 기준으로 만들어졌다.
        데코를 main-body-well 안으로 옮길 때 x/y를 그대로 적용하면
        더 아래에서 시작하는 새 좌표계 때문에 장식이 바닥으로 밀린다.

        실제 화면상의 위치는 유지하고 부모만 우물로 바꾼다.
        기존 기준면의 padding-box 원점과 새 우물의 padding-box 원점 차이를
        정수 px로 더해 새 로컬 좌표를 만든다.
        */
        if (
            currentPageKey() === '대문' &&
            actualTarget &&
            actualTarget.classList &&
            actualTarget.classList.contains('main-body-well') &&
            !usesCanonicalMainPageCoordinates(entry, actualTarget)
        ) {
            sourceSelector = String(entry && entry.target || '').trim() ||
                '.liberty-content-main';
            sourceTarget = queryDecorationTarget(sourceSelector) ||
                document.querySelector('.liberty-content-main') ||
                document.querySelector('.main-portal');

            if (sourceTarget && sourceTarget !== actualTarget) {
                sourceRect = sourceTarget.getBoundingClientRect();
                actualRect = actualTarget.getBoundingClientRect();

                sourceOriginX = sourceRect.left + normalizeNumber(sourceTarget.clientLeft, 0);
                sourceOriginY = sourceRect.top + normalizeNumber(sourceTarget.clientTop, 0);
                actualOriginX = actualRect.left + normalizeNumber(actualTarget.clientLeft, 0);
                actualOriginY = actualRect.top + normalizeNumber(actualTarget.clientTop, 0);

                x += Math.round(sourceOriginX - actualOriginX);
                y += Math.round(sourceOriginY - actualOriginY);
            }
        }

        return {
            x: Math.round(x),
            y: Math.round(y),
            sourceX: Math.round(sourceX),
            sourceY: Math.round(sourceY),
            sourceSelector: sourceSelector || ''
        };
    }

    function applyDecorationBaseStyle(node, entry, actualTarget) {
        var position = getDecorationLocalPosition(entry, actualTarget);

        node.style.left = position.x + 'px';
        node.style.top = position.y + 'px';
        node.style.opacity = String(normalizeNumber(entry.opacity, 1));
        node.style.zIndex = String(Math.round(normalizeNumber(entry.zIndex, 0)));
        node.style.pointerEvents = String(entry.pointerEvents || 'none');

        if (position.sourceSelector) {
            node.setAttribute('data-decoration-coordinate-source', position.sourceSelector);
            node.setAttribute('data-decoration-source-x', String(position.sourceX));
            node.setAttribute('data-decoration-source-y', String(position.sourceY));
            node.setAttribute('data-decoration-local-x', String(position.x));
            node.setAttribute('data-decoration-local-y', String(position.y));
        }

        if (entry.blendMode) node.style.mixBlendMode = String(entry.blendMode);
        if (entry.filter) node.style.filter = String(entry.filter);
        if (entry.transform) node.style.transform = String(entry.transform);
    }

    function isDecorationNodeActiveForNationsState(node) {
        var era = node ? String(node.getAttribute('data-decoration-era') || '').trim() : '';
        var continent = node ? String(node.getAttribute('data-decoration-continent') || '').trim() : '';
        if (era && era !== getActiveNationsEra()) return false;
        if (continent && continent !== getActiveNationsContinent()) return false;
        return true;
    }

    function setDecorationNodeVisibility(node, visible) {
        if (!node) return;
        visible = visible !== false;
        /* aria-hidden stays true because wiki decorations are purely visual. */
        if (node.hidden !== !visible) node.hidden = !visible;
        if (node.style.display !== (visible ? '' : 'none')) node.style.display = visible ? '' : 'none';
        node.setAttribute('data-decoration-visible', visible ? '1' : '0');
    }

    function updateDecorationVisibility(root) {
        var scope = root && root.querySelectorAll ? root : document;
        var count = 0;
        Array.prototype.forEach.call(scope.querySelectorAll('[' + RENDERED_ATTR + '="1"]'), function (node) {
            setDecorationNodeVisibility(node, isDecorationNodeActiveForNationsState(node));
            count += 1;
        });
        return count;
    }

    function hasRenderedDecorations(root) {
        var scope = root && root.querySelector ? root : document;
        return !!(scope && scope.querySelector && scope.querySelector('[' + RENDERED_ATTR + '="1"]'));
    }

    function applyPixelJsonDecoration(entry, target, visible) {
        var ref = getPixelJsonRef(entry);
        var template;
        var canvas;
        var ctx;
        if (!ref) return false;

        function makeCanvasFromTemplate(source) {
            var out = document.createElement('canvas');
            var outCtx;
            out.className = 'wiki-decoration wiki-decoration-pixel-json';
            out.setAttribute(RENDERED_ATTR, '1');
            out.setAttribute('aria-hidden', 'true');
            out.setAttribute('data-decoration-id', String(entry.id || ''));
            out.setAttribute('data-decoration-asset-type', 'pixel-json');
            if (entry.placement) out.setAttribute('data-decoration-placement', String(entry.placement));
            if (entry.era) out.setAttribute('data-decoration-era', String(entry.era));
            if (entry.continent) out.setAttribute('data-decoration-continent', String(entry.continent));
            out.width = source.width;
            out.height = source.height;
            out.style.imageRendering = 'pixelated';
            out.style.width = source.width + 'px';
            out.style.height = source.height + 'px';
            out.setAttribute('data-decoration-asset-size', source.width + 'x' + source.height);
            outCtx = out.getContext('2d');
            outCtx.imageSmoothingEnabled = false;
            outCtx.drawImage(source, 0, 0);
            applyDecorationBaseStyle(out, entry, target);
            setDecorationNodeVisibility(out, visible);
            return out;
        }

        template = getPreparedPixelCanvasSync(ref);
        if (template) {
            target.appendChild(makeCanvasFromTemplate(template));
            return true;
        }

        /* Fallback path only.  A full entry pack should prepare the template before the
           normal UI is released, so users should not see a blank decoration canvas. */
        preparePixelCanvas(ref).then(function (source) {
            if (!target || !target.parentNode || !source) return;
            target.appendChild(makeCanvasFromTemplate(source));
        }).catch(function () {});

        return true;
    }

    function preloadPixelAssetsForRegistry(registry) {
        var promises = [];
        decorationList(registry).forEach(function (entry) {
            var ref;
            if (!entry || !matchesPage(entry) || !isPixelJsonDecoration(entry)) return;
            ref = getPixelJsonRef(entry);
            if (!ref) return;
            promises.push(preparePixelCanvas(ref).catch(function () { return null; }));
        });
        return Promise.all(promises);
    }

    function decorationList(registry) {
        if (!registry || typeof registry !== 'object') return [];
        if (Array.isArray(registry)) return registry;
        if (Array.isArray(registry.decorations)) return registry.decorations;
        return [];
    }

    function matchesPage(entry) {
        var page = currentPageKey();
        var underscored = page.replace(/ /g, '_');
        var pages = entry && entry.pages;
        var target = entry && entry.page;
        var i;

        if (normalizeBool(entry && entry.global, false)) return true;
        if (String(entry && entry.placement || '').trim() === 'boot-gate' || String(entry && entry.placement || '').trim() === 'loading-screen') {
            return true;
        }
        if (normalizePageName(target).toLowerCase() === '__boot__' || normalizePageName(target).toLowerCase() === 'loading-screen') return true;
        if (!target && !pages) return true;

        if (Array.isArray(pages)) {
            for (i = 0; i < pages.length; i += 1) {
                if (normalizePageName(pages[i]) === page || String(pages[i] || '') === underscored) return true;
            }
        }

        target = normalizePageName(target);
        return target === page || target === underscored;
    }

    function clearRendered(root) {
        var scope = root && root.querySelectorAll ? root : document;
        Array.prototype.forEach.call(scope.querySelectorAll('[' + RENDERED_ATTR + '="1"]'), function (node) {
            if (node.parentNode) node.parentNode.removeChild(node);
        });
    }

    function ensureHost(target) {
        var style;
        if (!target) return;
        target.setAttribute(HOST_ATTR, '1');
        style = window.getComputedStyle ? window.getComputedStyle(target) : null;
        if (style && style.position === 'static') target.style.position = 'relative';
    }

    function applyDecoration(entry) {
        var resolved;
        var target;
        var src;
        var img;
        var width;
        var height;

        if (!entry || typeof entry !== 'object' || normalizeBool(entry.enabled, true) === false) return false;
        resolved = resolveDecorationPlacement(entry);
        if (!resolved) return false;
        target = resolved.target;
        if (!target) return false;

        ensureHost(target);

        if (isPixelJsonDecoration(entry)) {
            return applyPixelJsonDecoration(entry, target, resolved.visible !== false);
        }

        src = normalizeSrc(entry.src);
        if (!src) return false;

        img = document.createElement('img');
        img.className = 'wiki-decoration';
        img.setAttribute(RENDERED_ATTR, '1');
        img.setAttribute('aria-hidden', 'true');
        img.setAttribute('alt', '');
        img.setAttribute('decoding', 'async');
        img.setAttribute('loading', 'eager');
        img.setAttribute('data-decoration-id', String(entry.id || ''));
        if (entry.placement) img.setAttribute('data-decoration-placement', String(entry.placement));
        if (entry.era) img.setAttribute('data-decoration-era', String(entry.era));
        if (entry.continent) img.setAttribute('data-decoration-continent', String(entry.continent));
        img.src = src;

        applyDecorationBaseStyle(img, entry, target);
        width = normalizeNumber(entry.width, NaN);
        height = normalizeNumber(entry.height, NaN);
        if (Number.isFinite(width) && width > 0) img.style.width = width + 'px';
        if (Number.isFinite(height) && height > 0) img.style.height = height + 'px';
        if (entry.objectFit) img.style.objectFit = String(entry.objectFit);
        setDecorationNodeVisibility(img, resolved.visible !== false);

        target.appendChild(img);
        return true;
    }

    function mainPageLayoutSignature() {
        var source = document.querySelector('.liberty-content-main');
        var well = ensureMainPageBodyWell();
        var sourceRect;
        var wellRect;

        if (!source || !well) return '';
        sourceRect = source.getBoundingClientRect();
        wellRect = well.getBoundingClientRect();

        return [
            Math.round((wellRect.left + normalizeNumber(well.clientLeft, 0)) - (sourceRect.left + normalizeNumber(source.clientLeft, 0))),
            Math.round((wellRect.top + normalizeNumber(well.clientTop, 0)) - (sourceRect.top + normalizeNumber(source.clientTop, 0))),
            Math.round(wellRect.width),
            Math.round(wellRect.height)
        ].join('|');
    }

    function isMainPageDecorationLayoutReady() {
        var portal = document.querySelector('.main-portal');
        var panel = portal && portal.querySelector('.main-body-panel');
        var topMount = portal && portal.querySelector('[data-component="category-nav"]');

        if (!portal || !panel || !ensureMainPageBodyWell()) return false;
        if (!document.body || !document.body.classList.contains('clbi-main-page')) return false;
        if (topMount && !topMount.querySelector('.portal-category-nav')) return false;
        return true;
    }

    function waitForMainPageDecorationLayout() {
        var started;
        var previous = '';
        var stableFrames = 0;

        if (currentPageKey() !== '대문') return Promise.resolve(true);
        started = Date.now();

        return new Promise(function (resolve) {
            function check() {
                var signature = mainPageLayoutSignature();

                if (signature && signature === previous) stableFrames += 1;
                else stableFrames = 0;
                previous = signature;

                if ((isMainPageDecorationLayoutReady() && stableFrames >= 2) || Date.now() - started >= 1200) {
                    resolve(true);
                    return;
                }

                if (window.requestAnimationFrame) window.requestAnimationFrame(check);
                else window.setTimeout(check, 16);
            }

            check();
        });
    }

    function rebaseLegacyMainPageDecorations() {
        var well;

        if (currentPageKey() !== '대문') return;
        well = ensureMainPageBodyWell();
        if (!well) return;

        document.querySelectorAll('[' + RENDERED_ATTR + '="1"][data-decoration-coordinate-source]').forEach(function (node) {
            var position = getDecorationLocalPosition({
                target: node.getAttribute('data-decoration-coordinate-source') || '.liberty-content-main',
                x: normalizeNumber(node.getAttribute('data-decoration-source-x'), 0),
                y: normalizeNumber(node.getAttribute('data-decoration-source-y'), 0)
            }, well);

            node.style.left = position.x + 'px';
            node.style.top = position.y + 'px';
            node.setAttribute('data-decoration-local-x', String(position.x));
            node.setAttribute('data-decoration-local-y', String(position.y));
        });
    }

    function scheduleMainPageDecorationRebase() {
        if (scheduledMainPageRebase) return;
        scheduledMainPageRebase = window.requestAnimationFrame ? window.requestAnimationFrame(function () {
            scheduledMainPageRebase = 0;
            rebaseLegacyMainPageDecorations();
        }) : window.setTimeout(function () {
            scheduledMainPageRebase = 0;
            rebaseLegacyMainPageDecorations();
        }, 0);
    }

    function bindMainPagePlacementRefresh() {
        var portal = document.querySelector('.main-portal');
        var nodes;

        if (currentPageKey() !== '대문' || !portal) {
            if (mainPagePlacementObserver) mainPagePlacementObserver.disconnect();
            if (mainPagePlacementMutationObserver) mainPagePlacementMutationObserver.disconnect();
            observedMainPagePortal = null;
            return;
        }

        if (observedMainPagePortal !== portal) {
            if (mainPagePlacementObserver) mainPagePlacementObserver.disconnect();
            if (mainPagePlacementMutationObserver) mainPagePlacementMutationObserver.disconnect();
            observedMainPagePortal = portal;

            if (typeof ResizeObserver === 'function') {
                mainPagePlacementObserver = new ResizeObserver(scheduleMainPageDecorationRebase);
                nodes = [
                    document.querySelector('.liberty-content-main'),
                    portal,
                    portal.querySelector('[data-component="category-nav"]'),
                    portal.querySelector('.main-body-panel'),
                    portal.querySelector('.main-body-well')
                ];
                nodes.forEach(function (node) {
                    if (node) mainPagePlacementObserver.observe(node);
                });
            }

            /*
            장식 좌표는 크기 변화에만 의존한다. portal subtree DOM 변경을 감시하면
            프레임 SVG와 선언문 애니메이션이 장식 재배치를 반복 호출한다.
            ResizeObserver와 명시적 렌더 시점만 사용한다.
            */
            mainPagePlacementMutationObserver = null;
        }

        scheduleMainPageDecorationRebase();
    }

    function renderForCurrentLayout(registry, token) {
        if (currentPageKey() !== '대문') {
            if (token === runtimeToken) render(registry);
            return Promise.resolve(registry);
        }

        return waitForMainPageDecorationLayout().then(function () {
            if (token === runtimeToken) render(registry);
            return registry;
        });
    }

    function render(registry) {
        var token = runtimeToken;
        lastRegistry = registry || { decorations: [] };
        lastRenderedPageKey = currentPageKey();
        bindNationsPlacementRefresh();
        clearRendered(document);
        decorationList(lastRegistry).forEach(function (entry) {
            if (token === runtimeToken && matchesPage(entry)) applyDecoration(entry);
        });
        bindMainPagePlacementRefresh();
    }

    function renderPrepared() {
        var registry = getPreparedRegistrySync() || lastRegistry;
        var token = runtimeToken;
        if (!registry) return false;
        renderForCurrentLayout(registry, token);
        return true;
    }

    function syncDecorationState() {
        if (lastRenderedPageKey === currentPageKey() && hasRenderedDecorations(document)) {
            updateDecorationVisibility(document);
            return true;
        }
        if (lastRegistry) {
            renderForCurrentLayout(lastRegistry, runtimeToken);
            return true;
        }
        return renderPrepared();
    }

    function scheduleRenderFromCache() {
        if (scheduledRender) return;
        scheduledRender = window.requestAnimationFrame ? window.requestAnimationFrame(function () {
            scheduledRender = 0;
            if (!syncDecorationState()) reload();
        }) : window.setTimeout(function () {
            scheduledRender = 0;
            if (!syncDecorationState()) reload();
        }, 0);
    }

    function scheduleDecorationVisibilityUpdate() {
        if (scheduledRender) return;
        scheduledRender = window.requestAnimationFrame ? window.requestAnimationFrame(function () {
            scheduledRender = 0;
            if (lastRenderedPageKey !== currentPageKey() || !hasRenderedDecorations(document)) {
                if (!syncDecorationState()) reload();
                return;
            }
            updateDecorationVisibility(document);
        }) : window.setTimeout(function () {
            scheduledRender = 0;
            if (lastRenderedPageKey !== currentPageKey() || !hasRenderedDecorations(document)) {
                if (!syncDecorationState()) reload();
                return;
            }
            updateDecorationVisibility(document);
        }, 0);
    }

    function bindNationsPlacementRefresh() {
        var stack = document.querySelector('.clbi-nations-panel-stack');

        if (!stack || observedNationsStack === stack) return;
        observedNationsStack = stack;

        if (nationsPlacementObserver) {
            nationsPlacementObserver.disconnect();
        }

        if (typeof MutationObserver === 'function') {
            nationsPlacementObserver = new MutationObserver(scheduleDecorationVisibilityUpdate);
            nationsPlacementObserver.observe(stack, {
                subtree: true,
                attributes: true,
                attributeFilter: ['class', 'hidden', 'aria-selected', 'data-current-era', 'data-nations-current-era', 'data-era-year']
            });
        }
    }

    function getPreparedRegistrySync() {
        var registry = null;
        var url;
        try {
            if (window.EntryStore && typeof window.EntryStore.getJsonSync === 'function') {
                registry = window.EntryStore.getJsonSync(REGISTRY_TITLE);
                if (!registry && mw && mw.util && typeof mw.util.getUrl === 'function') {
                    url = mw.util.getUrl(REGISTRY_TITLE, { action: 'raw', ctype: 'application/json' });
                    registry = window.EntryStore.getJsonSync(url);
                }
            }
        } catch (err) {}
        return registry && typeof registry === 'object' ? registry : null;
    }

    function fetchRegistry() {
        var url;
        var prepared = getPreparedRegistrySync();
        if (prepared) return Promise.resolve(prepared);
        if (!mw || !mw.util || typeof fetch !== 'function') return Promise.resolve({ decorations: [] });
        url = mw.util.getUrl(REGISTRY_TITLE, {
            action: 'raw',
            ctype: 'application/json'
        });
        return fetch(url, { credentials: 'same-origin', cache: 'force-cache' })
            .then(function (response) {
                if (!response.ok) throw new Error('HTTP ' + response.status);
                return response.text();
            })
            .then(function (text) {
                if (!text || !text.trim()) return { decorations: [] };
                return JSON.parse(text);
            })
            .catch(function () {
                return { decorations: [] };
            });
    }

    function reload() {
        var token;
        runtimeToken += 1;
        token = runtimeToken;
        return fetchRegistry().then(function (registry) {
            return preloadPixelAssetsForRegistry(registry).then(function () {
                return renderForCurrentLayout(registry, token);
            });
        });
    }

    document.addEventListener('click', function (event) {
        var target = event.target && event.target.closest ? event.target.closest('.clbi-nations-tabpanel-tab[data-continent], [data-nations-era-move]') : null;
        if (target) window.setTimeout(scheduleDecorationVisibilityUpdate, 0);
    }, true);

    function decorationDiagnostics() {
        return {
            build: '20260713-main-body-well-coordinate-003',
            hasRegistry: !!lastRegistry,
            entries: decorationList(lastRegistry).length,
            rendered: document.querySelectorAll('[' + RENDERED_ATTR + '="1"]').length,
            preparedRegistry: !!getPreparedRegistrySync(),
            pixelAssets: Object.keys(pixelAssetCache || {}).length,
            pixelCanvases: Object.keys(pixelCanvasCache || {}).length,
            visibilityOnlySync: true,
            mainPageCoordinateSurface: MAIN_PAGE_TARGET,
            mainPagePlacementObserved: !!observedMainPagePortal,
            lastRenderedPage: lastRenderedPageKey,
            visibleRendered: document.querySelectorAll('[' + RENDERED_ATTR + '="1"][data-decoration-visible="1"]').length,
            scheduled: !!scheduledRender
        };
    }

    window.Decorations = window.Decorations || {};
    window.Decorations.reload = reload;
    window.Decorations.render = render;
    window.Decorations.renderPrepared = renderPrepared;
    window.Decorations.sync = syncDecorationState;
    window.Decorations.updateVisibility = updateDecorationVisibility;
    window.Decorations.diagnostics = decorationDiagnostics;
    window.Decorations.clear = clearRendered;
    window.Decorations.apply = applyDecoration;
    window.Decorations.pageKey = currentPageKey;
    window.Decorations.loadPixelAsset = fetchPixelAsset;
    window.Decorations.preparePixelCanvas = preparePixelCanvas;
    window.Decorations.getPreparedPixelCanvasSync = getPreparedPixelCanvasSync;
    window.Decorations.drawPixelAssetToCanvas = drawPixelAssetToCanvas;

    window.CLBI_DECORATIONS = window.CLBI_DECORATIONS || {};
    window.CLBI_DECORATIONS.reload = reload;
    window.CLBI_DECORATIONS.render = render;
    window.CLBI_DECORATIONS.renderPrepared = renderPrepared;
    window.CLBI_DECORATIONS.sync = syncDecorationState;
    window.CLBI_DECORATIONS.updateVisibility = updateDecorationVisibility;
    window.CLBI_DECORATIONS.diagnostics = decorationDiagnostics;
    window.CLBI_DECORATIONS.clear = clearRendered;
    window.CLBI_DECORATIONS.apply = applyDecoration;
    window.CLBI_DECORATIONS.pageKey = currentPageKey;
    window.CLBI_DECORATIONS.loadPixelAsset = fetchPixelAsset;
    window.CLBI_DECORATIONS.preparePixelCanvas = preparePixelCanvas;
    window.CLBI_DECORATIONS.getPreparedPixelCanvasSync = getPreparedPixelCanvasSync;
    window.CLBI_DECORATIONS.drawPixelAssetToCanvas = drawPixelAssetToCanvas;

    if (document.readyState === 'loading') {
        document.addEventListener('DOMContentLoaded', reload);
    } else {
        reload();
    }

    if (mw && mw.hook) {
        mw.hook('wikipage.content').add(function () {
            reload();
        });
    }
})(mediaWiki);

/* =========================================
   Unified Shortcuts loader
   ========================================= */
(function () {
    'use strict';

    if (!window.mw || !mw.loader) return;
    loadClbiRawScript('MediaWiki:Nation_List_Manager.js');
    loadClbiRawScript('MediaWiki:History_Event_Manager.js');
    loadClbiRawScript('MediaWiki:Shortcuts.js');
})();

/* =========================================
   Main page manifesto bitmap renderer
   Integrated build: no extra raw-script page, no image/data-URL decoding.
   New shared systems do not use the legacy CLBI prefix.
   ========================================= */
(function (window, document, mw) {
    'use strict';

    /*
    Retired compatibility block
    -----------------------------------------
    선언문은 실제 HTML과 ManifestoIntro의 실선 요소만 사용한다.
    이 구식 렌더러는 제목·본문과 완성형 구분선을 canvas에 한 번에 그려
    새 애니메이션의 선 뒤에 트랙이 나타날 수 있었다.

    기존 Common.js를 장기 캐시한 뒤 새 파일로 넘어오는 경우를 위해
    남아 있는 canvas와 상태만 제거하고 즉시 종료한다. 새 공개 이름에는
    프로젝트 접두사를 사용하지 않는 규칙을 유지한다.
    */
    function removeRetiredBitmap() {
        Array.prototype.forEach.call(
            document.querySelectorAll('.main-portal .main-manifesto-bitmap'),
            function (canvas) {
                if (canvas.parentNode) canvas.parentNode.removeChild(canvas);
            }
        );

        Array.prototype.forEach.call(
            document.querySelectorAll('.main-portal .main-manifesto-inner'),
            function (inner) {
                inner.classList.remove('is-bitmap-ready');
                [
                    'data-bitmap-width',
                    'data-bitmap-optical-shift-x',
                    'data-bitmap-state',
                    'data-bitmap-error'
                ].forEach(function (name) {
                    inner.removeAttribute(name);
                });
            }
        );
    }

    removeRetiredBitmap();

    try {
        if (mw && mw.hook) {
            mw.hook('wikipage.content').add(removeRetiredBitmap);
        }
    } catch (ignoreRetiredBitmapHook) {}

    window.MainPageBitmap = {
        version:'retired-20260713-divider-no-track-001',
        recalculate:removeRetiredBitmap,
        status:function () {
            return {
                integrated:false,
                state:'retired',
                canvas:!!document.querySelector('.main-portal .main-manifesto-bitmap')
            };
        }
    };

    return;

    var VERSION = '20260712-bitmap18-inline-002';
    var SELECTOR = '.main-portal .main-manifesto-inner';
    var CELL_W = 24;
    var CELL_H = 24;
    var GLYPHS = {"진":[19,0,0,0,0,0,12288,24576,24576,25472,25584,25056,25072,25568,26160,26136,24576,9088,8960,768,256,32512,15872,0,0],"창":[19,0,0,0,0,0,6144,12512,12480,12800,13280,78328,258272,12768,14128,13848,4360,3840,7040,12672,4480,8064,3840,0,0],"바":[19,0,0,0,0,0,6144,12288,12288,12672,12672,14080,13056,13064,258456,127472,12784,12504,12408,12312,12288,12288,4096,0,0],"다":[19,0,0,0,0,0,6144,12288,12288,12288,13056,12792,12400,12320,258096,128528,14328,13048,12304,12288,12288,12288,4096,0,0],"는":[19,0,0,0,0,0,0,96,192,192,14528,16064,960,0,130048,118780,56,448,448,128,128,16256,7936,0,0],"왕":[19,0,0,0,0,0,12288,28768,25568,25456,25392,25392,254944,24960,16064,10236,12344,7680,14080,12672,12672,16128,7936,0,0],"과":[19,0,0,0,0,0,12288,28672,24576,26560,26352,26112,26112,26208,255680,254912,29248,32736,25084,8192,8192,8192,8192,0,0],"노":[19,0,0,0,0,0,0,0,0,96,192,192,4288,15552,16320,7040,7168,3072,50688,131064,124,0,0,0,0],"예":[19,0,0,0,0,0,24576,60416,22528,22528,22544,24048,24504,23320,23320,24504,23024,22528,22528,18432,16384,16384,24576,0,0],"를":[19,0,0,0,0,0,7168,16320,6144,8128,448,15552,1984,122880,262140,16636,8128,8128,3840,8064,1792,960,65408,4096,0],"같":[19,0,0,0,0,0,6144,12288,12288,13248,13304,110976,127168,12384,12344,4104,3584,0,8064,1920,256,14720,8064,0,0],"은":[19,0,0,0,0,0,384,3968,6528,6336,6272,3968,1792,32768,130944,115708,16,448,448,128,128,16256,7936,0,0],"깊":[19,0,0,0,0,0,12288,24576,24576,26496,25592,25472,25024,24800,24624,8216,12288,16256,4864,15872,7680,31232,32640,256,0],"이":[19,0,0,0,0,0,12288,24576,24576,24576,24592,25072,25584,25368,25368,25496,25072,24640,24576,24576,24576,8192,8192,0,0],"로":[19,0,0,0,0,0,0,0,15360,16320,6272,7168,8128,384,14528,16320,7040,3072,50688,131064,124,0,0,0,0],"삼":[19,0,0,0,0,0,6144,12288,12352,12736,12480,78064,258272,13296,14136,13324,4096,16256,16256,4224,4480,8064,8064,0,0],"켰":[19,0,0,0,0,0,12288,24576,26112,26600,29560,32656,25072,31992,26720,8224,8216,13056,15104,5056,15232,28544,58992,0,0],"고":[19,0,0,0,0,0,0,0,0,7680,16352,12416,12288,12288,12544,13056,6912,4864,49920,131064,124,0,0,0,0],",":[8,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,12,12,8,0,0,0],"겨":[19,0,0,0,0,0,12288,24576,24576,24576,25344,26616,32560,31104,24768,31840,31792,24600,24584,24576,24576,24576,8192,0,0],"울":[19,0,0,0,0,0,1792,8064,6528,6272,7552,3840,114688,262140,18428,1536,16320,8128,3840,8064,1920,896,65280,0,0],"사":[19,0,0,0,0,0,6144,12288,12288,12352,12480,12480,12480,12400,258272,127920,14104,13828,13312,12288,12288,12288,4096,0,0],"람":[19,0,0,0,0,0,6144,12288,12288,13296,13240,78208,258552,12304,14224,13296,4096,15744,16256,4288,4480,8064,8064,0,0],"짐":[19,0,0,0,0,0,12288,24576,24576,25472,25584,24992,25072,26592,26160,9756,0,32512,32640,12672,12672,16128,16128,0,0],"승":[19,0,0,0,0,0,512,3584,1536,1792,8064,29056,24800,32768,130816,115708,272,3840,7040,4224,6272,8064,3840,0,0],"의":[19,0,0,0,0,0,28672,57344,24608,25536,26592,26160,26160,26464,25568,24576,24576,32704,25596,24576,24576,24576,8192,0,0],"살":[19,0,0,0,0,0,6144,12288,12352,12736,12480,110832,127456,13232,13848,5132,8064,8064,3840,8064,1792,896,65024,0,0],"을":[19,0,0,0,0,0,1792,8064,6528,6272,7552,3840,32768,130944,115708,0,16320,8128,3840,8064,1920,896,65280,0,0],"빛":[19,0,0,0,0,0,12288,24576,25344,25344,26376,25496,25592,25584,25552,25088,7168,12288,32640,6400,7680,32256,25472,0,0],"으":[19,0,0,0,0,0,0,0,128,3968,8064,4544,4288,6336,7616,3968,0,0,57344,131068,56,0,0,0,0],"얼":[19,0,0,0,0,0,12288,24576,24592,24816,25008,32536,31512,25016,25072,8192,16256,16256,7680,16128,3584,1792,130560,0,0],"렸":[19,0,0,0,0,0,12288,24576,24576,25584,32568,27008,31224,31792,26544,9200,8192,13056,15104,4928,15232,28544,58976,0,0],".":[8,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,12,12,0,0,0,0],"그":[19,0,0,0,0,0,0,0,0,7168,16352,12672,12288,12288,12288,12288,6144,4096,57344,131068,56,0,0,0,0],"들":[19,0,0,0,0,0,6144,16320,896,384,14528,16320,960,130048,118780,120,16320,8128,3840,8064,1920,896,65280,0,0],"돌":[19,0,0,0,0,0,6144,16320,896,384,14528,16320,8128,130048,253948,16504,16320,8128,3840,8064,1920,896,65280,0,0],"벽":[19,0,0,0,0,0,12288,24576,24832,25344,32520,31512,25560,32632,25584,9104,8192,31744,32640,8192,8192,8192,8192,0,0],"세":[19,0,0,0,0,0,24576,60416,22528,22528,22720,22720,22752,24432,22752,22960,23320,23300,22528,18432,16384,16384,24576,0,0],"우":[19,0,0,0,0,0,0,384,7936,6528,12480,6336,7552,3968,0,131008,116732,1552,1536,1536,1536,1536,512,0,0],"땅":[19,0,0,0,0,0,12288,28672,24576,28560,9200,74160,254128,30936,16376,12280,12544,7936,13184,12672,12672,8064,3840,0,0],"찢":[19,0,0,0,0,0,12288,24576,24704,26616,28656,26560,25456,28656,28056,26848,8192,32512,16128,7680,15872,25344,16768,0,0],"어":[19,0,0,0,0,0,12288,24576,24576,24576,24592,24816,25520,32536,27416,25496,25072,24640,24576,24576,24576,8192,8192,0,0],"길":[19,0,0,0,0,0,12288,24576,24576,26496,25592,25472,24768,24672,24624,8216,16256,16128,7680,16128,3584,1792,130560,0,0],"냈":[19,0,0,0,0,0,8192,58368,19456,27672,27704,31792,31792,32560,28656,27120,8704,13824,14080,13248,15232,28544,50272,0,0],"며":[19,0,0,0,0,0,12288,24576,24576,24576,25472,26600,32536,25368,25400,32560,25584,25056,24576,24576,24576,24576,8192,0,0],"쇠":[19,0,0,0,0,0,28672,57344,24704,24960,24960,25056,25536,26208,27888,24960,24960,32704,25596,24576,24576,24576,8192,0,0],"에":[19,0,0,0,0,0,24576,60416,22528,22528,22544,23024,23480,24344,24344,22968,23024,22528,22528,18432,16384,16384,24576,0,0],"제":[19,0,0,0,0,0,24576,60416,22528,22528,23488,23032,22976,24512,24184,23024,23344,23320,23052,18432,16384,16384,24576,0,0],"름":[19,0,0,0,0,0,6144,16320,6272,8128,960,14528,16320,384,130560,116732,56,15872,16320,4288,4544,7552,8064,0,0],"새":[19,0,0,0,0,0,24576,60416,23552,19456,27840,27840,31968,31856,23776,23984,20248,20228,16384,16384,16384,16384,24576,0,0],"겼":[19,0,0,0,0,0,12288,24576,24576,26496,30712,32640,25024,31968,26672,8216,8200,15104,15104,6976,15232,28608,58992,0,0],"감":[19,0,0,0,0,0,6144,12288,12288,13184,13304,78208,258240,12384,12336,4120,4096,16256,16320,4224,4480,8064,8064,0,0],"옥":[19,0,0,0,0,0,1792,7936,6528,6272,6528,8064,3584,1536,130816,116732,56,16128,15296,4096,4096,4096,4096,0,0],"되":[19,0,0,0,0,0,28672,57344,25088,26608,25072,24672,25632,26416,26608,25456,24960,32704,25596,24576,24576,24576,8192,0,0],"군":[19,0,0,0,0,0,0,7680,16352,12288,12288,12288,6144,126976,262140,18172,1536,1728,1472,192,192,16256,8064,0,0],"대":[19,0,0,0,0,0,24576,60416,23552,19456,28032,28144,31856,31776,23600,24344,20440,19960,16400,16384,16384,16384,24576,0,0],"나":[19,0,0,0,0,0,6144,12288,12288,12288,12296,12312,12336,12304,259600,128784,14256,12528,12320,12288,12288,12288,4096,0,0],"르":[19,0,0,0,0,0,0,0,6144,16320,6336,7168,8128,448,12480,16320,3968,0,57344,131068,56,0,0,0,0],"오":[19,0,0,0,0,0,0,0,256,7936,15232,12672,12416,12672,8064,7936,3072,1024,49664,131064,124,0,0,0,0],"래":[19,0,0,0,0,0,24576,60416,23552,19456,28144,27064,32128,32248,23664,23568,20240,20464,16880,16384,16384,16384,24576,0,0],"된":[19,0,0,0,0,0,28672,57344,26368,25584,24800,25696,28656,26608,29440,32736,25080,24832,25344,768,768,65280,15872,0,0],"원":[19,0,0,0,0,0,28672,57440,25536,26464,26160,26160,29664,32640,26592,26620,25360,25024,25536,768,768,65280,15872,0,0],"한":[19,0,0,0,0,0,6144,28704,12512,12800,14328,12784,258528,13088,12592,12784,12512,12416,4544,384,128,16256,7936,0,0],"함":[19,0,0,0,0,0,6144,12512,12480,12800,14328,78032,258528,13104,12720,12784,4192,15744,16256,4288,4480,8064,8064,0,0],"께":[19,0,0,0,0,0,24576,55296,55296,55296,55488,57336,57280,57184,57184,55600,55704,55500,55392,49152,49152,16384,16384,0,0],"음":[19,0,0,0,0,0,1920,8064,6528,6336,6336,3968,1792,32768,131040,16892,0,16128,16320,4288,4544,7552,8064,0,0],"손":[19,0,0,0,0,0,512,1536,1536,1920,7936,14720,26304,1584,130560,249852,120,448,448,128,128,16256,7936,0,0],"넘":[19,0,0,0,0,0,12288,24576,24576,24600,31792,31792,24624,28208,26608,8416,0,32512,32640,12672,12672,16256,15616,0,0],"갔":[19,0,0,0,0,0,6144,12288,12288,13184,13304,78208,258240,12384,12336,12312,4352,15104,6912,6528,7616,30656,25136,0,0],"\n":[8,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],"줄":[19,0,0,0,0,0,6144,16320,3520,3840,15296,12480,41056,130976,116732,1536,16320,8128,3840,8064,1920,896,65280,0,0],"처":[19,0,0,0,0,0,12288,24576,24800,25024,25344,26592,25072,32448,27616,25392,25360,24840,24832,24960,24960,8320,8192,0,0],"부":[19,0,0,0,0,0,6144,12288,28768,14400,15552,13248,8128,7872,0,131008,116732,1552,1536,1536,1536,1536,512,0,0],"터":[19,0,0,0,0,0,12288,24576,24576,25056,24768,25344,25584,31984,31840,25648,28568,26360,24624,24576,24576,8192,8192,0,0],"목":[19,0,0,0,0,0,14336,32512,15328,6336,3264,16064,7296,3072,130816,116732,56,16128,15296,4096,4096,4096,4096,0,0],"둘":[19,0,0,0,0,0,6144,16320,896,384,14528,16320,960,130944,116732,1592,16320,8128,3840,8064,1920,896,65280,0,0],"레":[19,0,0,0,0,0,24576,60416,22528,22528,23024,22968,22912,24568,22640,22544,23312,23536,23024,18432,16384,16384,24576,0,0],"있":[19,0,0,0,0,0,12288,24576,24576,24816,25072,25400,25368,25368,25072,8416,8192,15104,15104,7104,14720,28608,26224,0,0],"었":[19,0,0,0,0,0,12288,24576,24576,24816,25072,32568,32536,25496,25072,8416,8192,15104,15104,7104,14720,28608,26224,0,0],"끝":[19,0,0,0,0,0,0,14720,16368,13152,12672,12672,4480,130560,116732,56,3840,3584,8128,1920,384,16320,4032,0,0],"속":[19,0,0,0,0,0,512,3584,1536,1792,7936,12672,26336,1568,130560,116732,56,16128,15296,4096,4096,4096,4096,0,0],"뿌":[19,0,0,0,0,0,4096,30720,14080,15904,16352,13280,16320,14208,4608,130944,126972,1552,1536,1536,1536,1536,512,0,0],"리":[19,0,0,0,0,0,12288,24576,24576,24576,25568,25528,24960,25040,24688,25648,26160,26608,25056,24576,24576,8192,8192,0,0],"와":[19,0,0,0,0,0,12288,28672,24608,25568,25456,26160,26416,25584,254432,254336,28864,32736,25084,8192,8192,8192,8192,0,0],"빨":[19,0,0,0,0,0,12288,28672,26112,26176,26304,255944,255992,10200,10224,1728,16256,16256,7936,7936,1792,896,65024,0,0],"굶":[19,0,0,0,0,0,0,16128,13248,12288,12288,6144,118784,262140,18428,1536,14080,16368,13792,14272,14272,16352,16352,0,0],"주":[19,0,0,0,0,0,0,14336,16320,3264,1792,7936,12736,24672,32,131008,116732,1552,1536,1536,1536,1536,512,0,0],"림":[19,0,0,0,0,0,12288,24576,24576,25584,25400,25472,25072,24624,26544,9200,8192,15616,32640,12672,12672,16256,16128,0,0],"매":[19,0,0,0,0,0,24576,60416,23552,19456,28544,28644,32156,32024,23832,23992,19952,19504,16384,16384,16384,16384,24576,0,0],"였":[19,0,0,0,0,0,12288,24576,24576,24816,32752,32568,25368,32664,25072,8416,8192,15104,15104,7104,14720,28608,26224,0,0],"른":[19,0,0,0,0,0,4096,16320,6336,7744,4032,14528,16320,896,126976,253948,120,64,448,128,128,16256,7936,0,0],"아":[19,0,0,0,0,0,6144,12288,12288,12288,12304,12528,12720,13080,258840,127896,12784,12352,12288,12288,12288,12288,4096,0,0],"직":[19,0,0,0,0,0,12288,24576,24576,25472,25584,24992,25072,25568,26160,9752,0,31744,32640,8192,8192,12288,12288,0,0],"태":[19,0,0,0,0,0,24576,60416,23552,19680,27840,28032,32240,31856,23600,24336,20440,20472,16432,16384,16384,16384,24576,0,0],"지":[19,0,0,0,0,0,12288,24576,24576,24576,26496,25584,24992,24768,24816,25568,26160,27672,25612,24576,24576,24576,8192,0,0],"않":[19,0,0,0,0,0,6144,12288,12304,12528,12784,111384,258840,13208,12784,4160,7168,6240,32352,15456,13408,14176,8160,0,0],"자":[19,0,0,0,0,0,6144,12288,12288,12288,13184,13304,12720,12480,258296,127984,14128,13848,13324,12288,12288,12288,4096,0,0],"졌":[19,0,0,0,0,0,12288,24576,24576,25472,32752,31136,32240,32736,26160,9756,8704,13056,15104,4928,15232,28544,50784,0,0],"앞":[19,0,0,0,0,0,6144,12288,12304,12528,12784,111384,258840,13208,12784,4160,6144,8128,2432,7936,2816,15616,16320,128,0],"갈":[19,0,0,0,0,0,6144,12288,12288,13248,13304,127360,127168,12384,12344,4104,16256,8064,3840,8064,1792,896,65280,0,0],"때":[19,0,0,0,0,0,24576,52224,55296,55296,57096,56312,63920,63632,63704,56536,57336,57336,49216,49152,49152,16384,16384,0,0],"마":[19,0,0,0,0,0,6144,12288,12288,12288,13184,13284,13084,13080,258840,127792,12784,12464,12288,12288,12288,12288,4096,0,0],"듭":[19,0,0,0,0,0,6144,16320,1984,384,12480,16064,3008,32768,131008,115708,14336,12352,12480,14720,16256,8064,7808,0,0],"파":[19,0,0,0,0,0,6144,12288,12288,12288,13248,12792,12688,13296,258528,127392,16352,12412,12288,12288,12288,12288,4096,0,0],"각":[19,0,0,0,0,0,6144,12288,12288,13184,13304,12672,258240,12384,12336,4120,4096,16128,13248,12288,4096,4096,4096,0,0],"쥔":[19,0,0,0,0,0,28672,57856,26608,25584,25024,26592,28208,27664,28544,28668,25360,24960,25536,768,768,65280,15872,0,0],"당":[19,0,0,0,0,0,6144,12288,12288,13248,12536,77920,258096,15888,14328,12920,4352,7936,7040,12672,4480,8064,3840,0,0],"기":[19,0,0,0,0,0,12288,24576,24576,24576,25344,26616,25392,24960,24768,24672,24624,24600,24584,24576,24576,24576,8192,0,0],"면":[19,0,0,0,0,0,12288,24576,24576,26496,26604,32536,25368,32560,31728,25072,24576,25344,8960,768,768,32512,15872,0,0],"풀":[19,0,0,0,0,0,12288,16352,7296,8064,3328,32704,41440,131008,116732,1536,16320,8128,3840,8064,1920,896,65280,0,0],"려":[19,0,0,0,0,0,12288,24576,24576,24576,25568,32696,31104,25040,31856,27696,26160,26608,25056,24576,24576,8192,8192,0,0],"날":[19,0,0,0,0,0,6144,12288,12288,12312,12344,110640,128560,14256,13296,4192,8064,8064,3840,8064,1792,896,65024,0,0],"것":[19,0,0,0,0,0,12288,24576,24576,26496,25592,24960,32704,26848,24624,24600,11272,3072,1792,3584,15104,29056,24800,0,0],"라":[19,0,0,0,0,0,6144,12288,12288,12288,13280,12728,12672,12792,258168,127504,13840,14320,12784,12288,12288,12288,4096,0,0],"여":[19,0,0,0,0,0,12288,24576,24576,24576,24592,24816,32688,25368,25368,32664,25072,24640,24576,24576,24576,8192,8192,0,0],"므":[19,0,0,0,0,0,0,0,4096,31744,32736,6624,3264,1728,16064,7360,0,0,57344,131068,56,0,0,0,0],"번":[19,0,0,0,0,0,12288,24576,24576,25344,26376,26392,32728,25464,25584,25552,24576,25344,8960,768,768,32512,15872,0,0],"도":[19,0,0,0,0,0,0,0,0,15360,4064,960,384,14528,16064,7136,7360,3072,50688,131064,124,0,0,0,0],"느":[19,0,0,0,0,0,0,0,0,32,192,192,192,14528,16064,7104,0,0,57344,131068,56,0,0,0,0],"슨":[19,0,0,0,0,0,512,1536,1536,1920,7936,14720,24768,48,130048,118780,56,448,448,128,128,16256,7936,0,0],"해":[19,0,0,0,0,0,24576,60416,23552,19680,27968,28664,32240,32224,23984,23984,19952,19568,16384,16384,16384,16384,24576,0,0],"았":[19,0,0,0,0,0,6144,12288,12288,12528,12784,78616,258840,13208,12784,12512,4352,15104,6912,6528,7616,30656,25136,0,0],"침":[19,0,0,0,0,0,12288,24768,25024,25088,26592,25584,24800,25568,26416,9232,8,15616,32640,12672,12672,16256,16128,0,0],"내":[19,0,0,0,0,0,24576,58368,19456,19456,27656,27672,31792,31792,31760,24368,20464,19952,16384,16384,24576,24576,8192,0,0],"멎":[19,0,0,0,0,0,12288,24576,24576,26496,25576,32536,31512,25392,25072,8224,8192,32640,6912,3584,15872,25344,16768,0,0],"서":[19,0,0,0,0,0,12288,24576,24576,24640,24768,25024,24768,32496,24800,25520,26392,26124,25600,24576,24576,8192,8192,0,0],"긴":[19,0,0,0,0,0,12288,24576,24576,26368,25592,25520,25024,24768,24688,24632,24584,25472,8960,768,768,32512,15872,0,0],"붙":[19,0,0,0,0,0,6144,12288,14432,16064,13248,7872,38912,130816,116732,1552,3840,3584,8064,1920,384,15808,4032,0,0],"잡":[19,0,0,0,0,0,6144,12288,12288,13184,13304,111040,258288,13280,13872,5660,4096,12352,12416,14720,16256,8064,7296,0,0],"피":[19,0,0,0,0,0,12288,24576,24576,24576,26496,25592,25392,25568,25440,24992,28640,26744,24576,24576,24576,24576,8192,0,0],"가":[19,0,0,0,0,0,6144,12288,12288,12288,13056,13304,12728,12736,127168,258144,12336,12312,12300,12288,12288,12288,4096,0,0],"굴":[19,0,0,0,0,0,0,16128,13248,12288,12288,6144,126976,262140,18172,1536,16320,8128,3840,8064,1920,896,65280,0,0],"보":[19,0,0,0,0,0,0,0,6144,12288,28768,14528,16064,13248,16320,7360,3072,1024,50688,131064,124,0,0,0,0],"하":[19,0,0,0,0,0,6144,12288,12320,12512,12800,14332,12792,12512,259040,127792,12720,12528,12288,12288,12288,12288,4096,0,0],"입":[19,0,0,0,0,0,12288,24576,24576,24816,25584,25368,25368,25496,25072,8256,4096,28800,24960,14720,16128,16128,15616,0,0],"말":[19,0,0,0,0,0,6144,12288,12288,13184,13308,111384,127768,13240,12784,4096,8064,8064,3840,8064,1792,896,65024,0,0],"했":[19,0,0,0,0,0,24576,58368,23776,19776,28664,27888,32224,23984,19888,18672,96,13824,15872,13184,15232,28544,50272,0,0],"조":[19,0,0,0,0,0,0,0,0,15360,8128,3072,1792,16128,28864,25184,1568,1536,50688,131064,124,0,0,0,0],"올":[19,0,0,0,0,0,1792,8064,6528,6528,8064,3840,1536,128768,253948,16504,16320,8128,3840,8064,1920,896,65280,0,0],"수":[19,0,0,0,0,0,0,1536,3072,1792,1792,6912,29056,24800,32,131008,116732,1552,1536,1536,1536,1536,512,0,0],"록":[19,0,0,0,0,0,6144,16320,6272,8128,960,14528,16320,3968,128512,253948,120,15872,16320,4096,4096,4096,6144,0,0],"열":[19,0,0,0,0,0,12288,24576,24592,24816,32688,31512,25368,32696,25072,8192,16256,16256,7680,16128,3584,1792,130560,0,0],"단":[19,0,0,0,0,0,6144,28672,12288,13184,12792,12400,258096,13872,14232,14328,12336,12672,4480,384,384,16256,7936,0,0],"질":[19,0,0,0,0,0,12288,24576,24576,26496,25584,24992,25072,26464,26160,9240,16128,16128,7680,16128,3584,1792,130560,0,0],"히":[19,0,0,0,0,0,12288,24576,24576,25024,26112,26616,25584,25024,25440,25392,25392,25056,24576,24576,24576,8192,8192,0,0],"운":[19,0,0,0,0,0,384,3968,6528,6336,6272,3968,1792,32768,131040,17916,3072,1984,1472,128,192,16256,7936,0,0],"데":[19,0,0,0,0,0,24576,60416,22528,22528,22912,23024,22640,24352,22576,23320,23512,23032,22544,18432,16384,16384,24576,0,0],"끌":[19,0,0,0,0,0,0,14720,16368,13152,12672,12672,4480,130560,116732,56,16320,8128,3840,8064,1920,896,65280,0,0],"린":[19,0,0,0,0,0,12288,24576,24576,25568,25464,24960,25592,24624,26416,26608,24672,8320,9088,256,256,32512,15872,0,0],"게":[19,0,0,0,0,0,24576,60416,22528,22528,23424,23544,22960,24000,24512,22624,22576,22552,22536,18432,16384,16384,24576,0,0],"누":[19,0,0,0,0,0,0,96,192,192,192,15552,16320,960,0,131008,116732,1552,1536,1536,1536,1536,512,0,0],"인":[19,0,0,0,0,0,12288,24576,24576,24624,25072,25400,25368,25368,25072,24800,24576,9088,8960,256,256,32512,15872,0,0],"쳐":[19,0,0,0,0,0,12288,24576,24800,25024,25344,26592,32752,31936,25568,32560,25360,24840,24832,24960,24960,8320,8192,0,0],"문":[19,0,0,0,0,0,12288,15872,15328,6336,3264,7872,7360,32768,131040,17916,3072,1984,1472,128,192,16256,7936,0,0],"몸":[19,0,0,0,0,0,14336,32512,15328,6336,3264,16064,7296,3072,130816,116732,56,16128,16320,4288,4544,7552,8064,0,0],"식":[19,0,0,0,0,0,12288,24576,24768,24960,25024,24816,24800,25568,26160,9244,0,31744,32640,8192,8192,12288,12288,0,0],"억":[19,0,0,0,0,0,12288,24576,24576,24816,25072,32536,32536,25368,25072,8416,0,31744,32640,12288,12288,12288,12288,0,0],"전":[19,0,0,0,0,0,12288,24576,24576,25472,25592,24992,31984,25568,26160,26136,24576,9088,8960,768,256,32512,15872,0,0],"품":[19,0,0,0,0,0,12288,16352,7360,8064,7040,16128,26592,127040,253948,18040,1536,16128,16320,4288,4544,7552,8064,0,0],"발":[19,0,0,0,0,0,6144,12288,12672,13056,14092,111512,127992,13176,13272,4096,8064,8064,3840,8064,1792,896,65024,0,0],"머":[19,0,0,0,0,0,12288,24576,24576,24576,25472,25576,25368,32536,27448,25392,25584,25056,24576,24576,24576,24576,8192,0,0],"두":[19,0,0,0,0,0,0,14336,8128,960,384,12480,16064,5056,192,131008,116732,1552,1536,1536,1536,1536,512,0,0],"닐":[19,0,0,0,0,0,12288,24576,24576,24600,24624,24624,27696,28464,26608,8288,16256,16256,7680,16128,3584,1792,130560,0,0],"격":[19,0,0,0,0,0,12288,24576,24576,26496,26616,32640,25024,31968,26672,8216,8200,32256,26496,8192,8192,8192,8192,0,0],"없":[19,0,0,0,0,0,12288,24576,24592,24816,25072,32536,31512,25496,25072,8256,8192,13824,15936,4928,15296,28608,51008,0,0],"니":[19,0,0,0,0,0,12288,24576,24576,24576,24584,24624,24624,24624,25648,28208,26544,25328,24576,24576,24576,8192,8192,0,0],"산":[19,0,0,0,0,0,6144,28672,12352,12480,12480,12496,258272,13280,14128,13852,12288,12672,4480,384,384,16256,7936,0,0],"복":[19,0,0,0,0,0,14336,12288,28736,15552,14272,16320,7808,3072,130560,249852,120,15872,16320,4096,4096,4096,6144,0,0],"종":[19,0,0,0,0,0,6144,16320,3520,1792,7936,12736,26208,1568,130816,116732,280,3840,7040,4224,6272,8064,3840,0,0],"죽":[19,0,0,0,0,0,6144,16320,3520,1792,7936,12736,24672,122912,262140,18172,1536,16128,15296,4096,4096,4096,4096,0,0],"망":[19,0,0,0,0,0,6144,12288,12288,13248,13308,78620,258840,13112,12784,12336,4352,7936,7040,12672,4480,8064,3840,0,0],"베":[19,0,0,0,0,0,24576,60416,22528,22912,22912,24448,24448,24456,24536,23024,23024,22776,22648,18456,16384,16384,24576,0,0],"막":[19,0,0,0,0,0,6144,12288,12288,13248,13308,13084,258840,13080,12784,12464,4096,15872,16320,12288,4096,4096,4096,0,0],"무":[19,0,0,0,0,0,0,30720,32608,15296,6336,3520,16320,15488,0,131008,116732,1552,1536,1536,1536,1536,512,0,0],"릎":[19,0,0,0,0,0,6144,16320,6272,8128,960,14528,8128,0,130560,116732,56,16320,3456,8064,3840,16128,14272,0,0],"꿇":[19,0,0,0,0,0,0,14720,16368,12640,12672,12672,118912,262140,18428,1536,7936,7152,32736,14784,11712,16352,7648,0,0],"까":[19,0,0,0,0,0,12288,28672,24576,24576,24768,10232,10176,9824,254752,254768,8600,8388,8288,8192,8192,8192,12288,0,0],"놓":[19,0,0,0,0,0,0,224,192,12480,15552,8128,7552,130048,253948,16760,1792,15872,16320,3584,6912,3328,1792,0,0],"러":[19,0,0,0,0,0,12288,24576,24576,24576,25568,25528,29056,32720,24688,25648,26160,26608,25056,24576,24576,8192,8192,0,0],"재":[19,0,0,0,0,0,24576,60416,23552,19456,28608,28152,32192,31936,23672,24048,20272,20248,16908,16384,16384,16384,24576,0,0],"받":[19,0,0,0,0,0,6144,12288,12672,13056,14092,78744,259064,13176,13304,12672,0,7936,3584,768,24960,32640,1920,0,0],"묵":[19,0,0,0,0,0,14336,32512,15328,6336,3264,16064,3200,130048,118780,1592,1536,16128,15296,4096,4096,4096,4096,0,0],"스":[19,0,0,0,0,0,0,0,512,1536,3584,1920,1792,7936,12672,24800,0,0,57344,131068,56,0,0,0,0],"릴":[19,0,0,0,0,0,12288,24576,24832,25592,25360,25536,25072,25136,26608,8432,14464,16256,7936,16128,3840,1792,130560,0,0],"계":[19,0,0,0,0,0,24576,60416,22528,22528,23424,23544,24496,24256,22720,24416,22576,22552,22536,18432,16384,16384,24576,0,0],"구":[19,0,0,0,0,0,0,6144,16352,12736,12288,12288,12288,6144,4096,131056,67324,1536,1536,1536,1536,1536,512,0,0],"물":[19,0,0,0,0,0,14336,32544,15328,3264,7872,16064,32768,131040,18428,1536,16320,8128,3840,8064,1920,896,65280,0,0],"못":[19,0,0,0,0,0,14336,32512,15328,7360,3776,16064,7296,3072,130816,116732,1048,3584,1536,1792,7936,12480,8304,0,0],"시":[19,0,0,0,0,0,12288,24576,24576,24640,24768,25024,24768,24816,24800,25520,26424,26124,25600,24576,24576,8192,8192,0,0],"육":[19,0,0,0,0,0,1792,8064,6528,6336,6528,3968,1536,130048,118780,6584,6528,16256,15296,4096,4096,4096,4096,0,0],"신":[19,0,0,0,0,0,12288,24576,24640,25024,25024,24768,24800,25568,26416,25628,24576,9088,8960,768,256,32512,15872,0,0],"일":[19,0,0,0,0,0,12288,24576,24592,24816,25584,25368,25368,25528,25072,8192,16256,16256,7680,16128,3584,1792,130560,0,0],"모":[19,0,0,0,0,0,0,0,14336,32256,16352,6368,3264,7872,16064,6144,2048,1024,50688,131064,124,0,0,0,0],"행":[19,0,0,0,0,0,24576,58368,23776,19776,20472,27888,32224,32176,23984,19696,25184,15872,30208,25344,25344,16128,7680,0,0],"절":[19,0,0,0,0,0,12288,24576,24576,25472,25584,32160,32752,26464,26160,9752,16128,16128,7680,16128,3584,1792,130560,0,0],"멸":[19,0,0,0,0,0,12288,24576,24576,26496,32760,31512,25400,32688,25072,8224,16128,16128,7680,16128,3584,1792,130560,0,0],"끊":[19,0,0,0,0,0,0,14592,16368,13280,12672,12672,12672,114688,262140,16636,7168,6240,32352,15456,13408,16224,7648,0,0],"순":[19,0,0,0,0,0,512,3584,1536,1792,8064,29056,24800,32768,131008,116732,3072,1984,1472,128,192,16256,7936,0,0],"간":[19,0,0,0,0,0,6144,28672,12288,13184,13304,12688,258240,12384,12336,12312,12296,12672,4480,384,384,16256,7936,0,0]};
    var BODY_SCALE = 1;
    var TITLE_SCALE = 2;
    var BODY_TRACKING = -2;
    var TITLE_TRACKING = 0;
    var SPACE_ADVANCE = Math.round(CELL_H * 0.5);
    var BODY_LINE_HEIGHT = CELL_H + 8;
    var PARAGRAPH_GAP = 20;
    var DIVIDER_Y = 52;
    var BODY_TOP = 68;
    var BODY_COLOR = '#d8d8d8';
    var TITLE_COLOR = '#ffffff';
    var DIVIDER_COLOR = '#555555';
    var resizeTimer = 0;
    var observed = typeof WeakSet === 'function' ? new WeakSet() : null;
    var resizeObserver = typeof ResizeObserver === 'function' ? new ResizeObserver(function (entries) {
        entries.forEach(function (entry) { schedule(entry.target, false); });
    }) : null;

    function glyphFor(character) {
        return Object.prototype.hasOwnProperty.call(GLYPHS, character) ? GLYPHS[character] : null;
    }

    function measureText(text, scale, tracking) {
        var width = 0;
        var glyphCount = 0;
        Array.from(String(text || '')).forEach(function (character) {
            var glyph;
            if (character === ' ') {
                width += SPACE_ADVANCE * scale;
                return;
            }
            glyph = glyphFor(character);
            if (!glyph) return;
            width += glyph[0] * scale;
            width += tracking * scale;
            glyphCount += 1;
        });
        if (glyphCount && tracking) width -= tracking * scale;
        return Math.max(0, Math.round(width));
    }

    function breakLongToken(token, maxWidth) {
        var pieces = [];
        var current = '';
        Array.from(token).forEach(function (character) {
            var candidate = current + character;
            if (current && measureText(candidate, BODY_SCALE, BODY_TRACKING) > maxWidth) {
                pieces.push(current);
                current = character;
            } else {
                current = candidate;
            }
        });
        if (current) pieces.push(current);
        return pieces;
    }

    function wrapParagraph(text, maxWidth) {
        var words = String(text || '').trim().split(/\s+/).filter(Boolean);
        var lines = [];
        var current = '';

        words.forEach(function (word) {
            var candidate = current ? current + ' ' + word : word;
            var pieces;
            if (measureText(candidate, BODY_SCALE, BODY_TRACKING) <= maxWidth) {
                current = candidate;
                return;
            }
            if (current) {
                lines.push(current);
                current = '';
            }
            if (measureText(word, BODY_SCALE, BODY_TRACKING) <= maxWidth) {
                current = word;
                return;
            }
            pieces = breakLongToken(word, maxWidth);
            pieces.forEach(function (piece, index) {
                if (index === pieces.length - 1) current = piece;
                else lines.push(piece);
            });
        });
        if (current) lines.push(current);
        return lines.length ? lines : [''];
    }

    function calculateOpticalShift(lines, width) {
        var weightedCenter = 0;
        var totalWeight = 0;
        var maxLineWidth = 0;
        var shift;
        lines.forEach(function (line) {
            var lineWidth = line.width;
            var weight = Math.max(1, lineWidth) * BODY_LINE_HEIGHT;
            weightedCenter += (lineWidth / 2) * weight;
            totalWeight += weight;
            maxLineWidth = Math.max(maxLineWidth, lineWidth);
        });
        if (!totalWeight) return 0;
        shift = Math.round((width / 2) - (weightedCenter / totalWeight));
        return Math.max(0, Math.min(Math.max(0, width - maxLineWidth), shift));
    }

    function drawGlyph(context, glyph, x, y, scale) {
        var py;
        var px;
        var row;
        for (py = 0; py < CELL_H; py += 1) {
            row = glyph[py + 1] || 0;
            if (!row) continue;
            for (px = 0; px < CELL_W; px += 1) {
                if (row & (1 << px)) {
                    context.fillRect(
                        Math.round(x + px * scale),
                        Math.round(y + py * scale),
                        scale,
                        scale
                    );
                }
            }
        }
    }

    function drawText(context, text, x, y, scale, tracking, color) {
        context.fillStyle = color;
        Array.from(String(text || '')).forEach(function (character) {
            var glyph;
            if (character === ' ') {
                x += SPACE_ADVANCE * scale;
                return;
            }
            glyph = glyphFor(character);
            if (!glyph) return;
            drawGlyph(context, glyph, x, y, scale);
            x += (glyph[0] + tracking) * scale;
        });
    }

    function sourceContent(inner) {
        var titleNode = inner.querySelector('.main-manifesto-title');
        var paragraphs = [];
        Array.prototype.forEach.call(inner.children || [], function (child) {
            if (child && child.tagName === 'P') {
                var value = String(child.textContent || '').trim();
                if (value) paragraphs.push(value);
            }
        });
        return {
            title: titleNode ? String(titleNode.textContent || '').trim() : '',
            paragraphs: paragraphs
        };
    }

    function ensureCanvas(inner) {
        var canvas = inner.querySelector('.main-manifesto-bitmap');
        if (!canvas) {
            canvas = document.createElement('canvas');
            canvas.className = 'main-manifesto-bitmap';
            canvas.setAttribute('aria-hidden', 'true');
            canvas.setAttribute('data-bitmap-version', VERSION);
            inner.insertBefore(canvas, inner.firstChild);
        }
        return canvas;
    }

    function render(inner, force) {
        var width;
        var source;
        var paragraphs;
        var flatLines = [];
        var titleWidth;
        var height;
        var canvas;
        var context;
        var y;
        var opticalShift;

        if (!inner || !inner.isConnected) return;
        width = Math.max(1, Math.floor(inner.clientWidth || inner.getBoundingClientRect().width || 0));
        if (width <= 1) {
            window.setTimeout(function () { schedule(inner, true); }, 60);
            return;
        }
        if (!force && Number(inner.getAttribute('data-bitmap-width')) === width && inner.classList.contains('is-bitmap-ready')) return;

        source = sourceContent(inner);
        if (!source.title || !source.paragraphs.length) {
            fail(inner, new Error('Manifesto source nodes were not found.'));
            return;
        }

        paragraphs = source.paragraphs.map(function (paragraph) {
            return wrapParagraph(paragraph, width).map(function (line) {
                var item = { text:line, width:measureText(line, BODY_SCALE, BODY_TRACKING) };
                flatLines.push(item);
                return item;
            });
        });

        titleWidth = measureText(source.title, TITLE_SCALE, TITLE_TRACKING);
        height = BODY_TOP;
        paragraphs.forEach(function (lines, index) {
            height += lines.length * BODY_LINE_HEIGHT;
            if (index < paragraphs.length - 1) height += PARAGRAPH_GAP;
        });
        height = Math.max(height, DIVIDER_Y + 2);

        canvas = ensureCanvas(inner);
        canvas.width = width;
        canvas.height = Math.ceil(height);
        canvas.style.width = width + 'px';
        canvas.style.height = Math.ceil(height) + 'px';
        context = canvas.getContext('2d', { alpha:true });
        if (!context) {
            fail(inner, new Error('Canvas 2D context is unavailable.'));
            return;
        }
        context.imageSmoothingEnabled = false;
        context.clearRect(0, 0, canvas.width, canvas.height);

        drawText(
            context,
            source.title,
            Math.round((width - titleWidth) / 2),
            0,
            TITLE_SCALE,
            TITLE_TRACKING,
            TITLE_COLOR
        );

        /* 완성형 구분선은 폐기했다. 실선 애니메이션은 ManifestoIntro만 담당한다. */

        opticalShift = calculateOpticalShift(flatLines, width);
        y = BODY_TOP;
        paragraphs.forEach(function (lines, paragraphIndex) {
            lines.forEach(function (line) {
                drawText(context, line.text, opticalShift, y, BODY_SCALE, BODY_TRACKING, BODY_COLOR);
                y += BODY_LINE_HEIGHT;
            });
            if (paragraphIndex < paragraphs.length - 1) y += PARAGRAPH_GAP;
        });

        inner.setAttribute('data-bitmap-width', String(width));
        inner.setAttribute('data-bitmap-optical-shift-x', String(opticalShift));
        inner.setAttribute('data-bitmap-state', 'ready');
        inner.classList.add('is-bitmap-ready');
    }

    function fail(inner, error) {
        if (!inner) return;
        inner.classList.remove('is-bitmap-ready');
        inner.setAttribute('data-bitmap-state', 'failed');
        inner.setAttribute('data-bitmap-error', String(error && error.message ? error.message : error));
        try { console.error('[MainPageBitmap]', error); } catch (ignore) {}
    }

    function schedule(inner, force) {
        if (!inner) return;
        window.requestAnimationFrame(function () {
            try { render(inner, force); }
            catch (error) { fail(inner, error); }
        });
    }

    function mount(root) {
        var scope = root && root.querySelectorAll ? root : document;
        var nodes = [];
        if (scope.matches && scope.matches(SELECTOR)) nodes.push(scope);
        nodes = nodes.concat(Array.prototype.slice.call(scope.querySelectorAll(SELECTOR)));
        nodes.forEach(function (inner) {
            if (resizeObserver && (!observed || !observed.has(inner))) {
                resizeObserver.observe(inner);
                if (observed) observed.add(inner);
            }
            schedule(inner, true);
        });
    }

    function recalculate() {
        Array.prototype.forEach.call(document.querySelectorAll(SELECTOR), function (inner) {
            schedule(inner, true);
        });
    }

    function boot() {
        mount(document);
        window.setTimeout(function () { mount(document); }, 120);
        window.setTimeout(function () { mount(document); }, 500);
    }

    if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', boot, { once:true });
    else boot();

    window.addEventListener('resize', function () {
        window.clearTimeout(resizeTimer);
        resizeTimer = window.setTimeout(recalculate, 100);
    }, { passive:true });

    try {
        if (mw && mw.hook) {
            mw.hook('wikipage.content').add(function (content) {
                mount(content && content[0] ? content[0] : document);
            });
        }
    } catch (ignoreHook) {}

    window.MainPageBitmap = {
        version:VERSION,
        recalculate:recalculate,
        status:function () {
            var inner = document.querySelector(SELECTOR);
            return inner ? {
                integrated:true,
                state:inner.getAttribute('data-bitmap-state') || 'pending',
                error:inner.getAttribute('data-bitmap-error') || '',
                width:inner.getAttribute('data-bitmap-width') || '',
                opticalShiftX:inner.getAttribute('data-bitmap-optical-shift-x') || '',
                canvas:!!inner.querySelector('.main-manifesto-bitmap')
            } : { integrated:true, state:'not-mounted' };
        }
    };

    window.MainPageManifesto = window.MainPageManifesto || {};
    window.MainPageManifesto.recalculate = recalculate;
})(window, document, window.mw);