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

(Install package: wiki-catlinks-single-source-cleanup-20260705 / js/Common.js)
태그: 되돌려진 기여
(Install package: wiki-catlinks-single-source-cleanup-20260705 / backup/미디어위키_Common.js.txt)
태그: 수동 되돌리기
1번째 줄: 1번째 줄:
mw.loader.load('https://cdnjs.cloudflare.com/ajax/libs/cropperjs/1.5.13/cropper.min.js');
mw.loader.load('https://cdnjs.cloudflare.com/ajax/libs/cropperjs/1.5.13/cropper.min.js');
mw.loader.load('/index.php?title=MediaWiki:DevTools.js&action=raw&ctype=text/javascript');
mw.loader.load('/index.php?title=MediaWiki:CategoryNav.js&action=raw&ctype=text/javascript');


function loadLangScript(done) {
/* CLBI raw MediaWiki script cache key.
     $.getScript('/index.php?title=미디어위키:Lang.js&action=raw&ctype=text/javascript')
  This is not a semantic version. It only prevents stale raw JS during active editing. */
        .done(function() {
window.CLBI_RAW_LOAD_BUST = window.CLBI_RAW_LOAD_BUST || String(Date.now());
            if (typeof done === 'function') done();
function loadClbiRawScript(title) {
        })
     mw.loader.load('/index.php?title=' + encodeURIComponent(title) + '&action=raw&ctype=text/javascript&v=' + encodeURIComponent(window.CLBI_RAW_LOAD_BUST));
        .fail(function(a, b, c) {
            console.error('Lang.js load failed:', b, c);
            if (typeof done === 'function') done();
        });
}
}


loadClbiRawScript('MediaWiki:DevTools.js');
loadClbiRawScript('MediaWiki:CategoryNav.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');
    }


function initHalftoneBackground() {
    if (list) {
    try {
         list.classList.remove('is-adaptive-faded');
         initWebGLHalftoneBackground();
        list.removeAttribute('data-adaptive-limit');
    } catch (err) {
         list.style.removeProperty('--adaptive-recent-h');
         console.error('WebGL halftone background failed:', err);
     }
     }
    items.forEach(function (item) {
        item.classList.remove('is-adaptive-hidden');
    });
}
}


