/**
 * Melon Lite - HTML5 Audio ?????쟿??곷선 ??뽯선 JS (4筌???源???疫꿸퀣肉?獄??類ㅼ삢 ???쟿??곷선 ?⑥쥓猷??甕곌쑴??
 */

document.addEventListener('DOMContentLoaded', function() {
    if (window.__MELON_LITE_PLAYER_INITED) return;
    window.__MELON_LITE_PLAYER_INITED = true;
    function readUserPlayerSettingOverrides() {
        try {
            const raw = localStorage.getItem('melon_lite_user_preferences_v1');
            if (!raw) return {};
            const prefs = JSON.parse(raw);
            if (!prefs || typeof prefs !== 'object') return {};
            const overrides = {};
            if (prefs.resumePlayback === '0' || prefs.resumePlayback === false) overrides.resumeEnabled = false;
            if (prefs.resumePlayback === '1' || prefs.resumePlayback === true) overrides.resumeEnabled = true;
            if (prefs.defaultShuffle === '0' || prefs.defaultShuffle === false) overrides.defaultShuffle = false;
            if (prefs.defaultShuffle === '1' || prefs.defaultShuffle === true) overrides.defaultShuffle = true;
            if (['none', 'all', 'one'].includes(prefs.defaultRepeat)) overrides.defaultRepeat = prefs.defaultRepeat;
            const volume = parseInt(prefs.defaultVolume, 10);
            if (Number.isFinite(volume)) overrides.defaultVolume = Math.max(0, Math.min(100, volume)) / 100;
            return overrides;
        } catch (err) {
            return {};
        }
    }
    const appPlayerSettings = Object.assign({
        defaultVolume: 0.8,
        resumeEnabled: true,
        autoNextEnabled: true,
        historyEnabled: true,
        countRule: 'start',
        defaultRepeat: 'none',
        defaultShuffle: false,
        mobileCompact: false,
        mediaSessionEnabled: true,
        keyboardShortcutsEnabled: true,
        lazyLoadingEnabled: true,
        isLoggedIn: false,
        guestPreviewSeconds: 30
    }, window.APP_PLAYER_SETTINGS || {}, readUserPlayerSettingOverrides());
    const countedSongIds = new Set();
    const guestPreviewMode = appPlayerSettings.isLoggedIn !== true;
    const guestPreviewSeconds = Math.max(5, Number(appPlayerSettings.guestPreviewSeconds || 30));
    const TEXT_FALLBACKS = {
        title: '제목 없음',
        artist: '알 수 없는 아티스트',
        album: '',
        emptyQueue: '재생 대기열이 비어 있습니다.',
        noCurrentTrack: '재생 중인 곡이 없습니다.',
        noLyrics: '등록된 가사가 없습니다.',
        loadLyricsError: '가사를 불러오지 못했습니다.'
    };

    function safeText(value, fallback = '') {
        if (value === null || value === undefined) return fallback;
        const text = String(value);
        if (!text || text === 'null' || text === 'undefined') return fallback;
        return text;
    }

    function setElementText(element, value, fallback = '') {
        if (element) element.textContent = safeText(value, fallback);
    }
    // ==========================================================================
    // 1. DOM Elements
    // ==========================================================================
    const audio = document.getElementById('audio-player');
    if (!audio) return;
    const playerBar = document.querySelector('.player-bar');
    const btnPlayPause = document.getElementById('btn-play-pause');
    
    // 筌롫뗄????롫뼊 ?⑥쥙?????쟿??곷선 ?뚢뫂?껅에?    const btnPlayPause = document.getElementById('btn-play-pause');
    const btnPrev = document.getElementById('btn-prev');
    const btnNext = document.getElementById('btn-next');
    const btnShuffle = document.getElementById('btn-shuffle');
    const btnRepeat = document.getElementById('btn-repeat');
    const btnQueue = document.getElementById('btn-queue');
    const btnExpandPlayer = document.getElementById('btn-expand-player');
    
    const playerTitle = document.getElementById('player-title');
    const playerArtist = document.getElementById('player-artist');
    const timeCurrent = document.getElementById('time-current');
    const timeTotal = document.getElementById('time-total');
    const progressWrapper = document.getElementById('progress-wrapper');
    const progressBar = document.getElementById('progress-bar');
    
    const btnMute = document.getElementById('btn-mute');
    const volumeWrapper = document.getElementById('volume-wrapper');
    const volumeBar = document.getElementById('volume-bar');
    
    const playerAlbumImage = document.getElementById('player-album-image');
    const brandIcon = document.querySelector('.brand-icon');
    const albumArt = document.querySelector('.player-album-art');

    // 筌뤴뫀而????뺤쨮??    const mobileHamburger = document.getElementById('mobile-hamburger');
    const mobileHamburger = document.getElementById('mobile-hamburger');
    const appSidebar = document.getElementById('app-sidebar');
    const sidebarOverlay = document.getElementById('sidebar-overlay');

    // ???쟿?????쎈뱜 ?곕떽? ??밸씜 筌뤴뫀??
    const playlistModal = document.getElementById('playlist-modal');
    const modalClose = document.getElementById('modal-close');
    const modalSongId = document.getElementById('modal-song-id');
    const modalOptions = document.getElementById('modal-playlist-options');

    // 4筌??醫됲뇣: ??源???疫꿸퀣肉?Queue) ???????ㅺ섯 DOM
    const queuePanel = document.getElementById('queue-panel');
    const queueClose = document.getElementById('queue-close');
    const btnClearQueue = document.getElementById('btn-clear-queue');
    const queueListContainer = document.getElementById('queue-list-container');
    const queueCount = document.getElementById('queue-count');

    // 4筌??醫됲뇣: ?袁⑷퍥 ?遺얇늺 ?類ㅼ삢 ???쟿??곷선 筌뤴뫀??DOM
    const expandPlayerOverlay = document.getElementById('expand-player-overlay');
    const expandPlayerClose = document.getElementById('expand-player-close');
    const expandAlbumImage = document.getElementById('expand-album-image');
    const expandPlayerTitle = document.getElementById('expand-player-title');
    const expandPlayerArtist = document.getElementById('expand-player-artist');
    const expandPlayerAlbum = document.getElementById('expand-player-album');
    const miniLyricsLine = document.getElementById('mini-lyrics-line');
    
    // ?類ㅼ삢 ???쟿??곷선 ????뽯선
    const tabBtnQueue = document.getElementById('tab-btn-queue');
    const tabBtnLyrics = document.getElementById('tab-btn-lyrics');
    const expandTabQueueContent = document.getElementById('expand-tab-queue-content');
    const expandTabLyricsContent = document.getElementById('expand-tab-lyrics-content');
    const expandQueueList = document.getElementById('expand-queue-list');
    const expandLyricsFullText = document.getElementById('expand-lyrics-full-text');
    const expandBtnLyricsToggle = document.getElementById('expand-btn-lyrics-toggle');
    let expandTabCollapseButton = null;

    // ?類ㅼ삢 ???쟿??곷선 ??롫뼊 ?뚢뫂?껅에?살쑎
    const expandProgressWrapper = document.getElementById('expand-progress-wrapper');
    const expandProgressBar = document.getElementById('expand-progress-bar');
    const expandTimeCurrent = document.getElementById('expand-time-current');
    const expandTimeTotal = document.getElementById('expand-time-total');
    
    const expandBtnShuffle = document.getElementById('expand-btn-shuffle');
    const expandBtnPrev = document.getElementById('expand-btn-prev');
    const expandBtnPlayPause = document.getElementById('expand-btn-play-pause');
    const expandBtnNext = document.getElementById('expand-btn-next');
    const expandBtnRepeat = document.getElementById('expand-btn-repeat');
    
    const expandBtnLike = document.getElementById('expand-btn-like');
    const expandBtnAddPlaylist = document.getElementById('expand-btn-add-playlist');
    const expandBtnSongDetail = document.getElementById('expand-btn-song-detail');

    // ==========================================================================
    // 2. State Variables & LocalStorage Integration
    // ==========================================================================
    let isPlaying = false; // ??源???? ???삋域?    
    let playerState = {
        queue: [],           // ??源???疫꿸퀣肉?(揶??ⓥ돦??id, title, artist, duration, loved)
        originalQueue: [],   // ?酉逾???곸젫 ????쑴???癒?궚 ??源???뽮퐣 ??疫꿸퀣肉?
        currentIndex: -1,
        currentSongId: null,
        shuffle: appPlayerSettings.defaultShuffle === true,
        repeatMode: ['none', 'all', 'one'].includes(appPlayerSettings.defaultRepeat) ? appPlayerSettings.defaultRepeat : 'none',
        currentTime: 0,
        volume: Number.isFinite(appPlayerSettings.defaultVolume) ? appPlayerSettings.defaultVolume : 0.8,
        muted: false,
        queueContext: null,
        expanded: false,
        previewMode: guestPreviewMode,
        wasPlaying: false,
        userActivated: false,
        lastUpdated: 0
    };
    window.addEventListener('melonlite:preferences-changed', function (event) {
        const prefs = event && event.detail ? event.detail : {};
        if (prefs.resumePlayback === '0' || prefs.resumePlayback === '1') {
            appPlayerSettings.resumeEnabled = prefs.resumePlayback === '1';
        }
        if (prefs.defaultShuffle === '0' || prefs.defaultShuffle === '1') {
            appPlayerSettings.defaultShuffle = prefs.defaultShuffle === '1';
        }
        if (['none', 'all', 'one'].includes(prefs.defaultRepeat)) {
            appPlayerSettings.defaultRepeat = prefs.defaultRepeat;
        }
        const volume = parseInt(prefs.defaultVolume, 10);
        if (Number.isFinite(volume)) {
            const normalizedVolume = Math.max(0, Math.min(100, volume)) / 100;
            appPlayerSettings.defaultVolume = normalizedVolume;
            playerState.volume = normalizedVolume;
            audio.volume = normalizedVolume;
            if (volumeBar) volumeBar.style.width = `${audio.muted ? 0 : normalizedVolume * 100}%`;
            if (btnMute) updateVolumeIcon(normalizedVolume);
            saveStateToStorage();
        }
    });
    let pendingRestoreTime = null;
    let lastMediaPositionUpdate = 0;
    let progressFrameId = null;
    let previewPromptShownFor = null;
    const EXPAND_PLAYER_MOTION_MS = 260;
    let expandPlayerMotionTimer = null;
    const PREVIEW_PROMPT_DISMISS_KEY = 'melon_lite_guest_preview_prompt_dismissed_until';
    const PREVIEW_PROMPT_SESSION_KEY = 'melon_lite_guest_preview_prompt_seen_session';
    const PREVIEW_PROMPT_DISMISS_MS = 1000 * 60 * 60 * 24;
    const lyricsCache = new Map();
    const syncedLyricsState = {
        songId: null,
        lines: [],
        activeIndex: -1
    };
    const expandSwipeState = {
        active: false,
        startX: 0,
        startY: 0,
        lastY: 0,
        lastTime: 0,
        velocity: 0,
        deltaY: 0,
        scrollable: null,
        closeTimer: null
    };
    const expandArtSwipeState = {
        active: false,
        tracking: false,
        startX: 0,
        startY: 0,
        lastX: 0,
        lastTime: 0,
        velocity: 0,
        deltaX: 0,
        timer: null
    };
    const streamFailureCounts = new Map();
    const playbackFailureReportTimes = new Map();
    let isTrackTransitioning = false;
    let transitionClearTimer = null;
    let lastEndedSignature = '';
    let lastEndedAt = 0;
    let playRequestSerial = 0;

    function showPlayerNotice(message, type = 'info') {
        if (window.showToast && window.showToast !== showPlayerNotice) {
            window.showToast(message, type);
            return;
        }
        let toast = document.getElementById('melon-lite-toast');
        if (!toast) {
            toast = document.createElement('div');
            toast.id = 'melon-lite-toast';
            toast.className = 'melon-lite-toast';
            toast.setAttribute('role', 'status');
            toast.setAttribute('aria-live', 'polite');
            document.body.appendChild(toast);
        }
        toast.textContent = message;
        toast.dataset.type = type;
        toast.classList.add('show');
        window.clearTimeout(toast.__hideTimer);
        toast.__hideTimer = window.setTimeout(() => toast.classList.remove('show'), 3600);
    }
    window.showToast = window.showToast || showPlayerNotice;

    function prefersReducedPlayerMotion() {
        try {
            return window.matchMedia && window.matchMedia('(prefers-reduced-motion: reduce)').matches;
        } catch (err) {
            return false;
        }
    }

    function safePlayerIssue(err) {
        const raw = err && err.message ? String(err.message) : String(err || '');
        if (!raw) return 'unknown player issue';
        if (/(SQLSTATE|PDOException|Stack trace|Fatal error|Warning:|Notice:|[A-Z]:[\\\/]|\/(?:Users|laragon|xampp|var|home|srv|www)\/)/i.test(raw)) {
            return 'internal error';
        }
        return raw.replace(/\s+/g, ' ').slice(0, 180);
    }

    function logPlayerIssue(label, err) {
        try {
            console.warn('[MelonLitePlayer]', label, safePlayerIssue(err));
        } catch (noop) {}
    }

    function ensurePlayerTransitionNotice() {
        if (!playerBar) return null;
        let notice = playerBar.querySelector('.player-transition-notice');
        if (!notice) {
            notice = document.createElement('div');
            notice.className = 'player-transition-notice';
            notice.setAttribute('aria-live', 'polite');
            playerBar.appendChild(notice);
        }
        return notice;
    }

    function setPlayerTransitionState(active, track = null, message = '') {
        isTrackTransitioning = !!active;
        window.clearTimeout(transitionClearTimer);
        transitionClearTimer = null;

        const notice = ensurePlayerTransitionNotice();
        const text = active
            ? (message || (track ? `다음 곡 준비 중 · ${safeText(track.title, TEXT_FALLBACKS.title)}` : '다음 곡을 준비하고 있습니다.'))
            : '';

        if (notice) {
            notice.textContent = text;
            notice.classList.toggle('show', !!active);
        }
        if (playerBar) {
            playerBar.classList.toggle('is-track-loading', !!active);
        }
        if (expandPlayerOverlay) {
            expandPlayerOverlay.classList.toggle('is-track-loading', !!active);
        }
        document.body.classList.toggle('player-track-loading', !!active);
    }

    function clearPlayerTransitionState(delay = 0) {
        window.clearTimeout(transitionClearTimer);
        transitionClearTimer = window.setTimeout(function () {
            setPlayerTransitionState(false);
        }, Math.max(0, delay));
    }

    function getStorageNumber(storage, key) {
        try {
            const value = storage && storage.getItem(key);
            const parsed = Number(value || 0);
            return Number.isFinite(parsed) ? parsed : 0;
        } catch (err) {
            return 0;
        }
    }

    function setStorageValue(storage, key, value) {
        try {
            if (storage) storage.setItem(key, String(value));
        } catch (err) {}
    }

    function parseDurationValue(value) {
        if (typeof value === 'number' && Number.isFinite(value)) {
            return Math.max(0, Math.round(value));
        }
        const text = safeText(value, '').trim();
        if (!text) return 0;
        if (/^\d+(?:\.\d+)?$/.test(text)) {
            return Math.max(0, Math.round(Number(text)));
        }
        const parts = text.split(':').map(part => Number(part.trim()));
        if (parts.length >= 2 && parts.length <= 3 && parts.every(part => Number.isFinite(part))) {
            return parts.reduce((total, part) => (total * 60) + part, 0);
        }
        return Math.max(0, parseInt(text, 10) || 0);
    }

    function getEffectiveDuration(duration = null) {
        if (Number.isFinite(duration) && duration > 0) return duration;
        if (audio && Number.isFinite(audio.duration) && audio.duration > 0) return audio.duration;
        try {
            if (audio && audio.seekable && audio.seekable.length > 0) {
                const seekableEnd = audio.seekable.end(audio.seekable.length - 1);
                if (Number.isFinite(seekableEnd) && seekableEnd > 0) return seekableEnd;
            }
        } catch (err) {}
        const track = currentTrack();
        return track ? parseDurationValue(track.duration) : 0;
    }

    function isGuestPreviewPromptSuppressed() {
        if (!guestPreviewMode) return true;
        if (getStorageNumber(window.localStorage, PREVIEW_PROMPT_DISMISS_KEY) > Date.now()) return true;
        try {
            return window.sessionStorage && window.sessionStorage.getItem(PREVIEW_PROMPT_SESSION_KEY) === '1';
        } catch (err) {
            return false;
        }
    }

    function rememberGuestPreviewPromptSeen(dismissForToday = false) {
        setStorageValue(window.sessionStorage, PREVIEW_PROMPT_SESSION_KEY, '1');
        if (dismissForToday) {
            setStorageValue(window.localStorage, PREVIEW_PROMPT_DISMISS_KEY, Date.now() + PREVIEW_PROMPT_DISMISS_MS);
        }
    }

    function showGuestPreviewPrompt() {
        if (!guestPreviewMode) return;
        if (document.getElementById('guest-preview-prompt')) return;
        const track = currentTrack();
        const trackId = track ? String(track.id) : 'unknown';
        if (previewPromptShownFor === trackId) return;
        previewPromptShownFor = trackId;
        if (isGuestPreviewPromptSuppressed()) {
            return;
        }
        rememberGuestPreviewPromptSeen(false);
        document.body.classList.add('guest-preview-ended');
        document.dispatchEvent(new CustomEvent('melonlite:guest-preview-ended', { detail: { trackId } }));
        const box = document.createElement('div');
        box.id = 'guest-preview-prompt';
        box.className = 'guest-preview-prompt preview-premium-modal';
        box.setAttribute('role', 'dialog');
        box.setAttribute('aria-modal', 'true');
        box.setAttribute('aria-live', 'polite');
        box.setAttribute('aria-labelledby', 'guest-preview-title');
        box.tabIndex = -1;

        const basePath = window.APP_BASE_PATH || './';
        const currentPath = `${window.location.pathname.replace(/^\/+/, '')}${window.location.search || ''}`;
        const returnPath = currentPath.replace(/^music\//, '') || 'index.php';
        const loginUrl = `${basePath}login.php?return=${encodeURIComponent(returnPath)}`;
        const coverUrl = track ? getTrackCoverUrl(track) : `${basePath}assets/img/default-cover.svg`;

        const media = document.createElement('div');
        media.className = 'guest-preview-media';
        const art = document.createElement('img');
        art.src = coverUrl;
        art.alt = '';
        art.loading = 'lazy';
        art.width = 56;
        art.height = 56;
        art.onerror = function () {
            this.onerror = null;
            this.src = `${basePath}assets/img/default-cover.svg`;
        };
        media.appendChild(art);

        const copy = document.createElement('div');
        copy.className = 'guest-preview-copy';
        const badge = document.createElement('span');
        badge.className = 'guest-preview-badge';
        badge.textContent = `${guestPreviewSeconds}초 미리듣기 종료`;
        const title = document.createElement('strong');
        title.id = 'guest-preview-title';
        title.textContent = '미리듣기가 종료되었습니다.';
        const desc = document.createElement('span');
        desc.textContent = '로그인하면 전체 곡을 계속 감상할 수 있습니다.';
        const now = document.createElement('em');
        now.className = 'guest-preview-track';
        now.textContent = track ? `${safeText(track.title, TEXT_FALLBACKS.title)} · ${safeText(track.artist, TEXT_FALLBACKS.artist)}` : '현재는 방문자 미리듣기 모드입니다.';
        copy.append(badge, title, desc, now);

        const actions = document.createElement('div');
        actions.className = 'guest-preview-actions';
        const login = document.createElement('a');
        login.className = 'btn btn-primary btn-sm guest-preview-login';
        login.href = loginUrl;
        login.textContent = '로그인하기';
        login.addEventListener('click', saveStateToStorage);
        const close = document.createElement('button');
        close.type = 'button';
        close.className = 'btn btn-secondary btn-sm';
        close.id = 'guest-preview-close';
        close.textContent = '계속 둘러보기';
        const dismiss = document.createElement('button');
        dismiss.type = 'button';
        dismiss.className = 'guest-preview-dismiss';
        dismiss.setAttribute('aria-label', '닫기');
        dismiss.innerHTML = '<i class="fas fa-times"></i>';
        actions.append(login, close);

        const card = document.createElement('div');
        card.className = 'guest-preview-card';
        card.append(dismiss, media, copy, actions);
        box.appendChild(card);
        document.body.appendChild(box);
        const closePrompt = function (rememberDismissal = true) {
            if (rememberDismissal) rememberGuestPreviewPromptSeen(true);
            box.remove();
            document.body.classList.remove('guest-preview-ended');
        };
        close.addEventListener('click', function () { closePrompt(true); }, { once: true });
        dismiss.addEventListener('click', function () { closePrompt(true); }, { once: true });
        box.addEventListener('click', function (event) {
            if (event.target === box) closePrompt(true);
        });
        box.addEventListener('keydown', function (event) {
            if (event.key === 'Escape') closePrompt(true);
        });
        window.setTimeout(() => box.classList.add('show'), 20);
        login.focus({ preventScroll: true });
    }

    function updatePreviewBadge() {
        document.body.classList.toggle('guest-preview-mode', guestPreviewMode);
        let badge = document.getElementById('player-preview-badge');
        if (!guestPreviewMode) {
            if (badge) badge.remove();
            return;
        }
        if (!badge && playerArtist && playerArtist.parentElement) {
            badge = document.createElement('span');
            badge.id = 'player-preview-badge';
            badge.className = 'player-preview-badge';
            badge.textContent = `${guestPreviewSeconds}초 미리듣기`;
            playerArtist.parentElement.appendChild(badge);
        }
    }

    function finishExpandPlayerClose() {
        if (!expandPlayerOverlay) return;
        expandPlayerOverlay.style.display = 'none';
        expandPlayerOverlay.hidden = true;
        document.body.classList.remove('expand-player-open');
        expandPlayerOverlay.classList.remove('is-expand-open');
        expandPlayerOverlay.classList.remove('is-expand-opening');
        expandPlayerOverlay.classList.remove('is-expand-closing');
        expandPlayerOverlay.classList.remove('is-expand-motion-active');
        expandPlayerOverlay.classList.remove('is-lyrics-view');
        expandPlayerOverlay.classList.remove('is-queue-view');
        setExpandPanelCollapsed(false);
        saveStateToStorage();
    }

    function closeExpandPlayer(options = {}) {
        if (!expandPlayerOverlay) return;
        window.clearTimeout(expandSwipeState.closeTimer);
        expandSwipeState.closeTimer = null;
        window.clearTimeout(expandPlayerMotionTimer);
        expandPlayerMotionTimer = null;
        const immediate = options && options.immediate === true;
        if (immediate || prefersReducedPlayerMotion() || expandPlayerOverlay.style.display !== 'flex') {
            finishExpandPlayerClose();
            return;
        }
        expandPlayerOverlay.classList.remove('is-expand-opening');
        expandPlayerOverlay.classList.remove('is-expand-open');
        expandPlayerOverlay.classList.add('is-expand-closing');
        expandPlayerOverlay.classList.add('is-expand-motion-active');
        expandPlayerMotionTimer = window.setTimeout(function () {
            finishExpandPlayerClose();
        }, EXPAND_PLAYER_MOTION_MS);
    }

    function openExpandPlayer() {
        if (!expandPlayerOverlay) {
            showPlayerNotice('확장 플레이어를 열 수 없습니다. 기본 플레이어로 계속 재생합니다.', 'info');
            return;
        }
        window.clearTimeout(expandPlayerMotionTimer);
        expandPlayerMotionTimer = null;
        window.clearTimeout(expandSwipeState.closeTimer);
        expandSwipeState.closeTimer = null;
        resetExpandSwipeVisual();
        expandPlayerOverlay.hidden = false;
        expandPlayerOverlay.style.display = 'flex';
        expandPlayerOverlay.classList.remove('is-expand-closing');
        expandPlayerOverlay.classList.remove('is-expand-open');
        expandPlayerOverlay.classList.add('is-expand-opening');
        expandPlayerOverlay.classList.remove('is-expand-motion-active');
        document.body.classList.add('expand-player-open');
        ensureExpandTabCollapseButton();
        syncExpandTabMode();
        setExpandPanelCollapsed(true);
        renderQueueUI();

        updateProgressUi(audio.currentTime || playerState.currentTime || 0, getEffectiveDuration());
        if (prefersReducedPlayerMotion()) {
            expandPlayerOverlay.classList.remove('is-expand-opening');
            expandPlayerOverlay.classList.add('is-expand-open');
        } else {
            expandPlayerOverlay.offsetHeight;
            window.requestAnimationFrame(function () {
                if (!expandPlayerOverlay || expandPlayerOverlay.style.display !== 'flex') return;
                expandPlayerOverlay.classList.add('is-expand-motion-active');
                expandPlayerMotionTimer = window.setTimeout(function () {
                    if (!expandPlayerOverlay) return;
                    expandPlayerOverlay.classList.remove('is-expand-opening');
                    expandPlayerOverlay.classList.remove('is-expand-motion-active');
                    expandPlayerOverlay.classList.add('is-expand-open');
                }, EXPAND_PLAYER_MOTION_MS);
            });
        }
        saveStateToStorage();
    }

    function syncExpandTabMode(mode) {
        if (!expandPlayerOverlay) return;
        const lyricsVisible = mode ? mode === 'lyrics' : (
            tabBtnLyrics && tabBtnLyrics.classList.contains('active')
        );
        const queueVisible = mode ? mode === 'queue' : !lyricsVisible;
        expandPlayerOverlay.classList.toggle('is-lyrics-view', !!lyricsVisible);
        expandPlayerOverlay.classList.toggle('is-queue-view', !!queueVisible);
        if (expandPlayerOverlay.firstElementChild) {
            expandPlayerOverlay.firstElementChild.classList.toggle('is-lyrics-view', !!lyricsVisible);
            expandPlayerOverlay.firstElementChild.classList.toggle('is-queue-view', !!queueVisible);
        }
    }

    function ensureExpandTabCollapseButton() {
        if (expandTabCollapseButton || !tabBtnQueue || !tabBtnQueue.parentElement) return;
        expandTabCollapseButton = document.createElement('button');
        expandTabCollapseButton.type = 'button';
        expandTabCollapseButton.className = 'tab-header-btn expand-tab-collapse-btn';
        expandTabCollapseButton.setAttribute('aria-expanded', 'true');
        expandTabCollapseButton.setAttribute('aria-label', '대기열/가사 창 접기');
        expandTabCollapseButton.innerHTML = '<i class="fas fa-chevron-down" aria-hidden="true"></i><span>접기</span>';
        expandTabCollapseButton.addEventListener('click', function(event) {
            event.preventDefault();
            event.stopPropagation();
            toggleExpandPanelCollapsed();
        });
        tabBtnQueue.parentElement.appendChild(expandTabCollapseButton);
    }

    function toggleExpandPanelCollapsed() {
        const container = expandPlayerOverlay ? expandPlayerOverlay.firstElementChild : null;
        const collapsed = !!(container && container.classList.contains('is-tab-collapsed'));
        setExpandPanelCollapsed(!collapsed);
    }

    function setExpandTabContentDisplay(lyricsActive, collapsed) {
        if (collapsed) {
            if (expandTabQueueContent) expandTabQueueContent.style.display = 'none';
            if (expandTabLyricsContent) expandTabLyricsContent.style.display = 'none';
            return;
        }

        if (expandTabQueueContent) expandTabQueueContent.style.display = lyricsActive ? 'none' : 'flex';
        if (expandTabLyricsContent) expandTabLyricsContent.style.display = lyricsActive ? 'flex' : 'none';
    }

    function setExpandPanelCollapsed(collapsed) {
        if (!expandPlayerOverlay) return;
        const lyricsActive = tabBtnLyrics && tabBtnLyrics.classList.contains('active');
        syncExpandTabMode(lyricsActive ? 'lyrics' : 'queue');
        expandPlayerOverlay.classList.toggle('is-tab-collapsed', !!collapsed);
        if (expandPlayerOverlay.firstElementChild) {
            expandPlayerOverlay.firstElementChild.classList.toggle('is-tab-collapsed', !!collapsed);
        }
        setExpandTabContentDisplay(lyricsActive, !!collapsed);
        if (!collapsed) {
            syncExpandTabMode(lyricsActive ? 'lyrics' : 'queue');
        }
        if (expandTabCollapseButton) {
            expandTabCollapseButton.setAttribute('aria-expanded', collapsed ? 'false' : 'true');
            expandTabCollapseButton.setAttribute('aria-label', collapsed ? '대기열/가사 창 펼치기' : '대기열/가사 창 접기');
            expandTabCollapseButton.innerHTML = collapsed
                ? '<i class="fas fa-chevron-up" aria-hidden="true"></i><span>펼치기</span>'
                : '<i class="fas fa-chevron-down" aria-hidden="true"></i><span>접기</span>';
        }
    }

    function focusCurrentQueueItemInExpand(force = false) {
        if (!expandQueueList || !expandPlayerOverlay) return;
        if (expandPlayerOverlay.style.display !== 'flex') return;
        if (expandPlayerOverlay.classList.contains('is-lyrics-view')) return;
        if (expandPlayerOverlay.classList.contains('is-tab-collapsed')) return;
        const activeItem = expandQueueList.querySelector('.queue-item.is-playing, .queue-item.active');
        if (activeItem) {
            const listRect = expandQueueList.getBoundingClientRect();
            const itemRect = activeItem.getBoundingClientRect();
            const isVisible = itemRect.top >= listRect.top && itemRect.bottom <= listRect.bottom;
            if (!isVisible || force) {
                activeItem.scrollIntoView({ block: 'nearest', behavior: 'auto' });
            }
        }
    }

    function isMobilePlayerViewport() {
        return !!(window.matchMedia && window.matchMedia('(max-width: 1199px)').matches);
    }

    function findScrollableParentWithinExpand(target) {
        const container = expandPlayerOverlay ? expandPlayerOverlay.firstElementChild : null;
        let node = target;
        while (node && node !== container && node.nodeType === 1) {
            const style = window.getComputedStyle(node);
            const canScroll = /(auto|scroll)/.test(style.overflowY) && node.scrollHeight > node.clientHeight + 2;
            if (canScroll) return node;
            node = node.parentElement;
        }
        return null;
    }

    function resetExpandSwipeVisual() {
        const container = expandPlayerOverlay ? expandPlayerOverlay.firstElementChild : null;
        if (!container) return;
        container.style.transform = '';
        container.style.opacity = '';
        if (expandPlayerOverlay) expandPlayerOverlay.style.removeProperty('--swipe-progress');
        container.classList.remove('is-swipe-dragging');
        container.classList.remove('is-swipe-closing');
        if (expandPlayerOverlay) expandPlayerOverlay.classList.remove('is-swipe-active');
    }

    function startExpandSwipe(event) {
        if (!expandPlayerOverlay || !isMobilePlayerViewport()) return;
        if (expandPlayerOverlay.style.display !== 'flex') return;
        if (!event.touches || event.touches.length !== 1) return;
        const target = event.target;
        if (target && target.closest && target.closest('button, a, input, select, textarea, .progress-bar-wrapper, .synced-lyrics-line, .queue-item-move, .queue-item-remove')) {
            return;
        }
        const touch = event.touches[0];
        const container = expandPlayerOverlay.firstElementChild;
        const rect = container ? container.getBoundingClientRect() : null;
        const topDragLimit = Math.min(128, Math.max(82, (rect ? rect.height : window.innerHeight) * 0.16));
        const startedInTopDragZone = rect && touch.clientY >= rect.top && touch.clientY <= rect.top + topDragLimit;
        if (!startedInTopDragZone) return;
        window.clearTimeout(expandSwipeState.closeTimer);
        expandSwipeState.active = true;
        expandSwipeState.startX = touch.clientX;
        expandSwipeState.startY = touch.clientY;
        expandSwipeState.lastY = touch.clientY;
        expandSwipeState.lastTime = performance.now();
        expandSwipeState.velocity = 0;
        expandSwipeState.deltaY = 0;
        expandSwipeState.scrollable = findScrollableParentWithinExpand(target);
    }

    function moveExpandSwipe(event) {
        if (!expandSwipeState.active || !event.touches || event.touches.length !== 1) return;
        const touch = event.touches[0];
        const deltaX = touch.clientX - expandSwipeState.startX;
        const deltaY = touch.clientY - expandSwipeState.startY;
        if (deltaY <= 0 || Math.abs(deltaY) < Math.abs(deltaX) * 1.35) return;
        if (expandSwipeState.scrollable && expandSwipeState.scrollable.scrollTop > 2) return;

        expandSwipeState.deltaY = deltaY;
        const now = performance.now();
        const elapsed = Math.max(1, now - expandSwipeState.lastTime);
        expandSwipeState.velocity = (touch.clientY - expandSwipeState.lastY) / elapsed;
        expandSwipeState.lastY = touch.clientY;
        expandSwipeState.lastTime = now;
        const container = expandPlayerOverlay ? expandPlayerOverlay.firstElementChild : null;
        if (!container) return;
        event.preventDefault();
        const distance = Math.min(deltaY < 140 ? deltaY * 0.82 : 115 + Math.sqrt(deltaY - 140) * 5.6, 240);
        const progress = Math.min(distance / 220, 1);
        expandPlayerOverlay.style.setProperty('--swipe-progress', progress.toFixed(3));
        expandPlayerOverlay.classList.add('is-swipe-active');
        container.classList.add('is-swipe-dragging');
        container.classList.remove('is-swipe-closing');
        container.style.transform = `translate3d(0, ${distance}px, 0) scale(${Math.max(0.965, 1 - progress * 0.028)})`;
        container.style.opacity = String(Math.max(0.78, 1 - progress * 0.14));
    }

    function endExpandSwipe() {
        if (!expandSwipeState.active) return;
        const shouldClose = expandSwipeState.deltaY > 128 || (expandSwipeState.deltaY > 58 && expandSwipeState.velocity > 0.62);
        expandSwipeState.active = false;
        expandSwipeState.scrollable = null;
        if (shouldClose) {
            const container = expandPlayerOverlay ? expandPlayerOverlay.firstElementChild : null;
            if (!container) {
                closeExpandPlayer();
                return;
            }
            container.classList.remove('is-swipe-dragging');
            container.classList.add('is-swipe-closing');
            if (expandPlayerOverlay) {
                expandPlayerOverlay.classList.add('is-swipe-active');
                expandPlayerOverlay.style.setProperty('--swipe-progress', '1');
            }
            container.style.transform = 'translate3d(0, 104dvh, 0) scale(0.965)';
            container.style.opacity = '0';
            expandSwipeState.closeTimer = window.setTimeout(function() {
                closeExpandPlayer({ immediate: true });
                resetExpandSwipeVisual();
            }, 190);
        } else {
            resetExpandSwipeVisual();
        }
    }

    function navigateToLyricsEditorFromExpand(track = currentTrack()) {
        if (!track || !track.id) {
            showPlayerNotice('가사를 등록할 곡을 먼저 선택해 주세요.', 'info');
            return;
        }
        const url = new URL(`${window.APP_BASE_PATH}lyrics_edit.php?song_id=${encodeURIComponent(track.id)}`, window.location.href);
        saveStateToStorage();
        closeExpandPlayer();
        if (typeof window.MelonLiteNavigate === 'function') {
            window.MelonLiteNavigate(url, { push: true });
        } else {
            window.location.href = url.href;
        }
    }

    function describePlayError(error) {
        const name = error && error.name ? error.name : '';
        if (name === 'NotAllowedError') return '브라우저 정책 때문에 자동 재생이 막혔습니다. 재생 버튼을 눌러 이어 들어 주세요.';
        if (name === 'AbortError') return '재생 요청이 취소되었습니다. 다시 시도해 주세요.';
        if (name === 'NotSupportedError') return '이 브라우저에서 지원하지 않는 오디오 형식입니다.';
        return '재생을 시작하지 못했습니다. 네트워크 또는 파일 상태를 확인해 주세요.';
    }

    function describeStreamFailure(error, canAutoNext, count) {
        const name = error && error.name ? error.name : '';
        if (typeof navigator !== 'undefined' && navigator.onLine === false) {
            return '네트워크 연결이 없어 재생하지 못했습니다. 연결 후 다시 시도해 주세요.';
        }
        if (name === 'NotSupportedError') {
            return '지원하지 않는 오디오 형식입니다. 파일 형식이나 변환 상태를 확인해 주세요.';
        }
        if (count > 1) {
            return canAutoNext
                ? '같은 곡 재생에 계속 실패해 다음 곡으로 넘어갑니다.'
                : '이 곡을 재생할 수 없습니다. 파일 상태를 확인하거나 다른 곡을 선택해 주세요.';
        }
        return canAutoNext
            ? '이 곡을 재생할 수 없어 다음 곡으로 넘어갑니다.'
            : '재생 중 문제가 발생했습니다. 파일 상태를 확인하거나 다시 시도해 주세요.';
    }

    function requestPlay(options = {}) {
        const track = currentTrack();
        const requestSerial = ++playRequestSerial;
        const requestedTrackId = track ? String(track.id) : '';
        const isStaleRequest = function () {
            return requestSerial !== playRequestSerial
                || (requestedTrackId && String(playerState.currentSongId || '') !== requestedTrackId);
        };
        const promise = audio.play();
        if (!promise || typeof promise.then !== 'function') return Promise.resolve();
        return promise.then(() => {
            if (isStaleRequest()) return;
            isPlaying = true;
            clearPlayerTransitionState(180);
            if (track) streamFailureCounts.delete(String(track.id));
            updatePlayButtonUI(true);
            updateProgressUi(audio.currentTime || playerState.currentTime || 0);
            startProgressTicker();
            updateMediaSessionPlaybackState();
            if (track) synchronizeActiveListUI(track.id);
            renderQueueUI();
            saveStateToStorage();
        }).catch(error => {
            if (isStaleRequest()) {
                logPlayerIssue('Stale audio play request ignored', error);
                return Promise.reject(error);
            }
            const message = describePlayError(error);
            logPlayerIssue('Audio play failed', error);
            isPlaying = false;
            clearPlayerTransitionState(0);
            playerState.wasPlaying = options.keepResume !== false;
            updatePlayButtonUI(false);
            if (btnPlayPause) {
                btnPlayPause.classList.add('resume-ready');
                btnPlayPause.title = '재생 버튼을 눌러 이어 듣기';
            }
            updateMediaSessionPlaybackState();
            saveStateToStorage();
            if (options.autoNextOnFailure && error && error.name !== 'NotAllowedError' && error.name !== 'AbortError') {
                handleAudioStreamFailure(error);
            } else {
                showPlayerNotice(message, error && error.name === 'NotAllowedError' ? 'info' : 'error');
            }
            return Promise.reject(error);
        });
    }

    function applyPendingRestoreTime() {
        if (pendingRestoreTime === null) return;
        const restoreTime = clampResumeTime(pendingRestoreTime, getEffectiveDuration());
        pendingRestoreTime = null;
        if (!Number.isFinite(restoreTime) || restoreTime <= 0) return;
        try {
            audio.currentTime = restoreTime;
            playerState.currentTime = audio.currentTime;
            updateProgressUi(audio.currentTime);
        } catch (err) {
            logPlayerIssue('Deferred seek restore failed', err);
        }
    }

    function updateProgressUi(current = 0, duration = null) {
        const total = getEffectiveDuration(duration);
        const now = Math.max(0, Number(current) || 0);
        const pct = total > 0 ? Math.max(0, Math.min(100, (now / total) * 100)) : 0;
        const progressGradient = `linear-gradient(90deg, var(--accent-color, #00c73c) 0%, var(--accent-color, #00c73c) ${pct}%, var(--player-progress-bg, rgba(148, 163, 184, 0.28)) ${pct}%, var(--player-progress-bg, rgba(148, 163, 184, 0.28)) 100%)`;
        if (progressWrapper) {
            progressWrapper.style.setProperty('--player-progress-width', `${pct}%`);
            progressWrapper.style.setProperty('background', progressGradient, 'important');
        }
        if (progressBar) {
            progressBar.style.setProperty('--player-progress-width', `${pct}%`);
            progressBar.style.setProperty('width', `${pct}%`, 'important');
        }
        if (expandProgressWrapper) {
            expandProgressWrapper.style.setProperty('--player-progress-width', `${pct}%`);
            expandProgressWrapper.style.setProperty('background', progressGradient, 'important');
        }
        if (expandProgressBar) {
            expandProgressBar.style.setProperty('--player-progress-width', `${pct}%`);
            expandProgressBar.style.setProperty('width', `${pct}%`, 'important');
        }
        if (timeCurrent) timeCurrent.textContent = formatTime(now);
        if (expandTimeCurrent) expandTimeCurrent.textContent = formatTime(now);
        if (total > 0) {
            if (timeTotal) timeTotal.textContent = formatTime(total);
            if (expandTimeTotal) expandTimeTotal.textContent = formatTime(total);
        }
    }

    function stopProgressTicker() {
        if (!progressFrameId) return;
        if (typeof window.cancelAnimationFrame === 'function') {
            window.cancelAnimationFrame(progressFrameId);
        } else {
            window.clearTimeout(progressFrameId);
        }
        progressFrameId = null;
    }

    function startProgressTicker() {
        if (progressFrameId) return;
        const scheduleProgressTick = typeof window.requestAnimationFrame === 'function'
            ? window.requestAnimationFrame.bind(window)
            : function(callback) { return window.setTimeout(callback, 250); };
        const tick = function() {
            if (!audio || audio.paused || audio.ended) {
                progressFrameId = null;
                return;
            }
            playerState.currentTime = Number.isFinite(audio.currentTime) ? audio.currentTime : playerState.currentTime;
            updateProgressUi(playerState.currentTime);
            progressFrameId = scheduleProgressTick(tick);
        };
        progressFrameId = scheduleProgressTick(tick);
    }

    function handleAudioStreamFailure(error) {
        const track = currentTrack();
        const trackId = track ? String(track.id) : 'unknown';
        const count = (streamFailureCounts.get(trackId) || 0) + 1;
        streamFailureCounts.set(trackId, count);
        isPlaying = false;
        updatePlayButtonUI(false);
        updateMediaSessionPlaybackState();
        const canAutoNext = playerState.queue.length > 1 && count <= 2 && appPlayerSettings.autoNextEnabled;
        showPlayerNotice(describeStreamFailure(error, canAutoNext, count), 'error');
        reportPlaybackFailure(track, error);
        if (canAutoNext) {
            window.setTimeout(() => playNext({ reason: 'failure', skipFailed: true }), 700);
        }
        saveStateToStorage();
    }

    function audioErrorClientCode(error) {
        if (!error) return '';
        if (error.code === 1) return 'MEDIA_ERR_ABORTED';
        if (error.code === 2) return 'MEDIA_ERR_NETWORK';
        if (error.code === 3) return 'MEDIA_ERR_DECODE';
        if (error.code === 4) return 'MEDIA_ERR_SRC_NOT_SUPPORTED';
        return error.name || String(error.code || '');
    }

    function playbackFailureTypeFromAudioError(error) {
        if (typeof navigator !== 'undefined' && navigator.onLine === false) return 'network';
        if (error && (error.name === 'NotSupportedError' || error.code === 4)) return 'unsupported';
        return 'client_error';
    }

    function reportPlaybackFailure(track, error) {
        if (!track || !track.id) return;
        const songId = String(track.id);
        const type = playbackFailureTypeFromAudioError(error);
        const key = `${songId}:${type}`;
        const now = Date.now();
        const last = playbackFailureReportTimes.get(key) || 0;
        if (now - last < 60000) return;
        playbackFailureReportTimes.set(key, now);

        try {
            const formData = new FormData();
            formData.set('csrf_token', window.APP_CSRF_TOKEN || '');
            formData.set('song_id', songId);
            formData.set('error_type', type);
            formData.set('client_code', audioErrorClientCode(error));
            formData.set('context', error && error.message ? safePlayerIssue(error) : 'audio error');
            fetch(`${window.APP_BASE_PATH || './'}playback_failure_report.php`, {
                method: 'POST',
                body: formData,
                credentials: 'same-origin',
                cache: 'no-store',
                headers: {
                    'X-Requested-With': 'XMLHttpRequest',
                    'Accept': 'application/json'
                }
            }).catch(function (err) {
                logPlayerIssue('Playback failure report failed', err);
            });
        } catch (err) {
            logPlayerIssue('Playback failure report setup failed', err);
        }
    }

    function normalizeTrack(track) {
        if (!track || !track.id) return null;
        const id = String(track.id);
        const coverVersion = safeText(track.cover_version || track.coverVersion || '', '');
        return {
            id,
            title: safeText(track.title, TEXT_FALLBACKS.title),
            artist: safeText(track.artist, TEXT_FALLBACKS.artist),
            album: safeText(track.album, TEXT_FALLBACKS.album),
            duration: parseDurationValue(track.duration),
            liked: coerceBoolean(track.liked),
            hasLyrics: track.hasLyrics === true || track.has_lyrics === true || track.has_lyrics === '1',
            cover_version: coverVersion,
            cover_url: normalizeCoverUrl(track.cover_url || track.coverUrl || buildCoverUrl(id, coverVersion)),
            stream_url: `stream.php?id=${encodeURIComponent(id)}`
        };
    }

    function sanitizeQueue(queue) {
        if (!Array.isArray(queue)) return [];
        const seen = new Set();
        return queue.slice(0, 300).map(normalizeTrack).filter(track => {
            if (!track || seen.has(track.id)) return false;
            seen.add(track.id);
            return true;
        });
    }

    function clampResumeTime(value, duration = 0) {
        let time = Number.parseFloat(value);
        if (!Number.isFinite(time) || time < 0) time = 0;
        const safeDuration = parseDurationValue(duration);
        if (safeDuration > 0) time = Math.min(time, Math.max(0, safeDuration - 1));
        if (guestPreviewMode) time = Math.min(time, Math.max(0, guestPreviewSeconds - 1));
        return Math.max(0, Math.round(time));
    }

    function resetStoredPlayerState() {
        playerState.queue = [];
        playerState.originalQueue = [];
        playerState.currentIndex = -1;
        playerState.currentSongId = null;
        playerState.currentTime = 0;
        playerState.wasPlaying = false;
        try { localStorage.removeItem('melon_lite_state'); } catch (err) {}
    }

    // 嚥≪뮇類???쎈꽅?귐??癒?퐣 ?怨밴묶揶?癰귣벊????뺣즲
    function loadStateFromStorage() {
        if (!appPlayerSettings.resumeEnabled) {
            try {
                localStorage.removeItem('melon_lite_state');
            } catch (err) {}
            return;
        }
        try {
            const savedState = localStorage.getItem('melon_lite_state');
            if (savedState) {
                const parsed = JSON.parse(savedState);
                if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
                    resetStoredPlayerState();
                    return;
                }
                const lastUpdated = parseInt(parsed.lastUpdated, 10) || 0;
                if (lastUpdated > 0 && Date.now() - lastUpdated > 1000 * 60 * 60 * 24 * 14) {
                    resetStoredPlayerState();
                    return;
                }
                const queue = sanitizeQueue(parsed.queue || []);
                const originalQueue = sanitizeQueue(parsed.originalQueue || parsed.queue || []);
                if (queue.length === 0) {
                    resetStoredPlayerState();
                    return;
                }
                let currentIndex = Number.parseInt(parsed.currentIndex, 10);
                if (!Number.isFinite(currentIndex)) currentIndex = -1;
                const currentSongId = parsed.currentSongId ? String(parsed.currentSongId) : null;
                if (currentSongId) {
                    const foundIndex = queue.findIndex(track => track.id === currentSongId);
                    currentIndex = foundIndex >= 0 ? foundIndex : -1;
                }
                if (currentIndex < 0 || currentIndex >= queue.length) currentIndex = 0;
                const currentTrackForRestore = queue[currentIndex] || null;
                playerState.queue = queue;
                playerState.originalQueue = originalQueue.length ? originalQueue : queue;
                playerState.currentIndex = currentIndex;
                playerState.currentSongId = currentTrackForRestore ? currentTrackForRestore.id : null;
                playerState.shuffle = parsed.shuffle === true;
                playerState.repeatMode = ['none', 'all', 'one'].includes(parsed.repeatMode) ? parsed.repeatMode : 'none';
                playerState.currentTime = clampResumeTime(parsed.currentTime, currentTrackForRestore ? currentTrackForRestore.duration : parsed.currentDuration);
                const savedVolume = parseFloat(parsed.volume);
                playerState.volume = Number.isFinite(savedVolume) ? Math.min(1, Math.max(0, savedVolume)) : playerState.volume;
                playerState.muted = parsed.muted === true;
                playerState.queueContext = parsed.queueContext || null;
                playerState.expanded = parsed.expanded === true;
                playerState.wasPlaying = parsed.wasPlaying === true;
                playerState.userActivated = parsed.userActivated === true;
                playerState.lastUpdated = lastUpdated;
            }
        } catch (err) {
            logPlayerIssue('LocalStorage restore failed; player state was reset', err);
            resetStoredPlayerState();
        }
    }

    // ?怨밴묶揶쏅???嚥≪뮇類???쎈꽅?귐????紐꾩뵠??    function saveStateToStorage() {
    function saveStateToStorage() {
        try {
            if (audio && Number.isFinite(audio.currentTime)) {
                playerState.currentTime = clampResumeTime(audio.currentTime, getEffectiveDuration());
            }
            playerState.queue = sanitizeQueue(playerState.queue);
            playerState.originalQueue = sanitizeQueue(playerState.originalQueue.length ? playerState.originalQueue : playerState.queue);
            if (playerState.currentIndex < 0 || playerState.currentIndex >= playerState.queue.length) {
                const foundIndex = playerState.currentSongId
                    ? playerState.queue.findIndex(track => track.id === String(playerState.currentSongId))
                    : -1;
                playerState.currentIndex = foundIndex >= 0 ? foundIndex : (playerState.queue.length ? 0 : -1);
            }
            const track = currentTrack();
            playerState.currentSongId = track ? track.id : null;
            playerState.wasPlaying = isPlaying || !audio.paused;
            playerState.muted = audio.muted === true;
            playerState.lastUpdated = Date.now();
            localStorage.setItem('melon_lite_state', JSON.stringify({
                queue: playerState.queue,
                originalQueue: playerState.originalQueue,
                currentIndex: playerState.currentIndex,
                currentSongId: playerState.currentSongId,
                currentTitle: track ? safeText(track.title, TEXT_FALLBACKS.title) : '',
                currentArtist: track ? safeText(track.artist, TEXT_FALLBACKS.artist) : '',
                currentAlbum: track ? safeText(track.album, '') : '',
                currentDuration: getEffectiveDuration(),
                shuffle: playerState.shuffle,
                repeatMode: playerState.repeatMode,
                currentTime: playerState.currentTime,
                volume: playerState.volume,
                muted: playerState.muted,
                queueContext: playerState.queueContext,
                expanded: expandPlayerOverlay && expandPlayerOverlay.style.display === 'flex',
                wasPlaying: playerState.wasPlaying,
                userActivated: playerState.userActivated,
                previewMode: guestPreviewMode,
                lastUpdated: playerState.lastUpdated
            }));
            updateHomeResumeCard();
        } catch (err) {
            logPlayerIssue('LocalStorage save failed', err);
        }
    }

    function currentTrack() {
        if (playerState.currentIndex < 0 || playerState.currentIndex >= playerState.queue.length) return null;
        return playerState.queue[playerState.currentIndex] || null;
    }

    function buildCoverUrl(id, version = '') {
        const cleanId = encodeURIComponent(String(id || ''));
        const url = `cover.php?id=${cleanId}`;
        const fallbackVersion = version || window.APP_COVER_REVISION || '';
        return fallbackVersion ? `${url}&v=${encodeURIComponent(String(fallbackVersion))}` : url;
    }

    function normalizeCoverUrl(url) {
        const text = safeText(url, '');
        if (!text) return '';
        if (/^[a-z]:[\\/]/i.test(text) || text.includes('..')) return '';
        if (/^https?:\/\//i.test(text)) {
            try {
                const parsed = new URL(text);
                if (parsed.origin !== window.location.origin) return '';
                return parsed.href;
            } catch (err) {
                return '';
            }
        }
        return text;
    }

    function getTrackCoverUrl(track, absolute = false) {
        const basePath = window.APP_BASE_PATH || './';
        let raw = normalizeCoverUrl(track && (track.cover_url || track.coverUrl)) || buildCoverUrl(track && track.id, track && (track.cover_version || track.coverVersion));
        if (/cover\.php\?id=/i.test(raw) && raw.indexOf('v=') === -1 && (track && track.id)) {
            raw = buildCoverUrl(track.id, track.cover_version || track.coverVersion || '');
        }
        const url = /^https?:\/\//i.test(raw) || raw.startsWith('/') ? raw : `${basePath}${raw.replace(/^\.?\//, '')}`;
        return absolute ? new URL(url, window.location.href).href : url;
    }

    function updateMediaSessionMetadata(track) {
        if (!appPlayerSettings.mediaSessionEnabled || !('mediaSession' in navigator) || !window.MediaMetadata || !track) return;
        const coverUrl = getTrackCoverUrl(track, true);
        try {
            navigator.mediaSession.metadata = new MediaMetadata({
                title: safeText(track.title, TEXT_FALLBACKS.title),
                artist: safeText(track.artist, TEXT_FALLBACKS.artist),
                album: safeText(track.album, ''),
                artwork: [
                    { src: coverUrl, sizes: '96x96', type: 'image/png' },
                    { src: coverUrl, sizes: '192x192', type: 'image/png' },
                    { src: coverUrl, sizes: '512x512', type: 'image/png' }
                ]
            });
        } catch (err) {
            logPlayerIssue('Media Session metadata skipped', err);
        }
    }

    function updateMediaSessionPlaybackState() {
        if (!appPlayerSettings.mediaSessionEnabled || !('mediaSession' in navigator)) return;
        try {
            navigator.mediaSession.playbackState = isPlaying ? 'playing' : 'paused';
            if ('setPositionState' in navigator.mediaSession && Number.isFinite(audio.duration) && audio.duration > 0) {
                navigator.mediaSession.setPositionState({
                    duration: audio.duration,
                    playbackRate: audio.playbackRate || 1,
                    position: Math.min(audio.currentTime || 0, audio.duration)
                });
            }
        } catch (err) {}
    }

    function setupMediaSessionHandlers() {
        if (!appPlayerSettings.mediaSessionEnabled || !('mediaSession' in navigator)) return;
        try {
            navigator.mediaSession.setActionHandler('play', () => togglePlay());
            navigator.mediaSession.setActionHandler('pause', () => pauseTrack());
            navigator.mediaSession.setActionHandler('previoustrack', () => playPrev());
            navigator.mediaSession.setActionHandler('nexttrack', () => playNext());
            navigator.mediaSession.setActionHandler('seekbackward', (details) => {
                audio.currentTime = Math.max(0, audio.currentTime - (details.seekOffset || 5));
                playerState.currentTime = audio.currentTime;
                updateMediaSessionPlaybackState();
                saveStateToStorage();
            });
            navigator.mediaSession.setActionHandler('seekforward', (details) => {
                const max = Number.isFinite(audio.duration) ? audio.duration : audio.currentTime + 5;
                const target = Math.min(max, audio.currentTime + (details.seekOffset || 5));
                if (guestPreviewMode && target > guestPreviewSeconds) {
                    audio.currentTime = Math.min(guestPreviewSeconds, max);
                    showGuestPreviewPrompt();
                    return;
                }
                audio.currentTime = target;
                playerState.currentTime = audio.currentTime;
                updateMediaSessionPlaybackState();
                saveStateToStorage();
            });
            navigator.mediaSession.setActionHandler('seekto', (details) => {
                const target = Number(details.seekTime) || 0;
                if (guestPreviewMode && target > guestPreviewSeconds) {
                    audio.currentTime = Math.min(guestPreviewSeconds, Number.isFinite(audio.duration) ? audio.duration : guestPreviewSeconds);
                    showGuestPreviewPrompt();
                    return;
                }
                if (details.fastSeek && 'fastSeek' in audio) audio.fastSeek(target);
                else audio.currentTime = target;
                playerState.currentTime = audio.currentTime;
                updateMediaSessionPlaybackState();
                saveStateToStorage();
            });
        } catch (err) {
            logPlayerIssue('Media Session handlers skipped', err);
        }
    }

    // ==========================================================================
    // 3. Initialize & Set Player UI state
    // ==========================================================================
    function initPlayerUI() {
        if (appPlayerSettings.mobileCompact) {
            document.body.classList.add('player-mobile-compact');
        }
        setupMediaSessionHandlers();
        // 癰귥눖????쇱젟
        audio.volume = playerState.volume;
        audio.muted = playerState.muted === true;
        if (volumeBar) {
            volumeBar.style.width = `${audio.muted ? 0 : playerState.volume * 100}%`;
        }
        updateVolumeIcon(playerState.volume);

        // ?酉逾?獄?獄쏆꼶??筌뤴뫀諭?UI ??쇱젟
        updateShuffleUI();
        updateRepeatUI();

        // ??源???疫꿸퀣肉?癰귣벀??獄???嚥≪뮆諭?
        if (playerState.queue.length > 0 && playerState.currentIndex >= 0 && playerState.currentIndex < playerState.queue.length) {
            const track = playerState.queue[playerState.currentIndex];
            loadTrackDataOnly(track, playerState.currentTime);
            if (playerState.wasPlaying && btnPlayPause) {
                btnPlayPause.classList.add('resume-ready');
                btnPlayPause.title = '이어 듣기';
            }
        }
        
        renderQueueUI();
        bindHomeResumeCard();
        updateMediaSessionPlaybackState();
    }

    function loadTrackDataOnly(track, restoreTime = 0) {
        playerState.currentSongId = track.id;

        setElementText(playerTitle, track.title, TEXT_FALLBACKS.title);
        setElementText(playerArtist, track.artist, TEXT_FALLBACKS.artist);

        setElementText(expandPlayerTitle, track.title, TEXT_FALLBACKS.title);
        setElementText(expandPlayerAlbum, track.album, '');
        setElementText(expandPlayerArtist, track.artist, TEXT_FALLBACKS.artist);

        const coverUrl = getTrackCoverUrl(track);
        if (playerAlbumImage) playerAlbumImage.src = coverUrl;
        if (expandAlbumImage) expandAlbumImage.src = coverUrl;

        audio.src = `${window.APP_BASE_PATH}stream.php?id=${track.id}`;
        audio.load();

        const safeRestoreTime = clampResumeTime(restoreTime, track.duration);
        pendingRestoreTime = safeRestoreTime > 0 ? safeRestoreTime : null;
        if (timeCurrent) timeCurrent.textContent = formatTime(safeRestoreTime);
        if (expandTimeCurrent) expandTimeCurrent.textContent = formatTime(safeRestoreTime);
        updatePreviewBadge();

        if (expandBtnSongDetail) {
            expandBtnSongDetail.href = `${window.APP_BASE_PATH}song.php?id=${track.id}`;
        }

        setLyricsControlsState(track.hasLyrics ? true : null);
        loadLyrics(track.id);

        updateLikeButtonUI(track.id);
        updateMediaSessionMetadata(track);
        updateMediaSessionPlaybackState();
        
        synchronizeActiveListUI(track.id);
        saveStateToStorage();
    }

    // ==========================================================================
    // 4. Play / Pause & Queue Control
    // ==========================================================================
    
    // ?諭??????源???쎈뻬
    function playTrack(index, options = {}) {
        if (playerState.queue.length === 0 || index < 0 || index >= playerState.queue.length) return;
        playerState.userActivated = true;
        
        const track = playerState.queue[index];
        const isNewTrack = (playerState.currentIndex !== index || playerState.currentSongId !== track.id);
        
        playerState.currentIndex = index;
        playerState.currentSongId = track.id;
        
        if (isNewTrack) {
            if (!isTrackTransitioning || options.transition !== true) {
                setPlayerTransitionState(true, track, `곡을 준비하는 중 · ${safeText(track.title, TEXT_FALLBACKS.title)}`);
            }
            // ??삳탵?????뮞 ??쇱젟 獄?筌앸맩????源?
            audio.src = `${window.APP_BASE_PATH}stream.php?id=${track.id}`;
            audio.load();
            playerState.currentTime = 0;
            pendingRestoreTime = null;
            updateProgressUi(0, parseDurationValue(track.duration));
            
            // UI ?袁⑷퍥 揶쏄퉮??
            setElementText(playerTitle, track.title, TEXT_FALLBACKS.title);
            setElementText(playerArtist, track.artist, TEXT_FALLBACKS.artist);
            setElementText(expandPlayerTitle, track.title, TEXT_FALLBACKS.title);
            setElementText(expandPlayerAlbum, track.album, '');
            setElementText(expandPlayerArtist, track.artist, TEXT_FALLBACKS.artist);
            
            const coverUrl = getTrackCoverUrl(track);
            if (playerAlbumImage) playerAlbumImage.src = coverUrl;
            if (expandAlbumImage) expandAlbumImage.src = coverUrl;
            
            if (expandBtnSongDetail) {
                expandBtnSongDetail.href = `${window.APP_BASE_PATH}song.php?id=${track.id}`;
            }
            updatePreviewBadge();
            setLyricsControlsState(track.hasLyrics ? true : null);

            loadLyrics(track.id);
            updateLikeButtonUI(track.id);
            updateMediaSessionMetadata(track);
            if (expandPlayerOverlay && expandPlayerOverlay.style.display === 'flex') {
                syncExpandTabMode();
            }
            renderQueueUI();
            synchronizeActiveListUI(track.id);
            updateMediaSessionPlaybackState();
            saveStateToStorage();
            
            // ??源?疫꿸퀡以?燁삳똻???2筌?API ?紐꾪뀱)
            if (!guestPreviewMode && appPlayerSettings.countRule === 'start') {
                maybeIncrementPlayCount(track.id);
            }
        }
        
        if (options.userNotice === true) {
            showPlayerNotice('\uC74C\uC545\uC744 \uC7AC\uC0DD\uD569\uB2C8\uB2E4.', 'info');
        }
        requestPlay({ autoNextOnFailure: true, keepResume: true }).catch(() => {});
    }

    function pauseTrack() {
        playRequestSerial += 1;
        audio.pause();
        isPlaying = false;
        updatePlayButtonUI(false);
        updateMediaSessionPlaybackState();
        playerState.currentTime = audio.currentTime;
        saveStateToStorage();
    }

    function togglePlay() {
        if (playerState.queue.length === 0) {
            // ??源???疫꿸퀣肉????쑴堉??덈뮉 野껋럩????륁뵠筌왖 ???ⓦ볥굶??疫꿸낯堉?? ??疫꿸퀣肉??類ㅺ쉐 ??뺣즲
            const collected = collectPageSongs();
            if (collected.length > 0) {
                setQueueAndPlay(collected, 0, false, { userNotice: true });
            }
            return;
        }
        
        if (playerState.currentIndex === -1) {
            playTrack(0, { userNotice: true });
        } else {
            if (isPlaying) {
                pauseTrack();
            } else {
                playerState.userActivated = true;
                showPlayerNotice('\uC74C\uC545\uC744 \uC7AC\uC0DD\uD569\uB2C8\uB2E4.', 'info');
                requestPlay({ keepResume: true }).catch(() => {});
            }
        }
    }

    function ensureNavigableQueue() {
        if (
            playerState.queue.length > 1 &&
            playerState.currentIndex >= 0 &&
            playerState.currentIndex < playerState.queue.length
        ) {
            return true;
        }

        const activeTrack = currentTrack();
        const activeId = playerState.currentSongId || (activeTrack && activeTrack.id);
        const pageTracks = collectPageSongs();

        if (!Array.isArray(pageTracks) || pageTracks.length <= 1) {
            return playerState.queue.length > 1;
        }

        let activeIndex = activeId ? pageTracks.findIndex(track => String(track.id) === String(activeId)) : -1;
        if (activeIndex < 0) {
            activeIndex = 0;
        }

        playerState.originalQueue = [...pageTracks];
        playerState.queue = [...pageTracks];
        playerState.currentIndex = activeIndex;
        playerState.currentSongId = pageTracks[activeIndex] ? pageTracks[activeIndex].id : activeId;
        renderQueueUI();
        saveStateToStorage();
        return true;
    }

    // ??쇱벉 ????猷??怨쀪텦
    function findNextPlayableIndex(direction = 1, options = {}) {
        const hadSingleQueue = playerState.queue.length === 1 && playerState.currentIndex === 0;
        if (!ensureNavigableQueue()) {
            return hadSingleQueue && playerState.repeatMode === 'all' ? 0 : undefined;
        }

        const queueLength = playerState.queue.length;
        if (queueLength <= 0) return -1;
        if (queueLength === 1) {
            return playerState.repeatMode === 'all' ? 0 : -1;
        }
        const step = direction >= 0 ? 1 : -1;
        let candidate = playerState.currentIndex + step;

        if (candidate >= queueLength || candidate < 0) {
            if (playerState.repeatMode !== 'all') {
                return -1;
            }
            candidate = step > 0 ? 0 : queueLength - 1;
        }

        if (options.skipFailed === true && queueLength > 1) {
            let checked = 0;
            while (checked < queueLength) {
                const track = playerState.queue[candidate];
                const failed = track && streamFailureCounts.get(String(track.id)) > 0;
                if (!failed) break;
                candidate += step;
                if (candidate >= queueLength) candidate = 0;
                if (candidate < 0) candidate = queueLength - 1;
                checked += 1;
            }
        }

        return candidate;
    }

    function handleQueueEnd(direction = 'next') {
        stopProgressTicker();
        playRequestSerial += 1;
        audio.pause();
        try { audio.currentTime = 0; } catch (err) {}
        isPlaying = false;
        playerState.currentTime = 0;
        updateProgressUi(0, getEffectiveDuration());
        updatePlayButtonUI(false);
        updateMediaSessionPlaybackState();
        saveStateToStorage();
        const message = direction === 'prev'
            ? '이전 곡이 없습니다. 반복 재생을 켜면 처음과 끝을 이어 들을 수 있습니다.'
            : '대기열이 끝났습니다. 자동 믹스를 만들거나 반복 재생을 켜서 계속 들을 수 있습니다.';
        showPlayerNotice(message, 'info');
    }

    function playNext(options = {}) {
        const nextIndex = findNextPlayableIndex(1, options);
        if (nextIndex === undefined) return;
        if (nextIndex < 0) {
            handleQueueEnd('next');
            return;
        }
        const nextTrack = playerState.queue[nextIndex];
        if (nextTrack) {
            setPlayerTransitionState(true, nextTrack, options.reason === 'failure'
                ? '재생할 수 없는 곡을 건너뛰고 있습니다.'
                : `다음 곡 준비 중 · ${safeText(nextTrack.title, TEXT_FALLBACKS.title)}`);
        }
        if (nextIndex === playerState.currentIndex) {
            try { audio.currentTime = 0; } catch (err) {}
            playerState.currentTime = 0;
            updateProgressUi(0, getEffectiveDuration());
            requestPlay({ keepResume: true }).catch(() => {});
            return;
        }
        playTrack(nextIndex, { transition: true });
    }

    // ??곸읈 ????猷??怨쀪텦 (3????곴맒 ??源?빳臾믪뵠??됱몵筌???筌ｌ꼷???袁⑺뒄嚥? ?袁⑤빍筌?筌욊쑴彛???곸읈?ⓥ돦?앮에?
    function playPrev(options = {}) {
        const prevIndex = findNextPlayableIndex(-1, options);
        if (prevIndex === undefined) return;
        if (prevIndex < 0) {
            handleQueueEnd('prev');
            return;
        }
        const prevTrack = playerState.queue[prevIndex];
        if (prevTrack) {
            setPlayerTransitionState(true, prevTrack, `이전 곡 준비 중 · ${safeText(prevTrack.title, TEXT_FALLBACKS.title)}`);
        }
        if (prevIndex === playerState.currentIndex) {
            try { audio.currentTime = 0; } catch (err) {}
            playerState.currentTime = 0;
            updateProgressUi(0, getEffectiveDuration());
            requestPlay({ keepResume: true }).catch(() => {});
            return;
        }
        playTrack(prevIndex, { transition: true });
    }

    function resetExpandAlbumSwipeVisual() {
        window.clearTimeout(expandArtSwipeState.timer);
        expandArtSwipeState.timer = null;
        if (!expandAlbumImage) return;
        expandAlbumImage.style.transition = '';
        expandAlbumImage.style.transform = '';
        expandAlbumImage.style.opacity = '';
        expandAlbumImage.classList.remove('is-art-dragging');
        expandAlbumImage.classList.remove('is-art-swipe-out-left');
        expandAlbumImage.classList.remove('is-art-swipe-out-right');
        expandAlbumImage.classList.remove('is-art-swipe-in-left');
        expandAlbumImage.classList.remove('is-art-swipe-in-right');
        expandAlbumImage.classList.remove('is-art-swipe-settling');
    }

    function canSwipeExpandedAlbumArt() {
        return !!(
            expandPlayerOverlay &&
            expandPlayerOverlay.style.display === 'flex' &&
            expandAlbumImage &&
            playerState.queue.length > 1
        );
    }

    function canNavigateExpandedAlbumArt(direction) {
        if (!canSwipeExpandedAlbumArt()) return false;
        if (!ensureNavigableQueue()) return false;
        if (direction === 'next') {
            return playerState.currentIndex < playerState.queue.length - 1 || playerState.repeatMode === 'all';
        }
        return playerState.currentIndex > 0 || playerState.repeatMode === 'all';
    }

    function startExpandAlbumSwipe(event) {
        if (!canSwipeExpandedAlbumArt()) return;
        if (!event.touches || event.touches.length !== 1) return;
        const target = event.target;
        if (target && target.closest && target.closest('button, a, input, select, textarea, .progress-bar-wrapper')) return;
        const touch = event.touches[0];
        window.clearTimeout(expandArtSwipeState.timer);
        expandArtSwipeState.active = false;
        expandArtSwipeState.tracking = true;
        expandArtSwipeState.startX = touch.clientX;
        expandArtSwipeState.startY = touch.clientY;
        expandArtSwipeState.lastX = touch.clientX;
        expandArtSwipeState.lastTime = performance.now();
        expandArtSwipeState.velocity = 0;
        expandArtSwipeState.deltaX = 0;
    }

    function moveExpandAlbumSwipe(event) {
        if (!expandArtSwipeState.tracking || !event.touches || event.touches.length !== 1) return;
        const touch = event.touches[0];
        const deltaX = touch.clientX - expandArtSwipeState.startX;
        const deltaY = touch.clientY - expandArtSwipeState.startY;
        if (!expandArtSwipeState.active) {
            if (Math.abs(deltaY) > 22 && Math.abs(deltaY) > Math.abs(deltaX) * 1.05) {
                expandArtSwipeState.tracking = false;
                return;
            }
            if (Math.abs(deltaX) < 12 || Math.abs(deltaX) < Math.abs(deltaY) * 1.15) return;
            expandArtSwipeState.active = true;
            if (expandAlbumImage) expandAlbumImage.classList.add('is-art-dragging');
        }
        if (!expandAlbumImage) return;
        event.preventDefault();
        expandArtSwipeState.deltaX = deltaX;
        const now = performance.now();
        const elapsed = Math.max(1, now - expandArtSwipeState.lastTime);
        expandArtSwipeState.velocity = (touch.clientX - expandArtSwipeState.lastX) / elapsed;
        expandArtSwipeState.lastX = touch.clientX;
        expandArtSwipeState.lastTime = now;
        const distance = Math.max(-180, Math.min(180, deltaX * 0.56));
        const progress = Math.min(Math.abs(distance) / 180, 1);
        const rotate = Math.max(-4.5, Math.min(4.5, distance / 38));
        expandAlbumImage.style.transform = `translate3d(${distance}px, 0, 0) rotate(${rotate}deg) scale(${Math.max(0.965, 1 - progress * 0.025)})`;
        expandAlbumImage.style.opacity = String(Math.max(0.78, 1 - progress * 0.14));
    }

    function animateExpandAlbumTrackChange(direction) {
        if (!expandAlbumImage) return;
        if (!canNavigateExpandedAlbumArt(direction)) {
            resetExpandAlbumSwipeVisual();
            showPlayerNotice(direction === 'next' ? '다음 곡이 없습니다.' : '이전 곡이 없습니다.', 'info');
            return;
        }
        if (prefersReducedPlayerMotion()) {
            if (direction === 'next') playNext();
            else playPrev();
            resetExpandAlbumSwipeVisual();
            return;
        }

        expandAlbumImage.classList.remove('is-art-dragging');
        const outX = direction === 'next' ? '-24%' : '24%';
        const outRotate = direction === 'next' ? '-3deg' : '3deg';
        const inX = direction === 'next' ? '16%' : '-16%';
        const inRotate = direction === 'next' ? '2deg' : '-2deg';
        expandAlbumImage.style.transition = 'transform 150ms cubic-bezier(0.32, 0, 0.67, 0), opacity 140ms ease';
        expandAlbumImage.style.transform = `translate3d(${outX}, 0, 0) rotate(${outRotate}) scale(0.982)`;
        expandAlbumImage.style.opacity = '0.82';
        expandAlbumImage.offsetHeight;

        expandArtSwipeState.timer = window.setTimeout(function () {
            if (direction === 'next') playNext();
            else playPrev();
            if (!expandAlbumImage) return;
            expandAlbumImage.style.transition = 'none';
            expandAlbumImage.style.transform = `translate3d(${inX}, 0, 0) rotate(${inRotate}) scale(0.985)`;
            expandAlbumImage.style.opacity = '0.72';
            expandAlbumImage.offsetHeight;
            window.requestAnimationFrame(function () {
                if (!expandAlbumImage) return;
                expandAlbumImage.style.transition = 'transform 240ms cubic-bezier(0.22, 1, 0.36, 1), opacity 220ms ease';
                expandAlbumImage.style.transform = 'translate3d(0, 0, 0) rotate(0deg) scale(1)';
                expandAlbumImage.style.opacity = '1';
                expandArtSwipeState.timer = window.setTimeout(function () {
                    if (!expandAlbumImage) return;
                    expandAlbumImage.style.transition = '';
                    expandAlbumImage.style.transform = '';
                    expandAlbumImage.style.opacity = '';
                }, 260);
            });
        }, 135);
    }

    function endExpandAlbumSwipe() {
        if (!expandArtSwipeState.tracking) return;
        const shouldChange = expandArtSwipeState.active && (
            Math.abs(expandArtSwipeState.deltaX) > 72 ||
            (Math.abs(expandArtSwipeState.deltaX) > 34 && Math.abs(expandArtSwipeState.velocity) > 0.55)
        );
        const direction = expandArtSwipeState.deltaX < 0 ? 'next' : 'prev';
        expandArtSwipeState.tracking = false;
        expandArtSwipeState.active = false;
        if (shouldChange) {
            animateExpandAlbumTrackChange(direction);
        } else {
            resetExpandAlbumSwipeVisual();
        }
    }

    // ?酉逾?獄?獄쏆꼶??筌뤴뫀諭??筌띿쉸????疫꿸퀣肉??類ｌ졊 ?????    function toggleShuffle(forceValue = null) {
    function toggleShuffle(forceValue = null) {
        playerState.shuffle = (forceValue !== null) ? forceValue : !playerState.shuffle;
        
        if (playerState.shuffle) {
            // ?酉逾???뽮쉐?? ?袁⑹삺 ??源?餓λ쵐???ⓥ돦??癰귣똻???랁???롢돢筌왖 ?ⓦ볥굶????뺣쑁??띿쓺 ??롮벉
            if (playerState.queue.length > 1) {
                const currentTrack = playerState.currentIndex >= 0 ? playerState.queue[playerState.currentIndex] : null;
                let remainder = [...playerState.queue];
                
                if (currentTrack) {
                    remainder.splice(playerState.currentIndex, 1);
                }
                
                // Fisher-Yates Shuffle ???х뵳?弛?
                for (let i = remainder.length - 1; i > 0; i--) {
                    const j = Math.floor(Math.random() * (i + 1));
                    [remainder[i], remainder[j]] = [remainder[j], remainder[i]];
                }
                
                if (currentTrack) {
                    playerState.queue = [currentTrack, ...remainder];
                    playerState.currentIndex = 0;
                } else {
                    playerState.queue = remainder;
                    playerState.currentIndex = -1;
                }
            }
        } else {
            // ?酉逾???쑵??源딆넅: ?癒?궚 ??뽮퐣(originalQueue)嚥?癰귣벀???랁? ?袁⑹삺 ?怨쀯폒 ?ⓥ돦???紐껊쑔??? 筌≪뼚釉?揶쏄퉮??
            const currentSongId = playerState.currentSongId;
            playerState.queue = [...playerState.originalQueue];
            
            if (currentSongId) {
                const origIndex = playerState.queue.findIndex(t => t.id === currentSongId);
                playerState.currentIndex = (origIndex !== -1) ? origIndex : 0;
            }
        }
        
        updateShuffleUI();
        renderQueueUI();
        saveStateToStorage();
    }

    function toggleRepeatMode() {
        const modes = ['none', 'all', 'one'];
        const nextIdx = (modes.indexOf(playerState.repeatMode) + 1) % modes.length;
        playerState.repeatMode = modes[nextIdx];
        
        updateRepeatUI();
        saveStateToStorage();
    }

    // ==========================================================================
    // 5. ??疫꿸퀣肉?Queue) UI ?닌딇뀧 獄???뽯선
    // ==========================================================================
    
    // ?袁⑹삺 ????륁뵠筌왖?????쐭筌띻낮留????툢 筌뤴뫖以?.song-item) ?袁⑷퍥??疫꿸낯堉??븍┛
    function collectPageSongs() {
        const songRows = document.querySelectorAll('.song-item');
        const collected = [];
        songRows.forEach(row => {
            collected.push({
                id: row.getAttribute('data-id'),
                title: row.getAttribute('data-title'),
                artist: row.getAttribute('data-artist') || TEXT_FALLBACKS.artist,
                album: row.getAttribute('data-album') || '',
                duration: parseDurationValue(row.getAttribute('data-duration')),
                cover_url: normalizeCoverUrl(row.getAttribute('data-cover-url')) || `cover.php?id=${encodeURIComponent(row.getAttribute('data-id') || '')}`,
                stream_url: `stream.php?id=${encodeURIComponent(row.getAttribute('data-id') || '')}`
            });
        });
        return collected;
    }

    // ??뽮쉐 ??疫꿸퀣肉??揶쏄퉮???랁??怨쀯폒 ??뽰삂
    function setQueueAndPlay(tracksList, startIndex = 0, isShuffleClick = false, options = {}) {
        const tracks = sanitizeQueue(tracksList);
        if (tracks.length === 0) return;
        const safeStartIndex = Math.min(Math.max(parseInt(startIndex, 10) || 0, 0), tracks.length - 1);
        
        playerState.originalQueue = [...tracks];
        playerState.queue = [...tracks];
        playerState.queueContext = document.body ? document.body.className : null;
        
        if (isShuffleClick) {
            playerState.shuffle = true;
            // ?袁⑷퍥 ?酉逾???源?筌ｌ꼶??
            for (let i = playerState.queue.length - 1; i > 0; i--) {
                const j = Math.floor(Math.random() * (i + 1));
                [playerState.queue[i], playerState.queue[j]] = [playerState.queue[j], playerState.queue[i]];
            }
            playerState.currentIndex = 0;
        } else {
            playerState.shuffle = false;
            playerState.currentIndex = safeStartIndex;
        }
        
        updateShuffleUI();
        updateRepeatUI();
        renderQueueUI();
        
        // ?????紐껊쑔???怨쀯폒 ??뽰삂
        playTrack(playerState.currentIndex, options);
    }

    function appendTracksToQueue(tracksList) {
        const tracks = sanitizeQueue(tracksList);
        if (tracks.length === 0) return 0;
        const existingIds = new Set(playerState.queue.map(track => String(track.id)));
        const appended = tracks.filter(track => !existingIds.has(String(track.id)));
        if (appended.length === 0) return 0;
        playerState.queue = playerState.queue.concat(appended);
        playerState.originalQueue = playerState.originalQueue.concat(appended);
        if (playerState.currentIndex < 0 && playerState.queue.length > 0) {
            playerState.currentIndex = 0;
        }
        renderQueueUI();
        saveStateToStorage();
        return appended.length;
    }

    function insertTracksNext(tracksList) {
        const tracks = sanitizeQueue(tracksList);
        if (tracks.length === 0) return 0;
        const existingIds = new Set(playerState.queue.map(track => String(track.id)));
        const inserted = tracks.filter(track => !existingIds.has(String(track.id)));
        if (inserted.length === 0) return 0;
        const insertAt = playerState.currentIndex >= 0 ? playerState.currentIndex + 1 : 0;
        playerState.queue.splice(insertAt, 0, ...inserted);
        playerState.originalQueue = sanitizeQueue(playerState.queue);
        if (playerState.currentIndex < 0 && playerState.queue.length > 0) {
            playerState.currentIndex = 0;
        }
        renderQueueUI();
        saveStateToStorage();
        return inserted.length;
    }

    async function runAutoMixAction(action = 'play', limit = 24, button = null) {
        const normalizedAction = action === 'append' ? 'append' : 'play';
        const safeLimit = Math.max(10, Math.min(30, parseInt(limit || '24', 10) || 24));
        const originalHtml = button ? button.innerHTML : '';
        if (button) {
            button.disabled = true;
            button.innerHTML = '<i class="fa-solid fa-spinner fa-spin"></i> 믹스 생성 중';
        }
        try {
            const response = await fetch(`${window.APP_BASE_PATH || './'}api/auto_mix.php?limit=${encodeURIComponent(safeLimit)}&_=${Date.now()}`, {
                headers: { 'Accept': 'application/json', 'Cache-Control': 'no-cache' },
                credentials: 'same-origin',
                cache: 'no-store'
            });
            const payload = await response.json();
            const tracks = sanitizeQueue((((payload || {}).data || {}).tracks) || []);
            if (!response.ok || !payload || payload.success !== true || tracks.length === 0) {
                showPlayerNotice((payload && payload.message) || '자동 믹스를 만들 곡이 부족합니다.', 'warning');
                return 0;
            }
            if (normalizedAction === 'append') {
                const addedCount = appendTracksToQueue(tracks);
                showPlayerNotice(addedCount > 0 ? `자동 믹스 ${addedCount}곡을 대기열에 추가했습니다.` : '이미 대기열에 있는 곡들입니다.', addedCount > 0 ? 'success' : 'info');
                return addedCount;
            }
            setQueueAndPlay(tracks, 0, false, { userNotice: true });
            showPlayerNotice(`자동 믹스 ${tracks.length}곡을 재생합니다.`, 'success');
            return tracks.length;
        } catch (err) {
            logPlayerIssue('Auto mix failed', err);
            showPlayerNotice('자동 믹스를 만들지 못했습니다.', 'error');
            return 0;
        } finally {
            if (button) {
                button.disabled = false;
                button.innerHTML = originalHtml;
            }
        }
    }

    // ??疫꿸퀣肉?UI????ㅺ섯 ?臾믡걹????덉읅??곗쨮 域밸챶??빳?(???????ㅺ섯 獄??類ㅼ삢 ???쟿??곷선 ??ㅺ섯 ??덈뻻 筌왖??
    function renderQueueUI() {
        ensureQueuePanelActions();
        if (queueCount) queueCount.textContent = playerState.queue.length;
        
        let html = '';
        if (playerState.queue.length === 0) {
            html = `
                <div class="queue-empty-state">
                    <p class="queue-empty-text">${escapeHtml(TEXT_FALLBACKS.emptyQueue)}</p>
                    <button type="button" class="btn btn-primary btn-sm queue-auto-mix-btn">
                        <i class="fa-solid fa-wand-magic-sparkles"></i> 자동 믹스 만들기
                    </button>
                </div>`;
            if (queueListContainer) queueListContainer.innerHTML = html;
            if (expandQueueList) expandQueueList.innerHTML = html;
            updateQueueActionButtons();
            return;
        }
        
        playerState.queue.forEach((track, idx) => {
            const isActive = (idx === playerState.currentIndex);
            const activeClass = isActive ? 'active is-playing' : '';
            const activeAttr = isActive ? ' aria-current="true"' : '';
            const thumbUrl = getTrackCoverUrl(track);
            const offsetFromCurrent = idx - playerState.currentIndex;
            const queueLabel = isActive
                ? '재생 중'
                : (idx > playerState.currentIndex
                    ? (offsetFromCurrent === 1 ? '다음 곡' : `다음 ${offsetFromCurrent}곡`)
                    : String(idx + 1));
            const nowPlayingBadge = '';
            
            html += `
                <div class="queue-item ${activeClass}" data-index="${idx}"${activeAttr}>
                    <div class="queue-item-thumb">
                        <img src="${thumbUrl}" alt="Cover" loading="lazy" width="44" height="44" onerror="this.onerror=null;this.src='${window.APP_BASE_PATH}assets/img/default-cover.svg';">
                    </div>
                    <div class="queue-item-meta">
                        <div class="queue-item-title">${escapeHtml(track.title)}${nowPlayingBadge}</div>
                        <div class="queue-item-artist"><span class="queue-item-position">${escapeHtml(queueLabel)}</span>${escapeHtml(track.artist)}</div>
                    </div>
                    <div class="queue-item-duration">${formatTime(track.duration)}</div>
                    <div class="queue-item-order" aria-label="대기열 순서 변경">
                        <button type="button" class="queue-item-move" data-index="${idx}" data-direction="up" title="위로 이동" aria-label="위로 이동" ${idx === 0 ? 'disabled' : ''}>
                            <i class="fas fa-chevron-up"></i>
                        </button>
                        <button type="button" class="queue-item-move" data-index="${idx}" data-direction="down" title="아래로 이동" aria-label="아래로 이동" ${idx === playerState.queue.length - 1 ? 'disabled' : ''}>
                            <i class="fas fa-chevron-down"></i>
                        </button>
                    </div>
                    <button type="button" class="queue-item-remove" data-index="${idx}" title="제거" aria-label="대기열에서 제거">
                        <i class="fas fa-trash-alt"></i>
                    </button>
                </div>
            `;
        });

        // 1. ???????疫꿸퀣肉????쐭筌?        if (queueListContainer) {
        if (queueListContainer) {
            queueListContainer.innerHTML = html;
            bindQueueItemEvents(queueListContainer);
        }
        
        // 2. ?類ㅼ삢 ???쟿??곷선 ??疫꿸퀣肉????쐭筌?        if (expandQueueList) {
        if (expandQueueList) {
            expandQueueList.innerHTML = html;
            bindQueueItemEvents(expandQueueList);
            focusCurrentQueueItemInExpand(true);
        }
        updateQueueActionButtons();
    }

    // ??疫꿸퀣肉??귐딅뮞???袁⑹뵠??????獄??????紐껊굶??獄쏅뗄???    function bindQueueItemEvents(container) {
    function bindQueueItemEvents(container) {
        const items = container.querySelectorAll('.queue-item');
        items.forEach(item => {
            // ??????????疫꿸퀣肉???????ⓥ돦?앮에??紐껊쑔????猷??뤿연 ??源?
            item.addEventListener('click', function(e) {
                // ?怨뺤쟿疫꿸퀬????뽰뇚 甕곌쑵????????뽯퓠????源??? ??놁벉
                if (e.target.closest('.queue-item-remove, .queue-item-move')) return;
                
                const idx = parseInt(this.getAttribute('data-index'));
                playTrack(idx, { userNotice: true });
            });
        });

        // 揶쏆뮆??????볤탢 ??源??獄쏅뗄???        const removeButtons = container.querySelectorAll('.queue-item-remove');
        const removeButtons = container.querySelectorAll('.queue-item-remove');
        removeButtons.forEach(btn => {
            btn.addEventListener('click', function(e) {
                e.stopPropagation();
                const idx = parseInt(this.getAttribute('data-index'));
                removeTrackFromQueue(idx);
            });
        });

        const moveButtons = container.querySelectorAll('.queue-item-move');
        moveButtons.forEach(btn => {
            btn.addEventListener('click', function(e) {
                e.preventDefault();
                e.stopPropagation();
                const idx = parseInt(this.getAttribute('data-index'), 10);
                const direction = this.getAttribute('data-direction') === 'up' ? -1 : 1;
                moveQueueTrack(idx, direction);
            });
        });
    }

    function coerceBoolean(value) {
        return value === true || value === 1 || value === '1' || value === 'true';
    }

    function getSongRowsById(songId) {
        const targetId = String(songId);
        return Array.from(document.querySelectorAll('.song-item')).filter(row => row.getAttribute('data-id') === targetId);
    }

    function setQueueLikedState(songId, liked) {
        const targetId = String(songId);
        const nextLiked = coerceBoolean(liked);
        [playerState.queue, playerState.originalQueue].forEach(queue => {
            if (!Array.isArray(queue)) return;
            queue.forEach(track => {
                if (track && String(track.id) === targetId) {
                    track.liked = nextLiked;
                }
            });
        });
    }

    function getKnownLikedState(songId) {
        const targetId = String(songId);
        const queueTrack = (Array.isArray(playerState.queue) ? playerState.queue : []).find(track => track && String(track.id) === targetId);
        if (queueTrack && typeof queueTrack.liked !== 'undefined') return coerceBoolean(queueTrack.liked);
        const originalTrack = (Array.isArray(playerState.originalQueue) ? playerState.originalQueue : []).find(track => track && String(track.id) === targetId);
        if (originalTrack && typeof originalTrack.liked !== 'undefined') return coerceBoolean(originalTrack.liked);
        const firstRow = getSongRowsById(targetId)[0];
        return firstRow ? firstRow.getAttribute('data-liked') === '1' : false;
    }

    // ??疫꿸퀣肉?癒?퐣 ??μ뵬 ????볤탢 筌ｌ꼶??
    function removeTrackFromQueue(index) {
        if (index < 0 || index >= playerState.queue.length) return;
        
        const removedTrack = playerState.queue[index];
        const isRemovingCurrent = (index === playerState.currentIndex);
        
        // ??疫꿸퀣肉?癒?퐣 ??볤탢
        playerState.queue.splice(index, 1);
        
        // ?癒?궚 獄쏄퉮毓???疫꿸퀣肉?癒?퐣????볤탢
        const origIdx = playerState.originalQueue.findIndex(t => t.id === removedTrack.id);
        if (origIdx !== -1) {
            playerState.originalQueue.splice(origIdx, 1);
        }

        // ?紐껊쑔??癰귣똻??
        if (isRemovingCurrent) {
            if (playerState.queue.length === 0) {
                // ??疫꿸퀣肉???袁⑹읈????쑴堉김린袁ⓥ뵛 野껋럩??
                playerState.currentIndex = -1;
                playerState.currentSongId = null;
                pauseTrack();
                resetPlayerInfo();
            } else {
                // ??쇰퓠 ?ⓥ돦????됱몵筌?域??紐껊쑔??? ??源? ?袁⑤빍筌???곸읈 ?紐껊쑔??                if (playerState.currentIndex >= playerState.queue.length) {
                if (playerState.currentIndex >= playerState.queue.length) {
                    playerState.currentIndex = playerState.queue.length - 1;
                }
                playTrack(playerState.currentIndex);
            }
        } else {
            // ?袁⑹삺 ?ⓦ볥궖????롢걹 ?ⓥ돦???????뤿???삠늺 ?紐껊쑔???怨좉쾿?귐됥돧??癰귣똻??
            if (index < playerState.currentIndex) {
                playerState.currentIndex--;
            }
        }
        
        renderQueueUI();
        saveStateToStorage();
    }

    function moveQueueTrack(index, direction) {
        const targetIndex = index + direction;
        if (index < 0 || targetIndex < 0 || index >= playerState.queue.length || targetIndex >= playerState.queue.length) return;

        const wasCurrentTrack = playerState.queue[playerState.currentIndex] || null;
        const [track] = playerState.queue.splice(index, 1);
        playerState.queue.splice(targetIndex, 0, track);
        playerState.originalQueue = sanitizeQueue(playerState.queue);

        if (wasCurrentTrack) {
            playerState.currentIndex = playerState.queue.findIndex(item => String(item.id) === String(wasCurrentTrack.id));
        }

        renderQueueUI();
        saveStateToStorage();
        showPlayerNotice('대기열 순서를 변경했습니다.', 'success');
    }

    function clearQueue() {
        if (!confirm('재생 대기열을 비울까요?')) return;
        playerState.queue = [];
        playerState.originalQueue = [];
        playerState.currentIndex = -1;
        playerState.currentSongId = null;
        
        pauseTrack();
        resetPlayerInfo();
        renderQueueUI();
        saveStateToStorage();
    }

    function clearQueueAfterCurrent() {
        if (playerState.queue.length === 0 || playerState.currentIndex < 0) {
            showPlayerNotice('현재 재생 중인 곡이 없습니다.', 'info');
            return;
        }
        if (playerState.currentIndex >= playerState.queue.length - 1) {
            showPlayerNotice('현재 곡 이후에 비울 곡이 없습니다.', 'info');
            return;
        }
        if (!confirm('현재 곡 이후의 대기열을 비울까요?')) return;
        const current = playerState.queue[playerState.currentIndex];
        playerState.queue = playerState.queue.slice(0, playerState.currentIndex + 1);
        playerState.originalQueue = sanitizeQueue(playerState.queue);
        playerState.currentIndex = current
            ? playerState.queue.findIndex(track => String(track.id) === String(current.id))
            : Math.min(playerState.currentIndex, playerState.queue.length - 1);
        playerState.currentSongId = current ? current.id : playerState.currentSongId;
        renderQueueUI();
        saveStateToStorage();
        showPlayerNotice('현재 곡 이후의 대기열을 비웠습니다.', 'success');
    }

    function queueSongIdsForSave() {
        const ids = [];
        const seen = new Set();
        playerState.queue.forEach(track => {
            const id = track && track.id ? String(track.id) : '';
            if (!id || seen.has(id)) return;
            seen.add(id);
            ids.push(id);
        });
        return ids;
    }

    function defaultQueuePlaylistName() {
        const now = new Date();
        const yyyy = now.getFullYear();
        const mm = String(now.getMonth() + 1).padStart(2, '0');
        const dd = String(now.getDate()).padStart(2, '0');
        return `재생 대기열 ${yyyy}-${mm}-${dd}`;
    }

    function ensureQueueSaveModal() {
        let modal = document.getElementById('queue-save-modal');
        if (modal) return modal;
        modal = document.createElement('div');
        modal.id = 'queue-save-modal';
        modal.className = 'modal-overlay queue-save-modal-overlay';
        modal.hidden = true;
        modal.setAttribute('aria-hidden', 'true');
        modal.innerHTML = `
            <div class="modal-box queue-save-modal-box" role="dialog" aria-modal="true" aria-labelledby="queue-save-title">
                <button type="button" class="modal-close-btn queue-save-close" aria-label="닫기"><i class="fas fa-times"></i></button>
                <h2 id="queue-save-title">대기열을 플레이리스트로 저장</h2>
                <p class="queue-save-summary">현재 대기열의 곡 순서를 그대로 저장합니다.</p>
                <form id="queue-save-form" class="queue-save-form">
                    <label for="queue-save-name">플레이리스트 이름</label>
                    <input type="text" id="queue-save-name" name="name" class="form-control" maxlength="255" required>
                    <label for="queue-save-description">설명</label>
                    <textarea id="queue-save-description" name="description" class="form-control" maxlength="1000" rows="3" placeholder="선택 입력"></textarea>
                    <div class="modal-actions">
                        <button type="button" class="btn btn-secondary queue-save-cancel">취소</button>
                        <button type="submit" class="btn btn-primary queue-save-submit"><i class="fas fa-save"></i> 저장</button>
                    </div>
                </form>
            </div>`;
        document.body.appendChild(modal);
        const closeModal = function () {
            modal.hidden = true;
            modal.setAttribute('aria-hidden', 'true');
            document.body.classList.remove('queue-save-open');
        };
        modal.querySelectorAll('.queue-save-close, .queue-save-cancel').forEach(button => {
            button.addEventListener('click', closeModal);
        });
        modal.addEventListener('click', function(event) {
            if (event.target === modal) closeModal();
        });
        modal.addEventListener('keydown', function(event) {
            if (event.key === 'Escape') closeModal();
        });
        const form = modal.querySelector('#queue-save-form');
        form.addEventListener('submit', function(event) {
            event.preventDefault();
            saveQueueAsPlaylist(modal);
        });
        return modal;
    }

    function openQueueSaveModal() {
        const ids = queueSongIdsForSave();
        if (ids.length === 0) {
            showPlayerNotice('저장할 대기열이 없습니다.', 'info');
            return;
        }
        const modal = ensureQueueSaveModal();
        const nameInput = modal.querySelector('#queue-save-name');
        const summary = modal.querySelector('.queue-save-summary');
        if (nameInput) {
            nameInput.value = defaultQueuePlaylistName();
        }
        if (summary) {
            summary.textContent = `${ids.length}곡을 현재 순서대로 저장합니다.`;
        }
        modal.hidden = false;
        modal.setAttribute('aria-hidden', 'false');
        document.body.classList.add('queue-save-open');
        window.setTimeout(() => {
            if (nameInput) {
                nameInput.focus({ preventScroll: true });
                nameInput.select();
            }
        }, 30);
    }

    async function saveQueueAsPlaylist(modal) {
        const ids = queueSongIdsForSave();
        const nameInput = modal.querySelector('#queue-save-name');
        const descriptionInput = modal.querySelector('#queue-save-description');
        const submit = modal.querySelector('.queue-save-submit');
        const name = nameInput ? nameInput.value.trim() : '';
        if (!name) {
            showPlayerNotice('플레이리스트 이름을 입력해 주세요.', 'warning');
            if (nameInput) nameInput.focus();
            return;
        }
        if (ids.length === 0) {
            showPlayerNotice('저장할 대기열이 없습니다.', 'info');
            return;
        }
        const originalHtml = submit ? submit.innerHTML : '';
        if (submit) {
            submit.disabled = true;
            submit.innerHTML = '<i class="fa-solid fa-spinner fa-spin"></i> 저장 중';
        }
        try {
            const formData = new FormData();
            formData.set('csrf_token', window.APP_CSRF_TOKEN || '');
            formData.set('name', name);
            formData.set('description', descriptionInput ? descriptionInput.value.trim() : '');
            formData.set('song_ids', JSON.stringify(ids));
            const response = await fetch(`${window.APP_BASE_PATH || './'}api/save_queue_playlist.php`, {
                method: 'POST',
                body: formData,
                credentials: 'same-origin',
                cache: 'no-store',
                headers: {
                    'Accept': 'application/json',
                    'X-Requested-With': 'XMLHttpRequest'
                }
            });
            const payload = await response.json();
            if (!response.ok || !payload || payload.success !== true) {
                showPlayerNotice((payload && payload.message) || '대기열 저장에 실패했습니다.', 'warning');
                return;
            }
            modal.hidden = true;
            modal.setAttribute('aria-hidden', 'true');
            document.body.classList.remove('queue-save-open');
            showPlayerNotice(payload.message || '대기열을 플레이리스트로 저장했습니다.', 'success');
        } catch (err) {
            logPlayerIssue('Queue playlist save failed', err);
            showPlayerNotice('대기열 저장 중 오류가 발생했습니다.', 'error');
        } finally {
            if (submit) {
                submit.disabled = false;
                submit.innerHTML = originalHtml;
            }
        }
    }

    function ensureQueuePanelActions() {
        if (!queuePanel) return;
        const actions = queuePanel.querySelector('.queue-header-actions');
        if (!actions || actions.dataset.queueEnhanced === '1') return;
        actions.dataset.queueEnhanced = '1';

        const clearAfterBtn = document.createElement('button');
        clearAfterBtn.type = 'button';
        clearAfterBtn.className = 'btn btn-secondary btn-sm queue-clear-after-btn';
        clearAfterBtn.innerHTML = '<i class="fas fa-broom"></i> 이후 비우기';
        clearAfterBtn.addEventListener('click', function(event) {
            event.preventDefault();
            event.stopPropagation();
            clearQueueAfterCurrent();
        });

        const saveBtn = document.createElement('button');
        saveBtn.type = 'button';
        saveBtn.className = 'btn btn-primary btn-sm queue-save-btn';
        saveBtn.innerHTML = '<i class="fas fa-save"></i> 저장';
        saveBtn.addEventListener('click', function(event) {
            event.preventDefault();
            event.stopPropagation();
            openQueueSaveModal();
        });

        if (btnClearQueue) {
            actions.insertBefore(clearAfterBtn, btnClearQueue);
            actions.insertBefore(saveBtn, btnClearQueue);
        } else if (queueClose) {
            actions.insertBefore(clearAfterBtn, queueClose);
            actions.insertBefore(saveBtn, queueClose);
        } else {
            actions.append(clearAfterBtn, saveBtn);
        }
        updateQueueActionButtons();
    }

    function updateQueueActionButtons() {
        const saveBtn = queuePanel ? queuePanel.querySelector('.queue-save-btn') : null;
        const clearAfterBtn = queuePanel ? queuePanel.querySelector('.queue-clear-after-btn') : null;
        const hasQueue = playerState.queue.length > 0;
        const hasAfterCurrent = hasQueue && playerState.currentIndex >= 0 && playerState.currentIndex < playerState.queue.length - 1;
        if (saveBtn) saveBtn.disabled = !hasQueue;
        if (clearAfterBtn) clearAfterBtn.disabled = !hasAfterCurrent;
        if (btnClearQueue) btnClearQueue.disabled = !hasQueue;
    }

    function resetPlayerInfo() {
        setElementText(playerTitle, TEXT_FALLBACKS.noCurrentTrack);
        setElementText(playerArtist, "-");
        setElementText(expandPlayerTitle, TEXT_FALLBACKS.noCurrentTrack);
        setElementText(expandPlayerArtist, "-");
        
        const defaultCover = `${window.APP_BASE_PATH}assets/img/default-cover.svg`;
        if (playerAlbumImage) playerAlbumImage.src = defaultCover;
        if (expandAlbumImage) expandAlbumImage.src = defaultCover;
        
        progressBar.style.width = '0%';
        if (expandProgressBar) expandProgressBar.style.width = '0%';
        timeCurrent.textContent = '00:00';
        timeTotal.textContent = '00:00';
        if (expandTimeCurrent) expandTimeCurrent.textContent = '00:00';
        if (expandTimeTotal) expandTimeTotal.textContent = '00:00';
        if (miniLyricsLine) miniLyricsLine.textContent = '';
        resetSyncedLyricsState();
        if (expandLyricsFullText) {
            expandLyricsFullText.classList.add('is-lyrics-empty');
            expandLyricsFullText.innerHTML = '<p class="queue-empty-text">곡을 먼저 재생해 주세요.</p>';
        }
        
        // 筌뤴뫖以???뽮쉐 ?????삳즲 ?袁? ??곸젫
        document.querySelectorAll('.song-item').forEach(el => el.classList.remove('playing'));
    }

    // ==========================================================================
    // 6. ??쑬猷욄묾?揶쎛????얜굣 獄???덉읅 ???쐭筌?    // ==========================================================================
    function setLyricsControlsState(hasLyrics) {
        const available = hasLyrics === true;
        const knownEmpty = hasLyrics === false;
        [tabBtnLyrics, expandBtnLyricsToggle].forEach(btn => {
            if (!btn) return;
            btn.classList.toggle('has-lyrics', available);
            btn.classList.toggle('no-lyrics', knownEmpty);
            btn.setAttribute('aria-disabled', 'false');
            if (btn === expandBtnLyricsToggle) {
                btn.textContent = available ? '가사 보기' : '가사 등록/수정';
            }
        });
    }

    function resetSyncedLyricsState() {
        syncedLyricsState.songId = null;
        syncedLyricsState.lines = [];
        syncedLyricsState.activeIndex = -1;
        if (expandLyricsFullText) {
            expandLyricsFullText.classList.remove('is-synced-lyrics');
        }
    }

    function parseSyncedLyrics(lyrics) {
        const text = String(lyrics || '');
        if (!/\[\d{1,2}:\d{2}(?:[.:]\d{1,3})?\]/.test(text)) return [];
        const lines = [];
        text.split(/\r?\n/).forEach(rawLine => {
            const timestamps = [...rawLine.matchAll(/\[(\d{1,2}):(\d{2})(?:[.:](\d{1,3}))?\]/g)];
            if (!timestamps.length) return;
            const lyricText = rawLine.replace(/\[[^\]]+\]/g, '').trim();
            timestamps.forEach(match => {
                const minutes = Number(match[1] || 0);
                const seconds = Number(match[2] || 0);
                const fractionText = match[3] || '0';
                const fraction = Number(fractionText.padEnd(3, '0').slice(0, 3)) / 1000;
                const time = (minutes * 60) + seconds + fraction;
                if (Number.isFinite(time)) {
                    lines.push({ time, text: lyricText || '...' });
                }
            });
        });
        return lines
            .sort((a, b) => a.time - b.time)
            .filter((line, index, list) => index === 0 || line.time !== list[index - 1].time || line.text !== list[index - 1].text);
    }

    function renderSyncedLyrics(lines) {
        if (!expandLyricsFullText || !Array.isArray(lines) || lines.length < 2) return false;
        resetSyncedLyricsState();
        const track = currentTrack();
        syncedLyricsState.songId = track && track.id ? String(track.id) : null;
        syncedLyricsState.lines = lines;
        expandLyricsFullText.classList.remove('is-lyrics-empty');
        expandLyricsFullText.textContent = '';
        expandLyricsFullText.classList.add('is-synced-lyrics');

        const list = document.createElement('div');
        list.className = 'synced-lyrics-list';
        list.setAttribute('role', 'list');

        lines.forEach((line, index) => {
            const item = document.createElement('button');
            item.type = 'button';
            item.className = 'synced-lyrics-line';
            item.dataset.index = String(index);
            item.dataset.time = String(line.time);
            item.setAttribute('role', 'listitem');
            item.textContent = line.text;
            item.addEventListener('click', function() {
                if (!Number.isFinite(line.time)) return;
                try {
                    audio.currentTime = Math.max(0, line.time);
                    updateSyncedLyrics(audio.currentTime, true);
                } catch (err) {
                    logPlayerIssue('Synced lyrics seek failed', err);
                }
            });
            list.appendChild(item);
        });

        expandLyricsFullText.appendChild(list);
        updateSyncedLyrics(audio.currentTime || 0, true);
        return true;
    }

    function updateSyncedLyrics(currentTime, force = false) {
        if (!syncedLyricsState.lines.length || !expandLyricsFullText) return;
        const track = currentTrack();
        if (track && syncedLyricsState.songId && String(track.id) !== syncedLyricsState.songId) return;

        let nextIndex = 0;
        for (let i = 0; i < syncedLyricsState.lines.length; i++) {
            if (currentTime + 0.18 >= syncedLyricsState.lines[i].time) nextIndex = i;
            else break;
        }
        if (!force && nextIndex === syncedLyricsState.activeIndex) return;

        const previous = expandLyricsFullText.querySelector('.synced-lyrics-line.is-active');
        if (previous) {
            previous.classList.remove('is-active');
            previous.removeAttribute('aria-current');
        }

        const current = expandLyricsFullText.querySelector(`.synced-lyrics-line[data-index="${nextIndex}"]`);
        if (current) {
            current.classList.add('is-active');
            current.setAttribute('aria-current', 'true');
            if (expandPlayerOverlay && expandPlayerOverlay.style.display === 'flex') {
                current.scrollIntoView({ block: 'center', behavior: force ? 'auto' : 'smooth' });
            }
        }

        syncedLyricsState.activeIndex = nextIndex;
        if (miniLyricsLine) {
            miniLyricsLine.textContent = syncedLyricsState.lines[nextIndex] ? syncedLyricsState.lines[nextIndex].text : '';
        }
    }

    function renderLyricsText(lyrics) {
        if (!expandLyricsFullText) return false;
        const syncedLines = parseSyncedLyrics(lyrics);
        if (renderSyncedLyrics(syncedLines)) return true;
        resetSyncedLyricsState();
        expandLyricsFullText.classList.remove('is-lyrics-empty');
        expandLyricsFullText.textContent = '';
        const wrapper = document.createElement('div');
        wrapper.style.padding = '10px 0';
        wrapper.style.whiteSpace = 'pre-line';
        wrapper.style.wordBreak = 'break-word';
        wrapper.textContent = lyrics;
        expandLyricsFullText.appendChild(wrapper);
        return false;
    }

    function renderLyricsEmpty(message) {
        if (!expandLyricsFullText) return;
        resetSyncedLyricsState();
        expandLyricsFullText.classList.add('is-lyrics-empty');
        expandLyricsFullText.textContent = '';
        const empty = document.createElement('p');
        empty.className = 'queue-empty-text';
        empty.textContent = message || TEXT_FALLBACKS.noLyrics;
        expandLyricsFullText.appendChild(empty);
        const track = currentTrack();
        if (track && track.id) {
            const action = document.createElement('a');
            action.className = 'btn btn-secondary btn-sm lyrics-edit-inline-link';
            action.href = `${window.APP_BASE_PATH}lyrics_edit.php?song_id=${encodeURIComponent(track.id)}`;
            action.textContent = '가사 등록/수정';
            action.addEventListener('click', function() {
                saveStateToStorage();
                closeExpandPlayer();
            });
            expandLyricsFullText.appendChild(action);
        }
    }

    function loadLyrics(songId, forceRefresh = false) {
        if (!songId) return;

        const cacheKey = String(songId);
        if (!forceRefresh && lyricsCache.has(cacheKey)) {
            const cached = lyricsCache.get(cacheKey);
            if (cached && cached.has_lyrics && cached.lyrics) {
                const synced = renderLyricsText(cached.lyrics);
                if (!synced && miniLyricsLine) miniLyricsLine.textContent = cached.lyrics.split('\n')[0] || '가사 정보 있음';
                setLyricsControlsState(true);
            } else {
                renderLyricsEmpty(TEXT_FALLBACKS.noLyrics);
                if (miniLyricsLine) miniLyricsLine.textContent = TEXT_FALLBACKS.noLyrics;
                setLyricsControlsState(false);
            }
            return;
        }

        const url = `${window.APP_BASE_PATH}api/get_lyrics.php?song_id=${encodeURIComponent(songId)}&_=${Date.now()}`;
        fetch(url, {
            headers: {
                'Accept': 'application/json',
                'Cache-Control': 'no-cache'
            },
            credentials: 'same-origin',
            cache: 'no-store'
        })
            .then(res => res.json())
            .then(data => {
                const payload = data && data.data ? data.data : data;
                const cleanLyrics = payload && payload.lyrics ? String(payload.lyrics) : '';
                const hasLyrics = Boolean(payload && (payload.has_lyrics || cleanLyrics.trim() !== ''));
                lyricsCache.set(cacheKey, {
                    lyrics: cleanLyrics,
                    has_lyrics: hasLyrics,
                    updated_at: payload ? payload.updated_at : ''
                });

                if (data.success && hasLyrics) {
                    const synced = renderLyricsText(cleanLyrics);
                    setLyricsControlsState(true);
                    if (!synced && miniLyricsLine) {
                        const lines = cleanLyrics.split('\n');
                        miniLyricsLine.textContent = lines[0] || '가사 정보 있음';
                    }
                } else {
                    renderLyricsEmpty('등록된 가사가 없습니다. 상세 페이지에서 가사를 추가해 보세요.');
                    setLyricsControlsState(false);
                    if (miniLyricsLine) {
                        miniLyricsLine.textContent = TEXT_FALLBACKS.noLyrics;
                    }
                }
            })
            .catch(err => {
                logPlayerIssue('Lyrics load failed', err);
                if (miniLyricsLine) miniLyricsLine.textContent = TEXT_FALLBACKS.loadLyricsError;
            });
    }

    // ==========================================================================
    // 7. UI Helpers / Sync State Elements
    // ==========================================================================
    
    function updateShuffleUI() {
        const shuffleOn = playerState.shuffle;
        
        if (btnShuffle) btnShuffle.classList.toggle('active', shuffleOn);
        if (expandBtnShuffle) expandBtnShuffle.classList.toggle('active', shuffleOn);
        
        const titleText = shuffleOn ? '셔플 재생 켜짐' : '셔플 재생';
        if (btnShuffle) btnShuffle.title = titleText;
        if (expandBtnShuffle) expandBtnShuffle.title = titleText;
    }

    function updateRepeatUI() {
        const mode = playerState.repeatMode;
        const btns = [btnRepeat, expandBtnRepeat];
        
        btns.forEach(btn => {
            if (!btn) return;
            
            if (mode === 'none') {
                btn.classList.remove('active');
                btn.innerHTML = '<i class="fas fa-redo"></i>';
                btn.title = '반복 재생 꺼짐';
            } else if (mode === 'all') {
                btn.classList.add('active');
                btn.innerHTML = '<i class="fas fa-redo"></i>';
                btn.title = '반복 재생 (전체 반복)';
            } else if (mode === 'one') {
                btn.classList.add('active');
                btn.innerHTML = '<i class="fas fa-redo"></i><span style="font-size:9px; position:absolute; top:-2px; right:-2px; background:#00CD3C; color:#000; font-weight:700; width:12px; height:12px; border-radius:50%; display:inline-flex; align-items:center; justify-content:center;">1</span>';
                btn.title = '반복 재생 (한 곡 반복)';
            }
        });
    }

    function updatePlayButtonUI(playing) {
        const playHtml = playing ? '<i class="fas fa-pause"></i>' : '<i class="fas fa-play"></i>';
        const titleText = playing ? '일시정지' : '재생';
        
        if (btnPlayPause) {
            btnPlayPause.innerHTML = playHtml;
            btnPlayPause.title = titleText;
        }
        if (playerBar) {
            playerBar.classList.toggle('is-playing', !!playing);
        }
        if (expandBtnPlayPause) {
            expandBtnPlayPause.innerHTML = playHtml;
            expandBtnPlayPause.title = titleText;
        }
        
        if (brandIcon) {
            brandIcon.style.animationPlayState = playing ? 'running' : 'paused';
        }
        if (albumArt) {
            albumArt.classList.toggle('playing', playing);
        }

        // 筌뤴뫖以???깅퓠 ?紐꾪뀱????源??袁⑹뵠????녿┛??        document.querySelectorAll('.song-item').forEach(el => {
        document.querySelectorAll('.song-item').forEach(el => {
            const elId = el.getAttribute('data-id');
            const rowIcon = el.querySelector('.play-row-btn i');
            if (rowIcon) {
                if (playing && elId === playerState.currentSongId) {
                    rowIcon.className = 'fas fa-pause';
                } else {
                    rowIcon.className = 'fas fa-play';
                }
            }
        });
    }

    // ?ル뿭釉????묐뱜) UI ?怨뺣짗
    function updateLikeButtonUI(songId, forcedLiked = null) {
        if (!songId) return;
        const isLiked = forcedLiked === null ? getKnownLikedState(songId) : coerceBoolean(forcedLiked);
        setQueueLikedState(songId, isLiked);

        getSongRowsById(songId).forEach(row => {
            row.setAttribute('data-liked', isLiked ? '1' : '0');
            const rowLikeBtn = row.querySelector('.like-btn');
            if (rowLikeBtn) {
                rowLikeBtn.classList.toggle('liked', isLiked);
                rowLikeBtn.innerHTML = isLiked ? '<i class="fas fa-heart"></i>' : '<i class="far fa-heart"></i>';
                rowLikeBtn.setAttribute('aria-pressed', isLiked ? 'true' : 'false');
                rowLikeBtn.setAttribute('title', isLiked ? '좋아요 취소' : '좋아요');
            }
        });

        const pageLikeBtn = document.querySelector(`.song-detail-art-card .like-btn`);

        if (pageLikeBtn) {
            pageLikeBtn.classList.toggle('liked', isLiked);
            pageLikeBtn.innerHTML = isLiked ? '<i class="fas fa-heart"></i>' : '<i class="far fa-heart"></i>';
            pageLikeBtn.setAttribute('aria-pressed', isLiked ? 'true' : 'false');
            pageLikeBtn.setAttribute('title', isLiked ? '좋아요 취소' : '좋아요');
        }

        if (expandBtnLike) {
            if (isLiked) {
                expandBtnLike.classList.add('liked');
                expandBtnLike.innerHTML = '<i class="fas fa-heart" style="color: #ff3b30;"></i> 좋아요';
            } else {
                expandBtnLike.classList.remove('liked');
                expandBtnLike.innerHTML = '<i class="far fa-heart"></i> 좋아요';
            }
            expandBtnLike.setAttribute('aria-pressed', isLiked ? 'true' : 'false');
        }
    }

    function isSongLikedInQueue(songId) {
        return getKnownLikedState(songId);
    }

    // ?袁⑹삺 ??源???筌뤴뫖以???륁뵠??깆뵠????뽮쉐??    function synchronizeActiveListUI(songId) {
    function synchronizeActiveListUI(songId) {
        document.querySelectorAll('.song-item').forEach(row => {
            const rowId = row.getAttribute('data-id');
            if (rowId === songId) {
                row.classList.add('playing');
            } else {
                row.classList.remove('playing');
            }
        });
    }

    // ==========================================================================
    // 8. Event Listeners for Player Control Actions
    // ==========================================================================
    function bindPlayerControl(button, handler) {
        if (!button || typeof handler !== 'function') return;
        button.addEventListener('click', function(event) {
            event.preventDefault();
            event.stopPropagation();
            handler();
        });
    }

    bindPlayerControl(btnPlayPause, togglePlay);
    bindPlayerControl(expandBtnPlayPause, togglePlay);
    
    bindPlayerControl(btnNext, playNext);
    bindPlayerControl(expandBtnNext, playNext);
    
    bindPlayerControl(btnPrev, playPrev);
    bindPlayerControl(expandBtnPrev, playPrev);
    
    bindPlayerControl(btnShuffle, () => toggleShuffle());
    bindPlayerControl(expandBtnShuffle, () => toggleShuffle());
    
    bindPlayerControl(btnRepeat, toggleRepeatMode);
    bindPlayerControl(expandBtnRepeat, toggleRepeatMode);

    // ??源???疫꿸퀣肉???뺤쨮???醫?
    if (btnQueue) {
        btnQueue.addEventListener('click', function(e) {
            e.stopPropagation();
            if (queuePanel.style.display === 'none' || queuePanel.style.display === '') {
                queuePanel.style.display = 'flex';
                renderQueueUI();
            } else {
                queuePanel.style.display = 'none';
            }
        });
    }

    if (queueClose) {
        queueClose.addEventListener('click', function() {
            queuePanel.style.display = 'none';
        });
    }

    if (btnClearQueue) {
        btnClearQueue.addEventListener('click', clearQueue);
    }

    // ??뺤쨮??獄??怨몃열 ?怨쀭뒄 ????る┛
    window.addEventListener('click', function(e) {
        if (queuePanel && queuePanel.style.display === 'flex' && !queuePanel.contains(e.target) && e.target !== btnQueue && !e.target.closest('#btn-queue')) {
            queuePanel.style.display = 'none';
        }
    });

    // ?類ㅼ삢 ???쟿??곷선 筌뤴뫀????뽯선
    if (btnExpandPlayer) {
        bindPlayerControl(btnExpandPlayer, openExpandPlayer);
    }

    if (playerBar) {
        playerBar.setAttribute('tabindex', '0');
        playerBar.setAttribute('role', 'button');
        playerBar.setAttribute('aria-label', '플레이어 열기');
        playerBar.addEventListener('click', function(event) {
            const interactive = event.target.closest('button, a, input, select, textarea, .progress-bar-wrapper, .volume-slider-wrapper');
            if (interactive) return;
            openExpandPlayer();
        });
        playerBar.addEventListener('keydown', function(event) {
            if (event.key !== 'Enter' && event.key !== ' ') return;
            if (event.target !== playerBar) return;
            event.preventDefault();
            openExpandPlayer();
        });
    }

    if (expandPlayerClose) {
        bindPlayerControl(expandPlayerClose, closeExpandPlayer);
    }
    if (expandPlayerOverlay) {
        expandPlayerOverlay.addEventListener('click', function(event) {
            const collapseBtn = event.target && event.target.closest
                ? event.target.closest('.expand-tab-collapse-btn')
                : null;
            if (collapseBtn) {
                event.preventDefault();
                event.stopPropagation();
                setExpandPanelCollapsed(!(expandPlayerOverlay && expandPlayerOverlay.classList.contains('is-tab-collapsed')));
                return;
            }
            if (event.target === expandPlayerOverlay) {
                closeExpandPlayer();
            }
        });
        expandPlayerOverlay.addEventListener('touchstart', startExpandSwipe, { passive: true });
        expandPlayerOverlay.addEventListener('touchmove', moveExpandSwipe, { passive: false });
        expandPlayerOverlay.addEventListener('touchend', endExpandSwipe, { passive: true });
        expandPlayerOverlay.addEventListener('touchcancel', endExpandSwipe, { passive: true });
    }

    if (expandAlbumImage) {
        expandAlbumImage.addEventListener('touchstart', startExpandAlbumSwipe, { passive: true });
        expandAlbumImage.addEventListener('touchmove', moveExpandAlbumSwipe, { passive: false });
        expandAlbumImage.addEventListener('touchend', endExpandAlbumSwipe, { passive: true });
        expandAlbumImage.addEventListener('touchcancel', function () {
            expandArtSwipeState.tracking = false;
            expandArtSwipeState.active = false;
            resetExpandAlbumSwipeVisual();
        }, { passive: true });
    }

    function syncPlayerUiFromAudio() {
        const actualPlaying = !!(audio && !audio.paused && !audio.ended);
        isPlaying = actualPlaying;
        if (audio && Number.isFinite(audio.currentTime)) {
            playerState.currentTime = audio.currentTime;
        }
        if (audio) {
            playerState.muted = audio.muted === true;
            if (Number.isFinite(audio.volume)) playerState.volume = audio.volume;
        }
        updatePlayButtonUI(actualPlaying);
        updateShuffleUI();
        updateRepeatUI();
        renderQueueUI();
        updateMediaSessionMetadata(currentTrack());
        updateMediaSessionPlaybackState();
        synchronizeActiveListUI(playerState.currentSongId);
    }

    if (expandBtnSongDetail) {
        expandBtnSongDetail.addEventListener('click', function() {
            saveStateToStorage();
            closeExpandPlayer();
        });
    }

    // ?類ㅼ삢 ???쟿??곷선 ????쇱맄燁?獄쏅뗄???    if (tabBtnQueue && tabBtnLyrics) {
    if (tabBtnQueue && tabBtnLyrics) {
        tabBtnQueue.addEventListener('click', function(event) {
            event.preventDefault();
            event.stopPropagation();
            tabBtnQueue.classList.add('active');
            tabBtnLyrics.classList.remove('active');
            setExpandPanelCollapsed(false);
            setExpandTabContentDisplay(false, false);
            syncExpandTabMode('queue');
            requestAnimationFrame(function() {
                focusCurrentQueueItemInExpand();
            });
        });
        
        tabBtnLyrics.addEventListener('click', function(event) {
            event.preventDefault();
            event.stopPropagation();
            tabBtnLyrics.classList.add('active');
            tabBtnQueue.classList.remove('active');
            setExpandPanelCollapsed(false);
            setExpandTabContentDisplay(true, false);
            syncExpandTabMode('lyrics');
            if (expandTabLyricsContent && window.matchMedia && window.matchMedia('(max-width: 1199px)').matches) {
                requestAnimationFrame(function() {
                    expandTabLyricsContent.scrollIntoView({ block: 'nearest', behavior: 'smooth' });
                    if (expandLyricsFullText) expandLyricsFullText.scrollTop = 0;
                });
            }
            if (playerState.currentSongId) loadLyrics(playerState.currentSongId, false);
        });
        
        if (expandBtnLyricsToggle) {
            expandBtnLyricsToggle.addEventListener('click', function(event) {
                if (expandBtnLyricsToggle.classList.contains('no-lyrics')) {
                    event.preventDefault();
                    navigateToLyricsEditorFromExpand();
                    return;
                }
                tabBtnLyrics.click();
            });
        }
    }

    // ?類ㅼ삢 ???쟿??곷선 ???ル뿭釉??獄????쟿?????쎈뱜 ??용┛ 甕곌쑵???怨뺣짗
    if (expandBtnLike) {
        expandBtnLike.addEventListener('click', function(event) {
            event.preventDefault();
            event.stopPropagation();
            if (playerState.currentSongId) {
                // ?袁⑹삺 嚥≪뮆逾????ID 疫꿸퀣???곗쨮 ??쑬猷욄묾??醫?
                const pageSongIdx = playerState.queue.findIndex(t => t.id === playerState.currentSongId);
                toggleLike(playerState.currentSongId, pageSongIdx);
            }
        });
    }

    if (expandBtnAddPlaylist) {
        expandBtnAddPlaylist.addEventListener('click', function(event) {
            event.preventDefault();
            event.stopPropagation();
            if (playerState.currentSongId) {
                openPlaylistModal(playerState.currentSongId);
            }
        });
    }

    // ==========================================================================
    // 9. ??삳탵????源???귐딅뮞??獄?5??雅뚯눊由??袁⑺뒄 獄쏄퉮毓?
    // ==========================================================================
    audio.addEventListener('timeupdate', function() {
        const effectiveDuration = getEffectiveDuration();
        if (guestPreviewMode && audio.currentTime >= guestPreviewSeconds) {
            audio.pause();
            try { audio.currentTime = Math.min(guestPreviewSeconds, effectiveDuration || guestPreviewSeconds); } catch (err) {}
            playerState.currentTime = guestPreviewSeconds;
            updateProgressUi(playerState.currentTime, effectiveDuration);
            saveStateToStorage();
            showGuestPreviewPrompt();
            return;
        }

        const pct = effectiveDuration > 0 ? (audio.currentTime / effectiveDuration) * 100 : 0;
        updateProgressUi(audio.currentTime, effectiveDuration);
        updateSyncedLyrics(audio.currentTime);

        if (!guestPreviewMode && playerState.currentSongId && appPlayerSettings.countRule === '30s' && audio.currentTime >= 30) {
            maybeIncrementPlayCount(playerState.currentSongId);
        }
        if (!guestPreviewMode && playerState.currentSongId && appPlayerSettings.countRule === 'half' && pct >= 50) {
            maybeIncrementPlayCount(playerState.currentSongId);
        }
        if (Date.now() - lastMediaPositionUpdate > 2000) {
            lastMediaPositionUpdate = Date.now();
            updateMediaSessionPlaybackState();
        }
    });

    audio.addEventListener('loadedmetadata', function() {
        applyPendingRestoreTime();
        if (audio.duration && audio.currentTime > audio.duration - 1) {
            audio.currentTime = 0;
            playerState.currentTime = 0;
        }
        const track = currentTrack();
        if (track && Number.isFinite(audio.duration) && audio.duration > 0) {
            track.duration = Math.round(audio.duration);
        }
        updateProgressUi(audio.currentTime || 0, getEffectiveDuration());
        updateMediaSessionPlaybackState();
        saveStateToStorage();
        if (!audio.paused) clearPlayerTransitionState(160);
    });

    audio.addEventListener('play', function() {
        isPlaying = true;
        playerState.userActivated = true;
        clearPlayerTransitionState(160);
        if (btnPlayPause) btnPlayPause.classList.remove('resume-ready');
        updatePlayButtonUI(true);
        updateProgressUi(audio.currentTime || playerState.currentTime || 0);
        startProgressTicker();
        updateMediaSessionPlaybackState();
        saveStateToStorage();
    });

    audio.addEventListener('pause', function() {
        isPlaying = false;
        stopProgressTicker();
        if (!isTrackTransitioning) clearPlayerTransitionState(0);
        updateProgressUi(audio.currentTime || playerState.currentTime || 0);
        updatePlayButtonUI(false);
        updateMediaSessionPlaybackState();
        saveStateToStorage();
    });

    audio.addEventListener('error', function() {
        clearPlayerTransitionState(0);
        logPlayerIssue('Audio stream error; attempting next track if available', audio.error);
        handleAudioStreamFailure(audio.error || new Error('audio error'));
    });

    audio.addEventListener('ended', function() {
        const endedSignature = `${playerState.currentSongId || ''}:${audio.currentSrc || audio.src || ''}`;
        const now = Date.now();
        if (endedSignature && endedSignature === lastEndedSignature && now - lastEndedAt < 1200) {
            return;
        }
        lastEndedSignature = endedSignature;
        lastEndedAt = now;
        stopProgressTicker();
        if (guestPreviewMode) {
            pauseTrack();
            showGuestPreviewPrompt();
            return;
        }
        if (playerState.repeatMode === 'one') {
            setPlayerTransitionState(true, currentTrack(), '한 곡 반복으로 다시 재생합니다.');
            audio.currentTime = 0;
            requestPlay({ keepResume: true }).catch(() => {});
        } else if (appPlayerSettings.autoNextEnabled) {
            playNext({ reason: 'ended' });
        } else {
            pauseTrack();
        }
    });

    // 5??雅뚯눊由?怨몄뵥 ??볦퍢 嚥≪뮇類??쎈꽅?귐? 獄쏄퉮毓?
    setInterval(function() {
        if (isPlaying && audio.currentTime > 0) {
            playerState.currentTime = audio.currentTime;
            saveStateToStorage();
        }
    }, 5000);

    window.addEventListener('beforeunload', saveStateToStorage);
    window.addEventListener('pagehide', saveStateToStorage);
    document.addEventListener('melonlite:page-ready', function () {
        const prompt = document.getElementById('guest-preview-prompt');
        if (prompt) prompt.remove();
        document.body.classList.remove('guest-preview-ended');
        syncPlayerUiFromAudio();
    });
    window.addEventListener('pageshow', function(event) {
        if (event.persisted) {
            syncPlayerUiFromAudio();
            saveStateToStorage();
        }
    });
    document.addEventListener('visibilitychange', function() {
        if (document.visibilityState === 'hidden') {
            saveStateToStorage();
        } else {
            syncPlayerUiFromAudio();
        }
    });

    // ??쀪쾿獄???????뽯선 (筌롫뗄??& ?類ㅼ삢 ??燁??臾믪삺)
    function getSeekClientX(event) {
        if (event && Number.isFinite(event.clientX)) return event.clientX;
        if (event && event.touches && event.touches[0] && Number.isFinite(event.touches[0].clientX)) {
            return event.touches[0].clientX;
        }
        if (event && event.changedTouches && event.changedTouches[0] && Number.isFinite(event.changedTouches[0].clientX)) {
            return event.changedTouches[0].clientX;
        }
        return null;
    }

    function seekAudio(e, wrapperElement) {
        const effectiveDuration = getEffectiveDuration();
        if (!effectiveDuration) return;
        const rect = wrapperElement.getBoundingClientRect();
        const clientX = getSeekClientX(e);
        if (clientX === null || rect.width <= 0) return;
        const clickX = Math.max(0, Math.min(rect.width, clientX - rect.left));
        const width = rect.width;
        const percentage = Math.max(0, Math.min(1, clickX / width));
        try {
            const requestedTime = percentage * effectiveDuration;
            const targetTime = guestPreviewMode ? Math.min(requestedTime, guestPreviewSeconds) : requestedTime;
            audio.currentTime = targetTime;
            playerState.currentTime = audio.currentTime;
            updateProgressUi(audio.currentTime, effectiveDuration);
            if (guestPreviewMode && requestedTime > guestPreviewSeconds) {
                showPlayerNotice(`비로그인 상태에서는 ${guestPreviewSeconds}초까지만 미리듣기할 수 있습니다.`, 'info');
            }
            updateMediaSessionPlaybackState();
            saveStateToStorage();
        } catch (err) {
            logPlayerIssue('Seek failed', err);
            showPlayerNotice('재생 위치를 변경하지 못했습니다. 잠시 후 다시 시도해 주세요.', 'error');
        }
    }

    function bindSeekBar(wrapperElement) {
        if (!wrapperElement || wrapperElement.__melonLiteSeekBound) return;
        wrapperElement.__melonLiteSeekBound = true;
        let dragging = false;
        const handleSeek = function(event) {
            if (event.cancelable) event.preventDefault();
            seekAudio(event, wrapperElement);
        };
        wrapperElement.addEventListener('pointerdown', function(event) {
            dragging = true;
            try { wrapperElement.setPointerCapture(event.pointerId); } catch (err) {}
            handleSeek(event);
        });
        wrapperElement.addEventListener('pointermove', function(event) {
            if (!dragging) return;
            handleSeek(event);
        });
        ['pointerup', 'pointercancel', 'pointerleave'].forEach(type => {
            wrapperElement.addEventListener(type, function(event) {
                if (!dragging) return;
                dragging = false;
                try { wrapperElement.releasePointerCapture(event.pointerId); } catch (err) {}
            });
        });
        wrapperElement.addEventListener('click', function(event) {
            if (event.detail === 0) return;
            seekAudio(event, wrapperElement);
        });
    }

    bindSeekBar(progressWrapper);
    bindSeekBar(expandProgressWrapper);

    // 癰귥눖??鈺곌퀣??
    if (volumeWrapper) {
        volumeWrapper.addEventListener('click', function(e) {
            const rect = volumeWrapper.getBoundingClientRect();
            const clickX = e.clientX - rect.left;
            const width = rect.width;
            let percentage = clickX / width;
            
            if (percentage < 0) percentage = 0;
            if (percentage > 1) percentage = 1;
            
            audio.volume = percentage;
            volumeBar.style.width = `${percentage * 100}%`;
            
            if (audio.muted) {
                audio.muted = false;
                btnMute.className = 'fas fa-volume-up volume-icon';
            }
            
            playerState.volume = percentage;
            updateVolumeIcon(percentage);
            saveStateToStorage();
        });
    }

    if (btnMute) {
        btnMute.addEventListener('click', function() {
            if (audio.muted) {
                audio.muted = false;
                volumeBar.style.width = `${playerState.volume * 100}%`;
                audio.volume = playerState.volume;
                updateVolumeIcon(playerState.volume);
            } else {
                audio.muted = true;
                volumeBar.style.width = '0%';
                btnMute.className = 'fas fa-volume-mute volume-icon';
            }
            playerState.muted = audio.muted === true;
            saveStateToStorage();
        });
    }

    function updateVolumeIcon(volume) {
        if (volume === 0) {
            btnMute.className = 'fas fa-volume-mute volume-icon';
        } else if (volume < 0.5) {
            btnMute.className = 'fas fa-volume-down volume-icon';
        } else {
            btnMute.className = 'fas fa-volume-up volume-icon';
        }
    }

    // ==========================================================================
    // 10. 筌뤴뫖以됭퉪?"?袁⑷퍥 ??源? 獄?"?酉逾???源? 甕곌쑵????源??獄쏅뗄???    // ==========================================================================
    const btnPlayAll = document.querySelector('.btn-play-all');
    const btnShuffleAll = document.querySelector('.btn-shuffle-all');
    const btnAddPageToQueue = document.querySelector('.btn-add-page-to-queue');

    if (btnPlayAll) {
        btnPlayAll.dataset.playerBound = '1';
        btnPlayAll.addEventListener('click', function() {
            const tracks = collectPageSongs();
            if (tracks.length > 0) {
                setQueueAndPlay(tracks, 0, false, { userNotice: true });
            } else {
                showPlayerNotice('재생할 음악 목록이 없습니다.', 'warning');
            }
        });
    }

    if (btnShuffleAll) {
        btnShuffleAll.dataset.playerBound = '1';
        btnShuffleAll.addEventListener('click', function() {
            const tracks = collectPageSongs();
            if (tracks.length > 0) {
                setQueueAndPlay(tracks, 0, true, { userNotice: true });
            } else {
                showPlayerNotice('재생할 음악 목록이 없습니다.', 'warning');
            }
        });
    }

    if (btnAddPageToQueue) {
        btnAddPageToQueue.dataset.playerBound = '1';
        btnAddPageToQueue.addEventListener('click', function() {
            const tracks = collectPageSongs();
            const addedCount = appendTracksToQueue(tracks);
            if (addedCount > 0) {
                showPlayerNotice(`${addedCount}곡을 현재 대기열에 추가했습니다.`, 'success');
            } else {
                showPlayerNotice('새로 추가할 곡이 없습니다.', 'info');
            }
        });
    }

    function bindPageActionButtons() {
        document.querySelectorAll('.btn-play-all').forEach(button => {
            if (button.dataset.playerBound === '1') return;
            button.dataset.playerBound = '1';
            button.addEventListener('click', function() {
                const tracks = collectPageSongs();
                if (tracks.length > 0) {
                    setQueueAndPlay(tracks, 0, false, { userNotice: true });
                } else {
                    showPlayerNotice('재생할 음악 목록이 없습니다.', 'warning');
                }
            });
        });

        document.querySelectorAll('.btn-shuffle-all').forEach(button => {
            if (button.dataset.playerBound === '1') return;
            button.dataset.playerBound = '1';
            button.addEventListener('click', function() {
                const tracks = collectPageSongs();
                if (tracks.length > 0) {
                    setQueueAndPlay(tracks, 0, true, { userNotice: true });
                } else {
                    showPlayerNotice('재생할 음악 목록이 없습니다.', 'warning');
                }
            });
        });

        document.querySelectorAll('.btn-add-page-to-queue').forEach(button => {
            if (button.dataset.playerBound === '1') return;
            button.dataset.playerBound = '1';
            button.addEventListener('click', function() {
                const addedCount = appendTracksToQueue(collectPageSongs());
                showPlayerNotice(addedCount > 0 ? `${addedCount}곡을 현재 대기열에 추가했습니다.` : '새로 추가할 곡이 없습니다.', addedCount > 0 ? 'success' : 'info');
            });
        });

        document.querySelectorAll('.btn-add-page-next').forEach(button => {
            if (button.dataset.playerBound === '1') return;
            button.dataset.playerBound = '1';
            button.addEventListener('click', function() {
                const addedCount = insertTracksNext(collectPageSongs());
                showPlayerNotice(addedCount > 0 ? `${addedCount}곡을 다음 재생 위치에 추가했습니다.` : '새로 추가할 곡이 없습니다.', addedCount > 0 ? 'success' : 'info');
            });
        });

        document.querySelectorAll('.btn-auto-mix').forEach(button => {
            if (button.dataset.playerBound === '1') return;
            button.dataset.playerBound = '1';
            button.addEventListener('click', async function() {
                const action = button.dataset.autoMixAction === 'append' ? 'append' : 'play';
                const limit = Math.max(10, Math.min(30, parseInt(button.dataset.limit || '24', 10) || 24));
                await runAutoMixAction(action, limit, button);
            });
        });
    }

    document.addEventListener('click', function(event) {
        const button = event.target && event.target.closest ? event.target.closest('.queue-auto-mix-btn') : null;
        if (!button) return;
        event.preventDefault();
        event.stopPropagation();
        runAutoMixAction('play', 24, button);
    });

    // 揶쏆뮆????筌뤴뫖以?????源?린袁る뱣 ?怨뺣짗 ?????    function bindPageListPlay() {
    function bindPageListPlay() {
        const songRows = document.querySelectorAll('.song-item');
        songRows.forEach((row, index) => {
            const playBtn = row.querySelector('.play-row-btn');
            if (playBtn) {
                // 疫꿸퀣???紐껋뵬???귐딅뮞??? ??덈뼄筌?筌왖?怨뚭탢????堉?怨뚮┛ ?袁る퉸 ??덉쨮 ??以??뤿연 ?대Ŋ猿?
                const newBtn = playBtn.cloneNode(true);
                playBtn.parentNode.replaceChild(newBtn, playBtn);
                
                newBtn.addEventListener('click', function(e) {
                    e.stopPropagation();
                    const tracks = collectPageSongs();
                    // ???????깆벥 ??ID
                    const songId = row.getAttribute('data-id');
                    const targetIdx = tracks.findIndex(t => t.id === songId);
                    
                    // ?袁⑹삺 ??源?筌뤴뫖以??袁⑷퍥????疫꿸퀣肉닸에??諛댄??醫뤾문???紐껊쑔??살쨮 ?怨쀯폒 ??뽰삂
                    setQueueAndPlay(tracks, targetIdx !== -1 ? targetIdx : 0, false, { userNotice: true });
                });
            }
            
            // ?ル뿭釉???醫? 甕곌쑵??獄쏅뗄????????            const likeBtn = row.querySelector('.like-btn');
            const likeBtn = row.querySelector('.like-btn');
            if (likeBtn) {
                const newLikeBtn = likeBtn.cloneNode(true);
                likeBtn.parentNode.replaceChild(newLikeBtn, likeBtn);
                newLikeBtn.addEventListener('click', function(e) {
                    e.stopPropagation();
                    const songId = row.getAttribute('data-id');
                    toggleLike(songId, index);
                });
            }

            const addPlaylistBtn = row.querySelector('.add-playlist-btn');
            if (addPlaylistBtn) {
                const newAddPlaylistBtn = addPlaylistBtn.cloneNode(true);
                addPlaylistBtn.parentNode.replaceChild(newAddPlaylistBtn, addPlaylistBtn);
                newAddPlaylistBtn.addEventListener('click', function(e) {
                    e.preventDefault();
                    e.stopPropagation();
                    const songId = row.getAttribute('data-id');
                    if (songId) openPlaylistModal(songId);
                });
            }

            const addNextBtn = row.querySelector('.add-next-btn');
            if (addNextBtn) {
                const newAddNextBtn = addNextBtn.cloneNode(true);
                addNextBtn.parentNode.replaceChild(newAddNextBtn, addNextBtn);
                newAddNextBtn.addEventListener('click', function(e) {
                    e.preventDefault();
                    e.stopPropagation();
                    const track = normalizeTrack({
                        id: row.getAttribute('data-id'),
                        title: row.getAttribute('data-title'),
                        artist: row.getAttribute('data-artist') || TEXT_FALLBACKS.artist,
                        album: row.getAttribute('data-album') || '',
                        duration: parseDurationValue(row.getAttribute('data-duration'))
                    });
                    const addedCount = track ? insertTracksNext([track]) : 0;
                    showPlayerNotice(addedCount > 0 ? '다음 재생 위치에 추가했습니다.' : '이미 대기열에 있는 곡입니다.', addedCount > 0 ? 'success' : 'info');
                });
            }
        });
    }

    async function postRecentHistoryAction(action, songId = null) {
        const formData = new FormData();
        formData.set('csrf_token', window.APP_CSRF_TOKEN || '');
        formData.set('action', action);
        if (songId !== null) formData.set('song_id', String(songId));

        const response = await fetch(`${window.APP_BASE_PATH || './'}api/recent_history.php`, {
            method: 'POST',
            body: formData,
            credentials: 'same-origin',
            cache: 'no-store',
            headers: {
                'X-Requested-With': 'XMLHttpRequest',
                'Accept': 'application/json'
            }
        });
        const payload = await response.json().catch(() => ({}));
        if (!response.ok || payload.success === false) {
            throw new Error(payload.message || '최근 재생 기록을 처리하지 못했습니다.');
        }
        return payload;
    }

    function syncRecentHistoryEmptyState(page) {
        if (!page) return;
        const rows = page.querySelectorAll('.song-item');
        const clearButton = page.querySelector('[data-recent-clear-all]');
        if (clearButton) clearButton.disabled = rows.length === 0;
        if (rows.length > 0 || page.querySelector('.recent-history-empty')) return;
        const container = page.querySelector('.song-list-container');
        if (container) {
            container.innerHTML = '<div class="recent-history-empty empty-list-state"><i class="fa-regular fa-clock"></i><p>최근 재생 기록이 없습니다.</p></div>';
        }
    }

    function bindRecentHistoryActions() {
        const page = document.querySelector('.recent-history-page');
        if (!page || page.dataset.recentHistoryBound === '1') return;
        page.dataset.recentHistoryBound = '1';

        page.addEventListener('click', async function (event) {
            const removeButton = event.target.closest('.recent-remove-btn');
            const clearButton = event.target.closest('[data-recent-clear-all]');
            if (!removeButton && !clearButton) return;

            event.preventDefault();
            try {
                if (removeButton) {
                    const row = removeButton.closest('.song-item');
                    const songId = removeButton.getAttribute('data-song-id') || (row && row.getAttribute('data-id'));
                    if (!songId) return;
                    removeButton.disabled = true;
                    await postRecentHistoryAction('delete_song', songId);
                    if (row) row.remove();
                    syncRecentHistoryEmptyState(page);
                    showPlayerNotice('최근 재생 기록에서 삭제했습니다.', 'success');
                    return;
                }

                if (clearButton) {
                    if (!window.confirm('최근 재생 기록을 모두 삭제할까요?')) return;
                    clearButton.disabled = true;
                    await postRecentHistoryAction('clear_all');
                    page.querySelectorAll('.song-item').forEach(row => row.remove());
                    syncRecentHistoryEmptyState(page);
                    showPlayerNotice('최근 재생 기록을 모두 삭제했습니다.', 'success');
                }
            } catch (err) {
                if (removeButton) removeButton.disabled = false;
                if (clearButton) clearButton.disabled = false;
                showPlayerNotice(err.message || '최근 재생 기록을 처리하지 못했습니다.', 'error');
            }
        });
        syncRecentHistoryEmptyState(page);
    }

    function updateHomeResumeCard() {
        const card = document.querySelector('[data-home-resume-card]');
        if (!card) return;
        const track = currentTrack();
        if (!appPlayerSettings.resumeEnabled || !track || !track.id) {
            card.hidden = true;
            return;
        }

        const duration = getEffectiveDuration(track.duration);
        const current = clampResumeTime(audio && Number.isFinite(audio.currentTime) ? audio.currentTime : playerState.currentTime, duration);
        const shouldShow = playerState.wasPlaying || current > 3 || playerState.queue.length > 0;
        if (!shouldShow) {
            card.hidden = true;
            return;
        }

        const cover = card.querySelector('[data-home-resume-cover]');
        const title = card.querySelector('[data-home-resume-title]');
        const meta = card.querySelector('[data-home-resume-meta]');
        const progress = card.querySelector('[data-home-resume-progress]');
        const time = card.querySelector('[data-home-resume-time]');
        if (cover) cover.src = getTrackCoverUrl(track);
        if (title) title.textContent = safeText(track.title, TEXT_FALLBACKS.title);
        if (meta) meta.textContent = `${safeText(track.artist, TEXT_FALLBACKS.artist)}${track.album ? ' · ' + safeText(track.album, '') : ''}`;
        if (progress) progress.style.width = duration > 0 ? `${Math.max(0, Math.min(100, (current / duration) * 100))}%` : '0%';
        if (time) time.textContent = duration > 0 ? `${formatTime(current)} / ${formatTime(duration)}` : formatTime(current);
        card.hidden = false;
    }

    function bindHomeResumeCard() {
        const card = document.querySelector('[data-home-resume-card]');
        if (!card) return;
        if (card.dataset.resumeBound !== '1') {
            card.dataset.resumeBound = '1';
            card.addEventListener('click', function (event) {
                const button = event.target.closest('[data-home-resume-play]');
                if (!button) return;
                event.preventDefault();
                togglePlay();
            });
        }
        updateHomeResumeCard();
    }

    window.MelonLiteRebindPlayerPage = function () {
        try {
            bindPageActionButtons();
            bindPageListPlay();
            bindRecentHistoryActions();
            bindHomeResumeCard();
            synchronizeActiveListUI(playerState.currentSongId);
        } catch (err) {
            logPlayerIssue('Player page rebind failed', err);
        }
    };

    window.AppPlayer = window.AppPlayer || {};
    window.AppPlayer.playTrackObject = function (track) {
        const normalized = normalizeTrack(track);
        if (!normalized || !normalized.id) return;
        const pageTracks = collectPageSongs();
        const pageIndex = Array.isArray(pageTracks)
            ? pageTracks.findIndex(item => String(item.id) === String(normalized.id))
            : -1;
        if (pageIndex >= 0 && pageTracks.length > 1) {
            setQueueAndPlay(pageTracks, pageIndex, false, { userNotice: true });
            return;
        }
        setQueueAndPlay([normalized], 0, false, { userNotice: true });
    };
    window.AppPlayer.appendTracks = function (tracks) {
        return appendTracksToQueue(Array.isArray(tracks) ? tracks : []);
    };
    window.AppPlayer.playTracks = function (tracks, options = {}) {
        const normalized = sanitizeQueue(Array.isArray(tracks) ? tracks : []);
        if (normalized.length === 0) return 0;
        setQueueAndPlay(normalized, 0, options.shuffle === true, { userNotice: options.userNotice !== false });
        return normalized.length;
    };
    window.AppPlayer.insertNext = function (tracks) {
        return insertTracksNext(Array.isArray(tracks) ? tracks : []);
    };
    window.AppPlayer.saveState = saveStateToStorage;
    window.AppPlayer.syncState = syncPlayerUiFromAudio;
    window.AppPlayer.currentTrack = currentTrack;
    window.AppPlayer.getState = function () {
        return Object.assign({}, playerState, {
            isPlaying: isPlaying || (audio && !audio.paused && !audio.ended),
            currentTime: audio && Number.isFinite(audio.currentTime) ? audio.currentTime : playerState.currentTime,
            duration: getEffectiveDuration()
        });
    };
    window.AppPlayer.refreshLyrics = function (songId, forceRefresh = true) {
        const targetId = songId || playerState.currentSongId;
        if (!targetId) return;
        lyricsCache.delete(String(targetId));
        loadLyrics(targetId, forceRefresh);
    };

    window.addEventListener('melonlite:lyrics-updated', function (event) {
        const detail = event && event.detail ? event.detail : {};
        const songId = parseInt(detail.song_id || detail.id || 0, 10);
        if (!songId) return;
        lyricsCache.delete(String(songId));
        if (detail.has_lyrics && typeof detail.lyrics === 'string') {
            lyricsCache.set(String(songId), {
                lyrics: detail.lyrics,
                has_lyrics: true,
                updated_at: detail.updated_at || ''
            });
        }
        if (String(songId) === String(playerState.currentSongId)) {
            loadLyrics(songId, true);
        }
    });

    // ==========================================================================
    // 11. 2筌?3筌???? API ?怨뺣짗
    // ==========================================================================
    function maybeIncrementPlayCount(songId) {
        if (guestPreviewMode) return;
        if (!appPlayerSettings.historyEnabled || countedSongIds.has(String(songId))) return;
        countedSongIds.add(String(songId));
        incrementPlayCount(songId);
    }

    function incrementPlayCount(songId) {
        const formData = new FormData();
        formData.append('song_id', songId);
        if (window.APP_CSRF_TOKEN) formData.append('csrf_token', window.APP_CSRF_TOKEN);
        
        fetch(`${window.APP_BASE_PATH}api/play_count.php`, {
            method: 'POST',
            headers: window.APP_CSRF_TOKEN ? { 'X-CSRF-Token': window.APP_CSRF_TOKEN } : {},
            body: formData
        })
        .then(res => res.json())
        .then(data => {
            if (data.success) {
                const countTd = document.querySelector(`.song-item[data-id="${songId}"] .play-count-td`);
                if (countTd) {
                    let currentCount = parseInt(countTd.innerText.replace(/,/g, '')) || 0;
                    countTd.innerText = (currentCount + 1).toLocaleString();
                }
            }
        })
        .catch(err => logPlayerIssue('Play count API failed', err));
    }

    function toggleLike(songId, index) {
        const formData = new FormData();
        formData.append('song_id', songId);
        if (window.APP_CSRF_TOKEN) formData.append('csrf_token', window.APP_CSRF_TOKEN);
        
        fetch(`${window.APP_BASE_PATH}api/toggle_like.php`, {
            method: 'POST',
            headers: window.APP_CSRF_TOKEN ? { 'X-CSRF-Token': window.APP_CSRF_TOKEN } : {},
            cache: 'no-store',
            body: formData
        })
        .then(res => res.json())
        .then(data => {
            if (data.success) {
                const liked = coerceBoolean(data.liked ?? (data.data && data.data.liked));
                setQueueLikedState(songId, liked);
                const songRows = getSongRowsById(songId);
                songRows.forEach(row => {
                    row.setAttribute('data-liked', liked ? '1' : '0');
                    const rowLikeBtn = row.querySelector('.like-btn');
                    if (rowLikeBtn) {
                        rowLikeBtn.classList.toggle('liked', liked);
                        rowLikeBtn.innerHTML = liked ? '<i class="fas fa-heart"></i>' : '<i class="far fa-heart"></i>';
                        rowLikeBtn.setAttribute('aria-pressed', liked ? 'true' : 'false');
                        rowLikeBtn.setAttribute('title', liked ? '좋아요 취소' : '좋아요');
                    }
                });

                updateLikeButtonUI(songId, liked);
                saveStateToStorage();
                
                if (window.location.pathname.includes('liked.php') && !liked) {
                    songRows.forEach(row => {
                        row.style.transition = 'opacity 0.3s ease';
                        row.style.opacity = '0';
                        setTimeout(() => {
                            row.remove();
                            // ??疫꿸퀣肉???녿┛????볤탢 筌ｌ꼶??
                            const queueIdx = playerState.queue.findIndex(t => String(t.id) === String(songId));
                            if (queueIdx !== -1) {
                                removeTrackFromQueue(queueIdx);
                            }
                        }, 300);
                    });
                }
            } else {
                showPlayerNotice("좋아요 반영 실패: " + (data.message || '요청을 처리하지 못했습니다.'), 'warning');
            }
        })
        .catch(err => logPlayerIssue('Like toggle API failed', err));
    }

    // ???쟿?????쎈뱜 筌뤴뫀????용┛
    function openPlaylistModal(songId) {
        if (!playlistModal || !modalSongId || !modalOptions) {
            showPlayerNotice('플레이리스트 창을 열 수 없습니다.', 'warning');
            return;
        }
        modalSongId.value = songId;
        playlistModal.style.display = 'flex';
        modalOptions.innerHTML = '<p class="loading-text">플레이리스트 목록을 불러오는 중...</p>';
        
        fetch(`${window.APP_BASE_PATH}api/get_playlists.php`, {
            headers: {
                'Accept': 'application/json',
                'Cache-Control': 'no-cache'
            },
            credentials: 'same-origin',
            cache: 'no-store'
        })
            .then(res => res.json())
            .then(data => {
                if (data.success) {
                    if (data.playlists.length === 0) {
                        modalOptions.innerHTML = '<p class="empty-text">보유한 플레이리스트가 없습니다.</p>';
                    } else {
                        let html = '';
                        data.playlists.forEach(pl => {
                            html += `
                                <div class="playlist-option-item" data-playlist-id="${pl.id}">
                                    <span class="playlist-option-name">${escapeHtml(pl.name)}</span>
                                    <span class="playlist-option-count">${pl.song_count}곡 수록</span>
                                </div>
                            `;
                        });
                        modalOptions.innerHTML = html;
                        
                        modalOptions.querySelectorAll('.playlist-option-item').forEach(item => {
                            item.addEventListener('click', function() {
                                const plId = this.getAttribute('data-playlist-id');
                                addTrackToPlaylist(plId, songId);
                            });
                        });
                    }
                } else {
                    modalOptions.innerHTML = '<p class="empty-text">조회 실패</p>';
                }
            })
            .catch(err => {
                logPlayerIssue('Playlist list load failed', err);
                modalOptions.innerHTML = '<p class="empty-text">오류 발생</p>';
            });
    }

    function closePlaylistModal() {
        playlistModal.style.display = 'none';
        modalSongId.value = '';
    }

    function addTrackToPlaylist(playlistId, songId) {
        const formData = new FormData();
        formData.append('playlist_id', playlistId);
        formData.append('song_id', songId);
        if (window.APP_CSRF_TOKEN) formData.append('csrf_token', window.APP_CSRF_TOKEN);
        
        fetch(`${window.APP_BASE_PATH}api/add_to_playlist.php`, {
            method: 'POST',
            headers: window.APP_CSRF_TOKEN ? { 'X-CSRF-Token': window.APP_CSRF_TOKEN } : {},
            body: formData
        })
        .then(res => res.json())
            .then(data => {
                if (data.success) {
                    showPlayerNotice('플레이리스트에 곡을 추가했습니다.', 'success');
                    closePlaylistModal();
                } else {
                    showPlayerNotice(data.message || '플레이리스트 추가에 실패했습니다.', 'warning');
                }
        })
        .catch(err => {
            logPlayerIssue('Add to playlist failed', err);
            showPlayerNotice('플레이리스트 추가 중 오류가 발생했습니다.', 'error');
        });
    }

    if (modalClose) modalClose.addEventListener('click', closePlaylistModal);
    window.addEventListener('click', function(e) {
        if (e.target === playlistModal) {
            closePlaylistModal();
        }
    });

    // ==========================================================================
    // 12. 筌뤴뫀而????뺤쨮???袁⑥쒔椰?筌롫뗀??獄쏅뗄???(3筌??꾨뗀諭??醫?)
    // ==========================================================================
    if (mobileHamburger && appSidebar && sidebarOverlay && mobileHamburger.dataset.sidebarBound !== '1') {
        mobileHamburger.addEventListener('click', function() {
            appSidebar.classList.add('open');
            sidebarOverlay.classList.add('open');
        });
        
        sidebarOverlay.addEventListener('click', function() {
            appSidebar.classList.remove('open');
            sidebarOverlay.classList.remove('open');
        });
        
        appSidebar.querySelectorAll('.nav-item').forEach(item => {
            item.addEventListener('click', function() {
                appSidebar.classList.remove('open');
                sidebarOverlay.classList.remove('open');
            });
        });
    }

    document.addEventListener('keydown', function(e) {
        if (!appPlayerSettings.keyboardShortcutsEnabled) return;
        const active = document.activeElement;
        const tagName = active && active.tagName ? active.tagName.toUpperCase() : '';
        if (['INPUT', 'TEXTAREA', 'SELECT'].includes(tagName) || (active && active.isContentEditable)) return;

        const key = e.key.toLowerCase();
        if (e.code === 'Space') {
            e.preventDefault();
            togglePlay();
        } else if (e.key === 'ArrowRight') {
            e.preventDefault();
            audio.currentTime = Math.min(Number.isFinite(audio.duration) ? audio.duration : audio.currentTime + 5, audio.currentTime + 5);
            updateMediaSessionPlaybackState();
        } else if (e.key === 'ArrowLeft') {
            e.preventDefault();
            audio.currentTime = Math.max(0, audio.currentTime - 5);
            updateMediaSessionPlaybackState();
        } else if (key === 'n') {
            playNext();
        } else if (key === 'p') {
            playPrev();
        } else if (key === 'l' && playerState.currentSongId) {
            toggleLike(playerState.currentSongId, playerState.currentIndex);
        } else if (key === 's') {
            toggleShuffle();
        } else if (key === 'r') {
            toggleRepeatMode();
        }
    });

    // HTML Escape Helper
    function escapeHtml(text) {
        if (!text) return '';
        return text
            .replace(/&/g, "&amp;")
            .replace(/</g, "&lt;")
            .replace(/>/g, "&gt;")
            .replace(/"/g, "&quot;")
            .replace(/'/g, "&#039;");
    }

    // Time Format Helper (seconds -> mm:ss)
    function formatTime(seconds) {
        if (isNaN(seconds) || seconds === Infinity) return '00:00';
        const mins = Math.floor(seconds / 60);
        const secs = Math.floor(seconds % 60);
        return `${mins.toString().padStart(2, '0')}:${secs.toString().padStart(2, '0')}`;
    }

    // ==========================================================================
    // 13. Run Core
    // ==========================================================================
    loadStateFromStorage();
    initPlayerUI();
    bindPageActionButtons();
    bindPageListPlay();
    bindRecentHistoryActions();
    bindHomeResumeCard();
});

