(function () {
    const formatTime = (sec) => {
        const s = Math.max(0, Number.isFinite(sec) ? sec : 0);
        const m = Math.floor(s / 60);
        const r = Math.floor(s % 60);
        return m + ':' + String(r).padStart(2, '0');
    };

	const fetchJson = async (url, nonce) => {
        const res = await fetch(url, {
            method: 'GET',
            headers: {
                'Accept': 'application/json',
                'X-WP-Nonce': nonce || ''
            }
        });
        if (!res.ok) {
            throw new Error('HTTP ' + res.status);
        }
        return await res.json();
    };



    const buildAudioGraph = (audioCtx, mediaElNode, variantCount, audioInfoEnabled) => {
        const channelCount = Math.max(2, variantCount * 2);
        const splitter = audioCtx.createChannelSplitter(channelCount);
        const master = audioCtx.createGain();
        master.gain.value = 1;

        try { mediaElNode.channelCountMode = 'explicit'; } catch (e) {}
        try { mediaElNode.channelInterpretation = 'discrete'; } catch (e) {}
        try { splitter.channelInterpretation = 'discrete'; } catch (e) {}
        try { master.channelInterpretation = 'discrete'; } catch (e) {}

        mediaElNode.connect(splitter);

        const variantNodes = [];
        for (let v = 0; v < variantCount; v++) {
            const merger = audioCtx.createChannelMerger(2);
            try { merger.channelInterpretation = 'discrete'; } catch (e) {}
            let analyser = null;
            if (audioInfoEnabled) {
                analyser = audioCtx.createAnalyser();
                analyser.fftSize = 2048;
                analyser.smoothingTimeConstant = 0.75;
            }
            const gain = audioCtx.createGain();
            gain.gain.value = (v === 0) ? 1 : 0;
            splitter.connect(merger, v * 2, 0);
            splitter.connect(merger, v * 2 + 1, 1);
            if (analyser) {
                merger.connect(analyser);
                analyser.connect(gain);
            } else {
                merger.connect(gain);
            }
            gain.connect(master);
            variantNodes.push({ merger, gain, analyser });
        }

        master.connect(audioCtx.destination);
        return { splitter, master, variantNodes };
    };

    const setActiveVariant = (variantNodes, idx) => {
        for (let i = 0; i < variantNodes.length; i++) {
            variantNodes[i].gain.gain.value = (i === idx) ? 1 : 0;
        }
    };

	// deprecated, will be deleted soon
	const renderVariants = (wrap, variants, onSelect) => {
		return;
	};

	// deprecated, will be deleted soon
	const renderPlaylist = (wrap, tracks, onSelect, fallbackCoverUrl) => {
		return;
	};

	const initPlayer = async (root) => {
		const playlistId = root.getAttribute('data-playlist-id');
		const cfg = window.thesplPlayer || {};
		const isProFlag = !!(window.THESPL_Player_Pro && typeof window.THESPL_Player_Pro.init === 'function');
        const attrDebug = root.getAttribute('data-debug');
        let adminDefaultDebug;
        if (attrDebug === '1') {
            adminDefaultDebug = true;
        } else if (attrDebug === '0') {
            adminDefaultDebug = false;
        } else {
            adminDefaultDebug = !!(cfg && cfg.debug);
        }
        let debugEnabled = adminDefaultDebug;
        let debugBox = null;
        let debugScrollAuto = true;
        let debugScrollListenerBound = false;
        const ensureDebugBox = function(){
            if (!debugBox) {
                debugBox = root.querySelector('.thespl-debug-log');
                if (!debugBox) {
                    debugWrap = root.querySelector('.thespl-debug-wrap');
                    if (!debugWrap) {
                        debugWrap = document.createElement('div');
                        debugWrap.className = 'thespl-debug-wrap';
                        debugWrap.style.cssText = 'position:relative; margin-top:8px;';
                        root.appendChild(debugWrap);
                    }
                    debugBox = document.createElement('pre');
                    debugBox.className = 'thespl-debug-log';
                    debugBox.style.cssText = 'max-height:150px; overflow:auto; font:12px/1.4 ui-monospace,monospace; background:#111; color:#8cf; padding:8px; border:1px solid #333; text-align:left;';
                    debugWrap.appendChild(debugBox);
                } else {
                    debugWrap = debugBox.parentNode;
                }
                try { ensureDebugCopyBtn(); } catch (e) {}
            }
            if (!debugScrollListenerBound && debugBox) {
                debugScrollListenerBound = true;
                try {
                    debugBox.addEventListener('scroll', function () {
                        try {
                            const el = debugBox;
                            if (!el) {
                                return;
                            }
                            const atBottom = el.scrollHeight - el.scrollTop - el.clientHeight <= 4;
                            debugScrollAuto = atBottom;
                        } catch (e) {}
                    });
                } catch (e) {}
            }
            return debugBox;
        };
        const ensureDebugCopyBtn = function(){
            if (!debugEnabled) return null;
            ensureDebugBox();
            if (!debugWrap) return null;
            if (debugCopyBtn) return debugCopyBtn;
            const btn = document.createElement('button');
            btn.type = 'button';
            btn.className = 'thespl-debug-copy';
            btn.textContent = 'Copy';
            btn.style.cssText = 'position:absolute; bottom:4px; right:4px; padding:4px 8px; font:12px system-ui,sans-serif; background:#333; color:#8cf; border:1px solid #555; border-radius:3px; cursor:pointer; z-index:10;';
            btn.addEventListener('click', () => {
                try {
                    const txt = debugBox ? (debugBox.textContent || '') : '';
                    if (navigator && navigator.clipboard && typeof navigator.clipboard.writeText === 'function') {
                        navigator.clipboard.writeText(txt).then(() => {
                            btn.textContent = 'Copied';
                            setTimeout(() => { btn.textContent = 'Copy'; }, 1000);
                        }).catch(() => {});
                    } else {
                        const ta = document.createElement('textarea');
                        ta.value = txt;
                        ta.style.position = 'fixed';
                        ta.style.opacity = '0';
                        document.body.appendChild(ta);
                        ta.focus();
                        ta.select();
                        try { document.execCommand('copy'); } catch (e) {}
                        document.body.removeChild(ta);
                        btn.textContent = 'Copied';
                        setTimeout(() => { btn.textContent = 'Copy'; }, 1000);
                    }
                } catch (e) {}
            });
            if (debugWrap) {
                debugWrap.appendChild(btn);
            }
            debugCopyBtn = btn;
            return btn;
        };
        let dbgHistoryLines = [];
        let dbgLinesMap = new Map();
        let debugCopyBtn = null;
        let debugWrap = null;
        const dbgRender = function(){
            if (!debugEnabled) return;
            try {
                ensureDebugBox();
                ensureDebugCopyBtn();
                const header = Array.from(dbgLinesMap.values());
                const tail = dbgHistoryLines.slice(Math.max(0, dbgHistoryLines.length - 200));
                const all = header.concat(tail);
                debugBox.textContent = all.join('\n');
                if (debugScrollAuto) {
                    try { debugBox.scrollTop = debugBox.scrollHeight; } catch (e) {}
                }
            } catch(e){}
        };
        const dbg = function(){
            if (!debugEnabled) return;
            try {
                if (window.console && console.log) {
                    console.log.apply(console, ['[THESPL]'].concat(Array.prototype.slice.call(arguments)));
                }
            } catch(e){}
            try {
                const msg = Array.prototype.map.call(arguments, function(x){ try { return (typeof x==='string')?x:JSON.stringify(x); } catch(e){ return String(x); } }).join(' ');
                dbgHistoryLines.push(msg);
                if (dbgHistoryLines.length > 1000) {
                    dbgHistoryLines = dbgHistoryLines.slice(-1000);
                }
                dbgRender();
            } catch(e){}
        };
        const dbgSetLine = function(key, text){
            if (!debugEnabled) return;
            const msg = String(text || '');
            dbgLinesMap.set(String(key||''), msg);
            dbgRender();
        };
        const dbgRemoveLine = function(key){
            if (!debugEnabled) return;
            dbgLinesMap.delete(String(key||''));
            dbgRender();
        };
        const restUrl = cfg.restUrl || '';
        const restNonce = cfg.restNonce || '';
        const defaultCoverUrl = cfg.defaultCoverUrl || '';
        let skipFetch = (!playlistId || !restUrl);
        const coreUiAttr = (root.getAttribute('data-thespl-core-ui') || '').toLowerCase();
        const coreUiEnabled = coreUiAttr !== 'off';
        const audioInfoEnabled = root.getAttribute('data-thespl-audio-info-enabled') !== '0';

		const headerTitle = root.querySelector('.thespl-player__title');
		const headerSub = root.querySelector('.thespl-player__subtitle');
        const variantsWrap = root.querySelector('.thespl-player__variants');
        const canvasWrap = root.querySelector('.thespl-player__canvas-wrap');
		const playlistWrap = root.querySelector('.thespl-player__playlist');
		const audioEl = root.querySelector('.thespl-player__audio');
		const btnPlay = root.querySelector('.thespl-player__btn--play');
		const btnPause = root.querySelector('.thespl-player__btn--pause');
		const btnStop = root.querySelector('.thespl-player__btn--stop');
		const controlsWrap = root.querySelector('.thespl-player__controls');
		const timeCur = root.querySelector('.thespl-player__time-current');
		const timeTot = root.querySelector('.thespl-player__time-total');
		const canvasEl = root.querySelector('.thespl-player__canvas');
		const audioInfoWrap = root.querySelector('.thespl-player__audio-info');
		const pichoRoot = root.querySelector('[data-thespl-meta="picho"]');
		const btnDebugToggle = null;
		const callUI = function(name, fallback) { try { var o = window.THESPL_Player_UI; var fn = o && o[name]; return typeof fn === 'function' ? fn : fallback; } catch(e) { return fallback; } };
		const callStore = function(name, fallback) { try { var o = window.THESPL_Player_Store; var fn = o && o[name]; return typeof fn === 'function' ? fn : fallback; } catch (e) { return fallback; } };
		const getTheme = function(){
			return {
				progress: 'rgba(34,211,238,0.9)',
				ringBase: '#222',
				waveform: 'rgba(90,90,90,0.65)',
				buffered: 'rgba(180,230,255,0.45)',
				knob: '#fff'
			};
		};

        let canvasCtx = null;
        let canvasSize = 0;
        let center = 0;
        let radius = 0;
        const ringWidth = 24;
        const knobRadius = 9;

        let coverImg = null;
        let coverReady = false;
        let coverBufferCanvas = null;

        let drawCurrentSec = 0;
        let drawDurationSec = 0;
        let drawProgress = 0;
        let targetProgress = 0;
        let drawBufferedProgress = 0;
        let targetBufferedProgress = 0;
        let rafId = null;
        let lastTimeTextUpdateMs = 0;

		let statusEl = null;
		let statusAnimId = null;
		let statusCssReady = false;
		// Core status UI is deprecated in the core. Visual status handling is
		// owned by templates via THESPL_Player_UI; these helpers remain only for
		// backward reference and will be removed soon.
		// deprecated, will be deleted soon
		const ensureStatusEl = () => {
			return null;
		};
		// deprecated, will be deleted soon
		const ensureStatusChildren = () => {
			return null;
		};
		// deprecated, will be deleted soon
		const ensureStatusCss = () => {
			return;
		};
		// deprecated, will be deleted soon
		const stopStatusAnim = () => {
			statusAnimId = null;
			return;
		};
		// deprecated, will be deleted soon
		const startStatusAnim = () => {
			return;
		};
		// deprecated, will be deleted soon
		const setStatusReady = () => {
			return;
		};
		const markTrackUnplayable = (idx, reasonKey) => {
			const i = Number.isFinite(idx) ? idx : currentTrackIndex;
			if (!Number.isFinite(i) || i < 0 || i >= tracks.length) {
				return;
			}
			try {
				unplayableTracks.add(i);
			} catch (e) {}
			try {
				callUI('setTrackPlayableState', () => {})(root, i, false, typeof reasonKey === 'string' ? reasonKey : '');
			} catch (e) {}
		};

		const findNextPlayableTrackIndex = (fromIdx) => {
			if (!tracks.length) {
				return -1;
			}
			const start = Number.isFinite(fromIdx) ? fromIdx : 0;
			for (let i = start + 1; i < tracks.length; i++) {
				if (!unplayableTracks.has(i)) {
					return i;
				}
			}
			return -1;
		};

		// deprecated, will be deleted soon
		const showStatusError = (msg, key) => {
			currentErrorMsg = msg;
            currentUiMessageKey = (typeof key === 'string') ? key : '';
			return;
		};
		// deprecated, will be deleted soon
		const hideStatus = () => {
			return;
		};

        const waveformsByTrack = new Map();
        let currentWaveforms = [];

		let playlist = null;
		if (!playlist && root && root.parentNode) {
			try {
				var sel = 'script.thespl-playlist-json[data-thespl-playlist="1"][data-playlist-id="' + String(playlistId) + '"]';
				var s = root.parentNode.querySelector(sel);
				if (s && s.textContent) {
					try { playlist = JSON.parse(s.textContent); } catch (e) {}
					if (playlist && typeof playlist === 'object') {
						skipFetch = true;
					}
				}
			} catch (e) {}
		}
		if (skipFetch && !playlist) {
                    playlist = { id: playlistId, title: '', variants: [], max_variants: 2, tracks: [] };
        }
		if (!skipFetch) {
            try {
                playlist = await fetchJson(restUrl.replace(/\/$/, '') + '/playlist/' + encodeURIComponent(playlistId), restNonce);
            } catch (e) {
                if (headerTitle) headerTitle.textContent = 'Player error';
                if (headerSub) headerSub.textContent = String(e && e.message ? e.message : e);
                let key = 'error-playlist-load';
                let msg = '';
                try {
                    const raw = String(e && e.message ? e.message : e || '');
                    const m = raw.match(/HTTP\s+(\d+)/);
                    const code = m ? parseInt(m[1], 10) : 0;
                    if (code === 404) {
                        key = 'error-playlist-not-found';
                    } else if (code === 401 || code === 403) {
                        key = 'error-auth';
                    }
                } catch (err) {}
                if (cfg.l10n && typeof cfg.l10n[key] === 'string' && cfg.l10n[key]) {
                    msg = cfg.l10n[key];
                }
                if (!msg) {
                    if (key === 'error-playlist-not-found') {
                        msg = 'Playlist not found.';
                    } else if (key === 'error-auth') {
                        msg = 'Authorization error. Please check your access or reload the page.';
                    } else {
                        msg = 'Unable to load playlist. Please try again later.';
                    }
                }
                showStatusError(msg, key);
                playlist = { id: playlistId, title: '', variants: [], max_variants: 2, tracks: [] };
            }
        }
		const playlistVariants = Array.isArray(playlist.variants) ? playlist.variants : [];
		let maxVariants = Number.isFinite(playlist.max_variants) ? playlist.max_variants : 0;
		if (!Number.isFinite(maxVariants) || maxVariants < 1) {
			maxVariants = Array.isArray(playlistVariants) && playlistVariants.length ? playlistVariants.length : 2;
		}
		dbg('playlist', { id: playlist && playlist.id, title: playlist && playlist.title, maxVariants });
		let currentVariantCount = Math.min(Math.max(1, playlistVariants.length || maxVariants), maxVariants);
		const tracks = Array.isArray(playlist.tracks) ? playlist.tracks : [];
		const customTitleAttr = root.getAttribute('data-thespl-title');
		if (headerTitle) headerTitle.textContent = (customTitleAttr && customTitleAttr.length ? customTitleAttr : (playlist.title || ''));
		if (headerSub) headerSub.textContent = tracks.length ? '' : 'No tracks';
        if (!tracks.length && !currentErrorMsg) {
            let msgNoTracks = '';
            const keyNoTracks = 'error-no-playable-tracks';
            if (cfg.l10n && typeof cfg.l10n[keyNoTracks] === 'string' && cfg.l10n[keyNoTracks]) {
                msgNoTracks = cfg.l10n[keyNoTracks];
            }
            if (!msgNoTracks) {
                msgNoTracks = 'No playable tracks are available in this playlist.';
            }
            showStatusError(msgNoTracks, keyNoTracks);
        }

        const scanTracksForAvailability = () => {
            try {
                let hasMedia404 = false;
                const limit = Math.min(tracks.length, 20);
                for (let i = 0; i < limit; i++) {
                    const t = tracks[i] || {};
                    const audio = t && t.audio ? t.audio : null;
                    const url = audio && typeof audio.stream_url === 'string' ? audio.stream_url : '';
                    if (!url) {
                        continue;
                    }
                    (async (trackIndex, trackUrl) => {
                        try {
                            const resp = await fetch(trackUrl, { method: 'HEAD', cache: 'no-cache' });
                            if (resp.status === 404) {
                                markTrackUnplayable(trackIndex, 'error-media-file-not-found');
                                hasMedia404 = true;
                                if (!currentErrorMsg) {
                                    let msg404 = '';
                                    const key404 = 'error-media-file-not-found-global';
                                    if (cfg.l10n && typeof cfg.l10n[key404] === 'string' && cfg.l10n[key404]) {
                                        msg404 = cfg.l10n[key404];
                                    }
                                    if (!msg404) {
                                        msg404 = 'Media file(s) not found (404)';
                                    }
                                    showStatusError(msg404, key404);
                                }
                            }
                        } catch (e) {}
                    })(i, url);
                }
            } catch (e) {}
        };

        scanTracksForAvailability();

        if (debugEnabled) { ensureDebugBox(); }

		let audioCtx = null;
		let graph = null;
		let mediaElNode = null;
		let graphVariantCount = 0;
		let currentTrackIndex = 0;
		let currentVariantIndex = 0;
        let userStopped = false;
        let gateToken = 0;
        let requestedPlay = false;
        let isPrebufferGating = false;
        let currentErrorMsg = null;
        let currentUiMessageKey = '';
        let unplayableTracks = new Set();

        const fadeSwitchMsRaw = Number.isFinite(cfg.fadeSwitchDurationMs) ? cfg.fadeSwitchDurationMs : 1;
        const fadeSwitchSec = Math.max(0, Math.min(3, (fadeSwitchMsRaw || 0) / 1000));
        const streamingCfg = (cfg && cfg.streaming) ? cfg.streaming : {};
        const prebufferTargetSec = Math.max(0, Math.min(30, (Number.isFinite(streamingCfg.prebufferSec) ? streamingCfg.prebufferSec : 0)));
        const autoDetectNetwork = !!(streamingCfg && streamingCfg.autoDetect);
        const continueBufferOnPause = !!(streamingCfg && streamingCfg.continueBufferOnPause);
        const prefetchNextOnAhead = !!(streamingCfg && streamingCfg.prefetchNextOnAhead);
        const enforceChunk = true;
        const prefetchThresholdSec = 10;
        const readyAheadSec = Math.max(1, Math.min(10, Number.isFinite(streamingCfg.readyAheadSec) ? streamingCfg.readyAheadSec : 1));

        dbgSetLine('streaming-cfg', 'chunkLevel=' + String(streamingCfg.chunkSizeLevel) + ' prebufferSec=' + String(prebufferTargetSec) + ' autoDetect=' + String(autoDetectNetwork) + ' continueOnPause=' + String(continueBufferOnPause) + ' prefetchNext=' + String(prefetchNextOnAhead) + ' readyAheadSec=' + String(readyAheadSec));
        
        let runtimeChunkLevel = Number.isFinite(streamingCfg.chunkSizeLevel) ? streamingCfg.chunkSizeLevel : 4;
        const minAutoLevel = 2;
        let emaBps = 0;
        let networkMonTimer = null;
        let networkMonUrl = '';
        let networkMonPeriodMs = 5000;
        let consecutiveLowRatioTicks = 0;
        let lastComputedRatio = 0;
        let waitingEventsTimes = [];
        let bootstrapLowStartActive = false;
        let bootstrapRampTicks = 0;
        let lastPrefetchBps = 0;
        let noProbeMode = false;
        let stableHighRatioTicks = 0;
        let lastTickTimeMs = 0;
        let lastAheadSec = 0;
        let prefetchProgressByUrl = new Map();
        let prefetchingUrls = new Set();
        const addParam = (url, key, val) => {
            try {
                const u = new URL(url, window.location.href);
                u.searchParams.set(String(key), String(val));
                return u.toString();
            } catch (e) {
                const sep = url.indexOf('?') === -1 ? '?' : '&';
                return url + sep + encodeURIComponent(String(key)) + '=' + encodeURIComponent(String(val));
            }
        };
		const chooseAutoChunkLevel = (bps) => {
            const levels = [128 * 1024, 256 * 1024, 512 * 1024, 768 * 1024, 1024 * 1024, 1536 * 1024, 2048 * 1024, 3072 * 1024, 4096 * 1024, 6144 * 1024];
            const highAhead = (lastAheadSec >= 10) && (countWaitingInWindow(8000) === 0);
            const targetMs = highAhead ? 1200 : 700;
            const overshoot = highAhead ? 1.8 : 1.3;
            const base = (bps > 0 ? bps : (128 * 1024));
            const budget = Math.max(1, Math.floor(base * (targetMs / 1000)));
            const allow = Math.floor(budget * overshoot);
            let idx = 0;
            for (let i = 0; i < levels.length; i++) {
                if (levels[i] <= allow) idx = i;
            }
            return idx;
        };

        const computeCeilChunkLevel = (bps) => {
            const b = Math.max(0, bps || 0);
            if (b <= 650 * 1024) return 3;      // ≤ ~5 Mbit/s → max 768 KiB
            if (b <= 900 * 1024) return 4;      // ≤ ~7 Mbit/s → max 1 MiB
            if (b <= 1400 * 1024) return 5;     // ≤ ~11 Mbit/s → max 1.5 MiB
            if (b <= 2200 * 1024) return 6;     // ≤ ~17 Mbit/s → max 2 MiB
            return 9;                            // darüber frei bis 6 MiB
        };

        const setChunkLevelCookie = (level) => {
            try {
                const lv = Math.max(0, Math.min(9, Number.isFinite(level) ? Math.floor(level) : 0));
                const maxAge = 120;
                document.cookie = 'thespl_chunk_level=' + String(lv) + ';path=/;max-age=' + String(maxAge);
            } catch (e) {}
        };

        const computeEffectiveBps = async (url) => {
            let hint = 0;
            try { hint = getNetworkHintBps(); } catch (e) {}
            let probe = 0;
            if (url) {
                try { probe = await probeDownloadRateBps(url); } catch (e) {}
            }
            const eff = Math.max(probe, Math.floor(hint * 0.5));
            return eff > 0 ? eff : 0;
        };

        const countWaitingInWindow = (ms) => {
            const now = performance.now();
            waitingEventsTimes = waitingEventsTimes.filter((t) => (now - t) <= ms);
            return waitingEventsTimes.length;
        };

        const permitLevelDown = () => {
            return (countWaitingInWindow(5000) >= 1) || (consecutiveLowRatioTicks >= 1) || (lastComputedRatio < 0.85);
        };

        const updateChunkLevelFromBps = (bps) => {
            let autoLevel = chooseAutoChunkLevel(bps);
            const prev = runtimeChunkLevel;
            if (Number.isFinite(prev)) {
                if (autoLevel > prev) {
                    const step = (bootstrapRampTicks > 0) ? 2 : 1;
                    autoLevel = Math.min(prev + step, autoLevel);
                    if (bootstrapRampTicks > 0) bootstrapRampTicks--;
                } else if (autoLevel < prev) {
                    if (permitLevelDown()) {
                        const downStep = (lastComputedRatio < 0.75 || countWaitingInWindow(8000) >= 2) ? 2 : 1;
                        autoLevel = Math.max(prev - downStep, autoLevel);
                    } else {
                        autoLevel = prev;
                    }
                }
            }
            const ceil = computeCeilChunkLevel(bps);
            const floor = (lastAheadSec >= 10 && countWaitingInWindow(8000) === 0) ? Math.max(minAutoLevel, 3) : minAutoLevel;
            runtimeChunkLevel = Math.max(floor, Math.min(ceil, autoLevel));
            setChunkLevelCookie(runtimeChunkLevel);
            dbgSetLine('auto-chunk', 'bps=' + String(emaBps) + ' level=' + String(runtimeChunkLevel));
        };

        const setNetworkPeriod = (ms) => {
            const p = Math.max(1500, Math.min(60000, Math.floor(ms || 5000)));
            if (p === networkMonPeriodMs) return;
            networkMonPeriodMs = p;
            if (networkMonTimer !== null) {
                try { window.clearInterval(networkMonTimer); } catch (e) {}
                networkMonTimer = null;
            }
            try { networkMonTimer = window.setInterval(() => { try { networkTick(false); } catch (e) {} }, networkMonPeriodMs); } catch (e) { networkMonTimer = null; }
            dbgSetLine('net-period', String(networkMonPeriodMs) + 'ms');
        };

        const networkTick = async (force) => {
            const cur = Number.isFinite(audioEl.currentTime) ? audioEl.currentTime : 0;
            const ahead = Math.max(0, getBufferedEndSec() - cur);
            const playing = !audioEl.paused && !audioEl.ended;
            if (!force && !playing && ahead >= 3) {
                return;
            }
            const t = tracks[currentTrackIndex] || null;
            const url = networkMonUrl || (t && t.audio && t.audio.stream_url ? t.audio.stream_url : '');
            if (!url) return;
            const nowMs = performance.now();
            const dtSec = lastTickTimeMs > 0 ? Math.max(0.001, (nowMs - lastTickTimeMs) / 1000) : 0;
            if (dtSec > 0) {
                const aheadDelta = ahead - lastAheadSec;
                const r = playing ? (1 + (aheadDelta / dtSec)) : Math.max(0, aheadDelta / dtSec);
                lastComputedRatio = Math.max(0, Math.min(2, r));
                dbgSetLine('ratio', lastComputedRatio.toFixed(2));
                if (countWaitingInWindow(8000) === 0 && lastComputedRatio >= 0.95) { stableHighRatioTicks++; } else { stableHighRatioTicks = 0; }
                if (lastComputedRatio < 0.85 || countWaitingInWindow(8000) >= 1) { noProbeMode = false; } else if (stableHighRatioTicks >= 3) { noProbeMode = true; }
                if (lastComputedRatio < 0.9 || countWaitingInWindow(8000) >= 1) {
                    consecutiveLowRatioTicks++;
                } else {
                    consecutiveLowRatioTicks = 0;
                }
                dbgSetLine('low-ratio', String(consecutiveLowRatioTicks));
                dbgSetLine('no-probe', noProbeMode ? '1' : '0');
            }
            lastTickTimeMs = nowMs;
            lastAheadSec = ahead;
            let bpsRaw = 0;
            const tCur = tracks[currentTrackIndex] || null;
            const pcmBps = computePcmUpperBoundBps(tCur);
            const aheadBased = (pcmBps > 0) ? Math.round(pcmBps * Math.max(0.5, Math.min(1.5, lastComputedRatio || 1))) : 0;
            if (aheadBased > 0) {
                bpsRaw = aheadBased;
            } else if (lastPrefetchBps > 0) {
                bpsRaw = lastPrefetchBps;
            } else {
                bpsRaw = getNetworkHintBps();
            }
            emaBps = emaBps > 0 ? Math.round(((lastComputedRatio < 0.85 || countWaitingInWindow(8000) >= 1) ? (emaBps * 0.4 + bpsRaw * 0.6) : (emaBps * 0.6 + bpsRaw * 0.4))) : bpsRaw;
            const targetPeriod = (bootstrapRampTicks > 0) ? 3000 : ((!playing && ahead >= 3) ? 15000 : ((lastComputedRatio < 0.85 || countWaitingInWindow(5000) >= 1) ? 1500 : 5000));
            setNetworkPeriod(targetPeriod);
            updateChunkLevelFromBps(emaBps);
        };

        const startNetworkMonitor = (url) => {
            networkMonUrl = url || networkMonUrl || '';
            if (networkMonTimer !== null) return;
            try { networkTick(true); } catch (e) {}
            setNetworkPeriod(networkMonPeriodMs);
            try {
                const nc = (navigator && (navigator.connection || navigator.webkitConnection || navigator.mozConnection)) || null;
                if (nc && typeof nc.addEventListener === 'function') {
                    const onChange = () => { try { networkTick(true); } catch (e) {} };
                    nc.addEventListener('change', onChange);
                    startNetworkMonitor._ncOnChange = onChange;
                }
            } catch (e) {}
            try {
                const onOnline = () => { try { networkTick(true); } catch (e) {} };
                const onOffline = () => { try { networkTick(true); } catch (e) {} };
                const onVis = () => { if (document.visibilityState === 'visible') { try { networkTick(true); } catch (e) {} } };
                window.addEventListener('online', onOnline);
                window.addEventListener('offline', onOffline);
                document.addEventListener('visibilitychange', onVis);
                startNetworkMonitor._onOnline = onOnline;
                startNetworkMonitor._onOffline = onOffline;
                startNetworkMonitor._onVis = onVis;
            } catch (e) {}
        };

        const stopNetworkMonitor = () => {
            if (networkMonTimer !== null) {
                try { window.clearInterval(networkMonTimer); } catch (e) {}
                networkMonTimer = null;
            }
            try {
                const nc = (navigator && (navigator.connection || navigator.webkitConnection || navigator.mozConnection)) || null;
                if (nc && startNetworkMonitor._ncOnChange && typeof nc.removeEventListener === 'function') {
                    nc.removeEventListener('change', startNetworkMonitor._ncOnChange);
                }
                startNetworkMonitor._ncOnChange = null;
            } catch (e) {}
            try {
                if (startNetworkMonitor._onOnline) { window.removeEventListener('online', startNetworkMonitor._onOnline); }
                if (startNetworkMonitor._onOffline) { window.removeEventListener('offline', startNetworkMonitor._onOffline); }
                if (startNetworkMonitor._onVis) { document.removeEventListener('visibilitychange', startNetworkMonitor._onVis); }
                startNetworkMonitor._onOnline = null;
                startNetworkMonitor._onOffline = null;
                startNetworkMonitor._onVis = null;
            } catch (e) {}
        };

        const getVariantLabelsForTrack = (track, count) => {
            const raw = (track && Array.isArray(track.variants) && track.variants.length) ? track.variants : playlistVariants;
            const labels = Array.isArray(raw) && raw.length ? raw : Array.from({ length: count }, (_, i) => 'Variant ' + (i + 1));
            const out = [];
            for (let i = 0; i < count; i++) {
                out.push(labels[i] || ('Variant ' + (i + 1)));
            }
            return out;
        };

		const setAudioInfoValue = (key, value) => {
			if (!audioInfoWrap) return;
			const el = audioInfoWrap.querySelector('[data-thespl-audio-info="' + key + '"]');
			if (!el) return;
			el.textContent = (value === null || typeof value === 'undefined') ? '' : String(value);
		};

		const updateStorePlaylist = () => {
			callStore('setPlaylist', function(){})(root, {
				tracks: tracks,
				currentTrackIndex: currentTrackIndex,
				currentVariantIndex: currentVariantIndex,
				maxVariants: maxVariants
			});
		};

		const updateStoreSelection = () => {
			callStore('setSelection', function(){})(root, {
				trackIndex: currentTrackIndex,
				variantIndex: currentVariantIndex
			});
		};

		const pichosync = (rootEl, pluginCfg) => {
			if (!rootEl || !pluginCfg || typeof pluginCfg !== 'object') {
				return;
			}
			const metaEl = rootEl.querySelector('[data-thespl-meta="picho"]');
			if (!metaEl) {
				return;
			}
			if (pluginCfg && pluginCfg.pro && pluginCfg.pro.hideBranding) {
				if (metaEl.parentNode) {
					metaEl.parentNode.removeChild(metaEl);
				}
				return;
			}
			const vEl = metaEl.querySelector('[data-thespl-meta="pv"]');
			const bEl = metaEl.querySelector('[data-thespl-meta="pb"]');
			const pvRaw = typeof pluginCfg.version === 'string' ? pluginCfg.version : '';
			const pbRaw = typeof pluginCfg.build === 'string' ? pluginCfg.build : '';
			if (vEl) {
				vEl.textContent = pvRaw ? String(pvRaw) : '';
			}
			if (bEl) {
				bEl.textContent = pbRaw ? ('build ' + String(pbRaw)) : '';
			}
		};

        const formatHz = (hz) => {
            const n = Number.isFinite(hz) ? hz : 0;
            if (!n) return '';
            if (n >= 1000) {
                const khz = n / 1000;
                const decimals = (Math.round(khz * 10) !== Math.round(khz) * 10) ? 1 : 0;
                return khz.toFixed(decimals) + ' kHz';
            }
            return String(Math.round(n)) + ' Hz';
        };

		const updateAudioInfo = (track) => {
			const t = track || tracks[currentTrackIndex] || null;
			const plugin = cfg && cfg.plugin && typeof cfg.plugin === 'object' ? cfg.plugin : {};
			pichosync(root, plugin);
			if (!audioInfoEnabled) {
				return;
			}
			const ch = t && t.audio && Number.isFinite(t.audio.channels) ? t.audio.channels : 0;
			const sr = t && t.audio && Number.isFinite(t.audio.sample_rate) ? t.audio.sample_rate : 0;
			const bps = t && t.audio && Number.isFinite(t.audio.bits_per_sample) ? t.audio.bits_per_sample : 0;

			const trackTitle = t && typeof t.title === 'string' ? t.title : '';
			const trackText = trackTitle || '';
			const pv = plugin && typeof plugin.version === 'string' ? plugin.version : '';
			const pc = plugin && typeof plugin.codename === 'string' ? plugin.codename : '';
			const pb = plugin && typeof plugin.build === 'string' ? plugin.build : '';
			const labels = getVariantLabelsForTrack(t, currentVariantCount);
			const label = labels[currentVariantIndex] || ('Variant ' + (currentVariantIndex + 1));
			const variantText = label;

			let channelsText = '';
			if (ch && ch >= 2 && ch % 2 === 0) {
				const pairs = ch / 2;
				channelsText = ch + ' (' + pairs + '×stereo)';
			} else {
				channelsText = ch ? String(ch) : '';
			}

			const meta = {
				plugin_version: pv ? ('v' + pv) : '',
				plugin_codename: pc ? pc : '',
				plugin_build: pb ? ('build ' + pb) : '',
				track: trackText,
				variant: variantText,
				sample_rate: formatHz(sr),
				bit_depth: bps ? (bps + '-bit') : '',
				channels: channelsText
			};

			callStore('setMeta', function(){ })(root, meta);
		};

        let audioInfoIntervalId = null;
        let audioInfoLevelRows = [];
        let peakHoldDbfs = [];
        let rmsSmoothDbfs = [];
        let lastRmsUpdateTimeMs = 0;

		const resetPeakHolds = (count) => {
			const n = Number.isFinite(count) ? count : currentVariantCount;
			const len = Math.max(0, n || 0);
			peakHoldDbfs = Array.from({ length: len }, () => -Infinity);
		};

		// deprecated, will be deleted soon - legacy core audio-info levels DOM fallback
		const ensureAudioInfoLevels = (track) => {
			return;
		};

        const formatDbfs = (db) => {
            if (!Number.isFinite(db)) return '-∞ dBFS';
            const v = Math.max(-200, Math.min(0, db));
            return v.toFixed(1) + ' dBFS';
        };

        const formatDbfsParts = (db) => {
            const s = formatDbfs(db);
            const i = s.indexOf(' ');
            if (i >= 0) return { num: s.slice(0, i), unit: s.slice(i + 1) };
            return { num: s, unit: '' };
        };

        const computeAnalyserLevels = (analyser, scratch) => {
            if (!analyser) {
                return { rmsDbfs: -Infinity, peakDbfs: -Infinity };
            }
            let buf = scratch;
            if (!buf || buf.length !== analyser.fftSize) {
                buf = new Float32Array(analyser.fftSize);
            }
            try {
                if (typeof analyser.getFloatTimeDomainData === 'function') {
                    analyser.getFloatTimeDomainData(buf);
                } else {
                    const bytes = new Uint8Array(analyser.fftSize);
                    analyser.getByteTimeDomainData(bytes);
                    for (let i = 0; i < bytes.length; i++) {
                        buf[i] = (bytes[i] - 128) / 128;
                    }
                }
            } catch (e) {
                return { rmsDbfs: -Infinity, peakDbfs: -Infinity };
            }

            let sum = 0;
            let peak = 0;
            for (let i = 0; i < buf.length; i++) {
                const x = buf[i];
                sum += x * x;
                const a = Math.abs(x);
                if (a > peak) peak = a;
            }
            const rms = Math.sqrt(sum / buf.length);
            const rmsDbfs = rms > 0 ? (20 * Math.log10(rms)) : -Infinity;
            const peakDbfs = peak > 0 ? (20 * Math.log10(peak)) : -Infinity;
            return { rmsDbfs, peakDbfs, scratch: buf };
        };

        const bufferedRangeForTime = (t) => {
            try {
                const b = audioEl.buffered;
                if (!b || b.length === 0) return null;
                for (let i = 0; i < b.length; i++) {
                    const start = b.start(i);
                    const end = b.end(i);
                    if (t >= start && t <= end) {
                        return { start, end };
                    }
                }
                let best = null;
                for (let i = 0; i < b.length; i++) {
                    const start = b.start(i);
                    const end = b.end(i);
                    if (!best || end > best.end) {
                        best = { start, end };
                    }
                }
                return best;
            } catch (e) {
                return null;
            }
        };

		let analyserScratch = [];
		const updateAudioInfoLive = () => {
			if (!audioInfoEnabled) {
				return;
			}
			try {
				const ctxText = audioCtx ? (audioCtx.state + ' / ' + Math.round(audioCtx.sampleRate) + ' Hz') : 'not initialized';
				let playback = '';
				if (audioEl.ended) {
					playback = 'ended';
				} else if (userStopped && audioEl.paused) {
					const ct = Number.isFinite(audioEl.currentTime) ? audioEl.currentTime : 0;
					playback = ct <= 0.001 ? 'stopped' : 'paused';
				} else {
					playback = audioEl.paused ? 'paused' : 'playing';
				}
				const cur = Number.isFinite(audioEl.currentTime) ? audioEl.currentTime : 0;
				const dur = getDisplayDurationSecForTrack(tracks[currentTrackIndex] || null);
				let ahead = 0;
				let pct = 0;
				const br = bufferedRangeForTime(cur);
				const bufEnd = br ? br.end : 0;
				ahead = Math.max(0, bufEnd - cur);
				pct = (dur > 0 && bufEnd > 0) ? Math.max(0, Math.min(1, bufEnd / dur)) : 0;
				const bufferText = (ahead ? ahead.toFixed(1) + 's' : '0.0s') + ' / ' + Math.round(pct * 100) + '%';
				const live = {
					audiocontext: ctxText,
					playback: playback,
					buffer: bufferText,
					bufferFraction: pct,
					aheadSec: ahead
				};
				try {
					callStore('setLive', function(){})(root, live);
				} catch (e) {}
				try {
					dbgSetLine('ahead', bufferText);
				} catch (e) {}
				const t = tracks[currentTrackIndex] || null;
				const labels = getVariantLabelsForTrack(t, currentVariantCount);
				const count = currentVariantCount;
				if (peakHoldDbfs.length !== count) {
					resetPeakHolds(count);
				}
				if (rmsSmoothDbfs.length !== count) {
					rmsSmoothDbfs = Array.from({ length: count }, () => -Infinity);
					lastRmsUpdateTimeMs = 0;
				}
				const nowMs = (typeof performance !== 'undefined' && performance && typeof performance.now === 'function') ? performance.now() : Date.now();
				const dtMs = lastRmsUpdateTimeMs > 0 ? Math.max(0, nowMs - lastRmsUpdateTimeMs) : 0;
				const tauMs = 450;
				const alpha = dtMs > 0 ? (1 - Math.exp(-dtMs / tauMs)) : 1;
				const levelsPayload = [];
				for (let i = 0; i < count; i++) {
					const node = graph && graph.variantNodes && graph.variantNodes[i] ? graph.variantNodes[i] : null;
					const analyser = node && node.analyser ? node.analyser : null;
					const prevScratch = analyserScratch[i] || null;
					const levels = computeAnalyserLevels(analyser, prevScratch);
					analyserScratch[i] = levels.scratch;
					const instRms = Number.isFinite(levels.rmsDbfs) ? levels.rmsDbfs : -Infinity;
					const prevRms = (i < rmsSmoothDbfs.length && Number.isFinite(rmsSmoothDbfs[i])) ? rmsSmoothDbfs[i] : instRms;
					const nextRms = prevRms + alpha * ((Number.isFinite(instRms) ? instRms : prevRms) - prevRms);
					if (i < rmsSmoothDbfs.length) {
						rmsSmoothDbfs[i] = nextRms;
					}
					const instPeak = Number.isFinite(levels.peakDbfs) ? levels.peakDbfs : -Infinity;
					const prevHold = (i < peakHoldDbfs.length && Number.isFinite(peakHoldDbfs[i])) ? peakHoldDbfs[i] : -Infinity;
					const nextHold = Math.max(prevHold, instPeak);
					if (i < peakHoldDbfs.length) {
						peakHoldDbfs[i] = nextHold;
					}
					const label = labels[i] || ('Variant ' + (i + 1));
					const rmsParts = formatDbfsParts(nextRms);
					const peakParts = formatDbfsParts(nextHold);
					levelsPayload.push({
						label: label,
						rms: rmsParts.num,
						peak: peakParts.num,
						peakDbfs: nextHold,
						active: i === currentVariantIndex ? 1 : 0
					});
				}
				lastRmsUpdateTimeMs = nowMs;
				try {
					callStore('setLevels', function(){})(root, levelsPayload);
				} catch (e) {}
			} catch (e) {}
		};

        const startAudioInfoLoop = () => {
            if (!audioInfoEnabled) return;
            if (!audioInfoWrap) return;
            if (audioInfoIntervalId !== null) return;
            updateAudioInfoLive();
            audioInfoIntervalId = window.setInterval(updateAudioInfoLive, 250);
        };

        const calcVariantCountForTrack = (track) => {
            const ch = track && track.audio && Number.isFinite(track.audio.channels) ? track.audio.channels : 0;
            let v = 0;
            if (Number.isFinite(ch) && ch >= 2) {
                v = Math.floor(ch / 2);
            }
            if (!v) {
                const labels = track && Array.isArray(track.variants) && track.variants.length ? track.variants : playlistVariants;
                v = labels.length ? labels.length : maxVariants;
            }
            return Math.min(Math.max(1, v), maxVariants);
        };

		// UI: Canvas-Ring-Setup und Zeichnen (Classic-Ring-Visualisierung)
		// deprecated, will be deleted soon
		const setupCanvas = () => {
			return;
		};

		// deprecated, will be deleted soon
		const drawCoverCircle = () => {
			return;
		};

		// deprecated, will be deleted soon
		const drawCenterTime = () => {
			return;
		};

		// deprecated, will be deleted soon
		const drawWaveform = (progress, wf) => {
			return;
		};

		// deprecated, will be deleted soon
		const renderFrame = () => {
			return;
		};

		// deprecated, will be deleted soon
		const setCoverFromTrack = (track) => {
			coverImg = null;
			coverReady = false;
			coverBufferCanvas = null;
			return;
		};

		let waveformReqId = 0;
		// deprecated, will be deleted soon
		const loadWaveformForTrack = async (track) => {
			currentWaveforms = [];
			return;
		};

        const ensureAudioContext = async () => {
            if (audioCtx) {
                if (audioCtx.state === 'suspended') {
                    await audioCtx.resume();
                }
                if (graph && graphVariantCount !== currentVariantCount) {
                    try {
                        mediaElNode.disconnect();
                    } catch (e) {
                    }
                    try {
                        graph.master.disconnect();
                    } catch (e) {
                    }
                    graph = buildAudioGraph(audioCtx, mediaElNode, currentVariantCount, audioInfoEnabled);
                    graphVariantCount = currentVariantCount;
                    dbg('graph-rebuilt', { currentVariantCount, audioCtxState: audioCtx.state, sampleRate: Math.round(audioCtx.sampleRate) });
                    selectVariant(Math.min(currentVariantIndex, currentVariantCount - 1));
                    ensureAudioInfoLevels(tracks[currentTrackIndex] || null);
                    updateAudioInfoLive();
                }
                return;
            }

            try {
                const AudioContextCtor = window.AudioContext || window.webkitAudioContext;
                audioCtx = new AudioContextCtor({ latencyHint: 'playback' });
            } catch (e) {
                try {
                    const AudioContextCtor = window.AudioContext || window.webkitAudioContext;
                    audioCtx = new AudioContextCtor();
                } catch (err) {
                    let msg = '';
                    const key = 'error-audio-context';
                    if (cfg.l10n && typeof cfg.l10n[key] === 'string' && cfg.l10n[key]) {
                        msg = cfg.l10n[key];
                    }
                    if (!msg) {
                        msg = 'Audio playback is not available in this browser.';
                    }
                    showStatusError(msg, key);
                    return;
                }
            }
            mediaElNode = audioCtx.createMediaElementSource(audioEl);
            graph = buildAudioGraph(audioCtx, mediaElNode, currentVariantCount, audioInfoEnabled);
            graphVariantCount = currentVariantCount;
            dbg('graph-init', { currentVariantCount, audioCtxState: audioCtx.state, sampleRate: Math.round(audioCtx.sampleRate) });
            if (audioInfoEnabled) {
                try {
                    const levels = [];
                    for (let i = 0; i < graph.variantNodes.length; i++) {
                        const analyser = graph.variantNodes[i] && graph.variantNodes[i].analyser ? graph.variantNodes[i].analyser : null;
                        const scratch = new Float32Array(analyser ? analyser.fftSize : 0);
                        const lv = computeAnalyserLevels(analyser, scratch);
                        levels.push({ rmsDbfs: lv.rmsDbfs, peakDbfs: lv.peakDbfs });
                    }
                    dbg('levels-initial', levels);
                } catch(e){}
            }
            ensureAudioInfoLevels(tracks[currentTrackIndex] || null);
            startAudioInfoLoop();
            updateAudioInfoLive();
        };

        const safeMasterGain = () => {
            if (graph && graph.master && graph.master.gain) {
                return graph.master.gain;
            }
            return null;
        };

        const rampMasterTo = (value, durSec) => {
            const g = safeMasterGain();
            if (!g || !audioCtx) return;
            const now = audioCtx.currentTime;
            const v = Math.max(0, Math.min(1, value));
            const d = Math.max(0, Number.isFinite(durSec) ? durSec : 0);
            try {
                g.cancelScheduledValues(now);
                g.setValueAtTime(g.value, now);
                g.linearRampToValueAtTime(v, now + d);
            } catch (e) {
                try {
                    g.value = v;
                } catch (err) {
                }
            }
        };

        const getBufferedEndSec = () => {
            try {
                const b = audioEl.buffered;
                if (!b || b.length === 0) return 0;
                let maxEnd = 0;
                for (let i = 0; i < b.length; i++) {
                    const end = b.end(i);
                    if (end > maxEnd) maxEnd = end;
                }
                return maxEnd;
            } catch (e) {
                return 0;
            }
        };

        

        const getDisplayDurationSecForTrack = (track) => {
            const dNative = audioEl.duration;
            if (dNative && Number.isFinite(dNative) && dNative > 0) return dNative;
            const t = track || tracks[currentTrackIndex] || null;
            const dMeta = (t && Number.isFinite(t.duration) && t.duration > 0) ? t.duration : 0;
            return dMeta;
        };

        const prefetchedIndices = new Set();
        const prefetchStreamUrl = async (url, byteLen) => {
            if (!url) return false;
            const target = Math.max(65536, Math.floor(byteLen || 262144));
            const now = performance.now();
            let sess = prefetchProgressByUrl.get(url);
            if (!sess) {
                sess = { got: 0, target: target, lastMs: 0, t0: now };
                prefetchProgressByUrl.set(url, sess);
            } else {
                sess.target = Math.max(sess.target, target);
            }
            if (prefetchingUrls.has(url)) return false;
            prefetchingUrls.add(url);
            let piece = 0;
            const start = sess.got;
            const end = sess.target - 1;
            const tReq = performance.now();
            try {
                const res = await fetch(url, { method: 'GET', headers: { 'Range': 'bytes=' + String(start) + '-' + String(end), 'Accept': 'application/octet-stream' } });
                if (!res || !res.ok) {
                    return false;
                }
                const cr = res.headers && res.headers.get ? res.headers.get('Content-Range') : '';
                if (cr && /bytes\s+(\d+)-(\d+)\//.test(cr)) {
                    const m = cr.match(/bytes\s+(\d+)-(\d+)\//);
                    const s = parseInt(m[1], 10) || 0;
                    const e = parseInt(m[2], 10) || 0;
                    piece = (e - s + 1);
                } else {
                    const cl = res.headers && res.headers.get ? res.headers.get('Content-Length') : null;
                    if (cl && /^\d+$/.test(cl)) {
                        piece = parseInt(cl, 10) || 0;
                    }
                }
                try {
                    const srvChunkSize = res.headers && res.headers.get ? res.headers.get('X-THESPL-Chunk-Size') : '';
                    const srvChunkLevel = res.headers && res.headers.get ? res.headers.get('X-THESPL-Chunk-Level') : '';
                    dbg('prefetch-piece', { start, end, piece, got: sess.got + piece, srvChunkSize, srvChunkLevel });
                    dbgSetLine('prefetch-last', 'plan=' + String(sess.target) + ' got=' + String(sess.got + piece) + ' piece=' + String(piece) + ' srvChunk=' + String(srvChunkSize) + ' srvLevel=' + String(srvChunkLevel));
                } catch (e) {}
                if (piece > 0) {
                    sess.got += piece;
                    sess.lastMs = performance.now();
                    const dtTot = Math.max(1, sess.lastMs - sess.t0);
                    const bpsTot = Math.round(sess.got / (dtTot / 1000));
                    if (bpsTot > 0) { lastPrefetchBps = bpsTot; dbgSetLine('prefetch-bps', String(bpsTot)); }
                } else {
                    return false;
                }
            } catch (e) {
                return false;
            } finally {
                prefetchingUrls.delete(url);
                prefetchProgressByUrl.set(url, sess);
            }
            return sess.got >= sess.target;
        };
        const maybePrefetchNext = async () => {
            if (!prefetchNextOnAhead) return;
            const cur = Number.isFinite(audioEl.currentTime) ? audioEl.currentTime : 0;
            const ahead = Math.max(0, getBufferedEndSec() - cur);
            if (ahead < prefetchThresholdSec) return;
            const nextIdx = currentTrackIndex + 1;
            if (!tracks[nextIdx] || prefetchedIndices.has(nextIdx)) return;
            const t = tracks[nextIdx];
            let url = t && t.audio && t.audio.stream_url ? t.audio.stream_url : '';
            if (!url) return;
            if (enforceChunk) {
                url = addParam(url, 'thespl_chunk_level', runtimeChunkLevel);
            }
            const levels = [128 * 1024, 256 * 1024, 512 * 1024, 768 * 1024, 1024 * 1024, 1536 * 1024, 2048 * 1024, 3072 * 1024, 4096 * 1024, 6144 * 1024];
            const clamped = Math.max(0, Math.min(levels.length - 1, runtimeChunkLevel));
            const baseBytes = levels[clamped];
            const measured = emaBps > 0 ? emaBps : (lastPrefetchBps > 0 ? lastPrefetchBps : getNetworkHintBps());
            const startTarget = computeDynamicPrebufferTargetSec(t, Math.max(4, prebufferTargetSec), measured);
            const targetBytes = Math.max(baseBytes, Math.floor(measured * Math.max(2, Math.min(12, startTarget))));
            const prefetchBytes = Math.max(baseBytes, Math.min(8 * 1024 * 1024, targetBytes));
            if (countWaitingInWindow(5000) >= 1) return;
            noProbeMode = true;
            const sess = prefetchProgressByUrl.get(url) || null;
            if (sess && sess.lastMs && (performance.now() - sess.lastMs) < 1200) return;
            dbgSetLine('prefetch-plan', 'level=' + String(clamped) + ' base=' + String(baseBytes) + ' startSec=' + String(startTarget) + ' measured=' + String(measured) + ' bytes=' + String(prefetchBytes));
            const factor = (prefetchBytes > 0 && baseBytes > 0) ? (prefetchBytes / baseBytes) : 0;
            const ok = await prefetchStreamUrl(url, prefetchBytes);
            if (ok) {
                prefetchedIndices.add(nextIdx);
                dbg('prefetch-next', { index: nextIdx, bytes: prefetchBytes, factor: Math.round(factor * 10) / 10, chunkLevel: clamped });
                try { prefetchProgressByUrl.delete(url); } catch (e) {}
            }
        };

        const probeDownloadRateBps = async (url) => {
            const sizes = [65536, 262144, 1048576];
            const samples = [];
            for (let i = 0; i < sizes.length; i++) {
                const len = sizes[i];
                try {
                    const t0 = performance.now();
                    const u = enforceChunk ? addParam(url, 'thespl_chunk_level', runtimeChunkLevel) : url;
                    const res = await fetch(u, { method: 'GET', headers: { 'Range': 'bytes=0-' + String(len - 1), 'Accept': 'application/octet-stream' } });
                    if (!res || !res.ok) {
                        continue;
                    }
                    try {
                        const srvChunkSize = res.headers && res.headers.get ? res.headers.get('X-THESPL-Chunk-Size') : '';
                        const srvChunkLevel = res.headers && res.headers.get ? res.headers.get('X-THESPL-Chunk-Level') : '';
                        const cr = res.headers && res.headers.get ? res.headers.get('Content-Range') : '';
                        const cl = res.headers && res.headers.get ? res.headers.get('Content-Length') : '';
                        dbgSetLine('server-x-thespl', 'level=' + String(srvChunkLevel || '') + ' size=' + String(srvChunkSize || '') + ' len=' + String(cl || '') + ' cr=' + String(cr || ''));
                    } catch (e) {}
                    const buf = await res.arrayBuffer();
                    const bytes = (buf && buf.byteLength) ? buf.byteLength : 0;
                    const dt = Math.max(1, performance.now() - t0);
                    const bps = Math.round((bytes / (dt / 1000)));
                    if (Number.isFinite(bps) && bps > 0) {
                        samples.push(bps);
                    }
                    try { dbgSetLine('last-probe', 'bytes=' + String(bytes) + ' dt=' + String(Math.round(dt)) + 'ms bps=' + String(bps)); } catch (e) {}
                } catch (e) {
                }
            }
            if (!samples.length) return 0;
            const use = samples.length >= 2 ? samples.slice(-2) : samples;
            use.sort(function(a,b){return a-b;});
            const mid = use[Math.floor(use.length / 2)] || use[0];
            return mid > 0 ? mid : 0;
        };

        const getNetworkHintBps = () => {
            let v = 0;
            try {
                const nc = (navigator && (navigator.connection || navigator.webkitConnection || navigator.mozConnection)) || null;
                const dl = nc && Number.isFinite(nc.downlink) ? nc.downlink : 0;
                if (dl && dl > 0) {
                    v = Math.floor((dl * 1024 * 1024) / 8);
                }
            } catch (e) {}
            return v > 0 ? v : 0;
        };

        const computePcmUpperBoundBps = (track) => {
            const sr = track && track.audio && Number.isFinite(track.audio.sample_rate) ? track.audio.sample_rate : 48000;
            const bps = track && track.audio && Number.isFinite(track.audio.bits_per_sample) ? track.audio.bits_per_sample : 24;
            const ch = track && track.audio && Number.isFinite(track.audio.channels) ? track.audio.channels : 2;
            const bytesPerSec = Math.max(1, Math.floor((sr * bps * ch) / 8));
            return bytesPerSec;
        };

        const computeDynamicPrebufferTargetSec = (track, userTargetSec, measuredBps) => {
            const pcmBps = computePcmUpperBoundBps(track);
            const r = measuredBps > 0 ? (measuredBps / pcmBps) : 0;
            let base = 5;
            if (r >= 1) {
                base = userTargetSec > 0 ? userTargetSec : 5;
            } else {
                const extra = Math.round((1 - Math.max(0, Math.min(1, r))) * 18);
                base = Math.max(12, 12 + extra);
            }
            return Math.max(1, Math.min(30, base));
        };

        

        const updateBufferedProgress = () => {
            const t = tracks[currentTrackIndex] || null;
            const d = getDisplayDurationSecForTrack(t);
            if (!d || !Number.isFinite(d)) {
                targetBufferedProgress = 0;
                return;
            }
            const end = getBufferedEndSec();
            const frac = (d > 0 && Number.isFinite(end)) ? Math.max(0, Math.min(1, end / d)) : 0;
            targetBufferedProgress = frac;
        };

		const updateUiControls = () => {
			try {
				if (btnPlay) {
					const playing = !audioEl.paused && !audioEl.ended;
					const cur = Number.isFinite(audioEl.currentTime) ? audioEl.currentTime : 0;
					const ahead = Math.max(0, getBufferedEndSec() - cur);
					const startTargetSec = Math.max(readyAheadSec, prebufferTargetSec);
					const buffering = !!(isPrebufferGating && requestedPlay);
					const ready = (audioEl.readyState >= 3) && (ahead >= startTargetSec);
					const state = buffering ? 'buffering' : (playing ? 'playing' : (ready ? 'ready' : 'idle'));
					btnPlay.setAttribute('data-state', state);
					btnPlay.setAttribute('aria-busy', buffering ? 'true' : 'false');
					btnPlay.title = buffering ? 'Puffert…' : (playing ? 'Spielt' : (ready ? 'Bereit' : 'Nicht bereit'));
					const paused = (!!audioEl.paused && !audioEl.ended && (cur > 0));
					const stopped = (!!audioEl.paused && ((cur === 0) || !!audioEl.ended));
					btnPlay.setAttribute('aria-pressed', playing ? 'true' : 'false');
					if (btnPause) btnPause.setAttribute('aria-pressed', paused ? 'true' : 'false');
					if (btnStop) btnStop.setAttribute('aria-pressed', stopped ? 'true' : 'false');
				}
				const el = ensureStatusEl();
				const rs = audioEl.readyState;
				const srcPresent = !!audioEl.src;
				const cur = Number.isFinite(audioEl.currentTime) ? audioEl.currentTime : 0;
				const ahead = Math.max(0, getBufferedEndSec() - cur);
				const playing = !audioEl.paused && !audioEl.ended;
				const paused = (!!audioEl.paused && !audioEl.ended && (cur > 0));
				const stopped = (!!audioEl.paused && userStopped && ((cur === 0) || !!audioEl.ended));
				const startTargetSec = Math.max(readyAheadSec, prebufferTargetSec);
				try {
					dbgSetLine('start-target', String(startTargetSec));
				} catch (e) {}
				const rebuffering = !!(playing && srcPresent && rs < 4 && ahead < 0.1);
				const buffering = isPrebufferGating || rebuffering || (!playing && ((srcPresent && rs < 3) || ((rs >= 3) && (ahead < startTargetSec))));
				let phase = '';
				if (currentErrorMsg) {
					phase = 'error';
				} else if (buffering) {
					phase = 'buffering';
				} else if (paused) {
					phase = 'paused';
				} else if (stopped) {
					phase = 'stopped';
				} else if (!playing && rs >= 3 && ahead >= startTargetSec) {
					phase = 'ready';
				} else if (playing) {
					phase = 'playing';
				} else {
					phase = 'idle';
				}
				try {
					var storeSetUiState = callStore('setUiState', null);
					if (typeof storeSetUiState === 'function') {
						storeSetUiState(root, {
							phase: phase,
							isBinaural: !!(root && root.getAttribute && root.getAttribute('data-binaural-status') === '1'),
							error: currentErrorMsg || '',
                            uiMessageKey: currentUiMessageKey || ''
						});
					}
				} catch (e) {}
				try {
					callUI('renderStatus', function(){ return; })(root);
				} catch (e) {}
				if (currentErrorMsg) {
					const elChildren = ensureStatusChildren();
					if (elChildren && coreUiEnabled) {
						elChildren.style.visibility = 'visible';
						if (statusEl._text) statusEl._text.textContent = currentErrorMsg;
						if (statusEl._dots) statusEl._dots.style.opacity = '0';
						stopStatusAnim();
					}
				} else if (buffering) {
					callUI('statusStart', startStatusAnim)(root);
				} else if (paused) {
					callUI('statusPaused', hideStatus)(root);
				} else if (stopped) {
					callUI('statusStopped', hideStatus)(root);
				} else if (!playing && rs >= 3 && ahead >= startTargetSec) {
					callUI('statusReady', setStatusReady)(root);
				} else {
					callUI('statusHide', hideStatus)(root);
				}
			} catch (e) {}
		};

		let lastRenderedVariantsKey = '';
		let lastBinauralOverlayTrackIndex = -1;
		let lastBinauralOverlayVariantIndex = -1;
		const updateBinauralFlag = () => {
			try {
				const track = tracks[currentTrackIndex];
				if (!track || !track.audio) {
					root.removeAttribute('data-binaural-status');
					callUI('setBinauralOverlay', () => {})(root, false);
					return;
				}
				const labels = getVariantLabelsForTrack(track, currentVariantCount);
				const label = (labels && labels[currentVariantIndex]) ? String(labels[currentVariantIndex]) : '';
				const low = label.toLowerCase();
				const keywords = [
					'dolby atmos binaural',
					'dolby binaural',
					'apple special',
					'apple spatial',
					'spatial audio',
					'3d-audio',
					'3d audio',
					'binaural',
					'atmos'
				];
				let isBinaural = false;
				for (let i = 0; i < keywords.length; i++) {
					if (low.indexOf(keywords[i]) !== -1) {
						isBinaural = true;
						break;
					}
				}
				let allowOverlay = false;
				const storeGetUiState = callStore('getUiState', null);
				if (typeof storeGetUiState === 'function') {
					try {
						const uiState = storeGetUiState(root) || {};
						let phase = '';
						if (typeof uiState.uiPhase === 'string' && uiState.uiPhase.length) {
							phase = uiState.uiPhase;
						} else if (typeof uiState.phase === 'string' && uiState.phase.length) {
							phase = uiState.phase;
						}
						if (phase === 'playing') {
							allowOverlay = true;
						}
					} catch (e) {}
				}
				if (isBinaural) {
					root.setAttribute('data-binaural-status', '1');
					callUI('setBinauralOverlay', () => {})(root, allowOverlay);
					if (allowOverlay && (currentTrackIndex !== lastBinauralOverlayTrackIndex || currentVariantIndex !== lastBinauralOverlayVariantIndex)) {
						lastBinauralOverlayTrackIndex = currentTrackIndex;
						lastBinauralOverlayVariantIndex = currentVariantIndex;
						try {
							callUI('showBinauralStatus', () => {})(root);
						} catch (e) {}
					}
				} else {
					root.removeAttribute('data-binaural-status');
					callUI('setBinauralOverlay', () => {})(root, false);
				}
			} catch (e) {}
		};
			const renderVariantsForTrack = (track) => {
				if (!variantsWrap) return;
				// Core UI gate for variants: When data-thespl-core-ui="off" and no external UI
				// implementation is present, core must not render variant controls. This preserves
				// template ownership and prevents accidental fallback elements from appearing.
				// The wrapper is cleared to avoid stale DOM when switching templates.
				const hasUiVariants = !!(window.THESPL_Player_UI && typeof window.THESPL_Player_UI.renderVariants === 'function');
				if (!coreUiEnabled && !hasUiVariants) {
					try { variantsWrap.innerHTML = ''; } catch (e) {}
					return;
				}
				const labels = getVariantLabelsForTrack(track, currentVariantCount);
				if (!currentVariantCount || currentVariantCount <= 1) {
					try { variantsWrap.innerHTML = ''; } catch (e) {}
					lastRenderedVariantsKey = '';
					updateBinauralFlag();
					return;
				}
				const key = String(currentVariantCount) + '|' + labels.join('\u0000');
				if (key !== lastRenderedVariantsKey) {
					// When core UI is disabled, skip the built-in variant renderer so templates
					// can fully control how variant buttons are rendered.
					const variantsRenderer = coreUiEnabled ? renderVariants : (wrap, list, onSelect) => {};
				callUI('renderVariants', variantsRenderer)(variantsWrap, labels, (idx) => {
					try {
						if (audioEl.ended) {
							audioEl.currentTime = 0;
						}
					} catch (e) {}
					requestedPlay = true;
					gateToken++;
					setVariantAndEnsurePlayback(idx);
					updateUiControls();
				});
				lastRenderedVariantsKey = key;
                dbg('variants-rendered', { labels, currentVariantCount });
			}
			const all = variantsWrap.querySelectorAll('.thespl-player__variant');
			all.forEach((b, i) => b.setAttribute('aria-selected', i === currentVariantIndex ? 'true' : 'false'));
			updateBinauralFlag();
		};

		const loadTrack = async (idx, opts) => {
			const t = tracks[idx];
			if (!t || !t.audio || !t.audio.stream_url) {
				return;
			}

            const shouldAutoplay = !!(opts && opts.autoplay);

            // Cancel any pending gated starts and normalize state for new track
            gateToken++;
            requestedPlay = shouldAutoplay;
            isPrebufferGating = false;
            currentErrorMsg = null;
            try { prefetchProgressByUrl.clear(); } catch (e) {}

			currentTrackIndex = idx;
			updateStorePlaylist();
			updateStoreSelection();

            const desiredVariantCount = calcVariantCountForTrack(t);
            if (desiredVariantCount !== currentVariantCount) {
                currentVariantCount = desiredVariantCount;
                currentVariantIndex = Math.max(0, Math.min(currentVariantIndex, currentVariantCount - 1));
                if (audioCtx) {
                    await ensureAudioContext();
                }
                dbg('variant-count', { channels: (t.audio && t.audio.channels) || 0, desiredVariantCount });
                dbg('track-meta', { sr: (t.audio && t.audio.sample_rate) || 0, bps: (t.audio && t.audio.bits_per_sample) || 0 });
            }

            renderVariantsForTrack(t);
            selectVariant(currentVariantIndex);
            resetPeakHolds(currentVariantCount);
            rmsSmoothDbfs = Array.from({ length: currentVariantCount }, () => -Infinity);
            lastRmsUpdateTimeMs = 0;
            if (audioCtx && fadeSwitchSec > 0) {
                try { rampMasterTo(0, fadeSwitchSec); dbg('track-fade-out', { durSec: fadeSwitchSec }); } catch (e) {}
            }
            let srcUrl = t.audio.stream_url;
            if (autoDetectNetwork) {
                bootstrapLowStartActive = true;
                bootstrapRampTicks = 3;
                runtimeChunkLevel = 0;
                noProbeMode = true;
                stableHighRatioTicks = 0;
            }
            if (autoDetectNetwork && srcUrl) {
                try {
                    const hint = getNetworkHintBps();
                    emaBps = emaBps > 0 ? Math.round(emaBps * 0.6 + hint * 0.4) : hint;
                    updateChunkLevelFromBps(emaBps);
                } catch (e) {}
            }
            try { setChunkLevelCookie(runtimeChunkLevel); } catch (e) {}
            audioEl.src = srcUrl;
            try { audioEl.preload = 'auto'; } catch (e) {}
            audioEl.load();
            dbg('track-load', { src: srcUrl });
            try { setChunkLevelCookie(runtimeChunkLevel); } catch (e) {}
            try { if (autoDetectNetwork) startNetworkMonitor(t && t.audio && t.audio.stream_url ? t.audio.stream_url : ''); } catch (e) {}
            updateUiControls();
            if (audioCtx && fadeSwitchSec > 0) {
                const onCanPlay = () => {
                    try { rampMasterTo(1, fadeSwitchSec); dbg('track-fade-in', { durSec: fadeSwitchSec }); } catch (e) {}
                };
                try { audioEl.addEventListener('canplay', onCanPlay, { once: true }); } catch (e) { audioEl.addEventListener('canplay', onCanPlay); }
            }

            if (coreUiEnabled) {
                setupCanvas();
                setCoverFromTrack(t);
                loadWaveformForTrack(t);
                if (canvasCtx && rafId === null) {
                    rafId = window.requestAnimationFrame(renderFrame);
                }
            }

            drawCurrentSec = 0;
            drawDurationSec = 0;
            drawProgress = 0;
            targetProgress = 0;
            drawBufferedProgress = 0;
            targetBufferedProgress = 0;

			if (shouldAutoplay) {
                try {
                    await ensureAudioContext();
                    selectVariant(currentVariantIndex);
                    await setVariantAndEnsurePlayback(currentVariantIndex);
                } catch (e) {
                }
            }

		if (isProFlag && window.THESPL_Player_Pro && typeof window.THESPL_Player_Pro.onTrackChanged === 'function') {
			window.THESPL_Player_Pro.onTrackChanged(root, playlist, t, currentTrackIndex);
		}

			updateAudioInfo(t);
			ensureAudioInfoLevels(t);
			startAudioInfoLoop();
			updateAudioInfoLive();
		};

        function crossfadeToVariant(nodes, idx, durSec) {
            if (!nodes || !nodes.length || !audioCtx) return;
            const now = audioCtx.currentTime;
            for (let i = 0; i < nodes.length; i++) {
                const param = nodes[i] && nodes[i].gain ? nodes[i].gain.gain : null;
                if (!param) continue;
                const target = (i === idx) ? 1 : 0;
                const d = Math.max(0, Number.isFinite(durSec) ? durSec : 0);
                try {
                    param.cancelScheduledValues(now);
                    param.setValueAtTime(param.value, now);
                    param.linearRampToValueAtTime(target, now + d);
                } catch (e) {
                    try { param.value = target; } catch (err) {}
                }
            }
        }

		function selectVariant(idx) {
			const clamped = Math.max(0, Math.min(currentVariantCount - 1, idx));
			currentVariantIndex = clamped;
			if (variantsWrap) {
				const all = variantsWrap.querySelectorAll('.thespl-player__variant');
				all.forEach((b, i) => b.setAttribute('aria-selected', i === clamped ? 'true' : 'false'));
			}
			if (graph) {
				if (audioCtx && fadeSwitchSec > 0) {
					crossfadeToVariant(graph.variantNodes, clamped, fadeSwitchSec);
					dbg('crossfade', { to: clamped, durSec: fadeSwitchSec });
				} else {
					setActiveVariant(graph.variantNodes, clamped);
					dbg('switch-hard', { to: clamped });
				}
			}
			updateStoreSelection();
			updateBinauralFlag();
			updateAudioInfo();
			updateAudioInfoLive();
		}

        async function setVariantAndEnsurePlayback(idx) {
            currentErrorMsg = null;
            currentUiMessageKey = '';
            const myToken = gateToken;
            await ensureAudioContext();
            selectVariant(idx);
            if (audioEl.paused) {
                try {
                    const baseTargetSec = Math.max(readyAheadSec, prebufferTargetSec);
                    let targetSec = baseTargetSec;
                    
                    if (!requestedPlay || myToken !== gateToken) {
                        isPrebufferGating = false;
                        updateUiControls();
                        return;
                    }
                    const shouldGate = targetSec > 0;
                    if (shouldGate) {
                        isPrebufferGating = true;
                        updateUiControls();
                        const t0 = performance.now();
                        const maxWaitMs = Math.max(targetSec * 1000 + 3000, 5000);
                        const waitOnce = () => new Promise((resolve) => {
                            let done = false;
                            let progressSeen = false;
                            const onProgress = () => { progressSeen = true; check(); };
                            const check = () => {
                                if (done) return;
                                const now = performance.now();
                                const ahead = Math.max(0, getBufferedEndSec() - (Number.isFinite(audioEl.currentTime) ? audioEl.currentTime : 0));
                                const rs = audioEl.readyState;
                                if (ahead >= targetSec || (now - t0) >= maxWaitMs) {
                                    done = true;
                                    cleanup();
                                    resolve();
                                    return;
                                }
                                if (rs >= 4 || (rs >= 3 && ahead >= targetSec) || myToken !== gateToken || !requestedPlay) {
                                    done = true;
                                    cleanup();
                                    resolve();
                                    return;
                                }
                            };
                            const tick = setInterval(check, 250);
                            const cleanup = () => {
                                try { audioEl.removeEventListener('progress', onProgress); } catch (e) {}
                                try { clearInterval(tick); } catch (e) {}
                            };
                            try { audioEl.addEventListener('progress', onProgress); } catch (e) { /* noop */ }
                            check();
                        });
                        dbg('prebuffer-wait', { targetSec, maxWaitMs });
                        await waitOnce();
                        isPrebufferGating = false;
                        updateUiControls();
                        dbg('prebuffer-done');
                    }
                    if (!requestedPlay || myToken !== gateToken) {
                        isPrebufferGating = false;
                        updateUiControls();
                        return;
                    }
                    if (fadeSwitchSec > 0) { try { rampMasterTo(1, fadeSwitchSec); dbg('play-fade-in', { durSec: fadeSwitchSec }); } catch (e) {} }
                    await audioEl.play();
                    updateUiControls();
                } catch (e) {
                    let key = '';
                    if (e && (e.name === 'NotAllowedError' || e.name === 'SecurityError')) {
                        key = 'error-autoplay-blocked';
                    } else {
                        key = 'error-internal';
                    }
                    let msg = '';
                    if (cfg.l10n && typeof cfg.l10n[key] === 'string' && cfg.l10n[key]) {
                        msg = cfg.l10n[key];
                    }
                    if (!msg) {
                        if (key === 'error-autoplay-blocked') {
                            msg = 'Autoplay was blocked by your browser.';
                        } else {
                            msg = 'Unexpected player error. Please try again.';
                        }
                    }
                    showStatusError(msg, key);
                    updateUiControls();
                }
            }
        }
		// Core UI gate for playlist: With data-thespl-core-ui="off" core must not render
		// the fallback playlist when no external UI exists. This ensures that templates
		// own the playlist layout and avoids unexpected DOM output from the core.
		if (coreUiEnabled) {
			callUI('renderPlaylist', renderPlaylist)(playlistWrap, tracks, (idx) => { loadTrack(idx, { autoplay: true }); }, defaultCoverUrl);
		} else {
			try {
				if (playlistWrap) {
					playlistWrap.innerHTML = '';
				}
			} catch (e) {}
		}

        if (tracks.length) {
            await loadTrack(0, { autoplay: false });
        }

        const playerId = String(root.getAttribute('data-playlist-id') || Math.random().toString(36).slice(2));

        if (!root.hasAttribute('tabindex')) {
            root.tabIndex = 0;
        }

        const safeFocusRoot = () => {
            try {
                root.focus({ preventScroll: true });
            } catch (e) {
            }
        };

        root.addEventListener('pointerdown', (e) => {
            try {
                if (e && e.pointerType === 'mouse') {
                    safeFocusRoot();
                }
            } catch (err) {}
            try {
                const G = window.THESPL_Player_Global || (window.THESPL_Player_Global = {});
                G.activeId = playerId;
            } catch (e) {}
        });

        const isTextInputTarget = (el) => {
            if (!el || el === document.body) return false;
            if (el.isContentEditable) return true;
            const tag = (el.tagName || '').toLowerCase();
            return tag === 'input' || tag === 'textarea' || tag === 'select';
        };

        const togglePlayPause = async () => {
            if (!audioEl.paused && !audioEl.ended) {
                userStopped = false;
                requestedPlay = false;
                gateToken++;
                audioEl.pause();
                updateAudioInfoLive();
                updateUiControls();
                return;
            }
            try { if (audioEl.ended) { audioEl.currentTime = 0; } } catch (err) {}
            userStopped = false;
            requestedPlay = true;
            gateToken++;
            await setVariantAndEnsurePlayback(currentVariantIndex);
        };

        // Root no longer handles Space; global controller below handles Space universally

        btnPlay.addEventListener('click', async () => {
            userStopped = false;
            requestedPlay = true;
            gateToken++;
            updateUiControls();
            await setVariantAndEnsurePlayback(currentVariantIndex);
            try {
                callUI('statusHide', hideStatus)(root);
            } catch (e) {}
        });
        btnPause.addEventListener('click', () => {
            userStopped = false;
            if (audioCtx && fadeSwitchSec > 0) { try { rampMasterTo(0, fadeSwitchSec); dbg('pause-fade-out', { durSec: fadeSwitchSec }); } catch (e) {} }
            requestedPlay = false;
            gateToken++;
            audioEl.pause();
            updateAudioInfoLive();
            updateUiControls();
        });
        btnStop.addEventListener('click', () => {
            userStopped = true;
            if (audioCtx && fadeSwitchSec > 0) { try { rampMasterTo(0, fadeSwitchSec); dbg('stop-fade-out', { durSec: fadeSwitchSec }); } catch (e) {} }
            requestedPlay = false;
            gateToken++;
            audioEl.pause();
            audioEl.currentTime = 0;
            drawCurrentSec = 0;
            targetProgress = 0;
            targetBufferedProgress = 0;
            resetPeakHolds(currentVariantCount);
            rmsSmoothDbfs = Array.from({ length: currentVariantCount }, () => -Infinity);
            lastRmsUpdateTimeMs = 0;
            updateAudioInfoLive();
            updateUiControls();
        });

        if (coreUiEnabled && canvasEl && !canvasCtx) {
            setupCanvas();
        }
        if (coreUiEnabled && canvasCtx && rafId === null) {
            rafId = window.requestAnimationFrame(renderFrame);
        }

        const clamp01 = (v) => Math.max(0, Math.min(1, v));
        const clientToCanvasXY = (clientX, clientY) => {
            if (!canvasEl) return null;
            const rect = canvasEl.getBoundingClientRect();
            const w = rect.width || 0;
            const h = rect.height || 0;
            if (!w || !h) return null;
            const scaleX = (canvasEl.width || w) / w;
            const scaleY = (canvasEl.height || h) / h;
            return {
                x: (clientX - rect.left) * scaleX,
                y: (clientY - rect.top) * scaleY,
                scale: (scaleX + scaleY) / 2,
            };
        };
        const progressFromClientXYLoose = (clientX, clientY) => {
            if (!canvasEl || !canvasCtx) return null;
            const pt = clientToCanvasXY(clientX, clientY);
            if (!pt) return null;
            const x = pt.x - center;
            const y = pt.y - center;
            const dist = Math.sqrt(x * x + y * y);
            if (!Number.isFinite(dist) || dist < 6) return null;
            let angle = Math.atan2(y, x) + (Math.PI / 2);
            if (angle < 0) angle += Math.PI * 2;
            return clamp01(angle / (Math.PI * 2));
        };
        const progressFromClientXYStrict = (clientX, clientY) => {
            if (!canvasEl || !canvasCtx) return null;
            const pt = clientToCanvasXY(clientX, clientY);
            if (!pt) return null;
            const x = pt.x - center;
            const y = pt.y - center;
            const dist = Math.sqrt(x * x + y * y);
            if (!Number.isFinite(dist) || dist < 6) return null;
            if (dist < (radius - ringWidth * 1.8) || dist > (radius + ringWidth * 1.8)) return null;
            let angle = Math.atan2(y, x) + (Math.PI / 2);
            if (angle < 0) angle += Math.PI * 2;
            return clamp01(angle / (Math.PI * 2));
        };

        const applyPreviewProgress = (progress) => {
            const d = audioEl.duration;
            if (!d || !Number.isFinite(d)) return;
            const p = clamp01(progress);
            const tgt = p * d;
            drawCurrentSec = tgt;
            drawDurationSec = d;
            targetProgress = p;
            if (timeCur) {
                const nowMs = performance.now();
                if (!isDragging || (nowMs - lastTimeTextUpdateMs) > 80) {
                    timeCur.textContent = formatTime(tgt);
                    lastTimeTextUpdateMs = nowMs;
                }
            }
        };

        const commitSeekFromProgress = async (progress) => {
            const d = audioEl.duration;
            if (!d || !Number.isFinite(d)) return;
            const p = clamp01(progress);
            const tgt = p * d;
            if (audioCtx) {
                rampMasterTo(0, 0.02);
            }
            const onSeeked = () => {
                audioEl.removeEventListener('seeked', onSeeked);
                if (audioCtx) {
                    rampMasterTo(1, 0.05);
                }
            };
            audioEl.addEventListener('seeked', onSeeked);
            try {
                audioEl.currentTime = tgt;
            } catch (e) {
                audioEl.removeEventListener('seeked', onSeeked);
                if (audioCtx) {
                    rampMasterTo(1, 0.05);
                }
            }
            applyPreviewProgress(p);
        };

        let isDragging = false;
        let dragProgress = 0;
        let touchPreventScrollActive = false;
        let lastScrubUpTimeMs = 0;
        // UI: Interaktion für Canvas-Ring (Scrubbing/Seek über Canvas)
        if (coreUiEnabled && canvasEl) {
            canvasEl.addEventListener('touchstart', (e) => {
                const t = (e.touches && e.touches[0]) ? e.touches[0] : null;
                if (!t) return;
                setupCanvas();
                const pt = clientToCanvasXY(t.clientX, t.clientY);
                if (!pt) return;
                const currentP = clamp01(isDragging ? dragProgress : targetProgress);
                const knobAngle = (-Math.PI / 2) + currentP * Math.PI * 2;
                const knobX = center + Math.cos(knobAngle) * radius;
                const knobY = center + Math.sin(knobAngle) * radius;
                const dx = pt.x - knobX;
                const dy = pt.y - knobY;
                const knobHitRadius = Math.max(knobRadius * 2.6, 50 * pt.scale);
                if ((dx * dx + dy * dy) <= (knobHitRadius * knobHitRadius)) {
                    touchPreventScrollActive = true;
                    try { e.preventDefault(); } catch (err) {}
                } else {
                    touchPreventScrollActive = false;
                }
            }, { passive: false });
            canvasEl.addEventListener('touchmove', (e) => {
                if (touchPreventScrollActive) {
                    try { e.preventDefault(); } catch (err) {}
                }
            }, { passive: false });
            canvasEl.addEventListener('touchend', () => { touchPreventScrollActive = false; }, { passive: false });
            canvasEl.addEventListener('touchcancel', () => { touchPreventScrollActive = false; }, { passive: false });
            canvasEl.addEventListener('pointerdown', (e) => {
                setupCanvas();
                const pt = clientToCanvasXY(e.clientX, e.clientY);
                if (!pt) return;

                const currentP = clamp01(isDragging ? dragProgress : targetProgress);
                const knobAngle = (-Math.PI / 2) + currentP * Math.PI * 2;
                const knobX = center + Math.cos(knobAngle) * radius;
                const knobY = center + Math.sin(knobAngle) * radius;
                const dx = pt.x - knobX;
                const dy = pt.y - knobY;
                const knobHitRadius = Math.max(knobRadius * 2.6, 50 * pt.scale);

                if (e.pointerType !== 'mouse' && (dx * dx + dy * dy) > (knobHitRadius * knobHitRadius)) {
                    return;
                }

                const p = (e.pointerType === 'mouse') ? progressFromClientXYStrict(e.clientX, e.clientY) : progressFromClientXYLoose(e.clientX, e.clientY);
                if (p === null) return;
                isDragging = true;
                dragProgress = p;
                try {
                    canvasEl.setPointerCapture(e.pointerId);
                } catch (err) {
                }
                ensureAudioContext().then(() => {
                    rampMasterTo(0.2, 0.02);
                }).catch(() => {
                });
                applyPreviewProgress(p);
            });
            canvasEl.addEventListener('pointermove', (e) => {
                if (!isDragging) return;
                const p = progressFromClientXYLoose(e.clientX, e.clientY);
                if (p === null) return;
                dragProgress = p;
                applyPreviewProgress(p);
            });
            const stopScrub = (e) => {
                if (!isDragging) return;
                isDragging = false;
                try {
                    canvasEl.releasePointerCapture(e.pointerId);
                } catch (err) {
                }
                commitSeekFromProgress(dragProgress);
                userStopped = false;
                requestedPlay = true;
                gateToken++;
                updateUiControls();
                setVariantAndEnsurePlayback(currentVariantIndex);
                try { lastScrubUpTimeMs = performance.now(); } catch (err) { lastScrubUpTimeMs = Date.now(); }
            };
            canvasEl.addEventListener('pointerup', stopScrub);
            canvasEl.addEventListener('pointercancel', stopScrub);
            canvasEl.addEventListener('lostpointercapture', stopScrub);
            canvasEl.addEventListener('click', async (e) => {
                if (isDragging) return;
                const nowMs = (typeof performance !== 'undefined' && performance && typeof performance.now === 'function') ? performance.now() : Date.now();
                if (nowMs - lastScrubUpTimeMs < 350) return;
                setupCanvas();
                const pt = clientToCanvasXY(e.clientX, e.clientY);
                if (!pt) return;
                const currentP = clamp01(targetProgress);
                const knobAngle = (-Math.PI / 2) + currentP * Math.PI * 2;
                const knobX = center + Math.cos(knobAngle) * radius;
                const knobY = center + Math.sin(knobAngle) * radius;
                const dx = pt.x - knobX;
                const dy = pt.y - knobY;
                const knobHitRadius = Math.max(knobRadius * 2.6, 50 * pt.scale);
                if ((dx * dx + dy * dy) <= (knobHitRadius * knobHitRadius)) {
                    return;
                }
                const x = pt.x - center;
                const y = pt.y - center;
                const dist = Math.sqrt(x * x + y * y);
                const innerLimit = radius - ringWidth * 1.8;
                const outerLimit = radius + ringWidth * 1.8;
                if (dist >= innerLimit && dist <= outerLimit) {
                    const p = progressFromClientXYStrict(e.clientX, e.clientY);
                    if (p === null) return;
                    commitSeekFromProgress(p);
                    if (audioEl.paused || audioEl.ended) {
                        try { if (audioEl.ended) { audioEl.currentTime = 0; } } catch (err) {}
                        userStopped = false;
                        requestedPlay = true;
                        gateToken++;
                        await setVariantAndEnsurePlayback(currentVariantIndex);
                    }
                    return;
                }
                if (dist < innerLimit) {
                    const playing = (!audioEl.paused && !audioEl.ended);
                    if (playing) {
                        userStopped = false;
                        requestedPlay = false;
                        gateToken++;
                        audioEl.pause();
                        updateAudioInfoLive();
                        updateUiControls();
                        return;
                    }
                    try { if (audioEl.ended) { audioEl.currentTime = 0; } } catch (err) {}
                    userStopped = false;
                    requestedPlay = true;
                    gateToken++;
                    await setVariantAndEnsurePlayback(currentVariantIndex);
                    return;
                }
            });
        }

        audioEl.addEventListener('loadedmetadata', () => {
            if (timeTot && audioEl.duration && Number.isFinite(audioEl.duration)) {
                timeTot.textContent = formatTime(audioEl.duration);
            }
            if (audioEl.duration && Number.isFinite(audioEl.duration)) {
                drawDurationSec = audioEl.duration;
            }
            updateBufferedProgress();
            if (!audioCtx) {
                ensureAudioContext().catch(() => {});
            }
            updateUiControls();
        });
        audioEl.addEventListener('progress', () => {
            updateBufferedProgress();
            updateAudioInfoLive();
            updateUiControls();
            if (!audioEl.paused || continueBufferOnPause) { maybePrefetchNext(); }
        });
        audioEl.addEventListener('timeupdate', () => {
            if (isDragging) return;
            const d = audioEl.duration;
            const t = audioEl.currentTime;
            if (timeCur) timeCur.textContent = formatTime(t);
            drawCurrentSec = t;
            if (d && Number.isFinite(d)) {
                drawDurationSec = d;
                targetProgress = d > 0 ? t / d : 0;
            } else {
                targetProgress = 0;
            }
            updateBufferedProgress();
            updateAudioInfoLive();
            updateUiControls();
            if (!audioEl.paused || continueBufferOnPause) { maybePrefetchNext(); }
        });

        audioEl.addEventListener('play', () => {
            userStopped = false;
            updateAudioInfoLive();
            updateUiControls();
            try {
                const G = window.THESPL_Player_Global || (window.THESPL_Player_Global = {});
                if (!G.players) G.players = new Map();
                G.activeId = playerId;
                for (const [id, p] of G.players) {
                    if (id === playerId) continue;
                    try {
                        const el = p && p.audioEl ? p.audioEl : null;
                        if (el && !el.paused && !el.ended) {
                            if (typeof p.pause === 'function') { p.pause(); } else { el.pause(); }
                        }
                    } catch (err) {}
                }
            } catch (e) {}
        });
        audioEl.addEventListener('pause', () => {
            requestedPlay = false;
            gateToken++;
            updateAudioInfoLive();
            updateUiControls();
        });
        
        audioEl.addEventListener('stalled', () => { updateAudioInfoLive(); updateUiControls(); });
        
        const checkAudioHttpStatus = async () => {
            let msg = (cfg.l10n && cfg.l10n.streamingError) ? cfg.l10n.streamingError : 'Streaming Error';
            let key = 'error-streaming';
            try {
                const url = audioEl.src;
                if (url) {
                    const resp = await fetch(url, { method: 'HEAD', cache: 'no-cache' });
                    if (resp.status === 403) {
                        key = 'error-token-expired';
                        msg = (cfg.l10n && typeof cfg.l10n[key] === 'string' && cfg.l10n[key]) ? cfg.l10n[key] : ((cfg.l10n && cfg.l10n.tokenExpired) ? cfg.l10n.tokenExpired : 'Your session has expired. Please reload the page.');
                        showStatusError(msg, key);
                        return true;
                    } else if (resp.status === 404) {
                        key = 'error-media-file-not-found';
                        if (cfg.l10n && typeof cfg.l10n[key] === 'string' && cfg.l10n[key]) {
                            msg = cfg.l10n[key];
                        } else if (cfg.l10n && cfg.l10n.mediaMissing) {
                            msg = cfg.l10n.mediaMissing;
                        } else {
                            msg = 'Media file not found (404).';
                        }
                        markTrackUnplayable(currentTrackIndex, key);
                        showStatusError(msg, key);
                        const nextIdx = findNextPlayableTrackIndex(currentTrackIndex);
                        if (nextIdx >= 0) {
                            try {
                                await loadTrack(nextIdx, { autoplay: false });
                            } catch (e) {}
                        }
                        return true;
                    } else if (resp.status >= 400) {
                        key = 'error-streaming-http';
                        if (cfg.l10n && typeof cfg.l10n[key] === 'string' && cfg.l10n[key]) {
                            msg = cfg.l10n[key];
                        } else {
                            msg = 'Streaming error (network or server issue). Please try again.';
                        }
                        showStatusError(msg, key);
                        return true;
                    }
                }
            } catch (e) {
                dbg('check-status-failed', e);
            }
            return false;
        };

        audioEl.addEventListener('error', () => {
            requestedPlay = false;
            isPrebufferGating = false;
            gateToken++;
            updateUiControls();
            (async () => {
                const handled = await checkAudioHttpStatus();
                if (!handled) {
                    let msg = '';
                    const key = 'error-internal';
                    if (cfg.l10n && typeof cfg.l10n[key] === 'string' && cfg.l10n[key]) {
                        msg = cfg.l10n[key];
                    }
                    if (!msg) {
                        msg = 'Unexpected player error. Please contact the site administrator or plugin developer.';
                    }
                    showStatusError(msg, key);
                }
                dbg('audio-error', { code: audioEl.error ? audioEl.error.code : 'unknown' });
            })();
        });

				audioEl.addEventListener('waiting', async () => { 
					updateAudioInfoLive(); 
					updateUiControls(); 
					try { 
                const cur = Number.isFinite(audioEl.currentTime) ? audioEl.currentTime : 0;
                const ahead = Math.max(0, getBufferedEndSec() - cur);
                dbg('waiting', { ahead, readyState: audioEl.readyState, chunkLevel: runtimeChunkLevel });
                
                // If waiting for more than a few seconds and readyState is HAVE_NOTHING (0) or HAVE_METADATA (1),
                // it might be a 403/404 that hasn't triggered an 'error' event yet in some browsers.
                if (ahead < 0.1 && audioEl.readyState <= 1) {
                    const now = performance.now();
                    const lastWaiting = waitingEventsTimes.length ? waitingEventsTimes[waitingEventsTimes.length - 1] : 0;
						if (now - lastWaiting > 2000) {
							 await checkAudioHttpStatus();
						}
					}

                waitingEventsTimes.push(performance.now());
                setNetworkPeriod(1500);
                networkTick(true);
					const n = countWaitingInWindow(10000);
					if (n >= 3) {
                    try { rampMasterTo(0, fadeSwitchSec); } catch (e) {}
                    requestedPlay = true;
                    gateToken++;
                    try { audioEl.pause(); } catch (e) {}
                    let key = 'error-network-limited';
                    let msg = '';
                    if (cfg.l10n && typeof cfg.l10n[key] === 'string' && cfg.l10n[key]) {
                        msg = cfg.l10n[key];
                    }
                    if (!msg) {
                        msg = 'Your network connection is unstable. Playback was adjusted.';
                    }
                    showStatusError(msg, key);
                    setVariantAndEnsurePlayback(currentVariantIndex);
                }
            } catch (e) {}
        });
		
		if (isProFlag && window.THESPL_Player_Pro && typeof window.THESPL_Player_Pro.init === 'function') {
			window.THESPL_Player_Pro.init(root, playlist);
		}

		const api = {
			play: async () => {
				try {
					userStopped = false;
					requestedPlay = true;
					gateToken++;
					await setVariantAndEnsurePlayback(currentVariantIndex);
				} catch (err) {}
			},
			pause: () => {
				try {
					userStopped = false;
					requestedPlay = false;
					gateToken++;
					audioEl.pause();
					updateAudioInfoLive();
					updateUiControls();
				} catch (err) {}
			},
			stop: () => {
				try {
					userStopped = true;
					requestedPlay = false;
					gateToken++;
					audioEl.pause();
					audioEl.currentTime = 0;
					updateAudioInfoLive();
					updateUiControls();
				} catch (err) {}
			},
			playTrack: async (index) => {
				try {
					const idx = Number.isFinite(index) ? Math.max(0, Math.min(tracks.length - 1, index)) : 0;
					await loadTrack(idx, { autoplay: true });
				} catch (err) {}
			},
			selectVariant: (index) => {
				try {
					selectVariant(index);
				} catch (err) {}
			},
			toggle: () => { togglePlayPause(); }
		};

		try {
			root._thesplPlayerApi = api;
		} catch (e) {}

		try {
			const G = window.THESPL_Player_Global || (window.THESPL_Player_Global = {});
			if (!G.players) G.players = new Map();
			G.players.set(playerId, {
				id: playerId,
				root,
				audioEl,
				toggle: () => { api.toggle(); },
				pause: () => { api.pause(); },
				play: () => { api.play(); }
			});
		} catch (e) {}
    };

    const __thesplInitPlayers = () => {
        try {
            const G = window.THESPL_Player_Global || (window.THESPL_Player_Global = {});
            if (!G.players) G.players = new Map();
            if (!G._bound) {
                const isTextInputTarget = (el) => {
                    if (!el || el === document.body) return false;
                    if (el.isContentEditable) return true;
                    const tag = (el.tagName || '').toLowerCase();
                    return tag === 'input' || tag === 'textarea' || tag === 'select';
                };
                document.addEventListener('keydown', async (e) => {
                    if (e.defaultPrevented) return;
                    const isSpace = (e.key === ' ' || e.key === 'Spacebar' || e.code === 'Space');
                    if (!isSpace) return;
                    if (isTextInputTarget(e.target)) return;
                    let active = (G.activeId && G.players) ? G.players.get(G.activeId) : null;
                    if (!active && G.players && typeof G.players.values === 'function') {
                        for (const p of G.players.values()) {
                            try { if (p && p.audioEl && !p.audioEl.paused && !p.audioEl.ended) { active = p; break; } } catch (err) {}
                        }
                    }
                    if (!active) return;
                    try { e.preventDefault(); } catch (err) {}
                    try { await active.toggle(); } catch (err) { try { active.toggle(); } catch (e2) {} }
                });
                document.addEventListener('keyup', (e) => {
                    const isSpace = (e.key === ' ' || e.key === 'Spacebar' || e.code === 'Space');
                    if (!isSpace) return;
                    if (isTextInputTarget(e.target)) return;
                    try { e.preventDefault(); } catch (err) {}
                });
                G._bound = true;
            }
        } catch (e) {}
        const nodes = document.querySelectorAll('.thespl-player[data-playlist-id]');
        nodes.forEach((n) => {
            initPlayer(n);
        });
    };

    if (document.readyState === 'loading') {
        document.addEventListener('DOMContentLoaded', __thesplInitPlayers);
    } else {
        __thesplInitPlayers();
    }

    try {
        window.THESPL_Player_Core = window.THESPL_Player_Core || {};
        window.THESPL_Player_Core.initAll = __thesplInitPlayers;
        window.THESPL_Player_Core.initNode = function(n){ initPlayer(n); };
    } catch (e) {}
})();
