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

편집 요약 없음
편집 요약 없음
2,031번째 줄: 2,031번째 줄:
                 });
                 });
             });
             });
            // 초기 활성 탭 콘텐츠 로드
            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 || '');
            }


             document.addEventListener('keydown', function (e) {
             document.addEventListener('keydown', function (e) {
2,063번째 줄: 2,071번째 줄:
         tabs[nextIdx].classList.add('active');
         tabs[nextIdx].classList.add('active');


         var nextContent = tabs[nextIdx].dataset.content || '';
         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 () {
         glitchOut(display, dir, function () {

2026년 5월 10일 (일) 04:07 판

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

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() {
    $('body').prepend(
        '<div class="WW-bg" style="position:fixed;top:0;left:0;width:100%;height:100vh;"></div>'
    );
});

// 페이지 전환 사운드
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 updateLeftSidebarNationsImage() {
    var imageId = 'clbi-left-nations-image';

    // SPA 이동 시 이전 문서에서 삽입했던 이미지를 먼저 제거한다.
    // 이 처리가 없으면 국가_및_조합에서 다른 문서로 이동했을 때 이미지가 남을 수 있다.
    $('#' + imageId).remove();

    // 현재 문서명이 국가 및 조합일 때만 삽입한다.
    // wgPageName은 보통 국가_및_조합처럼 언더스코어를 포함하므로 normalizePageName으로 통일해서 비교한다.
    var pageName = normalizePageName(mw.config.get('wgPageName'));
    var targetPage = normalizePageName('국가_및_조합');

    if (pageName !== targetPage) {
        return;
    }

    var $leftSidebar = $('#clbi-left-sidebar');

    if (!$leftSidebar.length) {
        return;
    }

    // 왼쪽 사이드바 기본 자식 순서:
    // 0 = 언어 섹션, 1 = 카테고리 섹션.
    // 이미지는 카테고리 섹션 바로 아래에 삽입한다.
    var $categoryBox = $leftSidebar.children('.clbi-left-box').eq(1);

    if (!$categoryBox.length) {
        return;
    }

    // 컨테이너나 테두리를 추가하지 않고 img 요소만 넣는다.
    // 실제 파일을 바꾸려면 Redirect/file 뒤 파일명만 교체하면 된다.
    $categoryBox.after(
        '<img id="' + imageId + '" ' +
        'class="clbi-left-page-image" ' +
        'src="/index.php?title=특수:Redirect/file/img-nations-sidebar-001.png" ' +
        'alt="" loading="lazy">'
    );
}

// 사이드바 업데이트
function updateSidebar() {
    if (!window.LANG || !window.LANG_NAMES || !window.CAT_LINKS) {
        setTimeout(updateSidebar, 100);
        return;
    }

    var langData = document.getElementById('clbi-lang-data');
    var currentLang = getCurrentLang();
    var t = (window.LANG && window.LANG[currentLang]) ? window.LANG[currentLang] : window.LANG.ko;
    var names = (window.LANG_NAMES && window.LANG_NAMES[currentLang]) ? window.LANG_NAMES[currentLang] : window.LANG_NAMES.ko;
    var cl = (window.CAT_LINKS && window.CAT_LINKS[currentLang]) ? window.CAT_LINKS[currentLang] : window.CAT_LINKS.ko;

    var allLangs = langData ? {
        ko: langData.getAttribute('data-ko') || '',
        en: langData.getAttribute('data-en') || '',
        zh: langData.getAttribute('data-zh') || '',
        ja: langData.getAttribute('data-ja') || ''
    } : {
        ko: '',
        en: '',
        zh: '',
        ja: ''
    };

    $('#clbi-lang-current').text(names[currentLang]);

    var langHtml = '';
    $.each(['ko', 'en', 'zh', 'ja'], function(i, lang) {
        if (lang === currentLang) return;

        var target = allLangs[lang];
        if (target) {
            langHtml += '<div class="clbi-lang-link"><a href="' + buildWikiPath(target) + '?uselang=' + lang + '">' + names[lang] + '</a></div>';
        } else {
            langHtml += '<div class="clbi-lang-wip">' + names[lang] + '</div>';
        }
    });
    $('#clbi-lang-list').html(langHtml);

    $('#clbi-title-language').text(t.language);
    $('#clbi-title-categories').text(t.categories);
    $('#clbi-title-links').text(t.links);
    $('#clbi-title-search a').text(t.search);
    $('#clbi-search-input').attr('placeholder', t.search + '...');
    $('#clbi-title-recent a').text(t.recentChanges);
    $('#clbi-title-guide').text(t.guide);
    $('#clbi-guide-link').text(t.getStarted);
    $('#clbi-title-playlist').text(t.playlist);

    $('#clbi-cat-main a').attr('href', buildWikiPath(cl.main));
    $('#clbi-cat-main .clbi-cat-label').text(t.mainMenu);

    $('#clbi-cat-nations a').attr('href', buildWikiPath(cl.nations));
    $('#clbi-cat-nations .clbi-cat-label').text(t.nations);

    $('#clbi-cat-corporations a').attr('href', buildWikiPath(cl.corporations));
    $('#clbi-cat-corporations .clbi-cat-label').text(t.corporations);

    $('#clbi-cat-military a').attr('href', buildWikiPath(cl.military));
    $('#clbi-cat-military .clbi-cat-label').text(t.military);

    $('#clbi-cat-history a').attr('href', buildWikiPath(cl.history));
    $('#clbi-cat-history .clbi-cat-label').text(t.history);

    $('#clbi-cat-personnel a').attr('href', buildWikiPath(cl.personnel));
    $('#clbi-cat-personnel .clbi-cat-label').text(t.personnel);

    $('#clbi-btn-contribution').text(t.contribution);
    $('#clbi-btn-watchlist').text(t.watchlist);
    $('#clbi-btn-preferences').text(t.preferences);
    $('#clbi-btn-logout').text(t.logout);
    $('#clbi-btn-login').text(t.login);

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

    $('.clbi-cat-btn').removeClass('clbi-cat-active');
    $.each(['main', 'nations', 'corporations', 'military', 'history', 'personnel'], function(i, key) {
        if (cl[key] && pageName === normalizePageName(cl[key])) {
            $('#clbi-cat-' + key).addClass('clbi-cat-active');
        }
    });

    $('.clbi-user-btn').removeClass('clbi-user-btn-active');

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

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

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

    $('.toggleBtn').each(function() {
        var btn = $(this);
        if (!$('#' + btn.data('target')).hasClass('folding-open')) {
            btn.text(t.expand);
        } else {
            btn.text(t.collapse);
        }
    });

    // 왼쪽 사이드바의 문서별 전용 삽입물을 갱신한다.
    // 현재는 국가_및_조합 문서에서만 카테고리 섹션 아래 이미지를 삽입한다.
    updateLeftSidebarNationsImage();
}

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

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

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

    return true;
}

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

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

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

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

// 대문 스타일
function applyMainPageStyle() {
    var specialPage = mw.config.get('wgCanonicalSpecialPageName');
    if (specialPage === 'Preferences') return;

    var pageName = normalizePageName(mw.config.get('wgPageName'));
    var hideTools = (pageName === '대문' || !canShowContentTools());

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

    if (pageName === '대문') {
        $('.liberty-content-header').css('display', 'none');
        $('.mw-page-title-main').addClass('clbi-hide');
        $('.catlinks').css('display', 'none');
        $('.liberty-content-main').css('border-radius', '5px 5px 0 0');

        if ($('#clbi-main-logo').length === 0) {
            $('.liberty-content').prepend(
                '<div id="clbi-main-logo" style="text-align:center;padding:10px 0;">' +
                '<img src="/index.php?title=특수:Redirect/file/Img-clbi-001.png" style="width:900px;height:auto;">' +
                '</div>'
            );
        }

        if ($('#clbi-main-crt-hero').length && $('#clbi-main-crt-hero-wrap').length === 0) {
            var heroWrap = $('<div id="clbi-main-crt-hero-wrap"></div>');

            if ($('#clbi-main-logo').length) {
                heroWrap.insertAfter('#clbi-main-logo');
            } else {
                heroWrap.insertBefore('.liberty-content-main');
            }

            $('#clbi-main-crt-hero').appendTo('#clbi-main-crt-hero-wrap');
        }
    } else {
        $('.liberty-content-header').css('display', '');
        $('.mw-page-title-main').removeClass('clbi-hide');
        $('.catlinks').css('display', '');
        $('.liberty-content-main').css('border-radius', '');
        $('#clbi-main-logo').remove();
        $('#clbi-main-crt-hero-wrap').remove();
    }

    if (hideTools) {
        $('.content-tools').css('display', 'none');
        $('.liberty-content').addClass('content-tools-hidden');
    } else {
        $('.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="clbi-icon" style="--icon:var(--ic-ui-002)"></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);
    });
}

// 초기화 함수
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">' +
                    '<div style="display:flex;flex-direction:column;align-items:center;padding:14px 14px 10px;background:linear-gradient(to bottom, #171114 0%, #0a0909 100%);">' +
                        '<img src="' + avatarSrc + '" onerror="this.src=\'/index.php?title=특수:Redirect/file/Pfp-default.png\'" style="width:64px;height:64px;border-radius:5px;object-fit:cover;border:2px solid #854369;margin-bottom:8px;">' +
                        '<div style="position:relative;width:100%;margin-top:2px;height:18px;line-height:18px;text-align:center;">' +
                            '<button type="button" id="clbi-playlist-toggle" aria-label="플레이리스트" style="position:absolute;top:0;left:10px;background:none;border:none;padding:0;width:18px;height:18px;color:#E2E2E2;cursor:pointer;display:flex;align-items:center;justify-content:center;">' +
                                '<span class="clbi-icon" style="--icon:var(--ic-ui-009);width:16px;height:16px;"></span>' +
                            '</button>' +
                            '<a href="/index.php/사용자:' + username + '" style="font-size:13px;font-weight:700;color:#E2E2E2 !important;text-decoration:none !important;line-height:18px;display:inline-block;vertical-align:middle;max-width:calc(100% - 58px);overflow:hidden;text-overflow:ellipsis;white-space:nowrap;">' + username + '</a>' +
                            '<button type="button" id="clbi-notification-toggle" aria-label="알림" style="position:absolute;top:0;right:10px;background:none;border:none;padding:0;width:18px;height:18px;color:#E2E2E2;cursor:pointer;display:flex;align-items:center;justify-content:center;">' +
                                '<span class="clbi-icon" style="--icon:var(--ic-ui-009);width:16px;height:16px;"></span>' +
                                '<span id="clbi-notification-badge" style="display:none;position:absolute;top:-6px;right:-8px;min-width:14px;height:14px;padding:0 3px;border-radius:999px;background:#854369;color:#fff;font-size:9px;line-height:14px;font-weight:700;text-align:center;"></span>' +
                            '</button>' +
                        '</div>' +
                    '</div>' +
                    '<div class="clbi-right-content" style="border-top:1px solid #2a2a2a;padding:8px;">' +
                        '<a href="/index.php/특수:기여/' + username + '" class="clbi-user-btn" id="clbi-btn-contribution">기여</a>' +
                        '<a href="/index.php/특수:주시문서목록" class="clbi-user-btn" id="clbi-btn-watchlist">주시문서 목록</a>' +
                        '<a href="/index.php/특수:설정" class="clbi-user-btn" id="clbi-btn-preferences">설정</a>' +
                        '<a href="/index.php?title=특수:로그아웃&returnto=대문" class="clbi-user-btn clbi-user-btn-logout" id="clbi-btn-logout">로그아웃</a>' +
                    '</div>' +
                '</div>';
        } else {
            userBox =
                '<div class="clbi-right-box">' +
                    '<div style="display:flex;flex-direction:column;align-items:center;padding:14px 14px 10px;background:linear-gradient(to bottom, #171114 0%, #0a0909 100%);">' +
                        '<img src="/index.php?title=특수:Redirect/file/Pfp-default.png" style="width:64px;height:64px;border-radius:5px;object-fit:cover;border:2px solid #854369;margin-bottom:8px;">' +
                        '<span style="font-size:13px;font-weight:700;color:#E2E2E2;">Guest</span>' +
                    '</div>' +
                    '<div class="clbi-right-content" style="border-top:1px solid #2a2a2a;padding:8px;">' +
                        '<a href="/index.php?title=특수:로그인&returnto=대문" class="clbi-user-btn" id="clbi-btn-login">로그인</a>' +
                    '</div>' +
                '</div>';
        }

        var recentBox =
            '<div class="clbi-right-box">' +
                '<div class="clbi-right-title" id="clbi-title-recent"><span class="clbi-icon" style="--icon:var(--ic-ui-006)"></span> <a href="/index.php/특수:최근바뀜" style="color:#E2E2E2 !important;text-decoration:none !important;">최근 변경</a></div>' +
                '<div class="clbi-right-content" id="clbi-recent-list">불러오는 중...</div>' +
            '</div>';

        var linkBox =
            '<div class="clbi-right-box">' +
                '<div class="clbi-right-title" id="clbi-title-links"><span class="clbi-icon" style="--icon:var(--ic-ui-003)"></span> 링크</div>' +
                '<div class="clbi-right-content clbi-link-box">' +
                    '<ul>' +
                        '<li><a href="https://discord.gg/ctaeJ9d3Q5" target="_blank">Discord</a></li>' +
                        '<li><a href="https://www.youtube.com/@nxdsxn" target="_blank">YouTube</a></li>' +
                        '<li><a href="https://x.com/nxd_sxn" target="_blank">X</a></li>' +
                    '</ul>' +
                '</div>' +
            '</div>';

        var sidebar =
            userBox +
            '<div class="clbi-right-box">' +
                '<div class="clbi-right-title" id="clbi-title-search"><span class="clbi-icon" style="--icon:var(--ic-ui-005)"></span> <a href="/index.php/특수:검색" style="color:#E2E2E2 !important;text-decoration:none !important;">검색</a></div>' +
                '<div class="clbi-right-content">' +
                    '<input id="clbi-search-input" type="text" placeholder="검색...">' +
                    '<button id="clbi-search-btn">GO</button>' +
                '</div>' +
            '</div>' +
            recentBox +
            '<div class="clbi-right-box">' +
                '<div class="clbi-right-title" id="clbi-title-guide"><span class="clbi-icon" style="--icon:var(--ic-ui-007)"></span> 가이드</div>' +
                '<div class="clbi-right-content">' +
                    '<a href="/index.php/CLBI_Wiki/KR_시작하기_(CLBI)" id="clbi-guide-link">시작하기</a>' +
                '</div>' +
            '</div>' +
            linkBox;

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

        $('#clbi-search-btn').click(function() {
            var query = $('#clbi-search-input').val();
            if (query) {
                window.location.href = '/index.php?search=' + encodeURIComponent(query);
            }
        });

        $('#clbi-search-input').keypress(function(e) {
            if (e.which === 13) $('#clbi-search-btn').click();
        });

        $.getJSON(
            '/api.php?action=query&list=recentchanges&rclimit=5&rcprop=title|timestamp&format=json&rcnamespace=0&rctype=edit|new',
            function(data) {
                var items = data.query.recentchanges;
                var html = '';

                $.each(items, function(i, item) {
                    var label = timeAgo(item.timestamp);
                    html +=
                        '<div class="clbi-recent-item">' +
                            '<div class="clbi-recent-title-wrap">' +
                                '<a href="/index.php/' + encodeURIComponent(item.title) + '" class="clbi-recent-title">' + item.title + '</a>' +
                            '</div>' +
                            '<span class="clbi-recent-time">' + label + '</span>' +
                        '</div>';
                });

                $('#clbi-recent-list').html(html);

                $('#clbi-recent-list .clbi-recent-item').each(function() {
                    var wrap = $(this).find('.clbi-recent-title-wrap');
                    var title = $(this).find('.clbi-recent-title');
                    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() {
            $('#clbi-recent-list').html('불러오기 실패');
        });
    }

    if ($('#clbi-left-sidebar').length === 0) {
        var leftSidebar =
            '<div id="clbi-left-sidebar">' +
                '<div class="clbi-left-box">' +
                    '<div class="clbi-left-title"><span class="clbi-icon" style="--icon:var(--ic-ui-001)"></span> <span id="clbi-title-language">언어</span></div>' +
                    '<div class="clbi-left-content clbi-lang-box" id="clbi-lang-box">' +
                        '<div class="clbi-lang-current" id="clbi-lang-current">한국어</div>' +
                        '<div id="clbi-lang-list" style="display:none;"></div>' +
                    '</div>' +
                '</div>' +
                '<div class="clbi-left-box">' +
                    '<div class="clbi-left-title"><span class="clbi-icon" style="--icon:var(--ic-ui-002)"></span> <span id="clbi-title-categories">카테고리</span></div>' +
                    '<div class="clbi-left-content clbi-cat-box">' +
                        '<div class="clbi-cat-btn" id="clbi-cat-main"><a href="/index.php/대문"><div class="clbi-cat-text"><span class="clbi-cat-label">메인 메뉴</span><span class="clbi-cat-sub">MAIN MENU</span></div></a><div class="clbi-cat-arrow">▶</div></div>' +
                        '<div class="clbi-cat-btn" id="clbi-cat-nations"><a href="/index.php/국가_및_조합"><div class="clbi-cat-text"><span class="clbi-cat-label">국가 및 조합</span><span class="clbi-cat-sub">NATIONS & FACTIONS</span></div></a><div class="clbi-cat-arrow">▶</div></div>' +
                        '<div class="clbi-cat-btn" id="clbi-cat-corporations"><a href="/index.php/기업_및_공동체"><div class="clbi-cat-text"><span class="clbi-cat-label">기업 및 공동체</span><span class="clbi-cat-sub">CORPORATIONS & COMMUNITIES</span></div></a><div class="clbi-cat-arrow">▶</div></div>' +
                        '<div class="clbi-cat-btn" id="clbi-cat-military"><a href="/index.php/군_정치집단"><div class="clbi-cat-text"><span class="clbi-cat-label">군, 정치집단</span><span class="clbi-cat-sub">MILITARY & POLITICS</span></div></a><div class="clbi-cat-arrow">▶</div></div>' +
                        '<div class="clbi-cat-btn" id="clbi-cat-history"><a href="/index.php/역사적_사건"><div class="clbi-cat-text"><span class="clbi-cat-label">역사적 사건</span><span class="clbi-cat-sub">HISTORICAL EVENTS</span></div></a><div class="clbi-cat-arrow">▶</div></div>' +
                        '<div class="clbi-cat-btn" id="clbi-cat-personnel"><a href="/index.php/인물"><div class="clbi-cat-text"><span class="clbi-cat-label">인물</span><span class="clbi-cat-sub">KEY PERSONNEL</span></div></a><div class="clbi-cat-arrow">▶</div></div>' +
                    '</div>' +
                '</div>' +
            '</div>';

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

        // 왼쪽 사이드바가 처음 생성된 직후에도 한 번 실행한다.
        // 이후 언어 갱신과 SPA 이동 시에는 updateSidebar()에서 다시 실행된다.
        updateLeftSidebarNationsImage();

        $(document).on('click', '#clbi-lang-current', function() {
            if ($('#clbi-lang-list').is(':hidden')) {
                $('#clbi-lang-current').css('margin-bottom', '8px');
                $('#clbi-lang-list').slideDown(200);
            } else {
                $('#clbi-lang-list').slideUp(200, function() {
                    $('#clbi-lang-current').css('margin-bottom', '0');
                });
            }
        });
    }

    applyMainPageStyle();
    $('#side-toc-box').remove();

    mw.loader.using(['mediawiki.api']).then(function() {
        setTimeout(function() {
            initNotifications();
            initPlaylistPopup();
            initProfile();
            moveTocToLeftSidebar();
        }, 300);

        setTimeout(moveTocToLeftSidebar, 800);
        setTimeout(moveTocToLeftSidebar, 1500);
    });
}

$(function() {
    loadLangScript(function() {
        setTimeout(function() {
            initSidebars();
        }, 100);
    });
});

// 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) {
        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 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();
                    $('.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);
                $('#clbi-lang-list').hide();
                $('#clbi-lang-current').css('margin-bottom', '0');

                mw.hook('wikipage.content').fire($('.liberty-content-main'));
                applyMainPageStyle();

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

    var ns = mw.config.get('wgNamespaceNumber');
    var title = mw.config.get('wgTitle');

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

    if (mw.config.get('wgCanonicalSpecialPageName') === '사용자정보') {
        initUserProfilePage();
    }
}

function renderProfile(username) {
    var api = new mw.Api();
    api.get({
        action: 'query',
        list: 'users',
        ususers: username,
        usprop: 'editcount'
    }).then(function(data) {
        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;
    var editBtn = isOwnPage
        ? '<a href="/index.php/특수:사용자정보" class="profile-edit-btn">프로필 수정</a>'
        : '';

    var card = document.createElement('div');
    card.className = 'profile-card';
    card.innerHTML =
        '<div class="profile-header">' +
            '<div class="profile-avatar">' +
                '<img src="/index.php?title=특수:Redirect/file/Pfp-' + username + '.png&width=120"' +
                ' onerror="this.src=\'/index.php?title=특수:Redirect/file/Pfp-default.png&width=120\'"' +
                ' alt="' + username + '">' +
            '</div>' +
            '<div class="profile-info">' +
                '<h2 class="profile-username">' + username + '</h2>' +
                '<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 class="profile-bio" data-field="bio"></div>' +
                '<div class="profile-badges" data-field="badges"></div>' +
            '</div>' +
            editBtn +
        '</div>' +
        '<div class="profile-stats">' +
            '<div class="profile-stat">' +
                '<span class="clbi-stat-value">' + editCount + '</span>' +
                '<span class="clbi-stat-label">수정 횟수</span>' +
            '</div>' +
        '</div>';

    $('.profile-card').remove();
    container.insertBefore(card, container.firstChild);
    loadProfileFields(username, card);
}

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 || '',
            badges: profile.badges || ''
        });
    }).fail(function() {
        updateProfileFields(card, {
            name: '',
            discord: '',
            role: '',
            bio: '',
            badges: ''
        });
    });
}

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"]');
    var badgesEl = card.querySelector('[data-field="badges"]');

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

    if (badgesEl) {
        if (data.badges) {
            var badges = data.badges.split(',');
            var html = '';

            for (var i = 0; i < badges.length; i++) {
                html += '<span class="clbi-badge">' + badges[i].trim() + '</span>';
            }

            badgesEl.innerHTML = html;
        } else {
            badgesEl.innerHTML = '';
        }
    }
}
// ========== 프로필 시스템 끝 ==========

// ========== 알림 시스템 ==========
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:10px;' +
        '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('clbi-notification-toggle');
    var popup = document.getElementById('clbi-notification-popup');
    if (!btn || !popup) return;

    var rect = btn.getBoundingClientRect();
    var top = rect.bottom + 2;
    var left = rect.right - popup.offsetWidth;

    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, window.innerHeight - popup.offsetHeight - 8);
    }

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

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 btn = document.getElementById('clbi-notification-toggle');
    if (!btn) return;

    ensureNotificationPopup();
    loadNotificationsIntoPopup();

    $(document)
        .off('click.clbiNotificationToggle')
        .on('click.clbiNotificationToggle', '#clbi-notification-toggle', function(e) {
            e.preventDefault();
            e.stopPropagation();

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

            if (playlistPopup) {
                playlistPopup.style.display = 'none';
            }

            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 toggle = document.getElementById('clbi-notification-toggle');
            if (!popup || !toggle) return;

            if (!popup.contains(e.target) && !toggle.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 ensurePlaylistPopup() {
    if (document.getElementById('clbi-playlist-popup')) return;

    var popup = document.createElement('div');
    popup.id = 'clbi-playlist-popup';
    popup.style.cssText =
        'display:none;position:fixed;z-index:99999;width:690px;height:540px;' +
        'background:#0a0909;border:2px solid #854369;border-radius:10px;' +
        '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;">' +
            '<span>플레이리스트</span>' +
        '</div>' +
        '<div style="padding:0;background:#111;height:calc(100% - 42px);">' +
            '<iframe style="border:none;width:100%;height:100%;" src="https://open.spotify.com/embed/playlist/32l4ke6djdQn8LoBp1ipR9?utm_source=generator&theme=0" allowfullscreen allow="autoplay; clipboard-write; encrypted-media; fullscreen; picture-in-picture" loading="lazy"></iframe>' +
        '</div>';

    document.body.appendChild(popup);
}

function positionPlaylistPopup() {
    var btn = document.getElementById('clbi-playlist-toggle');
    var popup = document.getElementById('clbi-playlist-popup');
    if (!btn || !popup) return;

    var rect = btn.getBoundingClientRect();
    var top = rect.bottom + 2;
    var left = rect.right - popup.offsetWidth;

    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, window.innerHeight - popup.offsetHeight - 8);
    }

    popup.style.top = top + 'px';
    popup.style.left = left + 'px';
}

function initPlaylistPopup() {
    var btn = document.getElementById('clbi-playlist-toggle');
    if (!btn) return;

    ensurePlaylistPopup();

    $(document)
        .off('click.clbiPlaylistToggle')
        .on('click.clbiPlaylistToggle', '#clbi-playlist-toggle', function(e) {
            e.preventDefault();
            e.stopPropagation();

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

            if (notificationPopup) {
                notificationPopup.style.display = 'none';
            }

            if (popup.style.display === 'none' || popup.style.display === '') {
                popup.style.display = 'block';
                positionPlaylistPopup();
            } else {
                popup.style.display = 'none';
            }
        });

    $(document)
        .off('click.clbiPlaylistOutside')
        .on('click.clbiPlaylistOutside', function(e) {
            var popup = document.getElementById('clbi-playlist-popup');
            var toggle = document.getElementById('clbi-playlist-toggle');
            if (!popup || !toggle) return;

            if (!popup.contains(e.target) && !toggle.contains(e.target)) {
                popup.style.display = 'none';
            }
        });

    $(window)
        .off('resize.clbiPlaylist')
        .on('resize.clbiPlaylist', function() {
            var popup = document.getElementById('clbi-playlist-popup');
            if (popup && popup.style.display === 'block') {
                positionPlaylistPopup();
            }
        });
}
// ========== 플레이리스트 팝업 시스템 끝 ==========

function initUserProfilePage() {
    var saveBtn = document.getElementById('pref-save');
    if (!saveBtn) return;

    var adminEl = document.getElementById('pref-is-admin');
    var isAdmin = adminEl && adminEl.getAttribute('data-admin') === '1';
    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'];
        if (isAdmin) fields.push('badges');

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

    function initDocTabs() {
        var tabBars = document.querySelectorAll('.doc-tab-bar');
        if (!tabBars.length) return;

        tabBars.forEach(function (bar) {
            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) 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 || '');
            }

            document.addEventListener('keydown', function (e) {
                var tag = document.activeElement.tagName;
                if (tag === 'INPUT' || tag === 'TEXTAREA') 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();
    }

})();