function initWebGLHalftoneBackground() {
function resetLeftBillboardAdaptiveState() {
     var canvasId = 'site-halftone-bg';
     var box = document.querySelector('#clbi-left-sidebar .left-billboard-box');
     var existing = document.getElementById(canvasId);
 
     var canvas = existing || document.createElement('canvas');
    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');
}


    if (!existing) {
window.resetLeftRecentAdaptiveState = resetLeftRecentAdaptiveState;
        canvas.id = canvasId;
window.resetLeftBillboardAdaptiveState = resetLeftBillboardAdaptiveState;
        canvas.setAttribute('aria-hidden', 'true');
        document.body.insertBefore(canvas, document.body.firstChild || null);
    }


    canvas.style.position = 'fixed';
loadClbiRawScript('MediaWiki:AnecdoteViewer.js');
    canvas.style.inset = '0';
    canvas.style.width = '100vw';
    canvas.style.height = '100vh';
    canvas.style.pointerEvents = 'none';
    canvas.style.background = '#000000';


    var gl = canvas.getContext('webgl', {
        alpha: false,
        antialias: false,
        depth: false,
        stencil: false,
        preserveDrawingBuffer: false,
        powerPreference: 'high-performance'
    }) || canvas.getContext('experimental-webgl');


     if (!gl) {
(function () {
         console.warn('WebGL background unavailable.');
     'use strict';
         return;
 
    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);
     }
     }


     var vertexSrc = [
     function isBackendOrSystemPageForShell() {
         'attribute vec2 a_position;',
        var namespaceNumber = Number(mw.config.get('wgNamespaceNumber'));
         'void main() {',
         var action = String(mw.config.get('wgAction') || 'view').toLowerCase();
         '  gl_Position = vec4(a_position, 0.0, 1.0);',
         var contentModel = String(mw.config.get('wgPageContentModel') || '').toLowerCase();
         '}'
         var pageName = readCurrentPageNameForShell();
    ].join('\n');
        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;
        }


    var fragmentSrc = [
         if (/\.(css|js|json)$/i.test(pageName)) {
         'precision mediump float;',
            return true;
        'uniform vec2 u_resolution;',
         }
        'uniform float u_time;',
 
         'const float TAU = 6.28318530718;',
         if (/^(mediawiki|미디어위키|special|특수):/i.test(pageName)) {
        'float gaussian(float v, float r) {',
            return true;
         '  return exp(-((v * v) / max(0.0001, r * r)));',
         }
         '}',
 
        'float hash(vec2 p) {',
         return false;
         return fract(sin(dot(p, vec2(127.1, 311.7))) * 43758.5453123);',
    }
        '}',
 
        'float bucketAlpha(float a) {',
    function isMediaWikiSystemAssetPageForShell() {
         '  float i = floor(a * 9.0);',
         var namespaceNumber = Number(mw.config.get('wgNamespaceNumber'));
        ' if (i < 1.0) return 0.040;',
         var pageName = readCurrentPageNameForShell();
         '  if (i < 2.0) return 0.080;',
         var contentModel = String(mw.config.get('wgPageContentModel') || '').toLowerCase();
         '  if (i < 3.0) return 0.135;',
 
        ' if (i < 4.0) return 0.210;',
         return namespaceNumber === 8 &&
        ' if (i < 5.0) return 0.310;',
            (/\.(css|js)$/i.test(pageName) || contentModel === 'css' || contentModel === 'javascript' || contentModel === 'sanitized-css');
        '  if (i < 6.0) return 0.430;',
    }
         '  if (i < 7.0) return 0.580;',
 
        '  if (i < 8.0) return 0.760;',
 
        '  return 0.920;',
    var systemDocRawFetchToken = 0;
        '}',
 
        'void main() {',
    function cleanupLegacySystemDocCodeMutationsForShell() {
        ' vec2 frag = gl_FragCoord.xy;',
         document.querySelectorAll('.clbi-system-doc-codepane').forEach(function (pane) {
        ' float spacing = 5.0;',
            var parent;
        '  float dotSize = 1.08;',
 
        ' vec2 grid = floor(frag / spacing);',
            if (!pane || !pane.parentNode) return;
        '  vec2 inCell = mod(frag, spacing);',
 
        '  vec2 dotOrigin = vec2(1.0, 1.0);',
            parent = pane.parentNode;
        '  vec2 dotCenter = dotOrigin + vec2(dotSize * 0.5);',
            while (pane.firstChild) {
        '  vec2 local = abs(inCell - dotCenter);',
                parent.insertBefore(pane.firstChild, pane);
         '  float noise = hash(grid);',
            }
        '  float size = dotSize + noise * 0.18;',
            parent.removeChild(pane);
        '  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 +',
         document.querySelectorAll('.clbi-system-doc-codebox').forEach(function (node) {
        '    sin((uv.y * 1.32 + 0.08) * TAU) * 0.070 +',
            node.classList.remove('clbi-system-doc-codebox');
        '    sin((uv.y * 3.18 + 0.34) * TAU) * 0.030;',
            node.removeAttribute('data-clbi-system-doc-codebox');
        '  float u = uv.x - centerLine;',
            node.removeAttribute('style');
        '  float absU = abs(u);',
         });
        '  float sideLift = smoothstep(0.065, 0.44, absU);',
    }
        '  float valley = gaussian(u, 0.150);',
 
         '  float t = u_time;',
    function getSystemDocOutputForShell() {
         ' float leftRibbonCenter = -0.28 + sin((uv.y * 3.20 + 0.12) * TAU) * 0.050;',
         return document.querySelector('.liberty-content-main .mw-parser-output');
        '  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);',
    function findSystemDocSourceNodeForShell() {
        '  float foldedU = u +',
         var output = getSystemDocOutputForShell();
        '    sin((uv.y * 4.40 + 0.22) * TAU) * 0.050 * (0.3 + sideLift) +',
         var children;
        '   sin((uv.y * 7.20 + uv.x * 1.10) * TAU) * 0.022;',
         var preferred;
         '  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);',
         if (!output) return null;
        '  float waist = gaussian(uv.y - 0.50, 0.25) * gaussian(absU - 0.20, 0.19);',
 
        ' float grain = (noise - 0.5) * 0.050;',
         children = Array.prototype.slice.call(output.children || [])
        '  float staticField =',
            .filter(function (el) {
        '    0.055 +',
                return el && el.nodeType === 1 &&
        '    sideLift * 0.210 +',
                    el.id !== 'clbi-system-doc-indicator-row' &&
        '    (leftRibbon + rightRibbon) * 0.145 +',
                    el.id !== 'clbi-system-source-viewer' &&
         '    verticalFold * (0.055 + sideLift * 0.115) +',
                    !el.classList.contains('catlinks') &&
         '    diagonalFold * 0.045 +',
                    (el.textContent || '').trim().length > 200;
         '    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) {
        preferred = children.filter(function (el) {
        var shader = gl.createShader(type);
            return el.matches && el.matches('.mw-highlight, .mw-code, pre');
        gl.shaderSource(shader, source);
         })[0];
         gl.compileShader(shader);


         if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) {
         return preferred || children.sort(function (a, b) {
             console.error('WebGL shader compile error:', gl.getShaderInfoLog(shader));
             return (b.textContent || '').trim().length - (a.textContent || '').trim().length;
            gl.deleteShader(shader);
         })[0] || null;
            return null;
         }
 
        return shader;
     }
     }


     var vertexShader = compileShader(gl.VERTEX_SHADER, vertexSrc);
     function getSystemDocRawUrlForShell() {
     var fragmentShader = compileShader(gl.FRAGMENT_SHADER, fragmentSrc);
        var title = mw.config.get('wgPageName') || readCurrentPageNameForShell();
 
        if (window.mw && mw.util && typeof mw.util.getUrl === 'function') {
            return mw.util.getUrl(title, {
                action: 'raw',
                ctype: 'text/plain',
                _: String(Date.now())
            });
        }
 
        return '/index.php?title=' + encodeURIComponent(title) + '&action=raw&ctype=text/plain&_=' + Date.now();
     }
 
    function removeSystemDocSourceViewerForShell() {
        var viewer = document.getElementById('clbi-system-source-viewer');


    if (!vertexShader || !fragmentShader) return;
        if (viewer && viewer.parentNode) {
            viewer.parentNode.removeChild(viewer);
        }


    var program = gl.createProgram();
        document.querySelectorAll('.clbi-system-original-source-hidden').forEach(function (node) {
    gl.attachShader(program, vertexShader);
            node.classList.remove('clbi-system-original-source-hidden');
    gl.attachShader(program, fragmentShader);
            node.removeAttribute('data-clbi-system-source-hidden');
    gl.linkProgram(program);
            node.style.removeProperty('display');
        });


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


     var positionLoc = gl.getAttribLocation(program, 'a_position');
     function ensureSystemDocSourceViewerForShell() {
    var resolutionLoc = gl.getUniformLocation(program, 'u_resolution');
        var output = getSystemDocOutputForShell();
    var timeLoc = gl.getUniformLocation(program, 'u_time');
        var source;
        var viewer;
        var fallbackText;
 
        if (!output || !isMediaWikiSystemAssetPageForShell()) return null;


    var buffer = gl.createBuffer();
        cleanupLegacySystemDocCodeMutationsForShell();
    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);
        source = findSystemDocSourceNodeForShell();
    gl.bindBuffer(gl.ARRAY_BUFFER, buffer);
        if (!source) return null;
    gl.enableVertexAttribArray(positionLoc);
    gl.vertexAttribPointer(positionLoc, 2, gl.FLOAT, false, 0, 0);


    function resize() {
         viewer = document.getElementById('clbi-system-source-viewer');
        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) {
         if (!viewer) {
             canvas.width = w;
             viewer = document.createElement('pre');
             canvas.height = h;
             viewer.id = 'clbi-system-source-viewer';
             canvas.style.width = cssW + 'px';
             viewer.className = 'clbi-system-source-viewer';
             canvas.style.height = cssH + 'px';
             output.appendChild(viewer);
             gl.viewport(0, 0, w, h);
        }
 
        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;
     }
     }


     var prefersReducedMotion = false;
     function renderSystemDocSourceViewerForShell() {
    try {
        var viewer;
         prefersReducedMotion = window.matchMedia && window.matchMedia('(prefers-reduced-motion: reduce)').matches;
        var pageName;
    } catch (err) {}
         var token;
        var currentScrollTop;


    var lastFrame = 0;
        if (!isMediaWikiSystemAssetPageForShell()) return;
    var frameInterval = prefersReducedMotion ? 1000 : 66;
    var startTime = performance.now();


    function render(now) {
        pageName = String(mw.config.get('wgPageName') || readCurrentPageNameForShell());
        requestAnimationFrame(render);
         viewer = document.getElementById('clbi-system-source-viewer');
         if (document.hidden) return;
        if (now - lastFrame < frameInterval) return;


         lastFrame = now;
         /*
         resize();
        시스템 문서 뷰어가 이미 만들어져 있고 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;
        }


         gl.clearColor(0, 0, 0, 1);
         viewer = ensureSystemDocSourceViewerForShell();
         gl.clear(gl.COLOR_BUFFER_BIT);
         if (!viewer) return;
        gl.uniform2f(resolutionLoc, canvas.width, canvas.height);
        gl.uniform1f(timeLoc, now - startTime);
        gl.drawArrays(gl.TRIANGLES, 0, 6);
    }


    resize();
        currentScrollTop = viewer.scrollTop || 0;
    requestAnimationFrame(render);
        viewer.setAttribute('data-clbi-raw-title', pageName);
}
        token = ++systemDocRawFetchToken;


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>';
        fetch(getSystemDocRawUrlForShell(), { credentials: 'same-origin' })
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>';
            .then(function (res) {
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>';
                if (!res.ok) throw new Error('raw fetch failed ' + res.status);
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>';
                return res.text();
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>';
            .then(function (text) {
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>';
                if (token !== systemDocRawFetchToken) return;
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_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;
                currentScrollTop = viewer.scrollTop || currentScrollTop || 0;


function invalidateProfileRender() {
                if (text && viewer.textContent !== text) {
    PROFILE_RENDER_TOKEN++;
                    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');


$(function() {
        if (document.body) {
    initHalftoneBackground();
            document.body.classList.remove('clbi-system-doc-page');
        }


// ── 상단 네비게이션 바 ──
        if (existing && existing.parentNode) {
var navHtml =
            existing.parentNode.removeChild(existing);
    '<div id="clbi-top-nav-wrap">' +
         }
         '<div id="clbi-top-nav">' +


            '<div id="clbi-top-nav-main">' +
        removeSystemDocSourceViewerForShell();
    }


                '<div id="clbi-top-nav-tabs">' +
    function renderSystemDocIndicatorForShell() {
'<a class="clbi-top-nav-item" href="/index.php/대문">' +
        var pageName;
    '<img class="clbi-tnav-icon" src="/index.php?title=특수:Redirect/file/Ic-main-menu-001.png" alt="">' +
        var extMatch;
    '<span class="clbi-tnav-label">메인 메뉴</span>' +
        var ext;
'</a>' +
        var row;
'<a class="clbi-top-nav-item" href="/index.php/프로젝트:소개">' +
        var box;
    '<img class="clbi-tnav-icon" src="/index.php?title=특수:Redirect/file/Ic-project-001.png" alt="">' +
        var meta;
    '<span class="clbi-tnav-label">프로젝트</span>' +
        var label;
'</a>' +
        var type;
'<div class="clbi-top-nav-item" id="clbi-tnav-worldbuilding">' +
        var title;
    '<img class="clbi-tnav-icon" src="/index.php?title=특수:Redirect/file/Ic-worldbuilding-001.png" alt="">' +
        var anchor;
    '<span class="clbi-tnav-label">세계관</span>' +
        var main;
    '<span class="clbi-tnav-arrow">▾</span>' +
'</div>' +
'</div>' +


                '<div id="clbi-top-nav-search">' +
        if (!document.body || !isMediaWikiSystemAssetPageForShell()) return;
                    '<input type="text" id="clbi-top-search-input" placeholder="검색...">' +
                '</div>' +


                '<div id="clbi-top-nav-right">' +
        pageName = readCurrentPageNameForShell();
                    '<div class="clbi-top-nav-item" id="clbi-tnav-info">' +
        extMatch = pageName.match(/\.(css|js)$/i);
                        '<span class="clbi-tnav-label">ℹ</span>' +
        ext = extMatch ? extMatch[1].toUpperCase() : 'DOC';
                    '</div>' +
                '</div>' +


            '</div>' +
        document.body.classList.add('clbi-system-doc-page');


'<div id="clbi-sub-worldbuilding">' +
         row = document.getElementById('clbi-system-doc-indicator-row');
    '<div id="clbi-sub-worldbuilding-inner">' +
         '<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>' +
         if (!row) {
    '</div>';
            row = document.createElement('div');
            row.id = 'clbi-system-doc-indicator-row';
            row.className = 'clbi-system-doc-indicator-row';


$('.content-wrapper').before(navHtml);
            box = document.createElement('div');
            box.className = 'clbi-system-doc-indicator';


$('#clbi-tnav-worldbuilding').on('click', function() {
            meta = document.createElement('div');
    var $menu = $('#clbi-sub-worldbuilding');
            meta.className = 'clbi-system-doc-meta';
    var $btn = $(this);


    $menu.toggleClass('worldbuilding-open');
            label = document.createElement('span');
    $btn.toggleClass('clbi-tnav-active', $menu.hasClass('worldbuilding-open'));
            label.className = 'clbi-system-doc-label';
});
            label.textContent = 'SYSTEM DOCUMENT';


$('#clbi-top-search-input').on('keydown', function(e) {
            type = document.createElement('span');
    if (e.key === 'Enter') {
            type.className = 'clbi-system-doc-type';
        var q = $(this).val().trim();
        if (q) window.location.href = '/index.php?search=' + encodeURIComponent(q);
    }
});


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


// 페이지 전환 사운드
            meta.appendChild(label);
var transitionSound = new Audio('/index.php?title=특수:Redirect/file/Sfx-ui-001.mp3');
            meta.appendChild(type);
            box.appendChild(meta);
            box.appendChild(title);
            row.appendChild(box);


(function() {
            anchor = getSystemDocOutputForShell();
    var master = parseFloat(localStorage.getItem('clbi-audio-master') || 80) / 100;
            main = document.querySelector('.liberty-content-main');
    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() {
            if (anchor && anchor.parentNode) {
    var master = parseFloat(localStorage.getItem('clbi-audio-master') || 80) / 100;
                anchor.parentNode.insertBefore(row, anchor);
    var sfx = parseFloat(localStorage.getItem('clbi-audio-sfx') || 60) / 100;
            } else if (main) {
    var sfxOn = localStorage.getItem('clbi-audio-sfxOn') !== 'false';
                main.insertBefore(row, main.firstChild);
            }
        }


    if (!sfxOn) return;
        type = row.querySelector('.clbi-system-doc-type');
        title = row.querySelector('.clbi-system-doc-title');


    transitionSound.volume = master * sfx;
        if (type) type.textContent = ext;
    transitionSound.currentTime = 0;
        if (title) title.textContent = pageName;
    transitionSound.play();
}


// 현재 언어 감지
        renderSystemDocSourceViewerForShell();
function getCurrentLang() {
    }
     var langData = document.getElementById('clbi-lang-data');
 
    return langData ? (langData.getAttribute('data-lang') || 'ko') : 'ko';
     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 normalizePageName(value) {
    function setPageTitleDomHidden(hidden) {
    return String(value || '')
         var nodes = document.querySelectorAll(PAGE_TITLE_TARGET_SELECTORS.join(','));
         .split('?')[0]
        .replace(/^\/index\.php\//, '')
        .replace(/_/g, ' ')
        .trim();
}


function buildWikiPath(title) {
        nodes.forEach(function (node) {
    return '/index.php/' + encodeURI(String(title || '').replace(/ /g, '_'));
            if (!node || !node.style) return;
}


function getLangShortCode(lang) {
            if (hidden) {
    var map = { ko: 'KR', en: 'EN', zh: 'ZH', ja: 'JA', ru: 'RU', es: 'ES' };
                node.setAttribute('data-clbi-title-hidden', 'true');
    return map[lang] || String(lang || '').toUpperCase();
                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 getLanguageTargetTitle(lang) {
    function applyPageShellClasses() {
    var data = document.getElementById('clbi-lang-data');
        var body = document.body;
    if (!data || !lang) return '';
        var isSystemPage;


    var keys = [
         if (!body) return;
         'data-' + lang,
        'data-page-' + lang,
        'data-title-' + lang,
        'data-target-' + lang,
        'data-lang-' + lang
    ];


    for (var i = 0; i < keys.length; i++) {
         isSystemPage = isBackendOrSystemPageForShell();
         var value = data.getAttribute(keys[i]);
        if (value) return value;
    }


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


var SIDEBAR_LANG_SVG_NS = 'http://www.w3.org/2000/svg';
        if (isAnecdoteNamespaceForShell()) {
var SIDEBAR_LANGUAGE_STATUS_TITLE = 'MediaWiki:LanguageStatus.json';
            body.classList.add('anecdote-namespace-page');
var SIDEBAR_LANGUAGE_LABELS = {
        }
    ko: '한국어',
 
    en: 'English',
        if (isMediaWikiSystemAssetPageForShell()) {
    zh: '中文',
            body.classList.add('page-title-hidden', 'backend-system-page', 'clbi-system-doc-page');
    ja: '日本語',
            setPageTitleDomHidden(true);
    ru: 'Русский',
            renderSystemDocIndicatorForShell();
    es: 'Español'
        } else if (isSystemPage) {
};
            body.classList.add('page-title-visible', 'backend-system-page');
var SIDEBAR_LANGUAGE_DIAL_LABELS = {
            setPageTitleDomHidden(false);
     ko: '한국어',
        } else {
     en: 'ENG',
            body.classList.add('page-title-hidden');
    zh: '中文',
            setPageTitleDomHidden(true);
    ja: '日本語',
        }
     ru: 'РУС',
     }
     es: 'ESP'
 
};
     function applyPageShellClassesDeferred() {
var SIDEBAR_LANGUAGE_STATUS_VALUES = {
        applyPageShellClasses();
    available: true,
        window.setTimeout(applyPageShellClasses, 0);
    wip: true,
        window.setTimeout(applyPageShellClasses, 80);
    unavailable: true
        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;


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


var sidebarLanguageState = {
                    if (
    order: ['ko', 'en', 'zh', 'ja', 'ru', 'es'],
                        target &&
    currentLang: 'ko',
                        target.nodeType === 1 &&
    baseIndex: 0,
                        (
    selectedIndex: 0,
                            target.id === 'clbi-system-source-viewer' ||
    rotation: 0,
                            target.id === 'clbi-system-doc-indicator-row' ||
    dragging: false,
                            (target.closest && target.closest('#clbi-system-source-viewer, #clbi-system-doc-indicator-row'))
    dragMoved: false,
                        )
    dragStartX: 0,
                    ) {
    dragStartY: 0,
                        return;
    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) {
                if (
    return document.createElementNS(SIDEBAR_LANG_SVG_NS, tag);
                    document.getElementById('clbi-system-doc-indicator-row') &&
}
                    document.getElementById('clbi-system-source-viewer')
                ) {
                    return;
                }
            }


function normalizeSidebarLanguageIndex(index) {
            if (pageShellObserverTimer) return;
    var length = sidebarLanguageState.order.length;
    var normalized = index % length;
    return normalized < 0 ? normalized + length : normalized;
}


function getSidebarLanguageName(lang) {
            pageShellObserverTimer = window.setTimeout(function () {
    return SIDEBAR_LANGUAGE_LABELS[lang] || String(lang || '').toUpperCase();
                pageShellObserverTimer = null;
}
                applyPageShellClasses();
            }, 50);
        });


function getSidebarLanguageDialName(lang) {
        observer.observe(document.body, {
     return SIDEBAR_LANGUAGE_DIAL_LABELS[lang] || getSidebarLanguageName(lang);
            childList: true,
}
            subtree: true
        });
    }
 
    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 normalizeSidebarLanguageStatusValue(value) {
function loadLangScript(done) {
     value = String(value == null ? '' : value).toLowerCase().trim();
     $.getScript('/index.php?title=미디어위키:Lang.js&action=raw&ctype=text/javascript')
    return SIDEBAR_LANGUAGE_STATUS_VALUES[value] ? value : '';
        .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 getSidebarLanguageStatusPageKey() {
    var raw = String(mw.config.get('wgPageName') || '').trim();
    var normalized = normalizePageName(raw);


     return normalized || raw || '대문';
function initHalftoneBackground() {
     try {
        initWebGLHalftoneBackground();
    } catch (err) {
        console.error('WebGL halftone background failed:', err);
    }
}
}


function getSidebarLanguageStatusEntry() {
function initWebGLHalftoneBackground() {
     var registry = sidebarLanguageStatusRegistry || {};
     var canvasId = 'site-halftone-bg';
    var pages = registry.pages && typeof registry.pages === 'object' ? registry.pages : registry;
     var existing = document.getElementById(canvasId);
     var raw = String(mw.config.get('wgPageName') || '').trim();
     var canvas = existing || document.createElement('canvas');
    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 (!existing) {
         if (keys[i] && pages[keys[i]] && typeof pages[keys[i]] === 'object') {
        canvas.id = canvasId;
            return pages[keys[i]];
         canvas.setAttribute('aria-hidden', 'true');
         }
         document.body.insertBefore(canvas, document.body.firstChild || null);
     }
     }


     return {};
     canvas.style.position = 'fixed';
}
    canvas.style.inset = '0';
    canvas.style.width = '100vw';
    canvas.style.height = '100vh';
    canvas.style.pointerEvents = 'none';
    canvas.style.background = '#000000';


function getSidebarLanguageStatusOverride(lang) {
     var gl = canvas.getContext('webgl', {
     var entry = getSidebarLanguageStatusEntry();
        alpha: false,
     return normalizeSidebarLanguageStatusValue(entry[lang]);
        antialias: false,
}
        depth: false,
        stencil: false,
        preserveDrawingBuffer: false,
        powerPreference: 'high-performance'
     }) || canvas.getContext('experimental-webgl');


function flushSidebarLanguageStatusCallbacks() {
    if (!gl) {
    var callbacks = sidebarLanguageStatusCallbacks.slice();
        console.warn('WebGL background unavailable.');
     sidebarLanguageStatusCallbacks.length = 0;
        return;
     }


     callbacks.forEach(function(callback) {
     var vertexSrc = [
         if (typeof callback === 'function') {
         'attribute vec2 a_position;',
            callback(sidebarLanguageStatusRegistry);
        'void main() {',
         }
        '  gl_Position = vec4(a_position, 0.0, 1.0);',
     });
         '}'
}
     ].join('\n');


function loadSidebarLanguageStatusRegistry(callback, force) {
    var fragmentSrc = [
    if (typeof callback === 'function') {
        'precision mediump float;',
         sidebarLanguageStatusCallbacks.push(callback);
        'uniform vec2 u_resolution;',
    }
        'uniform float u_time;',
 
        'const float TAU = 6.28318530718;',
    if (sidebarLanguageStatusLoaded && !force) {
        'float gaussian(float v, float r) {',
         flushSidebarLanguageStatusCallbacks();
        '  return exp(-((v * v) / max(0.0001, r * r)));',
         return;
        '}',
    }
        'float hash(vec2 p) {',
 
         '  return fract(sin(dot(p, vec2(127.1, 311.7))) * 43758.5453123);',
    if (sidebarLanguageStatusLoading) return;
        '}',
 
        'float bucketAlpha(float a) {',
    sidebarLanguageStatusLoading = true;
        '  float i = floor(a * 9.0);',
 
        '  if (i < 1.0) return 0.040;',
    $.ajax({
         '  if (i < 2.0) return 0.080;',
         url: mw.util.getUrl(SIDEBAR_LANGUAGE_STATUS_TITLE, {
         '  if (i < 3.0) return 0.135;',
            action: 'raw',
        '  if (i < 4.0) return 0.210;',
            ctype: 'application/json',
        '  if (i < 5.0) return 0.310;',
            _: String(Date.now())
        '  if (i < 6.0) return 0.430;',
         }),
        '  if (i < 7.0) return 0.580;',
         dataType: 'text',
        '  if (i < 8.0) return 0.760;',
         cache: false
        '  return 0.920;',
    }).done(function(text) {
        '}',
         var parsed = {};
        'void main() {',
 
         '  vec2 frag = gl_FragCoord.xy;',
         try {
        '  float spacing = 5.0;',
            parsed = text ? JSON.parse(text) : {};
        ' float dotSize = 1.08;',
         } catch (err) {
        ' vec2 grid = floor(frag / spacing);',
            console.error('LanguageStatus.json parse failed:', err);
        '  vec2 inCell = mod(frag, spacing);',
            parsed = {};
        '  vec2 dotOrigin = vec2(1.0, 1.0);',
         }
        '  vec2 dotCenter = dotOrigin + vec2(dotSize * 0.5);',
 
        '  vec2 local = abs(inCell - dotCenter);',
         sidebarLanguageStatusRegistry = parsed && typeof parsed === 'object' ? parsed : {};
         '  float noise = hash(grid);',
    }).fail(function() {
         ' float size = dotSize + noise * 0.18;',
         sidebarLanguageStatusRegistry = {};
         '  float dotMask = 1.0 - smoothstep(size * 0.5, size * 0.5 + 0.22, max(local.x, local.y));',
    }).always(function() {
         '  vec2 uv = frag / u_resolution;',
         sidebarLanguageStatusLoaded = true;
         '  float centerLine = 0.50 +',
         sidebarLanguageStatusLoading = false;
        '    sin((uv.y * 1.32 + 0.08) * TAU) * 0.070 +',
         flushSidebarLanguageStatusCallbacks();
         '    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);',
window.CLBI_LANGUAGE_STATUS = {
         '  float valley = gaussian(u, 0.150);',
    title: SIDEBAR_LANGUAGE_STATUS_TITLE,
         '  float t = u_time;',
    languages: sidebarLanguageState.order.slice(),
        ' float leftRibbonCenter = -0.28 + sin((uv.y * 3.20 + 0.12) * TAU) * 0.050;',
    labels: SIDEBAR_LANGUAGE_LABELS,
        '  float rightRibbonCenter = 0.27 + sin((uv.y * 2.85 + 0.56) * TAU) * 0.055;',
    dialLabels: SIDEBAR_LANGUAGE_DIAL_LABELS,
        '  float leftRibbon = gaussian(u - leftRibbonCenter, 0.105);',
    getPageKey: getSidebarLanguageStatusPageKey,
        '  float rightRibbon = gaussian(u - rightRibbonCenter, 0.110);',
    getRegistry: function() {
         '  float foldedU = u +',
         return sidebarLanguageStatusRegistry || {};
        '    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;',
    reload: function(callback) {
         '  float verticalFold = pow(0.5 + 0.5 * cos(((foldedU * 3.05) + (sin(uv.y * TAU * 2.35) * 0.18)) * TAU), 2.5);',
         sidebarLanguageStatusLoaded = false;
         '  float diagonalFold = pow(0.5 + 0.5 * cos(((foldedU * 1.80) - (uv.y * 1.12) + 0.18) * TAU), 2.1);',
         loadSidebarLanguageStatusRegistry(function() {
         '  float waist = gaussian(uv.y - 0.50, 0.25) * gaussian(absU - 0.20, 0.19);',
            renderSidebarLanguageBox();
        '  float grain = (noise - 0.5) * 0.050;',
            if (typeof callback === 'function') callback(sidebarLanguageStatusRegistry);
        '  float staticField =',
         }, true);
        '    0.055 +',
    },
        '    sideLift * 0.210 +',
    refreshDial: function() {
        '    (leftRibbon + rightRibbon) * 0.145 +',
        renderSidebarLanguageBox();
        '    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 getSidebarLanguageMeta(lang) {
    function compileShader(type, source) {
    var currentLang = getCurrentLang();
        var shader = gl.createShader(type);
    var targetTitle = getLanguageTargetTitle(lang);
        gl.shaderSource(shader, source);
    var isCurrent = lang === currentLang;
        gl.compileShader(shader);


    return {
        if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) {
        lang: lang,
            console.error('WebGL shader compile error:', gl.getShaderInfoLog(shader));
        code: getLangShortCode(lang),
            gl.deleteShader(shader);
        name: getSidebarLanguageName(lang),
            return null;
        dialName: getSidebarLanguageDialName(lang),
         }
        targetTitle: targetTitle,
        isCurrent: isCurrent,
         canMove: !!targetTitle && !isCurrent
    };
}


function getSidebarLanguageStatus(meta) {
         return shader;
    var override;
 
    if (!meta) {
         return {
            className: 'is-locked',
            label: 'UNAVAILABLE',
            canApply: false
        };
     }
     }


     if (meta.isCurrent) {
     var vertexShader = compileShader(gl.VERTEX_SHADER, vertexSrc);
        return {
    var fragmentShader = compileShader(gl.FRAGMENT_SHADER, fragmentSrc);
            className: 'is-current',
            label: 'CURRENT',
            canApply: false
        };
    }


     override = getSidebarLanguageStatusOverride(meta.lang);
     if (!vertexShader || !fragmentShader) return;


     if (override === 'wip') {
     var program = gl.createProgram();
        return {
    gl.attachShader(program, vertexShader);
            className: 'is-locked',
    gl.attachShader(program, fragmentShader);
            label: 'WIP',
     gl.linkProgram(program);
            canApply: false
        };
     }


     if (override === 'unavailable') {
     if (!gl.getProgramParameter(program, gl.LINK_STATUS)) {
         return {
         console.error('WebGL program link error:', gl.getProgramInfoLog(program));
            className: 'is-locked',
         return;
            label: 'UNAVAILABLE',
            canApply: false
         };
     }
     }


     if (override === 'available' || meta.targetTitle) {
     var positionLoc = gl.getAttribLocation(program, 'a_position');
        return {
    var resolutionLoc = gl.getUniformLocation(program, 'u_resolution');
            className: meta.targetTitle ? 'is-ready' : 'is-locked',
    var timeLoc = gl.getUniformLocation(program, 'u_time');
            label: meta.targetTitle ? 'AVAILABLE' : 'UNAVAILABLE',
            canApply: !!meta.targetTitle
        };
    }


     return {
     var buffer = gl.createBuffer();
         className: 'is-locked',
    gl.bindBuffer(gl.ARRAY_BUFFER, buffer);
         label: 'UNAVAILABLE',
    gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([
         canApply: false
         -1, -1,
     };
        1, -1,
}
         -1,  1,
         -1,  1,
        1, -1,
        1,  1
     ]), gl.STATIC_DRAW);


function sidebarLanguageRad(deg) {
    gl.useProgram(program);
     return (deg * Math.PI) / 180;
     gl.bindBuffer(gl.ARRAY_BUFFER, buffer);
}
    gl.enableVertexAttribArray(positionLoc);
    gl.vertexAttribPointer(positionLoc, 2, gl.FLOAT, false, 0, 0);


function sidebarLanguagePointAt(radius, deg) {
    function resize() {
    var state = sidebarLanguageState;
        var dpr = Math.min(window.devicePixelRatio || 1, 1.5);
    var angle = sidebarLanguageRad(deg);
        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));


    return {
        if (canvas.width !== w || canvas.height !== h) {
        x: state.cx + Math.sin(angle) * radius,
            canvas.width = w;
        y: state.cy - Math.cos(angle) * radius
            canvas.height = h;
    };
            canvas.style.width = cssW + 'px';
}
            canvas.style.height = cssH + 'px';
            gl.viewport(0, 0, w, h);
        }
    }


function getSidebarLanguageSectorPath(start, end) {
     var prefersReducedMotion = false;
     var state = sidebarLanguageState;
     try {
     var p1 = sidebarLanguagePointAt(state.outerR, start);
        prefersReducedMotion = window.matchMedia && window.matchMedia('(prefers-reduced-motion: reduce)').matches;
    var p2 = sidebarLanguagePointAt(state.outerR, end);
     } catch (err) {}
    var p3 = sidebarLanguagePointAt(state.innerR, end);
    var p4 = sidebarLanguagePointAt(state.innerR, start);
     var largeArc = Math.abs(end - start) > 180 ? 1 : 0;


     return [
     var lastFrame = 0;
        'M', p1.x.toFixed(3), p1.y.toFixed(3),
    var frameInterval = prefersReducedMotion ? 1000 : 66;
        'A', state.outerR, state.outerR, 0, largeArc, 1, p2.x.toFixed(3), p2.y.toFixed(3),
     var startTime = performance.now();
        '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() {
    function render(now) {
    return getSidebarLanguageSectorPath(-68, 68);
        requestAnimationFrame(render);
}
        if (document.hidden) return;
        if (now - lastFrame < frameInterval) return;
 
        lastFrame = now;
        resize();


function getSidebarLanguageByStep(step) {
        gl.clearColor(0, 0, 0, 1);
    var state = sidebarLanguageState;
        gl.clear(gl.COLOR_BUFFER_BIT);
    var index = normalizeSidebarLanguageIndex(state.baseIndex + step);
        gl.uniform2f(resolutionLoc, canvas.width, canvas.height);
        gl.uniform1f(timeLoc, now - startTime);
        gl.drawArrays(gl.TRIANGLES, 0, 6);
    }


     return {
     resize();
        index: index,
     requestAnimationFrame(render);
        meta: getSidebarLanguageMeta(state.order[index])
     };
}
}


function getSidebarLanguagePreviewIndex() {
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 state = sidebarLanguageState;
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 step = Math.round(-state.rotation / state.sectorAngle);
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>';
    return normalizeSidebarLanguageIndex(state.baseIndex + step);
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 getSidebarLanguagePreviewMeta() {
function invalidateProfileRender() {
     var state = sidebarLanguageState;
     PROFILE_RENDER_TOKEN++;
    return getSidebarLanguageMeta(state.order[getSidebarLanguagePreviewIndex()]);
}
}


function makeSidebarLanguageSector(step) {
$(function() {
     var state = sidebarLanguageState;
     initHalftoneBackground();
    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');
// ── 하단 Plank 단축키 가이드 ──
    group.setAttribute('data-step', String(step));
function escapeClbiBottomGuideHtml(value) {
    group.setAttribute('data-index', String(item.index));
     return String(value == null ? '' : value)
    group.setAttribute('data-lang', item.meta.lang);
        .replace(/&/g, '&amp;')
    group.setAttribute('transform', 'rotate(' + angle + ' ' + state.cx + ' ' + state.cy + ')');
        .replace(/</g, '&lt;')
        .replace(/>/g, '&gt;')
        .replace(/"/g, '&quot;')
        .replace(/'/g, '&#039;');
}


     path.setAttribute('class', 'sidebar-lang-sector');
function readClbiBottomGuidePageName() {
    path.setAttribute('d', getSidebarLanguageSectorPath(-state.halfSector, state.halfSector));
     var pageName = window.mw && mw.config ? (mw.config.get('wgPageName') || '') : '';


     label.setAttribute('class', 'sidebar-lang-sector-label');
     if (!pageName) {
    label.setAttribute('x', String(state.cx));
        pageName = window.location.pathname || '';
    label.setAttribute('y', String(labelY + 5));
     }
     label.textContent = item.meta.dialName || item.meta.name;


     group.appendChild(path);
     return String(pageName)
    group.appendChild(label);
        .split('?')[0]
        .replace(/^\/index\.php\//, '')
        .replace(/_/g, ' ')
        .trim();
}


    group.addEventListener('click', function(e) {
function isClbiNationsShortcutContext() {
        if (sidebarLanguageState.dragging || performance.now() < sidebarLanguageState.suppressClickUntil) return;
    var pageName = readClbiBottomGuidePageName();


         e.preventDefault();
    return pageName === '국가 및 조합' ||
         e.stopPropagation();
         pageName === 'Nations & Factions' ||
         !!document.querySelector('.clbi-nations-panel-stack, .clbi-nations-globe-window, .clbi-nations-tabpanel');
}


        cancelSidebarLanguageSpin();
function isClbiShortcutEditableTarget(target) {
        snapSidebarLanguageToStep(parseInt(group.getAttribute('data-step') || '0', 10), true);
     var el = target;
     });


     return group;
     while (el && el !== document.body) {
}
        if (el.isContentEditable) return true;


function renderSidebarLanguageWheel() {
        if (el.tagName) {
    var state = sidebarLanguageState;
            var tag = String(el.tagName).toLowerCase();
    var fan = document.getElementById('clbi-sidebar-lang-fan');
            if (tag === 'input' || tag === 'textarea' || tag === 'select' || tag === 'button') return true;
    var svg;
        }
    var defs;
 
    var clip;
        if (el.classList && (
    var clipPath;
            el.classList.contains('oo-ui-inputWidget-input') ||
    var shadowBlur;
            el.classList.contains('ve-ce-documentNode') ||
    var blur;
            el.classList.contains('CodeMirror')
    var fixedDepthGradient;
        )) {
    var shell;
            return true;
    var clipped;
        }
    var rotor;
 
    var fixedDepthPath;
        el = el.parentNode;
    var fixedFocus;
     }
    var shadowSoft;
    var shadowHard;
     var rim;
    var pointer;
    var tri;
    var line;
    var step;


     if (!fan) return;
     return false;
}


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


     svg = createSidebarLanguageSvgEl('svg');
     return action === 'edit' ||
    svg.setAttribute('class', 'sidebar-lang-fan-svg');
        action === 'submit' ||
    svg.setAttribute('viewBox', '0 0 202 150');
        /(?:^|[?&])action=(?:edit|submit)(?:&|$)/.test(search) ||
    svg.setAttribute('preserveAspectRatio', 'xMidYMid meet');
        !!document.querySelector('#editform, #wpSave, input[name="wpSave"], button[name="wpSave"]');
    svg.setAttribute('role', 'img');
}
    svg.setAttribute('aria-label', '언어 선택 다이얼');


     defs = createSidebarLanguageSvgEl('defs');
function isClbiNationLinkEditorShortcutContext() {
     return !!document.querySelector('.nation-link-editor');
}


    clip = createSidebarLanguageSvgEl('clipPath');
function isClbiNationListManagerShortcutContext() {
     clip.setAttribute('id', 'clbi-sidebar-language-fan-clip');
     return !!document.querySelector('.nation-list-manager');
    clipPath = createSidebarLanguageSvgEl('path');
}
    clipPath.setAttribute('d', getSidebarLanguageShellPath());
    clip.appendChild(clipPath);


    shadowBlur = createSidebarLanguageSvgEl('filter');
function getClbiBottomShortcutItems() {
     shadowBlur.setAttribute('id', 'clbi-sidebar-language-shadow-blur');
     if (isClbiWikiEditShortcutContext()) {
    shadowBlur.setAttribute('x', '-20%');
        return [
     shadowBlur.setAttribute('y', '-20%');
            { key: 'Ctrl+S', label: '변경사항 저장' }
     shadowBlur.setAttribute('width', '140%');
        ];
    shadowBlur.setAttribute('height', '140%');
     }
    blur = createSidebarLanguageSvgEl('feGaussianBlur');
 
    blur.setAttribute('stdDeviation', '3');
     if (isClbiNationLinkEditorShortcutContext()) {
     shadowBlur.appendChild(blur);
        return [
            { key: 'Ctrl+S', label: '링크표 저장' }
        ];
     }


     fixedDepthGradient = createSidebarLanguageSvgEl('linearGradient');
     if (isClbiNationListManagerShortcutContext()) {
    fixedDepthGradient.setAttribute('id', 'clbi-sidebar-language-fixed-depth');
        return [
    fixedDepthGradient.setAttribute('x1', '0');
            { key: 'Ctrl+S', label: '국가 목록 저장' }
    fixedDepthGradient.setAttribute('y1', '0');
        ];
    fixedDepthGradient.setAttribute('x2', '0');
     }
     fixedDepthGradient.setAttribute('y2', '1');


     [
     if (isClbiNationsShortcutContext()) {
         ['0%', '#ffffff', '0.030'],
         return [
        ['34%', '#ffffff', '0.006'],
            { key: 'Q', label: '이전 시대' },
        ['58%', '#000000', '0.030'],
            { key: 'E', label: '다음 시대' },
        ['100%', '#000000', '0.250']
            { key: 'Shift+Q', label: '이전 연도' },
    ].forEach(function(item) {
            { key: 'Shift+E', label: '다음 연도' },
        var stop = createSidebarLanguageSvgEl('stop');
            { key: 'Ctrl+Q', label: '이전 대륙' },
        stop.setAttribute('offset', item[0]);
            { key: 'Ctrl+E', label: '다음 대륙' }
        stop.setAttribute('stop-color', item[1]);
         ];
         stop.setAttribute('stop-opacity', item[2]);
     }
        fixedDepthGradient.appendChild(stop);
     });


     defs.appendChild(clip);
     return [];
    defs.appendChild(shadowBlur);
}
    defs.appendChild(fixedDepthGradient);
    svg.appendChild(defs);


    shell = createSidebarLanguageSvgEl('path');
function renderClbiBottomShortcutGuide() {
     shell.setAttribute('class', 'sidebar-lang-shell');
     var guide = document.getElementById('clbi-bottom-shortcut-guide');
     shell.setAttribute('d', getSidebarLanguageShellPath());
     var items;
     svg.appendChild(shell);
     var html;


     clipped = createSidebarLanguageSvgEl('g');
     if (!guide) return;
    clipped.setAttribute('clip-path', 'url(#clbi-sidebar-language-fan-clip)');


     rotor = createSidebarLanguageSvgEl('g');
     items = getClbiBottomShortcutItems();
    rotor.setAttribute('id', 'clbi-sidebar-lang-wheel-rotor');
    rotor.setAttribute('class', 'sidebar-lang-wheel-rotor');


     for (step = -state.repeats; step <= state.repeats; step += 1) {
     if (!items.length) {
         rotor.appendChild(makeSidebarLanguageSector(step));
         guide.classList.add('is-empty');
        guide.innerHTML = '';
        return;
     }
     }


     clipped.appendChild(rotor);
     guide.classList.remove('is-empty');
     svg.appendChild(clipped);
 
    html = '<div class="clbi-bottom-shortcut-title">SHORTCUT GUIDE</div>';
     html += '<div class="clbi-bottom-shortcut-list">';


     fixedDepthPath = createSidebarLanguageSvgEl('path');
     items.forEach(function (item) {
    fixedDepthPath.setAttribute('class', 'sidebar-lang-fixed-depth');
        html += '<div class="clbi-bottom-shortcut-item">' +
    fixedDepthPath.setAttribute('d', getSidebarLanguageShellPath());
            '<span class="clbi-bottom-shortcut-key">' + escapeClbiBottomGuideHtml(item.key) + '</span>' +
     svg.appendChild(fixedDepthPath);
            '<span class="clbi-bottom-shortcut-label">' + escapeClbiBottomGuideHtml(item.label) + '</span>' +
        '</div>';
     });


     fixedFocus = createSidebarLanguageSvgEl('path');
     html += '</div>';
     fixedFocus.setAttribute('class', 'sidebar-lang-fixed-focus');
     guide.innerHTML = html;
    fixedFocus.setAttribute('d', getSidebarLanguageSectorPath(-state.halfSector, state.halfSector));
}
    svg.appendChild(fixedFocus);


     shadowSoft = createSidebarLanguageSvgEl('path');
function buildClbiBottomPlankHtml(wrapId, navId, mainId) {
    shadowSoft.setAttribute('class', 'sidebar-lang-inner-shadow-soft');
     return '' +
    shadowSoft.setAttribute('d', getSidebarLanguageShellPath());
        '<div id="' + wrapId + '">' +
    svg.appendChild(shadowSoft);
            '<div id="' + navId + '">' +
                '<div id="' + mainId + '">' +
                    '<div id="clbi-bottom-shortcut-guide" class="is-empty" aria-label="단축키 안내"></div>' +
                '</div>' +
            '</div>' +
        '</div>';
}


    shadowHard = createSidebarLanguageSvgEl('path');
var CLBI_NATIONS_LAST_POINTER_X = null;
    shadowHard.setAttribute('class', 'sidebar-lang-inner-shadow-hard');
var CLBI_NATIONS_LAST_POINTER_Y = null;
    shadowHard.setAttribute('d', getSidebarLanguageShellPath());
    svg.appendChild(shadowHard);


    rim = createSidebarLanguageSvgEl('path');
function getClbiNationsTabAtPointer(tabpanel) {
     rim.setAttribute('class', 'sidebar-lang-rim');
     var element;
     rim.setAttribute('d', getSidebarLanguageShellPath());
     var tab;
    svg.appendChild(rim);


     pointer = createSidebarLanguageSvgEl('g');
     if (!tabpanel) return null;
     pointer.setAttribute('class', 'sidebar-lang-fixed-pointer');
     if (CLBI_NATIONS_LAST_POINTER_X === null || CLBI_NATIONS_LAST_POINTER_Y === null) return null;
     pointer.setAttribute('clip-path', 'url(#clbi-sidebar-language-fan-clip)');
     if (typeof document.elementFromPoint !== 'function') return null;


     tri = createSidebarLanguageSvgEl('path');
     element = document.elementFromPoint(CLBI_NATIONS_LAST_POINTER_X, CLBI_NATIONS_LAST_POINTER_Y);
    tri.setAttribute('class', 'sidebar-lang-pointer-triangle');
     if (!element) return null;
    tri.setAttribute('d', 'M ' + (state.cx - 10) + ' 10 L ' + (state.cx + 10) + ' 10 L ' + state.cx + ' 26 Z');
     pointer.appendChild(tri);


     line = createSidebarLanguageSvgEl('line');
     tab = element.closest ? element.closest('.clbi-nations-tabpanel-tab[data-continent]') : null;
    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);
     if (!tab || !tabpanel.contains(tab)) return null;
    fan.appendChild(svg);


     state.rotor = rotor;
     return tab;
     setSidebarLanguageRotation(state.rotation, false);
}
}
 
function validateClbiNationsPointerHover(tabpanel, forceSuppress) {
     var tab = getClbiNationsTabAtPointer(tabpanel);
    var suppressContinent;
    var tabContinent;
    var tabIsActive;


function updateSidebarLanguageDial() {
     if (!tabpanel) return;
    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) {
     if (!tab) {
         selectedValue.textContent = meta.name;
         clearClbiNationsKeyboardHoverSuppressed(tabpanel);
        return;
     }
     }


     if (availabilityPanel) {
     tabContinent = tab.getAttribute('data-continent') || '';
        availabilityPanel.classList.remove('is-ready', 'is-current', 'is-locked');
    tabIsActive = tab.classList.contains('is-active') || tab.getAttribute('aria-selected') === 'true';
         availabilityPanel.classList.add(status.className);
 
    if (tabIsActive) {
         clearClbiNationsKeyboardHoverSuppressed(tabpanel);
        return;
     }
     }


     if (availabilityValue) {
     suppressContinent = tabpanel.getAttribute('data-clbi-hover-suppress-continent') || '';
        availabilityValue.textContent = status.label;
    }


     if (apply) {
     if (forceSuppress || !suppressContinent) {
         apply.classList.toggle('is-disabled', !status.canApply);
         tabpanel.classList.add('is-keyboard-switching');
         apply.setAttribute('aria-disabled', status.canApply ? 'false' : 'true');
         tabpanel.setAttribute('data-clbi-hover-suppress-continent', tabContinent);
         apply.setAttribute('aria-label', status.canApply ? (meta.name + ' 적용') : (meta.isCurrent ? '현재 언어' : '사용할 수 없는 언어'));
         return;
     }
     }


     if (selector) {
     if (suppressContinent === tabContinent) {
         selector.setAttribute('data-selected-lang', meta.lang);
         tabpanel.classList.add('is-keyboard-switching');
        selector.setAttribute('data-selected-code', meta.code);
         return;
        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,
    * The pointer actually moved onto another tab after the keyboard switch.
        status: status
    * At that point this is no longer stale browser :hover; let normal hover work.
     };
    */
     clearClbiNationsKeyboardHoverSuppressed(tabpanel);
}
}


function setSidebarLanguageRotation(value, animate) {
function validateAllClbiNationsPointerHovers() {
     var state = sidebarLanguageState;
     var panels = document.querySelectorAll('.clbi-nations-tabpanel.is-keyboard-switching');
    state.rotation = value;
    updateSidebarLanguageDial();


     if (!state.rotor) return;
     Array.prototype.forEach.call(panels, function (tabpanel) {
        validateClbiNationsPointerHover(tabpanel, false);
    });
}


    if (animate) {
function activateClbiNationsContinent(tabpanel, targetContinent) {
        $('#clbi-sidebar-lang-selector').addClass('is-snapping');
    var tabs;
     } else {
     var panels;
        $('#clbi-sidebar-lang-selector').removeClass('is-snapping');
    }


     state.rotor.style.transform = 'rotate(' + state.rotation.toFixed(3) + 'deg)';
     if (!tabpanel || !targetContinent) return false;
}


function requestSidebarLanguageRotation(value) {
    tabs = Array.prototype.slice.call(tabpanel.querySelectorAll('.clbi-nations-tabpanel-tab[data-continent]'));
     var state = sidebarLanguageState;
     panels = Array.prototype.slice.call(tabpanel.querySelectorAll('.clbi-nations-tabpanel-continent[data-continent-panel]'));
    state.pendingRotation = value;


     if (state.raf) return;
     if (!tabs.length || !panels.length) return false;


     state.raf = requestAnimationFrame(function() {
     tabs.forEach(function (tab) {
         state.raf = null;
         var active = tab.getAttribute('data-continent') === targetContinent;
         setSidebarLanguageRotation(state.pendingRotation, false);
         tab.classList.toggle('is-active', active);
        tab.setAttribute('aria-selected', active ? 'true' : 'false');
        tab.setAttribute('tabindex', active ? '0' : '-1');
     });
     });
}


function cancelSidebarLanguageSpin() {
    panels.forEach(function (panel) {
    var state = sidebarLanguageState;
        var active = panel.getAttribute('data-continent-panel') === targetContinent;
        panel.classList.toggle('is-active', active);
 
        if (active) {
            panel.removeAttribute('hidden');
        } else {
            panel.setAttribute('hidden', 'hidden');
        }
    });


     if (state.inertiaRaf) {
     return true;
        cancelAnimationFrame(state.inertiaRaf);
}
        state.inertiaRaf = null;
    }


    $('#clbi-sidebar-lang-selector').removeClass('is-spinning');
function setClbiNationsKeyboardHoverSuppressed(tabpanel) {
    validateClbiNationsPointerHover(tabpanel, true);
}
}


function finishSidebarLanguageSnap(nearestIndex, callback) {
function clearClbiNationsKeyboardHoverSuppressed(tabpanel) {
     var state = sidebarLanguageState;
     if (!tabpanel) return;
    tabpanel.classList.remove('is-keyboard-switching');
    tabpanel.removeAttribute('data-clbi-hover-suppress-continent');
}


     state.baseIndex = normalizeSidebarLanguageIndex(nearestIndex);
function moveClbiNationsContinent(direction) {
     state.selectedIndex = state.baseIndex;
     var tabpanel = document.querySelector('.clbi-nations-tabpanel');
     state.rotation = 0;
     var tabs;
     state.dragging = false;
    var activeIndex;
     var nextIndex;
     var target;


     $('#clbi-sidebar-lang-selector').removeClass('is-snapping is-dragging is-spinning');
     if (!tabpanel) return false;


     renderSidebarLanguageWheel();
     tabs = Array.prototype.slice.call(tabpanel.querySelectorAll('.clbi-nations-tabpanel-tab[data-continent]'));
     updateSidebarLanguageDial();
     if (!tabs.length) return false;


     if (typeof callback === 'function') {
     activeIndex = tabs.findIndex(function (tab) {
         callback(getSidebarLanguageMeta(state.order[state.selectedIndex]));
         return tab.classList.contains('is-active') || tab.getAttribute('aria-selected') === 'true';
     }
     });
}


function snapSidebarLanguageToStep(step, animate, callback) {
    if (activeIndex < 0) activeIndex = 0;
    var state = sidebarLanguageState;
    var targetRotation = -step * state.sectorAngle;
    var nearestIndex = normalizeSidebarLanguageIndex(state.baseIndex + step);


     cancelSidebarLanguageSpin();
     nextIndex = (activeIndex + direction + tabs.length) % tabs.length;
     clearTimeout(state.snapTimer);
     target = tabs[nextIndex].getAttribute('data-continent');


     state.selectedIndex = nearestIndex;
     if (activateClbiNationsContinent(tabpanel, target)) {
    setSidebarLanguageRotation(targetRotation, !!animate);
        setClbiNationsKeyboardHoverSuppressed(tabpanel);
        window.setTimeout(function () {
            validateClbiNationsPointerHover(tabpanel, false);
        }, 0);
        return true;
    }


     state.snapTimer = setTimeout(function() {
     return false;
        finishSidebarLanguageSnap(nearestIndex, callback);
    }, animate ? 230 : 0);
}
}


function snapSidebarLanguageNearest(callback) {
function moveClbiNationsEra(direction) {
     var state = sidebarLanguageState;
     var side = direction < 0 ? 'left' : 'right';
     var step = Math.round(-state.rotation / state.sectorAngle);
     var selector = '.clbi-nations-era-side-slot-' + side +
     snapSidebarLanguageToStep(step, true, callback);
        ' .mw-collapsible:not(.mw-collapsed) .clbi-nations-era-side-button-' + side;
    var button = document.querySelector(selector);
 
    if (!button) {
        button = document.querySelector('.clbi-nations-era-side-button-' + side);
    }
 
    if (!button) return false;
 
     button.dispatchEvent(new MouseEvent('click', {
        bubbles: true,
        cancelable: true,
        view: window
    }));
 
    return true;
}
}


function startSidebarLanguageInertiaSpin(initialVelocity) {
function moveClbiNationsHistoryYearShortcut(direction) {
     var state = sidebarLanguageState;
     var panel = document.querySelector('.clbi-nations-history-panel');
     var velocity;
    var tabs;
     var lastFrame;
    var pages;
     var activeIndex;
     var nextIndex;
    var target;
 
    if (!panel) return false;


     cancelSidebarLanguageSpin();
     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]'));


     velocity = Math.max(-state.maxSpinVelocity, Math.min(state.maxSpinVelocity, initialVelocity));
     if (!tabs.length || !pages.length) return false;


     if (Math.abs(velocity) < state.minSpinVelocity) {
     activeIndex = tabs.findIndex(function (tab) {
         snapSidebarLanguageNearest();
         return tab.classList.contains('is-active') || tab.getAttribute('aria-selected') === 'true';
        return;
     });
     }


     $('#clbi-sidebar-lang-selector').addClass('is-spinning');
     if (activeIndex < 0) activeIndex = 0;
    lastFrame = performance.now();


     function frame(now) {
     nextIndex = (activeIndex + direction + tabs.length) % tabs.length;
        var dt = Math.min(34, Math.max(1, now - lastFrame));
    target = tabs[nextIndex].getAttribute('data-year');
        var sign = velocity < 0 ? -1 : 1;
        var nextSpeed;


        lastFrame = now;
    if (!target) return false;
        state.rotation += velocity * dt;
        setSidebarLanguageRotation(state.rotation, false);


         nextSpeed = Math.max(0, Math.abs(velocity) - (state.spinDecel * dt));
    tabs.forEach(function (tab) {
        velocity = sign * nextSpeed;
         var active = tab.getAttribute('data-year') === target;
        tab.classList.toggle('is-active', active);
        tab.setAttribute('aria-selected', active ? 'true' : 'false');
        tab.setAttribute('tabindex', active ? '0' : '-1');
    });


        if (nextSpeed <= state.minSpinVelocity) {
    pages.forEach(function (page) {
            state.inertiaRaf = null;
        var active = page.getAttribute('data-year-panel') === target;
            $('#clbi-sidebar-lang-selector').removeClass('is-spinning');
        page.classList.toggle('is-active', active);
             snapSidebarLanguageNearest();
 
             return;
        if (active) {
             page.removeAttribute('hidden');
        } else {
             page.setAttribute('hidden', 'hidden');
         }
         }
    });


        state.inertiaRaf = requestAnimationFrame(frame);
     return true;
     }
 
    state.inertiaRaf = requestAnimationFrame(frame);
}
}


function scheduleSidebarLanguageNavigation(meta) {
function initClbiNationsTabpanelControls(root) {
     var status = getSidebarLanguageStatus(meta);
     var scope = root && root.querySelectorAll ? root : document;
    var panels = scope.querySelectorAll('.clbi-nations-tabpanel');


     if (!meta || !status.canApply) return;
     Array.prototype.forEach.call(panels, function (tabpanel) {
        if (tabpanel.getAttribute('data-clbi-nations-tabs-ready') === '1') return;


    clearTimeout(sidebarLanguageState.navigateTimer);
        tabpanel.setAttribute('data-clbi-nations-tabs-ready', '1');
    sidebarLanguageState.navigateTimer = setTimeout(function() {
        var title = getLanguageTargetTitle(meta.lang);


         if (!title || meta.lang === getCurrentLang()) return;
         tabpanel.addEventListener('pointerdown', function () {
            clearClbiNationsKeyboardHoverSuppressed(tabpanel);
        });


         window.location.href = buildWikiPath(title);
         tabpanel.addEventListener('pointerleave', function () {
    }, 70);
            clearClbiNationsKeyboardHoverSuppressed(tabpanel);
}
        });


function setSidebarLanguageSelection(lang) {
        tabpanel.addEventListener('click', function (event) {
    var state = sidebarLanguageState;
            var tab = event.target.closest ? event.target.closest('.clbi-nations-tabpanel-tab[data-continent]') : null;
    var index = state.order.indexOf(lang);


    if (index < 0) index = state.order.indexOf(getCurrentLang());
            if (!tab || !tabpanel.contains(tab)) return;
    if (index < 0) index = 0;


    if (state.raf) {
            clearClbiNationsKeyboardHoverSuppressed(tabpanel);
        cancelAnimationFrame(state.raf);
        state.raf = null;
    }


    cancelSidebarLanguageSpin();
            if (activateClbiNationsContinent(tabpanel, tab.getAttribute('data-continent'))) {
    clearTimeout(state.snapTimer);
                event.preventDefault();
            }
        });


    state.currentLang = lang;
        tabpanel.addEventListener('keydown', function (event) {
    state.baseIndex = index;
            var tab = event.target.closest ? event.target.closest('.clbi-nations-tabpanel-tab[data-continent]') : null;
    state.selectedIndex = index;
            var handled = false;
    state.rotation = 0;
    state.dragging = false;
    state.dragMoved = false;
    state.releaseVelocity = 0;


    renderSidebarLanguageWheel();
            if (!tab || !tabpanel.contains(tab)) return;
    updateSidebarLanguageDial();
}


function moveSidebarLanguageSelection(delta) {
            if (event.key === 'ArrowLeft') handled = moveClbiNationsContinent(-1);
     snapSidebarLanguageToStep(-delta, true);
            else if (event.key === 'ArrowRight') handled = moveClbiNationsContinent(1);
 
            if (handled) {
                event.preventDefault();
                event.stopPropagation();
            }
        });
     });
}
}


function bindSidebarLanguageSelector() {
function initClbiNationsPointerHoverValidation() {
     var state = sidebarLanguageState;
     if (document.body.getAttribute('data-clbi-nations-pointer-hover-ready') === '1') return;
    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;
     document.body.setAttribute('data-clbi-nations-pointer-hover-ready', '1');


     if (state.bound && state.boundElement === selector) return;
     document.addEventListener('pointermove', function (event) {
        CLBI_NATIONS_LAST_POINTER_X = event.clientX;
        CLBI_NATIONS_LAST_POINTER_Y = event.clientY;
        validateAllClbiNationsPointerHovers();
    }, true);


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


     fan.addEventListener('pointerdown', function(e) {
     window.addEventListener('blur', function () {
         cancelSidebarLanguageSpin();
         CLBI_NATIONS_LAST_POINTER_X = null;
        clearTimeout(state.snapTimer);
        CLBI_NATIONS_LAST_POINTER_Y = null;
        validateAllClbiNationsPointerHovers();
    });
}


        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');
function initClbiBottomShortcutSystem(root) {
        selector.classList.remove('is-snapping');
    renderClbiBottomShortcutGuide();
    initClbiNationsTabpanelControls(root || document);
    initClbiNationsPointerHoverValidation();
}


        /*
// ── 상·하단 네비게이션 바 ──
        Vertical page scrolling must stay available when the pointer starts on
function buildClbiNavHtml(position) {
        the language dial. Capture and preventDefault are delayed until a
    var isBottom = position === 'bottom';
        horizontal drag is confirmed.
    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 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';


     fan.addEventListener('pointermove', function(e) {
     if (isBottom) {
         var now;
         return buildClbiBottomPlankHtml(wrapId, navId, mainId);
        var totalDx;
    }
        var totalDy;
        var frameDx;
        var dt;
        var instantVelocity;


         if (!state.dragging) return;
    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 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>';
}
 
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;


        totalDx = e.clientX - state.dragStartX;
    if (!contentWrapper || !topNav || !bottomNav || !document.body) return;
        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';
    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 (state.dragAxis === 'y') {
    if (host) {
                state.dragging = false;
        host.classList.add('clbi-shell-host');
                state.dragMoved = false;
    }
                state.dragAxis = null;
                state.pointerCaptured = false;
                selector.classList.remove('is-dragging');
                return;
            }


            if (fan.setPointerCapture && e.pointerId != null) {
    viewportH = window.innerHeight || document.documentElement.clientHeight || 0;
                try {
    topRect = topNav.getBoundingClientRect();
                    fan.setPointerCapture(e.pointerId);
    wrapperRect = contentWrapper.getBoundingClientRect();
                    state.pointerCaptured = true;
    expectedWrapperTop = topRect.bottom + 8;
                } catch (err) {
                    state.pointerCaptured = false;
                }
            }
        }


        if (state.dragAxis !== 'x') return;
    needsRecovery = false;


        now = performance.now();
    if (viewportH > 0) {
         frameDx = e.clientX - state.lastX;
         if (topRect.top >= viewportH * 0.55) needsRecovery = true;
         dt = Math.max(1, now - state.lastTime);
         if (wrapperRect.top >= viewportH * 0.60) needsRecovery = true;
    }


        if (Math.abs(totalDx) > 3) state.dragMoved = true;
    if (topRect.top > 240 || wrapperRect.top > 320) {
        needsRecovery = true;
    }


         instantVelocity = (frameDx * state.dragSensitivity) / dt;
    if (wrapperRect.top < expectedWrapperTop - 1) {
         state.releaseVelocity = (state.releaseVelocity * 0.62) + (instantVelocity * 0.38);
         needsRecovery = true;
         state.lastX = e.clientX;
    }
        state.lastTime = now;
 
    if (!needsRecovery) {
         document.body.classList.add('clbi-shell-ready');
         return;
    }


        requestSidebarLanguageRotation(state.dragStartRotation + totalDx * state.dragSensitivity);
    anchor = canvas && canvas.parentNode === document.body
         e.preventDefault();
         ? canvas.nextSibling
         e.stopPropagation();
         : document.body.firstChild;
    });


     function finishDrag(e) {
     document.body.insertBefore(topNav, anchor);
        var velocityAge;
    document.body.insertBefore(contentWrapper, topNav.nextSibling);
        var throwVelocity;
    document.body.insertBefore(bottomNav, contentWrapper.nextSibling);
        var wasHorizontal;


        if (!state.dragging) return;
    document.body.classList.add('clbi-shell-ready');
}


        wasHorizontal = state.dragAxis === 'x';
window.normalizeClbiShellDomOrder = normalizeClbiShellDomOrder;
        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;
function ensureClbiVerticalScaleHost() {
        state.dragAxis = null;
    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 (!wasHorizontal && !state.dragMoved) {
    if (!topNav || !contentWrapper || !bottomNav || !document.body) return null;
            return;
        }


        velocityAge = performance.now() - state.lastTime;
    existing = document.getElementById('clbi-shell-scale-host');
        throwVelocity = velocityAge > 120 ? 0 : state.releaseVelocity;


        if (state.dragMoved) {
    if (existing && existing.contains(topNav) && existing.contains(contentWrapper) && existing.contains(bottomNav)) {
            state.suppressClickUntil = performance.now() + 180;
        existing.classList.add('clbi-shell-host');
         }
         return existing;
    }


        if (state.dragMoved && Math.abs(throwVelocity) >= state.minSpinVelocity) {
    parent = topNav.parentElement === contentWrapper.parentElement && contentWrapper.parentElement === bottomNav.parentElement
            startSidebarLanguageInertiaSpin(throwVelocity);
        ? topNav.parentElement
         } else {
         : null;
            snapSidebarLanguageNearest();
        }


        if (e) {
    if (parent && parent !== document.body) {
            e.preventDefault();
        parent.classList.add('clbi-shell-host');
            e.stopPropagation();
        parent.id = parent.id || 'clbi-shell-scale-host';
         }
         return parent;
     }
     }


     fan.addEventListener('pointerup', finishDrag);
     host = existing || document.createElement('div');
     fan.addEventListener('pointercancel', finishDrag);
     host.id = 'clbi-shell-scale-host';
     fan.addEventListener('lostpointercapture', function() {
     host.className = 'clbi-shell-host';
        if (!state.dragging) return;


         state.dragging = false;
    if (!host.parentNode) {
        state.pointerCaptured = false;
         document.body.insertBefore(host, topNav);
        state.dragAxis = null;
    }
        selector.classList.remove('is-dragging');
 
    host.appendChild(topNav);
    host.appendChild(contentWrapper);
    host.appendChild(bottomNav);
 
    return host;
}
 
function readClbiRootPx(name, fallback) {
    var raw = '';
    var value;


         if (state.dragMoved && Math.abs(state.releaseVelocity) >= state.minSpinVelocity) {
    try {
            state.suppressClickUntil = performance.now() + 180;
         raw = getComputedStyle(document.documentElement).getPropertyValue(name);
            startSidebarLanguageInertiaSpin(state.releaseVelocity);
    } catch (err) {}
        } else {
            snapSidebarLanguageNearest();
        }
    });


     apply.addEventListener('click', function(e) {
     value = parseFloat(raw);
        e.preventDefault();
    return isFinite(value) && value > 0 ? value : fallback;
        e.stopPropagation();
}


        snapSidebarLanguageNearest(function(meta) {
function resetLeftRecentAdaptiveState() {
            scheduleSidebarLanguageNavigation(meta);
    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')) : [];


     selector.addEventListener('keydown', function(e) {
     if (newsBox) {
         if (e.key === 'ArrowLeft') {
         newsBox.classList.remove('is-adaptive-constrained');
            moveSidebarLanguageSelection(-1);
        newsBox.style.removeProperty('--adaptive-news-h');
            e.preventDefault();
    }
        }


        if (e.key === 'ArrowRight') {
    if (list) {
            moveSidebarLanguageSelection(1);
        list.classList.remove('is-adaptive-faded');
            e.preventDefault();
        list.removeAttribute('data-adaptive-limit');
        }
        list.style.removeProperty('--adaptive-recent-h');
    }


        if (e.key === 'Enter' || e.key === ' ') {
    items.forEach(function (item) {
            apply.click();
        item.classList.remove('is-adaptive-hidden');
            e.preventDefault();
        }
     });
     });
}
}


function renderSidebarLanguageBox() {
function resetLeftBillboardAdaptiveState() {
     bindSidebarLanguageSelector();
     var box = document.querySelector('#clbi-left-sidebar .left-billboard-box');
    setSidebarLanguageSelection(getCurrentLang());


     if (!sidebarLanguageStatusLoaded) {
     if (!box) return;
        loadSidebarLanguageStatusRegistry(function() {
 
            setSidebarLanguageSelection(getCurrentLang());
    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 loadRecentChangesList(targetSelector, limit) {
function updateClbiShellVerticalScale() {
     var $target = $(targetSelector);
     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;


     if (!$target.length) return;
     root.style.setProperty('--clbi-stage-design-w', stageW + 'px');
    root.style.setProperty('--clbi-stage-design-h', Math.floor(stageH) + 'px');
    root.style.setProperty('--clbi-stage-content-h', contentH + 'px');
    root.style.setProperty('--clbi-shell-scale', String(scale));


     var lang = getCurrentLang();
     body.classList.toggle('clbi-shell-vertical-scale', shouldScale);
    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) {
     if (shouldScale) {
         return String(value == null ? '' : value)
         resetLeftRecentAdaptiveState();
            .replace(/&/g, '&amp;')
        resetLeftBillboardAdaptiveState();
            .replace(/</g, '&lt;')
            .replace(/>/g, '&gt;')
            .replace(/"/g, '&quot;')
            .replace(/'/g, '&#039;');
     }
     }
}
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();
}


    $target.html((t && t.loading) ? t.loading : '불러오는 중...');
var CLBI_SHELL_METRICS_RAF = null;


     $.getJSON(
function runClbiShellMetricsBatch() {
        '/api.php?action=query&list=recentchanges&rclimit=' + encodeURIComponent(limit || 5) + '&rcprop=title|timestamp|user&format=json&rcnamespace=0&rctype=edit|new',
     var top = document.getElementById('clbi-top-nav-wrap');
        function(data) {
    var bottom = document.getElementById('clbi-bottom-nav-wrap');
            var items = data && data.query ? data.query.recentchanges : [];
    var root = document.documentElement;
            var html = '';
    var topH = 0;
    var bottomH = 0;


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


            $.each(items, function(i, item) {
    if (!root) return;
                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) {
    if (top) {
                    html +=
        topH = Math.ceil(top.getBoundingClientRect().height || top.offsetHeight || 0);
                        '<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">' +
    if (bottom) {
                                '<div class="news-recent-title-wrap">' +
        bottomH = Math.ceil(bottom.getBoundingClientRect().height || bottom.offsetHeight || 0);
                                    '<span class="news-recent-title">' + escapeHtml(title) + '</span>' +
    }
                                '</div>' +
 
                                '<div class="news-recent-meta">' +
    root.style.setProperty('--clbi-top-nav-outer-h', topH + 'px');
                                    '<span class="news-recent-user">@' + escapeHtml(userName) + '</span>' +
    root.style.setProperty('--clbi-bottom-nav-outer-h', bottomH + 'px');
                                '</div>' +
 
                            '</div>' +
    if (typeof updateClbiShellVerticalScale === 'function') {
                            '<span class="news-recent-time">' + escapeHtml(label) + '</span>' +
        updateClbiShellVerticalScale();
                        '</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>';
                }
            });


            $target.html(html);
    if (typeof scheduleAdaptiveLeftRecentItems === 'function') {
        scheduleAdaptiveLeftRecentItems();
    }


            $target.find(isNewsList ? '.news-recent-item' : '.clbi-recent-item').each(function() {
    if (typeof scheduleClbiContentBottomGap === 'function') {
                var wrap = $(this).find(isNewsList ? '.news-recent-title-wrap' : '.clbi-recent-title-wrap');
        scheduleClbiContentBottomGap();
                var title = $(this).find(isNewsList ? '.news-recent-title' : '.clbi-recent-title');
    }
}


                if (!wrap.length || !title.length) return;
function updateClbiShellMetrics() {
    runClbiShellMetricsBatch();
}


                var wrapW = wrap.width();
function requestClbiShellMetricsFrame() {
                var titleW = title[0].scrollWidth;
    if (CLBI_SHELL_METRICS_RAF !== null) return;


                if (titleW > wrapW + 20) {
    CLBI_SHELL_METRICS_RAF = window.requestAnimationFrame
                    var duration = titleW / 40;
        ? window.requestAnimationFrame(runClbiShellMetricsBatch)
        : window.setTimeout(runClbiShellMetricsBatch, 16);
}


                    title.css({
function scheduleClbiShellMetrics() {
                        animation: 'clbi-scroll ' + duration + 's linear infinite',
    requestClbiShellMetricsFrame();
                        '--scroll-dist': '-' + (titleW - wrapW + 8) + 'px'
 
                    });
    window.setTimeout(requestClbiShellMetricsFrame, 0);
                }
    window.setTimeout(requestClbiShellMetricsFrame, 80);
            });
    window.setTimeout(requestClbiShellMetricsFrame, 240);
        }
}
    ).fail(function() {
function watchClbiShellMetrics() {
        var lang = getCurrentLang();
    var top = document.getElementById('clbi-top-nav-wrap');
        var t = (window.LANG && window.LANG[lang]) ? window.LANG[lang] : (window.LANG ? window.LANG.ko : null);
    var bottom = document.getElementById('clbi-bottom-nav-wrap');
    var observer;
 
    scheduleClbiShellMetrics();


        $target.html((t && t.loadFail) ? t.loadFail : '불러오기 실패');
    $(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) {
function updateLeftSidebarNationsImage() {
        observer = new ResizeObserver(scheduleClbiShellMetrics);
    $('#clbi-left-nations-image').remove();
        if (top) observer.observe(top);
        if (bottom) observer.observe(bottom);
        window.CLBI_SHELL_RESIZE_OBSERVER = observer;
    }
}
}


function setProfileActionLabel(selector, text) {
function bindClbiWorldbuildingToggle(buttonSelector, menuSelector) {
     var target = $(selector);
     $(buttonSelector).on('click', function() {
    var label = target.find('.profile-action-label');
        var $menu = $(menuSelector);
        var $btn = $(this);


    if (label.length) {
        $menu.toggleClass('worldbuilding-open');
         label.text(text);
         $btn.toggleClass('clbi-tnav-active', $menu.hasClass('worldbuilding-open'));
    } else {
         scheduleClbiShellMetrics();
         target.text(text);
     });
     }
}
}


// 사이드바 업데이트
bindClbiWorldbuildingToggle('#clbi-tnav-worldbuilding', '#clbi-sub-worldbuilding');
function updateSidebar() {
bindClbiWorldbuildingToggle('#clbi-bnav-worldbuilding', '#clbi-bottom-sub-worldbuilding');
     if (!window.LANG) {
 
         setTimeout(updateSidebar, 100);
$('#clbi-top-search-input, #clbi-bottom-search-input').on('keydown', function(e) {
         return;
     if (e.key === 'Enter') {
         var q = $(this).val().trim();
         if (q) window.location.href = '/index.php?search=' + encodeURIComponent(q);
     }
     }
});


    var currentLang = getCurrentLang();
if (window.mw && mw.hook) {
     var t = (window.LANG && window.LANG[currentLang]) ? window.LANG[currentLang] : window.LANG.ko;
     mw.hook('wikipage.content').add(function ($content) {
        initClbiBottomShortcutSystem($content && $content[0] ? $content[0] : document);
    });
}


    var newsTitle = t.news || '뉴스';
watchClbiShellMetrics();
    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);
var transitionSound = new Audio('/index.php?title=특수:Redirect/file/Sfx-ui-001.mp3');
    $('#clbi-left-news-recent-main').text(recentTitle);


    $('#clbi-title-search a').text(t.search);
(function() {
     $('#clbi-search-input').attr('placeholder', t.search + '...');
     var master = parseFloat(localStorage.getItem('clbi-audio-master') || 80) / 100;
     $('#clbi-title-recent a').text(recentTitle);
     var sfx = parseFloat(localStorage.getItem('clbi-audio-sfx') || 60) / 100;
     $('#clbi-title-guide-label').text(t.guide);
     var sfxOn = localStorage.getItem('clbi-audio-sfxOn') !== 'false';
     $('#clbi-guide-link').text(t.getStarted);
     transitionSound.volume = sfxOn ? master * sfx : 0;
    $('#clbi-title-links-label').text(t.links);
})();


    setProfileActionLabel('#clbi-btn-contribution', t.contribution);
function playStaticSound() {
     setProfileActionLabel('#clbi-btn-watchlist', t.watchlist);
     var master = parseFloat(localStorage.getItem('clbi-audio-master') || 80) / 100;
     setProfileActionLabel('#clbi-btn-preferences', t.preferences);
     var sfx = parseFloat(localStorage.getItem('clbi-audio-sfx') || 60) / 100;
     setProfileActionLabel('#clbi-btn-logout', t.logout);
     var sfxOn = localStorage.getItem('clbi-audio-sfxOn') !== 'false';
    setProfileActionLabel('#clbi-btn-login', t.login);


     var pageName = normalizePageName(mw.config.get('wgPageName'));
     if (!sfxOn) return;
    var specialPage = String(mw.config.get('wgCanonicalSpecialPageName') || '');


$('#clbi-left-news-changelog-main').text(changelogTitle);
    transitionSound.volume = master * sfx;
$('#clbi-left-news-recent-title').text('RECENT CHANGES');
    transitionSound.currentTime = 0;
    transitionSound.play();
}


     $('.clbi-user-btn').removeClass('clbi-user-btn-active');
// 현재 언어 감지
function getCurrentLang() {
     var langData = document.getElementById('clbi-lang-data');
    return langData ? (langData.getAttribute('data-lang') || 'ko') : 'ko';
}


     if (
function normalizePageName(value) {
        specialPage === 'Contributions' ||
     return String(value || '')
        specialPage === '기여' ||
         .split('?')[0]
         pageName.indexOf('특수:기여') === 0 ||
         .replace(/^\/index\.php\//, '')
         pageName.indexOf('Special:Contributions') === 0
         .replace(/_/g, ' ')
    ) {
        .trim();
         $('#clbi-btn-contribution').addClass('clbi-user-btn-active');
}
    }


    if (specialPage === 'Watchlist') {
function buildWikiPath(title) {
        $('#clbi-btn-watchlist').addClass('clbi-user-btn-active');
    return '/index.php/' + encodeURI(String(title || '').replace(/ /g, '_'));
    }
}


    if (
function getLangShortCode(lang) {
        specialPage === '설정' ||
    var map = { ko: 'KR', en: 'EN', zh: 'ZH', ja: 'JA', ru: 'RU', es: 'ES' };
        pageName === '특수:설정' ||
     return map[lang] || String(lang || '').toUpperCase();
        pageName === 'Special:설정'
}
     ) {
        $('#clbi-btn-preferences').addClass('clbi-user-btn-active');
    }


    $('.toggleBtn').each(function() {
function getLanguageTargetTitle(lang) {
        var btn = $(this);
    var data = document.getElementById('clbi-lang-data');
    if (!data || !lang) return '';


         if (!$('#' + btn.data('target')).hasClass('folding-open')) {
    var keys = [
            btn.text(t.expand);
         'data-' + lang,
         } else {
        'data-page-' + lang,
            btn.text(t.collapse);
        'data-title-' + lang,
         }
         'data-target-' + lang,
     });
         'data-lang-' + lang
     ];


     updateLeftSidebarNationsImage();
     for (var i = 0; i < keys.length; i++) {
}
        var value = data.getAttribute(keys[i]);
 
         if (value) return value;
function canShowContentTools() {
    // 비로그인 사용자는 편집/역사/공유 버튼을 숨김
    if (!mw.config.get('wgUserName')) {
         return false;
     }
     }


     // MediaWiki가 현재 문서를 편집 가능하지 않다고 판단하면 숨김
     return '';
    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 getCatlinkElements(root) {
function escapeClbiHtml(value) {
     var scope = root || document;
     return String(value == null ? '' : value)
    var found = [];
        .replace(/&/g, '&amp;')
    var seen = [];
        .replace(/</g, '&lt;')
 
         .replace(/>/g, '&gt;')
    $(scope).find('#catlinks, .catlinks').addBack('#catlinks, .catlinks').each(function () {
         .replace(/"/g, '&quot;')
         if (seen.indexOf(this) !== -1) return;
         .replace(/'/g, '&#039;');
         seen.push(this);
         found.push(this);
    });
 
    return $(found);
}
}


function normalizeCatlinksPanel(root) {
    getCatlinkElements(root).each(function () {
        var cat = this;
        var hiddenSections;


        cat.classList.add('clbi-catlinks-panel');
var SIDEBAR_LANG_SVG_NS = 'http://www.w3.org/2000/svg';
        cat.removeAttribute('aria-hidden');
var SIDEBAR_LANGUAGE_STATUS_TITLE = 'MediaWiki:LanguageStatus.json';
 
var SIDEBAR_LANGUAGE_LABELS = {
        hiddenSections = cat.querySelectorAll('#mw-hidden-catlinks, .mw-hidden-catlinks, .mw-hidden-cats-hidden, .mw-hidden-cats-user-shown');
    ko: '한국어',
 
    en: 'English',
        Array.prototype.forEach.call(hiddenSections, function (section) {
    zh: '中文',
            section.classList.add('clbi-catlinks-section-visible');
    ja: '日本語',
            section.removeAttribute('hidden');
    ru: 'Русский',
            section.removeAttribute('aria-hidden');
    es: 'Español'
            section.style.removeProperty('visibility');
};
            section.style.removeProperty('opacity');
var SIDEBAR_LANGUAGE_DIAL_LABELS = {
            section.style.setProperty('display', 'block', 'important');
    ko: '한국어',
        });
    en: 'ENG',
 
     zh: '中文',
        if (cat.classList.contains('catlinks-allhidden') && hiddenSections.length) {
     ja: '日本語',
            cat.classList.add('clbi-catlinks-show-hidden');
    ru: 'РУС',
        }
    es: 'ESP'
     });
};
}
var SIDEBAR_LANGUAGE_STATUS_VALUES = {
 
    available: true,
function setCatlinksPanelHidden(hidden) {
    wip: true,
     getCatlinkElements(document).each(function () {
    unavailable: true
        if (hidden) {
};
            this.style.setProperty('display', 'none', 'important');
        } else {
            this.style.removeProperty('display');
            normalizeCatlinksPanel(this);
        }
    });
}


function moveCatlinksToBottom() {
var sidebarLanguageStatusRegistry = {};
    var main = $('.liberty-content-main');
var sidebarLanguageStatusLoaded = false;
    var parserOutput = $('.liberty-content-main .mw-parser-output').first();
var sidebarLanguageStatusLoading = false;
    var catlinks = getCatlinkElements(document);
var sidebarLanguageStatusCallbacks = [];


     if (!main.length || !catlinks.length) return;
var sidebarLanguageState = {
 
    order: ['ko', 'en', 'zh', 'ja', 'ru', 'es'],
     catlinks.each(function () {
    currentLang: 'ko',
        var cat = $(this);
    baseIndex: 0,
 
    selectedIndex: 0,
        if (parserOutput.length) {
    rotation: 0,
            cat.appendTo(parserOutput);
    dragging: false,
        } else {
    dragMoved: false,
            cat.appendTo(main);
    dragStartX: 0,
        }
    dragStartY: 0,
    });
    dragStartRotation: 0,
 
    dragAxis: null,
     normalizeCatlinksPanel(parserOutput.length ? parserOutput[0] : main[0]);
    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) {
function initCategoryNavIfAvailable(root) {
     var length = sidebarLanguageState.order.length;
     /*
    var normalized = index % length;
     CategoryNav.js는 대문 카테고리 네비를 SVG로 생성한다.
     return normalized < 0 ? normalized + length : normalized;
}


    Common.js가 SPA로 본문을 갈아끼운 뒤에는 MediaWiki 원래 페이지 로드와 달리
function getSidebarLanguageName(lang) {
     CategoryNav.js의 초기 DOMContentLoaded만으로는 새 mount를 다시 잡지 못할 수 있다.
     return SIDEBAR_LANGUAGE_LABELS[lang] || String(lang || '').toUpperCase();
    CategoryNav.js 자체도 mw.hook('wikipage.content')를 듣지만, 로드 순서와 SPA 타이밍이
}
    엇갈릴 수 있으므로 Common.js 쪽에서도 존재 여부를 확인한 뒤 한 번 더 호출한다.


    이 함수는 CategoryNav.js가 아직 로드되지 않았으면 아무 것도 하지 않는다.
function getSidebarLanguageDialName(lang) {
    */
    return SIDEBAR_LANGUAGE_DIAL_LABELS[lang] || getSidebarLanguageName(lang);
    if (
        window.CLBI &&
        window.CLBI.categoryNav &&
        typeof window.CLBI.categoryNav.init === 'function'
    ) {
        window.CLBI.categoryNav.init(root || document);
    }
}
}


function removeLegacyMainPageHero() {
function normalizeSidebarLanguageStatusValue(value) {
     /*
     value = String(value == null ? '' : value).toLowerCase().trim();
    기존 대문 전용 레거시 요소 정리
     return SIDEBAR_LANGUAGE_STATUS_VALUES[value] ? value : '';
    -----------------------------------------
}
    이전 대문 구조에서는 Common.js가 본문 바깥에 #clbi-main-logo를 직접 삽입하고,
     본문 안의 #clbi-main-crt-hero를 #clbi-main-crt-hero-wrap으로 감싸서
    .liberty-content-main 위쪽으로 재배치했다.


     새 대문은 본문 내부의 .main-portal이 로고, 알림, 카테고리 네비, 이미지 피드,
function getSidebarLanguageStatusPageKey() {
    방명록, 상태 패널을 모두 담당한다. 따라서 Common.js가 별도 로고나 CRT 래퍼를
     var raw = String(mw.config.get('wgPageName') || '').trim();
     삽입하면 새 로고/콘텐츠와 중복된다.
     var normalized = normalizePageName(raw);


     여기서는 JS가 만들던 바깥 로고와 CRT 래퍼를 제거하고, 예전 대문 원본이나
     return normalized || raw || '대문';
    캐시된 렌더 결과에 남아 있을 수 있는 #clbi-main-crt-hero도 제거한다.
    */
    $('#clbi-main-logo').remove();
    $('#clbi-main-crt-hero-wrap').remove();
    $('#clbi-main-crt-hero').remove();
}
}


function applyMainPageStyle() {
function getSidebarLanguageStatusEntry() {
     var specialPage = mw.config.get('wgCanonicalSpecialPageName');
     var registry = sidebarLanguageStatusRegistry || {};
     if (specialPage === 'Preferences') return;
     var pages = registry.pages && typeof registry.pages === 'object' ? registry.pages : registry;
 
     var raw = String(mw.config.get('wgPageName') || '').trim();
     var pageName = normalizePageName(mw.config.get('wgPageName'));
     var normalized = normalizePageName(raw);
     var namespaceNumber = mw.config.get('wgNamespaceNumber');
    var title = String(mw.config.get('wgTitle') || '').trim();
     var isMainPage = (pageName === '대문');
     var keys = [
    var isUserProfilePage = (namespaceNumber === 2);
        normalized,
    var isScreenDoc = ($('.screen-header').length > 0);
        raw,
     var hideTools = (isMainPage || isUserProfilePage || !canShowContentTools());
        raw.replace(/_/g, ' '),
        normalized.replace(/ /g, '_'),
        title,
        title.replace(/_/g, ' ')
    ];
     var i;


     $('body').toggleClass('user-profile-page', isUserProfilePage);
     for (i = 0; i < keys.length; i += 1) {
        if (keys[i] && pages[keys[i]] && typeof pages[keys[i]] === 'object') {
            return pages[keys[i]];
        }
    }


     // 모든 문서에서 분류 바를 본문 컨테이너 아래로 이동
     return {};
    moveCatlinksToBottom();
}


    if (isMainPage) {
function getSidebarLanguageStatusOverride(lang) {
        $('.liberty-content-header').css('display', 'none');
    var entry = getSidebarLanguageStatusEntry();
        $('.mw-page-title-main').addClass('clbi-hide');
    return normalizeSidebarLanguageStatusValue(entry[lang]);
        setCatlinksPanelHidden(true);
}
        $('.liberty-content-main').css('border-radius', '0');


        // 새 대문은 .main-portal 본문 구조가 로고/히어로를 담당한다.
function flushSidebarLanguageStatusCallbacks() {
        // Common.js의 구식 바깥 로고/CRT 재배치 루틴은 사용하지 않는다.
    var callbacks = sidebarLanguageStatusCallbacks.slice();
        removeLegacyMainPageHero();
    sidebarLanguageStatusCallbacks.length = 0;
        $('#clbi-tools-box').remove();


         $('.content-tools').css('display', 'none');
    callbacks.forEach(function(callback) {
         $('.liberty-content').addClass('content-tools-hidden');
         if (typeof callback === 'function') {
            callback(sidebarLanguageStatusRegistry);
         }
    });
}


         initCategoryNavIfAvailable(document);
function loadSidebarLanguageStatusRegistry(callback, force) {
    if (typeof callback === 'function') {
         sidebarLanguageStatusCallbacks.push(callback);
    }


     } else if (isUserProfilePage) {
     if (sidebarLanguageStatusLoaded && !force) {
         $('.liberty-content-header').css('display', 'none');
         flushSidebarLanguageStatusCallbacks();
         $('.mw-page-title-main, .mw-page-title-namespace, .mw-page-title-separator').addClass('clbi-hide');
         return;
        setCatlinksPanelHidden(true);
    }
        $('.liberty-content-main').css('border-radius', '0');


        $('#clbi-main-logo').remove();
    if (sidebarLanguageStatusLoading) return;
        $('#clbi-main-crt-hero-wrap').remove();
        $('#clbi-main-crt-hero').remove();
        $('#clbi-tools-box').remove();


        $('.content-tools').css('display', 'none');
    sidebarLanguageStatusLoading = true;
        $('.liberty-content').addClass('content-tools-hidden');


     } else if (isScreenDoc) {
     $.ajax({
         $('.liberty-content-header').css('display', 'none');
         url: mw.util.getUrl(SIDEBAR_LANGUAGE_STATUS_TITLE, {
        $('.mw-page-title-main').addClass('clbi-hide');
            action: 'raw',
         setCatlinksPanelHidden(false);
            ctype: 'application/json',
         $('.liberty-content-main').css('border-radius', '0');
            _: String(Date.now())
         }),
         dataType: 'text',
        cache: false
    }).done(function(text) {
        var parsed = {};


         $('#clbi-main-logo').remove();
         try {
        $('#clbi-main-crt-hero-wrap').remove();
             parsed = text ? JSON.parse(text) : {};
 
        } catch (err) {
        if ($('#clbi-tools-box').length === 0 && canShowContentTools()) {
             console.error('LanguageStatus.json parse failed:', err);
             var $toolsBox = $('<div id="clbi-tools-box" class="clbi-left-box"></div>');
             parsed = {};
            var $toolsTitle = $('<div class="clbi-left-title"><span class="clbi-icon" style="--icon:var(--ic-ui-003)"></span> 관리</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');
         sidebarLanguageStatusRegistry = parsed && typeof parsed === 'object' ? parsed : {};
         $('.liberty-content').addClass('content-tools-hidden');
    }).fail(function() {
        sidebarLanguageStatusRegistry = {};
    }).always(function() {
        sidebarLanguageStatusLoaded = true;
        sidebarLanguageStatusLoading = false;
         flushSidebarLanguageStatusCallbacks();
    });
}


     } else {
window.CLBI_LANGUAGE_STATUS = {
        $('.liberty-content-header').css('display', '');
     title: SIDEBAR_LANGUAGE_STATUS_TITLE,
        $('.mw-page-title-main, .mw-page-title-namespace, .mw-page-title-separator').removeClass('clbi-hide');
    languages: sidebarLanguageState.order.slice(),
         setCatlinksPanelHidden(false);
    labels: SIDEBAR_LANGUAGE_LABELS,
         $('.liberty-content-main').css('border-radius', '0');
    dialLabels: SIDEBAR_LANGUAGE_DIAL_LABELS,
 
    getPageKey: getSidebarLanguageStatusPageKey,
        $('#clbi-main-logo').remove();
    getRegistry: function() {
         $('#clbi-main-crt-hero-wrap').remove();
        return sidebarLanguageStatusRegistry || {};
         $('#clbi-tools-box').remove();
    },
    reload: function(callback) {
         sidebarLanguageStatusLoaded = false;
         loadSidebarLanguageStatusRegistry(function() {
            renderSidebarLanguageBox();
            if (typeof callback === 'function') callback(sidebarLanguageStatusRegistry);
         }, true);
    },
    refreshDial: function() {
         renderSidebarLanguageBox();
     }
     }
};


    if (!isUserProfilePage) {
function getSidebarLanguageMeta(lang) {
        $('.profile-card').remove();
    var currentLang = getCurrentLang();
        $('.user-profile-portal').removeClass('user-profile-portal');
    var targetTitle = getLanguageTargetTitle(lang);
     }
     var isCurrent = lang === currentLang;


     if (hideTools) {
     return {
         $('.content-tools').css('display', 'none');
         lang: lang,
         $('.liberty-content').addClass('content-tools-hidden');
         code: getLangShortCode(lang),
    } else if (!isScreenDoc && !isUserProfilePage) {
        name: getSidebarLanguageName(lang),
         $('.content-tools').css('display', '');
         dialName: getSidebarLanguageDialName(lang),
         $('.liberty-content').removeClass('content-tools-hidden');
         targetTitle: targetTitle,
    }
        isCurrent: isCurrent,
 
        canMove: !!targetTitle && !isCurrent
     updateSidebar();
     };
}
}


// 본문 기본 목차 제거
function getSidebarLanguageStatus(meta) {
function removeNativeTocFromContent() {
     var override;
     $('.liberty-content-main #toc, .liberty-content-main .toc').remove();
}


// 왼쪽 목차: MediaWiki 문단 ID 가져오기
     if (!meta) {
function getHeadingId(heading) {
         return {
     if (heading.id) {
            className: 'is-locked',
         return heading.id;
            label: 'UNAVAILABLE',
            canApply: false
        };
     }
     }


    var headline = heading.querySelector('.mw-headline[id]');
     if (meta.isCurrent) {
     if (headline && headline.id) {
         return {
         return headline.id;
            className: 'is-current',
            label: 'CURRENT',
            canApply: false
        };
     }
     }


     return '';
     override = getSidebarLanguageStatusOverride(meta.lang);
}


// 왼쪽 목차: MediaWiki 문단 제목 텍스트 가져오기
    if (override === 'wip') {
function getHeadingText(heading) {
        return {
    var headline = heading.querySelector('.mw-headline');
            className: 'is-locked',
     var source = headline || heading;
            label: 'WIP',
     var clone = source.cloneNode(true);
            canApply: false
        };
     }
 
     if (override === 'unavailable') {
        return {
            className: 'is-locked',
            label: 'UNAVAILABLE',
            canApply: false
        };
    }


     $(clone).find('.mw-editsection, .mw-editsection-bracket, .mw-editsection-divider').remove();
     if (override === 'available' || meta.targetTitle) {
        return {
            className: meta.targetTitle ? 'is-ready' : 'is-locked',
            label: meta.targetTitle ? 'AVAILABLE' : 'UNAVAILABLE',
            canApply: !!meta.targetTitle
        };
    }


     return (clone.textContent || '')
     return {
         .replace(/\s+/g, ' ')
        className: 'is-locked',
         .trim();
         label: 'UNAVAILABLE',
         canApply: false
    };
}
}


// 왼쪽 목차: 긴 제목에 자동 스크롤 적용
function sidebarLanguageRad(deg) {
function initTocTitleScroll(root) {
     return (deg * Math.PI) / 180;
     var $items = root
}
        ? $(root).find('.toc-scroll-text')
        : $('#side-toc-box .toc-scroll-text');


    $items.each(function () {
function sidebarLanguagePointAt(radius, deg) {
        var $text = $(this);
    var state = sidebarLanguageState;
        var $wrap = $text.closest('.toc-scroll-wrap');
    var angle = sidebarLanguageRad(deg);


         if (!$wrap.length) return;
    return {
         x: state.cx + Math.sin(angle) * radius,
        y: state.cy - Math.cos(angle) * radius
    };
}


        var wrapW = Math.floor($wrap.width());
function getSidebarLanguageSectorPath(start, end) {
        var textW = Math.ceil(this.scrollWidth);
    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 [
         if (!wrapW || !textW) 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(' ');
}


        if (textW <= wrapW + 12) {
function getSidebarLanguageShellPath() {
            // 왼쪽 목차: 칸을 넘지 않는 제목은 전체 텍스트를 그대로 보여준다.
    return getSidebarLanguageSectorPath(-68, 68);
            $wrap.removeClass('is-scrolling');
}


            if ($text.data('toc-scroll-enabled')) {
function getSidebarLanguageByStep(step) {
                $text.css({
    var state = sidebarLanguageState;
                    animation: '',
    var index = normalizeSidebarLanguageIndex(state.baseIndex + step);
                    'animation-delay': '',
                    '--scroll-dist': ''
                });
                $text.removeData('toc-scroll-enabled');
                $text.removeData('toc-scroll-key');
            }


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


        var scrollDist = '-' + (textW - wrapW + 10) + 'px';
function getSidebarLanguagePreviewIndex() {
        var duration = Math.max(7, textW / 38) * 1.25;
    var state = sidebarLanguageState;
        var scrollKey = scrollDist + '|' + duration;
    var step = Math.round(-state.rotation / state.sectorAngle);
    return normalizeSidebarLanguageIndex(state.baseIndex + step);
}


        // 왼쪽 목차: 긴 제목에는 오른쪽 페이드와 스크롤을 적용한다.
function getSidebarLanguagePreviewMeta() {
        $wrap.addClass('is-scrolling');
    var state = sidebarLanguageState;
    return getSidebarLanguageMeta(state.order[getSidebarLanguagePreviewIndex()]);
}


        // 왼쪽 목차: 같은 값으로 이미 적용된 애니메이션은 다시 초기화하지 않는다.
function makeSidebarLanguageSector(step) {
        if ($text.data('toc-scroll-key') === scrollKey) {
    var state = sidebarLanguageState;
            return;
    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;


        $text.data('toc-scroll-enabled', true);
    group.setAttribute('class', 'sidebar-lang-sector-group');
        $text.data('toc-scroll-key', scrollKey);
    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 + ')');


        $text.css({
    path.setAttribute('class', 'sidebar-lang-sector');
            // 왼쪽 목차: 페이지 진입 직후에는 잠시 읽을 시간을 준 뒤 흐르게 한다.
    path.setAttribute('d', getSidebarLanguageSectorPath(-state.halfSector, state.halfSector));
            animation: 'toc-scroll-blink-reset ' + duration + 's linear infinite',
            'animation-delay': '1s',
            '--scroll-dist': scrollDist
        });
    });
}


// 목차를 왼쪽 사이드바에 새로 생성
    label.setAttribute('class', 'sidebar-lang-sector-label');
function moveTocToLeftSidebar() {
     label.setAttribute('x', String(state.cx));
     // 왼쪽 목차: MediaWiki가 만든 원래 목차는 본문에서 제거한다.
     label.setAttribute('y', String(labelY + 5));
     removeNativeTocFromContent();
    label.textContent = item.meta.dialName || item.meta.name;


     var leftSidebar = document.getElementById('clbi-left-sidebar');
     group.appendChild(path);
     if (!leftSidebar) return;
     group.appendChild(label);


     var content =
     group.addEventListener('click', function(e) {
        document.querySelector('.liberty-content-main .mw-parser-output') ||
        if (sidebarLanguageState.dragging || performance.now() < sidebarLanguageState.suppressClickUntil) return;
        document.querySelector('.liberty-content-main');


    if (!content) return;
        e.preventDefault();
        e.stopPropagation();


    var headings = Array.prototype.slice.call(
         cancelSidebarLanguageSpin();
         content.querySelectorAll('h2, h3')
        snapSidebarLanguageToStep(parseInt(group.getAttribute('data-step') || '0', 10), true);
    ).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 group;
        return getHeadingId(heading) + '|' + getHeadingText(heading);
}
    }).join('||');


     var existingBox = document.getElementById('side-toc-box');
function renderSidebarLanguageWheel() {
 
    var state = sidebarLanguageState;
     // 왼쪽 목차: 같은 문서에서 같은 목차를 이미 만들었다면 다시 지우고 만들지 않는다.
     var fan = document.getElementById('clbi-sidebar-lang-fan');
     if (existingBox && existingBox.getAttribute('data-toc-key') === tocKey) {
    var svg;
        initTocTitleScroll(existingBox);
    var defs;
        return;
    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 (existingBox) {
     if (!fan) return;
        existingBox.remove();
    }


     if (!headings.length) return;
     fan.innerHTML = '';


     var tocBox = document.createElement('div');
     svg = createSidebarLanguageSvgEl('svg');
     tocBox.className = 'clbi-left-box';
     svg.setAttribute('class', 'sidebar-lang-fan-svg');
     tocBox.id = 'side-toc-box';
     svg.setAttribute('viewBox', '0 0 202 150');
     tocBox.setAttribute('data-toc-key', tocKey);
    svg.setAttribute('preserveAspectRatio', 'xMidYMid meet');
    svg.setAttribute('role', 'img');
     svg.setAttribute('aria-label', '언어 선택 다이얼');


     var title = document.createElement('div');
     defs = createSidebarLanguageSvgEl('defs');
    title.className = 'clbi-left-title';


     // 왼쪽 목차: 박스 제목은 Lang.js의 현재 UI 언어를 따른다.
     clip = createSidebarLanguageSvgEl('clipPath');
    var currentLang = getCurrentLang();
     clip.setAttribute('id', 'clbi-sidebar-language-fan-clip');
     var t = (window.LANG && window.LANG[currentLang]) ? window.LANG[currentLang] : window.LANG.ko;
     clipPath = createSidebarLanguageSvgEl('path');
     var tocTitleText = (t && t.toc) ? t.toc : '목차';
    clipPath.setAttribute('d', getSidebarLanguageShellPath());
    clip.appendChild(clipPath);


     title.innerHTML =
     shadowBlur = createSidebarLanguageSvgEl('filter');
        '<span class="sidebar-title-svg" aria-hidden="true">' + CLBI_SVG_LIST + '</span> ' + tocTitleText;
    shadowBlur.setAttribute('id', 'clbi-sidebar-language-shadow-blur');
 
    shadowBlur.setAttribute('x', '-20%');
     var body = document.createElement('div');
    shadowBlur.setAttribute('y', '-20%');
     body.className = 'clbi-left-content toc-sidebar-content';
     shadowBlur.setAttribute('width', '140%');
 
     shadowBlur.setAttribute('height', '140%');
     var list = document.createElement('ul');
     blur = createSidebarLanguageSvgEl('feGaussianBlur');
     list.className = 'generated-toc';
     blur.setAttribute('stdDeviation', '3');
    shadowBlur.appendChild(blur);


     headings.forEach(function (heading) {
     fixedDepthGradient = createSidebarLanguageSvgEl('linearGradient');
        var id = getHeadingId(heading);
    fixedDepthGradient.setAttribute('id', 'clbi-sidebar-language-fixed-depth');
        var text = getHeadingText(heading);
    fixedDepthGradient.setAttribute('x1', '0');
        var level = heading.tagName.toLowerCase() === 'h3' ? 3 : 2;
    fixedDepthGradient.setAttribute('y1', '0');
    fixedDepthGradient.setAttribute('x2', '0');
    fixedDepthGradient.setAttribute('y2', '1');


         var item = document.createElement('li');
    [
         item.className = 'toc-level-' + level;
        ['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);
    });


        var link = document.createElement('a');
    defs.appendChild(clip);
        link.setAttribute('href', '#' + id);
    defs.appendChild(shadowBlur);
    defs.appendChild(fixedDepthGradient);
    svg.appendChild(defs);


        // 왼쪽 목차: 긴 제목 스크롤을 위해 텍스트를 별도 span으로 감싼다.
    shell = createSidebarLanguageSvgEl('path');
        var textWrap = document.createElement('span');
    shell.setAttribute('class', 'sidebar-lang-shell');
        textWrap.className = 'toc-scroll-wrap';
    shell.setAttribute('d', getSidebarLanguageShellPath());
    svg.appendChild(shell);


        var textSpan = document.createElement('span');
    clipped = createSidebarLanguageSvgEl('g');
        textSpan.className = 'toc-scroll-text';
    clipped.setAttribute('clip-path', 'url(#clbi-sidebar-language-fan-clip)');
        textSpan.textContent = text;


        textWrap.appendChild(textSpan);
    rotor = createSidebarLanguageSvgEl('g');
        link.appendChild(textWrap);
    rotor.setAttribute('id', 'clbi-sidebar-lang-wheel-rotor');
    rotor.setAttribute('class', 'sidebar-lang-wheel-rotor');


        item.appendChild(link);
    for (step = -state.repeats; step <= state.repeats; step += 1) {
         list.appendChild(item);
         rotor.appendChild(makeSidebarLanguageSector(step));
     });
     }


     body.appendChild(list);
     clipped.appendChild(rotor);
     tocBox.appendChild(title);
     svg.appendChild(clipped);
    tocBox.appendChild(body);
    leftSidebar.appendChild(tocBox);


     // 왼쪽 목차: DOM 배치가 끝난 뒤 긴 제목 스크롤 여부를 계산한다.
     fixedDepthPath = createSidebarLanguageSvgEl('path');
     requestAnimationFrame(function () {
    fixedDepthPath.setAttribute('class', 'sidebar-lang-fixed-depth');
        initTocTitleScroll(tocBox);
     fixedDepthPath.setAttribute('d', getSidebarLanguageShellPath());
    svg.appendChild(fixedDepthPath);


        setTimeout(function () {
    fixedFocus = createSidebarLanguageSvgEl('path');
            initTocTitleScroll(tocBox);
    fixedFocus.setAttribute('class', 'sidebar-lang-fixed-focus');
        }, 120);
    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');
var RIGHT_BILLBOARD_ITEMS = [
     shadowHard.setAttribute('class', 'sidebar-lang-inner-shadow-hard');
     {
    shadowHard.setAttribute('d', getSidebarLanguageShellPath());
        file: 'Side-visual-001.png',
     svg.appendChild(shadowHard);
        alt: 'PROOF TO THE WORLD / YOU ONCE PART OF IT',
 
        duration: 3000,
    rim = createSidebarLanguageSvgEl('path');
        caption: [
     rim.setAttribute('class', 'sidebar-lang-rim');
            '"당신이 한때 이 세계의',
     rim.setAttribute('d', getSidebarLanguageShellPath());
            '',
     svg.appendChild(rim);
            '일부였다는 것을 증명하십시오"'
        ]
    },
     {
        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) {
    pointer = createSidebarLanguageSvgEl('g');
     var items = RIGHT_BILLBOARD_ITEMS;
    pointer.setAttribute('class', 'sidebar-lang-fixed-pointer');
     pointer.setAttribute('clip-path', 'url(#clbi-sidebar-language-fan-clip)');


     if (!items || !items.length) {
     tri = createSidebarLanguageSvgEl('path');
        return {
    tri.setAttribute('class', 'sidebar-lang-pointer-triangle');
            file: 'Side-visual-001.png',
    tri.setAttribute('d', 'M ' + (state.cx - 10) + ' 10 L ' + (state.cx + 10) + ' 10 L ' + state.cx + ' 26 Z');
            alt: '',
     pointer.appendChild(tri);
            caption: []
        };
     }


     var normalized = index % items.length;
     line = createSidebarLanguageSvgEl('line');
     if (normalized < 0) normalized += items.length;
    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);


     return items[normalized];
     svg.appendChild(pointer);
}
    fan.appendChild(svg);


function getRightBillboardImageUrl(fileName) {
     state.rotor = rotor;
     return '/index.php?title=특수:Redirect/file/' + encodeURIComponent(fileName || 'Side-visual-001.png');
    setSidebarLanguageRotation(state.rotation, false);
}
}


function getRightBillboardCaptionHtml(item) {
function updateSidebarLanguageDial() {
     var lines = item && item.caption ? item.caption : [];
     var state = sidebarLanguageState;
     var html = '';
    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');


     function escapeCaptionText(value) {
     if (selectedValue) {
         return String(value == null ? '' : value)
         selectedValue.textContent = meta.name;
            .replace(/&/g, '&amp;')
            .replace(/</g, '&lt;')
            .replace(/>/g, '&gt;')
            .replace(/\"/g, '&quot;')
            .replace(/'/g, '&#039;');
     }
     }


     lines.forEach(function(line) {
     if (availabilityPanel) {
         var text = String(line == null ? '' : line);
         availabilityPanel.classList.remove('is-ready', 'is-current', 'is-locked');
        var isGap = !text.trim();
         availabilityPanel.classList.add(status.className);
        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) {
     if (availabilityValue) {
         emptySub.textContent = item.file || 'Side-visual-001.png';
         availabilityValue.textContent = status.label;
     }
     }
}


function getRightBillboardItemDuration(item) {
    if (apply) {
    var duration = item && item.duration ? parseInt(item.duration, 10) : 3000;
        apply.classList.toggle('is-disabled', !status.canApply);
 
        apply.setAttribute('aria-disabled', status.canApply ? 'false' : 'true');
    if (Number.isNaN(duration) || duration < 500) {
        apply.setAttribute('aria-label', status.canApply ? (meta.name + ' 적용') : (meta.isCurrent ? '현재 언어' : '사용할 수 없는 언어'));
        duration = 3000;
     }
     }


     return duration;
    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 initRightBillboardCarousel() {
function setSidebarLanguageRotation(value, animate) {
     var box = document.querySelector('.right-billboard-box');
     var state = sidebarLanguageState;
     if (!box || box.getAttribute('data-billboard-ready') === '1') return;
    state.rotation = value;
     updateSidebarLanguageDial();


     box.setAttribute('data-billboard-ready', '1');
     if (!state.rotor) return;
    box.setAttribute('data-billboard-index', '0');


     setRightBillboardItem(0);
     if (animate) {
        $('#clbi-sidebar-lang-selector').addClass('is-snapping');
    } else {
        $('#clbi-sidebar-lang-selector').removeClass('is-snapping');
    }


     if (!RIGHT_BILLBOARD_ITEMS || RIGHT_BILLBOARD_ITEMS.length <= 1) return;
     state.rotor.style.transform = 'rotate(' + state.rotation.toFixed(3) + 'deg)';
}


    function scheduleNext() {
function requestSidebarLanguageRotation(value) {
        var current = parseInt(box.getAttribute('data-billboard-index') || '0', 10);
    var state = sidebarLanguageState;
        if (Number.isNaN(current)) current = 0;
    state.pendingRotation = value;


        var currentItem = getRightBillboardItem(current);
    if (state.raf) return;
        var delay = getRightBillboardItemDuration(currentItem);


        window.setTimeout(function() {
    state.raf = requestAnimationFrame(function() {
            if (!document.body.contains(box)) return;
        state.raf = null;
        setSidebarLanguageRotation(state.pendingRotation, false);
    });
}


            if (!document.hidden) {
function cancelSidebarLanguageSpin() {
                setRightBillboardItem(current + 1);
    var state = sidebarLanguageState;
            }


            scheduleNext();
    if (state.inertiaRaf) {
         }, delay);
        cancelAnimationFrame(state.inertiaRaf);
         state.inertiaRaf = null;
     }
     }


     scheduleNext();
     $('#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;


function escapeRightBillboardAttr(value) {
     $('#clbi-sidebar-lang-selector').removeClass('is-snapping is-dragging is-spinning');
     return String(value == null ? '' : value)
        .replace(/&/g, '&amp;')
        .replace(/</g, '&lt;')
        .replace(/>/g, '&gt;')
        .replace(/"/g, '&quot;')
        .replace(/'/g, '&#039;');
}


function buildRightBillboardBox() {
     renderSidebarLanguageWheel();
     var billboardInitial = getRightBillboardItem(0);
     updateSidebarLanguageDial();
     var billboardSrc = getRightBillboardImageUrl(billboardInitial.file);


     return '' +
     if (typeof callback === 'function') {
        '<div class="clbi-right-box right-billboard-box" data-billboard-index="0">' +
        callback(getSidebarLanguageMeta(state.order[state.selectedIndex]));
            '<div class="clbi-right-title right-billboard-title">' +
    }
                '<span class="right-billboard-title-text"><span class="right-billboard-title-main">Ad</span><span class="right-billboard-title-note">(not really)</span></span>' +
            '</div>' +
            '<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>' +
        '</div>';
}
}


function buildSiteInformationBox() {
function snapSidebarLanguageToStep(step, animate, callback) {
     return '' +
     var state = sidebarLanguageState;
        '<div class="clbi-right-box site-info-sidebar">' +
    var targetRotation = -step * state.sectorAngle;
            '<div class="clbi-right-title site-info-title">' +
    var nearestIndex = normalizeSidebarLanguageIndex(state.baseIndex + step);
                '<span>SITE INFORMATION</span>' +
 
                '<span class="site-info-title-meta">LINKS</span>' +
    cancelSidebarLanguageSpin();
            '</div>' +
    clearTimeout(state.snapTimer);
            '<div class="clbi-right-content site-info-content">' +
 
                '<div class="policy-list">' +
    state.selectedIndex = nearestIndex;
                    '<div class="policy-row"><a href="/index.php/개인정보처리방침">개인정보처리방침</a><span class="policy-mark" aria-hidden="true">›</span></div>' +
    setSidebarLanguageRotation(targetRotation, !!animate);
                    '<div class="policy-row"><a href="/index.php/면책_조항">면책 조항</a><span class="policy-mark" aria-hidden="true">›</span></div>' +
 
                    '<div class="policy-row"><a href="/index.php/라이선스">라이선스</a><span class="policy-mark" aria-hidden="true">›</span></div>' +
    state.snapTimer = setTimeout(function() {
                    '<div class="policy-row"><a href="/index.php/크레딧">크레딧</a><span class="policy-mark" aria-hidden="true">›</span></div>' +
        finishSidebarLanguageSnap(nearestIndex, callback);
                '</div>' +
    }, animate ? 230 : 0);
                '<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 snapSidebarLanguageNearest(callback) {
    var state = sidebarLanguageState;
    var step = Math.round(-state.rotation / state.sectorAngle);
    snapSidebarLanguageToStep(step, true, callback);
}


// 초기화 함수
function startSidebarLanguageInertiaSpin(initialVelocity) {
function initSidebars() {
     var state = sidebarLanguageState;
     var header = $('.liberty-content-header');
    var velocity;
     var content = $('.liberty-content');
     var lastFrame;


     if (header.length && content.length) {
     cancelSidebarLanguageSpin();
        header.prependTo(content);
    }


     if ($('#clbi-right-sidebar').length === 0) {
    velocity = Math.max(-state.maxSpinVelocity, Math.min(state.maxSpinVelocity, initialVelocity));
         var username = mw.config.get('wgUserName');
 
         var isLoggedIn = username !== null;
     if (Math.abs(velocity) < state.minSpinVelocity) {
         var avatarSrc = isLoggedIn
        snapSidebarLanguageNearest();
            ? '/index.php?title=특수:Redirect/file/Pfp-' + username + '.png'
        return;
            : '/index.php?title=특수:Redirect/file/Pfp-default.png';
    }
 
    $('#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);


         var userBox;
         nextSpeed = Math.max(0, Math.abs(velocity) - (state.spinDecel * dt));
        velocity = sign * nextSpeed;


         if (isLoggedIn) {
         if (nextSpeed <= state.minSpinVelocity) {
             userBox =
             state.inertiaRaf = null;
                '<div class="clbi-right-box profile-card-box">' +
            $('#clbi-sidebar-lang-selector').removeClass('is-spinning');
                    '<div id="clbi-user-avatar-wrap" class="profile-identity-panel">' +
            snapSidebarLanguageNearest();
                        '<div class="profile-avatar-stage">' +
             return;
                            '<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 rightVisualBox = '';
        var siteInformationBox = '';


         try {
         state.inertiaRaf = requestAnimationFrame(frame);
            rightVisualBox = buildRightBillboardBox();
    }
        } catch (err) {
            console.error('Right billboard build failed:', err);
            rightVisualBox = '';
        }


        try {
    state.inertiaRaf = requestAnimationFrame(frame);
            siteInformationBox = buildSiteInformationBox();
}
        } catch (err) {
            console.error('Site information build failed:', err);
            siteInformationBox = '';
        }


function scheduleSidebarLanguageNavigation(meta) {
    var status = getSidebarLanguageStatus(meta);


        var sidebar = userBox + rightVisualBox + siteInformationBox;
    if (!meta || !status.canApply) return;


        $('.content-wrapper').append('<div id="clbi-right-sidebar">' + sidebar + '</div>');
    clearTimeout(sidebarLanguageState.navigateTimer);
    }
    sidebarLanguageState.navigateTimer = setTimeout(function() {
        var title = getLanguageTargetTitle(meta.lang);


        if (!title || meta.lang === getCurrentLang()) return;


    if ($('#clbi-left-sidebar').length === 0) {
        window.location.href = buildWikiPath(title);
var leftSidebar =
     }, 70);
     '<div id="clbi-left-sidebar">' +
}
        '<div class="clbi-left-box clbi-left-lang-box">' +
            '<div class="clbi-left-title">' +
                '<span class="sidebar-title-svg clbi-left-language-icon" aria-hidden="true">' + CLBI_SVG_LANGUAGES + '</span> ' +
                '<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 class="news-title-icon sidebar-title-svg" aria-hidden="true">' + CLBI_SVG_NEWSPAPER + '</span> ' +
                '<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 setSidebarLanguageSelection(lang) {
                '<div class="news-left-changelog-feed">' +
    var state = sidebarLanguageState;
                    '<a href="/index.php/체인지로그" class="news-post-item">' +
    var index = state.order.indexOf(lang);
                        '<div class="news-post-title-wrap">' +
                            '<span class="news-post-title" id="clbi-left-news-changelog-main">체인지로그</span>' +
                            '<i class="hn hn-angle-right-solid news-post-arrow" aria-hidden="true"></i>' +
                        '</div>' +
                        '<span class="news-post-tag">POST</span>' +
                    '</a>' +
                '</div>' +


                '<div class="news-divider"></div>' +
    if (index < 0) index = state.order.indexOf(getCurrentLang());
    if (index < 0) index = 0;


                '<div class="news-feed-title" id="clbi-left-news-recent-title">RECENT CHANGES</div>' +
    if (state.raf) {
                '<div class="news-left-recent-feed" id="clbi-left-recent-list">불러오는 중...</div>' +
        cancelAnimationFrame(state.raf);
        state.raf = null;
    }


            '</div>' +
    cancelSidebarLanguageSpin();
        '</div>' +
     clearTimeout(state.snapTimer);
     '</div>';


        $('.content-wrapper').prepend(leftSidebar);
    state.currentLang = lang;
    state.baseIndex = index;
    state.selectedIndex = index;
    state.rotation = 0;
    state.dragging = false;
    state.dragMoved = false;
    state.releaseVelocity = 0;


        renderSidebarLanguageBox();
    renderSidebarLanguageWheel();
        loadRecentChangesList('#clbi-left-recent-list', 10);
    updateSidebarLanguageDial();
        updateLeftSidebarNationsImage();
}
    }


    try {
function moveSidebarLanguageSelection(delta) {
        initRightBillboardCarousel();
     snapSidebarLanguageToStep(-delta, true);
     } catch (err) {
}
        console.error('Right billboard carousel failed:', err);
    }


     applyMainPageStyle();
function bindSidebarLanguageSelector() {
     initCategoryNavIfAvailable(document);
     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 (window.ProgressSystemWebUi && typeof window.ProgressSystemWebUi.boot === 'function') {
     if (!selector || !fan || !apply) return;
        window.ProgressSystemWebUi.boot('initSidebars');
    }


     $('#side-toc-box').remove();
     if (state.bound && state.boundElement === selector) return;


     mw.loader.using(['mediawiki.api']).then(function() {
     state.bound = true;
        setTimeout(function() {
    state.boundElement = selector;
            initNotifications();
            initProfile();
            moveTocToLeftSidebar();
        }, 300);


        setTimeout(moveTocToLeftSidebar, 800);
    fan.addEventListener('pointerdown', function(e) {
         setTimeout(moveTocToLeftSidebar, 1500);
         cancelSidebarLanguageSpin();
    });
        clearTimeout(state.snapTimer);
}


$(function() {
        state.dragging = true;
    loadLangScript(function() {
        state.dragMoved = false;
         setTimeout(function() {
        state.dragStartX = e.clientX;
            initSidebars();
        state.dragStartY = e.clientY || 0;
         }, 100);
        state.dragStartRotation = state.rotation;
    });
         state.dragAxis = null;
});
        state.pointerCaptured = false;
         state.lastX = e.clientX;
        state.lastTime = performance.now();
        state.releaseVelocity = 0;


$(document).on('click.profileQuickPlaceholder', '#profile-quick-inventory, #profile-quick-achievements', function(e) {
        selector.classList.add('is-dragging');
    e.preventDefault();
        selector.classList.remove('is-snapping');
    e.stopPropagation();
});


// SPA 네비게이션
        /*
function shouldSkip(url) {
        Vertical page scrolling must stay available when the pointer starts on
    return url.match(/action=edit|action=submit|action=history|action=delete|action=protect|action=purge|특수:로그인|특수:로그아웃|Special:UserLogin|Special:UserLogout|특수:사용자정보|특수:비밀번호바꾸기|uselang=/);
        the language dial. Capture and preventDefault are delayed until a
}
        horizontal drag is confirmed.
        */
    });


$(function() {
    fan.addEventListener('pointermove', function(e) {
    if (window._spaInitialized) return;
        var now;
    window._spaInitialized = true;
        var totalDx;
        var totalDy;
        var frameDx;
        var dt;
        var instantVelocity;


    function isInternal(url) {
        if (!state.dragging) return;
        var a = document.createElement('a');
        a.href = url;
        return a.hostname === window.location.hostname;
    }


    function loadPage(url) {
        totalDx = e.clientX - state.dragStartX;
         invalidateProfileRender();
         totalDy = (e.clientY || 0) - state.dragStartY;


         fetch(url)
         if (!state.dragAxis && (Math.abs(totalDx) > 4 || Math.abs(totalDy) > 4)) {
            .then(function(res) {
             state.dragAxis = Math.abs(totalDx) >= Math.abs(totalDy) ? 'x' : 'y';
                return res.text();
            })
             .then(function(html) {
                var parser = new DOMParser();
                var doc = parser.parseFromString(html, 'text/html');


                var scripts = doc.querySelectorAll('script');
            if (state.dragAxis === 'y') {
                 for (var i = 0; i < scripts.length; i++) {
                state.dragging = false;
                    var src = scripts[i].textContent;
                 state.dragMoved = false;
                state.dragAxis = null;
                state.pointerCaptured = false;
                selector.classList.remove('is-dragging');
                return;
            }


                    if (src.indexOf('wgNamespaceNumber') !== -1) {
            if (fan.setPointerCapture && e.pointerId != null) {
                        var match = src.match(/"wgNamespaceNumber":(-?\d+)/);
                try {
                        if (match) mw.config.set('wgNamespaceNumber', parseInt(match[1], 10));
                    fan.setPointerCapture(e.pointerId);
 
                    state.pointerCaptured = true;
                        var matchTitle = src.match(/"wgTitle":"([^"]+)"/);
                } catch (err) {
                        if (matchTitle) mw.config.set('wgTitle', matchTitle[1]);
                    state.pointerCaptured = false;
                }
            }
        }


                        var matchPage = src.match(/"wgPageName":"([^"]+)"/);
        if (state.dragAxis !== 'x') return;
                        if (matchPage) mw.config.set('wgPageName', matchPage[1]);


                        var matchArticle = src.match(/"wgArticleId":(\d+)/);
        now = performance.now();
                        if (matchArticle) {
        frameDx = e.clientX - state.lastX;
                            mw.config.set('wgArticleId', parseInt(matchArticle[1], 10));
        dt = Math.max(1, now - state.lastTime);
                        } else {
                            mw.config.set('wgArticleId', 0);
                        }


                        var matchIsMainPage = src.match(/"wgIsMainPage":(true|false)/);
        if (Math.abs(totalDx) > 3) state.dragMoved = true;
                        if (matchIsMainPage) {
                            mw.config.set('wgIsMainPage', matchIsMainPage[1] === 'true');
                        } else {
                            mw.config.set('wgIsMainPage', false);
                        }


                        var matchSpecial = src.match(/"wgCanonicalSpecialPageName":"([^"]+)"/);
        instantVelocity = (frameDx * state.dragSensitivity) / dt;
                        if (matchSpecial) {
        state.releaseVelocity = (state.releaseVelocity * 0.62) + (instantVelocity * 0.38);
                            mw.config.set('wgCanonicalSpecialPageName', matchSpecial[1]);
        state.lastX = e.clientX;
                        } else {
        state.lastTime = now;
                            mw.config.set('wgCanonicalSpecialPageName', false);
                        }
                        break;
                    }
                }


                var newContent = doc.querySelector('.liberty-content-main');
        requestSidebarLanguageRotation(state.dragStartRotation + totalDx * state.dragSensitivity);
                var newTitle = doc.querySelector('.mw-page-title-main');
        e.preventDefault();
                var newHead = doc.querySelector('title');
        e.stopPropagation();
                var newHeader = doc.querySelector('.liberty-content-header');
    });


                if (newContent) {
    function finishDrag(e) {
                    $('#side-toc-box').remove();
        var velocityAge;
                    $('.profile-card').remove();
        var throwVelocity;
                    $('.user-profile-portal').removeClass('user-profile-portal');
        var wasHorizontal;
                    $('.liberty-content-main').html(newContent.innerHTML);
                    $('.profile-card').remove();
                    $('body').removeClass('page-loading');
                }


                if (newTitle) {
        if (!state.dragging) return;
                    $('.mw-page-title-main').html(newTitle.innerHTML);
                }


                if (newHead) {
        wasHorizontal = state.dragAxis === 'x';
                    document.title = newHead.textContent;
        state.dragging = false;
                }
        selector.classList.remove('is-dragging');


                if (newHeader) {
        if (fan.releasePointerCapture && state.pointerCaptured && e && e.pointerId != null) {
                    $('.liberty-content-header').html(newHeader.innerHTML);
            try { fan.releasePointerCapture(e.pointerId); } catch (err) {}
                }
        }


                window.scrollTo(0, 0);
        state.pointerCaptured = false;
                mw.hook('wikipage.content').fire($('.liberty-content-main'));
        state.dragAxis = null;
                applyMainPageStyle();
                initCategoryNavIfAvailable(document);


                if (window.ProgressSystemWebUi && typeof window.ProgressSystemWebUi.handleSpaPageView === 'function') {
        if (!wasHorizontal && !state.dragMoved) {
                    window.ProgressSystemWebUi.handleSpaPageView();
            return;
                } else if (window.ProgressSystemWebUi && typeof window.ProgressSystemWebUi.boot === 'function') {
        }
                    window.ProgressSystemWebUi.boot('spa');
                }


                $('#side-toc-box').remove();
        velocityAge = performance.now() - state.lastTime;
                setTimeout(moveTocToLeftSidebar, 100);
        throwVelocity = velocityAge > 120 ? 0 : state.releaseVelocity;
                setTimeout(moveTocToLeftSidebar, 500);
                setTimeout(moveTocToLeftSidebar, 1200);


                mw.loader.using(['mediawiki.api']).then(function() {
        if (state.dragMoved) {
                    initProfile();
            state.suppressClickUntil = performance.now() + 180;
                    moveTocToLeftSidebar();
        }
                });
            });
    }


// 목차 링크는 전용 처리
        if (state.dragMoved && Math.abs(throwVelocity) >= state.minSpinVelocity) {
$(document).on('click', '#side-toc-box a, #toc a, .toc a', function(e) {
            startSidebarLanguageInertiaSpin(throwVelocity);
    var href = $(this).attr('href');
        } else {
    if (!href || href.charAt(0) !== '#') return;
            snapSidebarLanguageNearest();
        }


    var rawId = href.slice(1);
        if (e) {
    if (!rawId) return;
            e.preventDefault();
 
            e.stopPropagation();
    var decodedId = rawId;
         }
 
    try {
        decodedId = decodeURIComponent(rawId);
    } catch (err) {
         decodedId = rawId;
     }
     }


     var target = document.getElementById(decodedId);
     fan.addEventListener('pointerup', finishDrag);
    fan.addEventListener('pointercancel', finishDrag);
    fan.addEventListener('lostpointercapture', function() {
        if (!state.dragging) return;


    if (!target && window.CSS && CSS.escape) {
        state.dragging = false;
         target = document.querySelector('#' + CSS.escape(decodedId));
        state.pointerCaptured = false;
    }
         state.dragAxis = null;
        selector.classList.remove('is-dragging');


    if (!target) return;
        if (state.dragMoved && Math.abs(state.releaseVelocity) >= state.minSpinVelocity) {
            state.suppressClickUntil = performance.now() + 180;
            startSidebarLanguageInertiaSpin(state.releaseVelocity);
        } else {
            snapSidebarLanguageNearest();
        }
    });


     e.preventDefault();
     apply.addEventListener('click', function(e) {
    e.stopPropagation();
        e.preventDefault();
        e.stopPropagation();


    var scrollTarget = target.closest('h2, h3') || target;
        snapSidebarLanguageNearest(function(meta) {
 
            scheduleSidebarLanguageNavigation(meta);
    scrollTarget.scrollIntoView({
         });
         behavior: 'auto',
        block: 'start'
     });
     });


     history.replaceState(null, '', '#' + rawId);
     selector.addEventListener('keydown', function(e) {
});
        if (e.key === 'ArrowLeft') {
            moveSidebarLanguageSelection(-1);
            e.preventDefault();
        }


    $(document).on('click', 'a', function(e) {
        if (e.key === 'ArrowRight') {
        // 휠 클릭, 새 탭 열기, 보조키 이동은 브라우저 기본 동작을 유지한다.
            moveSidebarLanguageSelection(1);
        if (e.which && e.which !== 1) return;
            e.preventDefault();
        if (e.button && e.button !== 0) return;
         }
         if (e.ctrlKey || e.metaKey || e.shiftKey || e.altKey) return;


         var href = $(this).attr('href');
         if (e.key === 'Enter' || e.key === ' ') {
         if (!href) return;
            apply.click();
            e.preventDefault();
         }
    });
}


        // 목차 링크는 별도 핸들러에서 처리
function renderSidebarLanguageBox() {
        if ($(this).closest('#side-toc-box, #toc, .toc').length) return;
    bindSidebarLanguageSelector();
    setSidebarLanguageSelection(getCurrentLang());


         // 단순 해시 링크는 SPA 가로채기 제외
    if (!sidebarLanguageStatusLoaded) {
        if (href.startsWith('#')) return;
         loadSidebarLanguageStatusRegistry(function() {
            setSidebarLanguageSelection(getCurrentLang());
        });
    }
}


        var link = document.createElement('a');
function loadRecentChangesList(targetSelector, limit) {
        link.href = href;
    var $target = $(targetSelector);


        var samePath = decodeURIComponent(link.pathname) === decodeURIComponent(window.location.pathname);
    if (!$target.length) return;
        var sameSearch = (link.search || '') === (window.location.search || '');


        if (link.hash && samePath && sameSearch) 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;


         var currentBase = window.location.href.split('#')[0];
    function escapeHtml(value) {
        var targetBase = link.href.split('#')[0];
         return String(value == null ? '' : value)
            .replace(/&/g, '&amp;')
            .replace(/</g, '&lt;')
            .replace(/>/g, '&gt;')
            .replace(/"/g, '&quot;')
            .replace(/'/g, '&#039;');
    }


        if (link.hash && currentBase === targetBase) return;
    $target.html((t && t.loading) ? t.loading : '불러오는 중...');


         if (!isInternal(href)) return;
    $.getJSON(
         if (shouldSkip(href)) return;
         '/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 = '';


        e.preventDefault();
            if (!items || !items.length) {
        playStaticSound();
                $target.html('표시할 변경 사항이 없습니다.');
        $('body').addClass('page-loading');
                return;
        history.pushState(null, '', href);
            }
        loadPage(href);
    });


    window.addEventListener('popstate', function() {
            $.each(items, function(i, item) {
        loadPage(window.location.href);
                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 timeAgo(timestamp) {
                    html +=
    var now = new Date();
                        '<a href="' + escapeHtml(pageHref) + '" class="news-recent-item">' +
    var date = new Date(timestamp);
                            '<img class="news-recent-avatar" src="' + escapeHtml(avatarSrc) + '" alt="" onerror="this.onerror=null;this.src=\'/index.php?title=특수:Redirect/file/Pfp-default.png\';">' +
    var diff = Math.floor((now - date) / 1000);
                            '<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 (diff < 60) return diff + '초 전';
            if (isNewsList) {
    if (diff < 3600) return Math.floor(diff / 60) + '분 전';
                $target.html(
    if (diff < 86400) return Math.floor(diff / 3600) + '시간 전';
                    '<div class="news-recent-viewport">' +
    return Math.floor(diff / 86400) + '일 전';
                        '<div class="news-recent-stack">' + html + '</div>' +
}
                    '</div>'
                );


// 펼접 토글
                if (typeof ensureNewsBottomFinish === 'function') {
// 펼접 토글
                    ensureNewsBottomFinish();
function getFoldTexts() {
                }
    var lang = getCurrentLang();
            } else {
    return (window.LANG && window.LANG[lang])
                $target.html(html);
        ? window.LANG[lang]
            }
        : (window.LANG ? window.LANG.ko : { expand: '펼치기', collapse: '접기' });
 
}
            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;


function refreshOpenAncestors($start) {
                if (titleW > wrapW + 20) {
    $start.parents('[id^="collapsible"]').each(function () {
                    var duration = titleW / 40;
        var $parent = $(this);
        if (!$parent.hasClass('folding-open')) return;


        // 이미 fully open 상태면 굳이 다시 잠그지 않음
                    title.css({
        if ($parent.data('fold-state') === 'open') {
                        animation: 'clbi-scroll ' + duration + 's linear infinite',
             return;
                        '--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);


         $parent.css('max-height', this.scrollHeight + 'px');
         $target.html((t && t.loadFail) ? t.loadFail : '불러오기 실패');
     });
     });
}
}


function bindInnerResizeUpdates($target) {
 
     // 이미지 늦게 로드될 때 높이 갱신
function ensureRecentViewport() {
    $target.find('img').off('.foldimg').on('load.foldimg', function () {
     var list = document.getElementById('clbi-left-recent-list');
        if ($target.hasClass('folding-open')) {
    var viewport;
            if ($target.data('fold-state') !== 'open') {
    var stack;
                $target.css('max-height', $target[0].scrollHeight + 'px');
    var children;
            }
 
            refreshOpenAncestors($target);
    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 openFold($target, $btn) {
function ensureNewsBottomFinish() {
     var t = getFoldTexts();
     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;


     $target.data('fold-state', 'opening');
     finish = content.querySelector(':scope > .news-bottom-finish');
    $target.addClass('folding-open');


     // 열린 뒤 자연 확장 가능하게 만들기 위해 먼저 px로 열기
     if (!finish) {
    $target.css('max-height', '0px');
        finish = document.createElement('div');
    $target[0].offsetHeight;
        finish.className = 'news-bottom-finish';
    $target.css('max-height', $target[0].scrollHeight + 'px');
        finish.setAttribute('aria-hidden', 'true');
        content.appendChild(finish);
    }


     $btn.text(t.collapse);
     return finish;
}


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


     // 바깥 펼접 즉시 갱신
     if (typeof ensureRecentViewport === 'function') {
    refreshOpenAncestors($target);
        ensureRecentViewport();
    }


     // 전환 끝나면 none으로 풀어서 중첩 펼접/동적 내용 증가를 자연스럽게 허용
     if (typeof ensureNewsBottomFinish === 'function') {
    $target.off('transitionend.foldopen').on('transitionend.foldopen', function (e) {
         ensureNewsBottomFinish();
         if (e.target !== this) return;
    }
        if (!$target.hasClass('folding-open')) return;


        $target.css('max-height', 'none');
    if (typeof scheduleClbiContentBottomGap === 'function') {
         $target.data('fold-state', 'open');
         scheduleClbiContentBottomGap();
    }
}


         refreshOpenAncestors($target);
function scheduleAdaptiveLeftRecentItems() {
    window.requestAnimationFrame(function () {
         updateAdaptiveLeftRecentItems();
     });
     });


     // 늦게 렌더되는 콘텐츠 대응
     window.setTimeout(updateAdaptiveLeftRecentItems, 80);
    requestAnimationFrame(function () {
    window.setTimeout(updateAdaptiveLeftRecentItems, 240);
        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) {
function updateClbiContentBottomGap(iteration) {
     var t = getFoldTexts();
     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로 환산한다.


     // none 상태에서 닫으면 transition이 안 되므로 실제 높이로 고정
     목표:
     if ($target.css('max-height') === 'none' || $target.data('fold-state') === 'open') {
     .liberty-content-main.bottom === #clbi-bottom-nav-wrap.top - 8px
         $target.css('max-height', $target[0].scrollHeight + 'px');
    */
    } else {
    if (document.body && document.body.classList && document.body.classList.contains('clbi-shell-vertical-scale')) {
         $target.css('max-height', $target[0].scrollHeight + 'px');
         rootStyle = window.getComputedStyle(document.documentElement);
         scale = parseFloat(rootStyle.getPropertyValue('--clbi-shell-scale')) || 1;
        scale = Math.max(0.25, scale);
     }
     }


     $target.data('fold-state', 'closing');
     contentRect = content.getBoundingClientRect();
     $target[0].offsetHeight;
     bottomTop = bottomNav.getBoundingClientRect().top;
     $target.css('max-height', '0px');
     visualGap = desiredGap * scale;
     $target.removeClass('folding-open');
    targetHeight = Math.floor((bottomTop - contentRect.top - visualGap) / scale);
     targetHeight = Math.max(120, targetHeight);


     $btn.text(t.expand);
     currentHeight = Math.round(content.getBoundingClientRect().height / scale);


     refreshOpenAncestors($target);
     content.style.setProperty('--clbi-content-extra', '0px');
    content.style.setProperty('height', targetHeight + 'px', 'important');
    content.style.setProperty('max-height', targetHeight + 'px', 'important');


     setTimeout(function () {
     if (Math.abs(currentHeight - targetHeight) >= 1 && iteration < 4) {
        refreshOpenAncestors($target);
        window.requestAnimationFrame(function () {
         $target.data('fold-state', 'closed');
            updateClbiContentBottomGap(iteration + 1);
     }, 250);
         });
     }
}
}


$(function () {
function scheduleClbiContentBottomGap() {
     $(document)
    window.requestAnimationFrame(function () {
         .off('click.clbiToggle')
        updateClbiContentBottomGap(0);
         .on('click.clbiToggle', '.toggleBtn', function () {
    });
            var $btn = $(this);
     window.setTimeout(function () {
            var targetId = $btn.data('target');
         updateClbiContentBottomGap(0);
            var $target = $('#' + targetId);
    }, 40);
            if (!$target.length) return;
    window.setTimeout(function () {
         updateClbiContentBottomGap(0);
    }, 120);
    window.setTimeout(function () {
        updateClbiContentBottomGap(0);
    }, 280);
    window.setTimeout(function () {
        updateClbiContentBottomGap(0);
    }, 520);
}


            var scrollY = window.scrollY;


            if ($target.hasClass('folding-open')) {
function updateLeftBillboardAdaptive() {
                closeFold($target, $btn);
    /*
            } else {
    134 기준: Ad는 이미지/CRT 비율을 유지하는 고정 슬롯이다.
                openFold($target, $btn);
    남는 세로 공간은 뉴스 박스가 흡수하므로, Ad에 하단 finish를 늘리거나
            }
    title-only 상태로 접는 adaptive 보정은 사용하지 않는다.
    */
    resetLeftBillboardAdaptiveState();
}
 
function scheduleLeftBillboardAdaptive() {
    window.requestAnimationFrame(updateLeftBillboardAdaptive);
    window.setTimeout(updateLeftBillboardAdaptive, 80);
    window.setTimeout(updateLeftBillboardAdaptive, 240);
}


            window.scrollTo(0, scrollY);
function scheduleLeftSidebarVerticalFit() {
         });
    if (typeof scheduleAdaptiveLeftRecentItems === 'function') {
});
         scheduleAdaptiveLeftRecentItems();
    }


// ========== 프로필 시스템 ==========
    if (typeof scheduleLeftBillboardAdaptive === 'function') {
function initProfile() {
        scheduleLeftBillboardAdaptive();
    $('.profile-card').remove();
     }
     $('.user-profile-portal').removeClass('user-profile-portal');


     var token = ++PROFILE_RENDER_TOKEN;
     window.setTimeout(function () {
    var ns = mw.config.get('wgNamespaceNumber');
        if (typeof scheduleAdaptiveLeftRecentItems === 'function') {
    var title = mw.config.get('wgTitle');
            scheduleAdaptiveLeftRecentItems();
    var specialPage = mw.config.get('wgCanonicalSpecialPageName');
        }
    var isProfileSettings = specialPage === '사용자정보';


    $('body').toggleClass('user-profile-page', ns === 2);
        if (typeof scheduleLeftBillboardAdaptive === 'function') {
     $('body').toggleClass('user-profile-settings-page', isProfileSettings);
            scheduleLeftBillboardAdaptive();
        }
     }, 120);
}


    if (ns === 2) {
        var profileUser = title.split('/')[0];
        renderProfile(profileUser, token);
    }


    if (isProfileSettings) {
// 국가_및_조합 전용 왼쪽 사이드바 이미지
        initUserProfilePage();
function updateLeftSidebarNationsImage() {
    }
    $('#clbi-left-nations-image').remove();
}
}


function renderProfile(username, token) {
function setProfileActionLabel(selector, text) {
     var api = new mw.Api();
     var target = $(selector);
     api.get({
     var label = target.find('.profile-action-label');
        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 (label.length) {
         if (currentTitle !== username) return;
         label.text(text);
 
    } else {
        var user = data.query.users[0];
         target.text(text);
        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;
function updateSidebar() {
     var editCount = (userData && userData.editcount) ? userData.editcount : 0;
     if (!window.LANG) {
        setTimeout(updateSidebar, 100);
        return;
     }


     function escapeHtml(value) {
     var currentLang = getCurrentLang();
        return String(value == null ? '' : value)
    var t = (window.LANG && window.LANG[currentLang]) ? window.LANG[currentLang] : window.LANG.ko;
            .replace(/&/g, '&amp;')
            .replace(/</g, '&lt;')
            .replace(/>/g, '&gt;')
            .replace(/"/g, '&quot;')
            .replace(/'/g, '&#039;');
    }


     container.classList.add('user-profile-portal');
     var newsTitle = t.news || '뉴스';
    var changelogTitle = t.changelog || '체인지로그';
    var recentTitle = t.recentChanges || '최근 변경';
    var languageTitle = t.language || '언어';


     var safeUsername = escapeHtml(username);
     $('#clbi-title-left-language').text(languageTitle);
     var avatarSrc = '/index.php?title=특수:Redirect/file/Pfp-' + encodeURIComponent(username) + '.png&width=220';
    renderSidebarLanguageBox();
     var fallbackSrc = '/index.php?title=특수:Redirect/file/Pfp-default.png&width=220';
 
     var editBtn = isOwnPage
     $('#clbi-title-left-news').text(newsTitle);
        ? '<a href="/index.php/특수:사용자정보" class="profile-edit-btn"><span class="profile-edit-label">프로필 수정</span><span class="profile-edit-arrow">›</span></a>'
    $('#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);


     var progressHtml = isOwnPage
     setProfileActionLabel('#clbi-btn-contribution', t.contribution);
        ? '<div class="profile-page-progress is-syncing" data-profile-progress>' +
    setProfileActionLabel('#clbi-btn-watchlist', t.watchlist);
            '<div class="profile-section-title">LEVEL RECORD</div>' +
    setProfileActionLabel('#clbi-btn-preferences', t.preferences);
            '<div class="profile-page-progress-body">' +
    setProfileActionLabel('#clbi-btn-logout', t.logout);
                '<div class="profile-page-progress-row">' +
    setProfileActionLabel('#clbi-btn-login', t.login);
                    '<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');
     var pageName = normalizePageName(mw.config.get('wgPageName'));
     card.className = 'profile-card profile-page-console';
     var specialPage = String(mw.config.get('wgCanonicalSpecialPageName') || '');
    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();
$('#clbi-left-news-changelog-main').text(changelogTitle);
    container.insertBefore(card, container.firstChild);
$('#clbi-left-news-recent-title').text('RECENT CHANGES');
    loadProfileFields(username, card);
 
     loadProfileContributionPages(username, card);
     $('.clbi-user-btn').removeClass('clbi-user-btn-active');
    updateProfilePageEnvironment(card, null);


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


function getProfileLanguageLabel() {
    if (specialPage === 'Watchlist') {
    var lang = getCurrentLang();
        $('#clbi-btn-watchlist').addClass('clbi-user-btn-active');
    return SIDEBAR_LANGUAGE_LABELS[lang] || (lang ? lang.toUpperCase() : '');
    }
}


function updateProfilePageEnvironment(card, summary) {
    if (
    if (!card) return;
        specialPage === '설정' ||
        pageName === '특수:설정' ||
        pageName === 'Special:설정'
    ) {
        $('#clbi-btn-preferences').addClass('clbi-user-btn-active');
    }


     var timezone = summary && summary.timezone ? summary.timezone : 'UTC';
     $('.toggleBtn').each(function() {
    var timeEl = card.querySelector('[data-profile-time]');
        var btn = $(this);
    var langEl = card.querySelector('[data-profile-language]');


    if (timeEl) {
        if (!$('#' + btn.data('target')).hasClass('folding-open')) {
        try {
            btn.text(t.expand);
            timeEl.textContent = timezone + ' ' + new Intl.DateTimeFormat('ko-KR', {
        } else {
                hour: '2-digit',
            btn.text(t.collapse);
                minute: '2-digit',
        }
                hour12: false,
    });
                timeZone: timezone
 
            }).format(new Date());
    updateLeftSidebarNationsImage();
        } catch (err) {
}
            timeEl.textContent = 'UTC ' + new Intl.DateTimeFormat('ko-KR', {
 
                hour: '2-digit',
function canShowContentTools() {
                minute: '2-digit',
    // 비로그인 사용자는 편집/역사/공유 버튼을 숨김
                hour12: false,
    if (!mw.config.get('wgUserName')) {
                timeZone: 'UTC'
        return false;
            }).format(new Date());
    }
         }
 
    // MediaWiki가 현재 문서를 편집 가능하지 않다고 판단하면 숨김
    var isEditable = mw.config.get('wgIsProbablyEditable');
    if (isEditable === false) {
         return false;
     }
     }


     if (langEl) {
    var relevantEditable = mw.config.get('wgRelevantPageIsProbablyEditable');
         langEl.textContent = getProfileLanguageLabel();
     if (relevantEditable === false) {
         return false;
     }
     }
    return true;
}
}


function loadProfileContributionPages(username, card) {
function moveCatlinksToBottom() {
     if (!username || !card) return;
     var main = $('.liberty-content-main');
     if (!mw.loader || typeof mw.loader.using !== 'function') return;
     var parserOutput = $('.liberty-content-main .mw-parser-output').first();
    var catlinks = $('.catlinks');


    var valueEl = card.querySelector('[data-contrib-pages-value]');
     if (!main.length || !catlinks.length) return;
     if (valueEl) valueEl.textContent = 'SYNC';


     mw.loader.using(['mediawiki.api']).then(function () {
     catlinks.each(function () {
         var api = new mw.Api();
         var cat = $(this);
        var pages = Object.create(null);
        var cont = {};
        var guard = 0;


         function requestNext() {
         if (parserOutput.length) {
             guard++;
             cat.appendTo(parserOutput);
 
        } else {
            var params = Object.assign({
            cat.appendTo(main);
                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;
function initCategoryNavIfAvailable(root) {
     if (!mw.loader || typeof mw.loader.using !== 'function') return;
     /*
     CategoryNav.js는 대문 카테고리 네비를 SVG로 생성한다.


     mw.loader.using(['mediawiki.api']).then(function () {
     Common.js가 SPA로 본문을 갈아끼운 뒤에는 MediaWiki 원래 페이지 로드와 달리
        var api = new mw.Api();
    CategoryNav.js의 초기 DOMContentLoaded만으로는 새 mount를 다시 잡지 못할 수 있다.
         api.get({
    CategoryNav.js 자체도 mw.hook('wikipage.content')를 듣지만, 로드 순서와 SPA 타이밍이
            action: 'progress_summary',
    엇갈릴 수 있으므로 Common.js 쪽에서도 존재 여부를 확인한 뒤 한 번 더 호출한다.
            format: 'json',
 
            formatversion: 2
    이 함수는 CategoryNav.js가 아직 로드되지 않았으면 아무 것도 하지 않는다.
         }).then(function (data) {
    */
            var payload = data && data.progress_summary;
    if (
            if (!payload || !payload.available || !payload.summary) return;
         window.CLBI &&
            updateUserPageProgress(card, payload.summary);
         window.CLBI.categoryNav &&
            updateProfilePageEnvironment(card, payload.summary);
        typeof window.CLBI.categoryNav.init === 'function'
         });
    ) {
     });
         window.CLBI.categoryNav.init(root || document);
     }
}
}


function updateUserPageProgress(card, summary) {
function removeLegacyMainPageHero() {
     var panel = card.querySelector('[data-profile-progress]');
     /*
     if (!panel || !summary) return;
    기존 대문 전용 레거시 요소 정리
    -----------------------------------------
    이전 대문 구조에서는 Common.js가 본문 바깥에 #clbi-main-logo를 직접 삽입하고,
    본문 안의 #clbi-main-crt-hero를 #clbi-main-crt-hero-wrap으로 감싸서
     .liberty-content-main 위쪽으로 재배치했다.


     var level = summary.level || 1;
     새 대문은 본문 내부의 .main-portal이 로고, 알림, 카테고리 네비, 이미지 피드,
     var totalXp = summary.totalXp || 0;
     방명록, 상태 패널을 모두 담당한다. 따라서 Common.js가 별도 로고나 CRT 래퍼를
     var xpIntoLevel = summary.xpIntoLevel || 0;
     삽입하면 새 로고/콘텐츠와 중복된다.
     var xpForNext = summary.xpForNextLevel || 1;
 
     var percent = Math.max(0, Math.min(100, summary.progressPercent || 0));
    여기서는 JS가 만들던 바깥 로고와 CRT 래퍼를 제거하고, 예전 대문 원본이나
     var isMaxLevel = !!summary.isMaxLevel;
     캐시된 렌더 결과에 남아 있을 수 있는 #clbi-main-crt-hero도 제거한다.
     var dailyXp = summary.dailyXp || 0;
     */
    var discoveries = summary.discoveryCount || 0;
    $('#clbi-main-logo').remove();
     $('#clbi-main-crt-hero-wrap').remove();
     $('#clbi-main-crt-hero').remove();
}


    panel.classList.remove('is-syncing');
    panel.classList.toggle('is-max-level', isMaxLevel);


     var levelEl = panel.querySelector('.profile-page-level');
function setNativePageTitleHiddenHard(hidden) {
    var totalEl = panel.querySelector('.profile-page-total-xp');
     var selectors = [
    var fillEl = panel.querySelector('.profile-page-xp-fill');
        '.liberty-content-header',
    var subEl = panel.querySelector('.profile-page-progress-sub');
        '.liberty-content-header .title',
    var metaEl = panel.querySelector('.profile-page-progress-meta');
        '.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'
    ];


     if (levelEl) levelEl.textContent = (isMaxLevel ? 'MAX ' : 'LVL ') + level;
     document.querySelectorAll(selectors.join(',')).forEach(function(node) {
    if (totalEl) totalEl.textContent = totalXp + ' XP';
        if (!node || !node.style) return;
    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) {
        if (hidden) {
    var api = new mw.Api();
            node.setAttribute('data-clbi-title-hidden', 'true');
    api.get({
            node.style.setProperty('display', 'none', 'important');
        action: 'userprofile',
            node.style.setProperty('visibility', 'hidden', 'important');
        user: username
            node.style.setProperty('height', '0', 'important');
    }).then(function(data) {
             node.style.setProperty('min-height', '0', 'important');
        var profile = data.userprofile;
             node.style.setProperty('margin', '0', 'important');
        updateProfileFields(card, {
             node.style.setProperty('padding', '0', 'important');
             name: profile.name || '',
             node.style.setProperty('overflow', 'hidden', 'important');
             discord: profile.discord || '',
         } else if (node.getAttribute('data-clbi-title-hidden') === 'true') {
             role: profile.role || '',
            node.removeAttribute('data-clbi-title-hidden');
             bio: profile.bio || ''
            node.style.removeProperty('display');
         });
            node.style.removeProperty('visibility');
    }).fail(function() {
            node.style.removeProperty('height');
        updateProfileFields(card, {
             node.style.removeProperty('min-height');
             name: '',
             node.style.removeProperty('margin');
             discord: '',
             node.style.removeProperty('padding');
             role: '',
             node.style.removeProperty('overflow');
             bio: ''
         }
         });
     });
     });
}
}


function updateProfileFields(card, data) {
function applyDefaultPageTitleVisibility() {
     var nameEl = card.querySelector('[data-field="name"]');
     var hideTitle = true;
     var roleEl = card.querySelector('[data-field="role"]');
     var isSystemAssetPage = false;
     var discordEl = card.querySelector('[data-field="discord"]');
 
    var bioEl = card.querySelector('[data-field="bio"]');
     if (window.CLBI_PAGE_SHELL && typeof window.CLBI_PAGE_SHELL.isSystemAssetPage === 'function') {
     if (nameEl) nameEl.textContent = data.name || '';
        isSystemAssetPage = window.CLBI_PAGE_SHELL.isSystemAssetPage();
     if (roleEl) roleEl.textContent = data.role || '';
    }
    if (discordEl) discordEl.textContent = data.discord ? ('디스코드: ' + data.discord) : '';
 
     if (bioEl) bioEl.textContent = data.bio || '';
     if (isSystemAssetPage) {
}
        hideTitle = true;
// ========== 프로필 시스템 끝 ==========
     } else if (window.CLBI_PAGE_SHELL && typeof window.CLBI_PAGE_SHELL.isBackendOrSystemPage === 'function') {
        hideTitle = !window.CLBI_PAGE_SHELL.isBackendOrSystemPage();
     }


// ========== 알림 시스템 ==========
    $('body')
function ensureNotificationPopup() {
        .toggleClass('page-title-hidden', hideTitle)
    if (document.getElementById('clbi-notification-popup')) return;
        .toggleClass('page-title-visible', !hideTitle)
        .toggleClass('clbi-system-doc-page', isSystemAssetPage);


     var popup = document.createElement('div');
     $('.content-tools').css('display', 'none');
    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 =
     if (isSystemAssetPage && window.CLBI_PAGE_SHELL && typeof window.CLBI_PAGE_SHELL.renderSystemDocIndicator === '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;">' +
         window.CLBI_PAGE_SHELL.renderSystemDocIndicator();
            '<span>알림</span>' +
    } else if (!isSystemAssetPage && window.CLBI_PAGE_SHELL && typeof window.CLBI_PAGE_SHELL.removeSystemDocIndicator === 'function') {
            '<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>' +
         window.CLBI_PAGE_SHELL.removeSystemDocIndicator();
        '</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);
     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 positionNotificationPopup() {
function applyMainPageStyle() {
     var btn = document.getElementById('profile-quick-notifications');
     var specialPage = mw.config.get('wgCanonicalSpecialPageName');
     var popup = document.getElementById('clbi-notification-popup');
     if (specialPage === 'Preferences') return;
    if (!btn || !popup) return;


     var rect = btn.getBoundingClientRect();
     var pageName = normalizePageName(mw.config.get('wgPageName'));
     var top = rect.bottom + 6;
     var namespaceNumber = mw.config.get('wgNamespaceNumber');
     var left = rect.left + (rect.width / 2) - (popup.offsetWidth / 2);
     var isMainPage = (pageName === '대문');
    var isUserProfilePage = (namespaceNumber === 2);
    var isScreenDoc = ($('.screen-header').length > 0);
    var hideTools = (isMainPage || isUserProfilePage || !canShowContentTools());


     if (left < 8) left = 8;
     $('body').toggleClass('user-profile-page', isUserProfilePage);
    if (left + popup.offsetWidth > window.innerWidth - 8) {
     $('body').toggleClass('clbi-main-page', isMainPage);
        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';
     moveCatlinksToBottom();
}


function parseNotificationItemsFromHtml(html) {
    if (isMainPage) {
    var parser = new DOMParser();
        $('.liberty-content-header').css('display', 'none');
    var doc = parser.parseFromString(html, 'text/html');
        $('.mw-page-title-main').addClass('clbi-hide');
        setNativePageTitleHiddenHard(true);
        $('.catlinks').css('display', 'none');
        $('.liberty-content-main').css('border-radius', '0');


    var selectors = [
         // 새 대문은 .main-portal 본문 구조가 로고/히어로를 담당한다.
         '.mw-echo-ui-notificationItemWidget',
         // Common.js의 구식 바깥 로고/CRT 재배치 루틴은 사용하지 않는다.
         '.mw-echo-ui-notificationsInboxWidgetRow',
         removeLegacyMainPageHero();
         '.echo-ui-notificationItemWidget',
         $('#clbi-tools-box').remove();
         'li[data-notification-id]',
        '.mw-echo-notifications-list li'
    ];


    var items = [];
        $('.content-tools').css('display', 'none');
    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) {
        initCategoryNavIfAvailable(document);
         var link = item.querySelector('a[href]');
 
         var href = link ? link.getAttribute('href') : '/index.php?title=Special:Notifications';
    } else if (isUserProfilePage) {
         var text = (item.textContent || '').replace(/\s+/g, ' ').trim();
         $('.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');


         var notificationId =
         $('#clbi-main-logo').remove();
            item.getAttribute('data-notification-id') ||
        $('#clbi-main-crt-hero-wrap').remove();
            item.getAttribute('data-id') ||
        $('#clbi-main-crt-hero').remove();
            item.getAttribute('data-notification') ||
        $('#clbi-tools-box').remove();
            '';


         if (!notificationId) {
         $('.content-tools').css('display', 'none');
            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) {
    } else if (isScreenDoc) {
            href = href.charAt(0) === '/'
         $('.liberty-content-header').css('display', 'none');
                ? href
        $('.mw-page-title-main').addClass('clbi-hide');
                : '/index.php' + (href.charAt(0) === '?' ? href : '/' + href);
        $('.catlinks').css('display', '');
         }
         $('.liberty-content-main').css('border-radius', '0');


         return {
         $('#clbi-main-logo').remove();
            id: notificationId,
         $('#clbi-main-crt-hero-wrap').remove();
            href: href,
            text: text || '알림'
         };
    });
}


function setNotificationIcon(hasItems) {
        if ($('#clbi-tools-box').length === 0 && canShowContentTools()) {
    var quickIcon = document.getElementById('profile-quick-notification-icon');
            var $toolsBox = $('<div id="clbi-tools-box" class="clbi-left-box"></div>');
    var svg = hasItems ? CLBI_SVG_BELL_DOT : CLBI_SVG_BELL;
            var $toolsTitle = $('<div class="clbi-left-title">관리</div>');
            var $toolsContent = $('<div class="clbi-left-content"></div>');


    if (quickIcon) {
            $toolsContent.append($('.content-tools .btn-group').clone(true));
        quickIcon.innerHTML = svg;
            $toolsBox.append($toolsTitle).append($toolsContent);
        quickIcon.classList.toggle('has-notifications', !!hasItems);
            $('#clbi-left-sidebar').append($toolsBox);
    }
        }
}


function renderNotificationPopup(items) {
        $('.content-tools').css('display', 'none');
    var list = document.getElementById('clbi-notification-list');
    var badge = document.getElementById('clbi-notification-badge');
    if (!list) return;


     if (!items || !items.length) {
     } else {
         list.innerHTML = '<div style="padding:14px 12px;color:#999;">표시할 알림이 없습니다.</div>';
        $('.liberty-content-header').css('display', '');
         if (badge) badge.style.display = 'none';
         $('.mw-page-title-main, .mw-page-title-namespace, .mw-page-title-separator').removeClass('clbi-hide');
         setNotificationIcon(false);
         $('.catlinks').css('display', '');
        return;
         $('.liberty-content-main').css('border-radius', '0');
    }


    var html = '';
        $('#clbi-main-logo').remove();
    for (var i = 0; i < items.length; i++) {
         $('#clbi-main-crt-hero-wrap').remove();
         html +=
        $('#clbi-tools-box').remove();
            '<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) {
     if (!isUserProfilePage) {
         badge.textContent = items.length;
         $('.profile-card').remove();
         badge.style.display = 'block';
         $('.user-profile-portal').removeClass('user-profile-portal');
     }
     }
    setNotificationIcon(true);
}


function loadNotificationsIntoPopup() {
     $('.content-tools').css('display', 'none');
     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' })
     applyDefaultPageTitleVisibility();
        .then(function(res) {
    updateSidebar();
            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', {
function removeNativeTocFromContent() {
        action: 'echomarkread',
     $('.liberty-content-main #toc, .liberty-content-main .toc').remove();
        list: 'all'
    });
}
}


function markNotificationReadById(notificationId) {
// 왼쪽 목차: MediaWiki 문단 ID 가져오기
     if (!notificationId) {
function getHeadingId(heading) {
         return $.Deferred().resolve().promise();
     if (heading.id) {
         return heading.id;
    }
 
    var headline = heading.querySelector('.mw-headline[id]');
    if (headline && headline.id) {
        return headline.id;
     }
     }


     return new mw.Api().postWithToken('csrf', {
     return '';
        action: 'echomarkread',
        list: notificationId
    });
}
}


function initNotifications() {
// 왼쪽 목차: MediaWiki 문단 제목 텍스트 가져오기
     var quickBtn = document.getElementById('profile-quick-notifications');
function getHeadingText(heading) {
     var headline = heading.querySelector('.mw-headline');
    var source = headline || heading;
    var clone = source.cloneNode(true);


     if (!quickBtn) return;
     $(clone).find('.mw-editsection, .mw-editsection-bracket, .mw-editsection-divider').remove();


     ensureNotificationPopup();
     return (clone.textContent || '')
    loadNotificationsIntoPopup();
        .replace(/\s+/g, ' ')
        .trim();
}


     $(document)
// 왼쪽 목차: 긴 제목에 자동 스크롤 적용
        .off('click.clbiNotificationToggle')
function initTocTitleScroll(root) {
         .on('click.clbiNotificationToggle', '#profile-quick-notifications', function(e) {
     var $items = root
            e.preventDefault();
        ? $(root).find('.toc-scroll-text')
            e.stopPropagation();
         : $('#side-toc-box .toc-scroll-text');


            var popup = document.getElementById('clbi-notification-popup');
    $items.each(function () {
            if (!popup) return;
        var $text = $(this);
        var $wrap = $text.closest('.toc-scroll-wrap');


            if (popup.style.display === 'none' || popup.style.display === '') {
        if (!$wrap.length) return;
                popup.style.display = 'block';
                positionNotificationPopup();
                loadNotificationsIntoPopup();
            } else {
                popup.style.display = 'none';
            }
        });


    $(document)
         var wrapW = Math.floor($wrap.width());
         .off('click.clbiNotificationOutside')
        var textW = Math.ceil(this.scrollWidth);
        .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';
        if (!wrapW || !textW) return;
            }
        });


    $(document)
         if (textW <= wrapW + 12) {
         .off('click.clbiNotificationReadAll')
             // 왼쪽 목차: 칸을 넘지 않는 제목은 전체 텍스트를 그대로 보여준다.
        .on('click.clbiNotificationReadAll', '#clbi-notification-readall', function(e) {
             $wrap.removeClass('is-scrolling');
             e.preventDefault();
             e.stopPropagation();


             var button = this;
             if ($text.data('toc-scroll-enabled')) {
            button.disabled = true;
                 $text.css({
            button.textContent = '처리 중...';
                     animation: '',
 
                     'animation-delay': '',
            markAllNotificationsRead()
                     '--scroll-dist': ''
                 .then(function() {
                     loadNotificationsIntoPopup();
                })
                .always(function() {
                     button.disabled = false;
                     button.textContent = '전체 읽음';
                 });
                 });
        });
                $text.removeData('toc-scroll-enabled');
                $text.removeData('toc-scroll-key');
            }


    $(document)
             return;
        .off('click.clbiNotificationItem')
        }
        .on('click.clbiNotificationItem', '.clbi-notification-item', function(e) {
             e.preventDefault();
            e.stopPropagation();


            var href = this.getAttribute('href');
        var scrollDist = '-' + (textW - wrapW + 10) + 'px';
            var notificationId = this.getAttribute('data-notification-id') || '';
        var duration = Math.max(7, textW / 38) * 1.25;
        var scrollKey = scrollDist + '|' + duration;


            markNotificationReadById(notificationId).always(function() {
        // 왼쪽 목차: 긴 제목에는 오른쪽 페이드와 스크롤을 적용한다.
                loadNotificationsIntoPopup();
        $wrap.addClass('is-scrolling');
                if (href) {
 
                    window.location.href = href;
        // 왼쪽 목차: 같은 값으로 이미 적용된 애니메이션은 다시 초기화하지 않는다.
                }
        if ($text.data('toc-scroll-key') === scrollKey) {
            });
            return;
         });
        }
 
        $text.data('toc-scroll-enabled', true);
         $text.data('toc-scroll-key', scrollKey);


    $(window)
        $text.css({
        .off('resize.clbiNotification')
            // 왼쪽 목차: 페이지 진입 직후에는 잠시 읽을 시간을 준 뒤 흐르게 한다.
        .on('resize.clbiNotification', function() {
            animation: 'toc-scroll-blink-reset ' + duration + 's linear infinite',
             var popup = document.getElementById('clbi-notification-popup');
             'animation-delay': '1s',
             if (popup && popup.style.display === 'block') {
             '--scroll-dist': scrollDist
                positionNotificationPopup();
            }
         });
         });
    });
}
}
// ========== 알림 시스템 끝 ==========


function initUserProfilePage() {
// 목차를 왼쪽 사이드바에 새로 생성
     $('body').addClass('user-profile-settings-page');
function moveTocToLeftSidebar() {
    removeNativeTocFromContent();
     $('#side-toc-box').remove();
    return;


     var saveBtn = document.getElementById('pref-save');
     // 왼쪽 목차: MediaWiki가 만든 원래 목차는 본문에서 제거한다.
     if (!saveBtn) return;
     removeNativeTocFromContent();


     function getPrefRow(id) {
     var leftSidebar = document.getElementById('clbi-left-sidebar');
        var el = document.getElementById(id);
    if (!leftSidebar) return;
        if (!el) return null;
        return el.closest('.clbi-pref-row') || el.parentNode;
    }


     function removePrefRow(id) {
     var content =
        var row = getPrefRow(id);
         document.querySelector('.liberty-content-main .mw-parser-output') ||
         if (row && row.parentNode) {
        document.querySelector('.liberty-content-main');
            row.parentNode.removeChild(row);
 
        }
     if (!content) return;
     }


     function createPrefSection(className, titleText) {
     var headings = Array.prototype.slice.call(
         var section = document.createElement('div');
         content.querySelectorAll('h2, h3')
         section.className = 'clbi-pref-section ' + className;
    ).filter(function (heading) {
         if (heading.closest('#toc, .toc, #side-toc-box')) return false;


         var title = document.createElement('div');
         var id = getHeadingId(heading);
         title.className = 'clbi-pref-section-title';
         var text = getHeadingText(heading);
        title.textContent = titleText;


         var body = document.createElement('div');
         if (!id || !text) return false;
        body.className = 'clbi-pref-section-body';


         section.appendChild(title);
         return true;
        section.appendChild(body);
    });


        return {
    var tocKey = headings.map(function (heading) {
            section: section,
         return getHeadingId(heading) + '|' + getHeadingText(heading);
            body: body
     }).join('||');
         };
     }


     function moveRowToSection(id, targetBody, className) {
     var existingBox = document.getElementById('side-toc-box');
        var row = getPrefRow(id);
        if (!row || !targetBody) return false;


        row.classList.add('clbi-pref-row-key-' + className);
    // 왼쪽 목차: 같은 문서에서 같은 목차를 이미 만들었다면 다시 지우고 만들지 않는다.
         targetBody.appendChild(row);
    if (existingBox && existingBox.getAttribute('data-toc-key') === tocKey) {
         return true;
         initTocTitleScroll(existingBox);
         return;
     }
     }


     function rebuildProfileSettingsLayout() {
     if (existingBox) {
         var root = document.querySelector('.clbi-prefs-profile');
         existingBox.remove();
        if (!root || root.dataset.profileSettingsReworked === '1') return;
    }


        root.dataset.profileSettingsReworked = '1';
    if (!headings.length) return;
        root.classList.add('profile-settings-console');


        removePrefRow('pref-badges');
    var tocBox = document.createElement('div');
    tocBox.className = 'clbi-left-box';
    tocBox.id = 'side-toc-box';
    tocBox.setAttribute('data-toc-key', tocKey);


        var originalRows = Array.prototype.slice.call(root.querySelectorAll('.clbi-pref-row'));
    var title = document.createElement('div');
        var actionNodes = [];
    title.className = 'clbi-left-title';


        if (saveBtn.parentNode === root || saveBtn.closest('.clbi-prefs-profile') === root) {
    // 왼쪽 목차: 박스 제목은 Lang.js의 현재 UI 언어를 따른다.
            actionNodes.push(saveBtn);
    var currentLang = getCurrentLang();
        }
    var t = (window.LANG && window.LANG[currentLang]) ? window.LANG[currentLang] : window.LANG.ko;
    var tocTitleText = (t && t.toc) ? t.toc : '목차';


        var statusNode = document.getElementById('pref-status');
    title.textContent = tocTitleText;
        if (statusNode && statusNode.closest('.clbi-prefs-profile') === root) {
            actionNodes.push(statusNode);
        }


        var main = document.createElement('div');
    var body = document.createElement('div');
        main.className = 'clbi-pref-main-grid';
    body.className = 'clbi-left-content toc-sidebar-content';


        var media = createPrefSection('clbi-pref-section-media', 'PROFILE IMAGE');
    var list = document.createElement('ul');
        var identity = createPrefSection('clbi-pref-section-identity', 'IDENTITY RECORD');
    list.className = 'generated-toc';
        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);
    headings.forEach(function (heading) {
         main.appendChild(identity.section);
         var id = getHeadingId(heading);
         main.appendChild(bio.section);
         var text = getHeadingText(heading);
         main.appendChild(account.section);
         var level = heading.tagName.toLowerCase() === 'h3' ? 3 : 2;
        main.appendChild(misc.section);


         root.innerHTML = '';
         var item = document.createElement('li');
         root.appendChild(main);
         item.className = 'toc-level-' + level;


         moveRowToSection('pref-pfp-preview', media.body, 'pfp');
         var link = document.createElement('a');
         moveRowToSection('pref-pfp-btn', media.body, 'pfp');
         link.setAttribute('href', '#' + id);
        moveRowToSection('pref-pfp-input', media.body, 'pfp');


         moveRowToSection('pref-name', identity.body, 'name');
         // 왼쪽 목차: 긴 제목 스크롤을 위해 텍스트를 별도 span으로 감싼다.
         moveRowToSection('pref-role', identity.body, 'role');
         var textWrap = document.createElement('span');
         moveRowToSection('pref-discord', identity.body, 'discord');
         textWrap.className = 'toc-scroll-wrap';


         moveRowToSection('pref-bio', bio.body, 'bio');
         var textSpan = document.createElement('span');
        textSpan.className = 'toc-scroll-text';
        textSpan.textContent = text;


         moveRowToSection('pref-new-email', account.body, 'email');
         textWrap.appendChild(textSpan);
         moveRowToSection('pref-email-password', account.body, 'email');
         link.appendChild(textWrap);
        moveRowToSection('pref-email-save', account.body, 'email');


         originalRows.forEach(function (row) {
         item.appendChild(link);
            if (!row.parentNode && !row.className.match(/clbi-pref-row-key-/)) {
        list.appendChild(item);
                misc.body.appendChild(row);
    });
            }
        });


        if (!misc.body.children.length) {
    body.appendChild(list);
            misc.section.parentNode.removeChild(misc.section);
    tocBox.appendChild(title);
        }
    tocBox.appendChild(body);
    leftSidebar.appendChild(tocBox);


         var actions = document.createElement('div');
    // 왼쪽 목차: DOM 배치가 끝난 뒤 긴 제목 스크롤 여부를 계산한다.
         actions.className = 'clbi-pref-actions';
    requestAnimationFrame(function () {
        initTocTitleScroll(tocBox);
 
         setTimeout(function () {
            initTocTitleScroll(tocBox);
         }, 120);
    });
}


        if (saveBtn) actions.appendChild(saveBtn);
        if (statusNode) actions.appendChild(statusNode);


         root.appendChild(actions);
// 우측 광고판: 이미지/번역 캡션 목록
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: [
            '"지금 지원하세요!"'
        ]
     }
     }
];


    rebuildProfileSettingsLayout();
function getRightBillboardItem(index) {
    var items = RIGHT_BILLBOARD_ITEMS;


     var api = new mw.Api();
     if (!items || !items.length) {
    var selectedFile = null;
        return {
     var cropper = null;
            file: 'Side-visual-001.png',
            alt: '',
            caption: []
        };
     }


     if (!document.getElementById('clbi-gallery-modal')) {
     var normalized = index % items.length;
        var gModal = document.createElement('div');
    if (normalized < 0) normalized += items.length;
        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 =
    return items[normalized];
            '<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);
function getRightBillboardImageUrl(fileName) {
    }
    return '/index.php?title=특수:Redirect/file/' + encodeURIComponent(fileName || 'Side-visual-001.png');
}


    if (!document.getElementById('clbi-crop-modal')) {
function getRightBillboardCaptionHtml(item) {
        var cModal = document.createElement('div');
    var lines = item && item.caption ? item.caption : [];
        cModal.id = 'clbi-crop-modal';
    var html = '';
        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 =
    function escapeCaptionText(value) {
            '<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 String(value == null ? '' : value)
                '<div style="font-size:14px;font-weight:700;color:#e2e2e2;">사진 조정</div>' +
            .replace(/&/g, '&amp;')
                '<div style="width:100%;max-height:380px;overflow:hidden;border-radius:8px;">' +
            .replace(/</g, '&lt;')
                    '<img id="clbi-crop-image" style="max-width:100%;">' +
            .replace(/>/g, '&gt;')
                '</div>' +
            .replace(/\"/g, '&quot;')
                '<div style="display:flex;gap:8px;justify-content:flex-end;">' +
            .replace(/'/g, '&#039;');
                    '<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');
     lines.forEach(function(line) {
    var cModal = document.getElementById('clbi-crop-modal');
        var text = String(line == null ? '' : line);
    var cropImage = document.getElementById('clbi-crop-image');
        var isGap = !text.trim();
    var pfpInput = document.getElementById('pref-pfp-input');
        var className = 'right-billboard-caption-line' + (isGap ? ' is-gap' : '');
        html += '<span class="' + className + '">' + (isGap ? '&nbsp;' : escapeCaptionText(text)) + '</span>';
    });


     function openGallery() {
     return html;
        gModal.style.display = 'flex';
}


        var username = mw.config.get('wgUserName');
function setRightBillboardItem(index) {
        api.get({
    var box = document.querySelector('.right-billboard-box');
            action: 'query',
    if (!box) return;
            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 item = getRightBillboardItem(index);
            var sectionEl = document.getElementById('clbi-gallery-history-section');
    var src = getRightBillboardImageUrl(item.file);
            historyEl.innerHTML = '';
    var images = box.querySelectorAll('.right-billboard-image');
    var caption = box.querySelector('#right-billboard-caption');
    var emptySub = box.querySelector('.right-billboard-empty-sub');


            page.imageinfo.forEach(function(info, idx) {
    box.setAttribute('data-billboard-index', String(index));
                var wrap = document.createElement('div');
    box.classList.remove('is-empty');
                wrap.style.cssText = 'position:relative;cursor:pointer;';


                var img = document.createElement('img');
    Array.prototype.forEach.call(images, function(img) {
                img.src = info.url;
        img.style.display = '';
                img.style.cssText =
        img.setAttribute('src', src);
                    'width:72px;height:72px;object-fit:cover;border-radius:8px;border:2px solid #444;flex-shrink:0;';
        img.setAttribute('alt', img.classList.contains('right-billboard-image-base') ? (item.alt || '') : '');
    });


                if (idx === 0) {
    if (caption) {
                    img.style.borderColor = '#854369';
        caption.innerHTML = getRightBillboardCaptionHtml(item);
                    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 (emptySub) {
                    if (idx !== 0) img.style.borderColor = '#854369';
        emptySub.textContent = item.file || 'Side-visual-001.png';
                });
    }
}


                img.addEventListener('mouseleave', function() {
function getRightBillboardItemDuration(item) {
                    if (idx !== 0) img.style.borderColor = '#444';
    var duration = item && item.duration ? parseInt(item.duration, 10) : 3000;
                });


                img.addEventListener('click', function() {
    if (Number.isNaN(duration) || duration < 500) {
                    fetch(info.url)
        duration = 3000;
                        .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);
    return duration;
                historyEl.appendChild(wrap);
}
            });


            sectionEl.style.display = 'block';
function initRightBillboardCarousel() {
        });
    var box = document.querySelector('.right-billboard-box');
    }
    if (!box || box.getAttribute('data-billboard-ready') === '1') return;


     function openCrop(src) {
     box.setAttribute('data-billboard-ready', '1');
        cropImage.src = src;
    box.setAttribute('data-billboard-index', '0');
        cModal.style.display = 'flex';


        if (cropper) {
    setRightBillboardItem(0);
            cropper.destroy();
            cropper = null;
        }


        setTimeout(function() {
    if (!RIGHT_BILLBOARD_ITEMS || RIGHT_BILLBOARD_ITEMS.length <= 1) return;
            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() {
     function scheduleNext() {
         openGallery();
        var current = parseInt(box.getAttribute('data-billboard-index') || '0', 10);
    });
         if (Number.isNaN(current)) current = 0;


    document.getElementById('clbi-gallery-upload-btn').addEventListener('click', function() {
        var currentItem = getRightBillboardItem(current);
         pfpInput.click();
         var delay = getRightBillboardItemDuration(currentItem);
    });


    document.getElementById('clbi-gallery-close').addEventListener('click', function() {
        window.setTimeout(function() {
        gModal.style.display = 'none';
            if (!document.body.contains(box)) return;
    });


    pfpInput.addEventListener('change', function() {
            if (!document.hidden) {
        var file = this.files[0];
                setRightBillboardItem(current + 1);
        if (!file) return;
            }


         gModal.style.display = 'none';
            scheduleNext();
         }, delay);
    }


        var reader = new FileReader();
    scheduleNext();
        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() {
function escapeRightBillboardAttr(value) {
        if (!cropper) return;
     return String(value == null ? '' : value)
 
        .replace(/&/g, '&amp;')
         var canvas = cropper.getCroppedCanvas({ width: 256, height: 256 });
        .replace(/</g, '&lt;')
        if (!canvas) return;
        .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>';
}
 


        canvas.toBlob(function(blob) {
var GREAT_WALL_DATA_TITLE = '프로젝트:The_Great_Wall/Data.json';
            selectedFile = new File([blob], 'profile.png', { type: 'image/png' });
var GREAT_WALL_LIST_LIMIT = 0;
            document.getElementById('pref-pfp-preview').src = URL.createObjectURL(blob);
var greatWallState = {
            cModal.style.display = 'none';
    data: { entries: {} },
            cropper.destroy();
    loaded: false,
            cropper = null;
    loading: false,
            document.getElementById('pref-pfp-btn').textContent = '✓ 사진 선택됨';
    saving: false,
        }, 'image/png');
    selectedOwnEntry: false,
    });
    statusText: ''
};


    var emailSaveBtn = document.getElementById('pref-email-save');
function normalizeGreatWallData(data) {
    if (emailSaveBtn) {
    var normalized = { entries: {} };
        emailSaveBtn.addEventListener('click', function() {
    var entries = data && data.entries && typeof data.entries === 'object' ? data.entries : {};
            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) {
    Object.keys(entries).forEach(function(key) {
                statusEl.textContent = '이메일과 비밀번호를 입력해주세요.';
        var item = entries[key];
                return;
        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) : '';


            statusEl.textContent = '변경 중...';
        if (!user || !text) return;


            api.postWithToken('csrf', {
        normalized.entries[user] = {
                action: 'changeemail',
            user: user,
                email: newEmail,
            text: text.slice(0, 140),
                password: password
            timestamp: timestamp || new Date(0).toISOString()
            }).then(function() {
        };
                statusEl.textContent = '✓ 이메일 변경됨';
    });
                document.getElementById('pref-new-email').value = '';
 
                document.getElementById('pref-email-password').value = '';
    return normalized;
}
 
function parseGreatWallData(text) {
    var parsed;


                setTimeout(function() {
    try {
                    statusEl.textContent = '';
        parsed = text ? JSON.parse(text) : {};
                }, 3000);
    } catch (err) {
            }).fail(function(code, data) {
        console.error('The Great Wall data parse failed:', err);
                var msg = data && data.error && data.error.info ? data.error.info : '변경 실패';
         parsed = {};
                statusEl.textContent = msg;
            });
         });
     }
     }


     saveBtn.addEventListener('click', function() {
     return normalizeGreatWallData(parsed);
        var statusEl = document.getElementById('pref-status');
}
        statusEl.textContent = '저장 중...';
 
function stringifyGreatWallData(data) {
    return JSON.stringify(normalizeGreatWallData(data), null, 2) + '\n';
}


        var promises = [];
function getGreatWallRevisionText(page) {
    var rev;
    var slot;


        if (selectedFile) {
    if (!page || !page.revisions || !page.revisions.length) return '';
            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'];
    rev = page.revisions[0];


        for (var i = 0; i < fields.length; i++) {
    if (rev.slots && rev.slots.main) {
            var el = document.getElementById('pref-' + fields[i]);
        slot = rev.slots.main;
            if (!el) continue;
        return slot.content || slot['*'] || '';
    }


            promises.push(
    return rev.content || rev['*'] || '';
                api.postWithToken('csrf', {
}
                    action: 'options',
 
                    optionname: 'profile-' + fields[i],
function fetchGreatWallData() {
                    optionvalue: el.value
    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: {} };
         }
         }


         $.when.apply($, promises)
         return parseGreatWallData(getGreatWallRevisionText(page));
            .then(function() {
    }, function(err) {
                statusEl.textContent = '✓ 저장됨';
        console.error('The Great Wall load failed:', err);
                selectedFile = null;
        return { entries: {} };
                document.getElementById('pref-pfp-btn').textContent = '사진 선택';
    });
}
 
function getGreatWallEntries(data) {
    var entries = data && data.entries && typeof data.entries === 'object' ? data.entries : {};


                setTimeout(function() {
    return Object.keys(entries).map(function(key) {
                    statusEl.textContent = '';
        return entries[key];
                }, 2000);
    }).filter(function(item) {
            })
        return item && item.user && item.text;
            .fail(function() {
    }).sort(function(a, b) {
                statusEl.textContent = '저장 실패';
        return String(a.timestamp || '').localeCompare(String(b.timestamp || ''));
            });
     });
     });
}
}


/* =========================================
function getGreatWallMessageTime(timestamp) {
  Banner / CRT Page Monitor thumbnail slices
    var date = timestamp ? new Date(timestamp) : null;
  - base 이미지는 틀에 들어간 파일 문법 그대로 사용
    var month;
  - slice 레이어에는 300px MediaWiki 썸네일만 삽입
    var day;
  ========================================= */
    var hour;
    var minute;


(function ($, mw) {
    if (!date || isNaN(date.getTime())) return '—';
    var thumbCache = {};


     function parseSliceWidth(value) {
     month = String(date.getMonth() + 1).padStart(2, '0');
        var parsed = parseInt(value, 10);
    day = String(date.getDate()).padStart(2, '0');
    hour = String(date.getHours()).padStart(2, '0');
    minute = String(date.getMinutes()).padStart(2, '0');


        if (!isFinite(parsed) || parsed < 120) {
    return month + '.' + day + ' ' + hour + ':' + minute;
            return 300;
}
        }


        return parsed;
function getGreatWallAvatarSrc(user) {
    }
    return '/index.php?title=특수:Redirect/file/Pfp-' + encodeURIComponent(String(user || '')) + '.png';
}


    function getImageSrc(img) {
function buildGreatWallEntryHtml(item, currentUser, selectedOwnEntry) {
        return img ? (img.currentSrc || img.getAttribute('src') || img.src || '') : '';
    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 || '';


     function getFileNameFromSrc(src) {
     return '' +
        var a;
        '<' + tag + attrs + ' class="' + className + '">' +
         var parts;
            '<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;;">' +
        var fileName;
            '<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 + '>';
}


        if (!src) return '';
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 = {};


        a = document.createElement('a');
    if (!box || !list) return;
        a.href = src;


        parts = (a.pathname || '').split('/').filter(function (part) {
    box.classList.toggle('is-guest', !currentUser);
            return !!part;
    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);


         if (!parts.length) return '';
    entries.forEach(function(item) {
         if (seen[item.user]) return;
        visibleEntries.push(item);
        seen[item.user] = true;
    });


         fileName = parts.pop();
    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) {
        * MediaWiki thumb URL 예시:
        list.scrollTop = list.scrollHeight;
        * /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;
    if (status) {
         status.textContent = greatWallState.statusText || (currentUser ? (ownEntry ? 'SELECT YOUR MARK TO UPDATE' : 'LEAVE ONE MARK') : 'ACCOUNT REQUIRED');
     }
     }


     function resolveThumbUrl(img, width, callback) {
     if (!input || !submit) return;
        var src = getImageSrc(img);
        var fileName = getFileNameFromSrc(src);
        var cacheKey;
        var entry;


        if (!src) return;
    input.disabled = false;
    input.readOnly = false;
    input.removeAttribute('aria-readonly');


        if (!fileName || !mw || !mw.loader) {
    if (!currentUser) {
             callback(src);
        input.disabled = true;
             return;
        input.readOnly = false;
        input.value = '';
        input.placeholder = 'Login required';
        submit.textContent = 'LOGIN';
        submit.disabled = false;
        if (deleteButton) {
             deleteButton.disabled = true;
             deleteButton.title = 'Login required';
         }
         }
        return;
    }


         cacheKey = fileName + '|' + width;
    if (greatWallState.saving || greatWallState.loading) {
         entry = thumbCache[cacheKey];
        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 (entry) {
    if (deleteButton) {
            if (entry.resolved) {
        deleteButton.disabled = !ownEntry;
                callback(entry.url || src);
        deleteButton.title = ownEntry ? 'Delete your mark' : 'No mark to delete';
            } else {
    }
                entry.callbacks.push(callback);
            }
            return;
        }


         entry = {
    if (ownEntry && !greatWallState.selectedOwnEntry) {
            resolved: false,
        input.disabled = false;
            url: '',
         input.readOnly = true;
            callbacks: [callback]
        input.setAttribute('aria-readonly', 'true');
         };
        input.value = '';
        input.placeholder = 'Select your existing mark';
        submit.disabled = true;
        submit.textContent = 'UPDATE';
         return;
    }


         thumbCache[cacheKey] = entry;
    input.disabled = false;
    input.readOnly = false;
    input.removeAttribute('aria-readonly');
    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 finish(url) {
function saveGreatWallEntry() {
            var callbacks = entry.callbacks.slice();
    var input = document.getElementById('great-wall-input');
            var i;
    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;


            entry.resolved = true;
    if (!currentUser) {
            entry.url = url || src;
        window.location.href = '/index.php?title=특수:로그인&returnto=' + encodeURIComponent(mw.config.get('wgPageName') || '대문');
            entry.callbacks = [];
        return;
    }


            for (i = 0; i < callbacks.length; i++) {
    if (ownEntry && !greatWallState.selectedOwnEntry) {
                callbacks[i](entry.url);
        greatWallState.statusText = 'SELECT YOUR MARK FIRST';
            }
        renderGreatWallBox();
        }
        return;
    }


         mw.loader.using('mediawiki.api').done(function () {
    if (!text) {
            var api = new mw.Api();
         greatWallState.statusText = 'EMPTY MARK';
        renderGreatWallBox();
        return;
    }


            api.get({
    if (text.length > 140) {
                action: 'query',
        text = text.slice(0, 140);
                titles: 'File:' + fileName,
    }
                prop: 'imageinfo',
                iiprop: 'url',
                iiurlwidth: width,
                formatversion: 2
            }).done(function (data) {
                var page;
                var info;


                if (
    greatWallState.saving = true;
                    data &&
    greatWallState.statusText = 'SAVING';
                    data.query &&
    renderGreatWallBox();
                    data.query.pages &&
                    data.query.pages.length
                ) {
                    page = data.query.pages[0];


                    if (
    fetchGreatWallData().then(function(data) {
                        page &&
        data = normalizeGreatWallData(data);
                        page.imageinfo &&
        data.entries[currentUser] = {
                        page.imageinfo.length
            user: currentUser,
                    ) {
            text: text,
                        info = page.imageinfo[0];
            timestamp: new Date().toISOString()
                    }
        };
                }


                finish((info && (info.thumburl || info.url)) || src);
        api = new mw.Api();
            }).fail(function () {
        return api.postWithToken('csrf', {
                finish(src);
            action: 'edit',
             });
            title: GREAT_WALL_DATA_TITLE,
         }).fail(function () {
            text: stringifyGreatWallData(data),
             finish(src);
            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;
     }
     }


     function applySliceImages(frame, thumbUrl) {
     if (!ownEntry) {
         var slices;
         greatWallState.statusText = 'NO MARK TO DELETE';
         var i;
         renderGreatWallBox();
         var img;
         return;
    }


        if (!frame || !thumbUrl) return;
    greatWallState.saving = true;
    greatWallState.statusText = 'DELETING';
    renderGreatWallBox();


         slices = frame.querySelectorAll('.crt-page-monitor-slice');
    fetchGreatWallData().then(function(data) {
         data = normalizeGreatWallData(data);
        if (data.entries && data.entries[currentUser]) {
            delete data.entries[currentUser];
        }


         for (i = 0; i < slices.length; i++) {
         api = new mw.Api();
             slices[i].innerHTML = '';
        return api.postWithToken('csrf', {
 
             action: 'edit',
             img = document.createElement('img');
            title: GREAT_WALL_DATA_TITLE,
             img.className = 'crt-page-monitor-slice-img';
             text: stringifyGreatWallData(data),
             img.src = thumbUrl;
            summary: 'Delete The Great Wall entry',
             img.alt = '';
            format: 'json'
             img.decoding = 'async';
        }).then(function() {
            img.loading = 'eager';
            greatWallState.data = data;
            img.setAttribute('aria-hidden', 'true');
             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;


            slices[i].appendChild(img);
    if (mw.Api) {
         }
        initGreatWallBox();
         return;
    }


         frame.setAttribute('data-crt-slices-ready', '1');
    if (mw.loader && mw.loader.using) {
         mw.loader.using(['mediawiki.api']).then(function() {
            initGreatWallBox();
        });
     }
     }
}


    function initBannerFrame(frame) {
function initGreatWallBox() {
        var baseImg;
    var box = document.getElementById('great-wall-sidebar');
        var width;
    var input = document.getElementById('great-wall-input');
    var submit = document.getElementById('great-wall-submit');
    var deleteButton = document.getElementById('great-wall-delete');


        if (!frame) return;
    if (!box || box.getAttribute('data-great-wall-ready') === '1') return;
        if (frame.getAttribute('data-crt-slices-ready') === '1') return;


         baseImg = frame.querySelector('.crt-page-monitor-image-base img');
    if (!mw.Api) {
         initGreatWallBoxWhenReady();
        return;
    }


        if (!baseImg) return;
    box.setAttribute('data-great-wall-ready', '1');


         width = parseSliceWidth(frame.getAttribute('data-crt-slice-width'));
    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;


         resolveThumbUrl(baseImg, width, function (thumbUrl) {
         if (!ownButton || !ownEntry) return;
            if (!frame || !frame.parentNode) return;
            applySliceImages(frame, thumbUrl);
        });
    }


    function initBannerFrames(root) {
         greatWallState.selectedOwnEntry = true;
         var scope = root && root.querySelectorAll ? root : document;
         greatWallState.statusText = 'YOUR MARK SELECTED';
         var frames = scope.querySelectorAll('.crt-page-monitor-frame');
         renderGreatWallBox();
         var i;


         for (i = 0; i < frames.length; i++) {
         if (input) {
             initBannerFrame(frames[i]);
            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;


    $(function () {
        greatWallState.selectedOwnEntry = false;
         initBannerFrames(document);
        greatWallState.statusText = '';
        if (input) input.value = '';
         renderGreatWallBox();
     });
     });


     if (mw && mw.hook) {
     if (submit) {
        mw.hook('wikipage.content').add(function ($content) {
        submit.addEventListener('click', function(e) {
             initBannerFrames($content && $content[0] ? $content[0] : document);
            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();
         });
         });
     }
     }
})(jQuery, window.mw);


/* =========================================
    if (input) {
  Doc Tab System — Q/E 단축키 탭 전환
        input.addEventListener('keydown', function(e) {
  글리치 플리커 + RGB split + 방향 슬라이드
            if (e.key === 'Enter') {
  ========================================= */
                e.preventDefault();
                saveGreatWallEntry();
            }
        });
 
        input.addEventListener('input', function() {
            if (input.value.length > 140) {
                input.value = input.value.slice(0, 140);
            }
        });
    }


(function () {
    greatWallState.loading = true;
     'use strict';
     greatWallState.statusText = 'SYNCING WALL';
    renderGreatWallBox();


var keydownBound = false;
    fetchGreatWallData().then(function(data) {
        greatWallState.data = normalizeGreatWallData(data);
        greatWallState.loaded = true;
        greatWallState.loading = false;
        greatWallState.selectedOwnEntry = false;
        greatWallState.statusText = '';
        renderGreatWallBox();
    });
}


     function initDocTabs() {
function buildGreatWallBox() {
        var tabBars = document.querySelectorAll('.doc-tab-bar');
     return '' +
        if (!tabBars.length) 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() {
    var portal = document.querySelector('.main-portal');
    var guestbook = portal ? portal.querySelector('.guestbook-device') : null;
    var sideScreen;
    var grid;


        tabBars.forEach(function (bar) {
    if (!portal || !guestbook) return;
            if (bar.getAttribute('data-tabs-init')) return;
            bar.setAttribute('data-tabs-init', '1');


            var tabs = Array.from(bar.querySelectorAll('.doc-tab'));
    sideScreen = guestbook.closest ? guestbook.closest('.side-screen') : null;
            if (!tabs.length) return;
    grid = sideScreen && sideScreen.closest ? sideScreen.closest('.console-grid') : null;


            var panel = bar.closest('.doc-panel');
    if (guestbook.parentNode) {
            var display = panel ? panel.querySelector('.doc-display') : null;
        guestbook.parentNode.removeChild(guestbook);
            if (!display) display = document.getElementById('doc-main-display');
    }
            if (!display) return;


            tabs.forEach(function (tab, i) {
    if (sideScreen && !(sideScreen.textContent || '').trim() && !sideScreen.querySelector('img,svg,video,canvas,form,input,button,a')) {
                tab.addEventListener('click', function () {
        if (sideScreen.parentNode) {
                    var currentIdx = tabs.findIndex(function (t) {
            sideScreen.parentNode.removeChild(sideScreen);
                        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 (grid) {
             if (initIdx !== -1) {
             grid.style.gridTemplateColumns = 'minmax(0,1fr)';
                var initRef = tabs[initIdx].dataset.ref;
        }
                var initEl = initRef ? document.getElementById(initRef) : null;
    }
                display.innerHTML = initEl ? initEl.innerHTML : (tabs[initIdx].dataset.content || '');
            }
        });


        if (!keydownBound) {
    portal.classList.add('is-great-wall-relocated');
            keydownBound = true;
}
            document.addEventListener('keydown', function (e) {
                var tag = document.activeElement.tagName;
                if (tag === 'INPUT' || tag === 'TEXTAREA') return;


                 var bar = document.querySelector('.doc-tab-bar');
function buildSiteInformationBox() {
                 if (!bar) return;
    return '' +
                 var tabs = Array.from(bar.querySelectorAll('.doc-tab'));
        '<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>';
}


                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;


                var activeIdx = tabs.findIndex(function (t) {
// 초기화 함수
                    return t.classList.contains('active');
function initSidebars() {
                });
    var header = $('.liberty-content-header');
                if (activeIdx === -1) return;
    var content = $('.liberty-content');


                if (e.key === 'q' || e.key === 'Q') {
    if (header.length && content.length) {
                    e.preventDefault();
        header.prependTo(content);
                    var prev = (activeIdx - 1 + tabs.length) % tabs.length;
                    if (prev !== activeIdx) switchTab(tabs, display, prev, -1);
                } else if (e.key === 'e' || e.key === 'E') {
                    e.preventDefault();
                    var next = (activeIdx + 1) % tabs.length;
                    if (next !== activeIdx) switchTab(tabs, display, next, 1);
                }
            });
        }
     }
     }


     var isAnimating = false;
     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';


    function switchTab(tabs, display, nextIdx, dir) {
         var userBox;
         if (isAnimating) return;
        isAnimating = true;


         tabs.forEach(function (t) { t.classList.remove('active'); });
         if (isLoggedIn) {
        tabs[nextIdx].classList.add('active');
            userBox =
 
                '<div class="clbi-right-box profile-card-box">' +
        var ref = tabs[nextIdx].dataset.ref;
                    '<div id="clbi-user-avatar-wrap" class="profile-identity-panel">' +
        var nextContent;
                        '<div class="profile-avatar-stage">' +
        if (ref) {
                            '<img id="clbi-user-avatar" src="' + avatarSrc + '" onerror="this.onerror=null;this.src=\'/index.php?title=특수:Redirect/file/Pfp-default.png\';">' +
            var refEl = document.getElementById(ref);
                        '</div>' +
            nextContent = refEl ? refEl.innerHTML : '';
                        '<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 {
         } else {
             nextContent = tabs[nextIdx].dataset.content || '';
             userBox =
         }
                '<div class="clbi-right-box profile-card-box">' +
 
                    '<div id="clbi-user-avatar-wrap" class="profile-identity-panel">' +
         glitchOut(display, dir, function () {
                        '<div class="profile-avatar-stage">' +
             display.innerHTML = nextContent;
                            '<img id="clbi-user-avatar" src="/index.php?title=특수:Redirect/file/Pfp-default.png">' +
             glitchIn(display, dir, function () {
                        '</div>' +
                isAnimating = false;
                        '<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();
     }
     }


     function glitchOut(el, dir, cb) {
     initGreatWallBoxWhenReady();
        var duration = 160;
    removeMainPortalGuestbookPreview();
        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;
    if ($('#clbi-left-sidebar').length === 0) {
            var skew = dir * ease * 1.0;
var leftBillboardBox = '';
            var opacity = 1 - ease;
            var rgb = ease * 5;


             el.style.transform = 'translateX(' + tx + 'px) skewX(' + skew + 'deg)';
        try {
             el.style.opacity = opacity;
             leftBillboardBox = buildRightBillboardBox();
            el.style.filter =
        } catch (err) {
                'drop-shadow(' + (-rgb) + 'px 0 0 rgba(80,160,255,0.75)) ' +
             console.error('Left billboard build failed:', err);
                'drop-shadow(' + rgb + 'px 0 0 rgba(255,55,90,0.65)) ' +
             leftBillboardBox = '';
                'brightness(' + (1 + ease * 0.25) + ')';
 
             if (p < 1) {
                requestAnimationFrame(step);
            } else {
                el.style.opacity = '0';
                cb();
            }
         }
         }
        requestAnimationFrame(step);
    }


     function glitchIn(el, dir, cb) {
var leftSidebar =
         var duration = 200;
     '<div id="clbi-left-sidebar">' +
         var start = null;
         '<div class="clbi-left-box clbi-left-lang-box">' +
        var startX = -dir * 16;
            '<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">' +


        el.style.transform = 'translateX(' + startX + 'px) skewX(' + (-dir * 1.0) + 'deg)';
                '<div class="news-feed-title" id="clbi-left-news-changelog-title">CHANGELOG</div>' +
        el.style.opacity = '0';
                '<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>' +


        function step(ts) {
                '<div class="news-feed-title" id="clbi-left-news-recent-title">RECENT CHANGES</div>' +
            if (!start) start = ts;
                '<div class="news-left-recent-feed" id="clbi-left-recent-list">불러오는 중...</div>' +
            var p = Math.min((ts - start) / duration, 1);
            var ease = 1 - Math.pow(1 - p, 3);


            var tx = startX * (1 - ease);
                '<a class="news-fill-image-slot" id="clbi-left-news-fill-image" href="/index.php/특수:최근바뀜" aria-label="최근 바뀜으로 이동">' +
            var skew = -dir * 1.0 * (1 - ease);
                    '<div class="news-fill-image-frame">' +
            var opacity = ease;
                        '<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();">' +
            var rgb = (1 - ease) * 3;
                    '</div>' +
            var brightness = 1 + (1 - ease) * 0.35;
                '</a>' +


             el.style.transform = 'translateX(' + tx + 'px) skewX(' + skew + 'deg)';
             '</div>' +
            el.style.opacity = opacity;
        '</div>' +
            el.style.filter =
        leftBillboardBox +
                'drop-shadow(' + (-rgb) + 'px 0 0 rgba(80,160,255,0.65)) ' +
    '</div>';
                'drop-shadow(' + rgb + 'px 0 0 rgba(255,55,90,0.55)) ' +
                'brightness(' + brightness + ')';


            if (p < 1) {
        $('.content-wrapper').prepend(leftSidebar);
                requestAnimationFrame(step);
            } else {
                el.style.transform = '';
                el.style.opacity = '';
                el.style.filter = '';
                cb();
            }
        }
        requestAnimationFrame(step);
    }


if (document.readyState === 'loading') {
        renderSidebarLanguageBox();
    document.addEventListener('DOMContentLoaded', initDocTabs);
        loadRecentChangesList('#clbi-left-recent-list', 10);
} else {
        scheduleAdaptiveLeftRecentItems();
    initDocTabs();
        scheduleLeftBillboardAdaptive();
}
        scheduleClbiContentBottomGap();
        updateLeftSidebarNationsImage();
    }


if (typeof mw !== 'undefined' && mw.hook) {
    try {
     mw.hook('wikipage.content').add(function () {
        initRightBillboardCarousel();
         initDocTabs();
     } 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') {
  Doc Section Switch — 좌측 섹션 전환
        window.ProgressSystemWebUi.boot('initSidebars');
  ========================================= */
    }


$(document).on('click', '.doc-nav-item[data-section]', function () {
     $('#side-toc-box').remove();
     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;
     mw.loader.using(['mediawiki.api']).then(function() {
        setTimeout(function() {
            initNotifications();
            initProfile();
            moveTocToLeftSidebar();
        }, 300);


    $('.doc-nav-item[data-section]').removeClass('active');
        setTimeout(moveTocToLeftSidebar, 800);
     $('.doc-nav-item[data-section="' + name + '"]').addClass('active');
        setTimeout(moveTocToLeftSidebar, 1500);
     });
}


    if (name === 'text') {
$(function() {
        if (titleEl) titleEl.textContent = '개요';
    loadLangScript(function() {
         if (tabBar) $(tabBar).show();
         setTimeout(function() {
         var activeTab = tabBar ? tabBar.querySelector('.doc-tab.active') : null;
            initSidebars();
        if (!activeTab && tabBar) activeTab = tabBar.querySelector('.doc-tab');
         }, 100);
        if (activeTab) {
    });
            var ref = activeTab.dataset.ref;
});
            var refEl = ref ? document.getElementById(ref) : null;
 
            display.innerHTML = refEl ? refEl.innerHTML : (activeTab.dataset.content || '');
$(document).on('click.profileQuickPlaceholder', '#profile-quick-inventory, #profile-quick-achievements', function(e) {
        }
    e.preventDefault();
    } else {
    e.stopPropagation();
        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 : '';
    }
});
});


/* =========================================
// SPA 네비게이션
  CRT WebGL Renderer — cool-retro-term IBM DOS style
function shouldSkip(url) {
  ========================================= */
    return url.match(/action=edit|action=submit|action=history|action=delete|action=protect|action=purge|특수:로그인|특수:로그아웃|Special:UserLogin|Special:UserLogout|특수:사용자정보|특수:비밀번호바꾸기|uselang=/);
(function () {
}
     'use strict';
 
$(function() {
     if (window._spaInitialized) return;
    window._spaInitialized = true;


     function createNoiseTexture(gl) {
     function isInternal(url) {
         var size = 512;
         var a = document.createElement('a');
        var data = new Uint8Array(size * size * 4);
         a.href = url;
         var s = 12345;
         return a.hostname === window.location.hostname;
         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 = [
     function loadPage(url) {
        'attribute vec2 a_pos;',
         invalidateProfileRender();
        '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 = [
        fetch(url)
        'precision mediump float;',
            .then(function(res) {
        'uniform sampler2D u_tex;',
                return res.text();
        'uniform sampler2D u_noise;',
            })
        'uniform vec2 u_res;',
            .then(function(html) {
        'uniform vec2 u_imgSize;',
                var parser = new DOMParser();
        'uniform float u_time;',
                var doc = parser.parseFromString(html, 'text/html');
        'uniform vec2 u_noiseScale;',
        'varying vec2 v_uv;',


        'float sum2(vec2 v) { return v.x + v.y; }',
                var scripts = doc.querySelectorAll('script');
        'float min2(vec2 v) { return min(v.x, v.y); }',
                for (var i = 0; i < scripts.length; i++) {
        'float rgb2grey(vec3 v) { return dot(v, vec3(0.21, 0.72, 0.04)); }',
                    var src = scripts[i].textContent;


        'vec2 coverUV(vec2 uv) {',
                    if (src.indexOf('wgNamespaceNumber') !== -1) {
        '  float imgAR = u_imgSize.x / u_imgSize.y;',
                        var match = src.match(/"wgNamespaceNumber":(-?\d+)/);
        '  float scrAR = u_res.x / u_res.y;',
                        if (match) mw.config.set('wgNamespaceNumber', parseInt(match[1], 10));
        '  float scale = imgAR / scrAR;',
 
        '  float offsetY = (1.0 - scale) * 0.5;',
                        var matchTitle = src.match(/"wgTitle":"([^"]+)"/);
        '  return vec2(uv.x, uv.y * scale + offsetY);',
                        if (matchTitle) mw.config.set('wgTitle', matchTitle[1]);
        '}',


        'vec2 barrel(vec2 v, vec2 cc, float k) {',
                        var matchPage = src.match(/"wgPageName":"([^"]+)"/);
        '  float ar = u_res.x / u_res.y;',
                        if (matchPage) mw.config.set('wgPageName', matchPage[1]);
        '  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) {',
                        var matchArticle = src.match(/"wgArticleId":(\d+)/);
        ' return texture2D(u_noise, vec2(fract(t/2048.0), fract(t/1048576.0)));',
                        if (matchArticle) {
        '}',
                            mw.config.set('wgArticleId', parseInt(matchArticle[1], 10));
                        } else {
                            mw.config.set('wgArticleId', 0);
                        }


        'vec4 sampleScreenNoise(vec2 uv) {',
                        var matchIsMainPage = src.match(/"wgIsMainPage":(true|false)/);
        ' return texture2D(u_noise, u_noiseScale * uv);',
                        if (matchIsMainPage) {
        '}',
                            mw.config.set('wgIsMainPage', matchIsMainPage[1] === 'true');
                        } else {
                            mw.config.set('wgIsMainPage', false);
                        }


        'vec3 applyRgbShift(vec2 texUV, float shift) {',
                        var matchSpecial = src.match(/"wgCanonicalSpecialPageName":"([^"]+)"/);
        '  vec2 d = vec2(shift, 0.0);',
                        if (matchSpecial) {
        '  vec3 r = texture2D(u_tex, clamp(texUV + d, 0.0, 1.0)).rgb;',
                            mw.config.set('wgCanonicalSpecialPageName', matchSpecial[1]);
        '  vec3 c = texture2D(u_tex, texUV).rgb;',
                        } else {
        ' vec3 l = texture2D(u_tex, clamp(texUV - d, 0.0, 1.0)).rgb;',
                            mw.config.set('wgCanonicalSpecialPageName', false);
        '  return vec3(',
                        }
        '    l.r*0.10 + r.r*0.30 + c.r*0.60,',
                        break;
        '    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) {',
                var newContent = doc.querySelector('.liberty-content-main');
        '  vec2 px = 2.0 / u_res;',
                var newTitle = doc.querySelector('.mw-page-title-main');
        ' vec3 acc = vec3(0.0);',
                var newHead = doc.querySelector('title');
        ' acc += texture2D(u_tex, clamp(texUV + vec2( px.x,  0.0), 0.0, 1.0)).rgb;',
                var newHeader = doc.querySelector('.liberty-content-header');
        '  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) {',
                if (newContent) {
        ' float line = mod(uv.y * u_res.y, 2.0);',
                    $('#side-toc-box').remove();
        ' vec3 hi = ((1.0 + 0.30) - 0.2 * col) * col;',
                    $('.profile-card').remove();
        ' vec3 lo = ((1.0 - 0.30) + 0.1 * col) * col;',
                    $('.user-profile-portal').removeClass('user-profile-portal');
        ' return line < 1.0 ? lo : hi;',
                    $('.liberty-content-main').html(newContent.innerHTML);
        '}',
                    $('.profile-card').remove();
                    $('body').removeClass('page-loading');
                }


'vec3 applyRasterization(vec2 uv, vec3 col) {',
                if (newTitle) {
'  float t = u_time;',
                    $('.mw-page-title-main').html(newTitle.innerHTML);
'  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) {',
                if (newHead) {
'  float pos = fract(t * 0.2);',
                    document.title = newHead.textContent;
'  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) {',
                if (newHeader) {
        ' float randval = strength - noise.r;',
                    $('.liberty-content-header').html(newHeader.innerHTML);
        ' 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() {',
                if (typeof window.normalizeClbiShellDomOrder === 'function') window.normalizeClbiShellDomOrder();
        ' vec2 cc = vec2(0.5) - v_uv;',
                window.scrollTo(0, 0);
                mw.hook('wikipage.content').fire($('.liberty-content-main'));
                applyMainPageStyle();
                initClbiCustomDocumentScrollbars();
                initCategoryNavIfAvailable(document);


        ' float curvature = 0.18;',
                if (window.ProgressSystemWebUi && typeof window.ProgressSystemWebUi.handleSpaPageView === 'function') {
        ' vec2 uv = barrel(v_uv, cc, curvature);',
                    window.ProgressSystemWebUi.handleSpaPageView();
                } else if (window.ProgressSystemWebUi && typeof window.ProgressSystemWebUi.boot === 'function') {
                    window.ProgressSystemWebUi.boot('spa');
                }


        ' float inScreen = min2(step(vec2(0.0), uv) - step(vec2(1.0), uv));',
                $('#side-toc-box').remove();
        '  if (inScreen < 0.5) { gl_FragColor = vec4(0.0, 0.0, 0.0, 1.0); return; }',
                setTimeout(moveTocToLeftSidebar, 100);
                setTimeout(moveTocToLeftSidebar, 500);
                setTimeout(moveTocToLeftSidebar, 1200);


        ' vec2 texUV = clamp(coverUV(uv), 0.0, 1.0);',
                mw.loader.using(['mediawiki.api']).then(function() {
                    initProfile();
                    moveTocToLeftSidebar();
                });
            });
    }


        '  vec4 initNoise = sampleInitialNoise(u_time);',
// 목차 링크는 전용 처리
        '  vec4 screenNoise = sampleScreenNoise(uv);',
$(document).on('click', '#side-toc-box a, #toc a, .toc a', function(e) {
    var href = $(this).attr('href');
    if (!href || href.charAt(0) !== '#') return;


        '  texUV = applyHSync(texUV, initNoise, 0.006);',
    var rawId = href.slice(1);
        '  texUV = clamp(texUV, 0.0, 1.0);',
    if (!rawId) return;


        '  texUV += (vec2(screenNoise.b, screenNoise.a) - 0.5) * 0.0006;',
    var decodedId = rawId;
        '  texUV = clamp(texUV, 0.0, 1.0);',


         '  vec3 col = applyRgbShift(texUV, 0.003);',
    try {
         '  col += applyBloom(texUV, 0.22);',
         decodedId = decodeURIComponent(rawId);
    } catch (err) {
         decodedId = rawId;
    }


        '  vec2 bpx = 1.5 / u_res;',
    var target = document.getElementById(decodedId);
        '  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);',
    if (!target && window.CSS && CSS.escape) {
        ' col = applyRasterization(texUV, col);',
         target = document.querySelector('#' + CSS.escape(decodedId));
    }


        '  float glow = glowingLine(uv, u_time);',
    if (!target) return;
'  col += glow * 0.08 * vec3(0.85, 0.95, 1.0);',


        '  float dist = length(cc);',
    e.preventDefault();
        '  col += screenNoise.a * 0.07 * (1.0 - dist * 1.3);',
    e.stopPropagation();


        '  float grey = rgb2grey(col);',
    var scrollTarget = target.closest('h2, h3') || target;
        ' vec3 phosphor = vec3(0.75, 0.88, 1.0);',
        '  col = mix(col, grey * phosphor, 0.35);',


         ' vec2 vig = v_uv * (1.0 - v_uv);',
    scrollTarget.scrollIntoView({
         ' col *= pow(vig.x * vig.y * 15.0, 0.25);',
         behavior: 'auto',
         block: 'start'
    });


        ' col *= 1.0 + (initNoise.g - 0.5) * 0.06;',
    history.replaceState(null, '', '#' + rawId);
});


         '  col += vec3(0.012) * (1.0 - dist) * (1.0 - dist);',
    $(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;


         '  col = pow(clamp(col, 0.0, 1.0), vec3(0.90));',
         var href = $(this).attr('href');
        if (!href) return;


         '  gl_FragColor = vec4(col, 1.0);',
         // 목차 링크는 별도 핸들러에서 처리
        '}'
        if ($(this).closest('#side-toc-box, #toc, .toc').length) return;
    ].join('\n');


    function initCRTCanvas(screen, imgEl) {
        // 단순 해시 링크는 SPA 가로채기 제외
         var existing = screen.querySelector('.crt-webgl-canvas');
         if (href.startsWith('#')) return;
        if (existing) existing.remove();


         var canvas = document.createElement('canvas');
         var link = document.createElement('a');
         canvas.className = 'crt-webgl-canvas';
         link.href = href;
        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');
         var samePath = decodeURIComponent(link.pathname) === decodeURIComponent(window.location.pathname);
        if (!gl) return;
        var sameSearch = (link.search || '') === (window.location.search || '');


         function compile(type, src) {
         if (link.hash && samePath && sameSearch) return;
            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();
         var currentBase = window.location.href.split('#')[0];
        gl.attachShader(prog, compile(gl.VERTEX_SHADER, VERT));
         var targetBase = link.href.split('#')[0];
        gl.attachShader(prog, compile(gl.FRAGMENT_SHADER, FRAG));
         gl.linkProgram(prog);
        gl.useProgram(prog);


         var buf = gl.createBuffer();
         if (link.hash && currentBase === targetBase) return;
        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');
         if (!isInternal(href)) return;
         var uNoise  = gl.getUniformLocation(prog, 'u_noise');
        if (shouldSkip(href)) return;
         var uRes    = gl.getUniformLocation(prog, 'u_res');
 
         var uImgSize = gl.getUniformLocation(prog, 'u_imgSize');
         e.preventDefault();
         var uTime    = gl.getUniformLocation(prog, 'u_time');
         playStaticSound();
         var uNoiseSc = gl.getUniformLocation(prog, 'u_noiseScale');
         $('body').addClass('page-loading');
         history.pushState(null, '', href);
         loadPage(href);
    });
 
    window.addEventListener('popstate', function() {
        loadPage(window.location.href);
    });
});


        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;
/* ========== CLBI Custom Document Scrollbar ========== */
        function uploadImg() {
function isGeneralDocumentView() {
            if (!imgEl || !imgEl.complete || !imgEl.naturalWidth) return;
    var body = document.body;
            try {
    if (!body) return false;
                gl.activeTexture(gl.TEXTURE0);
 
                gl.bindTexture(gl.TEXTURE_2D, imgTex);
    return body.classList.contains('action-view') &&
                gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, imgEl);
        !body.classList.contains('clbi-main-page') &&
                texReady = true;
        !body.classList.contains('clbi-system-doc-page') &&
            } catch(e) { console.error('[CRT] tex:', e); }
        !body.classList.contains('backend-system-page') &&
        }
        !body.classList.contains('user-profile-page') &&
        !body.classList.contains('user-profile-settings-page');
}


        var lastW = 0, lastH = 0;
function getClbiDocumentScrollTargets() {
        function resize() {
    if (!isGeneralDocumentView()) return [];
            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;
    return Array.prototype.slice.call(document.querySelectorAll(
         var t0 = performance.now();
         '.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 render() {
function getClbiOuterWellForScroll(scrollEl) {
            raf = requestAnimationFrame(render);
    var main = scrollEl ? scrollEl.closest('.liberty-content-main') : null;
            if (!texReady) { uploadImg(); return; }
    var children;
            resize();
    var i;
            var t = (performance.now() - t0) / 1000;
    var child;
            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(); }
    if (!main) return null;
        else { imgEl.addEventListener('load', uploadImg); }


        render();
    children = Array.prototype.slice.call(main.children || []);
         screen._crtCleanup = function () { cancelAnimationFrame(raf); };
    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;
        }
     }
     }


     function initAllCRTScreens(root) {
     return scrollEl.parentElement || null;
        var scope = root && root.querySelectorAll ? root : document;
}
        scope.querySelectorAll('.crt-page-monitor-screen').forEach(function (screen) {
 
            if (screen.getAttribute('data-crt-webgl') === '1') return;
function buildClbiCustomScrollbar(well, scrollEl) {
            screen.setAttribute('data-crt-webgl', '1');
    var bar = well.querySelector(':scope > .clbi-custom-scrollbar');
             var frame = screen.closest('.crt-page-monitor-frame');
    var up;
             if (!frame) return;
    var track;
            var imgEl = frame.querySelector('.crt-page-monitor-slice-img, .crt-page-monitor-image-base img');
    var thumb;
             if (imgEl && imgEl.complete && imgEl.naturalWidth) {
    var down;
                initCRTCanvas(screen, imgEl);
 
            } else if (imgEl) {
    if (!bar) {
                imgEl.addEventListener('load', function () { initCRTCanvas(screen, imgEl); });
        bar = document.createElement('div');
            } else {
        bar.className = 'clbi-custom-scrollbar';
                var obs = new MutationObserver(function () {
        bar.setAttribute('aria-hidden', 'true');
                    var img = frame.querySelector('.crt-page-monitor-slice-img');
        bar.innerHTML =
                    if (!img) return;
             '<div class="clbi-custom-scroll-arrow clbi-custom-scroll-arrow-up" data-scroll-arrow="up"></div>' +
                    obs.disconnect();
             '<div class="clbi-custom-scroll-track"><div class="clbi-custom-scroll-thumb"></div></div>' +
                    if (img.complete && img.naturalWidth) {
             '<div class="clbi-custom-scroll-arrow clbi-custom-scroll-arrow-down" data-scroll-arrow="down"></div>';
                        initCRTCanvas(screen, img);
        well.appendChild(bar);
                    } else {
                        img.addEventListener('load', function () { initCRTCanvas(screen, img); });
                    }
                });
                obs.observe(frame, { childList: true, subtree: true });
            }
        });
     }
     }


     $(function () { initAllCRTScreens(document); });
     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 (typeof mw !== 'undefined' && mw.hook) {
     if (up && !up.__clbiBound) {
         mw.hook('wikipage.content').add(function ($c) {
         up.__clbiBound = true;
             document.querySelectorAll('.crt-page-monitor-screen').forEach(function (s) {
        up.addEventListener('mousedown', function (e) {
                if (s._crtCleanup) s._crtCleanup();
             e.preventDefault();
                s.removeAttribute('data-crt-webgl');
            e.stopPropagation();
             });
            if (bar.__clbiScrollTarget) bar.__clbiScrollTarget.scrollTop -= 48;
            initAllCRTScreens($c && $c[0] ? $c[0] : document);
             updateClbiCustomScrollbar(bar);
         });
         });
     }
     }
})();


/* =========================================
    if (down && !down.__clbiBound) {
Progress System UI
        down.__clbiBound = true;
MediaWiki:Common.js controlled frontend
        down.addEventListener('mousedown', function (e) {
========================================= */
            e.preventDefault();
(function (mw, $) {
            e.stopPropagation();
     'use strict';
            if (bar.__clbiScrollTarget) bar.__clbiScrollTarget.scrollTop += 48;
            updateClbiCustomScrollbar(bar);
        });
     }


     if (window.ProgressSystemWebUiInitialized) return;
     if (track && !track.__clbiBound) {
    window.ProgressSystemWebUiInitialized = true;
        track.__clbiBound = true;
        track.addEventListener('mousedown', function (e) {
            var rect;
            var thumbRect;
            var target;
            var direction;


    var api = null;
            if (e.target === thumb) return;
 
             e.preventDefault();
    function withApi(done, fail) {
             e.stopPropagation();
        if (api) {
             done(api);
             return;
        }


        if (!mw.loader || typeof mw.loader.using !== 'function') {
            target = bar.__clbiScrollTarget;
             if (typeof fail === 'function') fail();
             if (!target) return;
            return;
        }


        mw.loader.using(['mediawiki.api']).then(function () {
            rect = track.getBoundingClientRect();
             api = new mw.Api();
            thumbRect = thumb.getBoundingClientRect();
             done(api);
             direction = e.clientY < thumbRect.top ? -1 : 1;
        }, function () {
             target.scrollTop += direction * Math.max(60, Math.floor(target.clientHeight * 0.82));
             if (typeof fail === 'function') fail();
             updateClbiCustomScrollbar(bar);
         });
         });
     }
     }


     var inFlightPageIds = new Set();
     if (thumb && !thumb.__clbiBound) {
    var handledPageIds = new Set();
        thumb.__clbiBound = true;
    var notificationQueue = [];
        thumb.addEventListener('mousedown', function (e) {
    var notificationActive = false;
            var target = bar.__clbiScrollTarget;
    var summaryRequested = false;
            var startY;
    var currentSummary = null;
            var startScroll;
    var pendingSummary = null;
            var maxScroll;
    var pendingOptions = null;
            var maxThumbTop;
    var visibilityBound = false;
            var trackHeight;
    var barTimerA = null;
            var thumbHeight;
    var barTimerB = null;
 
    var barTimerC = null;
            if (!target) return;
 
            e.preventDefault();
            e.stopPropagation();


    function isLoggedIn() {
            startY = e.clientY;
        return !!mw.config.get('wgUserName');
            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);


    function getPageId() {
            bar.classList.add('is-dragging');
        var id = parseInt(mw.config.get('wgArticleId') || 0, 10);
        return Number.isFinite(id) ? id : 0;
    }


    function isRewardableClientSide() {
            function onMove(moveEvent) {
        if (!isLoggedIn()) return false;
                var dy = moveEvent.clientY - startY;
        if (parseInt(mw.config.get('wgNamespaceNumber'), 10) !== 0) return false;
                target.scrollTop = startScroll + (dy / maxThumbTop) * maxScroll;
        if (mw.config.get('wgIsMainPage')) return false;
                updateClbiCustomScrollbar(bar);
        if (getPageId() <= 0) return false;
                moveEvent.preventDefault();
         return true;
            }
 
            function onUp() {
                bar.classList.remove('is-dragging');
                document.removeEventListener('mousemove', onMove);
                document.removeEventListener('mouseup', onUp);
            }
 
            document.addEventListener('mousemove', onMove);
            document.addEventListener('mouseup', onUp);
         });
     }
     }


     function getPanelHtml() {
     if (!scrollEl.__clbiCustomScrollbarBound) {
         return '' +
         scrollEl.__clbiCustomScrollbarBound = true;
            '<div id="progress-panel" class="profile-progress-block is-syncing" aria-live="polite" data-progress-state="syncing">' +
        scrollEl.addEventListener('scroll', function () {
                '<div class="progress-title-row" hidden></div>' +
            if (scrollEl.__clbiCustomScrollbar) {
                '<div class="progress-level-row">' +
                 updateClbiCustomScrollbar(scrollEl.__clbiCustomScrollbar);
                    '<span class="progress-level-label">SYNC</span>' +
            }
                    '<span class="progress-total-xp">— XP</span>' +
        }, { passive: true });
                '</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() {
     scrollEl.__clbiCustomScrollbar = bar;
        return '<div id="profile-progress-divider" class="profile-divider" aria-hidden="true"></div>';
    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;


     function setPanelSync($panel) {
     if (!bar || !scrollEl || !track || !thumb) return;
        if (!$panel || !$panel.length) return;


        $panel.addClass('is-syncing').removeClass('is-max-level').attr('data-progress-state', 'syncing');
    maxScroll = scrollEl.scrollHeight - scrollEl.clientHeight;
        $panel.find('.progress-title-row').text('').prop('hidden', true);
    if (maxScroll <= 1) {
         $panel.find('.progress-level-label').text('SYNC');
         bar.classList.add('is-hidden');
        $panel.find('.progress-total-xp').text('— XP');
         return;
         $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) {
     bar.classList.remove('is-hidden');
        var $right = $('#clbi-right-sidebar');
        if (!$right.length) return false;


        var $userBox = $right.children('.clbi-right-box').first();
    trackHeight = Math.max(1, track.clientHeight || 1);
        if (!$userBox.length) return false;
    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 $buttonArea = $userBox.children('.clbi-right-content').first();
    thumb.style.height = thumbHeight + 'px';
        var $oldFallback = $panel.closest('.progress-panel-fallback');
    thumb.style.transform = 'translateY(' + top + 'px)';
}


        if ($buttonArea.length) {
function initClbiCustomDocumentScrollbars() {
            var $divider = $('#profile-progress-divider');
    var existing = Array.prototype.slice.call(document.querySelectorAll('.clbi-custom-scrollbar'));
    var targets = getClbiDocumentScrollTargets();
    var liveBars = [];


            $panel.insertBefore($buttonArea);
    if (!targets.length) {
        existing.forEach(function (bar) { bar.remove(); });
        return;
    }


            if (!$divider.length) {
    targets.forEach(function (scrollEl) {
                $divider = $(getDividerHtml());
        var well = getClbiOuterWellForScroll(scrollEl);
            }
        var bar;


            $divider.insertAfter($panel);
        if (!well) return;
         } else {
         bar = buildClbiCustomScrollbar(well, scrollEl);
            $('#profile-progress-divider').remove();
        liveBars.push(bar);
            $userBox.append($panel);
    });
        }


         if ($oldFallback.length && !$oldFallback.find('#progress-panel').length) {
    existing.forEach(function (bar) {
            $oldFallback.remove();
         if (liveBars.indexOf(bar) === -1) bar.remove();
        }
    });


         return true;
    window.requestAnimationFrame(function () {
     }
         liveBars.forEach(updateClbiCustomScrollbar);
     });


     function ensurePanel() {
     setTimeout(function () {
         if (!isLoggedIn()) return $();
         liveBars.forEach(updateClbiCustomScrollbar);
    }, 120);
}


        var $right = $('#clbi-right-sidebar');
if (!window.__clbiCustomScrollbarResizeBound) {
         if (!$right.length) return $();
    window.__clbiCustomScrollbarResizeBound = true;
    window.addEventListener('resize', function () {
         setTimeout(initClbiCustomDocumentScrollbars, 60);
    });
}


        var $panel = $('#progress-panel');


        if (!$panel.length) {
// 시간 계산 함수
            $panel = $(getPanelHtml());
function timeAgo(timestamp) {
            if (!placePanel($panel)) return $();
    var now = new Date();
            setPanelSync($panel);
    var date = new Date(timestamp);
        } else {
    var diff = Math.floor((now - date) / 1000);
            $panel.addClass('profile-progress-block');
 
            placePanel($panel);
    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) + '일 전';
}


            if (!currentSummary && $panel.attr('data-progress-state') !== 'syncing') {
// 펼접 토글
                setPanelSync($panel);
// 펼접 토글
            }
function getFoldTexts() {
        }
    var lang = getCurrentLang();
    return (window.LANG && window.LANG[lang])
        ? window.LANG[lang]
        : (window.LANG ? window.LANG.ko : { expand: '펼치기', collapse: '접기' });
}


         return $('#progress-panel');
function refreshOpenAncestors($start) {
    }
    $start.parents('[id^="collapsible"]').each(function () {
         var $parent = $(this);
        if (!$parent.hasClass('folding-open')) return;


    function clampPercent(value) {
        // 이미 fully open 상태면 굳이 다시 잠그지 않음
         return Math.max(0, Math.min(100, value || 0));
         if ($parent.data('fold-state') === 'open') {
    }
            return;
        }


    function hasXpNotification(items) {
         $parent.css('max-height', this.scrollHeight + 'px');
         if (!items || !items.length) return false;
    });
        return items.some(function (item) {
}
            return item && item.type === 'xp' && parseInt(item.amount || 0, 10) > 0;
        });
    }


     function clearBarTimers() {
function bindInnerResizeUpdates($target) {
         [barTimerA, barTimerB, barTimerC].forEach(function (timer) {
     // 이미지 늦게 로드될 때 높이 갱신
             if (timer) clearTimeout(timer);
    $target.find('img').off('.foldimg').on('load.foldimg', function () {
        });
         if ($target.hasClass('folding-open')) {
         barTimerA = null;
             if ($target.data('fold-state') !== 'open') {
        barTimerB = null;
                $target.css('max-height', $target[0].scrollHeight + 'px');
        barTimerC = null;
            }
    }
            refreshOpenAncestors($target);
         }
    });
}
 
function openFold($target, $btn) {
    var t = getFoldTexts();


     function setBarInstant($fill, $gain, percent) {
     $target.data('fold-state', 'opening');
        clearBarTimers();
    $target.addClass('folding-open');
        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) {
     // 열린 뒤 자연 확장 가능하게 만들기 위해 먼저 px로 열기
        clearBarTimers();
    $target.css('max-height', '0px');
    $target[0].offsetHeight;
    $target.css('max-height', $target[0].scrollHeight + 'px');


        fromPercent = clampPercent(fromPercent);
    $btn.text(t.collapse);
        toPercent = clampPercent(toPercent);


        $fill.css({ transition: 'none', width: fromPercent + '%' });
    bindInnerResizeUpdates($target);


        if (levelChanged) {
    // 바깥 펼접 즉시 갱신
            var firstDelta = Math.max(0, 100 - fromPercent);
    refreshOpenAncestors($target);


            $gain.css({
    // 전환 끝나면 none으로 풀어서 중첩 펼접/동적 내용 증가를 자연스럽게 허용
                transition: 'none',
    $target.off('transitionend.foldopen').on('transitionend.foldopen', function (e) {
                opacity: firstDelta > 0 ? 1 : 0,
        if (e.target !== this) return;
                left: fromPercent + '%',
        if (!$target.hasClass('folding-open')) return;
                width: firstDelta + '%'
            });


            if ($fill[0]) $fill[0].offsetHeight;
        $target.css('max-height', 'none');
        $target.data('fold-state', 'open');


            barTimerA = setTimeout(function () {
        refreshOpenAncestors($target);
                $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%' });
    requestAnimationFrame(function () {
                $gain.css({ transition: 'none', opacity: toPercent > 0 ? 1 : 0, left: '0%', width: toPercent + '%' });
        if ($target.hasClass('folding-open') && $target.data('fold-state') !== 'open') {
            $target.css('max-height', $target[0].scrollHeight + 'px');
            refreshOpenAncestors($target);
        }
    });


                if ($fill[0]) $fill[0].offsetHeight;
    setTimeout(function () {
 
        if ($target.hasClass('folding-open') && $target.data('fold-state') !== 'open') {
                $fill.css({
            $target.css('max-height', $target[0].scrollHeight + 'px');
                    transition: 'width 460ms cubic-bezier(0.22, 0.7, 0.18, 1)',
             refreshOpenAncestors($target);
                    width: toPercent + '%'
                });
            }, 860);
 
            barTimerC = setTimeout(function () {
                $gain.css({ transition: 'opacity 180ms ease', opacity: 0 });
             }, 1380);
 
            return;
         }
         }
    }, 80);


         var delta = Math.max(0, toPercent - fromPercent);
    setTimeout(function () {
 
         if ($target.hasClass('folding-open') && $target.data('fold-state') !== 'open') {
        if (delta <= 0.15) {
             $target.css('max-height', $target[0].scrollHeight + 'px');
             setBarInstant($fill, $gain, toPercent);
             refreshOpenAncestors($target);
             return;
         }
         }
    }, 220);
}


        $gain.css({
function closeFold($target, $btn) {
            transition: 'none',
    var t = getFoldTexts();
            opacity: 1,
            left: fromPercent + '%',
            width: delta + '%'
        });


         if ($fill[0]) $fill[0].offsetHeight;
    // 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');
    }


        barTimerA = setTimeout(function () {
    $target.data('fold-state', 'closing');
            $fill.css({
    $target[0].offsetHeight;
                transition: 'width 560ms cubic-bezier(0.22, 0.7, 0.18, 1)',
    $target.css('max-height', '0px');
                width: toPercent + '%'
    $target.removeClass('folding-open');
            });
        }, 260);


        barTimerB = setTimeout(function () {
    $btn.text(t.expand);
            $gain.css({ transition: 'opacity 180ms ease', opacity: 0 });
        }, 940);
    }


     function updatePanel(summary, options) {
     refreshOpenAncestors($target);
        if (!summary) return;


        options = options || {};
    setTimeout(function () {
 
         refreshOpenAncestors($target);
         var $panel = ensurePanel();
         $target.data('fold-state', 'closed');
         if (!$panel.length) {
    }, 250);
            pendingSummary = $.extend({}, summary);
}
            pendingOptions = $.extend({}, options);
            return;
        }


        var level = summary.level || 1;
$(function () {
         var totalXp = summary.totalXp || 0;
    $(document)
        var xpIntoLevel = summary.xpIntoLevel || 0;
         .off('click.clbiToggle')
         var xpForNext = summary.xpForNextLevel || 1;
         .on('click.clbiToggle', '.toggleBtn', function () {
        var percent = clampPercent(summary.progressPercent);
            var $btn = $(this);
        var isMaxLevel = !!summary.isMaxLevel;
            var targetId = $btn.data('target');
        var dailyXp = summary.dailyXp || 0;
            var $target = $('#' + targetId);
        var discoveries = summary.discoveryCount || 0;
            if (!$target.length) return;
        var title = summary.equippedTitle || summary.title || '';


        $panel.removeClass('is-syncing').toggleClass('is-max-level', isMaxLevel).attr('data-progress-state', 'ready');
            var scrollY = window.scrollY;
        $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 ($target.hasClass('folding-open')) {
        if (title) {
                closeFold($target, $btn);
            $title.text(title).prop('hidden', false);
            } else {
        } else {
                openFold($target, $btn);
            $title.text('').prop('hidden', true);
            }
        }


         var $fill = $panel.find('.progress-xp-fill');
            window.scrollTo(0, scrollY);
        var $gain = $panel.find('.progress-xp-gain');
         });
        var animate = !!options.animateGain && currentSummary && totalXp > (currentSummary.totalXp || 0);
});
 
// ========== 프로필 시스템 ==========
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 === '사용자정보';


        if (animate) {
    $('body').toggleClass('user-profile-page', ns === 2);
            animateGain(
    $('body').toggleClass('user-profile-settings-page', isProfileSettings);
                $fill,
                $gain,
                clampPercent(currentSummary.progressPercent),
                percent,
                level !== (currentSummary.level || 1)
            );
        } else {
            setBarInstant($fill, $gain, percent);
        }


         currentSummary = $.extend({}, summary);
    if (ns === 2) {
         pendingSummary = null;
         var profileUser = title.split('/')[0];
        pendingOptions = null;
         renderProfile(profileUser, token);
     }
     }


     function requestSummary() {
     if (isProfileSettings) {
         if (!isLoggedIn()) return;
         initUserProfilePage();
        if (summaryRequested) return;
    }
}


        summaryRequested = true;
function renderProfile(username, token) {
 
    var api = new mw.Api();
        withApi(function (api) {
    api.get({
            api.get({
        action: 'query',
                action: 'progress_summary',
        list: 'users',
                format: 'json',
        ususers: username,
                formatversion: 2
        usprop: 'editcount'
            }).then(function (data) {
    }).then(function(data) {
                var payload = data && data.progress_summary;
        if (token !== PROFILE_RENDER_TOKEN) return;
                if (payload && payload.available && payload.summary) {
        if (mw.config.get('wgNamespaceNumber') !== 2) return;
                    updatePanel(payload.summary, { animateGain: false });
                }
            }).always(function () {
                summaryRequested = false;
            });
        }, function () {
            summaryRequested = false;
        });
    }


    function queueNotifications(items) {
        var currentTitle = String(mw.config.get('wgTitle') || '').split('/')[0];
         if (!items || !items.length) return;
         if (currentTitle !== username) return;


         items.forEach(function (item) {
         var user = data.query.users[0];
            if (!item) return;
        var contentEl = document.getElementById('mw-content-text');
            notificationQueue.push(item);
         if (!contentEl) return;
         });


         showNextNotification();
         var pageContent = contentEl.querySelector('.mw-parser-output') || contentEl;
     }
        injectProfileCard(username, user, pageContent);
     });
}


    function notificationText(item) {
function injectProfileCard(username, userData, container) {
        if (item.type === 'xp') {
    var isOwnPage = mw.config.get('wgUserName') === username;
            return '+' + (item.amount || 0) + ' XP · ' + (item.label || '문서 열람');
    var editCount = (userData && userData.editcount) ? userData.editcount : 0;
        }


         if (item.type === 'achievement') {
    function escapeHtml(value) {
             var xp = item.amount ? ' · +' + item.amount + ' XP' : '';
         return String(value == null ? '' : value)
             return '업적 달성 · ' + (item.label || '새 업적') + xp;
             .replace(/&/g, '&amp;')
        }
            .replace(/</g, '&lt;')
 
             .replace(/>/g, '&gt;')
        if (item.type === 'level') {
            .replace(/"/g, '&quot;')
             return item.label || '레벨 상승';
             .replace(/'/g, '&#039;');
        }
 
        return item.label || '보상 획득';
     }
     }


     function showNextNotification() {
     container.classList.add('user-profile-portal');
        if (notificationActive) return;
        if (!notificationQueue.length) return;


        notificationActive = true;
    var safeUsername = escapeHtml(username);
        var item = notificationQueue.shift();
    var avatarSrc = '/index.php?title=특수:Redirect/file/Pfp-' + encodeURIComponent(username) + '.png&width=220';
        var $root = $('#progress-toast-root');
    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 (!$root.length) {
    var progressHtml = isOwnPage
             $('body').append('<div id="progress-toast-root"></div>');
         ? '<div class="profile-page-progress is-syncing" data-profile-progress>' +
            $root = $('#progress-toast-root');
            '<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 $toast = $('<div class="progress-toast"></div>');
    var card = document.createElement('div');
         $toast.text(notificationText(item));
    card.className = 'profile-card profile-page-console';
        $root.append($toast);
    card.innerHTML =
 
        '<div class="profile-card-titlebar">' +
        requestAnimationFrame(function () {
            '<span>USER PROFILE</span>' +
            $toast.addClass('is-visible');
            '<span>OFFICIAL ARCHIVE</span>' +
        });
        '</div>' +
 
         '<div class="profile-card-body">' +
        setTimeout(function () {
            '<div class="profile-identity-row">' +
             $toast.removeClass('is-visible');
                '<div class="profile-avatar-bay">' +
            setTimeout(function () {
                    '<img src="' + avatarSrc + '" onerror="this.onerror=null;this.src=\'' + fallbackSrc + '\';" alt="' + safeUsername + '">' +
                 $toast.remove();
                '</div>' +
                 notificationActive = false;
                '<div class="profile-info-panel">' +
                showNextNotification();
                    '<div class="profile-nameplate">' +
            }, 220);
                        '<h2 class="profile-username">' + safeUsername + '</h2>' +
        }, 2600);
                        editBtn +
    }
                    '</div>' +
 
                    '<div class="profile-name" data-field="name"></div>' +
    function applyPendingSummaryIfPossible() {
                    '<div class="profile-role" data-field="role"></div>' +
        if (!pendingSummary) return;
                    '<div class="profile-discord" data-field="discord"></div>' +
        updatePanel(pendingSummary, pendingOptions || { animateGain: false });
                '</div>' +
    }
            '</div>' +
 
             '<div class="profile-lower-grid">' +
    function handlePageView() {
                '<div class="profile-bio-panel">' +
        ensurePanel();
                    '<div class="profile-section-title">BIOGRAPHY</div>' +
         applyPendingSummaryIfPossible();
                    '<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>';


        if (!isRewardableClientSide()) {
    $('.profile-card').remove();
            requestSummary();
    container.insertBefore(card, container.firstChild);
            return;
    loadProfileFields(username, card);
        }
    loadProfileContributionPages(username, card);
    updateProfilePageEnvironment(card, null);


        var pageId = getPageId();
    if (isOwnPage) {
        if (handledPageIds.has(pageId)) {
        loadProfileProgressForUserPage(card);
            requestSummary();
    }
            return;
}
        }


        if (inFlightPageIds.has(pageId)) {
function getProfileLanguageLabel() {
            requestSummary();
    var lang = getCurrentLang();
            return;
    return SIDEBAR_LANGUAGE_LABELS[lang] || (lang ? lang.toUpperCase() : '—');
        }
}


        inFlightPageIds.add(pageId);
function updateProfilePageEnvironment(card, summary) {
    if (!card) return;


        withApi(function (api) {
    var timezone = summary && summary.timezone ? summary.timezone : 'UTC';
            api.postWithToken('csrf', {
    var timeEl = card.querySelector('[data-profile-time]');
                action: 'progress_view',
    var langEl = card.querySelector('[data-profile-language]');
                format: 'json',
                formatversion: 2,
                errorformat: 'plaintext',
                pageid: pageId
            }).then(function (data) {
                var payload = data && data.progress_view;
                if (!payload) return;


                 handledPageIds.add(pageId);
    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());
        }
    }


                var animate = hasXpNotification(payload.notifications);
    if (langEl) {
        langEl.textContent = getProfileLanguageLabel();
    }
}


                if (payload.summary) {
function loadProfileContributionPages(username, card) {
                    updatePanel(payload.summary, { animateGain: animate });
    if (!username || !card) return;
                }
    if (!mw.loader || typeof mw.loader.using !== 'function') return;


                if (payload.notifications && payload.notifications.length) {
    var valueEl = card.querySelector('[data-contrib-pages-value]');
                    queueNotifications(payload.notifications);
    if (valueEl) valueEl.textContent = 'SYNC';
                }
            }).catch(function () {
                requestSummary();
            }).always(function () {
                inFlightPageIds.delete(pageId);
            });
        }, function () {
            inFlightPageIds.delete(pageId);
            requestSummary();
        });
    }


     function bindVisibilitySync() {
     mw.loader.using(['mediawiki.api']).then(function () {
         if (visibilityBound) return;
         var api = new mw.Api();
         visibilityBound = true;
         var pages = Object.create(null);
        var cont = {};
        var guard = 0;


         document.addEventListener('visibilitychange', function () {
         function requestNext() {
             if (document.visibilityState === 'visible') {
             guard++;
                requestSummary();
            }
        });
    }


    function bootProgressSystem(reason) {
            var params = Object.assign({
        ensurePanel();
                action: 'query',
        applyPendingSummaryIfPossible();
                list: 'usercontribs',
 
                ucuser: username,
        if (isRewardableClientSide()) {
                ucnamespace: 0,
            handlePageView();
                ucprop: 'title',
        } else {
                uclimit: 'max',
             requestSummary();
                format: 'json',
        }
                formatversion: 2
    }
             }, cont);


    function handleSpaPageView() {
            return api.get(params).then(function (data) {
        ensurePanel();
                var rows = data && data.query && data.query.usercontribs ? data.query.usercontribs : [];
        applyPendingSummaryIfPossible();


        requestAnimationFrame(function () {
                rows.forEach(function (row) {
            setTimeout(function () {
                    if (row && row.title) pages[row.title] = true;
                 handlePageView();
                 });
            }, 80);
        });
    }


    function applySummary(summary, options) {
                if (data && data.continue && data.continue.uccontinue && guard < 40) {
        updatePanel(summary, options || { animateGain: false });
                    cont = data.continue;
    }
                    return requestNext();
                }


    window.ProgressSystemWebUi = {
                if (valueEl) valueEl.textContent = Object.keys(pages).length;
        boot: bootProgressSystem,
            });
        requestSummary: requestSummary,
         }
        applySummary: applySummary,
        handlePageView: handlePageView,
        handleSpaPageView: handleSpaPageView,
         ensurePanel: ensurePanel
    };


    $(function () {
        requestNext().fail(function () {
        bindVisibilitySync();
            if (valueEl) valueEl.textContent = '—';
         bootProgressSystem('documentReady');
         });
     });
     });
}
function loadProfileProgressForUserPage(card) {
    if (!mw.config.get('wgUserName')) return;
    if (!mw.loader || typeof mw.loader.using !== 'function') return;


     mw.hook('wikipage.content').add(function () {
     mw.loader.using(['mediawiki.api']).then(function () {
         ensurePanel();
        var api = new mw.Api();
         applyPendingSummaryIfPossible();
        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 t0 = performance.now();
 
        function render() {
            raf = requestAnimationFrame(render);
            if (!texReady) { uploadImg(); return; }
            resize();
            var t = (performance.now() - t0) / 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);
})(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);
/* =========================================
  Unified Shortcuts loader
  ========================================= */
(function () {
    'use strict';
    if (!window.mw || !mw.loader) return;
    loadClbiRawScript('MediaWiki:1950_Nation_Link_Editor.js');
    loadClbiRawScript('MediaWiki:Nation_List_Manager.js');
    loadClbiRawScript('MediaWiki:Shortcuts.js');
})();

2026년 7월 6일 (월) 01:54 판

mw.loader.load('https://cdnjs.cloudflare.com/ajax/libs/cropperjs/1.5.13/cropper.min.js');

/* CLBI 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 = window.CLBI_RAW_LOAD_BUST || String(Date.now());
function loadClbiRawScript(title) {
    mw.loader.load('/index.php?title=' + encodeURIComponent(title) + '&action=raw&ctype=text/javascript&v=' + encodeURIComponent(window.CLBI_RAW_LOAD_BUST));
}

loadClbiRawScript('MediaWiki:DevTools.js');
loadClbiRawScript('MediaWiki:CategoryNav.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();

        if (window.mw && mw.util && typeof mw.util.getUrl === 'function') {
            return mw.util.getUrl(title, {
                action: 'raw',
                ctype: 'text/plain',
                _: String(Date.now())
            });
        }

        return '/index.php?title=' + encodeURIComponent(title) + '&action=raw&ctype=text/plain&_=' + Date.now();
    }

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

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

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


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

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

    var gl = canvas.getContext('webgl', {
        alpha: false,
        antialias: false,
        depth: false,
        stencil: false,
        preserveDrawingBuffer: false,
        powerPreference: 'high-performance'
    }) || canvas.getContext('experimental-webgl');

    if (!gl) {
        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) {}

    var lastFrame = 0;
    var frameInterval = prefersReducedMotion ? 1000 : 66;
    var startTime = performance.now();

    function render(now) {
        requestAnimationFrame(render);
        if (document.hidden) return;
        if (now - lastFrame < frameInterval) return;

        lastFrame = now;
        resize();

        gl.clearColor(0, 0, 0, 1);
        gl.clear(gl.COLOR_BUFFER_BIT);
        gl.uniform2f(resolutionLoc, canvas.width, canvas.height);
        gl.uniform1f(timeLoc, now - startTime);
        gl.drawArrays(gl.TRIANGLES, 0, 6);
    }

    resize();
    requestAnimationFrame(render);
}

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 === 'Nations & Factions' ||
        !!document.querySelector('.clbi-nations-panel-stack, .clbi-nations-globe-window, .clbi-nations-tabpanel');
}

function isClbiShortcutEditableTarget(target) {
    var el = target;

    while (el && el !== document.body) {
        if (el.isContentEditable) return true;

        if (el.tagName) {
            var tag = String(el.tagName).toLowerCase();
            if (tag === 'input' || tag === 'textarea' || tag === 'select' || tag === 'button') return true;
        }

        if (el.classList && (
            el.classList.contains('oo-ui-inputWidget-input') ||
            el.classList.contains('ve-ce-documentNode') ||
            el.classList.contains('CodeMirror')
        )) {
            return true;
        }

        el = el.parentNode;
    }

    return false;
}

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 isClbiNationLinkEditorShortcutContext() {
    return !!document.querySelector('.nation-link-editor');
}

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

function getClbiBottomShortcutItems() {
    if (isClbiWikiEditShortcutContext()) {
        return [
            { key: 'Ctrl+S', label: '변경사항 저장' }
        ];
    }

    if (isClbiNationLinkEditorShortcutContext()) {
        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-title">SHORTCUT GUIDE</div>';
    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) {
        validateClbiNationsPointerHover(tabpanel, false);
    });
}

function activateClbiNationsContinent(tabpanel, targetContinent) {
    var tabs;
    var panels;

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

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

    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;

    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 moveClbiNationsEra(direction) {
    var side = direction < 0 ? 'left' : 'right';
    var selector = '.clbi-nations-era-side-slot-' + side +
        ' .mw-collapsible:not(.mw-collapsed) .clbi-nations-era-side-button-' + side;
    var button = document.querySelector(selector);

    if (!button) {
        button = document.querySelector('.clbi-nations-era-side-button-' + side);
    }

    if (!button) return false;

    button.dispatchEvent(new MouseEvent('click', {
        bubbles: true,
        cancelable: true,
        view: window
    }));

    return true;
}

function moveClbiNationsHistoryYearShortcut(direction) {
    var panel = document.querySelector('.clbi-nations-history-panel');
    var tabs;
    var pages;
    var activeIndex;
    var nextIndex;
    var target;

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

    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 (!target) return false;

    tabs.forEach(function (tab) {
        var active = tab.getAttribute('data-year') === target;
        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') === target;
        page.classList.toggle('is-active', active);

        if (active) {
            page.removeAttribute('hidden');
        } else {
            page.setAttribute('hidden', 'hidden');
        }
    });

    return true;
}

function initClbiNationsTabpanelControls(root) {
    var scope = root && root.querySelectorAll ? root : document;
    var panels = scope.querySelectorAll('.clbi-nations-tabpanel');

    Array.prototype.forEach.call(panels, function (tabpanel) {
        if (tabpanel.getAttribute('data-clbi-nations-tabs-ready') === '1') return;

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

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;

    root.style.setProperty('--clbi-stage-design-w', stageW + 'px');
    root.style.setProperty('--clbi-stage-design-h', Math.floor(stageH) + 'px');
    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 (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;

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.getBoundingClientRect().height || top.offsetHeight || 0);
    }

    if (bottom) {
        bottomH = Math.ceil(bottom.getBoundingClientRect().height || bottom.offsetHeight || 0);
    }

    root.style.setProperty('--clbi-top-nav-outer-h', topH + 'px');
    root.style.setProperty('--clbi-bottom-nav-outer-h', bottomH + 'px');

    if (typeof updateClbiShellVerticalScale === 'function') {
        updateClbiShellVerticalScale();
    }

    if (typeof scheduleAdaptiveLeftRecentItems === 'function') {
        scheduleAdaptiveLeftRecentItems();
    }

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

function updateClbiShellMetrics() {
    runClbiShellMetricsBatch();
}

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.setTimeout(requestClbiShellMetricsFrame, 0);
    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();

    $(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;

    $.ajax({
        url: mw.util.getUrl(SIDEBAR_LANGUAGE_STATUS_TITLE, {
            action: 'raw',
            ctype: 'application/json',
            _: String(Date.now())
        }),
        dataType: 'text',
        cache: false
    }).done(function(text) {
        var parsed = {};

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

        sidebarLanguageStatusRegistry = parsed && typeof parsed === 'object' ? parsed : {};
    }).fail(function() {
        sidebarLanguageStatusRegistry = {};
    }).always(function() {
        sidebarLanguageStatusLoaded = true;
        sidebarLanguageStatusLoading = false;
        flushSidebarLanguageStatusCallbacks();
    });
}

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 moveCatlinksToBottom() {
    var main = $('.liberty-content-main');
    var parserOutput = $('.liberty-content-main .mw-parser-output').first();
    var catlinks = $('.catlinks');

    if (!main.length || !catlinks.length) return;

    catlinks.each(function () {
        var cat = $(this);

        if (parserOutput.length) {
            cat.appendTo(parserOutput);
        } else {
            cat.appendTo(main);
        }
    });
}

// 대문 스타일
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.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.textContent = greatWallState.statusText || (currentUser ? (ownEntry ? 'SELECT YOUR MARK TO UPDATE' : 'LEAVE ONE MARK') : 'ACCOUNT REQUIRED');
    }

    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 = 'Login required';
        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 = 'Select your existing mark';
        submit.disabled = true;
        submit.textContent = 'UPDATE';
        return;
    }

    input.disabled = false;
    input.readOnly = false;
    input.removeAttribute('aria-readonly');
    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 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 = 'SELECT YOUR MARK FIRST';
        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 = 'YOUR MARK SELECTED';
        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="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() {
    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();
});

// 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 loadPage(url) {
        invalidateProfileRender();

        fetch(url)
            .then(function(res) {
                return res.text();
            })
            .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;
                    }
                }

                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) {
                    $('#side-toc-box').remove();
                    $('.profile-card').remove();
                    $('.user-profile-portal').removeClass('user-profile-portal');
                    $('.liberty-content-main').html(newContent.innerHTML);
                    $('.profile-card').remove();
                    $('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();
                });
            });
    }

// 목차 링크는 전용 처리
$(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', '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;

        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();
        $('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 t0 = performance.now();

        function render() {
            raf = requestAnimationFrame(render);
            if (!texReady) { uploadImg(); return; }
            resize();
            var t = (performance.now() - t0) / 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);

/* =========================================
   Unified Shortcuts loader
   ========================================= */
(function () {
    'use strict';

    if (!window.mw || !mw.loader) return;
    loadClbiRawScript('MediaWiki:1950_Nation_Link_Editor.js');
    loadClbiRawScript('MediaWiki:Nation_List_Manager.js');
    loadClbiRawScript('MediaWiki:Shortcuts.js');
})();