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

편집 요약 없음
(Install package: wiki-catlinks-single-source-cleanup-20260705 / js/Common.js)
태그: 되돌려진 기여
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');


/* CLBI raw MediaWiki script cache key.
function loadLangScript(done) {
  This is not a semantic version. It only prevents stale raw JS during active editing. */
     $.getScript('/index.php?title=미디어위키:Lang.js&action=raw&ctype=text/javascript')
window.CLBI_RAW_LOAD_BUST = window.CLBI_RAW_LOAD_BUST || String(Date.now());
        .done(function() {
function loadClbiRawScript(title) {
            if (typeof done === 'function') done();
     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 initHalftoneBackground() {
function resetLeftRecentAdaptiveState() {
     try {
     var list = document.getElementById('clbi-left-recent-list');
         initWebGLHalftoneBackground();
    var newsBox = list ? list.closest('.clbi-left-news-box') : null;
     } catch (err) {
    var items = list ? Array.prototype.slice.call(list.querySelectorAll('.news-recent-item')) : [];
         console.error('WebGL halftone background failed:', err);
 
    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() {
function initWebGLHalftoneBackground() {
     var box = document.querySelector('#clbi-left-sidebar .left-billboard-box');
     var canvasId = 'site-halftone-bg';
    var existing = document.getElementById(canvasId);
    var canvas = existing || document.createElement('canvas');


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


     box.classList.remove('is-left-ad-title-only', 'is-left-ad-extended');
     canvas.style.position = 'fixed';
     box.style.removeProperty('--left-billboard-h');
    canvas.style.inset = '0';
     box.style.removeProperty('--left-billboard-finish-h');
    canvas.style.width = '100vw';
}
     canvas.style.height = '100vh';
     canvas.style.pointerEvents = 'none';
    canvas.style.background = '#000000';


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


loadClbiRawScript('MediaWiki:AnecdoteViewer.js');
     if (!gl) {
 
         console.warn('WebGL background unavailable.');
 
        return;
(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 vertexSrc = [
         var pageName = mw.config.get('wgPageName') || '';
        'attribute vec2 a_position;',
        'void main() {',
         '  gl_Position = vec4(a_position, 0.0, 1.0);',
        '}'
    ].join('\n');


         if (pageName) {
    var fragmentSrc = [
            return normalizePageNameForShell(pageName);
         'precision mediump float;',
         }
        'uniform vec2 u_resolution;',
 
        'uniform float u_time;',
         return normalizePageNameForShell(window.location.pathname || '');
        'const float TAU = 6.28318530718;',
    }
        'float gaussian(float v, float r) {',
 
        '  return exp(-((v * v) / max(0.0001, r * r)));',
    function isAnecdoteNamespaceForShell() {
         '}',
         var namespaceNumber = Number(mw.config.get('wgNamespaceNumber'));
        'float hash(vec2 p) {',
         var canonicalNamespace = String(mw.config.get('wgCanonicalNamespace') || '').toLowerCase();
         return fract(sin(dot(p, vec2(127.1, 311.7))) * 43758.5453123);',
         var pageName = readCurrentPageNameForShell();
        '}',
 
        'float bucketAlpha(float a) {',
         return namespaceNumber === 3000 ||
         '  float i = floor(a * 9.0);',
            canonicalNamespace === 'anecdote' ||
        ' if (i < 1.0) return 0.040;',
            /^(anecdote|에넥도트):/i.test(pageName);
         '  if (i < 2.0) return 0.080;',
    }
        ' if (i < 3.0) return 0.135;',
 
        ' if (i < 4.0) return 0.210;',
    function isBackendOrSystemPageForShell() {
        '  if (i < 5.0) return 0.310;',
         var namespaceNumber = Number(mw.config.get('wgNamespaceNumber'));
         '  if (i < 6.0) return 0.430;',
         var action = String(mw.config.get('wgAction') || 'view').toLowerCase();
         '  if (i < 7.0) return 0.580;',
         var contentModel = String(mw.config.get('wgPageContentModel') || '').toLowerCase();
        ' if (i < 8.0) return 0.760;',
         var pageName = readCurrentPageNameForShell();
        '  return 0.920;',
         var lowerPageName = pageName.toLowerCase();
        '}',
 
        'void main() {',
         if (action && action !== 'view') {
         '  vec2 frag = gl_FragCoord.xy;',
            return true;
        ' float spacing = 5.0;',
         }
         '  float dotSize = 1.08;',
 
        ' vec2 grid = floor(frag / spacing);',
         if (pageName === '대문') {
        ' vec2 inCell = mod(frag, spacing);',
            return false;
         '  vec2 dotOrigin = vec2(1.0, 1.0);',
         }
        ' vec2 dotCenter = dotOrigin + vec2(dotSize * 0.5);',
 
         '  vec2 local = abs(inCell - dotCenter);',
        if (SYSTEM_TITLE_NAMESPACES[String(namespaceNumber)]) {
         '  float noise = hash(grid);',
            return true;
         '  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;',
         if (contentModel === 'css' || contentModel === 'javascript' || contentModel === 'json' || contentModel === 'sanitized-css') {
         '  float centerLine = 0.50 +',
            return true;
        '   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;',
         if (/\.(css|js|json)$/i.test(pageName)) {
         ' float absU = abs(u);',
            return true;
        ' float sideLift = smoothstep(0.065, 0.44, absU);',
         }
        ' float valley = gaussian(u, 0.150);',
 
         '  float t = u_time;',
        if (/^(mediawiki|미디어위키|special|특수):/i.test(pageName)) {
         '  float leftRibbonCenter = -0.28 + sin((uv.y * 3.20 + 0.12) * TAU) * 0.050;',
            return true;
         '  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);',
         return false;
         '  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;',
    function isMediaWikiSystemAssetPageForShell() {
         '  float verticalFold = pow(0.5 + 0.5 * cos(((foldedU * 3.05) + (sin(uv.y * TAU * 2.35) * 0.18)) * TAU), 2.5);',
         var namespaceNumber = Number(mw.config.get('wgNamespaceNumber'));
         '  float diagonalFold = pow(0.5 + 0.5 * cos(((foldedU * 1.80) - (uv.y * 1.12) + 0.18) * TAU), 2.1);',
         var pageName = readCurrentPageNameForShell();
         '  float waist = gaussian(uv.y - 0.50, 0.25) * gaussian(absU - 0.20, 0.19);',
         var contentModel = String(mw.config.get('wgPageContentModel') || '').toLowerCase();
        ' float grain = (noise - 0.5) * 0.050;',
 
        ' float staticField =',
         return namespaceNumber === 8 &&
        '    0.055 +',
            (/\.(css|js)$/i.test(pageName) || contentModel === 'css' || contentModel === 'javascript' || contentModel === 'sanitized-css');
        '    sideLift * 0.210 +',
    }
        '    (leftRibbon + rightRibbon) * 0.145 +',
 
        '    verticalFold * (0.055 + sideLift * 0.115) +',
 
         '    diagonalFold * 0.045 +',
    var systemDocRawFetchToken = 0;
        '   waist * 0.060 -',
 
        '    valley * 0.150 +',
    function cleanupLegacySystemDocCodeMutationsForShell() {
        '    grain;',
         document.querySelectorAll('.clbi-system-doc-codepane').forEach(function (pane) {
        '  float alpha = staticField;',
            var parent;
        '  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);',
            if (!pane || !pane.parentNode) return;
         '  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);',
            parent = pane.parentNode;
         '  alpha += 0.038 * (0.35 + sideLift) * (0.35 + noise) * sin(t * 0.00079 + ((uv.y * 4.6) + noise * 0.8) * TAU);',
            while (pane.firstChild) {
         '  alpha = bucketAlpha(clamp(alpha, 0.025, 0.96));',
                parent.insertBefore(pane.firstChild, pane);
         '  float value = alpha * dotMask;',
            }
         '  gl_FragColor = vec4(vec3(0.8862745 * value), 1.0);',
            parent.removeChild(pane);
        '}'
        });
    ].join('\n');
 
         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 || [])
    function compileShader(type, source) {
            .filter(function (el) {
        var shader = gl.createShader(type);
                return el && el.nodeType === 1 &&
        gl.shaderSource(shader, source);
                    el.id !== 'clbi-system-doc-indicator-row' &&
        gl.compileShader(shader);
                    el.id !== 'clbi-system-source-viewer' &&
                    !el.classList.contains('catlinks') &&
                    (el.textContent || '').trim().length > 200;
            });


         preferred = children.filter(function (el) {
         if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) {
             return el.matches && el.matches('.mw-highlight, .mw-code, pre');
             console.error('WebGL shader compile error:', gl.getShaderInfoLog(shader));
         })[0];
            gl.deleteShader(shader);
            return null;
         }


         return preferred || children.sort(function (a, b) {
         return shader;
            return (b.textContent || '').trim().length - (a.textContent || '').trim().length;
        })[0] || null;
     }
     }


     function getSystemDocRawUrlForShell() {
     var vertexShader = compileShader(gl.VERTEX_SHADER, vertexSrc);
        var title = mw.config.get('wgPageName') || readCurrentPageNameForShell();
    var fragmentShader = compileShader(gl.FRAGMENT_SHADER, fragmentSrc);


        if (window.mw && mw.util && typeof mw.util.getUrl === 'function') {
    if (!vertexShader || !fragmentShader) return;
            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();
    var program = gl.createProgram();
     }
    gl.attachShader(program, vertexShader);
    gl.attachShader(program, fragmentShader);
     gl.linkProgram(program);


     function removeSystemDocSourceViewerForShell() {
     if (!gl.getProgramParameter(program, gl.LINK_STATUS)) {
        var viewer = document.getElementById('clbi-system-source-viewer');
        console.error('WebGL program link error:', gl.getProgramInfoLog(program));
 
         return;
        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 positionLoc = gl.getAttribLocation(program, 'a_position');
        var output = getSystemDocOutputForShell();
    var resolutionLoc = gl.getUniformLocation(program, 'u_resolution');
        var source;
    var timeLoc = gl.getUniformLocation(program, 'u_time');
        var viewer;
        var fallbackText;


        if (!output || !isMediaWikiSystemAssetPageForShell()) return null;
    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);


        cleanupLegacySystemDocCodeMutationsForShell();
    gl.useProgram(program);
    gl.bindBuffer(gl.ARRAY_BUFFER, buffer);
    gl.enableVertexAttribArray(positionLoc);
    gl.vertexAttribPointer(positionLoc, 2, gl.FLOAT, false, 0, 0);


         source = findSystemDocSourceNodeForShell();
    function resize() {
         if (!source) return null;
        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));


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


         fallbackText = source.textContent || '';
    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();


         if (!viewer.textContent && fallbackText) {
    function render(now) {
            viewer.textContent = fallbackText;
        requestAnimationFrame(render);
         }
         if (document.hidden) return;
         if (now - lastFrame < frameInterval) return;


         source.classList.add('clbi-system-original-source-hidden');
         lastFrame = now;
        source.setAttribute('data-clbi-system-source-hidden', 'true');
         resize();
         source.style.setProperty('display', 'none', 'important');


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


     function renderSystemDocSourceViewerForShell() {
     resize();
        var viewer;
    requestAnimationFrame(render);
        var pageName;
}
        var token;
        var currentScrollTop;


        if (!isMediaWikiSystemAssetPageForShell()) return;
var CLBI_SVG_BELL = '<svg class="profile-svg profile-svg-bell" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.75" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M10.268 21a2 2 0 0 0 3.464 0"/><path d="M3.262 15.326A1 1 0 0 0 4 17h16a1 1 0 0 0 .74-1.673C19.41 13.956 18 12.499 18 8A6 6 0 0 0 6 8c0 4.499-1.411 5.956-2.738 7.326"/></svg>';
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_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>';


        pageName = String(mw.config.get('wgPageName') || readCurrentPageNameForShell());
var PROFILE_RENDER_TOKEN = 0;
        viewer = document.getElementById('clbi-system-source-viewer');


        /*
function invalidateProfileRender() {
        시스템 문서 뷰어가 이미 만들어져 있고 raw 원문도 로드된 상태라면
    PROFILE_RENDER_TOKEN++;
        다시 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();
$(function() {
        if (!viewer) return;
    initHalftoneBackground();


        currentScrollTop = viewer.scrollTop || 0;
// ── 상단 네비게이션 바 ──
        viewer.setAttribute('data-clbi-raw-title', pageName);
var navHtml =
         token = ++systemDocRawFetchToken;
    '<div id="clbi-top-nav-wrap">' +
         '<div id="clbi-top-nav">' +


        fetch(getSystemDocRawUrlForShell(), { credentials: 'same-origin' })
            '<div id="clbi-top-nav-main">' +
            .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;
                 '<div id="clbi-top-nav-tabs">' +
'<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="clbi-tnav-worldbuilding">' +
    '<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>' +


                 if (text && viewer.textContent !== text) {
                 '<div id="clbi-top-nav-search">' +
                     viewer.textContent = text;
                     '<input type="text" id="clbi-top-search-input" placeholder="검색...">' +
                 }
                 '</div>' +


                 viewer.setAttribute('data-clbi-raw-loaded', '1');
                 '<div id="clbi-top-nav-right">' +
                viewer.scrollTop = currentScrollTop;
                    '<div class="clbi-top-nav-item" id="clbi-tnav-info">' +
            })
                        '<span class="clbi-tnav-label">ℹ</span>' +
            .catch(function () {
                    '</div>' +
                viewer.setAttribute('data-clbi-raw-loaded', '0');
                '</div>' +
            });
    }


    function removeSystemDocIndicatorForShell() {
            '</div>' +
        var existing = document.getElementById('clbi-system-doc-indicator-row');


         if (document.body) {
'<div id="clbi-sub-worldbuilding">' +
            document.body.classList.remove('clbi-system-doc-page');
    '<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>' +


         if (existing && existing.parentNode) {
         '</div>' +
            existing.parentNode.removeChild(existing);
    '</div>';
        }


        removeSystemDocSourceViewerForShell();
$('.content-wrapper').before(navHtml);
    }


    function renderSystemDocIndicatorForShell() {
$('#clbi-tnav-worldbuilding').on('click', function() {
        var pageName;
    var $menu = $('#clbi-sub-worldbuilding');
        var extMatch;
    var $btn = $(this);
        var ext;
        var row;
        var box;
        var meta;
        var label;
        var type;
        var title;
        var anchor;
        var main;


        if (!document.body || !isMediaWikiSystemAssetPageForShell()) return;
    $menu.toggleClass('worldbuilding-open');
    $btn.toggleClass('clbi-tnav-active', $menu.hasClass('worldbuilding-open'));
});


        pageName = readCurrentPageNameForShell();
$('#clbi-top-search-input').on('keydown', function(e) {
         extMatch = pageName.match(/\.(css|js)$/i);
    if (e.key === 'Enter') {
         ext = extMatch ? extMatch[1].toUpperCase() : 'DOC';
         var q = $(this).val().trim();
         if (q) window.location.href = '/index.php?search=' + encodeURIComponent(q);
    }
});


        document.body.classList.add('clbi-system-doc-page');
});


        row = document.getElementById('clbi-system-doc-indicator-row');
// 페이지 전환 사운드
var transitionSound = new Audio('/index.php?title=특수:Redirect/file/Sfx-ui-001.mp3');


        if (!row) {
(function() {
            row = document.createElement('div');
    var master = parseFloat(localStorage.getItem('clbi-audio-master') || 80) / 100;
            row.id = 'clbi-system-doc-indicator-row';
    var sfx = parseFloat(localStorage.getItem('clbi-audio-sfx') || 60) / 100;
            row.className = 'clbi-system-doc-indicator-row';
    var sfxOn = localStorage.getItem('clbi-audio-sfxOn') !== 'false';
    transitionSound.volume = sfxOn ? master * sfx : 0;
})();


            box = document.createElement('div');
function playStaticSound() {
            box.className = 'clbi-system-doc-indicator';
    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';


            meta = document.createElement('div');
    if (!sfxOn) return;
            meta.className = 'clbi-system-doc-meta';


            label = document.createElement('span');
    transitionSound.volume = master * sfx;
            label.className = 'clbi-system-doc-label';
    transitionSound.currentTime = 0;
            label.textContent = 'SYSTEM DOCUMENT';
    transitionSound.play();
}


            type = document.createElement('span');
// 현재 언어 감지
            type.className = 'clbi-system-doc-type';
function getCurrentLang() {
    var langData = document.getElementById('clbi-lang-data');
    return langData ? (langData.getAttribute('data-lang') || 'ko') : 'ko';
}


            title = document.createElement('div');
function normalizePageName(value) {
            title.className = 'clbi-system-doc-title';
    return String(value || '')
 
        .split('?')[0]
            meta.appendChild(label);
        .replace(/^\/index\.php\//, '')
            meta.appendChild(type);
        .replace(/_/g, ' ')
            box.appendChild(meta);
        .trim();
            box.appendChild(title);
}
            row.appendChild(box);
 
            anchor = getSystemDocOutputForShell();
            main = document.querySelector('.liberty-content-main');


            if (anchor && anchor.parentNode) {
function buildWikiPath(title) {
                anchor.parentNode.insertBefore(row, anchor);
    return '/index.php/' + encodeURI(String(title || '').replace(/ /g, '_'));
            } else if (main) {
}
                main.insertBefore(row, main.firstChild);
            }
        }


        type = row.querySelector('.clbi-system-doc-type');
function getLangShortCode(lang) {
        title = row.querySelector('.clbi-system-doc-title');
    var map = { ko: 'KR', en: 'EN', zh: 'ZH', ja: 'JA', ru: 'RU', es: 'ES' };
    return map[lang] || String(lang || '').toUpperCase();
}


        if (type) type.textContent = ext;
function getLanguageTargetTitle(lang) {
        if (title) title.textContent = pageName;
    var data = document.getElementById('clbi-lang-data');
    if (!data || !lang) return '';


        renderSystemDocSourceViewerForShell();
     var keys = [
    }
         'data-' + lang,
 
         'data-page-' + lang,
     var PAGE_TITLE_TARGET_SELECTORS = [
         'data-title-' + lang,
         '.liberty-content-header',
         'data-target-' + lang,
        '.liberty-content-header .title',
         'data-lang-' + lang
        '.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;
     for (var i = 0; i < keys.length; i++) {
    var pageShellObserverTimer = null;
         var value = data.getAttribute(keys[i]);
 
         if (value) return value;
    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() {
     return '';
        var body = document.body;
}
        var isSystemPage;


        if (!body) return;


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


    function applyPageShellClassesDeferred() {
var sidebarLanguageStatusRegistry = {};
        applyPageShellClasses();
var sidebarLanguageStatusLoaded = false;
        window.setTimeout(applyPageShellClasses, 0);
var sidebarLanguageStatusLoading = false;
        window.setTimeout(applyPageShellClasses, 80);
var sidebarLanguageStatusCallbacks = [];
        window.setTimeout(applyPageShellClasses, 250);
    }


    function startPageShellObserver() {
var sidebarLanguageState = {
        var observer;
    order: ['ko', 'en', 'zh', 'ja', 'ru', 'es'],
 
    currentLang: 'ko',
        if (pageShellObserverStarted || !window.MutationObserver || !document.body) return;
    baseIndex: 0,
 
    selectedIndex: 0,
        pageShellObserverStarted = true;
    rotation: 0,
        observer = new MutationObserver(function (mutations) {
    dragging: false,
            var i;
    dragMoved: false,
            var target;
    dragStartX: 0,
 
    dragStartY: 0,
            /*
    dragStartRotation: 0,
            시스템 CSS/JS 문서는 applyPageShellClasses()가 초기에 한 번
    dragAxis: null,
            인디케이터와 source viewer를 만든 뒤에는 MutationObserver가 다시
    pointerCaptured: false,
            같은 렌더링을 반복할 필요가 없다. 이 반복이 DevTools에서 body/요소가
    lastX: 0,
            계속 플래시되는 직접 원인이다.
    lastTime: 0,
            SPA 전환 뒤의 처리는 loadPage()와 wikipage.content hook에서 따로 호출된다.
    releaseVelocity: 0,
            */
    suppressClickUntil: 0,
            if (isMediaWikiSystemAssetPageForShell()) {
    raf: null,
                for (i = 0; i < mutations.length; i += 1) {
    pendingRotation: 0,
                    target = mutations[i] && mutations[i].target;
    snapTimer: null,
 
    inertiaRaf: null,
                    if (
    navigateTimer: null,
                        target &&
    bound: false,
                        target.nodeType === 1 &&
    boundElement: null,
                        (
    rotor: null,
                            target.id === 'clbi-system-source-viewer' ||
    cx: 101,
                            target.id === 'clbi-system-doc-indicator-row' ||
    cy: 119,
                            (target.closest && target.closest('#clbi-system-source-viewer, #clbi-system-doc-indicator-row'))
    outerR: 109,
                        )
    innerR: 28,
                    ) {
    sectorAngle: 30,
                        return;
    halfSector: 15,
                    }
    repeats: 8,
                }
    dragSensitivity: 0.58,
 
    maxSpinVelocity: 1.75,
                if (
    minSpinVelocity: 0.055,
                    document.getElementById('clbi-system-doc-indicator-row') &&
    spinDecel: 0.00185
                    document.getElementById('clbi-system-source-viewer')
};
                ) {
                    return;
                }
            }


            if (pageShellObserverTimer) return;
function createSidebarLanguageSvgEl(tag) {
    return document.createElementNS(SIDEBAR_LANG_SVG_NS, tag);
}


            pageShellObserverTimer = window.setTimeout(function () {
function normalizeSidebarLanguageIndex(index) {
                pageShellObserverTimer = null;
    var length = sidebarLanguageState.order.length;
                applyPageShellClasses();
    var normalized = index % length;
            }, 50);
    return normalized < 0 ? normalized + length : normalized;
        });
}


        observer.observe(document.body, {
function getSidebarLanguageName(lang) {
            childList: true,
    return SIDEBAR_LANGUAGE_LABELS[lang] || String(lang || '').toUpperCase();
            subtree: true
}
        });
    }


    if (document.readyState === 'loading') {
function getSidebarLanguageDialName(lang) {
        document.addEventListener('DOMContentLoaded', function () {
     return SIDEBAR_LANGUAGE_DIAL_LABELS[lang] || getSidebarLanguageName(lang);
            applyPageShellClassesDeferred();
}
            startPageShellObserver();
        });
     } else {
        applyPageShellClassesDeferred();
        startPageShellObserver();
    }


    if (mw.hook) {
function normalizeSidebarLanguageStatusValue(value) {
        mw.hook('wikipage.content').add(applyPageShellClassesDeferred);
    value = String(value == null ? '' : value).toLowerCase().trim();
     }
     return SIDEBAR_LANGUAGE_STATUS_VALUES[value] ? value : '';
}


    window.CLBI_PAGE_SHELL = {
function getSidebarLanguageStatusPageKey() {
        refresh: applyPageShellClasses,
     var raw = String(mw.config.get('wgPageName') || '').trim();
        isBackendOrSystemPage: isBackendOrSystemPageForShell,
    var normalized = normalizePageName(raw);
        isSystemAssetPage: isMediaWikiSystemAssetPageForShell,
        renderSystemDocIndicator: renderSystemDocIndicatorForShell,
        removeSystemDocIndicator: removeSystemDocIndicatorForShell,
        refreshSystemDocSourceViewer: renderSystemDocSourceViewerForShell
     };
}());


function loadLangScript(done) {
     return normalized || raw || '대문';
     $.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 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;


function initHalftoneBackground() {
     for (i = 0; i < keys.length; i += 1) {
     try {
         if (keys[i] && pages[keys[i]] && typeof pages[keys[i]] === 'object') {
        initWebGLHalftoneBackground();
            return pages[keys[i]];
    } catch (err) {
        }
         console.error('WebGL halftone background failed:', err);
     }
     }
    return {};
}
}


function initWebGLHalftoneBackground() {
function getSidebarLanguageStatusOverride(lang) {
     var canvasId = 'site-halftone-bg';
     var entry = getSidebarLanguageStatusEntry();
    var existing = document.getElementById(canvasId);
     return normalizeSidebarLanguageStatusValue(entry[lang]);
     var canvas = existing || document.createElement('canvas');
}


    if (!existing) {
function flushSidebarLanguageStatusCallbacks() {
        canvas.id = canvasId;
    var callbacks = sidebarLanguageStatusCallbacks.slice();
        canvas.setAttribute('aria-hidden', 'true');
    sidebarLanguageStatusCallbacks.length = 0;
        document.body.insertBefore(canvas, document.body.firstChild || null);
    }


     canvas.style.position = 'fixed';
     callbacks.forEach(function(callback) {
    canvas.style.inset = '0';
        if (typeof callback === 'function') {
    canvas.style.width = '100vw';
            callback(sidebarLanguageStatusRegistry);
    canvas.style.height = '100vh';
        }
     canvas.style.pointerEvents = 'none';
     });
    canvas.style.background = '#000000';
}


     var gl = canvas.getContext('webgl', {
function loadSidebarLanguageStatusRegistry(callback, force) {
         alpha: false,
     if (typeof callback === 'function') {
        antialias: false,
         sidebarLanguageStatusCallbacks.push(callback);
        depth: false,
    }
        stencil: false,
        preserveDrawingBuffer: false,
        powerPreference: 'high-performance'
    }) || canvas.getContext('experimental-webgl');


     if (!gl) {
     if (sidebarLanguageStatusLoaded && !force) {
         console.warn('WebGL background unavailable.');
         flushSidebarLanguageStatusCallbacks();
         return;
         return;
     }
     }


     var vertexSrc = [
     if (sidebarLanguageStatusLoading) return;
         'attribute vec2 a_position;',
 
        'void main() {',
    sidebarLanguageStatusLoading = true;
         ' gl_Position = vec4(a_position, 0.0, 1.0);',
 
         '}'
    $.ajax({
    ].join('\n');
         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 = {};
        }


    var fragmentSrc = [
        sidebarLanguageStatusRegistry = parsed && typeof parsed === 'object' ? parsed : {};
        'precision mediump float;',
    }).fail(function() {
        'uniform vec2 u_resolution;',
         sidebarLanguageStatusRegistry = {};
        'uniform float u_time;',
    }).always(function() {
        'const float TAU = 6.28318530718;',
         sidebarLanguageStatusLoaded = true;
        'float gaussian(float v, float r) {',
         sidebarLanguageStatusLoading = false;
        '  return exp(-((v * v) / max(0.0001, r * r)));',
         flushSidebarLanguageStatusCallbacks();
        '}',
    });
        'float hash(vec2 p) {',
}
        '  return fract(sin(dot(p, vec2(127.1, 311.7))) * 43758.5453123);',
 
        '}',
window.CLBI_LANGUAGE_STATUS = {
        'float bucketAlpha(float a) {',
    title: SIDEBAR_LANGUAGE_STATUS_TITLE,
         '  float i = floor(a * 9.0);',
    languages: sidebarLanguageState.order.slice(),
        '  if (i < 1.0) return 0.040;',
    labels: SIDEBAR_LANGUAGE_LABELS,
        '  if (i < 2.0) return 0.080;',
    dialLabels: SIDEBAR_LANGUAGE_DIAL_LABELS,
        '  if (i < 3.0) return 0.135;',
    getPageKey: getSidebarLanguageStatusPageKey,
        '  if (i < 4.0) return 0.210;',
    getRegistry: function() {
        '  if (i < 5.0) return 0.310;',
         return sidebarLanguageStatusRegistry || {};
        '  if (i < 6.0) return 0.430;',
    },
        '  if (i < 7.0) return 0.580;',
    reload: function(callback) {
        '  if (i < 8.0) return 0.760;',
         sidebarLanguageStatusLoaded = false;
        '  return 0.920;',
         loadSidebarLanguageStatusRegistry(function() {
        '}',
            renderSidebarLanguageBox();
        'void main() {',
            if (typeof callback === 'function') callback(sidebarLanguageStatusRegistry);
         '  vec2 frag = gl_FragCoord.xy;',
         }, true);
         '  float spacing = 5.0;',
    },
         '  float dotSize = 1.08;',
    refreshDial: function() {
        '  vec2 grid = floor(frag / spacing);',
         renderSidebarLanguageBox();
        '  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) {
function getSidebarLanguageMeta(lang) {
        var shader = gl.createShader(type);
    var currentLang = getCurrentLang();
        gl.shaderSource(shader, source);
    var targetTitle = getLanguageTargetTitle(lang);
        gl.compileShader(shader);
    var isCurrent = lang === currentLang;


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


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


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


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


    var program = gl.createProgram();
     if (override === 'wip') {
    gl.attachShader(program, vertexShader);
         return {
    gl.attachShader(program, fragmentShader);
            className: 'is-locked',
    gl.linkProgram(program);
            label: 'WIP',
 
            canApply: false
     if (!gl.getProgramParameter(program, gl.LINK_STATUS)) {
         };
         console.error('WebGL program link error:', gl.getProgramInfoLog(program));
         return;
     }
     }


     var positionLoc = gl.getAttribLocation(program, 'a_position');
     if (override === 'unavailable') {
    var resolutionLoc = gl.getUniformLocation(program, 'u_resolution');
        return {
     var timeLoc = gl.getUniformLocation(program, 'u_time');
            className: 'is-locked',
            label: 'UNAVAILABLE',
            canApply: false
        };
     }


     var buffer = gl.createBuffer();
     if (override === 'available' || meta.targetTitle) {
    gl.bindBuffer(gl.ARRAY_BUFFER, buffer);
        return {
    gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([
            className: meta.targetTitle ? 'is-ready' : 'is-locked',
        -1, -1,
            label: meta.targetTitle ? 'AVAILABLE' : 'UNAVAILABLE',
        1, -1,
            canApply: !!meta.targetTitle
        -1,  1,
         };
         -1,  1,
     }
        1, -1,
        1,  1
     ]), gl.STATIC_DRAW);


     gl.useProgram(program);
     return {
    gl.bindBuffer(gl.ARRAY_BUFFER, buffer);
        className: 'is-locked',
     gl.enableVertexAttribArray(positionLoc);
        label: 'UNAVAILABLE',
    gl.vertexAttribPointer(positionLoc, 2, gl.FLOAT, false, 0, 0);
        canApply: false
     };
}


    function resize() {
function sidebarLanguageRad(deg) {
        var dpr = Math.min(window.devicePixelRatio || 1, 1.5);
    return (deg * Math.PI) / 180;
        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) {
function sidebarLanguagePointAt(radius, deg) {
            canvas.width = w;
    var state = sidebarLanguageState;
            canvas.height = h;
    var angle = sidebarLanguageRad(deg);
            canvas.style.width = cssW + 'px';
            canvas.style.height = cssH + 'px';
            gl.viewport(0, 0, w, h);
        }
    }


     var prefersReducedMotion = false;
     return {
    try {
         x: state.cx + Math.sin(angle) * radius,
         prefersReducedMotion = window.matchMedia && window.matchMedia('(prefers-reduced-motion: reduce)').matches;
        y: state.cy - Math.cos(angle) * radius
     } catch (err) {}
     };
}


     var lastFrame = 0;
function getSidebarLanguageSectorPath(start, end) {
     var frameInterval = prefersReducedMotion ? 1000 : 66;
    var state = sidebarLanguageState;
     var startTime = performance.now();
    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;


     function render(now) {
     return [
         requestAnimationFrame(render);
        'M', p1.x.toFixed(3), p1.y.toFixed(3),
         if (document.hidden) return;
         'A', state.outerR, state.outerR, 0, largeArc, 1, p2.x.toFixed(3), p2.y.toFixed(3),
         if (now - lastFrame < frameInterval) return;
        '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(' ');
}


        lastFrame = now;
function getSidebarLanguageShellPath() {
        resize();
    return getSidebarLanguageSectorPath(-68, 68);
}


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


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


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>';
function getSidebarLanguagePreviewIndex() {
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 state = sidebarLanguageState;
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 step = Math.round(-state.rotation / state.sectorAngle);
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 normalizeSidebarLanguageIndex(state.baseIndex + step);
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() {
function getSidebarLanguagePreviewMeta() {
     initHalftoneBackground();
     var state = sidebarLanguageState;
 
     return getSidebarLanguageMeta(state.order[getSidebarLanguagePreviewIndex()]);
// ── 하단 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() {
function makeSidebarLanguageSector(step) {
     var pageName = window.mw && mw.config ? (mw.config.get('wgPageName') || '') : '';
     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;


     if (!pageName) {
     group.setAttribute('class', 'sidebar-lang-sector-group');
        pageName = window.location.pathname || '';
    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 + ')');


     return String(pageName)
     path.setAttribute('class', 'sidebar-lang-sector');
        .split('?')[0]
    path.setAttribute('d', getSidebarLanguageSectorPath(-state.halfSector, state.halfSector));
        .replace(/^\/index\.php\//, '')
        .replace(/_/g, ' ')
        .trim();
}


function isClbiNationsShortcutContext() {
    label.setAttribute('class', 'sidebar-lang-sector-label');
     var pageName = readClbiBottomGuidePageName();
     label.setAttribute('x', String(state.cx));
    label.setAttribute('y', String(labelY + 5));
    label.textContent = item.meta.dialName || item.meta.name;


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


function isClbiShortcutEditableTarget(target) {
    group.addEventListener('click', function(e) {
    var el = target;
        if (sidebarLanguageState.dragging || performance.now() < sidebarLanguageState.suppressClickUntil) return;


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


         if (el.tagName) {
         cancelSidebarLanguageSpin();
            var tag = String(el.tagName).toLowerCase();
        snapSidebarLanguageToStep(parseInt(group.getAttribute('data-step') || '0', 10), true);
            if (tag === 'input' || tag === 'textarea' || tag === 'select' || tag === 'button') return true;
    });
        }


        if (el.classList && (
     return group;
            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() {
function renderSidebarLanguageWheel() {
     var action = window.mw && mw.config ? String(mw.config.get('wgAction') || '') : '';
     var state = sidebarLanguageState;
     var search = String(window.location.search || '');
    var fan = document.getElementById('clbi-sidebar-lang-fan');
 
     var svg;
     return action === 'edit' ||
    var defs;
        action === 'submit' ||
    var clip;
        /(?:^|[?&])action=(?:edit|submit)(?:&|$)/.test(search) ||
    var clipPath;
        !!document.querySelector('#editform, #wpSave, input[name="wpSave"], button[name="wpSave"]');
    var shadowBlur;
}
    var blur;
 
    var fixedDepthGradient;
function isClbiNationLinkEditorShortcutContext() {
    var shell;
     return !!document.querySelector('.nation-link-editor');
    var clipped;
}
    var rotor;
     var fixedDepthPath;
    var fixedFocus;
    var shadowSoft;
    var shadowHard;
    var rim;
    var pointer;
    var tri;
     var line;
    var step;


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


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


     if (isClbiNationLinkEditorShortcutContext()) {
     svg = createSidebarLanguageSvgEl('svg');
        return [
    svg.setAttribute('class', 'sidebar-lang-fan-svg');
            { key: 'Ctrl+S', label: '링크표 저장' }
    svg.setAttribute('viewBox', '0 0 202 150');
        ];
    svg.setAttribute('preserveAspectRatio', 'xMidYMid meet');
     }
    svg.setAttribute('role', 'img');
     svg.setAttribute('aria-label', '언어 선택 다이얼');


     if (isClbiNationListManagerShortcutContext()) {
     defs = createSidebarLanguageSvgEl('defs');
        return [
            { key: 'Ctrl+S', label: '국가 목록 저장' }
        ];
    }


     if (isClbiNationsShortcutContext()) {
     clip = createSidebarLanguageSvgEl('clipPath');
        return [
    clip.setAttribute('id', 'clbi-sidebar-language-fan-clip');
            { key: 'Q', label: '이전 시대' },
    clipPath = createSidebarLanguageSvgEl('path');
            { key: 'E', label: '다음 시대' },
    clipPath.setAttribute('d', getSidebarLanguageShellPath());
            { key: 'Shift+Q', label: '이전 연도' },
     clip.appendChild(clipPath);
            { key: 'Shift+E', label: '다음 연도' },
            { key: 'Ctrl+Q', label: '이전 대륙' },
            { key: 'Ctrl+E', label: '다음 대륙' }
        ];
     }


     return [];
     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);


function renderClbiBottomShortcutGuide() {
    fixedDepthGradient = createSidebarLanguageSvgEl('linearGradient');
     var guide = document.getElementById('clbi-bottom-shortcut-guide');
     fixedDepthGradient.setAttribute('id', 'clbi-sidebar-language-fixed-depth');
     var items;
     fixedDepthGradient.setAttribute('x1', '0');
     var html;
     fixedDepthGradient.setAttribute('y1', '0');
    fixedDepthGradient.setAttribute('x2', '0');
    fixedDepthGradient.setAttribute('y2', '1');


     if (!guide) return;
     [
        ['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);
    });


     items = getClbiBottomShortcutItems();
     defs.appendChild(clip);
    defs.appendChild(shadowBlur);
    defs.appendChild(fixedDepthGradient);
    svg.appendChild(defs);


     if (!items.length) {
     shell = createSidebarLanguageSvgEl('path');
        guide.classList.add('is-empty');
    shell.setAttribute('class', 'sidebar-lang-shell');
        guide.innerHTML = '';
    shell.setAttribute('d', getSidebarLanguageShellPath());
        return;
    svg.appendChild(shell);
    }


     guide.classList.remove('is-empty');
     clipped = createSidebarLanguageSvgEl('g');
    clipped.setAttribute('clip-path', 'url(#clbi-sidebar-language-fan-clip)');


     html = '<div class="clbi-bottom-shortcut-title">SHORTCUT GUIDE</div>';
     rotor = createSidebarLanguageSvgEl('g');
     html += '<div class="clbi-bottom-shortcut-list">';
    rotor.setAttribute('id', 'clbi-sidebar-lang-wheel-rotor');
     rotor.setAttribute('class', 'sidebar-lang-wheel-rotor');


     items.forEach(function (item) {
     for (step = -state.repeats; step <= state.repeats; step += 1) {
        html += '<div class="clbi-bottom-shortcut-item">' +
         rotor.appendChild(makeSidebarLanguageSector(step));
            '<span class="clbi-bottom-shortcut-key">' + escapeClbiBottomGuideHtml(item.key) + '</span>' +
     }
            '<span class="clbi-bottom-shortcut-label">' + escapeClbiBottomGuideHtml(item.label) + '</span>' +
         '</div>';
     });


     html += '</div>';
     clipped.appendChild(rotor);
     guide.innerHTML = html;
     svg.appendChild(clipped);
}


function buildClbiBottomPlankHtml(wrapId, navId, mainId) {
     fixedDepthPath = createSidebarLanguageSvgEl('path');
     return '' +
    fixedDepthPath.setAttribute('class', 'sidebar-lang-fixed-depth');
        '<div id="' + wrapId + '">' +
    fixedDepthPath.setAttribute('d', getSidebarLanguageShellPath());
            '<div id="' + navId + '">' +
    svg.appendChild(fixedDepthPath);
                '<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;
    fixedFocus = createSidebarLanguageSvgEl('path');
var CLBI_NATIONS_LAST_POINTER_Y = null;
    fixedFocus.setAttribute('class', 'sidebar-lang-fixed-focus');
    fixedFocus.setAttribute('d', getSidebarLanguageSectorPath(-state.halfSector, state.halfSector));
    svg.appendChild(fixedFocus);


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


     if (!tabpanel) return null;
     shadowHard = createSidebarLanguageSvgEl('path');
     if (CLBI_NATIONS_LAST_POINTER_X === null || CLBI_NATIONS_LAST_POINTER_Y === null) return null;
     shadowHard.setAttribute('class', 'sidebar-lang-inner-shadow-hard');
     if (typeof document.elementFromPoint !== 'function') return null;
     shadowHard.setAttribute('d', getSidebarLanguageShellPath());
    svg.appendChild(shadowHard);


     element = document.elementFromPoint(CLBI_NATIONS_LAST_POINTER_X, CLBI_NATIONS_LAST_POINTER_Y);
     rim = createSidebarLanguageSvgEl('path');
     if (!element) return null;
    rim.setAttribute('class', 'sidebar-lang-rim');
     rim.setAttribute('d', getSidebarLanguageShellPath());
    svg.appendChild(rim);


     tab = element.closest ? element.closest('.clbi-nations-tabpanel-tab[data-continent]') : null;
     pointer = createSidebarLanguageSvgEl('g');
    pointer.setAttribute('class', 'sidebar-lang-fixed-pointer');
    pointer.setAttribute('clip-path', 'url(#clbi-sidebar-language-fan-clip)');


     if (!tab || !tabpanel.contains(tab)) return null;
     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);


     return tab;
     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);


function validateClbiNationsPointerHover(tabpanel, forceSuppress) {
    svg.appendChild(pointer);
     var tab = getClbiNationsTabAtPointer(tabpanel);
     fan.appendChild(svg);
    var suppressContinent;
    var tabContinent;
    var tabIsActive;


     if (!tabpanel) return;
     state.rotor = rotor;
    setSidebarLanguageRotation(state.rotation, false);
}


    if (!tab) {
function updateSidebarLanguageDial() {
        clearClbiNationsKeyboardHoverSuppressed(tabpanel);
    var state = sidebarLanguageState;
        return;
    var meta = getSidebarLanguagePreviewMeta();
     }
    var status = getSidebarLanguageStatus(meta);
 
     var selector = document.getElementById('clbi-sidebar-lang-selector');
     tabContinent = tab.getAttribute('data-continent') || '';
     var apply = document.getElementById('clbi-sidebar-lang-apply');
     tabIsActive = tab.classList.contains('is-active') || tab.getAttribute('aria-selected') === 'true';
     var selectedValue = document.getElementById('clbi-sidebar-lang-selected-value');
 
    var availabilityPanel = document.getElementById('clbi-sidebar-lang-availability-panel');
     if (tabIsActive) {
    var availabilityValue = document.getElementById('clbi-sidebar-lang-availability-value');
         clearClbiNationsKeyboardHoverSuppressed(tabpanel);
 
        return;
     if (selectedValue) {
         selectedValue.textContent = meta.name;
     }
     }


     suppressContinent = tabpanel.getAttribute('data-clbi-hover-suppress-continent') || '';
     if (availabilityPanel) {
        availabilityPanel.classList.remove('is-ready', 'is-current', 'is-locked');
        availabilityPanel.classList.add(status.className);
    }


     if (forceSuppress || !suppressContinent) {
     if (availabilityValue) {
         tabpanel.classList.add('is-keyboard-switching');
         availabilityValue.textContent = status.label;
        tabpanel.setAttribute('data-clbi-hover-suppress-continent', tabContinent);
        return;
     }
     }


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


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


function validateAllClbiNationsPointerHovers() {
    return {
    var panels = document.querySelectorAll('.clbi-nations-tabpanel.is-keyboard-switching');
        meta: meta,
 
         status: status
    Array.prototype.forEach.call(panels, function (tabpanel) {
     };
         validateClbiNationsPointerHover(tabpanel, false);
     });
}
}


function activateClbiNationsContinent(tabpanel, targetContinent) {
function setSidebarLanguageRotation(value, animate) {
     var tabs;
     var state = sidebarLanguageState;
     var panels;
     state.rotation = value;
    updateSidebarLanguageDial();


     if (!tabpanel || !targetContinent) return false;
     if (!state.rotor) return;


     tabs = Array.prototype.slice.call(tabpanel.querySelectorAll('.clbi-nations-tabpanel-tab[data-continent]'));
     if (animate) {
     panels = Array.prototype.slice.call(tabpanel.querySelectorAll('.clbi-nations-tabpanel-continent[data-continent-panel]'));
        $('#clbi-sidebar-lang-selector').addClass('is-snapping');
     } else {
        $('#clbi-sidebar-lang-selector').removeClass('is-snapping');
    }


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


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


     panels.forEach(function (panel) {
     if (state.raf) return;
        var active = panel.getAttribute('data-continent-panel') === targetContinent;
        panel.classList.toggle('is-active', active);


        if (active) {
    state.raf = requestAnimationFrame(function() {
            panel.removeAttribute('hidden');
        state.raf = null;
         } else {
         setSidebarLanguageRotation(state.pendingRotation, false);
            panel.setAttribute('hidden', 'hidden');
        }
     });
     });
}


     return true;
function cancelSidebarLanguageSpin() {
}
     var state = sidebarLanguageState;


function setClbiNationsKeyboardHoverSuppressed(tabpanel) {
    if (state.inertiaRaf) {
    validateClbiNationsPointerHover(tabpanel, true);
        cancelAnimationFrame(state.inertiaRaf);
}
        state.inertiaRaf = null;
    }


function clearClbiNationsKeyboardHoverSuppressed(tabpanel) {
     $('#clbi-sidebar-lang-selector').removeClass('is-spinning');
    if (!tabpanel) return;
     tabpanel.classList.remove('is-keyboard-switching');
    tabpanel.removeAttribute('data-clbi-hover-suppress-continent');
}
}


function moveClbiNationsContinent(direction) {
function finishSidebarLanguageSnap(nearestIndex, callback) {
     var tabpanel = document.querySelector('.clbi-nations-tabpanel');
     var state = sidebarLanguageState;
    var tabs;
    var activeIndex;
    var nextIndex;
    var target;


     if (!tabpanel) return false;
     state.baseIndex = normalizeSidebarLanguageIndex(nearestIndex);
    state.selectedIndex = state.baseIndex;
    state.rotation = 0;
    state.dragging = false;


     tabs = Array.prototype.slice.call(tabpanel.querySelectorAll('.clbi-nations-tabpanel-tab[data-continent]'));
     $('#clbi-sidebar-lang-selector').removeClass('is-snapping is-dragging is-spinning');
    if (!tabs.length) return false;


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


     if (activeIndex < 0) activeIndex = 0;
     if (typeof callback === 'function') {
 
         callback(getSidebarLanguageMeta(state.order[state.selectedIndex]));
    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) {
function snapSidebarLanguageToStep(step, animate, callback) {
     var side = direction < 0 ? 'left' : 'right';
     var state = sidebarLanguageState;
     var selector = '.clbi-nations-era-side-slot-' + side +
     var targetRotation = -step * state.sectorAngle;
        ' .mw-collapsible:not(.mw-collapsed) .clbi-nations-era-side-button-' + side;
     var nearestIndex = normalizeSidebarLanguageIndex(state.baseIndex + step);
     var button = document.querySelector(selector);


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


     if (!button) return false;
     state.selectedIndex = nearestIndex;
    setSidebarLanguageRotation(targetRotation, !!animate);


     button.dispatchEvent(new MouseEvent('click', {
     state.snapTimer = setTimeout(function() {
         bubbles: true,
         finishSidebarLanguageSnap(nearestIndex, callback);
        cancelable: true,
    }, animate ? 230 : 0);
        view: window
}
    }));


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


function moveClbiNationsHistoryYearShortcut(direction) {
function startSidebarLanguageInertiaSpin(initialVelocity) {
     var panel = document.querySelector('.clbi-nations-history-panel');
     var state = sidebarLanguageState;
    var tabs;
     var velocity;
    var pages;
     var lastFrame;
    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]'));
     velocity = Math.max(-state.maxSpinVelocity, Math.min(state.maxSpinVelocity, initialVelocity));
    pages = Array.prototype.slice.call(panel.querySelectorAll('.clbi-nations-history-page[data-year-panel]'));


     if (!tabs.length || !pages.length) return false;
     if (Math.abs(velocity) < state.minSpinVelocity) {
        snapSidebarLanguageNearest();
        return;
    }


     activeIndex = tabs.findIndex(function (tab) {
     $('#clbi-sidebar-lang-selector').addClass('is-spinning');
        return tab.classList.contains('is-active') || tab.getAttribute('aria-selected') === 'true';
     lastFrame = performance.now();
     });


     if (activeIndex < 0) activeIndex = 0;
     function frame(now) {
        var dt = Math.min(34, Math.max(1, now - lastFrame));
        var sign = velocity < 0 ? -1 : 1;
        var nextSpeed;


    nextIndex = (activeIndex + direction + tabs.length) % tabs.length;
        lastFrame = now;
    target = tabs[nextIndex].getAttribute('data-year');
        state.rotation += velocity * dt;
        setSidebarLanguageRotation(state.rotation, false);


    if (!target) return false;
        nextSpeed = Math.max(0, Math.abs(velocity) - (state.spinDecel * dt));
        velocity = sign * nextSpeed;


    tabs.forEach(function (tab) {
        if (nextSpeed <= state.minSpinVelocity) {
        var active = tab.getAttribute('data-year') === target;
            state.inertiaRaf = null;
        tab.classList.toggle('is-active', active);
            $('#clbi-sidebar-lang-selector').removeClass('is-spinning');
        tab.setAttribute('aria-selected', active ? 'true' : 'false');
            snapSidebarLanguageNearest();
        tab.setAttribute('tabindex', active ? '0' : '-1');
            return;
    });
        }


    pages.forEach(function (page) {
        state.inertiaRaf = requestAnimationFrame(frame);
        var active = page.getAttribute('data-year-panel') === target;
    }
        page.classList.toggle('is-active', active);


        if (active) {
    state.inertiaRaf = requestAnimationFrame(frame);
            page.removeAttribute('hidden');
        } else {
            page.setAttribute('hidden', 'hidden');
        }
    });
 
    return true;
}
}


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


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


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


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


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


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


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


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


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


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


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


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


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


     document.body.setAttribute('data-clbi-nations-pointer-hover-ready', '1');
     if (!selector || !fan || !apply) return;


     document.addEventListener('pointermove', function (event) {
     if (state.bound && state.boundElement === selector) return;
         CLBI_NATIONS_LAST_POINTER_X = event.clientX;
 
         CLBI_NATIONS_LAST_POINTER_Y = event.clientY;
    state.bound = true;
         validateAllClbiNationsPointerHovers();
    state.boundElement = selector;
    }, true);
 
    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;


    document.addEventListener('pointerleave', function () {
        selector.classList.add('is-dragging');
        CLBI_NATIONS_LAST_POINTER_X = null;
         selector.classList.remove('is-snapping');
         CLBI_NATIONS_LAST_POINTER_Y = null;
        validateAllClbiNationsPointerHovers();
    }, true);


    window.addEventListener('blur', function () {
        /*
         CLBI_NATIONS_LAST_POINTER_X = null;
        Vertical page scrolling must stay available when the pointer starts on
         CLBI_NATIONS_LAST_POINTER_Y = null;
         the language dial. Capture and preventDefault are delayed until a
         validateAllClbiNationsPointerHovers();
         horizontal drag is confirmed.
         */
     });
     });
}


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


function initClbiBottomShortcutSystem(root) {
        if (!state.dragging) return;
    renderClbiBottomShortcutGuide();
    initClbiNationsTabpanelControls(root || document);
    initClbiNationsPointerHoverValidation();
}


// ── 상·하단 네비게이션 바 ──
        totalDx = e.clientX - state.dragStartX;
function buildClbiNavHtml(position) {
        totalDy = (e.clientY || 0) - state.dragStartY;
    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) {
        if (!state.dragAxis && (Math.abs(totalDx) > 4 || Math.abs(totalDy) > 4)) {
        return buildClbiBottomPlankHtml(wrapId, navId, mainId);
            state.dragAxis = Math.abs(totalDx) >= Math.abs(totalDy) ? 'x' : 'y';
    }


    return '' +
             if (state.dragAxis === 'y') {
        '<div id="' + wrapId + '">' +
                state.dragging = false;
             '<div id="' + navId + '">' +
                state.dragMoved = false;
                '<div id="' + mainId + '">' +
                state.dragAxis = null;
                    '<div id="' + tabsId + '">' +
                 state.pointerCaptured = false;
                        '<a class="clbi-top-nav-item" href="/index.php/대문">' +
                selector.classList.remove('is-dragging');
                            '<img class="clbi-tnav-icon" src="/index.php?title=특수:Redirect/file/Ic-main-menu-001.png" alt="">' +
                 return;
                            '<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() {
            if (fan.setPointerCapture && e.pointerId != null) {
    var contentWrapper = document.querySelector('.content-wrapper');
                try {
    var topNav = document.getElementById('clbi-top-nav-wrap');
                    fan.setPointerCapture(e.pointerId);
    var bottomNav = document.getElementById('clbi-bottom-nav-wrap');
                    state.pointerCaptured = true;
    var canvas = document.getElementById('site-halftone-bg');
                } catch (err) {
    var anchor;
                    state.pointerCaptured = false;
    var viewportH;
                }
    var topRect;
            }
    var wrapperRect;
        }
    var needsRecovery;
    var expectedWrapperTop;
    var host;


    if (!contentWrapper || !topNav || !bottomNav || !document.body) return;
        if (state.dragAxis !== 'x') return;


    /*
        now = performance.now();
    CLBI shell can live inside a Liberty <section>. Some skins/layouts give that
        frameDx = e.clientX - state.lastX;
    section a flow context that lets the top nav visually overlap the content
         dt = Math.max(1, now - state.lastTime);
    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) {
        if (Math.abs(totalDx) > 3) state.dragMoved = true;
        host.classList.add('clbi-shell-host');
    }


    viewportH = window.innerHeight || document.documentElement.clientHeight || 0;
        instantVelocity = (frameDx * state.dragSensitivity) / dt;
    topRect = topNav.getBoundingClientRect();
        state.releaseVelocity = (state.releaseVelocity * 0.62) + (instantVelocity * 0.38);
    wrapperRect = contentWrapper.getBoundingClientRect();
        state.lastX = e.clientX;
    expectedWrapperTop = topRect.bottom + 8;
        state.lastTime = now;


     needsRecovery = false;
        requestSidebarLanguageRotation(state.dragStartRotation + totalDx * state.dragSensitivity);
        e.preventDefault();
        e.stopPropagation();
     });


     if (viewportH > 0) {
     function finishDrag(e) {
         if (topRect.top >= viewportH * 0.55) needsRecovery = true;
         var velocityAge;
         if (wrapperRect.top >= viewportH * 0.60) needsRecovery = true;
         var throwVelocity;
    }
        var wasHorizontal;


    if (topRect.top > 240 || wrapperRect.top > 320) {
        if (!state.dragging) return;
        needsRecovery = true;
    }


    if (wrapperRect.top < expectedWrapperTop - 1) {
        wasHorizontal = state.dragAxis === 'x';
         needsRecovery = true;
         state.dragging = false;
    }
        selector.classList.remove('is-dragging');


    if (!needsRecovery) {
        if (fan.releasePointerCapture && state.pointerCaptured && e && e.pointerId != null) {
        document.body.classList.add('clbi-shell-ready');
            try { fan.releasePointerCapture(e.pointerId); } catch (err) {}
         return;
         }
    }


    anchor = canvas && canvas.parentNode === document.body
        state.pointerCaptured = false;
         ? canvas.nextSibling
         state.dragAxis = null;
        : document.body.firstChild;


    document.body.insertBefore(topNav, anchor);
        if (!wasHorizontal && !state.dragMoved) {
    document.body.insertBefore(contentWrapper, topNav.nextSibling);
            return;
    document.body.insertBefore(bottomNav, contentWrapper.nextSibling);
        }


    document.body.classList.add('clbi-shell-ready');
        velocityAge = performance.now() - state.lastTime;
}
        throwVelocity = velocityAge > 120 ? 0 : state.releaseVelocity;


window.normalizeClbiShellDomOrder = normalizeClbiShellDomOrder;
        if (state.dragMoved) {
            state.suppressClickUntil = performance.now() + 180;
        }


        if (state.dragMoved && Math.abs(throwVelocity) >= state.minSpinVelocity) {
            startSidebarLanguageInertiaSpin(throwVelocity);
        } else {
            snapSidebarLanguageNearest();
        }


function ensureClbiVerticalScaleHost() {
        if (e) {
    var topNav = document.getElementById('clbi-top-nav-wrap');
            e.preventDefault();
    var contentWrapper = document.querySelector('.content-wrapper');
            e.stopPropagation();
    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
     fan.addEventListener('pointerup', finishDrag);
         ? topNav.parentElement
    fan.addEventListener('pointercancel', finishDrag);
        : null;
    fan.addEventListener('lostpointercapture', function() {
         if (!state.dragging) return;


    if (parent && parent !== document.body) {
        state.dragging = false;
         parent.classList.add('clbi-shell-host');
        state.pointerCaptured = false;
        parent.id = parent.id || 'clbi-shell-scale-host';
        state.dragAxis = null;
        return parent;
         selector.classList.remove('is-dragging');
    }


    host = existing || document.createElement('div');
        if (state.dragMoved && Math.abs(state.releaseVelocity) >= state.minSpinVelocity) {
    host.id = 'clbi-shell-scale-host';
            state.suppressClickUntil = performance.now() + 180;
     host.className = 'clbi-shell-host';
            startSidebarLanguageInertiaSpin(state.releaseVelocity);
        } else {
            snapSidebarLanguageNearest();
        }
     });


     if (!host.parentNode) {
     apply.addEventListener('click', function(e) {
         document.body.insertBefore(host, topNav);
         e.preventDefault();
    }
        e.stopPropagation();


    host.appendChild(topNav);
        snapSidebarLanguageNearest(function(meta) {
    host.appendChild(contentWrapper);
            scheduleSidebarLanguageNavigation(meta);
     host.appendChild(bottomNav);
        });
     });


     return host;
     selector.addEventListener('keydown', function(e) {
}
        if (e.key === 'ArrowLeft') {
            moveSidebarLanguageSelection(-1);
            e.preventDefault();
        }


function readClbiRootPx(name, fallback) {
        if (e.key === 'ArrowRight') {
    var raw = '';
            moveSidebarLanguageSelection(1);
    var value;
            e.preventDefault();
        }


    try {
         if (e.key === 'Enter' || e.key === ' ') {
         raw = getComputedStyle(document.documentElement).getPropertyValue(name);
            apply.click();
    } catch (err) {}
            e.preventDefault();
 
        }
     value = parseFloat(raw);
     });
    return isFinite(value) && value > 0 ? value : fallback;
}
}


function resetLeftRecentAdaptiveState() {
function renderSidebarLanguageBox() {
     var list = document.getElementById('clbi-left-recent-list');
     bindSidebarLanguageSelector();
     var newsBox = list ? list.closest('.clbi-left-news-box') : null;
     setSidebarLanguageSelection(getCurrentLang());
    var items = list ? Array.prototype.slice.call(list.querySelectorAll('.news-recent-item')) : [];


     if (newsBox) {
     if (!sidebarLanguageStatusLoaded) {
         newsBox.classList.remove('is-adaptive-constrained');
         loadSidebarLanguageStatusRegistry(function() {
         newsBox.style.removeProperty('--adaptive-news-h');
            setSidebarLanguageSelection(getCurrentLang());
         });
     }
     }
    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() {
function loadRecentChangesList(targetSelector, limit) {
     var box = document.querySelector('#clbi-left-sidebar .left-billboard-box');
     var $target = $(targetSelector);


     if (!box) return;
     if (!$target.length) return;


     box.classList.remove('is-left-ad-title-only', 'is-left-ad-extended');
     var lang = getCurrentLang();
     box.style.removeProperty('--left-billboard-h');
     var t = (window.LANG && window.LANG[lang]) ? window.LANG[lang] : (window.LANG ? window.LANG.ko : null);
     box.style.removeProperty('--left-billboard-finish-h');
     var isNewsList = $target.closest('.clbi-left-news-box').length > 0;
}


function updateClbiShellVerticalScale() {
    function escapeHtml(value) {
    var root = document.documentElement;
        return String(value == null ? '' : value)
    var body = document.body;
            .replace(/&/g, '&amp;')
    var host;
            .replace(/</g, '&lt;')
    var topInner;
            .replace(/>/g, '&gt;')
    var bottomInner;
            .replace(/"/g, '&quot;')
    var thresholdH;
            .replace(/'/g, '&#039;');
    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();
    $target.html((t && t.loading) ? t.loading : '불러오는 중...');


if ($contentWrapper.length) {
     $.getJSON(
     $('#clbi-top-nav-wrap, #clbi-bottom-nav-wrap').remove();
        '/api.php?action=query&list=recentchanges&rclimit=' + encodeURIComponent(limit || 5) + '&rcprop=title|timestamp|user&format=json&rcnamespace=0&rctype=edit|new',
    $contentWrapper.before(buildClbiNavHtml('top'));
        function(data) {
    $contentWrapper.after(buildClbiNavHtml('bottom'));
            var items = data && data.query ? data.query.recentchanges : [];
    renderClbiBottomShortcutGuide();
            var html = '';
    initClbiNationsTabpanelControls(document);
    if (typeof window.normalizeClbiShellDomOrder === 'function') window.normalizeClbiShellDomOrder();
}


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


function runClbiShellMetricsBatch() {
            $.each(items, function(i, item) {
    var top = document.getElementById('clbi-top-nav-wrap');
                var label = timeAgo(item.timestamp);
    var bottom = document.getElementById('clbi-bottom-nav-wrap');
                var title = item.title || '';
    var root = document.documentElement;
                var userName = item.user || 'Unknown';
    var topH = 0;
                var pageHref = buildWikiPath(title);
    var bottomH = 0;
                var avatarSrc = '/index.php?title=특수:Redirect/file/Pfp-' + encodeURIComponent(userName) + '.png';


    CLBI_SHELL_METRICS_RAF = null;
                if (isNewsList) {
 
                    html +=
    if (!root) return;
                        '<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\';">' +
    if (top) {
                            '<div class="news-recent-main">' +
        topH = Math.ceil(top.getBoundingClientRect().height || top.offsetHeight || 0);
                                '<div class="news-recent-title-wrap">' +
    }
                                    '<span class="news-recent-title">' + escapeHtml(title) + '</span>' +
 
                                '</div>' +
    if (bottom) {
                                '<div class="news-recent-meta">' +
        bottomH = Math.ceil(bottom.getBoundingClientRect().height || bottom.offsetHeight || 0);
                                    '<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>';
                }
            });


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


    if (typeof updateClbiShellVerticalScale === 'function') {
            $target.find(isNewsList ? '.news-recent-item' : '.clbi-recent-item').each(function() {
        updateClbiShellVerticalScale();
                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 (typeof scheduleAdaptiveLeftRecentItems === 'function') {
                if (!wrap.length || !title.length) return;
        scheduleAdaptiveLeftRecentItems();
    }


    if (typeof scheduleClbiContentBottomGap === 'function') {
                var wrapW = wrap.width();
        scheduleClbiContentBottomGap();
                var titleW = title[0].scrollWidth;
    }
}


function updateClbiShellMetrics() {
                if (titleW > wrapW + 20) {
    runClbiShellMetricsBatch();
                    var duration = titleW / 40;
}


function requestClbiShellMetricsFrame() {
                    title.css({
    if (CLBI_SHELL_METRICS_RAF !== null) return;
                        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);


    CLBI_SHELL_METRICS_RAF = window.requestAnimationFrame
        $target.html((t && t.loadFail) ? t.loadFail : '불러오기 실패');
        ? window.requestAnimationFrame(runClbiShellMetricsBatch)
    });
        : window.setTimeout(runClbiShellMetricsBatch, 16);
}
}


function scheduleClbiShellMetrics() {
// 국가_및_조합 전용 왼쪽 사이드바 이미지
     requestClbiShellMetricsFrame();
function updateLeftSidebarNationsImage() {
 
     $('#clbi-left-nations-image').remove();
    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();
function setProfileActionLabel(selector, text) {
    var target = $(selector);
     var label = target.find('.profile-action-label');


     $(window).on('resize orientationchange', scheduleClbiShellMetrics);
     if (label.length) {
    $(window).on('pageshow.clbiShellScale focus.clbiShellScale', scheduleClbiShellMetrics);
         label.text(text);
    document.addEventListener('visibilitychange', function () {
     } else {
         if (!document.hidden) scheduleClbiShellMetrics();
         target.text(text);
     });
    $(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() {
function updateSidebar() {
        var $menu = $(menuSelector);
     if (!window.LANG) {
        var $btn = $(this);
         setTimeout(updateSidebar, 100);
 
         return;
        $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) {
    var currentLang = getCurrentLang();
     mw.hook('wikipage.content').add(function ($content) {
     var t = (window.LANG && window.LANG[currentLang]) ? window.LANG[currentLang] : window.LANG.ko;
        initClbiBottomShortcutSystem($content && $content[0] ? $content[0] : document);
    });
}


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


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


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


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


    transitionSound.volume = master * sfx;
$('#clbi-left-news-changelog-main').text(changelogTitle);
    transitionSound.currentTime = 0;
$('#clbi-left-news-recent-title').text('RECENT CHANGES');
    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';
}


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


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


function getLangShortCode(lang) {
    $('.toggleBtn').each(function() {
    var map = { ko: 'KR', en: 'EN', zh: 'ZH', ja: 'JA', ru: 'RU', es: 'ES' };
        var btn = $(this);
    return map[lang] || String(lang || '').toUpperCase();
}


function getLanguageTargetTitle(lang) {
        if (!$('#' + btn.data('target')).hasClass('folding-open')) {
    var data = document.getElementById('clbi-lang-data');
            btn.text(t.expand);
     if (!data || !lang) return '';
        } else {
            btn.text(t.collapse);
        }
     });


     var keys = [
     updateLeftSidebarNationsImage();
        'data-' + lang,
}
        'data-page-' + lang,
        'data-title-' + lang,
        'data-target-' + lang,
        'data-lang-' + lang
    ];


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


     return '';
     // MediaWiki가 현재 문서를 편집 가능하지 않다고 판단하면 숨김
}
    var isEditable = mw.config.get('wgIsProbablyEditable');
 
     if (isEditable === false) {
function escapeClbiHtml(value) {
         return false;
     return String(value == null ? '' : value)
    }
         .replace(/&/g, '&amp;')
 
        .replace(/</g, '&lt;')
    var relevantEditable = mw.config.get('wgRelevantPageIsProbablyEditable');
        .replace(/>/g, '&gt;')
    if (relevantEditable === false) {
         .replace(/"/g, '&quot;')
         return false;
        .replace(/'/g, '&#039;');
    }
 
    return true;
}
}


function getCatlinkElements(root) {
    var scope = root || document;
    var found = [];
    var seen = [];
    $(scope).find('#catlinks, .catlinks').addBack('#catlinks, .catlinks').each(function () {
        if (seen.indexOf(this) !== -1) return;
        seen.push(this);
        found.push(this);
    });


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


var sidebarLanguageStatusRegistry = {};
function normalizeCatlinksPanel(root) {
var sidebarLanguageStatusLoaded = false;
    getCatlinkElements(root).each(function () {
var sidebarLanguageStatusLoading = false;
        var cat = this;
var sidebarLanguageStatusCallbacks = [];
        var hiddenSections;


var sidebarLanguageState = {
        cat.classList.add('clbi-catlinks-panel');
    order: ['ko', 'en', 'zh', 'ja', 'ru', 'es'],
        cat.removeAttribute('aria-hidden');
    currentLang: 'ko',
 
    baseIndex: 0,
        hiddenSections = cat.querySelectorAll('#mw-hidden-catlinks, .mw-hidden-catlinks, .mw-hidden-cats-hidden, .mw-hidden-cats-user-shown');
    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) {
        Array.prototype.forEach.call(hiddenSections, function (section) {
    return document.createElementNS(SIDEBAR_LANG_SVG_NS, tag);
            section.classList.add('clbi-catlinks-section-visible');
}
            section.removeAttribute('hidden');
            section.removeAttribute('aria-hidden');
            section.style.removeProperty('visibility');
            section.style.removeProperty('opacity');
            section.style.setProperty('display', 'block', 'important');
        });


function normalizeSidebarLanguageIndex(index) {
        if (cat.classList.contains('catlinks-allhidden') && hiddenSections.length) {
    var length = sidebarLanguageState.order.length;
            cat.classList.add('clbi-catlinks-show-hidden');
    var normalized = index % length;
        }
     return normalized < 0 ? normalized + length : normalized;
     });
}
}


function getSidebarLanguageName(lang) {
function setCatlinksPanelHidden(hidden) {
     return SIDEBAR_LANGUAGE_LABELS[lang] || String(lang || '').toUpperCase();
     getCatlinkElements(document).each(function () {
        if (hidden) {
            this.style.setProperty('display', 'none', 'important');
        } else {
            this.style.removeProperty('display');
            normalizeCatlinksPanel(this);
        }
    });
}
}


function getSidebarLanguageDialName(lang) {
function moveCatlinksToBottom() {
     return SIDEBAR_LANGUAGE_DIAL_LABELS[lang] || getSidebarLanguageName(lang);
     var main = $('.liberty-content-main');
}
    var parserOutput = $('.liberty-content-main .mw-parser-output').first();
    var catlinks = getCatlinkElements(document);
 
    if (!main.length || !catlinks.length) return;


function normalizeSidebarLanguageStatusValue(value) {
    catlinks.each(function () {
    value = String(value == null ? '' : value).toLowerCase().trim();
        var cat = $(this);
    return SIDEBAR_LANGUAGE_STATUS_VALUES[value] ? value : '';
}


function getSidebarLanguageStatusPageKey() {
        if (parserOutput.length) {
    var raw = String(mw.config.get('wgPageName') || '').trim();
            cat.appendTo(parserOutput);
     var normalized = normalizePageName(raw);
        } else {
            cat.appendTo(main);
        }
     });


     return normalized || raw || '대문';
     normalizeCatlinksPanel(parserOutput.length ? parserOutput[0] : main[0]);
}
}


function getSidebarLanguageStatusEntry() {
// 대문 스타일
     var registry = sidebarLanguageStatusRegistry || {};
function initCategoryNavIfAvailable(root) {
     var pages = registry.pages && typeof registry.pages === 'object' ? registry.pages : registry;
     /*
     var raw = String(mw.config.get('wgPageName') || '').trim();
     CategoryNav.js는 대문 카테고리 네비를 SVG로 생성한다.
     var normalized = normalizePageName(raw);
 
    var title = String(mw.config.get('wgTitle') || '').trim();
    Common.js가 SPA로 본문을 갈아끼운 뒤에는 MediaWiki 원래 페이지 로드와 달리
    var keys = [
     CategoryNav.js의 초기 DOMContentLoaded만으로는 새 mount를 다시 잡지 못할 수 있다.
        normalized,
     CategoryNav.js 자체도 mw.hook('wikipage.content')를 듣지만, 로드 순서와 SPA 타이밍이
        raw,
    엇갈릴 수 있으므로 Common.js 쪽에서도 존재 여부를 확인한 뒤 한 번 더 호출한다.
        raw.replace(/_/g, ' '),
        normalized.replace(/ /g, '_'),
        title,
        title.replace(/_/g, ' ')
    ];
    var i;


     for (i = 0; i < keys.length; i += 1) {
     이 함수는 CategoryNav.js가 아직 로드되지 않았으면 아무 것도 하지 않는다.
        if (keys[i] && pages[keys[i]] && typeof pages[keys[i]] === 'object') {
    */
            return pages[keys[i]];
    if (
        }
        window.CLBI &&
        window.CLBI.categoryNav &&
        typeof window.CLBI.categoryNav.init === 'function'
    ) {
        window.CLBI.categoryNav.init(root || document);
     }
     }
    return {};
}
}


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


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


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


function loadSidebarLanguageStatusRegistry(callback, force) {
function applyMainPageStyle() {
     if (typeof callback === 'function') {
    var specialPage = mw.config.get('wgCanonicalSpecialPageName');
        sidebarLanguageStatusCallbacks.push(callback);
     if (specialPage === 'Preferences') return;
    }


     if (sidebarLanguageStatusLoaded && !force) {
     var pageName = normalizePageName(mw.config.get('wgPageName'));
        flushSidebarLanguageStatusCallbacks();
    var namespaceNumber = mw.config.get('wgNamespaceNumber');
        return;
    var isMainPage = (pageName === '대문');
     }
    var isUserProfilePage = (namespaceNumber === 2);
    var isScreenDoc = ($('.screen-header').length > 0);
     var hideTools = (isMainPage || isUserProfilePage || !canShowContentTools());


     if (sidebarLanguageStatusLoading) return;
     $('body').toggleClass('user-profile-page', isUserProfilePage);


     sidebarLanguageStatusLoading = true;
     // 모든 문서에서 분류 바를 본문 컨테이너 아래로 이동
    moveCatlinksToBottom();


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


         try {
         // 새 대문은 .main-portal 본문 구조가 로고/히어로를 담당한다.
            parsed = text ? JSON.parse(text) : {};
        // Common.js의 구식 바깥 로고/CRT 재배치 루틴은 사용하지 않는다.
         } catch (err) {
        removeLegacyMainPageHero();
            console.error('LanguageStatus.json parse failed:', err);
         $('#clbi-tools-box').remove();
            parsed = {};
        }


         sidebarLanguageStatusRegistry = parsed && typeof parsed === 'object' ? parsed : {};
         $('.content-tools').css('display', 'none');
    }).fail(function() {
         $('.liberty-content').addClass('content-tools-hidden');
         sidebarLanguageStatusRegistry = {};
    }).always(function() {
        sidebarLanguageStatusLoaded = true;
        sidebarLanguageStatusLoading = false;
        flushSidebarLanguageStatusCallbacks();
    });
}


window.CLBI_LANGUAGE_STATUS = {
         initCategoryNavIfAvailable(document);
    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) {
    } else if (isUserProfilePage) {
    var currentLang = getCurrentLang();
        $('.liberty-content-header').css('display', 'none');
    var targetTitle = getLanguageTargetTitle(lang);
        $('.mw-page-title-main, .mw-page-title-namespace, .mw-page-title-separator').addClass('clbi-hide');
    var isCurrent = lang === currentLang;
        setCatlinksPanelHidden(true);
        $('.liberty-content-main').css('border-radius', '0');


    return {
         $('#clbi-main-logo').remove();
         lang: lang,
         $('#clbi-main-crt-hero-wrap').remove();
         code: getLangShortCode(lang),
         $('#clbi-main-crt-hero').remove();
         name: getSidebarLanguageName(lang),
         $('#clbi-tools-box').remove();
         dialName: getSidebarLanguageDialName(lang),
        targetTitle: targetTitle,
        isCurrent: isCurrent,
        canMove: !!targetTitle && !isCurrent
    };
}


function getSidebarLanguageStatus(meta) {
        $('.content-tools').css('display', 'none');
    var override;
        $('.liberty-content').addClass('content-tools-hidden');


     if (!meta) {
     } else if (isScreenDoc) {
         return {
         $('.liberty-content-header').css('display', 'none');
            className: 'is-locked',
        $('.mw-page-title-main').addClass('clbi-hide');
            label: 'UNAVAILABLE',
        setCatlinksPanelHidden(false);
            canApply: false
         $('.liberty-content-main').css('border-radius', '0');
         };
    }


    if (meta.isCurrent) {
        $('#clbi-main-logo').remove();
         return {
         $('#clbi-main-crt-hero-wrap').remove();
            className: 'is-current',
            label: 'CURRENT',
            canApply: false
        };
    }


    override = getSidebarLanguageStatusOverride(meta.lang);
        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"><span class="clbi-icon" style="--icon:var(--ic-ui-003)"></span> 관리</div>');
            var $toolsContent = $('<div class="clbi-left-content"></div>');


    if (override === 'wip') {
            $toolsContent.append($('.content-tools .btn-group').clone(true));
        return {
            $toolsBox.append($toolsTitle).append($toolsContent);
             className: 'is-locked',
             $('#clbi-left-sidebar').append($toolsBox);
            label: 'WIP',
         }
            canApply: false
         };
    }


    if (override === 'unavailable') {
        $('.content-tools').css('display', 'none');
         return {
         $('.liberty-content').addClass('content-tools-hidden');
            className: 'is-locked',
 
            label: 'UNAVAILABLE',
    } else {
            canApply: false
        $('.liberty-content-header').css('display', '');
         };
        $('.mw-page-title-main, .mw-page-title-namespace, .mw-page-title-separator').removeClass('clbi-hide');
        setCatlinksPanelHidden(false);
        $('.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');
     }
     }


     if (override === 'available' || meta.targetTitle) {
     if (hideTools) {
         return {
         $('.content-tools').css('display', 'none');
            className: meta.targetTitle ? 'is-ready' : 'is-locked',
        $('.liberty-content').addClass('content-tools-hidden');
            label: meta.targetTitle ? 'AVAILABLE' : 'UNAVAILABLE',
    } else if (!isScreenDoc && !isUserProfilePage) {
            canApply: !!meta.targetTitle
        $('.content-tools').css('display', '');
         };
         $('.liberty-content').removeClass('content-tools-hidden');
     }
     }


     return {
     updateSidebar();
        className: 'is-locked',
        label: 'UNAVAILABLE',
        canApply: false
    };
}
}


function sidebarLanguageRad(deg) {
// 본문 기본 목차 제거
     return (deg * Math.PI) / 180;
function removeNativeTocFromContent() {
     $('.liberty-content-main #toc, .liberty-content-main .toc').remove();
}
}


function sidebarLanguagePointAt(radius, deg) {
// 왼쪽 목차: MediaWiki 문단 ID 가져오기
     var state = sidebarLanguageState;
function getHeadingId(heading) {
     var angle = sidebarLanguageRad(deg);
     if (heading.id) {
        return heading.id;
     }


     return {
     var headline = heading.querySelector('.mw-headline[id]');
        x: state.cx + Math.sin(angle) * radius,
    if (headline && headline.id) {
         y: state.cy - Math.cos(angle) * radius
         return headline.id;
     };
     }
 
    return '';
}
}


function getSidebarLanguageSectorPath(start, end) {
// 왼쪽 목차: MediaWiki 문단 제목 텍스트 가져오기
     var state = sidebarLanguageState;
function getHeadingText(heading) {
    var p1 = sidebarLanguagePointAt(state.outerR, start);
     var headline = heading.querySelector('.mw-headline');
    var p2 = sidebarLanguagePointAt(state.outerR, end);
     var source = headline || heading;
    var p3 = sidebarLanguagePointAt(state.innerR, end);
     var clone = source.cloneNode(true);
     var p4 = sidebarLanguagePointAt(state.innerR, start);
     var largeArc = Math.abs(end - start) > 180 ? 1 : 0;


     return [
     $(clone).find('.mw-editsection, .mw-editsection-bracket, .mw-editsection-divider').remove();
        '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 (clone.textContent || '')
    return getSidebarLanguageSectorPath(-68, 68);
        .replace(/\s+/g, ' ')
        .trim();
}
}


function getSidebarLanguageByStep(step) {
// 왼쪽 목차: 긴 제목에 자동 스크롤 적용
     var state = sidebarLanguageState;
function initTocTitleScroll(root) {
    var index = normalizeSidebarLanguageIndex(state.baseIndex + step);
     var $items = root
        ? $(root).find('.toc-scroll-text')
        : $('#side-toc-box .toc-scroll-text');


     return {
     $items.each(function () {
         index: index,
         var $text = $(this);
         meta: getSidebarLanguageMeta(state.order[index])
         var $wrap = $text.closest('.toc-scroll-wrap');
    };
}


function getSidebarLanguagePreviewIndex() {
        if (!$wrap.length) return;
    var state = sidebarLanguageState;
    var step = Math.round(-state.rotation / state.sectorAngle);
    return normalizeSidebarLanguageIndex(state.baseIndex + step);
}


function getSidebarLanguagePreviewMeta() {
        var wrapW = Math.floor($wrap.width());
    var state = sidebarLanguageState;
        var textW = Math.ceil(this.scrollWidth);
    return getSidebarLanguageMeta(state.order[getSidebarLanguagePreviewIndex()]);
}


function makeSidebarLanguageSector(step) {
        // 왼쪽 목차: 레이아웃 계산이 끝나지 않았으면 이번 실행에서는 건드리지 않는다.
    var state = sidebarLanguageState;
        if (!wrapW || !textW) 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;


    group.setAttribute('class', 'sidebar-lang-sector-group');
        if (textW <= wrapW + 12) {
    group.setAttribute('data-step', String(step));
            // 왼쪽 목차: 칸을 넘지 않는 제목은 전체 텍스트를 그대로 보여준다.
    group.setAttribute('data-index', String(item.index));
            $wrap.removeClass('is-scrolling');
    group.setAttribute('data-lang', item.meta.lang);
    group.setAttribute('transform', 'rotate(' + angle + ' ' + state.cx + ' ' + state.cy + ')');


    path.setAttribute('class', 'sidebar-lang-sector');
            if ($text.data('toc-scroll-enabled')) {
    path.setAttribute('d', getSidebarLanguageSectorPath(-state.halfSector, state.halfSector));
                $text.css({
                    animation: '',
                    'animation-delay': '',
                    '--scroll-dist': ''
                });
                $text.removeData('toc-scroll-enabled');
                $text.removeData('toc-scroll-key');
            }


    label.setAttribute('class', 'sidebar-lang-sector-label');
            return;
    label.setAttribute('x', String(state.cx));
        }
    label.setAttribute('y', String(labelY + 5));
 
    label.textContent = item.meta.dialName || item.meta.name;
        var scrollDist = '-' + (textW - wrapW + 10) + 'px';
        var duration = Math.max(7, textW / 38) * 1.25;
        var scrollKey = scrollDist + '|' + duration;


    group.appendChild(path);
        // 왼쪽 목차: 긴 제목에는 오른쪽 페이드와 스크롤을 적용한다.
    group.appendChild(label);
        $wrap.addClass('is-scrolling');


    group.addEventListener('click', function(e) {
        // 왼쪽 목차: 같은 값으로 이미 적용된 애니메이션은 다시 초기화하지 않는다.
         if (sidebarLanguageState.dragging || performance.now() < sidebarLanguageState.suppressClickUntil) return;
         if ($text.data('toc-scroll-key') === scrollKey) {
            return;
        }


         e.preventDefault();
         $text.data('toc-scroll-enabled', true);
         e.stopPropagation();
         $text.data('toc-scroll-key', scrollKey);


         cancelSidebarLanguageSpin();
         $text.css({
        snapSidebarLanguageToStep(parseInt(group.getAttribute('data-step') || '0', 10), true);
            // 왼쪽 목차: 페이지 진입 직후에는 잠시 읽을 시간을 준 뒤 흐르게 한다.
            animation: 'toc-scroll-blink-reset ' + duration + 's linear infinite',
            'animation-delay': '1s',
            '--scroll-dist': scrollDist
        });
     });
     });
}


     return group;
// 목차를 왼쪽 사이드바에 새로 생성
}
function moveTocToLeftSidebar() {
     // 왼쪽 목차: MediaWiki가 만든 원래 목차는 본문에서 제거한다.
    removeNativeTocFromContent();


function renderSidebarLanguageWheel() {
     var leftSidebar = document.getElementById('clbi-left-sidebar');
     var state = sidebarLanguageState;
     if (!leftSidebar) return;
    var fan = document.getElementById('clbi-sidebar-lang-fan');
 
     var svg;
     var content =
    var defs;
        document.querySelector('.liberty-content-main .mw-parser-output') ||
     var clip;
        document.querySelector('.liberty-content-main');
    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;
     if (!content) return;


     fan.innerHTML = '';
     var headings = Array.prototype.slice.call(
        content.querySelectorAll('h2, h3')
    ).filter(function (heading) {
        if (heading.closest('#toc, .toc, #side-toc-box')) return false;


    svg = createSidebarLanguageSvgEl('svg');
        var id = getHeadingId(heading);
    svg.setAttribute('class', 'sidebar-lang-fan-svg');
        var text = getHeadingText(heading);
    svg.setAttribute('viewBox', '0 0 202 150');
    svg.setAttribute('preserveAspectRatio', 'xMidYMid meet');
    svg.setAttribute('role', 'img');
    svg.setAttribute('aria-label', '언어 선택 다이얼');


    defs = createSidebarLanguageSvgEl('defs');
        if (!id || !text) return false;


    clip = createSidebarLanguageSvgEl('clipPath');
        return true;
    clip.setAttribute('id', 'clbi-sidebar-language-fan-clip');
     });
    clipPath = createSidebarLanguageSvgEl('path');
    clipPath.setAttribute('d', getSidebarLanguageShellPath());
     clip.appendChild(clipPath);


     shadowBlur = createSidebarLanguageSvgEl('filter');
     var tocKey = headings.map(function (heading) {
    shadowBlur.setAttribute('id', 'clbi-sidebar-language-shadow-blur');
        return getHeadingId(heading) + '|' + getHeadingText(heading);
    shadowBlur.setAttribute('x', '-20%');
     }).join('||');
    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');
     var existingBox = document.getElementById('side-toc-box');
    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'],
     if (existingBox && existingBox.getAttribute('data-toc-key') === tocKey) {
        ['34%', '#ffffff', '0.006'],
         initTocTitleScroll(existingBox);
        ['58%', '#000000', '0.030'],
         return;
        ['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);
     if (existingBox) {
    defs.appendChild(shadowBlur);
        existingBox.remove();
     defs.appendChild(fixedDepthGradient);
     }
    svg.appendChild(defs);


     shell = createSidebarLanguageSvgEl('path');
     if (!headings.length) return;
    shell.setAttribute('class', 'sidebar-lang-shell');
    shell.setAttribute('d', getSidebarLanguageShellPath());
    svg.appendChild(shell);


     clipped = createSidebarLanguageSvgEl('g');
     var tocBox = document.createElement('div');
     clipped.setAttribute('clip-path', 'url(#clbi-sidebar-language-fan-clip)');
     tocBox.className = 'clbi-left-box';
    tocBox.id = 'side-toc-box';
    tocBox.setAttribute('data-toc-key', tocKey);


     rotor = createSidebarLanguageSvgEl('g');
     var title = document.createElement('div');
     rotor.setAttribute('id', 'clbi-sidebar-lang-wheel-rotor');
     title.className = 'clbi-left-title';
    rotor.setAttribute('class', 'sidebar-lang-wheel-rotor');


     for (step = -state.repeats; step <= state.repeats; step += 1) {
     // 왼쪽 목차: 박스 제목은 Lang.js의 현재 UI 언어를 따른다.
        rotor.appendChild(makeSidebarLanguageSector(step));
    var currentLang = getCurrentLang();
     }
    var t = (window.LANG && window.LANG[currentLang]) ? window.LANG[currentLang] : window.LANG.ko;
     var tocTitleText = (t && t.toc) ? t.toc : '목차';


     clipped.appendChild(rotor);
     title.innerHTML =
    svg.appendChild(clipped);
        '<span class="sidebar-title-svg" aria-hidden="true">' + CLBI_SVG_LIST + '</span> ' + tocTitleText;


     fixedDepthPath = createSidebarLanguageSvgEl('path');
     var body = document.createElement('div');
     fixedDepthPath.setAttribute('class', 'sidebar-lang-fixed-depth');
     body.className = 'clbi-left-content toc-sidebar-content';
    fixedDepthPath.setAttribute('d', getSidebarLanguageShellPath());
    svg.appendChild(fixedDepthPath);


     fixedFocus = createSidebarLanguageSvgEl('path');
     var list = document.createElement('ul');
     fixedFocus.setAttribute('class', 'sidebar-lang-fixed-focus');
     list.className = 'generated-toc';
    fixedFocus.setAttribute('d', getSidebarLanguageSectorPath(-state.halfSector, state.halfSector));
    svg.appendChild(fixedFocus);


     shadowSoft = createSidebarLanguageSvgEl('path');
     headings.forEach(function (heading) {
    shadowSoft.setAttribute('class', 'sidebar-lang-inner-shadow-soft');
        var id = getHeadingId(heading);
    shadowSoft.setAttribute('d', getSidebarLanguageShellPath());
        var text = getHeadingText(heading);
    svg.appendChild(shadowSoft);
        var level = heading.tagName.toLowerCase() === 'h3' ? 3 : 2;


    shadowHard = createSidebarLanguageSvgEl('path');
        var item = document.createElement('li');
    shadowHard.setAttribute('class', 'sidebar-lang-inner-shadow-hard');
        item.className = 'toc-level-' + level;
    shadowHard.setAttribute('d', getSidebarLanguageShellPath());
    svg.appendChild(shadowHard);


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


    pointer = createSidebarLanguageSvgEl('g');
        textWrap.appendChild(textSpan);
    pointer.setAttribute('class', 'sidebar-lang-fixed-pointer');
        link.appendChild(textWrap);
    pointer.setAttribute('clip-path', 'url(#clbi-sidebar-language-fan-clip)');


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


     line = createSidebarLanguageSvgEl('line');
     body.appendChild(list);
    line.setAttribute('class', 'sidebar-lang-pointer-line');
     tocBox.appendChild(title);
     line.setAttribute('x1', String(state.cx));
     tocBox.appendChild(body);
     line.setAttribute('x2', String(state.cx));
     leftSidebar.appendChild(tocBox);
    line.setAttribute('y1', '24');
    line.setAttribute('y2', '112');
     pointer.appendChild(line);


     svg.appendChild(pointer);
     // 왼쪽 목차: DOM 배치가 끝난 뒤 긴 제목 스크롤 여부를 계산한다.
    fan.appendChild(svg);
    requestAnimationFrame(function () {
        initTocTitleScroll(tocBox);


    state.rotor = rotor;
        setTimeout(function () {
     setSidebarLanguageRotation(state.rotation, false);
            initTocTitleScroll(tocBox);
        }, 120);
     });
}
}


function updateSidebarLanguageDial() {
 
    var state = sidebarLanguageState;
// 우측 광고판: 이미지/번역 캡션 목록
    var meta = getSidebarLanguagePreviewMeta();
var RIGHT_BILLBOARD_ITEMS = [
     var status = getSidebarLanguageStatus(meta);
     {
    var selector = document.getElementById('clbi-sidebar-lang-selector');
        file: 'Side-visual-001.png',
     var apply = document.getElementById('clbi-sidebar-lang-apply');
        alt: 'PROOF TO THE WORLD / YOU ONCE PART OF IT',
     var selectedValue = document.getElementById('clbi-sidebar-lang-selected-value');
        duration: 3000,
     var availabilityPanel = document.getElementById('clbi-sidebar-lang-availability-panel');
        caption: [
    var availabilityValue = document.getElementById('clbi-sidebar-lang-availability-value');
            '"당신이 한때 이 세계의',
 
            '',
    if (selectedValue) {
            '일부였다는 것을 증명하십시오"'
         selectedValue.textContent = meta.name;
        ]
    },
     {
        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: [
            '"지금 지원하세요!"'
         ]
     }
     }
];


    if (availabilityPanel) {
function getRightBillboardItem(index) {
        availabilityPanel.classList.remove('is-ready', 'is-current', 'is-locked');
    var items = RIGHT_BILLBOARD_ITEMS;
        availabilityPanel.classList.add(status.className);
    }


     if (availabilityValue) {
     if (!items || !items.length) {
         availabilityValue.textContent = status.label;
         return {
            file: 'Side-visual-001.png',
            alt: '',
            caption: []
        };
     }
     }


     if (apply) {
     var normalized = index % items.length;
        apply.classList.toggle('is-disabled', !status.canApply);
    if (normalized < 0) normalized += items.length;
        apply.setAttribute('aria-disabled', status.canApply ? 'false' : 'true');
        apply.setAttribute('aria-label', status.canApply ? (meta.name + ' 적용') : (meta.isCurrent ? '현재 언어' : '사용할 수 없는 언어'));
    }


     if (selector) {
     return items[normalized];
        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 {
function getRightBillboardImageUrl(fileName) {
        meta: meta,
     return '/index.php?title=특수:Redirect/file/' + encodeURIComponent(fileName || 'Side-visual-001.png');
        status: status
    };
}
}


function setSidebarLanguageRotation(value, animate) {
function getRightBillboardCaptionHtml(item) {
     var state = sidebarLanguageState;
     var lines = item && item.caption ? item.caption : [];
     state.rotation = value;
     var html = '';
    updateSidebarLanguageDial();


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


     if (animate) {
     lines.forEach(function(line) {
         $('#clbi-sidebar-lang-selector').addClass('is-snapping');
         var text = String(line == null ? '' : line);
    } else {
        var isGap = !text.trim();
         $('#clbi-sidebar-lang-selector').removeClass('is-snapping');
        var className = 'right-billboard-caption-line' + (isGap ? ' is-gap' : '');
     }
         html += '<span class="' + className + '">' + (isGap ? '&nbsp;' : escapeCaptionText(text)) + '</span>';
     });


     state.rotor.style.transform = 'rotate(' + state.rotation.toFixed(3) + 'deg)';
     return html;
}
}


function requestSidebarLanguageRotation(value) {
function setRightBillboardItem(index) {
     var state = sidebarLanguageState;
     var box = document.querySelector('.right-billboard-box');
     state.pendingRotation = value;
    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');


     if (state.raf) return;
     box.setAttribute('data-billboard-index', String(index));
    box.classList.remove('is-empty');


     state.raf = requestAnimationFrame(function() {
     Array.prototype.forEach.call(images, function(img) {
         state.raf = null;
         img.style.display = '';
         setSidebarLanguageRotation(state.pendingRotation, false);
         img.setAttribute('src', src);
        img.setAttribute('alt', img.classList.contains('right-billboard-image-base') ? (item.alt || '') : '');
     });
     });
}


function cancelSidebarLanguageSpin() {
    if (caption) {
    var state = sidebarLanguageState;
        caption.innerHTML = getRightBillboardCaptionHtml(item);
    }


     if (state.inertiaRaf) {
     if (emptySub) {
         cancelAnimationFrame(state.inertiaRaf);
         emptySub.textContent = item.file || 'Side-visual-001.png';
        state.inertiaRaf = null;
     }
     }
    $('#clbi-sidebar-lang-selector').removeClass('is-spinning');
}
}


function finishSidebarLanguageSnap(nearestIndex, callback) {
function getRightBillboardItemDuration(item) {
     var state = sidebarLanguageState;
     var duration = item && item.duration ? parseInt(item.duration, 10) : 3000;


     state.baseIndex = normalizeSidebarLanguageIndex(nearestIndex);
     if (Number.isNaN(duration) || duration < 500) {
    state.selectedIndex = state.baseIndex;
        duration = 3000;
     state.rotation = 0;
     }
    state.dragging = false;


     $('#clbi-sidebar-lang-selector').removeClass('is-snapping is-dragging is-spinning');
     return duration;
}


     renderSidebarLanguageWheel();
function initRightBillboardCarousel() {
     updateSidebarLanguageDial();
     var box = document.querySelector('.right-billboard-box');
     if (!box || box.getAttribute('data-billboard-ready') === '1') return;


     if (typeof callback === 'function') {
     box.setAttribute('data-billboard-ready', '1');
        callback(getSidebarLanguageMeta(state.order[state.selectedIndex]));
    box.setAttribute('data-billboard-index', '0');
    }
}


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


     cancelSidebarLanguageSpin();
     if (!RIGHT_BILLBOARD_ITEMS || RIGHT_BILLBOARD_ITEMS.length <= 1) return;
    clearTimeout(state.snapTimer);


     state.selectedIndex = nearestIndex;
     function scheduleNext() {
    setSidebarLanguageRotation(targetRotation, !!animate);
        var current = parseInt(box.getAttribute('data-billboard-index') || '0', 10);
        if (Number.isNaN(current)) current = 0;


    state.snapTimer = setTimeout(function() {
        var currentItem = getRightBillboardItem(current);
         finishSidebarLanguageSnap(nearestIndex, callback);
         var delay = getRightBillboardItemDuration(currentItem);
    }, animate ? 230 : 0);
}


function snapSidebarLanguageNearest(callback) {
        window.setTimeout(function() {
    var state = sidebarLanguageState;
            if (!document.body.contains(box)) return;
    var step = Math.round(-state.rotation / state.sectorAngle);
    snapSidebarLanguageToStep(step, true, callback);
}


function startSidebarLanguageInertiaSpin(initialVelocity) {
            if (!document.hidden) {
    var state = sidebarLanguageState;
                setRightBillboardItem(current + 1);
    var velocity;
            }
    var lastFrame;


    cancelSidebarLanguageSpin();
            scheduleNext();
        }, delay);
    }


     velocity = Math.max(-state.maxSpinVelocity, Math.min(state.maxSpinVelocity, initialVelocity));
     scheduleNext();
}


    if (Math.abs(velocity) < state.minSpinVelocity) {
        snapSidebarLanguageNearest();
        return;
    }


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


    function frame(now) {
function buildRightBillboardBox() {
        var dt = Math.min(34, Math.max(1, now - lastFrame));
    var billboardInitial = getRightBillboardItem(0);
        var sign = velocity < 0 ? -1 : 1;
    var billboardSrc = getRightBillboardImageUrl(billboardInitial.file);
        var nextSpeed;


         lastFrame = now;
    return '' +
        state.rotation += velocity * dt;
         '<div class="clbi-right-box right-billboard-box" data-billboard-index="0">' +
         setSidebarLanguageRotation(state.rotation, false);
            '<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() {
    return '' +
        '<div class="clbi-right-box site-info-sidebar">' +
            '<div class="clbi-right-title site-info-title">' +
                '<span>SITE INFORMATION</span>' +
                '<span class="site-info-title-meta">LINKS</span>' +
            '</div>' +
            '<div class="clbi-right-content site-info-content">' +
                '<div class="policy-list">' +
                    '<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>' +
                    '<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>' +
                '</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>';
}


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


        if (nextSpeed <= state.minSpinVelocity) {
// 초기화 함수
            state.inertiaRaf = null;
function initSidebars() {
            $('#clbi-sidebar-lang-selector').removeClass('is-spinning');
    var header = $('.liberty-content-header');
            snapSidebarLanguageNearest();
    var content = $('.liberty-content');
            return;
        }


         state.inertiaRaf = requestAnimationFrame(frame);
    if (header.length && content.length) {
         header.prependTo(content);
     }
     }


     state.inertiaRaf = requestAnimationFrame(frame);
     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 scheduleSidebarLanguageNavigation(meta) {
        var userBox;
    var status = getSidebarLanguageStatus(meta);


    if (!meta || !status.canApply) return;
        if (isLoggedIn) {
 
            userBox =
    clearTimeout(sidebarLanguageState.navigateTimer);
                '<div class="clbi-right-box profile-card-box">' +
    sidebarLanguageState.navigateTimer = setTimeout(function() {
                    '<div id="clbi-user-avatar-wrap" class="profile-identity-panel">' +
        var title = getLanguageTargetTitle(meta.lang);
                        '<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\';">' +
        if (!title || meta.lang === getCurrentLang()) return;
                        '</div>' +
 
                        '<div id="clbi-user-name-row" class="profile-name-row">' +
        window.location.href = buildWikiPath(title);
                            '<a href="/index.php/사용자:' + username + '" id="clbi-user-name">' + username + '</a>' +
    }, 70);
                        '</div>' +
}
                    '</div>' +
 
                    '<div class="clbi-right-content profile-action-box">' +
function setSidebarLanguageSelection(lang) {
                        '<div class="profile-quick-actions" aria-label="프로필 빠른 메뉴">' +
    var state = sidebarLanguageState;
                            '<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>' +
    var index = state.order.indexOf(lang);
                            '<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>' +
    if (index < 0) index = state.order.indexOf(getCurrentLang());
                        '</div>' +
    if (index < 0) index = 0;
                        '<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>' +
    if (state.raf) {
                        '<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>' +
         cancelAnimationFrame(state.raf);
                        '<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>' +
         state.raf = null;
                    '</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 = '';


    cancelSidebarLanguageSpin();
        try {
    clearTimeout(state.snapTimer);
            rightVisualBox = buildRightBillboardBox();
        } catch (err) {
            console.error('Right billboard build failed:', err);
            rightVisualBox = '';
        }


    state.currentLang = lang;
        try {
    state.baseIndex = index;
            siteInformationBox = buildSiteInformationBox();
    state.selectedIndex = index;
        } catch (err) {
    state.rotation = 0;
            console.error('Site information build failed:', err);
    state.dragging = false;
            siteInformationBox = '';
    state.dragMoved = false;
        }
    state.releaseVelocity = 0;


    renderSidebarLanguageWheel();
    updateSidebarLanguageDial();
}


function moveSidebarLanguageSelection(delta) {
        var sidebar = userBox + rightVisualBox + siteInformationBox;
    snapSidebarLanguageToStep(-delta, true);
}


function bindSidebarLanguageSelector() {
        $('.content-wrapper').append('<div id="clbi-right-sidebar">' + sidebar + '</div>');
    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;
     if ($('#clbi-left-sidebar').length === 0) {
var leftSidebar =
    '<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">' +


    state.bound = true;
                '<div class="news-feed-title" id="clbi-left-news-changelog-title">CHANGELOG</div>' +
    state.boundElement = selector;
                '<div class="news-left-changelog-feed">' +
 
                    '<a href="/index.php/체인지로그" class="news-post-item">' +
    fan.addEventListener('pointerdown', function(e) {
                        '<div class="news-post-title-wrap">' +
        cancelSidebarLanguageSpin();
                            '<span class="news-post-title" id="clbi-left-news-changelog-main">체인지로그</span>' +
        clearTimeout(state.snapTimer);
                            '<i class="hn hn-angle-right-solid news-post-arrow" aria-hidden="true"></i>' +
                        '</div>' +
                        '<span class="news-post-tag">POST</span>' +
                    '</a>' +
                '</div>' +


        state.dragging = true;
                '<div class="news-divider"></div>' +
        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');
                '<div class="news-feed-title" id="clbi-left-news-recent-title">RECENT CHANGES</div>' +
        selector.classList.remove('is-snapping');
                '<div class="news-left-recent-feed" id="clbi-left-recent-list">불러오는 중...</div>' +


        /*
            '</div>' +
        Vertical page scrolling must stay available when the pointer starts on
         '</div>' +
        the language dial. Capture and preventDefault are delayed until a
     '</div>';
        horizontal drag is confirmed.
         */
     });


    fan.addEventListener('pointermove', function(e) {
        $('.content-wrapper').prepend(leftSidebar);
        var now;
        var totalDx;
        var totalDy;
        var frameDx;
        var dt;
        var instantVelocity;


         if (!state.dragging) return;
         renderSidebarLanguageBox();
        loadRecentChangesList('#clbi-left-recent-list', 10);
        updateLeftSidebarNationsImage();
    }


         totalDx = e.clientX - state.dragStartX;
    try {
         totalDy = (e.clientY || 0) - state.dragStartY;
         initRightBillboardCarousel();
    } catch (err) {
         console.error('Right billboard carousel failed:', err);
    }


        if (!state.dragAxis && (Math.abs(totalDx) > 4 || Math.abs(totalDy) > 4)) {
    applyMainPageStyle();
            state.dragAxis = Math.abs(totalDx) >= Math.abs(totalDy) ? 'x' : 'y';
    initCategoryNavIfAvailable(document);


            if (state.dragAxis === 'y') {
    if (window.ProgressSystemWebUi && typeof window.ProgressSystemWebUi.boot === 'function') {
                state.dragging = false;
        window.ProgressSystemWebUi.boot('initSidebars');
                state.dragMoved = false;
    }
                state.dragAxis = null;
                state.pointerCaptured = false;
                selector.classList.remove('is-dragging');
                return;
            }


            if (fan.setPointerCapture && e.pointerId != null) {
    $('#side-toc-box').remove();
                try {
                    fan.setPointerCapture(e.pointerId);
                    state.pointerCaptured = true;
                } catch (err) {
                    state.pointerCaptured = false;
                }
            }
        }


        if (state.dragAxis !== 'x') return;
    mw.loader.using(['mediawiki.api']).then(function() {
        setTimeout(function() {
            initNotifications();
            initProfile();
            moveTocToLeftSidebar();
        }, 300);


         now = performance.now();
         setTimeout(moveTocToLeftSidebar, 800);
         frameDx = e.clientX - state.lastX;
         setTimeout(moveTocToLeftSidebar, 1500);
        dt = Math.max(1, now - state.lastTime);
    });
}


        if (Math.abs(totalDx) > 3) state.dragMoved = true;
$(function() {
 
    loadLangScript(function() {
        instantVelocity = (frameDx * state.dragSensitivity) / dt;
         setTimeout(function() {
         state.releaseVelocity = (state.releaseVelocity * 0.62) + (instantVelocity * 0.38);
            initSidebars();
        state.lastX = e.clientX;
         }, 100);
        state.lastTime = now;
 
        requestSidebarLanguageRotation(state.dragStartRotation + totalDx * state.dragSensitivity);
         e.preventDefault();
        e.stopPropagation();
     });
     });
});


    function finishDrag(e) {
$(document).on('click.profileQuickPlaceholder', '#profile-quick-inventory, #profile-quick-achievements', function(e) {
        var velocityAge;
    e.preventDefault();
        var throwVelocity;
    e.stopPropagation();
        var wasHorizontal;
});


        if (!state.dragging) return;
// SPA 네비게이션
function shouldSkip(url) {
    return url.match(/action=edit|action=submit|action=history|action=delete|action=protect|action=purge|특수:로그인|특수:로그아웃|Special:UserLogin|Special:UserLogout|특수:사용자정보|특수:비밀번호바꾸기|uselang=/);
}


        wasHorizontal = state.dragAxis === 'x';
$(function() {
        state.dragging = false;
    if (window._spaInitialized) return;
        selector.classList.remove('is-dragging');
    window._spaInitialized = true;


        if (fan.releasePointerCapture && state.pointerCaptured && e && e.pointerId != null) {
    function isInternal(url) {
            try { fan.releasePointerCapture(e.pointerId); } catch (err) {}
        var a = document.createElement('a');
         }
        a.href = url;
         return a.hostname === window.location.hostname;
    }


        state.pointerCaptured = false;
    function loadPage(url) {
         state.dragAxis = null;
         invalidateProfileRender();


         if (!wasHorizontal && !state.dragMoved) {
         fetch(url)
            return;
            .then(function(res) {
        }
                return res.text();
            })
            .then(function(html) {
                var parser = new DOMParser();
                var doc = parser.parseFromString(html, 'text/html');


        velocityAge = performance.now() - state.lastTime;
                var scripts = doc.querySelectorAll('script');
        throwVelocity = velocityAge > 120 ? 0 : state.releaseVelocity;
                for (var i = 0; i < scripts.length; i++) {
                    var src = scripts[i].textContent;


        if (state.dragMoved) {
                    if (src.indexOf('wgNamespaceNumber') !== -1) {
            state.suppressClickUntil = performance.now() + 180;
                        var match = src.match(/"wgNamespaceNumber":(-?\d+)/);
        }
                        if (match) mw.config.set('wgNamespaceNumber', parseInt(match[1], 10));


        if (state.dragMoved && Math.abs(throwVelocity) >= state.minSpinVelocity) {
                        var matchTitle = src.match(/"wgTitle":"([^"]+)"/);
            startSidebarLanguageInertiaSpin(throwVelocity);
                        if (matchTitle) mw.config.set('wgTitle', matchTitle[1]);
        } else {
            snapSidebarLanguageNearest();
        }


        if (e) {
                        var matchPage = src.match(/"wgPageName":"([^"]+)"/);
            e.preventDefault();
                        if (matchPage) mw.config.set('wgPageName', matchPage[1]);
            e.stopPropagation();
        }
    }


    fan.addEventListener('pointerup', finishDrag);
                        var matchArticle = src.match(/"wgArticleId":(\d+)/);
    fan.addEventListener('pointercancel', finishDrag);
                        if (matchArticle) {
    fan.addEventListener('lostpointercapture', function() {
                            mw.config.set('wgArticleId', parseInt(matchArticle[1], 10));
        if (!state.dragging) return;
                        } else {
                            mw.config.set('wgArticleId', 0);
                        }


        state.dragging = false;
                        var matchIsMainPage = src.match(/"wgIsMainPage":(true|false)/);
        state.pointerCaptured = false;
                        if (matchIsMainPage) {
        state.dragAxis = null;
                            mw.config.set('wgIsMainPage', matchIsMainPage[1] === 'true');
        selector.classList.remove('is-dragging');
                        } else {
                            mw.config.set('wgIsMainPage', false);
                        }


        if (state.dragMoved && Math.abs(state.releaseVelocity) >= state.minSpinVelocity) {
                        var matchSpecial = src.match(/"wgCanonicalSpecialPageName":"([^"]+)"/);
            state.suppressClickUntil = performance.now() + 180;
                        if (matchSpecial) {
            startSidebarLanguageInertiaSpin(state.releaseVelocity);
                            mw.config.set('wgCanonicalSpecialPageName', matchSpecial[1]);
        } else {
                        } else {
            snapSidebarLanguageNearest();
                            mw.config.set('wgCanonicalSpecialPageName', false);
        }
                        }
    });
                        break;
                    }
                }


    apply.addEventListener('click', function(e) {
                var newContent = doc.querySelector('.liberty-content-main');
        e.preventDefault();
                var newTitle = doc.querySelector('.mw-page-title-main');
        e.stopPropagation();
                var newHead = doc.querySelector('title');
                var newHeader = doc.querySelector('.liberty-content-header');


        snapSidebarLanguageNearest(function(meta) {
                if (newContent) {
            scheduleSidebarLanguageNavigation(meta);
                    $('#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');
                }


    selector.addEventListener('keydown', function(e) {
                if (newTitle) {
        if (e.key === 'ArrowLeft') {
                    $('.mw-page-title-main').html(newTitle.innerHTML);
            moveSidebarLanguageSelection(-1);
                }
            e.preventDefault();
        }


        if (e.key === 'ArrowRight') {
                if (newHead) {
            moveSidebarLanguageSelection(1);
                    document.title = newHead.textContent;
            e.preventDefault();
                }
        }


        if (e.key === 'Enter' || e.key === ' ') {
                if (newHeader) {
            apply.click();
                    $('.liberty-content-header').html(newHeader.innerHTML);
            e.preventDefault();
                }
        }
    });
}


function renderSidebarLanguageBox() {
                window.scrollTo(0, 0);
    bindSidebarLanguageSelector();
                mw.hook('wikipage.content').fire($('.liberty-content-main'));
    setSidebarLanguageSelection(getCurrentLang());
                applyMainPageStyle();
                initCategoryNavIfAvailable(document);


    if (!sidebarLanguageStatusLoaded) {
                if (window.ProgressSystemWebUi && typeof window.ProgressSystemWebUi.handleSpaPageView === 'function') {
        loadSidebarLanguageStatusRegistry(function() {
                    window.ProgressSystemWebUi.handleSpaPageView();
            setSidebarLanguageSelection(getCurrentLang());
                } else if (window.ProgressSystemWebUi && typeof window.ProgressSystemWebUi.boot === 'function') {
        });
                    window.ProgressSystemWebUi.boot('spa');
    }
                }
}


function loadRecentChangesList(targetSelector, limit) {
                $('#side-toc-box').remove();
    var $target = $(targetSelector);
                setTimeout(moveTocToLeftSidebar, 100);
                setTimeout(moveTocToLeftSidebar, 500);
                setTimeout(moveTocToLeftSidebar, 1200);


    if (!$target.length) return;
                mw.loader.using(['mediawiki.api']).then(function() {
                    initProfile();
                    moveTocToLeftSidebar();
                });
            });
    }


     var lang = getCurrentLang();
// 목차 링크는 전용 처리
     var t = (window.LANG && window.LANG[lang]) ? window.LANG[lang] : (window.LANG ? window.LANG.ko : null);
$(document).on('click', '#side-toc-box a, #toc a, .toc a', function(e) {
     var isNewsList = $target.closest('.clbi-left-news-box').length > 0;
     var href = $(this).attr('href');
     if (!href || href.charAt(0) !== '#') return;
 
     var rawId = href.slice(1);
    if (!rawId) return;
 
    var decodedId = rawId;


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


     $target.html((t && t.loading) ? t.loading : '불러오는 중...');
     var target = document.getElementById(decodedId);
 
    if (!target && window.CSS && CSS.escape) {
        target = document.querySelector('#' + CSS.escape(decodedId));
    }


     $.getJSON(
     if (!target) 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 = '';


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


            $.each(items, function(i, item) {
    var scrollTarget = target.closest('h2, h3') || target;
                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) {
    scrollTarget.scrollIntoView({
                    html +=
        behavior: 'auto',
                        '<a href="' + escapeHtml(pageHref) + '" class="news-recent-item">' +
        block: 'start'
                            '<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) {
    history.replaceState(null, '', '#' + rawId);
                $target.html(
});
                    '<div class="news-recent-viewport">' +
                        '<div class="news-recent-stack">' + html + '</div>' +
                    '</div>'
                );


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


            if (isNewsList && typeof scheduleAdaptiveLeftRecentItems === 'function') {
        var href = $(this).attr('href');
                scheduleAdaptiveLeftRecentItems();
        if (!href) return;
            }


            $target.find(isNewsList ? '.news-recent-item' : '.clbi-recent-item').each(function() {
        // 목차 링크는 별도 핸들러에서 처리
                var wrap = $(this).find(isNewsList ? '.news-recent-title-wrap' : '.clbi-recent-title-wrap');
        if ($(this).closest('#side-toc-box, #toc, .toc').length) return;
                var title = $(this).find(isNewsList ? '.news-recent-title' : '.clbi-recent-title');


                if (!wrap.length || !title.length) return;
        // 단순 해시 링크는 SPA 가로채기 제외
        if (href.startsWith('#')) return;


                var wrapW = wrap.width();
        var link = document.createElement('a');
                var titleW = title[0].scrollWidth;
        link.href = href;


                if (titleW > wrapW + 20) {
        var samePath = decodeURIComponent(link.pathname) === decodeURIComponent(window.location.pathname);
                    var duration = titleW / 40;
        var sameSearch = (link.search || '') === (window.location.search || '');


                    title.css({
         if (link.hash && samePath && sameSearch) return;
                        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 : '불러오기 실패');
         var currentBase = window.location.href.split('#')[0];
    });
        var targetBase = link.href.split('#')[0];
}


        if (link.hash && currentBase === targetBase) return;


function ensureRecentViewport() {
        if (!isInternal(href)) return;
    var list = document.getElementById('clbi-left-recent-list');
        if (shouldSkip(href)) return;
    var viewport;
    var stack;
    var children;


    if (!list) return null;
        e.preventDefault();
        playStaticSound();
        $('body').addClass('page-loading');
        history.pushState(null, '', href);
        loadPage(href);
    });


     viewport = list.querySelector(':scope > .news-recent-viewport');
     window.addEventListener('popstate', function() {
    stack = viewport ? viewport.querySelector(':scope > .news-recent-stack') : null;
         loadPage(window.location.href);
 
    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);
function timeAgo(timestamp) {
    var now = new Date();
     var date = new Date(timestamp);
     var diff = Math.floor((now - date) / 1000);


     return viewport;
     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 ensureNewsBottomFinish() {
// 펼접 토글
     var newsBox = document.querySelector('#clbi-left-sidebar .clbi-left-news-box');
// 펼접 토글
     var content = newsBox ? newsBox.querySelector('.clbi-news-box') : null;
function getFoldTexts() {
    var finish;
     var lang = getCurrentLang();
 
     return (window.LANG && window.LANG[lang])
    if (!content) return null;
         ? window.LANG[lang]
 
         : (window.LANG ? window.LANG.ko : { expand: '펼치기', collapse: '접기' });
    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() {
function refreshOpenAncestors($start) {
     /*
     $start.parents('[id^="collapsible"]').each(function () {
    134 기준: 좌측 사이드는 뉴스 확장형 flex 레이아웃이 높이를 담당한다.
        var $parent = $(this);
    이전 adaptive 코드는 항목을 숨기거나 mask/fade를 걸기 위한 것이었으므로
        if (!$parent.hasClass('folding-open')) return;
    여기서는 잔여 상태만 정리하고 DOM 래퍼만 보장한다.
    */
    resetLeftRecentAdaptiveState();


    if (typeof ensureRecentViewport === 'function') {
        // 이미 fully open 상태면 굳이 다시 잠그지 않음
        ensureRecentViewport();
        if ($parent.data('fold-state') === 'open') {
    }
            return;
        }


    if (typeof ensureNewsBottomFinish === 'function') {
        $parent.css('max-height', this.scrollHeight + 'px');
        ensureNewsBottomFinish();
     });
     }
 
    if (typeof scheduleClbiContentBottomGap === 'function') {
        scheduleClbiContentBottomGap();
    }
}
}


function scheduleAdaptiveLeftRecentItems() {
function bindInnerResizeUpdates($target) {
     window.requestAnimationFrame(function () {
     // 이미지 늦게 로드될 때 높이 갱신
         updateAdaptiveLeftRecentItems();
    $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);
        }
     });
     });
    window.setTimeout(updateAdaptiveLeftRecentItems, 80);
    window.setTimeout(updateAdaptiveLeftRecentItems, 240);
}
}


function openFold($target, $btn) {
    var t = getFoldTexts();


    $target.data('fold-state', 'opening');
    $target.addClass('folding-open');


function updateClbiContentBottomGap(iteration) {
    // 열린 뒤 자연 확장 가능하게 만들기 위해 먼저 px로 열기
     var content = document.querySelector('.container-fluid.liberty-content');
     $target.css('max-height', '0px');
     var main = document.querySelector('.liberty-content-main');
     $target[0].offsetHeight;
     var bottomNav = document.getElementById('clbi-bottom-nav-wrap');
     $target.css('max-height', $target[0].scrollHeight + 'px');
    var desiredGap = 8;
    var rootStyle;
    var scale = 1;
    var contentRect;
    var bottomTop;
    var targetHeight;
    var currentHeight;
    var visualGap;


     iteration = iteration || 0;
     $btn.text(t.collapse);


     if (!content || !main || !bottomNav) return;
     bindInnerResizeUpdates($target);


     /*
     // 바깥 펼접 즉시 갱신
     하단 간격은 scale 모드에서도 같은 기준으로 계산한다.
     refreshOpenAncestors($target);
    transform:scale()이 걸리면 getBoundingClientRect()는 축소된 화면 좌표를 반환하므로,
    목표 간격 8px도 scale을 곱한 화면 좌표로 비교하고 다시 design px로 환산한다.


     목표:
     // 전환 끝나면 none으로 풀어서 중첩 펼접/동적 내용 증가를 자연스럽게 허용
     .liberty-content-main.bottom === #clbi-bottom-nav-wrap.top - 8px
     $target.off('transitionend.foldopen').on('transitionend.foldopen', function (e) {
    */
         if (e.target !== this) return;
    if (document.body && document.body.classList && document.body.classList.contains('clbi-shell-vertical-scale')) {
         if (!$target.hasClass('folding-open')) return;
         rootStyle = window.getComputedStyle(document.documentElement);
         scale = parseFloat(rootStyle.getPropertyValue('--clbi-shell-scale')) || 1;
        scale = Math.max(0.25, scale);
    }


    contentRect = content.getBoundingClientRect();
        $target.css('max-height', 'none');
    bottomTop = bottomNav.getBoundingClientRect().top;
        $target.data('fold-state', 'open');
    visualGap = desiredGap * scale;
    targetHeight = Math.floor((bottomTop - contentRect.top - visualGap) / scale);
    targetHeight = Math.max(120, targetHeight);


    currentHeight = Math.round(content.getBoundingClientRect().height / scale);
        refreshOpenAncestors($target);
    });


     content.style.setProperty('--clbi-content-extra', '0px');
     // 늦게 렌더되는 콘텐츠 대응
    content.style.setProperty('height', targetHeight + 'px', 'important');
    requestAnimationFrame(function () {
    content.style.setProperty('max-height', targetHeight + 'px', 'important');
        if ($target.hasClass('folding-open') && $target.data('fold-state') !== 'open') {
 
            $target.css('max-height', $target[0].scrollHeight + 'px');
    if (Math.abs(currentHeight - targetHeight) >= 1 && iteration < 4) {
             refreshOpenAncestors($target);
        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);
}


    setTimeout(function () {
        if ($target.hasClass('folding-open') && $target.data('fold-state') !== 'open') {
            $target.css('max-height', $target[0].scrollHeight + 'px');
            refreshOpenAncestors($target);
        }
    }, 80);


function updateLeftBillboardAdaptive() {
    setTimeout(function () {
    /*
        if ($target.hasClass('folding-open') && $target.data('fold-state') !== 'open') {
    134 기준: Ad는 이미지/CRT 비율을 유지하는 고정 슬롯이다.
            $target.css('max-height', $target[0].scrollHeight + 'px');
    남는 세로 공간은 뉴스 박스가 흡수하므로, Ad에 하단 finish를 늘리거나
            refreshOpenAncestors($target);
    title-only 상태로 접는 adaptive 보정은 사용하지 않는다.
        }
    */
     }, 220);
     resetLeftBillboardAdaptiveState();
}
}


function scheduleLeftBillboardAdaptive() {
function closeFold($target, $btn) {
     window.requestAnimationFrame(updateLeftBillboardAdaptive);
     var t = getFoldTexts();
    window.setTimeout(updateLeftBillboardAdaptive, 80);
    window.setTimeout(updateLeftBillboardAdaptive, 240);
}


function scheduleLeftSidebarVerticalFit() {
    // none 상태에서 닫으면 transition이 안 되므로 실제 높이로 고정
     if (typeof scheduleAdaptiveLeftRecentItems === 'function') {
     if ($target.css('max-height') === 'none' || $target.data('fold-state') === 'open') {
         scheduleAdaptiveLeftRecentItems();
        $target.css('max-height', $target[0].scrollHeight + 'px');
    } else {
         $target.css('max-height', $target[0].scrollHeight + 'px');
     }
     }


     if (typeof scheduleLeftBillboardAdaptive === 'function') {
     $target.data('fold-state', 'closing');
        scheduleLeftBillboardAdaptive();
    $target[0].offsetHeight;
     }
    $target.css('max-height', '0px');
    $target.removeClass('folding-open');
 
     $btn.text(t.expand);


     window.setTimeout(function () {
     refreshOpenAncestors($target);
        if (typeof scheduleAdaptiveLeftRecentItems === 'function') {
            scheduleAdaptiveLeftRecentItems();
        }


        if (typeof scheduleLeftBillboardAdaptive === 'function') {
    setTimeout(function () {
            scheduleLeftBillboardAdaptive();
        refreshOpenAncestors($target);
         }
         $target.data('fold-state', 'closed');
     }, 120);
     }, 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;
function updateLeftSidebarNationsImage() {
    $('#clbi-left-nations-image').remove();
}


function setProfileActionLabel(selector, text) {
            if ($target.hasClass('folding-open')) {
    var target = $(selector);
                closeFold($target, $btn);
    var label = target.find('.profile-action-label');
            } else {
                openFold($target, $btn);
            }


    if (label.length) {
            window.scrollTo(0, scrollY);
         label.text(text);
         });
    } else {
});
        target.text(text);
    }
}


// 사이드바 업데이트
// ========== 프로필 시스템 ==========
function updateSidebar() {
function initProfile() {
     if (!window.LANG) {
     $('.profile-card').remove();
        setTimeout(updateSidebar, 100);
    $('.user-profile-portal').removeClass('user-profile-portal');
        return;
    }


     var currentLang = getCurrentLang();
     var token = ++PROFILE_RENDER_TOKEN;
     var t = (window.LANG && window.LANG[currentLang]) ? window.LANG[currentLang] : window.LANG.ko;
    var ns = mw.config.get('wgNamespaceNumber');
     var title = mw.config.get('wgTitle');
    var specialPage = mw.config.get('wgCanonicalSpecialPageName');
    var isProfileSettings = specialPage === '사용자정보';


     var newsTitle = t.news || '뉴스';
     $('body').toggleClass('user-profile-page', ns === 2);
    var changelogTitle = t.changelog || '체인지로그';
     $('body').toggleClass('user-profile-settings-page', isProfileSettings);
     var recentTitle = t.recentChanges || '최근 변경';
    var languageTitle = t.language || '언어';


     $('#clbi-title-left-language').text(languageTitle);
     if (ns === 2) {
    renderSidebarLanguageBox();
        var profileUser = title.split('/')[0];
        renderProfile(profileUser, token);
    }


     $('#clbi-title-left-news').text(newsTitle);
     if (isProfileSettings) {
     $('#clbi-left-news-changelog-main').text(changelogTitle);
        initUserProfilePage();
    $('#clbi-left-news-recent-main').text(recentTitle);
     }
}


    $('#clbi-title-search a').text(t.search);
function renderProfile(username, token) {
     $('#clbi-search-input').attr('placeholder', t.search + '...');
    var api = new mw.Api();
     $('#clbi-title-recent a').text(recentTitle);
     api.get({
    $('#clbi-title-guide-label').text(t.guide);
        action: 'query',
    $('#clbi-guide-link').text(t.getStarted);
        list: 'users',
    $('#clbi-title-links-label').text(t.links);
        ususers: username,
        usprop: 'editcount'
     }).then(function(data) {
        if (token !== PROFILE_RENDER_TOKEN) return;
        if (mw.config.get('wgNamespaceNumber') !== 2) return;


    setProfileActionLabel('#clbi-btn-contribution', t.contribution);
        var currentTitle = String(mw.config.get('wgTitle') || '').split('/')[0];
    setProfileActionLabel('#clbi-btn-watchlist', t.watchlist);
        if (currentTitle !== username) return;
    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 user = data.query.users[0];
    var specialPage = String(mw.config.get('wgCanonicalSpecialPageName') || '');
        var contentEl = document.getElementById('mw-content-text');
        if (!contentEl) return;


$('#clbi-left-news-changelog-main').text(changelogTitle);
        var pageContent = contentEl.querySelector('.mw-parser-output') || contentEl;
$('#clbi-left-news-recent-title').text('RECENT CHANGES');
        injectProfileCard(username, user, pageContent);
    });
}


     $('.clbi-user-btn').removeClass('clbi-user-btn-active');
function injectProfileCard(username, userData, container) {
     var isOwnPage = mw.config.get('wgUserName') === username;
    var editCount = (userData && userData.editcount) ? userData.editcount : 0;


     if (
     function escapeHtml(value) {
         specialPage === 'Contributions' ||
         return String(value == null ? '' : value)
        specialPage === '기여' ||
            .replace(/&/g, '&amp;')
        pageName.indexOf('특수:기여') === 0 ||
            .replace(/</g, '&lt;')
        pageName.indexOf('Special:Contributions') === 0
            .replace(/>/g, '&gt;')
    ) {
            .replace(/"/g, '&quot;')
        $('#clbi-btn-contribution').addClass('clbi-user-btn-active');
            .replace(/'/g, '&#039;');
     }
     }


     if (specialPage === 'Watchlist') {
     container.classList.add('user-profile-portal');
        $('#clbi-btn-watchlist').addClass('clbi-user-btn-active');
    }


     if (
     var safeUsername = escapeHtml(username);
        specialPage === '설정' ||
    var avatarSrc = '/index.php?title=특수:Redirect/file/Pfp-' + encodeURIComponent(username) + '.png&width=220';
        pageName === '특수:설정' ||
    var fallbackSrc = '/index.php?title=특수:Redirect/file/Pfp-default.png&width=220';
        pageName === 'Special:설정'
     var editBtn = isOwnPage
     ) {
         ? '<a href="/index.php/특수:사용자정보" class="profile-edit-btn"><span class="profile-edit-label">프로필 수정</span><span class="profile-edit-arrow">›</span></a>'
         $('#clbi-btn-preferences').addClass('clbi-user-btn-active');
        : '';
    }


     $('.toggleBtn').each(function() {
     var progressHtml = isOwnPage
         var btn = $(this);
        ? '<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>'
        : '';


        if (!$('#' + btn.data('target')).hasClass('folding-open')) {
    var card = document.createElement('div');
             btn.text(t.expand);
    card.className = 'profile-card profile-page-console';
         } else {
    card.innerHTML =
             btn.text(t.collapse);
        '<div class="profile-card-titlebar">' +
        }
            '<span>USER PROFILE</span>' +
    });
             '<span>OFFICIAL ARCHIVE</span>' +
 
        '</div>' +
    updateLeftSidebarNationsImage();
         '<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>';


function canShowContentTools() {
    $('.profile-card').remove();
     // 비로그인 사용자는 편집/역사/공유 버튼을 숨김
     container.insertBefore(card, container.firstChild);
     if (!mw.config.get('wgUserName')) {
     loadProfileFields(username, card);
        return false;
    loadProfileContributionPages(username, card);
     }
     updateProfilePageEnvironment(card, null);


    // MediaWiki가 현재 문서를 편집 가능하지 않다고 판단하면 숨김
     if (isOwnPage) {
    var isEditable = mw.config.get('wgIsProbablyEditable');
         loadProfileProgressForUserPage(card);
     if (isEditable === false) {
         return false;
     }
     }
}


     var relevantEditable = mw.config.get('wgRelevantPageIsProbablyEditable');
function getProfileLanguageLabel() {
     if (relevantEditable === false) {
     var lang = getCurrentLang();
        return false;
     return SIDEBAR_LANGUAGE_LABELS[lang] || (lang ? lang.toUpperCase() : '—');
    }
 
    return true;
}
}


function moveCatlinksToBottom() {
function updateProfilePageEnvironment(card, summary) {
     var main = $('.liberty-content-main');
     if (!card) return;
    var parserOutput = $('.liberty-content-main .mw-parser-output').first();
    var catlinks = $('.catlinks');


     if (!main.length || !catlinks.length) return;
     var timezone = summary && summary.timezone ? summary.timezone : 'UTC';
    var timeEl = card.querySelector('[data-profile-time]');
    var langEl = card.querySelector('[data-profile-language]');


     catlinks.each(function () {
     if (timeEl) {
         var cat = $(this);
         try {
 
            timeEl.textContent = timezone + ' ' + new Intl.DateTimeFormat('ko-KR', {
         if (parserOutput.length) {
                hour: '2-digit',
             cat.appendTo(parserOutput);
                minute: '2-digit',
        } else {
                hour12: false,
             cat.appendTo(main);
                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) {
function initCategoryNavIfAvailable(root) {
         langEl.textContent = getProfileLanguageLabel();
    /*
    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() {
function loadProfileContributionPages(username, card) {
     /*
     if (!username || !card) return;
     기존 대문 전용 레거시 요소 정리
     if (!mw.loader || typeof mw.loader.using !== 'function') return;
    -----------------------------------------
    이전 대문 구조에서는 Common.js가 본문 바깥에 #clbi-main-logo를 직접 삽입하고,
    본문 안의 #clbi-main-crt-hero를 #clbi-main-crt-hero-wrap으로 감싸서
    .liberty-content-main 위쪽으로 재배치했다.


     새 대문은 본문 내부의 .main-portal이 로고, 알림, 카테고리 네비, 이미지 피드,
     var valueEl = card.querySelector('[data-contrib-pages-value]');
    방명록, 상태 패널을 모두 담당한다. 따라서 Common.js가 별도 로고나 CRT 래퍼를
     if (valueEl) valueEl.textContent = 'SYNC';
     삽입하면 새 로고/콘텐츠와 중복된다.


     여기서는 JS가 만들던 바깥 로고와 CRT 래퍼를 제거하고, 예전 대문 원본이나
     mw.loader.using(['mediawiki.api']).then(function () {
    캐시된 렌더 결과에 남아 있을 수 있는 #clbi-main-crt-hero도 제거한다.
        var api = new mw.Api();
    */
        var pages = Object.create(null);
    $('#clbi-main-logo').remove();
        var cont = {};
    $('#clbi-main-crt-hero-wrap').remove();
        var guard = 0;
    $('#clbi-main-crt-hero').remove();
 
}
        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 : [];


function setNativePageTitleHiddenHard(hidden) {
                rows.forEach(function (row) {
    var selectors = [
                    if (row && row.title) pages[row.title] = true;
        '.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 (data && data.continue && data.continue.uccontinue && guard < 40) {
        if (!node || !node.style) return;
                    cont = data.continue;
                    return requestNext();
                }


        if (hidden) {
                if (valueEl) valueEl.textContent = Object.keys(pages).length;
            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');
         }
         }
        requestNext().fail(function () {
            if (valueEl) valueEl.textContent = '—';
        });
     });
     });
}
}


function applyDefaultPageTitleVisibility() {
function loadProfileProgressForUserPage(card) {
     var hideTitle = true;
     if (!mw.config.get('wgUserName')) return;
     var isSystemAssetPage = false;
     if (!mw.loader || typeof mw.loader.using !== 'function') return;


     if (window.CLBI_PAGE_SHELL && typeof window.CLBI_PAGE_SHELL.isSystemAssetPage === 'function') {
     mw.loader.using(['mediawiki.api']).then(function () {
        isSystemAssetPage = window.CLBI_PAGE_SHELL.isSystemAssetPage();
        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);
        });
     });
}


    if (isSystemAssetPage) {
function updateUserPageProgress(card, summary) {
        hideTitle = true;
    var panel = card.querySelector('[data-profile-progress]');
     } else if (window.CLBI_PAGE_SHELL && typeof window.CLBI_PAGE_SHELL.isBackendOrSystemPage === 'function') {
     if (!panel || !summary) return;
        hideTitle = !window.CLBI_PAGE_SHELL.isBackendOrSystemPage();
    }


     $('body')
     var level = summary.level || 1;
        .toggleClass('page-title-hidden', hideTitle)
    var totalXp = summary.totalXp || 0;
        .toggleClass('page-title-visible', !hideTitle)
    var xpIntoLevel = summary.xpIntoLevel || 0;
        .toggleClass('clbi-system-doc-page', isSystemAssetPage);
    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;


     $('.content-tools').css('display', 'none');
     panel.classList.remove('is-syncing');
    panel.classList.toggle('is-max-level', isMaxLevel);


     if (isSystemAssetPage && window.CLBI_PAGE_SHELL && typeof window.CLBI_PAGE_SHELL.renderSystemDocIndicator === 'function') {
     var levelEl = panel.querySelector('.profile-page-level');
        window.CLBI_PAGE_SHELL.renderSystemDocIndicator();
    var totalEl = panel.querySelector('.profile-page-total-xp');
     } else if (!isSystemAssetPage && window.CLBI_PAGE_SHELL && typeof window.CLBI_PAGE_SHELL.removeSystemDocIndicator === 'function') {
    var fillEl = panel.querySelector('.profile-page-xp-fill');
        window.CLBI_PAGE_SHELL.removeSystemDocIndicator();
     var subEl = panel.querySelector('.profile-page-progress-sub');
    }
    var metaEl = panel.querySelector('.profile-page-progress-meta');


     if (hideTitle) {
     if (levelEl) levelEl.textContent = (isMaxLevel ? 'MAX ' : 'LVL ') + level;
        $('.liberty-content-header').css('display', 'none');
    if (totalEl) totalEl.textContent = totalXp + ' XP';
        $('.mw-page-title-main, .mw-page-title-namespace, .mw-page-title-separator').addClass('clbi-hide');
    if (fillEl) fillEl.style.width = percent + '%';
        $('#firstHeading, .firstHeading, .mw-first-heading, .page-heading, .page-header').css('display', 'none');
     if (subEl) subEl.textContent = isMaxLevel ? 'MAX LEVEL' : (xpIntoLevel + ' / ' + xpForNext + ' TO NEXT');
        setNativePageTitleHiddenHard(true);
    if (metaEl) metaEl.textContent = 'TODAY ' + dailyXp + ' XP · DISCOVERED ' + discoveries;
     } 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() {
function loadProfileFields(username, card) {
     var specialPage = mw.config.get('wgCanonicalSpecialPageName');
     var api = new mw.Api();
     if (specialPage === 'Preferences') return;
    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: ''
        });
    });
}


     var pageName = normalizePageName(mw.config.get('wgPageName'));
function updateProfileFields(card, data) {
     var namespaceNumber = mw.config.get('wgNamespaceNumber');
     var nameEl = card.querySelector('[data-field="name"]');
     var isMainPage = (pageName === '대문');
     var roleEl = card.querySelector('[data-field="role"]');
     var isUserProfilePage = (namespaceNumber === 2);
     var discordEl = card.querySelector('[data-field="discord"]');
     var isScreenDoc = ($('.screen-header').length > 0);
     var bioEl = card.querySelector('[data-field="bio"]');
     var hideTools = (isMainPage || isUserProfilePage || !canShowContentTools());
    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 || '';
}
// ========== 프로필 시스템 끝 ==========


    $('body').toggleClass('user-profile-page', isUserProfilePage);
// ========== 알림 시스템 ==========
     $('body').toggleClass('clbi-main-page', isMainPage);
function ensureNotificationPopup() {
     if (document.getElementById('clbi-notification-popup')) return;


     // 모든 문서에서 분류 바를 본문 컨테이너 아래로 이동
     var popup = document.createElement('div');
     moveCatlinksToBottom();
    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;';


     if (isMainPage) {
     popup.innerHTML =
         $('.liberty-content-header').css('display', 'none');
         '<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;">' +
        $('.mw-page-title-main').addClass('clbi-hide');
            '<span>알림</span>' +
         setNativePageTitleHiddenHard(true);
            '<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>' +
         $('.catlinks').css('display', 'none');
         '</div>' +
        $('.liberty-content-main').css('border-radius', '0');
         '<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>';


        // 새 대문은 .main-portal 본문 구조가 로고/히어로를 담당한다.
    document.body.appendChild(popup);
        // Common.js의 구식 바깥 로고/CRT 재배치 루틴은 사용하지 않는다.
}
        removeLegacyMainPageHero();
        $('#clbi-tools-box').remove();


        $('.content-tools').css('display', 'none');
function positionNotificationPopup() {
    var btn = document.getElementById('profile-quick-notifications');
    var popup = document.getElementById('clbi-notification-popup');
    if (!btn || !popup) return;


        initCategoryNavIfAvailable(document);
    var rect = btn.getBoundingClientRect();
    var top = rect.bottom + 6;
    var left = rect.left + (rect.width / 2) - (popup.offsetWidth / 2);


     } else if (isUserProfilePage) {
     if (left < 8) left = 8;
        $('.liberty-content-header').css('display', 'none');
    if (left + popup.offsetWidth > window.innerWidth - 8) {
         $('.mw-page-title-main, .mw-page-title-namespace, .mw-page-title-separator').addClass('clbi-hide');
         left = window.innerWidth - popup.offsetWidth - 8;
         $('.catlinks').css('display', 'none');
    }
        $('.liberty-content-main').css('border-radius', '0');
    if (top + popup.offsetHeight > window.innerHeight - 8) {
         top = Math.max(8, rect.top - popup.offsetHeight - 6);
    }


        $('#clbi-main-logo').remove();
    popup.style.top = top + 'px';
        $('#clbi-main-crt-hero-wrap').remove();
    popup.style.left = left + 'px';
        $('#clbi-main-crt-hero').remove();
}
        $('#clbi-tools-box').remove();


        $('.content-tools').css('display', 'none');
function parseNotificationItemsFromHtml(html) {
    var parser = new DOMParser();
    var doc = parser.parseFromString(html, 'text/html');


     } else if (isScreenDoc) {
     var selectors = [
         $('.liberty-content-header').css('display', 'none');
         '.mw-echo-ui-notificationItemWidget',
         $('.mw-page-title-main').addClass('clbi-hide');
         '.mw-echo-ui-notificationsInboxWidgetRow',
         $('.catlinks').css('display', '');
        '.echo-ui-notificationItemWidget',
         $('.liberty-content-main').css('border-radius', '0');
         'li[data-notification-id]',
         '.mw-echo-notifications-list li'
    ];


         $('#clbi-main-logo').remove();
    var items = [];
         $('#clbi-main-crt-hero-wrap').remove();
    for (var i = 0; i < selectors.length; i++) {
         items = Array.prototype.slice.call(doc.querySelectorAll(selectors[i]));
         if (items.length) break;
    }


        if ($('#clbi-tools-box').length === 0 && canShowContentTools()) {
    return items.slice(0, 5).map(function(item) {
            var $toolsBox = $('<div id="clbi-tools-box" class="clbi-left-box"></div>');
        var link = item.querySelector('a[href]');
            var $toolsTitle = $('<div class="clbi-left-title">관리</div>');
        var href = link ? link.getAttribute('href') : '/index.php?title=Special:Notifications';
            var $toolsContent = $('<div class="clbi-left-content"></div>');
        var text = (item.textContent || '').replace(/\s+/g, ' ').trim();


             $toolsContent.append($('.content-tools .btn-group').clone(true));
        var notificationId =
             $toolsBox.append($toolsTitle).append($toolsContent);
             item.getAttribute('data-notification-id') ||
             $('#clbi-left-sidebar').append($toolsBox);
            item.getAttribute('data-id') ||
        }
             item.getAttribute('data-notification') ||
             '';


         $('.content-tools').css('display', 'none');
         if (!notificationId) {
 
            var anyWithId = item.querySelector('[data-notification-id], [data-id], [data-notification]');
    } else {
            if (anyWithId) {
        $('.liberty-content-header').css('display', '');
                notificationId =
        $('.mw-page-title-main, .mw-page-title-namespace, .mw-page-title-separator').removeClass('clbi-hide');
                    anyWithId.getAttribute('data-notification-id') ||
        $('.catlinks').css('display', '');
                    anyWithId.getAttribute('data-id') ||
        $('.liberty-content-main').css('border-radius', '0');
                    anyWithId.getAttribute('data-notification') ||
                    '';
            }
        }


         $('#clbi-main-logo').remove();
         if (href && href.indexOf('http') !== 0) {
        $('#clbi-main-crt-hero-wrap').remove();
            href = href.charAt(0) === '/'
        $('#clbi-tools-box').remove();
                ? href
    }
                : '/index.php' + (href.charAt(0) === '?' ? href : '/' + href);
        }


    if (!isUserProfilePage) {
        return {
        $('.profile-card').remove();
            id: notificationId,
        $('.user-profile-portal').removeClass('user-profile-portal');
            href: href,
    }
            text: text || '알림'
        };
    });
}


    $('.content-tools').css('display', 'none');
function setNotificationIcon(hasItems) {
    var quickIcon = document.getElementById('profile-quick-notification-icon');
    var svg = hasItems ? CLBI_SVG_BELL_DOT : CLBI_SVG_BELL;


     applyDefaultPageTitleVisibility();
     if (quickIcon) {
    updateSidebar();
        quickIcon.innerHTML = svg;
        quickIcon.classList.toggle('has-notifications', !!hasItems);
    }
}
}


// 본문 기본 목차 제거
function renderNotificationPopup(items) {
function removeNativeTocFromContent() {
     var list = document.getElementById('clbi-notification-list');
     $('.liberty-content-main #toc, .liberty-content-main .toc').remove();
    var badge = document.getElementById('clbi-notification-badge');
}
    if (!list) return;


// 왼쪽 목차: MediaWiki 문단 ID 가져오기
    if (!items || !items.length) {
function getHeadingId(heading) {
        list.innerHTML = '<div style="padding:14px 12px;color:#999;">표시할 알림이 없습니다.</div>';
    if (heading.id) {
        if (badge) badge.style.display = 'none';
         return heading.id;
        setNotificationIcon(false);
         return;
     }
     }


     var headline = heading.querySelector('.mw-headline[id]');
     var html = '';
     if (headline && headline.id) {
     for (var i = 0; i < items.length; i++) {
         return headline.id;
         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;


     return '';
     if (badge) {
        badge.textContent = items.length;
        badge.style.display = 'block';
    }
    setNotificationIcon(true);
}
}


// 왼쪽 목차: MediaWiki 문단 제목 텍스트 가져오기
function loadNotificationsIntoPopup() {
function getHeadingText(heading) {
     var list = document.getElementById('clbi-notification-list');
     var headline = heading.querySelector('.mw-headline');
     if (list) {
     var source = headline || heading;
        list.innerHTML = '<div style="padding:14px 12px;color:#999;">불러오는 중...</div>';
     var clone = source.cloneNode(true);
     }


     $(clone).find('.mw-editsection, .mw-editsection-bracket, .mw-editsection-divider').remove();
     fetch('/index.php?title=Special:Notifications', { credentials: 'same-origin' })
 
        .then(function(res) {
    return (clone.textContent || '')
            return res.text();
        .replace(/\s+/g, ' ')
        })
         .trim();
        .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() {
function initTocTitleScroll(root) {
     return new mw.Api().postWithToken('csrf', {
     var $items = root
        action: 'echomarkread',
        ? $(root).find('.toc-scroll-text')
         list: 'all'
         : $('#side-toc-box .toc-scroll-text');
    });
}


    $items.each(function () {
function markNotificationReadById(notificationId) {
        var $text = $(this);
    if (!notificationId) {
         var $wrap = $text.closest('.toc-scroll-wrap');
         return $.Deferred().resolve().promise();
    }


        if (!$wrap.length) return;
    return new mw.Api().postWithToken('csrf', {
        action: 'echomarkread',
        list: notificationId
    });
}


        var wrapW = Math.floor($wrap.width());
function initNotifications() {
        var textW = Math.ceil(this.scrollWidth);
    var quickBtn = document.getElementById('profile-quick-notifications');


        // 왼쪽 목차: 레이아웃 계산이 끝나지 않았으면 이번 실행에서는 건드리지 않는다.
    if (!quickBtn) return;
        if (!wrapW || !textW) return;


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


            if ($text.data('toc-scroll-enabled')) {
    $(document)
                $text.css({
        .off('click.clbiNotificationToggle')
                    animation: '',
        .on('click.clbiNotificationToggle', '#profile-quick-notifications', function(e) {
                    'animation-delay': '',
            e.preventDefault();
                    '--scroll-dist': ''
            e.stopPropagation();
                });
                $text.removeData('toc-scroll-enabled');
                $text.removeData('toc-scroll-key');
            }


             return;
             var popup = document.getElementById('clbi-notification-popup');
        }
            if (!popup) return;


        var scrollDist = '-' + (textW - wrapW + 10) + 'px';
            if (popup.style.display === 'none' || popup.style.display === '') {
        var duration = Math.max(7, textW / 38) * 1.25;
                popup.style.display = 'block';
        var scrollKey = scrollDist + '|' + duration;
                positionNotificationPopup();
                loadNotificationsIntoPopup();
            } else {
                popup.style.display = 'none';
            }
        });


         // 왼쪽 목차: 긴 제목에는 오른쪽 페이드와 스크롤을 적용한다.
    $(document)
         $wrap.addClass('is-scrolling');
         .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))) {
        if ($text.data('toc-scroll-key') === scrollKey) {
                popup.style.display = 'none';
             return;
             }
         }
         });


         $text.data('toc-scroll-enabled', true);
    $(document)
         $text.data('toc-scroll-key', scrollKey);
         .off('click.clbiNotificationReadAll')
         .on('click.clbiNotificationReadAll', '#clbi-notification-readall', function(e) {
            e.preventDefault();
            e.stopPropagation();
 
            var button = this;
            button.disabled = true;
            button.textContent = '처리 중...';


        $text.css({
            markAllNotificationsRead()
            // 왼쪽 목차: 페이지 진입 직후에는 잠시 읽을 시간을 준 뒤 흐르게 한다.
                .then(function() {
            animation: 'toc-scroll-blink-reset ' + duration + 's linear infinite',
                    loadNotificationsIntoPopup();
            'animation-delay': '1s',
                })
            '--scroll-dist': scrollDist
                .always(function() {
                    button.disabled = false;
                    button.textContent = '전체 읽음';
                });
         });
         });
    });
}


// 목차를 왼쪽 사이드바에 새로 생성
    $(document)
function moveTocToLeftSidebar() {
        .off('click.clbiNotificationItem')
    removeNativeTocFromContent();
        .on('click.clbiNotificationItem', '.clbi-notification-item', function(e) {
    $('#side-toc-box').remove();
            e.preventDefault();
    return;
            e.stopPropagation();


    // 왼쪽 목차: MediaWiki가 만든 원래 목차는 본문에서 제거한다.
            var href = this.getAttribute('href');
    removeNativeTocFromContent();
            var notificationId = this.getAttribute('data-notification-id') || '';


    var leftSidebar = document.getElementById('clbi-left-sidebar');
            markNotificationReadById(notificationId).always(function() {
    if (!leftSidebar) return;
                loadNotificationsIntoPopup();
                if (href) {
                    window.location.href = href;
                }
            });
        });


     var content =
     $(window)
         document.querySelector('.liberty-content-main .mw-parser-output') ||
         .off('resize.clbiNotification')
        document.querySelector('.liberty-content-main');
        .on('resize.clbiNotification', function() {
            var popup = document.getElementById('clbi-notification-popup');
            if (popup && popup.style.display === 'block') {
                positionNotificationPopup();
            }
        });
}
// ========== 알림 시스템 끝 ==========


     if (!content) return;
function initUserProfilePage() {
     $('body').addClass('user-profile-settings-page');


     var headings = Array.prototype.slice.call(
     var saveBtn = document.getElementById('pref-save');
        content.querySelectorAll('h2, h3')
     if (!saveBtn) return;
     ).filter(function (heading) {
        if (heading.closest('#toc, .toc, #side-toc-box')) return false;


         var id = getHeadingId(heading);
    function getPrefRow(id) {
         var text = getHeadingText(heading);
         var el = document.getElementById(id);
        if (!el) return null;
         return el.closest('.clbi-pref-row') || el.parentNode;
    }


         if (!id || !text) return false;
    function removePrefRow(id) {
        var row = getPrefRow(id);
         if (row && row.parentNode) {
            row.parentNode.removeChild(row);
        }
    }


         return true;
    function createPrefSection(className, titleText) {
    });
         var section = document.createElement('div');
        section.className = 'clbi-pref-section ' + className;


    var tocKey = headings.map(function (heading) {
        var title = document.createElement('div');
         return getHeadingId(heading) + '|' + getHeadingText(heading);
         title.className = 'clbi-pref-section-title';
    }).join('||');
        title.textContent = titleText;


    var existingBox = document.getElementById('side-toc-box');
        var body = document.createElement('div');
        body.className = 'clbi-pref-section-body';


    // 왼쪽 목차: 같은 문서에서 같은 목차를 이미 만들었다면 다시 지우고 만들지 않는다.
        section.appendChild(title);
    if (existingBox && existingBox.getAttribute('data-toc-key') === tocKey) {
         section.appendChild(body);
         initTocTitleScroll(existingBox);
        return;
    }


    if (existingBox) {
        return {
         existingBox.remove();
            section: section,
            body: body
         };
     }
     }


     if (!headings.length) return;
     function moveRowToSection(id, targetBody, className) {
        var row = getPrefRow(id);
        if (!row || !targetBody) return false;


    var tocBox = document.createElement('div');
        row.classList.add('clbi-pref-row-key-' + className);
    tocBox.className = 'clbi-left-box';
        targetBody.appendChild(row);
    tocBox.id = 'side-toc-box';
        return true;
    tocBox.setAttribute('data-toc-key', tocKey);
    }


     var title = document.createElement('div');
     function rebuildProfileSettingsLayout() {
    title.className = 'clbi-left-title';
        var root = document.querySelector('.clbi-prefs-profile');
        if (!root || root.dataset.profileSettingsReworked === '1') return;


    // 왼쪽 목차: 박스 제목은 Lang.js의 현재 UI 언어를 따른다.
        root.dataset.profileSettingsReworked = '1';
    var currentLang = getCurrentLang();
        root.classList.add('profile-settings-console');
    var t = (window.LANG && window.LANG[currentLang]) ? window.LANG[currentLang] : window.LANG.ko;
    var tocTitleText = (t && t.toc) ? t.toc : '목차';


    title.textContent = tocTitleText;
        removePrefRow('pref-badges');


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


    var list = document.createElement('ul');
        if (saveBtn.parentNode === root || saveBtn.closest('.clbi-prefs-profile') === root) {
    list.className = 'generated-toc';
            actionNodes.push(saveBtn);
        }


    headings.forEach(function (heading) {
         var statusNode = document.getElementById('pref-status');
         var id = getHeadingId(heading);
         if (statusNode && statusNode.closest('.clbi-prefs-profile') === root) {
         var text = getHeadingText(heading);
            actionNodes.push(statusNode);
        var level = heading.tagName.toLowerCase() === 'h3' ? 3 : 2;
        }


         var item = document.createElement('li');
         var main = document.createElement('div');
         item.className = 'toc-level-' + level;
         main.className = 'clbi-pref-main-grid';


         var link = document.createElement('a');
         var media = createPrefSection('clbi-pref-section-media', 'PROFILE IMAGE');
         link.setAttribute('href', '#' + id);
         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');


         // 왼쪽 목차: 긴 제목 스크롤을 위해 텍스트를 별도 span으로 감싼다.
         main.appendChild(media.section);
         var textWrap = document.createElement('span');
        main.appendChild(identity.section);
         textWrap.className = 'toc-scroll-wrap';
         main.appendChild(bio.section);
         main.appendChild(account.section);
        main.appendChild(misc.section);


         var textSpan = document.createElement('span');
         root.innerHTML = '';
        textSpan.className = 'toc-scroll-text';
         root.appendChild(main);
         textSpan.textContent = text;


         textWrap.appendChild(textSpan);
         moveRowToSection('pref-pfp-preview', media.body, 'pfp');
         link.appendChild(textWrap);
        moveRowToSection('pref-pfp-btn', media.body, 'pfp');
         moveRowToSection('pref-pfp-input', media.body, 'pfp');


         item.appendChild(link);
         moveRowToSection('pref-name', identity.body, 'name');
         list.appendChild(item);
         moveRowToSection('pref-role', identity.body, 'role');
    });
        moveRowToSection('pref-discord', identity.body, 'discord');


    body.appendChild(list);
        moveRowToSection('pref-bio', bio.body, 'bio');
    tocBox.appendChild(title);
    tocBox.appendChild(body);
    leftSidebar.appendChild(tocBox);


    // 왼쪽 목차: DOM 배치가 끝난 뒤 긴 제목 스크롤 여부를 계산한다.
        moveRowToSection('pref-new-email', account.body, 'email');
    requestAnimationFrame(function () {
        moveRowToSection('pref-email-password', account.body, 'email');
         initTocTitleScroll(tocBox);
        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);
    }


        setTimeout(function () {
    rebuildProfileSettingsLayout();
            initTocTitleScroll(tocBox);
        }, 120);
    });
}


    var api = new mw.Api();
    var selectedFile = null;
    var cropper = null;


// 우측 광고판: 이미지/번역 캡션 목록
    if (!document.getElementById('clbi-gallery-modal')) {
var RIGHT_BILLBOARD_ITEMS = [
        var gModal = document.createElement('div');
    {
         gModal.id = 'clbi-gallery-modal';
         file: 'Side-visual-001.png',
        gModal.style.cssText =
        alt: 'PROOF TO THE WORLD / YOU ONCE PART OF IT',
            '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;';
        duration: 3000,
 
         caption: [
         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%;">' +
        file: 'Side-visual-002.png',
                    '<span style="font-size:32px;">🖼️</span>새 사진 업로드' +
        alt: 'APPLY NOW',
                '</button>' +
        duration: 1000,
                '<div id="clbi-gallery-history-section" style="display:none;">' +
        caption: [
                    '<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>';
    {
 
        file: 'Side-visual-003.png',
         document.body.appendChild(gModal);
        alt: 'APPLY NOW',
        duration: 1000,
        caption: [
            '"지금 지원하세요!"'
        ]
    },
    {
        file: 'Side-visual-002.png',
        alt: 'APPLY NOW',
        duration: 1000,
        caption: [
             '"지금 지원하세요!"'
         ]
     }
     }
];


function getRightBillboardItem(index) {
    if (!document.getElementById('clbi-crop-modal')) {
    var items = RIGHT_BILLBOARD_ITEMS;
        var cModal = document.createElement('div');
 
         cModal.id = 'clbi-crop-modal';
    if (!items || !items.length) {
        cModal.style.cssText =
         return {
             '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;';
            file: 'Side-visual-001.png',
             alt: '',
            caption: []
        };
    }


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


    return items[normalized];
        document.body.appendChild(cModal);
}
    }


function getRightBillboardImageUrl(fileName) {
    var gModal = document.getElementById('clbi-gallery-modal');
     return '/index.php?title=특수:Redirect/file/' + encodeURIComponent(fileName || 'Side-visual-001.png');
     var cModal = document.getElementById('clbi-crop-modal');
}
    var cropImage = document.getElementById('clbi-crop-image');
    var pfpInput = document.getElementById('pref-pfp-input');


function getRightBillboardCaptionHtml(item) {
    function openGallery() {
    var lines = item && item.caption ? item.caption : [];
        gModal.style.display = 'flex';
    var html = '';


    function escapeCaptionText(value) {
        var username = mw.config.get('wgUserName');
         return String(value == null ? '' : value)
         api.get({
             .replace(/&/g, '&amp;')
            action: 'query',
             .replace(/</g, '&lt;')
             titles: '파일:Pfp-' + username + '.png',
             .replace(/>/g, '&gt;')
            prop: 'imageinfo',
             .replace(/\"/g, '&quot;')
             iiprop: 'url|timestamp',
             .replace(/'/g, '&#039;');
             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;


    lines.forEach(function(line) {
            var historyEl = document.getElementById('clbi-gallery-history');
        var text = String(line == null ? '' : line);
            var sectionEl = document.getElementById('clbi-gallery-history-section');
        var isGap = !text.trim();
            historyEl.innerHTML = '';
        var className = 'right-billboard-caption-line' + (isGap ? ' is-gap' : '');
        html += '<span class="' + className + '">' + (isGap ? '&nbsp;' : escapeCaptionText(text)) + '</span>';
    });


    return html;
            page.imageinfo.forEach(function(info, idx) {
}
                var wrap = document.createElement('div');
                wrap.style.cssText = 'position:relative;cursor:pointer;';


function setRightBillboardItem(index) {
                var img = document.createElement('img');
    var box = document.querySelector('.right-billboard-box');
                img.src = info.url;
    if (!box) return;
                img.style.cssText =
                    'width:72px;height:72px;object-fit:cover;border-radius:8px;border:2px solid #444;flex-shrink:0;';


    var item = getRightBillboardItem(index);
                if (idx === 0) {
    var src = getRightBillboardImageUrl(item.file);
                    img.style.borderColor = '#854369';
    var images = box.querySelectorAll('.right-billboard-image');
                    var badge = document.createElement('div');
    var caption = box.querySelector('#right-billboard-caption');
                    badge.textContent = '현재';
    var emptySub = box.querySelector('.right-billboard-empty-sub');
                    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);
                }


    box.setAttribute('data-billboard-index', String(index));
                img.addEventListener('mouseenter', function() {
    box.classList.remove('is-empty');
                    if (idx !== 0) img.style.borderColor = '#854369';
                });


    Array.prototype.forEach.call(images, function(img) {
                img.addEventListener('mouseleave', function() {
        img.style.display = '';
                    if (idx !== 0) img.style.borderColor = '#444';
        img.setAttribute('src', src);
                });
        img.setAttribute('alt', img.classList.contains('right-billboard-image-base') ? (item.alt || '') : '');
    });


    if (caption) {
                img.addEventListener('click', function() {
        caption.innerHTML = getRightBillboardCaptionHtml(item);
                    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 = '✓ 사진 선택됨';
                        });
                });


    if (emptySub) {
                wrap.appendChild(img);
        emptySub.textContent = item.file || 'Side-visual-001.png';
                historyEl.appendChild(wrap);
    }
            });
}


function getRightBillboardItemDuration(item) {
            sectionEl.style.display = 'block';
    var duration = item && item.duration ? parseInt(item.duration, 10) : 3000;
        });
 
    if (Number.isNaN(duration) || duration < 500) {
        duration = 3000;
     }
     }


     return duration;
     function openCrop(src) {
}
        cropImage.src = src;
        cModal.style.display = 'flex';


function initRightBillboardCarousel() {
        if (cropper) {
    var box = document.querySelector('.right-billboard-box');
            cropper.destroy();
    if (!box || box.getAttribute('data-billboard-ready') === '1') return;
            cropper = null;
        }


    box.setAttribute('data-billboard-ready', '1');
        setTimeout(function() {
    box.setAttribute('data-billboard-index', '0');
            cropper = new Cropper(cropImage, {
                aspectRatio: 1,
                viewMode: 1,
                dragMode: 'move',
                autoCropArea: 0.8,
                cropBoxResizable: true,
                cropBoxMovable: true
            });
        }, 150);
    }


     setRightBillboardItem(0);
     document.getElementById('pref-pfp-btn').addEventListener('click', function() {
        openGallery();
    });


     if (!RIGHT_BILLBOARD_ITEMS || RIGHT_BILLBOARD_ITEMS.length <= 1) return;
     document.getElementById('clbi-gallery-upload-btn').addEventListener('click', function() {
        pfpInput.click();
    });


     function scheduleNext() {
     document.getElementById('clbi-gallery-close').addEventListener('click', function() {
        var current = parseInt(box.getAttribute('data-billboard-index') || '0', 10);
         gModal.style.display = 'none';
         if (Number.isNaN(current)) current = 0;
    });


         var currentItem = getRightBillboardItem(current);
    pfpInput.addEventListener('change', function() {
         var delay = getRightBillboardItemDuration(currentItem);
         var file = this.files[0];
         if (!file) return;


         window.setTimeout(function() {
         gModal.style.display = 'none';
            if (!document.body.contains(box)) return;


             if (!document.hidden) {
        var reader = new FileReader();
                setRightBillboardItem(current + 1);
        reader.onload = function(e) {
            }
             openCrop(e.target.result);
        };
        reader.readAsDataURL(file);
    });


             scheduleNext();
    document.getElementById('clbi-crop-cancel').addEventListener('click', function() {
         }, delay);
        cModal.style.display = 'none';
     }
        if (cropper) {
             cropper.destroy();
            cropper = null;
         }
        pfpInput.value = '';
     });


     scheduleNext();
     document.getElementById('clbi-crop-confirm').addEventListener('click', function() {
}
        if (!cropper) return;


        var canvas = cropper.getCroppedCanvas({ width: 256, height: 256 });
        if (!canvas) return;


function escapeRightBillboardAttr(value) {
        canvas.toBlob(function(blob) {
    return String(value == null ? '' : value)
            selectedFile = new File([blob], 'profile.png', { type: 'image/png' });
        .replace(/&/g, '&amp;')
            document.getElementById('pref-pfp-preview').src = URL.createObjectURL(blob);
        .replace(/</g, '&lt;')
            cModal.style.display = 'none';
        .replace(/>/g, '&gt;')
            cropper.destroy();
        .replace(/"/g, '&quot;')
            cropper = null;
         .replace(/'/g, '&#039;');
            document.getElementById('pref-pfp-btn').textContent = '✓ 사진 선택됨';
}
         }, 'image/png');
    });


function buildRightBillboardBox() {
    var emailSaveBtn = document.getElementById('pref-email-save');
    var billboardInitial = getRightBillboardItem(0);
    if (emailSaveBtn) {
    var billboardSrc = getRightBillboardImageUrl(billboardInitial.file);
        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;


    return '' +
             if (!newEmail || !password) {
        '<div class="clbi-left-box right-billboard-box left-billboard-box left-ad-box" data-billboard-index="0">' +
                statusEl.textContent = '이메일과 비밀번호를 입력해주세요.';
             '<div class="clbi-left-title right-billboard-title left-ad-title left-ad-title-iconless">' +
                return;
                '<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>';
}


            statusEl.textContent = '변경 중...';


var GREAT_WALL_DATA_TITLE = '프로젝트:The_Great_Wall/Data.json';
            api.postWithToken('csrf', {
var GREAT_WALL_LIST_LIMIT = 0;
                action: 'changeemail',
var greatWallState = {
                email: newEmail,
    data: { entries: {} },
                password: password
    loaded: false,
            }).then(function() {
    loading: false,
                statusEl.textContent = '✓ 이메일 변경됨';
    saving: false,
                document.getElementById('pref-new-email').value = '';
    selectedOwnEntry: false,
                document.getElementById('pref-email-password').value = '';
    statusText: ''
};


function normalizeGreatWallData(data) {
                setTimeout(function() {
    var normalized = { entries: {} };
                    statusEl.textContent = '';
    var entries = data && data.entries && typeof data.entries === 'object' ? data.entries : {};
                }, 3000);
            }).fail(function(code, data) {
                var msg = data && data.error && data.error.info ? data.error.info : '변경 실패';
                statusEl.textContent = msg;
            });
        });
    }


     Object.keys(entries).forEach(function(key) {
     saveBtn.addEventListener('click', function() {
         var item = entries[key];
         var statusEl = document.getElementById('pref-status');
        var user = item && item.user ? String(item.user) : String(key || '');
         statusEl.textContent = '저장 중...';
         var text = item && item.text ? String(item.text) : '';
        var timestamp = item && item.timestamp ? String(item.timestamp) : '';


         if (!user || !text) return;
         var promises = [];


         normalized.entries[user] = {
         if (selectedFile) {
             user: user,
             var username = mw.config.get('wgUserName');
            text: text.slice(0, 140),
             promises.push(
             timestamp: timestamp || new Date(0).toISOString()
                api.postWithToken('csrf', {
        };
                    action: 'upload',
    });
                    filename: 'Pfp-' + username + '.png',
 
                    ignorewarnings: true,
    return normalized;
                    file: selectedFile,
}
                    format: 'json'
 
                }, {
function parseGreatWallData(text) {
                    contentType: 'multipart/form-data'
    var parsed;
                })
 
            );
    try {
         }
        parsed = text ? JSON.parse(text) : {};
    } catch (err) {
        console.error('The Great Wall data parse failed:', err);
         parsed = {};
    }


    return normalizeGreatWallData(parsed);
        var fields = ['name', 'discord', 'role', 'bio'];
}


function stringifyGreatWallData(data) {
        for (var i = 0; i < fields.length; i++) {
    return JSON.stringify(normalizeGreatWallData(data), null, 2) + '\n';
            var el = document.getElementById('pref-' + fields[i]);
}
            if (!el) continue;


function getGreatWallRevisionText(page) {
            promises.push(
    var rev;
                api.postWithToken('csrf', {
    var slot;
                    action: 'options',
                    optionname: 'profile-' + fields[i],
                    optionvalue: el.value
                })
            );
        }


    if (!page || !page.revisions || !page.revisions.length) return '';
        $.when.apply($, promises)
            .then(function() {
                statusEl.textContent = '✓ 저장됨';
                selectedFile = null;
                document.getElementById('pref-pfp-btn').textContent = '사진 선택';


    rev = page.revisions[0];
                setTimeout(function() {
 
                    statusEl.textContent = '';
    if (rev.slots && rev.slots.main) {
                }, 2000);
        slot = rev.slots.main;
            })
        return slot.content || slot['*'] || '';
            .fail(function() {
    }
                statusEl.textContent = '저장 실패';
 
            });
     return rev.content || rev['*'] || '';
     });
}
}


function fetchGreatWallData() {
/* =========================================
     var api = new mw.Api();
  Banner / CRT Page Monitor thumbnail slices
  - base 이미지는 틀에 들어간 파일 문법 그대로 사용
  - slice 레이어에는 300px MediaWiki 썸네일만 삽입
  ========================================= */
 
(function ($, mw) {
     var thumbCache = {};


     return api.get({
     function parseSliceWidth(value) {
        action: 'query',
         var parsed = parseInt(value, 10);
        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) {
         if (!isFinite(parsed) || parsed < 120) {
             return { entries: {} };
             return 300;
         }
         }


         return parseGreatWallData(getGreatWallRevisionText(page));
         return parsed;
     }, function(err) {
     }
         console.error('The Great Wall load failed:', err);
 
        return { entries: {} };
    function getImageSrc(img) {
     });
         return img ? (img.currentSrc || img.getAttribute('src') || img.src || '') : '';
}
     }


function getGreatWallEntries(data) {
    function getFileNameFromSrc(src) {
    var entries = data && data.entries && typeof data.entries === 'object' ? data.entries : {};
        var a;
        var parts;
        var fileName;


    return Object.keys(entries).map(function(key) {
         if (!src) return '';
        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) {
        a = document.createElement('a');
    var date = timestamp ? new Date(timestamp) : null;
        a.href = src;
    var month;
    var day;
    var hour;
    var minute;


    if (!date || isNaN(date.getTime())) return '—';
        parts = (a.pathname || '').split('/').filter(function (part) {
            return !!part;
        });


    month = String(date.getMonth() + 1).padStart(2, '0');
        if (!parts.length) return '';
    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;
        fileName = parts.pop();
}


function getGreatWallAvatarSrc(user) {
        /*
    return '/index.php?title=특수:Redirect/file/Pfp-' + encodeURIComponent(String(user || '')) + '.png';
        * 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();
        }


function buildGreatWallEntryHtml(item, currentUser, selectedOwnEntry) {
        fileName = fileName.replace(/^\d+px-/, '');
    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 '' +
         try {
         '<' + tag + attrs + ' class="' + className + '">' +
             fileName = decodeURIComponent(fileName);
             '<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;;">' +
        } catch (e) {}
            '<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() {
         return fileName;
    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) {
     function resolveThumbUrl(img, width, callback) {
         list.scrollTop = list.scrollHeight;
         var src = getImageSrc(img);
    }
        var fileName = getFileNameFromSrc(src);
        var cacheKey;
        var entry;


    if (status) {
        if (!src) return;
        status.textContent = greatWallState.statusText || (currentUser ? (ownEntry ? 'SELECT YOUR MARK TO UPDATE' : 'LEAVE ONE MARK') : 'ACCOUNT REQUIRED');
    }


    if (!input || !submit) return;
        if (!fileName || !mw || !mw.loader) {
            callback(src);
            return;
        }


    input.disabled = false;
        cacheKey = fileName + '|' + width;
    input.readOnly = false;
        entry = thumbCache[cacheKey];
    input.removeAttribute('aria-readonly');


    if (!currentUser) {
        if (entry) {
        input.disabled = true;
            if (entry.resolved) {
        input.readOnly = false;
                callback(entry.url || src);
        input.value = '';
            } else {
        input.placeholder = 'Login required';
                entry.callbacks.push(callback);
        submit.textContent = 'LOGIN';
             }
        submit.disabled = false;
             return;
        if (deleteButton) {
             deleteButton.disabled = true;
             deleteButton.title = 'Login required';
         }
         }
        return;
    }


    if (greatWallState.saving || greatWallState.loading) {
         entry = {
         input.disabled = true;
            resolved: false,
        input.readOnly = false;
            url: '',
        submit.disabled = true;
             callbacks: [callback]
        submit.textContent = greatWallState.saving ? 'SAVE' : 'SYNC';
         };
        if (deleteButton) {
             deleteButton.disabled = true;
            deleteButton.title = greatWallState.saving ? 'Saving' : 'Syncing';
         }
        return;
    }


    if (deleteButton) {
         thumbCache[cacheKey] = entry;
         deleteButton.disabled = !ownEntry;
        deleteButton.title = ownEntry ? 'Delete your mark' : 'No mark to delete';
    }


    if (ownEntry && !greatWallState.selectedOwnEntry) {
        function finish(url) {
        input.disabled = false;
            var callbacks = entry.callbacks.slice();
        input.readOnly = true;
            var i;
        input.setAttribute('aria-readonly', 'true');
        input.value = '';
        input.placeholder = 'Select your existing mark';
        submit.disabled = true;
        submit.textContent = 'UPDATE';
        return;
    }


    input.disabled = false;
            entry.resolved = true;
    input.readOnly = false;
            entry.url = url || src;
    input.removeAttribute('aria-readonly');
            entry.callbacks = [];
    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() {
            for (i = 0; i < callbacks.length; i++) {
    var input = document.getElementById('great-wall-input');
                callbacks[i](entry.url);
    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) {
         mw.loader.using('mediawiki.api').done(function () {
         window.location.href = '/index.php?title=특수:로그인&returnto=' + encodeURIComponent(mw.config.get('wgPageName') || '대문');
            var api = new mw.Api();
        return;
    }


    if (ownEntry && !greatWallState.selectedOwnEntry) {
            api.get({
        greatWallState.statusText = 'SELECT YOUR MARK FIRST';
                action: 'query',
        renderGreatWallBox();
                titles: 'File:' + fileName,
        return;
                prop: 'imageinfo',
    }
                iiprop: 'url',
                iiurlwidth: width,
                formatversion: 2
            }).done(function (data) {
                var page;
                var info;


    if (!text) {
                if (
        greatWallState.statusText = 'EMPTY MARK';
                    data &&
        renderGreatWallBox();
                    data.query &&
        return;
                    data.query.pages &&
    }
                    data.query.pages.length
                ) {
                    page = data.query.pages[0];


    if (text.length > 140) {
                    if (
        text = text.slice(0, 140);
                        page &&
    }
                        page.imageinfo &&
                        page.imageinfo.length
                    ) {
                        info = page.imageinfo[0];
                    }
                }


    greatWallState.saving = true;
                finish((info && (info.thumburl || info.url)) || src);
    greatWallState.statusText = 'SAVING';
            }).fail(function () {
    renderGreatWallBox();
                finish(src);
 
             });
    fetchGreatWallData().then(function(data) {
         }).fail(function () {
        data = normalizeGreatWallData(data);
             finish(src);
        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 applySliceImages(frame, thumbUrl) {
        var slices;
        var i;
        var img;


function deleteGreatWallEntry() {
        if (!frame || !thumbUrl) return;
    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) {
         slices = frame.querySelectorAll('.crt-page-monitor-slice');
         window.location.href = '/index.php?title=특수:로그인&returnto=' + encodeURIComponent(mw.config.get('wgPageName') || '대문');
        return;
    }


    if (!ownEntry) {
        for (i = 0; i < slices.length; i++) {
        greatWallState.statusText = 'NO MARK TO DELETE';
            slices[i].innerHTML = '';
        renderGreatWallBox();
        return;
    }


    greatWallState.saving = true;
            img = document.createElement('img');
    greatWallState.statusText = 'DELETING';
            img.className = 'crt-page-monitor-slice-img';
    renderGreatWallBox();
            img.src = thumbUrl;
            img.alt = '';
            img.decoding = 'async';
            img.loading = 'eager';
            img.setAttribute('aria-hidden', 'true');


    fetchGreatWallData().then(function(data) {
            slices[i].appendChild(img);
        data = normalizeGreatWallData(data);
        if (data.entries && data.entries[currentUser]) {
            delete data.entries[currentUser];
         }
         }


         api = new mw.Api();
         frame.setAttribute('data-crt-slices-ready', '1');
        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) {
     function initBannerFrame(frame) {
         mw.loader.using(['mediawiki.api']).then(function() {
         var baseImg;
            initGreatWallBox();
         var width;
         });
    }
}


function initGreatWallBox() {
        if (!frame) return;
    var box = document.getElementById('great-wall-sidebar');
        if (frame.getAttribute('data-crt-slices-ready') === '1') return;
    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;
        baseImg = frame.querySelector('.crt-page-monitor-image-base img');


    if (!mw.Api) {
        if (!baseImg) return;
        initGreatWallBoxWhenReady();
        return;
    }


    box.setAttribute('data-great-wall-ready', '1');
        width = parseSliceWidth(frame.getAttribute('data-crt-slice-width'));


    box.addEventListener('click', function(e) {
        resolveThumbUrl(baseImg, width, function (thumbUrl) {
        var ownButton = e.target.closest ? e.target.closest('[data-great-wall-own-entry="1"]') : null;
            if (!frame || !frame.parentNode) return;
        var currentUser = mw.config.get('wgUserName') || '';
            applySliceImages(frame, thumbUrl);
         var ownEntry = currentUser && greatWallState.data.entries ? greatWallState.data.entries[currentUser] : null;
         });
    }


         if (!ownButton || !ownEntry) return;
    function initBannerFrames(root) {
        var scope = root && root.querySelectorAll ? root : document;
         var frames = scope.querySelectorAll('.crt-page-monitor-frame');
        var i;


         greatWallState.selectedOwnEntry = true;
         for (i = 0; i < frames.length; i++) {
        greatWallState.statusText = 'YOUR MARK SELECTED';
             initBannerFrame(frames[i]);
        renderGreatWallBox();
 
        if (input) {
             input.focus();
            input.setSelectionRange(input.value.length, input.value.length);
         }
         }
     });
     }


     document.addEventListener('click', function(e) {
     $(function () {
         var target = e.target;
         initBannerFrames(document);
        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) {
     if (mw && mw.hook) {
         submit.addEventListener('click', function(e) {
         mw.hook('wikipage.content').add(function ($content) {
             e.preventDefault();
             initBannerFrames($content && $content[0] ? $content[0] : document);
            if (!mw.config.get('wgUserName')) {
                window.location.href = '/index.php?title=특수:로그인&returnto=' + encodeURIComponent(mw.config.get('wgPageName') || '대문');
                return;
            }
            saveGreatWallEntry();
         });
         });
     }
     }
})(jQuery, window.mw);


    if (deleteButton) {
/* =========================================
        deleteButton.addEventListener('click', function(e) {
  Doc Tab System — Q/E 단축키 탭 전환
            e.preventDefault();
  글리치 플리커 + RGB split + 방향 슬라이드
            deleteGreatWallEntry();
  ========================================= */
        });
    }


    if (input) {
(function () {
        input.addEventListener('keydown', function(e) {
    'use strict';
            if (e.key === 'Enter') {
                e.preventDefault();
                saveGreatWallEntry();
            }
        });


        input.addEventListener('input', function() {
var keydownBound = false;
            if (input.value.length > 140) {
                input.value = input.value.slice(0, 140);
            }
        });
    }


     greatWallState.loading = true;
     function initDocTabs() {
    greatWallState.statusText = 'SYNCING WALL';
        var tabBars = document.querySelectorAll('.doc-tab-bar');
    renderGreatWallBox();
        if (!tabBars.length) return;


    fetchGreatWallData().then(function(data) {
        tabBars.forEach(function (bar) {
        greatWallState.data = normalizeGreatWallData(data);
            if (bar.getAttribute('data-tabs-init')) return;
        greatWallState.loaded = true;
            bar.setAttribute('data-tabs-init', '1');
        greatWallState.loading = false;
        greatWallState.selectedOwnEntry = false;
        greatWallState.statusText = '';
        renderGreatWallBox();
    });
}


function buildGreatWallBox() {
             var tabs = Array.from(bar.querySelectorAll('.doc-tab'));
    return '' +
             if (!tabs.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 panel = bar.closest('.doc-panel');
    var portal = document.querySelector('.main-portal');
            var display = panel ? panel.querySelector('.doc-display') : null;
    var guestbook = portal ? portal.querySelector('.guestbook-device') : null;
            if (!display) display = document.getElementById('doc-main-display');
    var sideScreen;
            if (!display) return;
    var grid;


    if (!portal || !guestbook) 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);
                });
            });


    sideScreen = guestbook.closest ? guestbook.closest('.side-screen') : null;
            var initIdx = tabs.findIndex(function (t) { return t.classList.contains('active'); });
    grid = sideScreen && sideScreen.closest ? sideScreen.closest('.console-grid') : null;
            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 || '');
            }
        });


    if (guestbook.parentNode) {
        if (!keydownBound) {
        guestbook.parentNode.removeChild(guestbook);
            keydownBound = true;
    }
            document.addEventListener('keydown', function (e) {
                var tag = document.activeElement.tagName;
                if (tag === 'INPUT' || tag === 'TEXTAREA') return;


    if (sideScreen && !(sideScreen.textContent || '').trim() && !sideScreen.querySelector('img,svg,video,canvas,form,input,button,a')) {
                var bar = document.querySelector('.doc-tab-bar');
        if (sideScreen.parentNode) {
                if (!bar) return;
            sideScreen.parentNode.removeChild(sideScreen);
                var tabs = Array.from(bar.querySelectorAll('.doc-tab'));
        }


        if (grid) {
                var panel = bar.closest('.doc-panel');
            grid.style.gridTemplateColumns = 'minmax(0,1fr)';
                var display = panel ? panel.querySelector('.doc-display') : null;
        }
                if (!display) display = document.getElementById('doc-main-display');
    }
                if (!display) return;


    portal.classList.add('is-great-wall-relocated');
                var activeIdx = tabs.findIndex(function (t) {
}
                    return t.classList.contains('active');
                });
                if (activeIdx === -1) return;


function buildSiteInformationBox() {
                 if (e.key === 'q' || e.key === 'Q') {
    return '' +
                    e.preventDefault();
        '<div class="clbi-right-box site-info-sidebar">' +
                    var prev = (activeIdx - 1 + tabs.length) % tabs.length;
            '<div class="clbi-right-title site-info-title">' +
                     if (prev !== activeIdx) switchTab(tabs, display, prev, -1);
                '<span>정보</span>' +
                 } else if (e.key === 'e' || e.key === 'E') {
            '</div>' +
                     e.preventDefault();
            '<div class="clbi-right-content site-info-content">' +
                     var next = (activeIdx + 1) % tabs.length;
                '<div class="policy-list">' +
                     if (next !== activeIdx) switchTab(tabs, display, next, 1);
                    '<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 || '');
            }
        });
     }
     }


6,770번째 줄: 4,429번째 줄:
     var barTimerB = null;
     var barTimerB = null;
     var barTimerC = null;
     var barTimerC = null;
    var summaryRetryTimer = null;
    var summaryRetryAttempts = 0;


     function isLoggedIn() {
     function isLoggedIn() {
6,811번째 줄: 4,468번째 줄:


     function getDividerHtml() {
     function getDividerHtml() {
        /* 프로필 패널 최신 규칙: 레벨 패널과 버튼 영역 사이에 별도 나눔선은 만들지 않는다. */
         return '<div id="profile-progress-divider" class="profile-divider" aria-hidden="true"></div>';
         return '';
     }
     }


7,044번째 줄: 4,700번째 줄:
         pendingSummary = null;
         pendingSummary = null;
         pendingOptions = 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);
     }
     }


7,083번째 줄: 4,716번째 줄:
                 var payload = data && data.progress_summary;
                 var payload = data && data.progress_summary;
                 if (payload && payload.available && payload.summary) {
                 if (payload && payload.available && payload.summary) {
                    clearSummaryRetry();
                     updatePanel(payload.summary, { animateGain: false });
                     updatePanel(payload.summary, { animateGain: false });
                } else {
                    scheduleSummaryRetry(2200);
                 }
                 }
            }).catch(function () {
                scheduleSummaryRetry(2200);
             }).always(function () {
             }).always(function () {
                 summaryRequested = false;
                 summaryRequested = false;
7,095번째 줄: 4,723번째 줄:
         }, function () {
         }, function () {
             summaryRequested = false;
             summaryRequested = false;
            scheduleSummaryRetry(2200);
         });
         });
     }
     }
7,238번째 줄: 4,865번째 줄:
             requestSummary();
             requestSummary();
         }
         }
        setTimeout(function () {
            ensurePanel();
            applyPendingSummaryIfPossible();
            requestSummary();
        }, 350);
        setTimeout(function () {
            ensurePanel();
            applyPendingSummaryIfPossible();
            requestSummary();
        }, 1500);
     }
     }


7,284번째 줄: 4,899번째 줄:
         ensurePanel();
         ensurePanel();
         applyPendingSummaryIfPossible();
         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);
})(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:53 판

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) {
    $.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_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();

// ── 상단 네비게이션 바 ──
var navHtml =
    '<div id="clbi-top-nav-wrap">' +
        '<div id="clbi-top-nav">' +

            '<div id="clbi-top-nav-main">' +

                '<div id="clbi-top-nav-tabs">' +
'<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="clbi-tnav-worldbuilding">' +
    '<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>' +

                '<div id="clbi-top-nav-search">' +
                    '<input type="text" id="clbi-top-search-input" placeholder="검색...">' +
                '</div>' +

                '<div id="clbi-top-nav-right">' +
                    '<div class="clbi-top-nav-item" id="clbi-tnav-info">' +
                        '<span class="clbi-tnav-label">ℹ</span>' +
                    '</div>' +
                '</div>' +

            '</div>' +

'<div id="clbi-sub-worldbuilding">' +
    '<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>' +
    '</div>';

$('.content-wrapper').before(navHtml);

$('#clbi-tnav-worldbuilding').on('click', function() {
    var $menu = $('#clbi-sub-worldbuilding');
    var $btn = $(this);

    $menu.toggleClass('worldbuilding-open');
    $btn.toggleClass('clbi-tnav-active', $menu.hasClass('worldbuilding-open'));
});

$('#clbi-top-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);
    }
});

});

// 페이지 전환 사운드
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 '';
}


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

            $target.html(html);

            $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 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 getCatlinkElements(root) {
    var scope = root || document;
    var found = [];
    var seen = [];

    $(scope).find('#catlinks, .catlinks').addBack('#catlinks, .catlinks').each(function () {
        if (seen.indexOf(this) !== -1) return;
        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');
        cat.removeAttribute('aria-hidden');

        hiddenSections = cat.querySelectorAll('#mw-hidden-catlinks, .mw-hidden-catlinks, .mw-hidden-cats-hidden, .mw-hidden-cats-user-shown');

        Array.prototype.forEach.call(hiddenSections, function (section) {
            section.classList.add('clbi-catlinks-section-visible');
            section.removeAttribute('hidden');
            section.removeAttribute('aria-hidden');
            section.style.removeProperty('visibility');
            section.style.removeProperty('opacity');
            section.style.setProperty('display', 'block', 'important');
        });

        if (cat.classList.contains('catlinks-allhidden') && hiddenSections.length) {
            cat.classList.add('clbi-catlinks-show-hidden');
        }
    });
}

function setCatlinksPanelHidden(hidden) {
    getCatlinkElements(document).each(function () {
        if (hidden) {
            this.style.setProperty('display', 'none', 'important');
        } else {
            this.style.removeProperty('display');
            normalizeCatlinksPanel(this);
        }
    });
}

function moveCatlinksToBottom() {
    var main = $('.liberty-content-main');
    var parserOutput = $('.liberty-content-main .mw-parser-output').first();
    var catlinks = getCatlinkElements(document);

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

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

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

    normalizeCatlinksPanel(parserOutput.length ? parserOutput[0] : main[0]);
}

// 대문 스타일
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 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);

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

    if (isMainPage) {
        $('.liberty-content-header').css('display', 'none');
        $('.mw-page-title-main').addClass('clbi-hide');
        setCatlinksPanelHidden(true);
        $('.liberty-content-main').css('border-radius', '0');

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

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

        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');
        setCatlinksPanelHidden(true);
        $('.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');
        $('.liberty-content').addClass('content-tools-hidden');

    } else if (isScreenDoc) {
        $('.liberty-content-header').css('display', 'none');
        $('.mw-page-title-main').addClass('clbi-hide');
        setCatlinksPanelHidden(false);
        $('.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"><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');
        $('.liberty-content').addClass('content-tools-hidden');

    } else {
        $('.liberty-content-header').css('display', '');
        $('.mw-page-title-main, .mw-page-title-namespace, .mw-page-title-separator').removeClass('clbi-hide');
        setCatlinksPanelHidden(false);
        $('.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');
    }

    if (hideTools) {
        $('.content-tools').css('display', 'none');
        $('.liberty-content').addClass('content-tools-hidden');
    } else if (!isScreenDoc && !isUserProfilePage) {
        $('.content-tools').css('display', '');
        $('.liberty-content').removeClass('content-tools-hidden');
    }

    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() {
    // 왼쪽 목차: 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.innerHTML =
        '<span class="sidebar-title-svg" aria-hidden="true">' + CLBI_SVG_LIST + '</span> ' + 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-right-box right-billboard-box" data-billboard-index="0">' +
            '<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() {
    return '' +
        '<div class="clbi-right-box site-info-sidebar">' +
            '<div class="clbi-right-title site-info-title">' +
                '<span>SITE INFORMATION</span>' +
                '<span class="site-info-title-meta">LINKS</span>' +
            '</div>' +
            '<div class="clbi-right-content site-info-content">' +
                '<div class="policy-list">' +
                    '<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>' +
                    '<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>' +
                '</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 rightVisualBox = '';
        var siteInformationBox = '';

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

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


        var sidebar = userBox + rightVisualBox + siteInformationBox;

        $('.content-wrapper').append('<div id="clbi-right-sidebar">' + sidebar + '</div>');
    }


    if ($('#clbi-left-sidebar').length === 0) {
var leftSidebar =
    '<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>' +
                '<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>' +
                            '<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>' +

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

            '</div>' +
        '</div>' +
    '</div>';

        $('.content-wrapper').prepend(leftSidebar);

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

    try {
        initRightBillboardCarousel();
    } catch (err) {
        console.error('Right billboard carousel failed:', err);
    }

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

                window.scrollTo(0, 0);
                mw.hook('wikipage.content').fire($('.liberty-content-main'));
                applyMainPageStyle();
                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);
    });
});

// 시간 계산 함수
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 — Q/E 단축키 탭 전환
   글리치 플리커 + RGB split + 방향 슬라이드
   ========================================= */

(function () {
    'use strict';

var keydownBound = false;

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

        if (!keydownBound) {
            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');
                if (!bar) return;
                var tabs = Array.from(bar.querySelectorAll('.doc-tab'));

                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');
                });
                if (activeIdx === -1) return;

                if (e.key === 'q' || e.key === 'Q') {
                    e.preventDefault();
                    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;

    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;

    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 '<div id="profile-progress-divider" class="profile-divider" aria-hidden="true"></div>';
    }

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

    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) {
                    updatePanel(payload.summary, { animateGain: false });
                }
            }).always(function () {
                summaryRequested = false;
            });
        }, function () {
            summaryRequested = false;
        });
    }

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

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