/**
 * Web OS shell: telemetría, notas rápidas, clima/ubicación, iconos de categoría, tray de taskbar.
 * Depende de window.__antaraGridDesktop (expuesto desde main.js).
 */
(function () {
    'use strict';

    const FA_ICONS = [
        'fas fa-folder', 'fas fa-briefcase', 'fas fa-code', 'fas fa-chart-line', 'fas fa-database',
        'fas fa-cloud', 'fas fa-shield-alt', 'fas fa-users', 'fas fa-cog', 'fas fa-wrench',
        'fas fa-palette', 'fas fa-image', 'fas fa-video', 'fas fa-music', 'fas fa-gamepad',
        'fas fa-graduation-cap', 'fas fa-flask', 'fas fa-heartbeat', 'fas fa-truck', 'fas fa-store',
        'fas fa-home', 'fas fa-globe', 'fas fa-server', 'fas fa-network-wired', 'fas fa-bolt'
    ];

    function api() {
        return window.__antaraGridDesktop || null;
    }

    function meterClass(pct) {
        if (pct == null || !isFinite(pct)) return '';
        if (pct >= 85) return 'os-meter__fill--crit';
        if (pct >= 65) return 'os-meter__fill--warn';
        return '';
    }

    var METER_RING_COLORS = {
        disk: '#38bdf8',
        memory: '#a78bfa',
        cpu: '#34d399'
    };

    function meterRingStrokeColor(key, pct) {
        var sev = meterClass(pct);
        if (sev === 'os-meter__fill--crit') return 'var(--os-shell-crit)';
        if (sev === 'os-meter__fill--warn') return 'var(--os-shell-warn)';
        return METER_RING_COLORS[key] || 'var(--os-shell-ok)';
    }

    function meterRingLegendSwatchColor(key) {
        return METER_RING_COLORS[key] || 'var(--os-shell-accent)';
    }

    function meterSeverityLabel(pct) {
        var sev = meterClass(pct);
        if (sev === 'os-meter__fill--crit') return 'crítico';
        if (sev === 'os-meter__fill--warn') return 'atención';
        if (pct != null && isFinite(pct)) return 'normal';
        return '';
    }

    function meterSeveritySlug(pct) {
        var sev = meterClass(pct);
        if (sev === 'os-meter__fill--crit') return 'crit';
        if (sev === 'os-meter__fill--warn') return 'warn';
        if (pct != null && isFinite(pct)) return 'ok';
        return '';
    }

    /**
     * Anillos concéntricos: mayor % en el exterior, menor % hacia el centro.
     */
    function renderConcentricMeterRings(container, items) {
        if (!container) return;
        container.innerHTML = '';

        var list = (items || []).slice();
        var withPct = list.filter(function (it) {
            return it.pct != null && isFinite(it.pct);
        });
        var noPct = list.filter(function (it) {
            return it.pct == null || !isFinite(it.pct);
        });
        withPct.sort(function (a, b) { return b.pct - a.pct; });
        var ordered = withPct.concat(noPct);
        if (!ordered.length) return;

        var ringWidth = 9;
        var gap = 2.5;
        var maxR = 46;
        var cx = 50;
        var cy = 50;
        var svgNs = 'http://www.w3.org/2000/svg';

        var wrap = document.createElement('div');
        wrap.className = 'os-meter-rings';

        var svg = document.createElementNS(svgNs, 'svg');
        svg.setAttribute('class', 'os-meter-rings__svg');
        svg.setAttribute('viewBox', '0 0 100 100');
        svg.setAttribute('role', 'img');
        svg.setAttribute('aria-label', 'Uso del sistema');

        ordered.forEach(function (it, i) {
            var r = maxR - i * (ringWidth + gap) - ringWidth / 2;
            if (r < ringWidth / 2 + 2) return;

            var track = document.createElementNS(svgNs, 'circle');
            track.setAttribute('cx', String(cx));
            track.setAttribute('cy', String(cy));
            track.setAttribute('r', String(r));
            track.setAttribute('fill', 'none');
            track.setAttribute('stroke', 'rgba(0, 0, 0, 0.38)');
            track.setAttribute('stroke-width', String(ringWidth));
            svg.appendChild(track);

            var pct = it.pct != null && isFinite(it.pct) ? Math.min(100, Math.max(0, it.pct)) : 0;
            if (pct > 0) {
                var circ = 2 * Math.PI * r;
                var filled = circ * (pct / 100);
                var arc = document.createElementNS(svgNs, 'circle');
                arc.setAttribute('cx', String(cx));
                arc.setAttribute('cy', String(cy));
                arc.setAttribute('r', String(r));
                arc.setAttribute('fill', 'none');
                arc.setAttribute('stroke', meterRingStrokeColor(it.key, pct));
                arc.setAttribute('stroke-width', String(ringWidth));
                arc.setAttribute('stroke-linecap', 'round');
                arc.setAttribute('stroke-dasharray', filled + ' ' + (circ - filled));
                arc.setAttribute('stroke-dashoffset', '0');
                arc.setAttribute('transform', 'rotate(-90 ' + cx + ' ' + cy + ')');
                svg.appendChild(arc);
            }
        });

        if (withPct.length) {
            var center = document.createElementNS(svgNs, 'text');
            center.setAttribute('class', 'os-meter-rings__center');
            center.setAttribute('x', String(cx));
            center.setAttribute('y', String(cy - 2));
            center.setAttribute('text-anchor', 'middle');
            center.setAttribute('dominant-baseline', 'middle');
            center.textContent = Math.round(withPct[0].pct) + '%';
            svg.appendChild(center);

            var centerSub = document.createElementNS(svgNs, 'text');
            centerSub.setAttribute('class', 'os-meter-rings__center-sub');
            centerSub.setAttribute('x', String(cx));
            centerSub.setAttribute('y', String(cy + 10));
            centerSub.setAttribute('text-anchor', 'middle');
            centerSub.setAttribute('dominant-baseline', 'middle');
            var subLabel = withPct[0].shortLabel || withPct[0].label;
            centerSub.textContent = subLabel.length > 14 ? subLabel.slice(0, 12) + '…' : subLabel;
            svg.appendChild(centerSub);
        }

        wrap.appendChild(svg);

        var legend = document.createElement('ul');
        legend.className = 'os-meter-rings__legend';
        ordered.forEach(function (it) {
            var li = document.createElement('li');
            li.className = 'os-meter-rings__legend-item';
            if (it.hint) li.title = it.hint;

            var swatch = document.createElement('span');
            swatch.className = 'os-meter-rings__swatch';
            swatch.style.background = meterRingLegendSwatchColor(it.key);
            swatch.setAttribute('aria-hidden', 'true');

            var label = document.createElement('span');
            label.className = 'os-meter-rings__legend-label';
            label.textContent = it.label;

            var val = document.createElement('span');
            val.className = 'os-meter-rings__legend-val';
            val.textContent = it.pct != null && isFinite(it.pct) ? Math.round(it.pct) + '%' : 'N/D';

            var sev = meterSeverityLabel(it.pct);
            var sevSlug = meterSeveritySlug(it.pct);
            if (sev && sevSlug) {
                var badge = document.createElement('span');
                badge.className = 'os-meter-rings__legend-sev os-meter-rings__legend-sev--' + sevSlug;
                badge.textContent = sev;
                li.appendChild(swatch);
                li.appendChild(label);
                li.appendChild(val);
                li.appendChild(badge);
            } else {
                li.appendChild(swatch);
                li.appendChild(label);
                li.appendChild(val);
            }
            legend.appendChild(li);
        });
        wrap.appendChild(legend);

        var scale = document.createElement('div');
        scale.className = 'os-meter-rings__scale';
        scale.setAttribute('aria-hidden', 'true');
        scale.innerHTML =
            '<span class="os-meter-rings__scale-item"><i class="os-meter-rings__scale-dot" style="background:#38bdf8"></i>Disco</span>' +
            '<span class="os-meter-rings__scale-item"><i class="os-meter-rings__scale-dot" style="background:#a78bfa"></i>Memoria</span>' +
            '<span class="os-meter-rings__scale-item"><i class="os-meter-rings__scale-dot" style="background:#34d399"></i>Carga</span>' +
            '<span class="os-meter-rings__scale-sep" aria-hidden="true">·</span>' +
            '<span class="os-meter-rings__scale-item"><i class="os-meter-rings__scale-dot os-meter-rings__scale-dot--warn"></i>≥65%</span>' +
            '<span class="os-meter-rings__scale-item"><i class="os-meter-rings__scale-dot os-meter-rings__scale-dot--crit"></i>≥85%</span>';
        wrap.appendChild(scale);

        container.appendChild(wrap);
    }

    function faIconOptionLabel(cls) {
        var parts = String(cls || '').trim().split(/\s+/);
        var last = parts[parts.length - 1] || '';
        if (last.indexOf('fa-') === 0) {
            return last.replace(/^fa-/, '').replace(/-/g, ' ');
        }
        return cls || '';
    }

    /**
     * Rellena un elemento select con iconos Font Awesome sugeridos (valor = clase completa, p. ej. fas fa-folder).
     * @param {string} selectId id del select
     * @param {function(string):void|null} onChange callback con el valor seleccionado (puede ser '')
     */
    function fillCategoryIconSelect(selectId, onChange) {
        var el = document.getElementById(selectId);
        if (!el || el.tagName !== 'SELECT') return;
        if (el.dataset.faInited === '1') return;
        el.dataset.faInited = '1';
        el.innerHTML = '';
        var opt0 = document.createElement('option');
        opt0.value = '';
        opt0.textContent = 'Predeterminado (carpeta)';
        el.appendChild(opt0);
        FA_ICONS.forEach(function (cls) {
            var o = document.createElement('option');
            o.value = cls;
            o.textContent = faIconOptionLabel(cls);
            el.appendChild(o);
        });
        el.addEventListener('change', function () {
            if (typeof onChange === 'function') onChange(el.value);
        });
    }

    function updateCategoryPreviewModal() {
        const nameEl = document.getElementById('categoryPreviewName');
        const descEl = document.getElementById('categoryPreviewDesc');
        const iconWrap = document.getElementById('categoryPreviewIcon');
        const nameInp = document.getElementById('categoryName');
        const descInp = document.getElementById('categoryDescription');
        const iconField = document.getElementById('categoryIconClass');
        const colorInp = document.getElementById('categoryColor');
        if (!nameEl || !iconWrap) return;
        const nm = nameInp && nameInp.value ? nameInp.value.trim() : 'Vista previa';
        var ic = 'fas fa-folder';
        if (iconField && iconField.value) ic = iconField.value;
        const col = colorInp && colorInp.value ? colorInp.value : '#38bdf8';
        nameEl.textContent = nm || 'Vista previa';
        if (descEl) descEl.textContent = (descInp && descInp.value) ? descInp.value.trim() : '';
        iconWrap.style.background = col + '22';
        iconWrap.style.color = col;
        iconWrap.innerHTML = '<i class="' + ic + '"></i>';
    }

    function bindCategoryModalPreview() {
        ['categoryName', 'categoryDescription', 'categoryColor'].forEach(function (id) {
            const el = document.getElementById(id);
            if (el && el.dataset.osPreviewBound !== '1') {
                el.dataset.osPreviewBound = '1';
                el.addEventListener('input', updateCategoryPreviewModal);
            }
        });
    }

    function resetCategoryModal() {
        const h = document.getElementById('categoryIconClass');
        if (h) h.value = '';
        const desc = document.getElementById('categoryDescription');
        if (desc) desc.value = '';
        const col = document.getElementById('categoryColor');
        if (col) col.value = '#38bdf8';
        updateCategoryPreviewModal();
    }

    let telemetryTimer = null;
    let notesSaveTimer = null;
    var __flexiaClientTraffic = { inBytes: 0, outBytes: 0 };

    function formatTrafficBytes(bytes) {
        var b = Number(bytes) || 0;
        if (b < 1024) return b + ' B';
        if (b < 1048576) return (b / 1024).toFixed(1) + ' KB';
        if (b < 1073741824) return (b / 1048576).toFixed(2) + ' MB';
        return (b / 1073741824).toFixed(2) + ' GB';
    }

    function latencyQualityLabel(latencyMs) {
        var ms = Number(latencyMs);
        if (!isFinite(ms) || ms < 0) return null;
        if (ms < 50) return 'Excelente';
        if (ms < 100) return 'Muy buena';
        if (ms < 200) return 'Aceptable';
        if (ms < 300) return 'Lenta';
        if (ms < 500) return 'Bastante lenta';
        return 'Mala';
    }

    function estimateFetchBodyBytes(input, init) {
        try {
            if (init && init.body != null) {
                var body = init.body;
                if (typeof body === 'string') return body.length;
                if (body instanceof Blob) return body.size;
                if (body instanceof ArrayBuffer) return body.byteLength;
                if (ArrayBuffer.isView(body)) return body.byteLength;
            }
            if (input && typeof input === 'object' && input.body != null) {
                var b2 = input.body;
                if (typeof b2 === 'string') return b2.length;
                if (b2 instanceof Blob) return b2.size;
            }
        } catch (_) {}
        return 0;
    }

    function installFlexiaFetchMeter() {
        if (window.__flexiaFetchMeterInstalled) return;
        window.__flexiaFetchMeterInstalled = true;
        var base = (typeof BASE_URL !== 'undefined' ? String(BASE_URL) : '');
        if (!base) return;
        var orig = window.fetch;
        if (typeof orig !== 'function') return;
        window.fetch = function (input, init) {
            var url = '';
            try {
                url = typeof input === 'string' ? input : (input && input.url ? input.url : '');
            } catch (_) {}
            var sameOrigin = false;
            try {
                if (url.indexOf(base) === 0 || url.indexOf('?action=') >= 0 || url.indexOf('/core/auth/api.php') >= 0) {
                    sameOrigin = true;
                }
            } catch (_) {}
            if (sameOrigin) {
                __flexiaClientTraffic.outBytes += estimateFetchBodyBytes(input, init);
            }
            return orig.apply(this, arguments).then(function (res) {
                if (sameOrigin) {
                    var cl = res.headers.get('content-length');
                    if (cl) {
                        var n = parseInt(cl, 10);
                        if (!isNaN(n) && n > 0) __flexiaClientTraffic.inBytes += n;
                    } else if (res.clone && typeof res.clone === 'function') {
                        res.clone().blob().then(function (blob) {
                            if (blob && blob.size) __flexiaClientTraffic.inBytes += blob.size;
                        }).catch(function () {});
                    }
                }
                return res;
            });
        };
    }

    function scheduleNotesSave() {
        const A = api();
        if (!A || typeof A.saveSettings !== 'function') return;
        clearTimeout(notesSaveTimer);
        notesSaveTimer = setTimeout(function () {
            A.saveSettings({ quickNotes: A.getQuickNotes() });
        }, 600);
    }

    var NOTE_BG_DEFAULT = '#0f172a';
    var NOTE_TEXT_DEFAULT = '#e2e8f0';
    var NOTE_TEXT_DARK = '#0f172a';
    var NOTE_TEXT_LIGHT = '#e2e8f0';
    var notesSearchQuery = '';

    function parseNoteHexColor(hex) {
        var h = String(hex || '').replace('#', '').trim();
        if (h.length === 3) {
            h = h.split('').map(function (c) { return c + c; }).join('');
        }
        if (h.length !== 6) return null;
        var n = parseInt(h, 16);
        if (Number.isNaN(n)) return null;
        return { r: (n >> 16) & 255, g: (n >> 8) & 255, b: n & 255 };
    }

    function noteRelativeLuminance(rgb) {
        var srgb = [rgb.r, rgb.g, rgb.b].map(function (c) {
            c = c / 255;
            return c <= 0.03928 ? c / 12.92 : Math.pow((c + 0.055) / 1.055, 2.4);
        });
        return 0.2126 * srgb[0] + 0.7152 * srgb[1] + 0.0722 * srgb[2];
    }

    function contrastTextForBg(bgHex) {
        var rgb = parseNoteHexColor(bgHex);
        if (!rgb) return NOTE_TEXT_DEFAULT;
        return noteRelativeLuminance(rgb) > 0.55 ? NOTE_TEXT_DARK : NOTE_TEXT_LIGHT;
    }

    function resolveNoteColors(note) {
        var bg = note && note.bgColor ? note.bgColor : '';
        var fg = (note && note.textColor) || '';
        if (!fg && bg) fg = contrastTextForBg(bg);
        return { bgColor: bg, textColor: fg };
    }

    function escapeHtml(s) {
        return String(s)
            .replace(/&/g, '&amp;')
            .replace(/</g, '&lt;')
            .replace(/>/g, '&gt;')
            .replace(/"/g, '&quot;');
    }

    function escapeRegex(s) {
        return String(s).replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
    }

    function getNotesSearchQuery() {
        return String(notesSearchQuery || '').trim();
    }

    function highlightHtmlForQuery(text, query) {
        var raw = String(text || '');
        if (!query) return escapeHtml(raw);
        var re = new RegExp(escapeRegex(query), 'gi');
        var out = '';
        var last = 0;
        var m;
        while ((m = re.exec(raw)) !== null) {
            out += escapeHtml(raw.slice(last, m.index));
            out += '<mark>' + escapeHtml(m[0]) + '</mark>';
            last = m.index + m[0].length;
            if (m[0].length === 0) {
                re.lastIndex++;
            }
        }
        out += escapeHtml(raw.slice(last));
        return out;
    }

    function updateNoteById(noteId, patch) {
        const A = api();
        if (!A) return;
        const list = A.getQuickNotes();
        const item = list.find(function (x) { return x.id === noteId; });
        if (!item) return;
        Object.keys(patch).forEach(function (k) {
            item[k] = patch[k];
        });
        item.updatedAt = Date.now();
        scheduleNotesSave();
    }

    function applyNoteCardColors(card, ta, highlightEl, note) {
        var colors = resolveNoteColors(note || {});
        var bg = colors.bgColor;
        var fg = colors.textColor;
        card.style.background = bg || '';
        card.style.color = fg || '';
        var titleInp = card.querySelector('.os-note-title');
        if (titleInp) titleInp.style.color = fg || '';
        if (ta) {
            ta.style.color = fg || '';
            ta.style.backgroundColor = bg ? 'rgba(0, 0, 0, 0.12)' : '';
        }
        if (highlightEl) {
            highlightEl.style.color = fg || '';
        }
    }

    function updateCardSearchHighlight(card) {
        if (!card || !card.dataset.noteId) return;
        const A = api();
        const q = getNotesSearchQuery();
        const ta = card.querySelector('.os-note-ta');
        const highlightEl = card.querySelector('.os-note-highlight');
        const titleInp = card.querySelector('.os-note-title');
        const note = A && A.getQuickNotes().find(function (x) { return x.id === card.dataset.noteId; });
        const text = ta ? ta.value : (note ? note.text : '');
        const title = titleInp ? titleInp.value : (note ? note.title : '');
        const titleHit = q && title.toLowerCase().indexOf(q.toLowerCase()) >= 0;
        const textHit = q && text.toLowerCase().indexOf(q.toLowerCase()) >= 0;

        if (titleInp) {
            titleInp.classList.toggle('os-notes-search-hit', !!titleHit);
        }

        if (!highlightEl || !ta) return;

        if (q && ta.readOnly && textHit) {
            highlightEl.innerHTML = highlightHtmlForQuery(text, q);
            highlightEl.hidden = false;
            card.classList.add('os-note-card--searching');
        } else {
            highlightEl.innerHTML = '';
            highlightEl.hidden = true;
            card.classList.remove('os-note-card--searching');
        }
    }

    function noteMatchesSearchCard(card, q) {
        if (!q) return true;
        var titleInp = card.querySelector('.os-note-title');
        var ta = card.querySelector('.os-note-ta');
        var title = titleInp ? titleInp.value : '';
        var text = ta ? ta.value : '';
        var ql = q.toLowerCase();
        return title.toLowerCase().indexOf(ql) >= 0 || text.toLowerCase().indexOf(ql) >= 0;
    }

    function notesCarouselVisibleCards(wrap) {
        var out = [];
        if (!wrap) return out;
        wrap.querySelectorAll('.os-note-card[data-note-id]').forEach(function (card) {
            if (!card.classList.contains('os-note-card--filtered-out')) out.push(card);
        });
        return out;
    }

    function scrollNotesWrapToCard(wrap, card) {
        if (!wrap || !card) return;
        var left = card.offsetLeft;
        if (typeof wrap.scrollTo === 'function') {
            wrap.scrollTo({ left: left, behavior: 'smooth' });
        } else {
            wrap.scrollLeft = left;
        }
    }

    function applyNotesSearchToWrap(wrap) {
        if (!wrap) return;
        var q = getNotesSearchQuery();
        var firstMatch = null;
        wrap.querySelectorAll('.os-note-card[data-note-id]').forEach(function (card) {
            var match = noteMatchesSearchCard(card, q);
            card.classList.toggle('os-note-card--filtered-out', !!q && !match);
            if (q && match && !firstMatch) firstMatch = card;
            updateCardSearchHighlight(card);
        });
        wrap.querySelectorAll('.os-note-card:not([data-note-id])').forEach(function (hint) {
            hint.classList.toggle('os-note-card--filtered-out', !!q);
        });
        if (q && firstMatch) {
            requestAnimationFrame(function () {
                scrollNotesWrapToCard(wrap, firstMatch);
                updateNotesCounter();
            });
        }
    }

    function applyNotesSearch() {
        ['osNotesCarousel', 'osNotesCarouselMobile'].forEach(function (id) {
            applyNotesSearchToWrap(document.getElementById(id));
        });
        updateNotesCounter();
    }

    function syncNotesSearchInputs() {
        ['osNotesSearch', 'osNotesSearchMobile'].forEach(function (id) {
            var el = document.getElementById(id);
            if (el && el.value !== notesSearchQuery) el.value = notesSearchQuery;
        });
    }

    function bindNotesSearch() {
        ['osNotesSearch', 'osNotesSearchMobile'].forEach(function (id) {
            var el = document.getElementById(id);
            if (!el || el.dataset.osNotesSearchBound === '1') return;
            el.dataset.osNotesSearchBound = '1';
            el.addEventListener('input', function () {
                notesSearchQuery = el.value;
                syncNotesSearchInputs();
                applyNotesSearch();
            });
        });
    }

    function useCompactNotesLayout() {
        try {
            return window.matchMedia('(max-width: 768px)').matches;
        } catch (_) {
            return false;
        }
    }

    /** Vista tipo escritorio (no el menú compacto solo-móvil). */
    function isMobileMode() {
        return document.body.classList.contains('mobile-mode');
    }

    function getNotesWraps() {
        const desktop = document.getElementById('osNotesCarousel');
        const mobile = document.getElementById('osNotesCarouselMobile');
        if (useCompactNotesLayout()) {
            return mobile ? [mobile] : [];
        }
        return desktop ? [desktop] : [];
    }

    function activeNotesWrapForScroll() {
        const modal = document.getElementById('osNotesMobileModal');
        const mobile = document.getElementById('osNotesCarouselMobile');
        const vis = modal && (modal.style.display === 'block' || modal.style.display === 'flex');
        if (vis && mobile) return mobile;
        const wraps = getNotesWraps();
        return wraps[0] || null;
    }

    /** En móvil: altura del textarea casi ajustada al texto (fallback si no hay field-sizing: content). */
    function bindNotesTextareaAutosize(textarea) {
        if (!textarea || textarea.dataset.osAutosize === '1') return;
        textarea.dataset.osAutosize = '1';
        var maxPx = 220;
        function fit() {
            textarea.style.height = 'auto';
            var h = Math.min(Math.max(textarea.scrollHeight, 40), maxPx);
            textarea.style.height = h + 'px';
        }
        textarea.addEventListener('input', fit);
        textarea.addEventListener('change', fit);
        requestAnimationFrame(fit);
    }

    function syncNoteThemeColorBtn(themeBtn, bgHex, hasCustomBg) {
        if (!themeBtn || !themeBtn.btn) return;
        if (!hasCustomBg) {
            themeBtn.btn.style.background = '';
            themeBtn.btn.style.color = '';
            themeBtn.btn.style.borderColor = '';
            return;
        }
        var bg = bgHex || NOTE_BG_DEFAULT;
        var fg = contrastTextForBg(bg);
        themeBtn.btn.style.background = bg;
        themeBtn.btn.style.color = fg;
        themeBtn.btn.style.borderColor = 'color-mix(in srgb, ' + fg + ' 28%, transparent)';
    }

    function createNoteThemeColorBtn(bgValue, onThemeChange) {
        const btn = document.createElement('button');
        btn.type = 'button';
        btn.className = 'os-note-delete os-note-theme-color';
        btn.title = 'Color de la nota';
        btn.setAttribute('aria-label', 'Color de la nota');
        const icon = document.createElement('i');
        icon.className = 'fas fa-palette';
        icon.setAttribute('aria-hidden', 'true');
        const inp = document.createElement('input');
        inp.type = 'color';
        inp.className = 'os-note-color-input os-note-theme-color-input';
        inp.value = bgValue || NOTE_BG_DEFAULT;
        inp.tabIndex = -1;
        inp.setAttribute('aria-hidden', 'true');
        var hasCustomBg = !!bgValue;
        function applyThemeChange() {
            var bg = inp.value || NOTE_BG_DEFAULT;
            var fg = contrastTextForBg(bg);
            hasCustomBg = true;
            syncNoteThemeColorBtn(themeBtn, bg, true);
            onThemeChange(bg, fg);
        }
        var themeBtn = { btn: btn, input: inp };
        inp.addEventListener('input', applyThemeChange);
        inp.addEventListener('change', applyThemeChange);
        btn.appendChild(icon);
        btn.appendChild(inp);
        syncNoteThemeColorBtn(themeBtn, inp.value, hasCustomBg);
        return themeBtn;
    }

    function syncNoteTextColorBtn(textBtn, fgHex, hasCustomText) {
        if (!textBtn || !textBtn.btn) return;
        if (!hasCustomText) {
            textBtn.btn.style.background = '';
            textBtn.btn.style.color = '';
            textBtn.btn.style.borderColor = '';
            return;
        }
        var fg = fgHex || NOTE_TEXT_DEFAULT;
        textBtn.btn.style.background = '';
        textBtn.btn.style.color = fg;
        textBtn.btn.style.borderColor = 'color-mix(in srgb, ' + fg + ' 35%, transparent)';
    }

    function createNoteTextColorBtn(textValue, hasCustomText, onTextChange) {
        const btn = document.createElement('button');
        btn.type = 'button';
        btn.className = 'os-note-delete os-note-text-color';
        btn.title = 'Color de texto';
        btn.setAttribute('aria-label', 'Color de texto');
        const icon = document.createElement('i');
        icon.className = 'fas fa-font';
        icon.setAttribute('aria-hidden', 'true');
        const inp = document.createElement('input');
        inp.type = 'color';
        inp.className = 'os-note-color-input os-note-text-color-input';
        inp.value = textValue || NOTE_TEXT_DEFAULT;
        inp.tabIndex = -1;
        inp.setAttribute('aria-hidden', 'true');
        function applyTextChange() {
            var fg = inp.value || NOTE_TEXT_DEFAULT;
            syncNoteTextColorBtn(textBtn, fg, true);
            onTextChange(fg);
        }
        var textBtn = { btn: btn, input: inp };
        inp.addEventListener('input', applyTextChange);
        inp.addEventListener('change', applyTextChange);
        btn.appendChild(icon);
        btn.appendChild(inp);
        syncNoteTextColorBtn(textBtn, inp.value, hasCustomText);
        return textBtn;
    }

    function buildNotesInto(wrap) {
        const A = api();
        if (!A || !wrap) return;
        const notes = A.getQuickNotes();
        wrap.innerHTML = '';
        (notes.length ? notes : [{ id: '_placeholder', text: '', updatedAt: 0 }]).forEach(function (n, idx) {
            if (n.id === '_placeholder' && !n.text) {
                const hint = document.createElement('div');
                hint.className = 'os-note-card';
                hint.innerHTML = '<p style="margin:0;opacity:.6;font-size:13px;">Pulsa «+ Añadir nota» para guardar ideas rápidas.</p>';
                wrap.appendChild(hint);
                return;
            }
            const card = document.createElement('div');
            card.className = 'os-note-card';
            card.dataset.noteId = n.id;

            const editorWrap = document.createElement('div');
            editorWrap.className = 'os-note-editor-wrap';

            const highlightEl = document.createElement('div');
            highlightEl.className = 'os-note-highlight';
            highlightEl.setAttribute('aria-hidden', 'true');
            highlightEl.hidden = true;

            const ta = document.createElement('textarea');
            ta.className = 'os-note-ta';
            ta.value = n.text || '';
            ta.placeholder = 'Escribe el contenido de la nota…';
            ta.setAttribute('aria-label', 'Nota ' + (idx + 1));
            ta.title = 'Pulse para editar la nota';
            ta.readOnly = true;

            function lockNoteTa() {
                ta.readOnly = true;
                ta.setAttribute('aria-readonly', 'true');
                ta.classList.remove('os-note-ta--editing');
                updateCardSearchHighlight(card);
            }
            function unlockNoteTa() {
                if (!ta.readOnly) return;
                ta.readOnly = false;
                ta.removeAttribute('aria-readonly');
                ta.classList.add('os-note-ta--editing');
                ta.title = '';
                card.classList.remove('os-note-card--searching');
                highlightEl.hidden = true;
            }
            ta.addEventListener('pointerdown', function () {
                unlockNoteTa();
            });
            ta.addEventListener('keydown', function (e) {
                if (!ta.readOnly) return;
                if (e.key === 'Enter' || e.key === ' ') {
                    e.preventDefault();
                    unlockNoteTa();
                }
            });
            ta.addEventListener('input', function () {
                updateNoteById(n.id, { text: ta.value });
                var noteWrap = card.closest('.os-notes-carousel');
                if (noteWrap && getNotesSearchQuery()) {
                    applyNotesSearchToWrap(noteWrap);
                    updateNotesCounter();
                } else if (ta.readOnly) {
                    updateCardSearchHighlight(card);
                }
            });
            ta.addEventListener('blur', function () {
                lockNoteTa();
                ta.title = 'Pulse para editar la nota';
            });
            ta.addEventListener('change', function () {
                updateNoteById(n.id, { text: ta.value });
            });

            lockNoteTa();

            const titleInp = document.createElement('input');
            titleInp.type = 'text';
            titleInp.className = 'os-note-title';
            titleInp.placeholder = 'Título';
            titleInp.value = n.title || '';
            titleInp.maxLength = 80;
            titleInp.setAttribute('aria-label', 'Título de la nota ' + (idx + 1));
            titleInp.addEventListener('input', function () {
                updateNoteById(n.id, { title: titleInp.value });
                applyNotesSearchToWrap(card.closest('.os-notes-carousel'));
                updateNotesCounter();
            });

            var textBtn;
            const themeBtn = createNoteThemeColorBtn(n.bgColor, function (bg, fg) {
                if (textBtn) {
                    textBtn.input.value = fg;
                    syncNoteTextColorBtn(textBtn, fg, true);
                }
                updateNoteById(n.id, { bgColor: bg, textColor: fg });
                applyNoteCardColors(card, ta, highlightEl, { bgColor: bg, textColor: fg });
            });

            textBtn = createNoteTextColorBtn(
                n.textColor || (n.bgColor ? contrastTextForBg(n.bgColor) : ''),
                !!n.textColor,
                function (fg) {
                    updateNoteById(n.id, { textColor: fg });
                    var currentNote = A.getQuickNotes().find(function (x) { return x.id === n.id; }) || n;
                    applyNoteCardColors(card, ta, highlightEl, currentNote);
                }
            );

            const del = document.createElement('button');
            del.type = 'button';
            del.className = 'os-note-delete';
            del.setAttribute('aria-label', 'Mover nota a la papelera');
            del.title = 'Mover a la papelera';
            del.innerHTML = '<i class="fas fa-trash-alt" aria-hidden="true"></i>';
            del.addEventListener('click', async function () {
                var ok = true;
                var msg = '¿Mover esta nota a la papelera de reciclaje? Podrá restaurarla desde la papelera.';
                if (window.appDialog && typeof window.appDialog.confirm === 'function') {
                    ok = await window.appDialog.confirm(msg, {
                        type: 'trash',
                        title: 'Mover nota a la papelera',
                        okLabel: 'Mover a papelera',
                        cancelLabel: 'Cancelar'
                    });
                } else {
                    ok = window.confirm(msg);
                }
                if (!ok) return;
                if (typeof A.moveQuickNoteToTrash === 'function') {
                    var currentNote = A.getQuickNotes().find(function (x) { return x.id === n.id; }) || n;
                    var snapshotBg = currentNote.bgColor || '';
                    var snapshot = {
                        title: titleInp ? titleInp.value : (currentNote.title || ''),
                        text: ta ? ta.value : (currentNote.text || ''),
                        bgColor: snapshotBg,
                        textColor: currentNote.textColor || (snapshotBg ? contrastTextForBg(snapshotBg) : '')
                    };
                    A.moveQuickNoteToTrash(n.id, snapshot).then(function (moved) {
                        if (moved) renderNotesCarousel();
                    });
                    return;
                }
                const next = A.getQuickNotes().filter(function (x) { return x.id !== n.id; });
                A.setQuickNotes(next);
                A.saveSettings({ quickNotes: next }).then(function () { renderNotesCarousel(); });
            });

            const footer = document.createElement('div');
            footer.className = 'os-note-footer';
            footer.appendChild(themeBtn.btn);
            footer.appendChild(textBtn.btn);
            footer.appendChild(del);

            editorWrap.appendChild(highlightEl);
            editorWrap.appendChild(ta);
            card.appendChild(titleInp);
            card.appendChild(editorWrap);
            card.appendChild(footer);
            wrap.appendChild(card);

            applyNoteCardColors(card, ta, highlightEl, n);
            updateCardSearchHighlight(card);

            if (useCompactNotesLayout()) {
                bindNotesTextareaAutosize(ta);
            }
        });
    }

    function renderNotesCarousel() {
        const A = api();
        if (!A) return;
        getNotesWraps().forEach(buildNotesInto);
        syncNotesSearchInputs();
        applyNotesSearch();
        if (typeof requestAnimationFrame === 'function') {
            requestAnimationFrame(function () { updateNotesCounter(); });
        } else {
            updateNotesCounter();
        }
    }

    var MEDIA_WIDGET_UPLOAD_PATH = 'widget-gallery';
    var mediaSliderIndex = 0;
    var mediaSliderShowSeq = 0;
    var mediaSliderZoomState = { scale: 1, translateX: 0, translateY: 0, isZoomed: false };
    var MEDIA_SLIDER_ZOOM_MIN = 1;
    var MEDIA_SLIDER_ZOOM_MAX = 3;
    var MEDIA_SLIDER_PINCH_EXP = 0.9;
    var MEDIA_SLIDER_RATIO_DEADZONE = 0.006;

    function getMediaWidgetItems() {
        var A = api();
        if (!A || typeof A.getMediaWidgetItems !== 'function') return [];
        return A.getMediaWidgetItems();
    }

    function setMediaWidgetItemsLocal(arr) {
        var A = api();
        if (!A || typeof A.setMediaWidgetItems !== 'function') return;
        A.setMediaWidgetItems(arr);
    }

    function persistMediaWidgetItems(arr) {
        var A = api();
        if (!A) return Promise.resolve();
        setMediaWidgetItemsLocal(arr);
        return A.saveSettings({ mediaWidgetItems: arr });
    }

    function mediaWidgetTr(key, fallback) {
        var A = api();
        if (A && typeof A.tr === 'function') {
            var t = A.tr(key);
            if (t) return t;
        }
        return fallback || key;
    }

    function resolveMediaDisplayUrl(url) {
        var A = api();
        if (A && typeof A.resolveWallpaperMediaUrl === 'function') {
            return A.resolveWallpaperMediaUrl(url);
        }
        if (!url) return '';
        var s = String(url).trim();
        if (/^https?:\/\//i.test(s)) return s;
        var base = (typeof BASE_URL !== 'undefined' ? BASE_URL : '');
        return base + s.replace(/^\//, '');
    }

    function isMediaWidgetVideoItem(item) {
        if (!item) return false;
        var mime = String(item.mimeType || '').toLowerCase();
        var name = String(item.name || '').toLowerCase();
        return mime.startsWith('video/') || /\.(mp4|webm|ogg|mov|mkv|m4v)$/i.test(name);
    }

    function mediaWidgetExplorerItem(item) {
        if (!item) return null;
        return {
            type: 'file',
            url: item.url,
            name: item.name || '',
            mimeType: item.mimeType || ''
        };
    }

    function getMediaSliderRoots() {
        return [
            {
                main: document.getElementById('osMediaSliderMain'),
                thumbs: document.getElementById('osMediaSliderThumbs'),
                counter: document.getElementById('osMediaSliderCounter'),
                viewport: document.getElementById('osMediaSliderViewport')
            },
            {
                main: document.getElementById('osMediaSliderMainMobile'),
                thumbs: document.getElementById('osMediaSliderThumbsMobile'),
                counter: document.getElementById('osMediaSliderCounterMobile'),
                viewport: document.getElementById('osMediaSliderViewportMobile')
            }
        ].filter(function (r) { return r.main && r.thumbs; });
    }

    function clampMediaSliderIndex(items) {
        if (!items.length) {
            mediaSliderIndex = 0;
            return;
        }
        if (mediaSliderIndex < 0) mediaSliderIndex = 0;
        if (mediaSliderIndex >= items.length) mediaSliderIndex = items.length - 1;
    }

    function resetMediaSliderZoomState() {
        mediaSliderZoomState.scale = 1;
        mediaSliderZoomState.translateX = 0;
        mediaSliderZoomState.translateY = 0;
        mediaSliderZoomState.isZoomed = false;
    }

    function mediaWidgetImageMime(item) {
        var mime = String(item && item.mimeType || '').toLowerCase();
        if (mime.startsWith('image/')) return mime;
        var name = String(item && item.name || '').toLowerCase();
        if (/\.jpe?g$/i.test(name)) return 'image/jpeg';
        if (/\.png$/i.test(name)) return 'image/png';
        if (/\.gif$/i.test(name)) return 'image/gif';
        if (/\.webp$/i.test(name)) return 'image/webp';
        if (/\.bmp$/i.test(name)) return 'image/bmp';
        if (/\.svg$/i.test(name)) return 'image/svg+xml';
        return 'image/jpeg';
    }

    function mediaSliderZoomTransform(state) {
        return 'translate(calc(-50% + ' + state.translateX + 'px), calc(-50% + ' + state.translateY + 'px)) scale(' + state.scale + ')';
    }

    function mediaSliderZoomToAt(state, imgEl, wrapEl, newScale, clientX, clientY) {
        if (!imgEl || !wrapEl) return;
        var clampedScale = Math.max(MEDIA_SLIDER_ZOOM_MIN, Math.min(newScale, MEDIA_SLIDER_ZOOM_MAX));
        var oldScale = state.scale || 1;
        if (clampedScale === MEDIA_SLIDER_ZOOM_MIN) {
            state.scale = 1;
            state.translateX = 0;
            state.translateY = 0;
            state.isZoomed = false;
            imgEl.classList.remove('os-media-slider-img--zoomed');
            imgEl.style.transform = mediaSliderZoomTransform(state);
            return;
        }
        var rect = wrapEl.getBoundingClientRect();
        var cx = rect.left + rect.width / 2;
        var cy = rect.top + rect.height / 2;
        var px = (clientX - cx - state.translateX) / oldScale;
        var py = (clientY - cy - state.translateY) / oldScale;
        state.scale = clampedScale;
        state.translateX = (clientX - cx) - px * state.scale;
        state.translateY = (clientY - cy) - py * state.scale;
        state.isZoomed = state.scale > MEDIA_SLIDER_ZOOM_MIN;
        imgEl.classList.toggle('os-media-slider-img--zoomed', state.isZoomed);
        imgEl.style.transform = mediaSliderZoomTransform(state);
    }

    function setupMediaSliderInlineZoom(wrapEl, imgEl) {
        if (!wrapEl || !imgEl) return;
        var state = mediaSliderZoomState;
        resetMediaSliderZoomState();
        imgEl.style.transform = mediaSliderZoomTransform(state);

        var ua = navigator.userAgent || '';
        var isIOS = /iPad|iPhone|iPod/i.test(ua) || (navigator.platform === 'MacIntel' && (navigator.maxTouchPoints || 0) > 1);
        if (isIOS) {
            var gestureBase = 1;
            wrapEl.addEventListener('gesturestart', function (e) {
                e.preventDefault();
                gestureBase = state.scale;
            }, { passive: false });
            wrapEl.addEventListener('gesturechange', function (e) {
                e.preventDefault();
                var raw = e.scale || 1;
                var ratio = Math.pow(raw, MEDIA_SLIDER_PINCH_EXP);
                if (Math.abs(ratio - 1) < MEDIA_SLIDER_RATIO_DEADZONE) return;
                var r = wrapEl.getBoundingClientRect();
                mediaSliderZoomToAt(state, imgEl, wrapEl, gestureBase * ratio, r.left + r.width / 2, r.top + r.height / 2);
            }, { passive: false });
            wrapEl.addEventListener('gestureend', function (e) {
                e.preventDefault();
            }, { passive: false });
        }

        var pointers = new Map();
        var pinchStartDist = 0;
        var pinchStartZoom = 1;
        var pinchAnchor = null;

        function updatePointer(ev) {
            pointers.set(ev.pointerId, { x: ev.clientX, y: ev.clientY });
        }

        wrapEl.addEventListener('pointerdown', function (ev) {
            if (ev.pointerType === 'touch') {
                updatePointer(ev);
                if (pointers.size === 2) {
                    wrapEl.style.touchAction = 'none';
                    var pts = Array.from(pointers.values());
                    var dx = pts[0].x - pts[1].x;
                    var dy = pts[0].y - pts[1].y;
                    pinchStartDist = Math.max(Math.hypot(dx, dy), 8);
                    pinchStartZoom = state.scale;
                    pinchAnchor = { x: (pts[0].x + pts[1].x) / 2, y: (pts[0].y + pts[1].y) / 2 };
                    try { ev.preventDefault(); } catch (_) {}
                } else if (pointers.size === 1 && state.isZoomed) {
                    var pid = ev.pointerId;
                    var ox = ev.clientX - state.translateX;
                    var oy = ev.clientY - state.translateY;
                    var move = function (e) {
                        if (e.pointerId !== pid || pointers.size !== 1) return;
                        e.preventDefault();
                        state.translateX = e.clientX - ox;
                        state.translateY = e.clientY - oy;
                        imgEl.style.transform = mediaSliderZoomTransform(state);
                    };
                    var end = function (e) {
                        if (e.pointerId !== pid) return;
                        wrapEl.removeEventListener('pointermove', move);
                        wrapEl.removeEventListener('pointerup', end);
                        wrapEl.removeEventListener('pointercancel', end);
                        try { wrapEl.releasePointerCapture(pid); } catch (_) {}
                    };
                    try { wrapEl.setPointerCapture(pid); } catch (_) {}
                    wrapEl.addEventListener('pointermove', move, { passive: false });
                    wrapEl.addEventListener('pointerup', end);
                    wrapEl.addEventListener('pointercancel', end);
                }
                return;
            }
        }, { passive: false });

        wrapEl.addEventListener('pointermove', function (ev) {
            if (ev.pointerType !== 'touch' || !pointers.has(ev.pointerId)) return;
            updatePointer(ev);
            if (pointers.size === 2 && pinchStartDist > 0) {
                var pts = Array.from(pointers.values());
                var dx = pts[0].x - pts[1].x;
                var dy = pts[0].y - pts[1].y;
                var dist = Math.hypot(dx, dy);
                var rawRatio = dist / pinchStartDist;
                var ratio = Math.pow(rawRatio, MEDIA_SLIDER_PINCH_EXP);
                if (Math.abs(rawRatio - 1) < MEDIA_SLIDER_RATIO_DEADZONE) return;
                pinchAnchor = { x: (pts[0].x + pts[1].x) / 2, y: (pts[0].y + pts[1].y) / 2 };
                mediaSliderZoomToAt(state, imgEl, wrapEl, pinchStartZoom * ratio, pinchAnchor.x, pinchAnchor.y);
                try { ev.preventDefault(); } catch (_) {}
            }
        }, { passive: false });

        function pointerUp(ev) {
            if (ev.pointerType !== 'touch') return;
            pointers.delete(ev.pointerId);
            if (pointers.size < 2) {
                pinchStartDist = 0;
                pinchAnchor = null;
                wrapEl.style.touchAction = 'none';
            }
        }
        wrapEl.addEventListener('pointerup', pointerUp);
        wrapEl.addEventListener('pointercancel', pointerUp);

        wrapEl.addEventListener('touchstart', function (e) {
            if (e.touches.length !== 2) return;
            var t0 = e.touches[0];
            var t1 = e.touches[1];
            var dx = t0.clientX - t1.clientX;
            var dy = t0.clientY - t1.clientY;
            var touchPinchDist = Math.max(Math.hypot(dx, dy), 8);
            var touchPinchZoom = state.scale;
            var touchAnchor = { x: (t0.clientX + t1.clientX) / 2, y: (t0.clientY + t1.clientY) / 2 };
            var onMove = function (ev) {
                if (ev.touches.length !== 2) return;
                ev.preventDefault();
                var a = ev.touches[0];
                var b = ev.touches[1];
                var ddx = a.clientX - b.clientX;
                var ddy = a.clientY - b.clientY;
                var dist = Math.hypot(ddx, ddy);
                var rawRatio = dist / touchPinchDist;
                var ratio = Math.pow(rawRatio, MEDIA_SLIDER_PINCH_EXP);
                if (Math.abs(rawRatio - 1) < MEDIA_SLIDER_RATIO_DEADZONE) return;
                touchAnchor = { x: (a.clientX + b.clientX) / 2, y: (a.clientY + b.clientY) / 2 };
                mediaSliderZoomToAt(state, imgEl, wrapEl, touchPinchZoom * ratio, touchAnchor.x, touchAnchor.y);
            };
            var onEnd = function () {
                wrapEl.removeEventListener('touchmove', onMove);
                wrapEl.removeEventListener('touchend', onEnd);
                wrapEl.removeEventListener('touchcancel', onEnd);
            };
            wrapEl.addEventListener('touchmove', onMove, { passive: false });
            wrapEl.addEventListener('touchend', onEnd);
            wrapEl.addEventListener('touchcancel', onEnd);
        }, { passive: true });
    }

    function openMediaWidgetImagePreview(item) {
        var A = api();
        if (!A || typeof A.showFilePreview !== 'function' || !item) return;
        A.showFilePreview({
            name: item.name || 'Imagen',
            url: item.url || '',
            type: mediaWidgetImageMime(item)
        });
    }

    function shareMediaWidgetItem(item) {
        var A = api();
        if (!A || typeof A.shareSecureUrl !== 'function' || !item) return;
        var rawUrl = item.url || '';
        if (!rawUrl) return;
        var title = item.name || mediaWidgetTr('share', 'Compartir');
        A.shareSecureUrl(rawUrl, title).catch(function () {
            showNotesToast('Compartir no disponible.', 'error');
        });
    }

    function renderMediaSliderMain(mainEl, item) {
        if (!mainEl) return;
        mainEl.innerHTML = '';
        resetMediaSliderZoomState();
        if (!item) {
            mainEl.innerHTML = '<div class="os-media-slider-empty"><i class="fas fa-photo-video" aria-hidden="true"></i><span>Sin medios. Pulsa + Añadir para cargar fotos o vídeos.</span></div>';
            return;
        }
        var url = resolveMediaDisplayUrl(item.url);
        if (!url) {
            mainEl.innerHTML = '<div class="os-media-slider-empty"><span>No se pudo cargar el archivo.</span></div>';
            return;
        }
        if (isMediaWidgetVideoItem(item)) {
            var video = document.createElement('video');
            video.src = url;
            video.controls = true;
            video.autoplay = true;
            video.muted = true;
            video.playsInline = true;
            video.setAttribute('aria-label', item.name || 'Vídeo');
            mainEl.appendChild(video);
        } else if (isMobileMode()) {
            var wrap = document.createElement('div');
            wrap.className = 'os-media-slider-image-wrap';
            var mobileImg = document.createElement('img');
            mobileImg.src = url;
            mobileImg.alt = item.name || 'Imagen';
            mobileImg.draggable = false;
            mobileImg.className = 'os-media-slider-zoom-img';
            wrap.appendChild(mobileImg);
            mainEl.appendChild(wrap);
            setupMediaSliderInlineZoom(wrap, mobileImg);
        } else {
            var img = document.createElement('img');
            img.src = url;
            img.alt = item.name || 'Imagen';
            img.draggable = false;
            img.className = 'os-media-slider-open-preview';
            img.title = mediaWidgetTr('open', 'Abrir');
            img.addEventListener('click', function (e) {
                e.preventDefault();
                e.stopPropagation();
                openMediaWidgetImagePreview(item);
            });
            mainEl.appendChild(img);
        }
    }

    function renderMediaSliderThumbs(thumbsEl, items, activeIdx) {
        if (!thumbsEl) return;
        thumbsEl.innerHTML = '';
        items.forEach(function (item, idx) {
            var btn = document.createElement('button');
            btn.type = 'button';
            btn.className = 'os-media-slider-thumb' + (idx === activeIdx ? ' is-active' : '');
            btn.setAttribute('role', 'tab');
            btn.setAttribute('aria-selected', idx === activeIdx ? 'true' : 'false');
            btn.title = item.name || '';
            btn.dataset.index = String(idx);
            var url = resolveMediaDisplayUrl(item.url);
            if (isMediaWidgetVideoItem(item)) {
                btn.innerHTML = '<span class="os-media-slider-thumb__video"><i class="fas fa-film" aria-hidden="true"></i></span>';
            } else if (url) {
                var thumbImg = document.createElement('img');
                thumbImg.src = url;
                thumbImg.alt = '';
                thumbImg.loading = 'lazy';
                btn.appendChild(thumbImg);
            }
            btn.addEventListener('click', function (e) {
                e.preventDefault();
                e.stopPropagation();
                showMediaSliderAt(idx);
            });
            thumbsEl.appendChild(btn);
        });
    }

    function updateMediaSliderCounter(counterEl, items, activeIdx) {
        if (!counterEl) return;
        if (!items.length) {
            counterEl.textContent = '';
            return;
        }
        counterEl.textContent = 'Medio ' + (activeIdx + 1) + ' de ' + items.length;
    }

    function updateMediaSliderTrashButtons(items) {
        var hasItems = items && items.length > 0;
        ['osMediaSliderTrash', 'osMediaSliderTrashMobile'].forEach(function (id) {
            var btn = document.getElementById(id);
            if (!btn) return;
            btn.disabled = !hasItems;
            btn.setAttribute('aria-disabled', hasItems ? 'false' : 'true');
        });
    }

    function renderMediaSlider() {
        var items = getMediaWidgetItems();
        clampMediaSliderIndex(items);
        var seq = ++mediaSliderShowSeq;
        var active = items[mediaSliderIndex] || null;
        getMediaSliderRoots().forEach(function (root) {
            renderMediaSliderMain(root.main, active);
            renderMediaSliderThumbs(root.thumbs, items, mediaSliderIndex);
            updateMediaSliderCounter(root.counter, items, mediaSliderIndex);
            if (root.thumbs && active) {
                var activeThumb = root.thumbs.querySelector('.os-media-slider-thumb.is-active');
                if (activeThumb && typeof activeThumb.scrollIntoView === 'function') {
                    try { activeThumb.scrollIntoView({ block: 'nearest', inline: 'nearest' }); } catch (_) {}
                }
            }
        });
        updateMediaSliderTrashButtons(items);
        if (seq !== mediaSliderShowSeq) return;
    }

    function showMediaSliderAt(index) {
        var items = getMediaWidgetItems();
        if (!items.length) return;
        mediaSliderIndex = Math.max(0, Math.min(index, items.length - 1));
        renderMediaSlider();
    }

    function stepMediaSlider(dir) {
        var items = getMediaWidgetItems();
        if (!items.length) return;
        var next = mediaSliderIndex + dir;
        if (next < 0) next = items.length - 1;
        if (next >= items.length) next = 0;
        showMediaSliderAt(next);
    }

    function mediaWidgetFileName(item) {
        if (!item) return '';
        var name = String(item.name || '').trim();
        if (name) return name;
        var url = String(item.url || '');
        var parts = url.split('/').filter(Boolean);
        if (!parts.length) return '';
        try {
            return decodeURIComponent(parts[parts.length - 1]);
        } catch (_) {
            return parts[parts.length - 1];
        }
    }

    function removeMediaWidgetItemAt(index) {
        var items = getMediaWidgetItems().slice();
        if (index < 0 || index >= items.length) return;
        items.splice(index, 1);
        if (mediaSliderIndex >= items.length) mediaSliderIndex = Math.max(0, items.length - 1);
        persistMediaWidgetItems(items).then(function () { renderMediaSlider(); });
    }

    async function confirmAndTrashMediaWidgetItemAt(index) {
        var items = getMediaWidgetItems();
        if (index < 0 || index >= items.length) return;
        var item = items[index];
        var fileName = mediaWidgetFileName(item);
        if (!fileName) {
            showNotesToast('No se pudo identificar el archivo.', 'error');
            return;
        }
        var A = api();
        var msg = mediaWidgetTr('explorerConfirmDeleteToTrash', '¿Eliminar y mover a la papelera?');
        var ok = true;
        if (window.appDialog && typeof window.appDialog.confirm === 'function') {
            ok = await window.appDialog.confirm(msg, {
                type: 'trash',
                title: 'Mover a la papelera',
                okLabel: 'Mover a papelera',
                cancelLabel: 'Cancelar'
            });
        } else {
            ok = window.confirm(msg);
        }
        if (!ok) return;
        if (!A || typeof A.explorerDeleteToTrash !== 'function') {
            showNotesToast('No se pudo mover a la papelera.', 'error');
            return;
        }
        try {
            var result = await A.explorerDeleteToTrash(MEDIA_WIDGET_UPLOAD_PATH, fileName, 'files');
            if (!result || !result.success) {
                showNotesToast((result && result.error) || 'No se pudo mover a la papelera.', 'error');
                return;
            }
            removeMediaWidgetItemAt(index);
            if (typeof A.syncExplorerTrashUi === 'function') {
                await A.syncExplorerTrashUi();
            }
            showNotesToast('«' + fileName + '» movido a la papelera.', 'success');
        } catch (_) {
            showNotesToast('Error al mover a la papelera.', 'error');
        }
    }

    function showMediaWidgetContextMenu(event) {
        var items = getMediaWidgetItems();
        if (!items.length) return;
        var item = items[mediaSliderIndex];
        var A = api();
        if (!A || typeof A.showContextMenu !== 'function') return;
        var explorerItem = mediaWidgetExplorerItem(item);
        var kind = (typeof A.getExplorerWallpaperMediaKind === 'function')
            ? A.getExplorerWallpaperMediaKind(explorerItem)
            : null;
        if (!kind || !explorerItem) return;
        var opts = [
            {
                className: 'wallpaper-apply',
                icon: kind === 'video' ? 'fas fa-film' : 'fas fa-image',
                label: kind === 'video'
                    ? mediaWidgetTr('wallpaperApplyDesktop', 'Aplicar como fondo de escritorio')
                    : mediaWidgetTr('wallpaperSetFixed', 'Fijar de fondo de pantalla'),
                action: function () {
                    if (typeof A.applyExplorerWallpaper === 'function') void A.applyExplorerWallpaper(explorerItem);
                }
            },
            {
                className: 'wallpaper-add-list',
                icon: 'fas fa-images',
                label: kind === 'video'
                    ? mediaWidgetTr('wallpaperAddToListVideos', 'Añadir a lista de fondo')
                    : mediaWidgetTr('wallpaperAddToListImages', 'Añadir a lista de fondos de pantalla'),
                action: function () {
                    if (typeof A.addExplorerWallpaperToList === 'function') void A.addExplorerWallpaperToList(explorerItem);
                }
            },
            'separator',
            {
                className: 'media-widget-share',
                icon: 'fas fa-share-alt',
                label: mediaWidgetTr('share', 'Compartir'),
                action: function () { shareMediaWidgetItem(item); }
            },
            'separator',
            {
                className: 'media-widget-remove',
                icon: 'fas fa-trash',
                label: 'Quitar del slider',
                action: function () { void confirmAndTrashMediaWidgetItemAt(mediaSliderIndex); }
            }
        ];
        A.showContextMenu(opts, event);
    }

    function isAcceptedMediaUpload(file) {
        if (!file) return false;
        var type = String(file.type || '').toLowerCase();
        if (type.startsWith('image/') || type.startsWith('video/')) return true;
        return /\.(jpe?g|png|gif|webp|bmp|svg|mp4|webm|ogg|mov|mkv|m4v)$/i.test(file.name || '');
    }

    function newMediaWidgetItemId() {
        return 'mw_' + Date.now() + '_' + Math.random().toString(36).slice(2, 8);
    }

    function fetchMediaWidgetExplorerRows() {
        return fetch('?action=explorerList', {
            method: 'POST',
            headers: { 'Content-Type': 'application/json' },
            body: JSON.stringify({ bucket: 'files', path: MEDIA_WIDGET_UPLOAD_PATH })
        }).then(function (r) { return r.json(); }).then(function (data) {
            if (!data || !data.success || !Array.isArray(data.items)) return [];
            return data.items.filter(function (it) {
                return it && it.type === 'file' && isAcceptedMediaUpload({ type: it.mimeType || '', name: it.name || '' });
            });
        }).catch(function () { return []; });
    }

    function uploadMediaWidgetFile(file) {
        var formData = new FormData();
        formData.append('path', MEDIA_WIDGET_UPLOAD_PATH);
        formData.append('bucket', 'files');
        formData.append('files[]', file, file.name);
        return fetch('?action=explorerUpload', { method: 'POST', body: formData })
            .then(function (r) { return r.json(); });
    }

    function isMediaWidgetGalleryPath(path) {
        var p = String(path || '').replace(/\\/g, '/').replace(/^\/+/, '');
        return p === MEDIA_WIDGET_UPLOAD_PATH || p.endsWith('/' + MEDIA_WIDGET_UPLOAD_PATH);
    }

    function syncMediaWidgetItemsWithExplorerFolder() {
        return fetchMediaWidgetExplorerRows().then(function (rows) {
            if (!rows.length) return false;
            var items = getMediaWidgetItems().slice();
            var existingUrls = {};
            var existingNames = {};
            items.forEach(function (it) {
                if (it && it.url) existingUrls[it.url] = true;
                if (it && it.name) existingNames[it.name] = true;
            });
            var changed = false;
            rows.forEach(function (row) {
                if (!row || !row.url) return;
                if (existingUrls[row.url] || (row.name && existingNames[row.name])) return;
                items.push({
                    id: newMediaWidgetItemId(),
                    url: row.url,
                    name: row.name || '',
                    mimeType: row.mimeType || ''
                });
                existingUrls[row.url] = true;
                if (row.name) existingNames[row.name] = true;
                changed = true;
            });
            if (!changed) return false;
            mediaSliderIndex = Math.max(0, items.length - 1);
            return persistMediaWidgetItems(items).then(function () { return true; });
        }).catch(function () { return false; });
    }

    function restoreMediaWidgetItemFromTrash(entry, restoredName) {
        if (!entry || entry.kind !== 'file') return Promise.resolve(false);
        if (!isMediaWidgetGalleryPath(entry.originalPath)) return Promise.resolve(false);
        var fileName = String(restoredName || entry.name || '').trim();
        if (!fileName) return Promise.resolve(false);
        if (!isAcceptedMediaUpload({ type: entry.mimeType || '', name: fileName })) return Promise.resolve(false);

        return fetchMediaWidgetExplorerRows().then(function (rows) {
            var row = rows.find(function (r) { return r && r.name === fileName; });
            if (!row || !row.url) return false;
            var items = getMediaWidgetItems().slice();
            var exists = items.some(function (it) {
                return it && (it.url === row.url || it.name === fileName);
            });
            if (exists) {
                renderMediaSlider();
                return false;
            }
            items.push({
                id: newMediaWidgetItemId(),
                url: row.url,
                name: row.name || fileName,
                mimeType: row.mimeType || entry.mimeType || ''
            });
            mediaSliderIndex = items.length - 1;
            return persistMediaWidgetItems(items).then(function () {
                renderMediaSlider();
                showNotesToast('«' + fileName + '» restaurado en la galería.', 'success');
                return true;
            });
        }).catch(function () { return false; });
    }

    function mergeMediaWidgetItemsFromRows(rows, storedNames) {
        var items = getMediaWidgetItems().slice();
        var existingUrls = {};
        items.forEach(function (it) { if (it && it.url) existingUrls[it.url] = true; });
        storedNames.forEach(function (storedAs) {
            var row = rows.find(function (r) { return r.name === storedAs; });
            if (!row || !row.url || existingUrls[row.url]) return;
            items.push({
                id: newMediaWidgetItemId(),
                url: row.url,
                name: row.name || storedAs,
                mimeType: row.mimeType || ''
            });
            existingUrls[row.url] = true;
        });
        return items;
    }

    function handleMediaWidgetUploadInput(input) {
        if (!input || !input.files || !input.files.length) return;
        var files = Array.from(input.files).filter(isAcceptedMediaUpload);
        input.value = '';
        if (!files.length) return;
        var uploadedNames = [];
        var chain = Promise.resolve();
        files.forEach(function (file) {
            chain = chain.then(function () {
                return uploadMediaWidgetFile(file).then(function (data) {
                    var res = data && data.results && data.results[0];
                    if (res && res.success && res.storedAs) uploadedNames.push(res.storedAs);
                });
            });
        });
        chain.then(function () {
            if (!uploadedNames.length) {
                showNotesToast('No se pudieron subir los archivos.', 'error');
                return;
            }
            return fetchMediaWidgetExplorerRows().then(function (rows) {
                var merged = mergeMediaWidgetItemsFromRows(rows, uploadedNames);
                mediaSliderIndex = Math.max(0, merged.length - 1);
                return persistMediaWidgetItems(merged).then(function () {
                    renderMediaSlider();
                    showNotesToast(uploadedNames.length === 1 ? 'Archivo añadido a la galería.' : 'Archivos añadidos a la galería.', 'success');
                });
            });
        }).catch(function () {
            showNotesToast('Error al subir archivos.', 'error');
        });
    }

    function bindMediaSliderSwipe(viewport) {
        if (!viewport || viewport.dataset.osMediaSwipeBound === '1') return;
        viewport.dataset.osMediaSwipeBound = '1';
        var startX = 0;
        var startY = 0;
        var tracking = false;
        viewport.addEventListener('pointerdown', function (e) {
            if (e.button !== 0) return;
            if (e.target.closest('.os-notes-nav, .os-notes-add, .os-media-slider-trash, video, button')) return;
            if (mediaSliderZoomState.isZoomed) return;
            tracking = true;
            startX = e.clientX;
            startY = e.clientY;
        });
        viewport.addEventListener('pointerup', function (e) {
            if (!tracking) return;
            tracking = false;
            if (mediaSliderZoomState.isZoomed) return;
            var dx = e.clientX - startX;
            var dy = e.clientY - startY;
            if (Math.abs(dx) < 40 || Math.abs(dx) < Math.abs(dy)) return;
            stepMediaSlider(dx < 0 ? 1 : -1);
        });
        viewport.addEventListener('pointercancel', function () { tracking = false; });
        viewport.addEventListener('contextmenu', function (e) {
            if (!getMediaWidgetItems().length) return;
            e.preventDefault();
            showMediaWidgetContextMenu(e);
        });
    }

    function bindMediaWidgetControls() {
        [
            ['osMediaSliderPrev', -1],
            ['osMediaSliderNext', 1],
            ['osMediaSliderPrevMobile', -1],
            ['osMediaSliderNextMobile', 1]
        ].forEach(function (pair) {
            var el = document.getElementById(pair[0]);
            if (!el || el.dataset.osMediaNavBound === '1') return;
            el.dataset.osMediaNavBound = '1';
            el.addEventListener('click', function (e) {
                e.preventDefault();
                e.stopPropagation();
                stepMediaSlider(pair[1]);
            });
        });

        [
            ['osMediaSliderAdd', 'osMediaSliderUpload'],
            ['osMediaSliderAddMobile', 'osMediaSliderUploadMobile']
        ].forEach(function (pair) {
            var addBtn = document.getElementById(pair[0]);
            var input = document.getElementById(pair[1]);
            if (!addBtn || !input || addBtn.dataset.osMediaAddBound === '1') return;
            addBtn.dataset.osMediaAddBound = '1';
            addBtn.addEventListener('click', function (e) {
                e.preventDefault();
                e.stopPropagation();
                input.click();
            });
            input.addEventListener('change', function () { handleMediaWidgetUploadInput(input); });
        });

        ['osMediaSliderTrash', 'osMediaSliderTrashMobile'].forEach(function (id) {
            var trashBtn = document.getElementById(id);
            if (!trashBtn || trashBtn.dataset.osMediaTrashBound === '1') return;
            trashBtn.dataset.osMediaTrashBound = '1';
            trashBtn.addEventListener('click', function (e) {
                e.preventDefault();
                e.stopPropagation();
                var items = getMediaWidgetItems();
                if (!items.length) return;
                void confirmAndTrashMediaWidgetItemAt(mediaSliderIndex);
            });
        });

        getMediaSliderRoots().forEach(function (root) {
            bindMediaSliderSwipe(root.viewport);
        });
    }

    function scrollNotes(dir) {
        const wrap = activeNotesWrapForScroll();
        if (!wrap) return;
        const w = Math.max(120, wrap.clientWidth * 0.85);
        wrap.scrollBy({ left: dir * w, behavior: 'smooth' });
    }

    function notesCarouselActiveCard(wrap) {
        if (!wrap) return null;
        var visible = notesCarouselVisibleCards(wrap);
        if (!visible.length) return null;
        var scrollLeft = wrap.scrollLeft;
        var idx = 0;
        for (var v = 0; v < visible.length; v++) {
            if (visible[v].offsetLeft <= scrollLeft + 6) idx = v;
        }
        return visible[idx] || null;
    }

    function getActiveNoteCard() {
        var wrap = activeNotesWrapForScroll();
        return wrap ? notesCarouselActiveCard(wrap) : null;
    }

    function isNoteCardEmpty(card) {
        if (!card) return false;
        var titleInp = card.querySelector('.os-note-title');
        var ta = card.querySelector('.os-note-ta');
        var title = titleInp ? String(titleInp.value || '').trim() : '';
        var text = ta ? String(ta.value || '').trim() : '';
        return !title && !text;
    }

    function canAddNote() {
        const A = api();
        if (!A) return false;
        var list = A.getQuickNotes() || [];
        if (!list.length) return true;
        return !isNoteCardEmpty(getActiveNoteCard());
    }

    function showNotesToast(message, type) {
        var msg = String(message || '');
        try {
            if (typeof window.showToast === 'function') {
                window.showToast(msg, 2200, type);
                return;
            }
        } catch (_) {}
        var resolvedType = type;
        if (!resolvedType && window.appDialog && typeof window.appDialog.detectFeedbackType === 'function') {
            resolvedType = window.appDialog.detectFeedbackType(msg, 'alert');
        }
        resolvedType = resolvedType || 'info';
        var iconClass = (window.appDialog && typeof window.appDialog.feedbackIconClass === 'function')
            ? window.appDialog.feedbackIconClass(resolvedType)
            : 'fas fa-circle-info';
        var toast = document.createElement('div');
        toast.className = 'toast';
        toast.dataset.type = resolvedType;
        var iconEl = document.createElement('i');
        iconEl.className = iconClass + ' toast-icon';
        iconEl.setAttribute('aria-hidden', 'true');
        var textEl = document.createElement('span');
        textEl.className = 'toast-text';
        textEl.textContent = msg;
        toast.appendChild(iconEl);
        toast.appendChild(textEl);
        document.body.appendChild(toast);
        setTimeout(function () {
            if (toast.parentNode) toast.parentNode.removeChild(toast);
        }, 2200);
    }

    function shakeNoteCard(card) {
        if (!card) return;
        card.classList.remove('os-note-card--shake');
        void card.offsetWidth;
        card.classList.add('os-note-card--shake');
        function done() {
            card.classList.remove('os-note-card--shake');
            card.removeEventListener('animationend', done);
        }
        card.addEventListener('animationend', done);
    }

    function notifyEmptyNoteCannotAdd() {
        shakeNoteCard(getActiveNoteCard());
        showNotesToast('La nota está vacía. Escribe un título o contenido antes de añadir otra.');
    }

    function addNote() {
        const A = api();
        if (!A) return;
        if (!canAddNote()) {
            notifyEmptyNoteCannotAdd();
            return;
        }
        const list = (A.getQuickNotes() || []).slice();
        list.push({
            id: 'n_' + Date.now(),
            text: '',
            title: '',
            bgColor: '',
            textColor: '',
            updatedAt: Date.now()
        });
        A.setQuickNotes(list);
        A.saveSettings({ quickNotes: list }).then(function () {
            renderNotesCarousel();
            const wrap = activeNotesWrapForScroll();
            if (wrap) wrap.scrollLeft = wrap.scrollWidth;
        });
    }

    function notesModalVisible() {
        const m = document.getElementById('osNotesMobileModal');
        return !!(m && (m.style.display === 'block' || m.style.display === 'flex'));
    }

    function mediaModalVisible() {
        const m = document.getElementById('osMediaMobileModal');
        return !!(m && (m.style.display === 'block' || m.style.display === 'flex'));
    }

    function systemModalVisible() {
        const m = document.getElementById('osSystemMobileModal');
        return !!(m && (m.style.display === 'block' || m.style.display === 'flex'));
    }

    var LS_OS_SYSTEM_COLLAPSED = 'antara_os_system_widget_collapsed';
    var LS_OS_NOTES_COLLAPSED = 'antara_os_notes_widget_collapsed';
    /** Sesiones activas de arrastre de widgets (panel + cabecera móvil). */
    var osWidgetDragSessions = [];

    var LS_OS_WEATHER_COLLAPSED = 'antara_os_weather_widget_collapsed';
    var LS_OS_MEDIA_COLLAPSED = 'antara_os_media_widget_collapsed';
    var LS_OS_SPOTIFY_COLLAPSED = 'antara_os_spotify_widget_collapsed';
    var LS_OS_WIDGETS_COLLAPSED_INIT = 'antara_os_widgets_collapsed_init_v1';
    var LS_WEATHER_CACHE = 'antara_os_weather_cache_v1';
    var WEATHER_REFRESH_MIN_MS = 15 * 60 * 1000;
    var WEATHER_FETCH_TIMEOUT_MS = 12000;
    var OS_WIDGET_DEFAULT_ORDER = ['spotify', 'system', 'weather', 'notes', 'media'];
    var OS_WIDGET_DOM_IDS = {
        system: 'osWidgetSystem',
        weather: 'osWidgetWeather',
        notes: 'osWidgetNotes',
        spotify: 'osWidgetSpotify',
        media: 'osWidgetMedia'
    };

    function normalizeOsWidgetOrder(order) {
        var valid = OS_WIDGET_DEFAULT_ORDER.slice();
        var seen = {};
        var out = [];
        (Array.isArray(order) ? order : []).forEach(function (k) {
            if (valid.indexOf(k) >= 0 && !seen[k]) {
                seen[k] = true;
                out.push(k);
            }
        });
        valid.forEach(function (k) {
            if (!seen[k]) out.push(k);
        });
        if (out.length === 4 && out[0] === 'system' && out[1] === 'weather' && out[2] === 'notes' && out[3] === 'spotify') {
            return OS_WIDGET_DEFAULT_ORDER.slice();
        }
        return out;
    }

    function ensureOsWidgetOrderOnInit() {
        var A = api();
        var raw = (A && typeof A.getSetting === 'function')
            ? A.getSetting('osWidgetOrder', OS_WIDGET_DEFAULT_ORDER)
            : OS_WIDGET_DEFAULT_ORDER;
        var normalized = normalizeOsWidgetOrder(raw);
        applyOsWidgetOrder(normalized);
        if (A && typeof A.saveSettings === 'function' && JSON.stringify(raw) !== JSON.stringify(normalized)) {
            persistOsWidgetOrder(normalized);
        }
    }

    function getOsWidgetOrder() {
        var A = api();
        if (A && typeof A.getSetting === 'function') {
            return normalizeOsWidgetOrder(A.getSetting('osWidgetOrder', OS_WIDGET_DEFAULT_ORDER));
        }
        return OS_WIDGET_DEFAULT_ORDER.slice();
    }

    function syncOsWidgetMobileHeaderOrder(order) {
        var mobileStart = document.querySelector('.os-shell__head-mobile-start');
        if (!mobileStart) return;
        normalizeOsWidgetOrder(order).forEach(function (key) {
            var chip = mobileStart.querySelector('.os-mobile-widget-chip[data-os-widget-key="' + key + '"]');
            if (chip) mobileStart.appendChild(chip);
        });
    }

    function applyOsWidgetOrder(order) {
        var panel = document.getElementById('osWidgetPanel');
        if (!panel) return;
        var ord = normalizeOsWidgetOrder(order);
        ord.forEach(function (key) {
            var el = document.getElementById(OS_WIDGET_DOM_IDS[key]);
            if (el) panel.appendChild(el);
        });
        syncOsWidgetMobileHeaderOrder(ord);
    }

    function readOsWidgetOrderFromDom() {
        var panel = document.getElementById('osWidgetPanel');
        if (!panel) return OS_WIDGET_DEFAULT_ORDER.slice();
        var order = [];
        panel.querySelectorAll('.os-widget[data-os-widget-key]').forEach(function (el) {
            var key = el.getAttribute('data-os-widget-key');
            if (key) order.push(key);
        });
        return normalizeOsWidgetOrder(order);
    }

    function persistOsWidgetOrder(order) {
        var A = api();
        if (!A || typeof A.saveSettings !== 'function') return;
        var normalized = normalizeOsWidgetOrder(order);
        A.saveSettings({ osWidgetOrder: normalized });
    }

    function readOsWidgetOrderFromMobileHeader() {
        var container = document.querySelector('.os-shell__head-mobile-start');
        if (!container) return OS_WIDGET_DEFAULT_ORDER.slice();
        var order = [];
        container.querySelectorAll('.os-mobile-widget-chip[data-os-widget-key]').forEach(function (el) {
            var key = el.getAttribute('data-os-widget-key');
            if (key) order.push(key);
        });
        return normalizeOsWidgetOrder(order);
    }

    function getOsWidgetSortItems(container, itemSelector, exclude) {
        return Array.prototype.slice.call(container.querySelectorAll(itemSelector)).filter(function (el) {
            return el !== exclude && !el.classList.contains('os-widget-drop-placeholder');
        });
    }

    function getOsWidgetDropAfterElement(container, itemSelector, clientPos, axis, exclude) {
        var els = getOsWidgetSortItems(container, itemSelector, exclude);
        return els.reduce(function (closest, child) {
            var box = child.getBoundingClientRect();
            var offset = axis === 'x'
                ? clientPos - box.left - box.width / 2
                : clientPos - box.top - box.height / 2;
            if (offset < 0 && offset > closest.offset) {
                return { offset: offset, element: child };
            }
            return closest;
        }, { offset: Number.NEGATIVE_INFINITY, element: null }).element;
    }

    /**
     * Reordenamiento desde la cabecera/chip: placeholder + position:fixed (un commit al soltar).
     */
    function mountOsWidgetPointerReorder(config) {
        var container = config.container;
        if (!container || container.dataset.osPointerReorderBound === '1') return;
        container.dataset.osPointerReorderBound = '1';

        var dragThreshold = typeof config.dragThreshold === 'number' ? config.dragThreshold : 8;
        var blockSelectors = config.blockSelectors || '.os-widget-system-toggle, .os-widget-notes-toggle, .os-widget-weather-toggle, .os-widget-spotify-toggle, .os-widget-media-toggle';

        var state = {
            active: false,
            armed: false,
            didDrag: false,
            item: null,
            captureEl: null,
            zone: null,
            placeholder: null,
            pointerId: null,
            lastDropKey: null,
            raf: 0,
            moveX: 0,
            moveY: 0,
            startPointerX: 0,
            startPointerY: 0,
            liftLeft: 0,
            liftTop: 0,
            liftW: 0,
            liftH: 0
        };

        function enabled() {
            return typeof config.isEnabled !== 'function' || config.isEnabled();
        }

        function ensurePlaceholder() {
            if (!state.placeholder) {
                state.placeholder = document.createElement('div');
                state.placeholder.className = 'os-widget-drop-placeholder';
                state.placeholder.setAttribute('aria-hidden', 'true');
            }
            return state.placeholder;
        }

        function dropTargetKey(afterEl) {
            if (!afterEl) return '__end__';
            return afterEl.getAttribute('data-os-widget-key')
                || afterEl.getAttribute('id')
                || afterEl.className;
        }

        function updatePlaceholder(afterEl) {
            var ph = ensurePlaceholder();
            var key = dropTargetKey(afterEl);
            if (key === state.lastDropKey && ph.parentNode === container) return;
            state.lastDropKey = key;
            if (afterEl == null) container.appendChild(ph);
            else container.insertBefore(ph, afterEl);
        }

        function clearLift() {
            if (!state.item) return;
            var el = state.item;
            el.classList.remove('os-widget-dragging-lift');
            el.style.removeProperty('position');
            el.style.removeProperty('left');
            el.style.removeProperty('top');
            el.style.removeProperty('width');
            el.style.removeProperty('height');
            el.style.removeProperty('z-index');
            el.style.removeProperty('margin');
            el.style.removeProperty('pointer-events');
            el.style.removeProperty('transform');
            el.removeAttribute('aria-grabbed');
        }

        function releasePointerCaptureSafe() {
            if (state.pointerId == null) return;
            var targets = [state.captureEl, document.body];
            targets.forEach(function (el) {
                if (!el || typeof el.releasePointerCapture !== 'function') return;
                try {
                    if (el.hasPointerCapture && el.hasPointerCapture(state.pointerId)) {
                        el.releasePointerCapture(state.pointerId);
                    }
                } catch (_) {}
            });
        }

        function resetPointerState() {
            state.active = false;
            state.armed = false;
            state.item = null;
            state.captureEl = null;
            state.zone = null;
            state.placeholder = null;
            state.pointerId = null;
            state.lastDropKey = null;
            state.didDrag = false;
        }

        function unbindGlobalPointer() {
            document.removeEventListener('pointermove', onGlobalPointerMove, true);
            document.removeEventListener('pointerup', onGlobalPointerUp, true);
            document.removeEventListener('pointercancel', onGlobalPointerCancel, true);
        }

        function finishDrag(commit) {
            if (state.raf) {
                cancelAnimationFrame(state.raf);
                state.raf = 0;
            }
            unbindGlobalPointer();
            container.classList.remove(config.dragOverClass || 'os-widget-reorder--active');

            var wasActive = state.active;
            if (wasActive && state.item && state.placeholder && state.placeholder.parentNode) {
                container.insertBefore(state.item, state.placeholder);
            }
            if (wasActive && commit) {
                var order = typeof config.readOrder === 'function' ? config.readOrder() : readOsWidgetOrderFromDom();
                applyOsWidgetOrder(order);
                persistOsWidgetOrder(order);
            }

            if (state.item && state.item.classList.contains('os-widget-dragging-lift')) {
                clearLift();
            }
            if (state.placeholder && state.placeholder.parentNode) {
                state.placeholder.parentNode.removeChild(state.placeholder);
            }
            releasePointerCaptureSafe();
            resetPointerState();
        }

        function onGlobalPointerMove(e) {
            if (e.pointerId !== state.pointerId) return;
            if (state.active) {
                e.preventDefault();
                schedulePointerMove(e.clientX, e.clientY);
                return;
            }
            if (!state.armed || !state.item || !state.zone) return;
            var dx = e.clientX - state.startPointerX;
            var dy = e.clientY - state.startPointerY;
            if (Math.abs(dx) < dragThreshold && Math.abs(dy) < dragThreshold) return;
            e.preventDefault();
            startDrag(state.item, state.zone, e);
            schedulePointerMove(e.clientX, e.clientY);
        }

        function onGlobalPointerUp(e) {
            if (e.pointerId !== state.pointerId) return;
            if (state.active) {
                if (state.didDrag) suppressClickInsideItem();
                finishDrag(true);
                return;
            }
            if (state.armed) {
                tryHeadTapCollapse(e);
            }
            finishDrag(false);
        }

        function onGlobalPointerCancel(e) {
            if (e.pointerId !== state.pointerId) return;
            finishDrag(state.active);
        }

        function suppressClickInsideItem() {
            if (!state.item) return;
            state.item.querySelectorAll('button, a').forEach(function (el) {
                el.dataset.osDragSuppress = '1';
            });
        }

        function applyPointerPos(clientX, clientY) {
            if (!state.active || !state.item) return;
            state.item.style.left = (state.liftLeft + (clientX - state.startPointerX)) + 'px';
            state.item.style.top = (state.liftTop + (clientY - state.startPointerY)) + 'px';
            var after = getOsWidgetDropAfterElement(
                container,
                config.itemSelector,
                config.axis === 'x' ? clientX : clientY,
                config.axis,
                state.item
            );
            updatePlaceholder(after);
        }

        function schedulePointerMove(clientX, clientY) {
            state.moveX = clientX;
            state.moveY = clientY;
            if (state.raf) return;
            state.raf = requestAnimationFrame(function () {
                state.raf = 0;
                applyPointerPos(state.moveX, state.moveY);
            });
        }

        function startDrag(item, captureEl, e) {
            if (!enabled() || state.active) return;
            var rect = item.getBoundingClientRect();
            state.active = true;
            state.armed = false;
            state.didDrag = true;
            state.item = item;
            state.captureEl = captureEl;
            state.pointerId = e.pointerId;
            state.startPointerX = e.clientX;
            state.startPointerY = e.clientY;
            state.moveX = e.clientX;
            state.moveY = e.clientY;
            state.liftLeft = rect.left;
            state.liftTop = rect.top;
            state.liftW = rect.width;
            state.liftH = rect.height;
            state.lastDropKey = null;

            var ph = ensurePlaceholder();
            ph.style.width = config.axis === 'x' ? state.liftW + 'px' : '';
            ph.style.height = config.axis === 'y' ? state.liftH + 'px' : '';
            ph.style.minWidth = config.axis === 'x' ? state.liftW + 'px' : '';
            ph.style.minHeight = config.axis === 'y' ? state.liftH + 'px' : '';
            container.insertBefore(ph, item);

            item.classList.add('os-widget-dragging-lift');
            item.setAttribute('aria-grabbed', 'true');
            item.style.position = 'fixed';
            item.style.left = state.liftLeft + 'px';
            item.style.top = state.liftTop + 'px';
            item.style.width = state.liftW + 'px';
            item.style.height = state.liftH + 'px';
            item.style.zIndex = '10000';
            item.style.margin = '0';
            item.style.pointerEvents = 'none';

            container.classList.add(config.dragOverClass || 'os-widget-reorder--active');
            try { document.body.setPointerCapture(e.pointerId); } catch (_) {
                try { captureEl.setPointerCapture(e.pointerId); } catch (_2) {}
            }

            applyPointerPos(e.clientX, e.clientY);
        }

        function resolveItem(zone) {
            if (zone.matches(config.itemSelector)) return zone;
            return zone.closest(config.itemSelector);
        }

        function canStartPointerDrag(e) {
            if (!blockSelectors) return true;
            return !e.target.closest(blockSelectors);
        }

        /** Clic corto en la cabecera (sin arrastre): plegar/desplegar vía el botón toggle. */
        function tryHeadTapCollapse(e) {
            if (config.headTapCollapse === false || !state.armed || !state.item || state.didDrag) return;
            var toggleBtn = state.item.querySelector('.os-widget-system-toggle, .os-widget-notes-toggle, .os-widget-weather-toggle, .os-widget-spotify-toggle, .os-widget-media-toggle');
            if (!toggleBtn || e.target.closest(blockSelectors)) return;
            toggleBtn.click();
        }

        container.addEventListener('click', function (e) {
            var el = e.target.closest('[data-os-drag-suppress]');
            if (!el) return;
            e.preventDefault();
            e.stopImmediatePropagation();
            delete el.dataset.osDragSuppress;
        }, true);

        osWidgetDragSessions.push({
            isActive: function () { return state.active || state.armed; },
            abort: function () { finishDrag(false); }
        });

        container.querySelectorAll(config.dragZoneSelector).forEach(function (zone) {
            zone.addEventListener('pointerdown', function (e) {
                if (!enabled() || e.button !== 0 || !canStartPointerDrag(e)) return;
                var item = resolveItem(zone);
                if (!item || !container.contains(item)) return;
                if (state.armed || state.active) finishDrag(false);
                state.armed = true;
                state.didDrag = false;
                state.item = item;
                state.zone = zone;
                state.captureEl = zone;
                state.pointerId = e.pointerId;
                state.startPointerX = e.clientX;
                state.startPointerY = e.clientY;
                unbindGlobalPointer();
                document.addEventListener('pointermove', onGlobalPointerMove, true);
                document.addEventListener('pointerup', onGlobalPointerUp, true);
                document.addEventListener('pointercancel', onGlobalPointerCancel, true);
            });
        });
    }

    function clearOsWidgetDragInlineStyles(el) {
        if (!el) return;
        el.classList.remove('os-widget-dragging-lift');
        el.style.removeProperty('position');
        el.style.removeProperty('left');
        el.style.removeProperty('top');
        el.style.removeProperty('width');
        el.style.removeProperty('height');
        el.style.removeProperty('z-index');
        el.style.removeProperty('margin');
        el.style.removeProperty('pointer-events');
        el.style.removeProperty('transform');
        el.removeAttribute('aria-grabbed');
    }

    function emergencyClearStuckWidgetLift() {
        document.querySelectorAll('.os-widget-dragging-lift').forEach(clearOsWidgetDragInlineStyles);
        var panel = document.getElementById('osWidgetPanel');
        if (panel) {
            panel.querySelectorAll('.os-widget[data-os-widget-key]').forEach(function (el) {
                var pos = (el.style.position || '').toLowerCase();
                if (pos === 'fixed' || pos === 'absolute') clearOsWidgetDragInlineStyles(el);
            });
            panel.classList.remove('os-widget-panel--drag-over');
        }
        document.querySelectorAll('.os-widget-drop-placeholder').forEach(function (ph) {
            if (ph.parentNode) ph.parentNode.removeChild(ph);
        });
        var mobileStart = document.querySelector('.os-shell__head-mobile-start');
        if (mobileStart) mobileStart.classList.remove('os-shell__head-mobile-start--drag-over');
    }

    function abortAllOsWidgetDrags() {
        osWidgetDragSessions.forEach(function (session) {
            if (session && typeof session.isActive === 'function' && session.isActive()) {
                session.abort();
            }
        });
        emergencyClearStuckWidgetLift();
    }

    function bindOsWidgetReorder() {
        var panel = document.getElementById('osWidgetPanel');
        if (panel) {
            mountOsWidgetPointerReorder({
                container: panel,
                itemSelector: '.os-widget[data-os-widget-key]',
                dragZoneSelector: '.os-widget-system__head, .os-widget-notes__head, .os-widget-weather__head, .os-widget-spotify__head, .os-widget-media__head',
                blockSelectors: '.os-widget-system-toggle, .os-widget-notes-toggle, .os-widget-weather-toggle, .os-widget-spotify-toggle, .os-widget-media-toggle, .os-spotify-search__input, .os-spotify-search__field, .os-spotify-tabs, .os-spotify-tab-panel, .os-spotify-media-row, .os-spotify-add-playlist__list, .os-spotify-playlists__tracks, .os-spotify-playlists__back, .os-spotify-playlists__play-all, .os-spotify-playlists__more, .os-spotify-card__controls, .os-spotify-card__volume, .os-spotify-card__progress, .os-media-slider-wrap, .os-media-slider-thumb, .os-media-slider-main video',
                axis: 'y',
                headTapCollapse: true,
                dragOverClass: 'os-widget-panel--drag-over',
                isEnabled: function () { return !isMobileMode(); },
                readOrder: readOsWidgetOrderFromDom
            });
        }

        var mobileStart = document.querySelector('.os-shell__head-mobile-start');
        if (mobileStart) {
            mountOsWidgetPointerReorder({
                container: mobileStart,
                itemSelector: '.os-mobile-widget-chip[data-os-widget-key]',
                dragZoneSelector: '.os-mobile-widget-chip[data-os-widget-key]',
                axis: 'x',
                dragThreshold: 10,
                blockSelectors: '',
                headTapCollapse: false,
                dragOverClass: 'os-shell__head-mobile-start--drag-over',
                isEnabled: useCompactNotesLayout,
                readOrder: readOsWidgetOrderFromMobileHeader
            });
        }
    }

    function ensureOsWidgetsCollapsedOnFirstRun() {
        try {
            if (localStorage.getItem(LS_OS_WIDGETS_COLLAPSED_INIT) === '1') return;
            [
                LS_OS_SYSTEM_COLLAPSED,
                LS_OS_NOTES_COLLAPSED,
                LS_OS_WEATHER_COLLAPSED,
                LS_OS_SPOTIFY_COLLAPSED,
                LS_OS_MEDIA_COLLAPSED
            ].forEach(function (key) {
                if (localStorage.getItem(key) === null) {
                    localStorage.setItem(key, '1');
                }
            });
            localStorage.setItem(LS_OS_WIDGETS_COLLAPSED_INIT, '1');
        } catch (_) {}
    }

    function syncSystemWidgetCollapsedUi(section, btn, collapsed) {
        if (!section || !btn) return;
        section.classList.toggle('os-widget--system-collapsed', collapsed);
        btn.setAttribute('aria-expanded', collapsed ? 'false' : 'true');
        btn.title = collapsed ? 'Maximizar sección' : 'Minimizar sección';
        clearOsWidgetDragInlineStyles(section);
    }

    function syncNotesWidgetCollapsedUi(section, btn, collapsed) {
        if (!section || !btn) return;
        section.classList.toggle('os-widget--notes-collapsed', collapsed);
        btn.setAttribute('aria-expanded', collapsed ? 'false' : 'true');
        btn.title = collapsed ? 'Maximizar sección' : 'Minimizar sección';
        clearOsWidgetDragInlineStyles(section);
    }

    function bindSystemWidgetCollapse() {
        var section = document.getElementById('osWidgetSystem');
        var btn = document.getElementById('osWidgetSystemToggle');
        if (!section || !btn || btn.dataset.osCollapseBound === '1') return;
        btn.dataset.osCollapseBound = '1';

        function readCollapsed() {
            try {
                return localStorage.getItem(LS_OS_SYSTEM_COLLAPSED) === '1';
            } catch (_) {
                return false;
            }
        }

        function writeCollapsed(v) {
            try {
                localStorage.setItem(LS_OS_SYSTEM_COLLAPSED, v ? '1' : '0');
            } catch (_) {}
        }

        function applyCollapsed(collapsed) {
            syncSystemWidgetCollapsedUi(section, btn, collapsed);
            writeCollapsed(collapsed);
        }

        applyCollapsed(readCollapsed());

        btn.addEventListener('click', function () {
            if (isMobileMode()) return;
            applyCollapsed(!section.classList.contains('os-widget--system-collapsed'));
        });
    }

    function bindNotesWidgetCollapse() {
        var section = document.getElementById('osWidgetNotes');
        var btn = document.getElementById('osWidgetNotesToggle');
        if (!section || !btn || btn.dataset.osCollapseBound === '1') return;
        btn.dataset.osCollapseBound = '1';

        function readCollapsed() {
            try {
                return localStorage.getItem(LS_OS_NOTES_COLLAPSED) === '1';
            } catch (_) {
                return false;
            }
        }

        function writeCollapsed(v) {
            try {
                localStorage.setItem(LS_OS_NOTES_COLLAPSED, v ? '1' : '0');
            } catch (_) {}
        }

        function applyCollapsed(collapsed) {
            syncNotesWidgetCollapsedUi(section, btn, collapsed);
            writeCollapsed(collapsed);
        }

        applyCollapsed(readCollapsed());

        btn.addEventListener('click', function () {
            if (isMobileMode()) return;
            applyCollapsed(!section.classList.contains('os-widget--notes-collapsed'));
        });
    }

    function syncMediaWidgetCollapsedUi(section, btn, collapsed) {
        if (!section || !btn) return;
        section.classList.toggle('os-widget--media-collapsed', collapsed);
        btn.setAttribute('aria-expanded', collapsed ? 'false' : 'true');
        btn.title = collapsed ? 'Maximizar sección' : 'Minimizar sección';
        clearOsWidgetDragInlineStyles(section);
    }

    function bindMediaWidgetCollapse() {
        var section = document.getElementById('osWidgetMedia');
        var btn = document.getElementById('osWidgetMediaToggle');
        if (!section || !btn || btn.dataset.osCollapseBound === '1') return;
        btn.dataset.osCollapseBound = '1';

        function readCollapsed() {
            try {
                return localStorage.getItem(LS_OS_MEDIA_COLLAPSED) === '1';
            } catch (_) {
                return false;
            }
        }

        function writeCollapsed(v) {
            try {
                localStorage.setItem(LS_OS_MEDIA_COLLAPSED, v ? '1' : '0');
            } catch (_) {}
        }

        function applyCollapsed(collapsed) {
            syncMediaWidgetCollapsedUi(section, btn, collapsed);
            writeCollapsed(collapsed);
        }

        applyCollapsed(readCollapsed());

        btn.addEventListener('click', function () {
            if (isMobileMode()) return;
            applyCollapsed(!section.classList.contains('os-widget--media-collapsed'));
        });
    }

    function syncWeatherWidgetCollapsedUi(section, btn, collapsed) {
        if (!section || !btn) return;
        section.classList.toggle('os-widget--weather-collapsed', collapsed);
        btn.setAttribute('aria-expanded', collapsed ? 'false' : 'true');
        btn.title = collapsed ? 'Maximizar sección' : 'Minimizar sección';
        clearOsWidgetDragInlineStyles(section);
    }

    function bindWeatherWidgetCollapse() {
        var section = document.getElementById('osWidgetWeather');
        var btn = document.getElementById('osWidgetWeatherToggle');
        if (!section || !btn || btn.dataset.osCollapseBound === '1') return;
        btn.dataset.osCollapseBound = '1';

        function readCollapsed() {
            try {
                return localStorage.getItem(LS_OS_WEATHER_COLLAPSED) === '1';
            } catch (_) {
                return false;
            }
        }

        function writeCollapsed(v) {
            try {
                localStorage.setItem(LS_OS_WEATHER_COLLAPSED, v ? '1' : '0');
            } catch (_) {}
        }

        function applyCollapsed(collapsed) {
            syncWeatherWidgetCollapsedUi(section, btn, collapsed);
            writeCollapsed(collapsed);
        }

        applyCollapsed(readCollapsed());

        btn.addEventListener('click', function () {
            if (isMobileMode()) return;
            applyCollapsed(!section.classList.contains('os-widget--weather-collapsed'));
        });
    }

    var weatherRefreshInFlight = false;
    var weatherLastAttemptAt = 0;

    var WMO_WEATHER = {
        0: { label: 'Despejado', icon: 'fa-sun' },
        1: { label: 'Mayormente despejado', icon: 'fa-sun' },
        2: { label: 'Parcialmente nublado', icon: 'fa-cloud-sun' },
        3: { label: 'Nublado', icon: 'fa-cloud' },
        45: { label: 'Niebla', icon: 'fa-smog' },
        48: { label: 'Niebla helada', icon: 'fa-smog' },
        51: { label: 'Llovizna ligera', icon: 'fa-cloud-rain' },
        53: { label: 'Llovizna', icon: 'fa-cloud-rain' },
        55: { label: 'Llovizna intensa', icon: 'fa-cloud-showers-heavy' },
        56: { label: 'Llovizna helada', icon: 'fa-cloud-rain' },
        57: { label: 'Llovizna helada intensa', icon: 'fa-cloud-showers-heavy' },
        61: { label: 'Lluvia ligera', icon: 'fa-cloud-rain' },
        63: { label: 'Lluvia', icon: 'fa-cloud-rain' },
        65: { label: 'Lluvia intensa', icon: 'fa-cloud-showers-heavy' },
        66: { label: 'Lluvia helada', icon: 'fa-cloud-rain' },
        67: { label: 'Lluvia helada intensa', icon: 'fa-cloud-showers-heavy' },
        71: { label: 'Nieve ligera', icon: 'fa-snowflake' },
        73: { label: 'Nieve', icon: 'fa-snowflake' },
        75: { label: 'Nieve intensa', icon: 'fa-snowflake' },
        77: { label: 'Granizo fino', icon: 'fa-snowflake' },
        80: { label: 'Chubascos ligeros', icon: 'fa-cloud-rain' },
        81: { label: 'Chubascos', icon: 'fa-cloud-showers-heavy' },
        82: { label: 'Chubascos fuertes', icon: 'fa-cloud-showers-heavy' },
        85: { label: 'Nevadas ligeras', icon: 'fa-snowflake' },
        86: { label: 'Nevadas fuertes', icon: 'fa-snowflake' },
        95: { label: 'Tormenta', icon: 'fa-bolt' },
        96: { label: 'Tormenta con granizo', icon: 'fa-bolt' },
        99: { label: 'Tormenta fuerte', icon: 'fa-bolt' }
    };

    function wmoWeatherMeta(code) {
        var c = Number(code);
        if (WMO_WEATHER[c]) return WMO_WEATHER[c];
        return { label: 'Condición desconocida', icon: 'fa-cloud' };
    }

    function loadWeatherCache() {
        try {
            var raw = localStorage.getItem(LS_WEATHER_CACHE);
            if (!raw) return null;
            var data = JSON.parse(raw);
            if (!data || typeof data !== 'object') return null;
            if (data.latitude == null || data.longitude == null) return null;
            return data;
        } catch (_) {
            return null;
        }
    }

    function saveWeatherCache(data) {
        try {
            localStorage.setItem(LS_WEATHER_CACHE, JSON.stringify(data));
        } catch (_) {}
    }

    function formatWeatherUpdatedAt(ts) {
        var t = Number(ts);
        if (!isFinite(t) || t <= 0) return '';
        try {
            return new Date(t).toLocaleString('es', {
                day: 'numeric',
                month: 'short',
                hour: '2-digit',
                minute: '2-digit'
            });
        } catch (_) {
            return new Date(t).toLocaleString();
        }
    }

    function weatherPanelSuffixes() {
        return ['', 'Mobile'];
    }

    function weatherMapEmbedUrl(lat, lon) {
        var la = Number(lat);
        var lo = Number(lon);
        if (!isFinite(la) || !isFinite(lo)) return '';
        var d = 0.006;
        var left = lo - d;
        var right = lo + d;
        var top = la + d;
        var bottom = la - d;
        return 'https://www.openstreetmap.org/export/embed.html?bbox='
            + [left, bottom, right, top].join('%2C')
            + '&layer=mapnik&marker='
            + encodeURIComponent(la) + '%2C' + encodeURIComponent(lo);
    }

    function weatherMapExternalUrl(lat, lon) {
        var la = Number(lat);
        var lo = Number(lon);
        if (!isFinite(la) || !isFinite(lo)) return '';
        return 'https://www.openstreetmap.org/?mlat=' + encodeURIComponent(la)
            + '&mlon=' + encodeURIComponent(lo)
            + '#map=15/' + encodeURIComponent(la) + '/' + encodeURIComponent(lo);
    }

    function hideWeatherMap(suffix) {
        var wrap = document.getElementById('osWeatherMap' + suffix);
        var frame = document.getElementById('osWeatherMapFrame' + suffix);
        var link = document.getElementById('osWeatherMapLink' + suffix);
        if (wrap) wrap.hidden = true;
        if (frame) frame.removeAttribute('src');
        if (link) link.hidden = true;
    }

    function updateWeatherMap(suffix, lat, lon) {
        var wrap = document.getElementById('osWeatherMap' + suffix);
        var frame = document.getElementById('osWeatherMapFrame' + suffix);
        var link = document.getElementById('osWeatherMapLink' + suffix);
        var src = weatherMapEmbedUrl(lat, lon);
        if (!src || !wrap || !frame) {
            hideWeatherMap(suffix);
            return;
        }
        if (frame.getAttribute('src') !== src) frame.setAttribute('src', src);
        wrap.hidden = false;
        if (link) {
            var ext = weatherMapExternalUrl(lat, lon);
            if (ext) {
                link.href = ext;
                link.hidden = false;
            } else {
                link.hidden = true;
            }
        }
    }

    function setWeatherLoading(suffix, message) {
        var place = document.getElementById('osWeatherPlace' + suffix);
        var icon = document.getElementById('osWeatherIcon' + suffix);
        if (place) place.textContent = message || 'Obteniendo datos…';
        if (icon) icon.innerHTML = '<i class="fas fa-spinner fa-spin" aria-hidden="true"></i>';
        hideWeatherMap(suffix);
    }

    function applyWeatherToPanel(suffix, payload, opts) {
        opts = opts || {};
        var stale = !!opts.stale;
        var meta = wmoWeatherMeta(payload.weatherCode);
        var tempEl = document.getElementById('osWeatherTemp' + suffix);
        var placeEl = document.getElementById('osWeatherPlace' + suffix);
        var humEl = document.getElementById('osWeatherHumidity' + suffix);
        var windEl = document.getElementById('osWeatherWind' + suffix);
        var iconEl = document.getElementById('osWeatherIcon' + suffix);
        var updatedEl = document.getElementById('osWeatherUpdated' + suffix);

        if (iconEl) {
            iconEl.innerHTML = '<i class="fas ' + meta.icon + '" aria-hidden="true"></i>';
            iconEl.title = meta.label;
        }
        if (tempEl) {
            var temp = payload.temperature;
            tempEl.textContent = (temp != null && isFinite(Number(temp)))
                ? Math.round(Number(temp)) + '°'
                : '—';
        }
        if (placeEl) {
            placeEl.textContent = payload.placeName || formatCoordsLabel(payload.latitude, payload.longitude);
        }
        if (humEl) {
            humEl.textContent = (payload.humidity != null && isFinite(Number(payload.humidity)))
                ? Math.round(Number(payload.humidity)) + ' %'
                : '—';
        }
        if (windEl) {
            windEl.textContent = (payload.windSpeed != null && isFinite(Number(payload.windSpeed)))
                ? Math.round(Number(payload.windSpeed)) + ' km/h'
                : '—';
        }
        if (updatedEl) {
            var when = formatWeatherUpdatedAt(payload.fetchedAt);
            if (when) {
                updatedEl.hidden = false;
                updatedEl.textContent = stale
                    ? 'Última actualización: ' + when + ' (sin conexión)'
                    : 'Actualizado: ' + when;
                updatedEl.classList.toggle('os-weather-updated--stale', stale);
            } else {
                updatedEl.hidden = true;
                updatedEl.textContent = '';
                updatedEl.classList.remove('os-weather-updated--stale');
            }
        }
        if (payload.latitude != null && payload.longitude != null) {
            updateWeatherMap(suffix, payload.latitude, payload.longitude);
        } else {
            hideWeatherMap(suffix);
        }
    }

    function renderWeatherFromCache(stale) {
        var cache = loadWeatherCache();
        if (!cache) return false;
        weatherPanelSuffixes().forEach(function (suf) {
            applyWeatherToPanel(suf, cache, { stale: stale });
        });
        return true;
    }

    function formatCoordsLabel(lat, lon) {
        var la = Number(lat);
        var lo = Number(lon);
        if (!isFinite(la) || !isFinite(lo)) return 'Ubicación desconocida';
        return la.toFixed(2) + '°, ' + lo.toFixed(2) + '°';
    }

    function companyLocationCoords() {
        var A = api();
        if (!A || typeof A.getSetting !== 'function') return null;
        var loc = A.getSetting('companyLocation', null);
        if (!loc || typeof loc !== 'object') return null;
        var coords = loc.coordinates;
        if (!coords || typeof coords !== 'object') return null;
        var lat = Number(coords.lat);
        var lon = Number(coords.lon);
        if (!isFinite(lat) || !isFinite(lon)) return null;
        var parts = [];
        if (loc.city) parts.push(String(loc.city));
        if (loc.state) parts.push(String(loc.state));
        if (loc.country) parts.push(String(loc.country));
        return {
            latitude: lat,
            longitude: lon,
            placeName: parts.length ? parts.join(', ') : null,
            source: 'company'
        };
    }

    function fetchJsonWithTimeout(url, timeoutMs) {
        var ctrl = typeof AbortController !== 'undefined' ? new AbortController() : null;
        var timer = ctrl ? setTimeout(function () { try { ctrl.abort(); } catch (_) {} }, timeoutMs) : null;
        var opts = ctrl ? { signal: ctrl.signal } : {};
        return fetch(url, opts).then(function (res) {
            if (!res.ok) throw new Error('HTTP ' + res.status);
            return res.json();
        }).finally(function () {
            if (timer) clearTimeout(timer);
        });
    }

    function fetchReverseGeocode(lat, lon) {
        var url = 'https://api.bigdatacloud.net/data/reverse-geocode-client?latitude='
            + encodeURIComponent(lat) + '&longitude=' + encodeURIComponent(lon) + '&localityLanguage=es';
        return fetchJsonWithTimeout(url, WEATHER_FETCH_TIMEOUT_MS).then(function (data) {
            var parts = [];
            if (data.city) parts.push(data.city);
            else if (data.locality) parts.push(data.locality);
            if (data.principalSubdivision) parts.push(data.principalSubdivision);
            if (data.countryName) parts.push(data.countryName);
            return parts.length ? parts.join(', ') : null;
        });
    }

    function fetchOpenMeteoWeather(lat, lon) {
        var url = 'https://api.open-meteo.com/v1/forecast?latitude=' + encodeURIComponent(lat)
            + '&longitude=' + encodeURIComponent(lon)
            + '&current=temperature_2m,relative_humidity_2m,weather_code,wind_speed_10m&timezone=auto';
        return fetchJsonWithTimeout(url, WEATHER_FETCH_TIMEOUT_MS).then(function (data) {
            var cur = data && data.current ? data.current : {};
            return {
                temperature: cur.temperature_2m,
                humidity: cur.relative_humidity_2m,
                weatherCode: cur.weather_code,
                windSpeed: cur.wind_speed_10m
            };
        });
    }

    function resolveCoordinates() {
        return new Promise(function (resolve, reject) {
            if (!navigator.geolocation) {
                var fallback = companyLocationCoords();
                if (fallback) return resolve(fallback);
                return reject(new Error('geolocation_unavailable'));
            }
            navigator.geolocation.getCurrentPosition(
                function (pos) {
                    resolve({
                        latitude: pos.coords.latitude,
                        longitude: pos.coords.longitude,
                        placeName: null,
                        source: 'gps'
                    });
                },
                function () {
                    var fallback = companyLocationCoords();
                    if (fallback) resolve(fallback);
                    else reject(new Error('geolocation_denied'));
                },
                { enableHighAccuracy: false, timeout: 10000, maximumAge: 300000 }
            );
        });
    }

    function buildWeatherPayload(coords, weather, placeName) {
        return {
            latitude: coords.latitude,
            longitude: coords.longitude,
            placeName: placeName || coords.placeName || formatCoordsLabel(coords.latitude, coords.longitude),
            temperature: weather.temperature,
            humidity: weather.humidity,
            weatherCode: weather.weatherCode,
            windSpeed: weather.windSpeed,
            fetchedAt: Date.now(),
            source: coords.source || 'live'
        };
    }

    function refreshWeatherWidget(force) {
        if (weatherRefreshInFlight) return Promise.resolve();
        var now = Date.now();
        if (!force && now - weatherLastAttemptAt < WEATHER_REFRESH_MIN_MS) {
            if (renderWeatherFromCache(false)) return Promise.resolve();
        }
        weatherRefreshInFlight = true;
        weatherLastAttemptAt = now;
        var hadCache = renderWeatherFromCache(false);
        if (!hadCache) {
            weatherPanelSuffixes().forEach(function (suf) {
                setWeatherLoading(suf, 'Obteniendo ubicación…');
            });
        }

        return resolveCoordinates()
            .then(function (coords) {
                weatherPanelSuffixes().forEach(function (suf) {
                    setWeatherLoading(suf, 'Consultando clima…');
                });
                return fetchOpenMeteoWeather(coords.latitude, coords.longitude).then(function (weather) {
                    var placePromise = coords.placeName
                        ? Promise.resolve(coords.placeName)
                        : fetchReverseGeocode(coords.latitude, coords.longitude).catch(function () { return null; });
                    return placePromise.then(function (placeName) {
                        return buildWeatherPayload(coords, weather, placeName);
                    });
                });
            })
            .then(function (payload) {
                saveWeatherCache(payload);
                weatherPanelSuffixes().forEach(function (suf) {
                    applyWeatherToPanel(suf, payload, { stale: false });
                });
            })
            .catch(function () {
                if (!renderWeatherFromCache(true)) {
                    weatherPanelSuffixes().forEach(function (suf) {
                        var place = document.getElementById('osWeatherPlace' + suf);
                        var icon = document.getElementById('osWeatherIcon' + suf);
                        var updated = document.getElementById('osWeatherUpdated' + suf);
                        if (place) place.textContent = 'No hay datos de clima disponibles';
                        if (icon) icon.innerHTML = '<i class="fas fa-cloud" aria-hidden="true"></i>';
                        if (updated) {
                            updated.hidden = false;
                            updated.textContent = 'Sin conexión ni datos guardados';
                            updated.classList.add('os-weather-updated--stale');
                        }
                        hideWeatherMap(suf);
                    });
                }
            })
            .finally(function () {
                weatherRefreshInFlight = false;
            });
    }

    function weatherModalVisible() {
        var m = document.getElementById('osWeatherMobileModal');
        return !!(m && (m.style.display === 'block' || m.style.display === 'flex'));
    }

    function closeWeatherMobileModal(skipBodyClass) {
        var modal = document.getElementById('osWeatherMobileModal');
        if (!modal) return;
        modal.style.display = 'none';
        if (!skipBodyClass) {
            if (!notesModalVisible() && !systemModalVisible() && !mediaModalVisible()) {
                document.body.classList.remove('modal-open');
            }
        }
    }

    function openWeatherMobileModal() {
        var modal = document.getElementById('osWeatherMobileModal');
        if (!modal) return;
        closeNotesMobileModal(true);
        closeSystemMobileModal();
        closeMediaMobileModal(true);
        modal.style.display = 'flex';
        document.body.classList.add('modal-open');
        renderWeatherFromCache(false);
        refreshWeatherWidget(false);
    }

    function bindWeatherRefreshButtons() {
        ['osWeatherRefresh', 'osWeatherRefreshMobile'].forEach(function (id) {
            var btn = document.getElementById(id);
            if (!btn || btn.dataset.osWeatherRefreshBound === '1') return;
            btn.dataset.osWeatherRefreshBound = '1';
            btn.addEventListener('click', function () {
                refreshWeatherWidget(true);
            });
        });
    }

    /* ───────────────────── Spotify (Web Playback SDK + playlists) ───────────────────── */

    var LS_SPOTIFY_TOKEN = 'flexia_spotify_token_v1';
    var LS_SPOTIFY_USER = 'flexia_spotify_user_v1';
    var LS_SPOTIFY_UI = 'flexia_spotify_ui_v1';
    var SPOTIFY_UI_PERSIST_MS = 400;
    var spotifyPersistTimer = null;
    var spotifyRestoreScrollRaf = null;
    var SS_SPOTIFY_VERIFIER = 'flexia_spotify_pkce_verifier';
    var SS_SPOTIFY_STATE = 'flexia_spotify_oauth_state';
    var SS_SPOTIFY_CLIENT_ID = 'flexia_spotify_oauth_client_id';
    var SPOTIFY_AUTH_URL = 'https://accounts.spotify.com/authorize';
    var SPOTIFY_TOKEN_URL = 'https://accounts.spotify.com/api/token';
    var SPOTIFY_API = 'https://api.spotify.com/v1';
    var SPOTIFY_CALLBACK_FILE = 'spotify-callback.php';
    var SPOTIFY_SDK_URL = 'https://sdk.scdn.co/spotify-player.js';
    var SPOTIFY_SCOPES = [
        'streaming',
        'user-read-private',
        'user-read-email',
        'user-modify-playback-state',
        'playlist-read-private',
        'playlist-read-collaborative',
        'playlist-modify-public',
        'playlist-modify-private',
        'user-library-read',
        'user-library-modify'
    ].join(' ');
    var SPOTIFY_POLL_MS = 1000;
    var SPOTIFY_PREMIUM_URL = 'https://www.spotify.com/premium';

    function spotifyPremiumUrl() {
        var market = spotifyGetMarket();
        if (market) return 'https://www.spotify.com/' + market.toLowerCase() + '/premium';
        return SPOTIFY_PREMIUM_URL;
    }

    var spotifyState = {
        polling: null,
        authPopup: null,
        authMessageBound: false,
        initInFlight: false,
        playlistsInFlight: false,
        playlistsCache: null,
        playlistsCacheAt: 0,
        uiTab: 'search',
        searchQuery: '',
        searchResults: [],
        searchLoading: false,
        searchDebounce: null,
        searchError: '',
        searchFocused: false,
        searchBrowsing: false,
        searchBrowseTimer: null,
        addPlaylistOpen: false,
        addPlaylistBrowsing: false,
        addPlaylistBrowseTimer: null,
        selectedPlaylist: null,
        playlistTracks: [],
        playlistTracksTotal: 0,
        playlistTracksLoading: false,
        playlistTracksLoadingMore: false,
        playlistTracksError: '',
        playlistTracksBrowsing: false,
        playlistTracksBrowseTimer: null,
        playbackContext: null,
        _prevSdkSnapshot: null,
        _queueAdvancing: false,
        feedback: null,
        feedbackTimer: null,
        inlineError: '',
        lastView: null,
        player: null,
        deviceId: null,
        sdkReady: false,
        sdkLoadPromise: null,
        connectPromise: null,
        playerState: null,
        progressTimer: null,
        volume: 0.7,
        artView: 'square',
        quickCompact: false,
        quickHoverPlaylist: null,
        quickHoverTracks: [],
        quickHoverTracksTotal: 0,
        quickHoverTracksLoading: false,
        quickHoverTracksError: '',
        /** Mantener quick play visible en pausa iniciada desde Flexia (no desde el widget de Windows). */
        quickPlayerHoldWhilePaused: false,
        _pendingQuickPause: false
    };

    var LS_SPOTIFY_QUICK_COMPACT = 'flexia_spotify_quick_compact_v1';
    var SPOTIFY_QUICK_HOVER_FADE_MS = 280;
    var SPOTIFY_QUICK_HOVER_AUTO_CLOSE_MS = 5000;

    function spotifyClientId() {
        var A = api();
        if (!A || typeof A.getSetting !== 'function') return '';
        var integ = A.getSetting('integrations', null);
        if (integ && integ.spotify && typeof integ.spotify.clientId === 'string') {
            return integ.spotify.clientId.trim();
        }
        return '';
    }

    function spotifyConnectionVerified() {
        var A = api();
        if (!A || typeof A.getSetting !== 'function') return false;
        var integ = A.getSetting('integrations', null);
        return !!(integ && integ.spotify && integ.spotify.connectionVerified === true);
    }

    function spotifyInstallReady() {
        return !!(spotifyClientId() && spotifyConnectionVerified());
    }

    function openSpotifyIntegrationsConfig() {
        if (typeof window.__flexiaOpenConfigIntegrationsTab === 'function') {
            window.__flexiaOpenConfigIntegrationsTab();
            return;
        }
        var configBtn = document.getElementById('configBtn');
        if (configBtn) configBtn.click();
    }

    function handleSpotifyConnectClick() {
        if (!spotifyInstallReady()) {
            openSpotifyIntegrationsConfig();
            return;
        }
        startSpotifyAuth();
    }

    function spotifyMarkConnectionVerified() {
        var A = api();
        if (!A || typeof A.getSetting !== 'function' || typeof A.saveSettings !== 'function') {
            return Promise.resolve(false);
        }
        var clientId = spotifyClientId();
        if (!clientId) return Promise.resolve(false);
        var integ = A.getSetting('integrations', null) || {};
        if (integ.spotify && integ.spotify.connectionVerified === true && integ.spotify.clientId === clientId) {
            return Promise.resolve(true);
        }
        var next = Object.assign({}, integ, {
            spotify: Object.assign({}, integ.spotify || {}, {
                clientId: clientId,
                redirectUri: spotifyRedirectUri(),
                connectionVerified: true
            })
        });
        return A.saveSettings({ integrations: next }).then(function (res) {
            if (res && res.success && typeof window.__flexiaRefreshIntegrationsTab === 'function') {
                window.__flexiaRefreshIntegrationsTab();
            }
            return !!(res && res.success);
        }).catch(function () { return false; });
    }

    function spotifyOnAuthSuccess() {
        spotifyDestroyPlayer();
        spotifyMarkConnectionVerified().finally(function () {
            spotifyState.playlistsCache = null;
            spotifyState.playlistsCacheAt = 0;
            refreshSpotifyWidget(true);
        });
    }

    function spotifyHasStreamingScope(token) {
        return spotifyHasScope(token, 'streaming');
    }

    function spotifyHasScope(token, scopeName) {
        var scope = (token && token.scope) ? String(token.scope) : '';
        return scope.indexOf(scopeName) >= 0;
    }

    function spotifyTrackUri(trackId) {
        return trackId ? 'spotify:track:' + String(trackId) : '';
    }

    function spotifyLibraryUrisQuery(uris) {
        var list = (Array.isArray(uris) ? uris : [uris]).filter(Boolean);
        if (!list.length) return '';
        return '?uris=' + encodeURIComponent(list.join(','));
    }

    function spotifyDedupePlaylists(items) {
        var seen = {};
        var out = [];
        (items || []).forEach(function (pl) {
            if (!pl || !pl.id || seen[pl.id]) return;
            seen[pl.id] = true;
            out.push(pl);
        });
        return out;
    }

    function spotifyEnsureScopes(requiredScopes, options) {
        var token = spotifyReadToken();
        var list = requiredScopes || [];
        var opts = options || {};
        var ok = opts.anyOf
            ? list.some(function (s) { return spotifyHasScope(token, s); })
            : list.every(function (s) { return spotifyHasScope(token, s); });
        if (ok) return Promise.resolve();
        var err = new Error('missing-scope');
        err.missingScopes = list;
        return Promise.reject(err);
    }

    function spotifySetFeedback(text, ok) {
        spotifyState.feedback = text ? { text: text, ok: !!ok } : null;
        if (spotifyState.feedbackTimer) clearTimeout(spotifyState.feedbackTimer);
        if (text) {
            spotifyState.feedbackTimer = setTimeout(function () {
                spotifyState.feedback = null;
                spotifyState.feedbackTimer = null;
                if (spotifyState.lastView && spotifyState.lastView.kind === 'connected') {
                    spotifyApplyConnectedView(spotifyState.lastView, false);
                }
            }, 3200);
        }
    }

    function spotifyGetAccessTokenString() {
        var token = spotifyReadToken();
        if (!token) return Promise.reject(new Error('not-connected'));
        if (spotifyTokenExpired(token)) return spotifyRefreshAccessToken().then(function (rec) { return rec.accessToken; });
        return Promise.resolve(token.accessToken);
    }

    function spotifyRedirectUri() {
        try {
            // Resuelve la URL absoluta del callback respetando subdirectorios de la instalación.
            return new URL(SPOTIFY_CALLBACK_FILE, document.baseURI).toString();
        } catch (_) {
            try { return window.location.origin + '/' + SPOTIFY_CALLBACK_FILE; } catch (__) { return ''; }
        }
    }

    function spotifyReadToken() {
        try {
            var raw = localStorage.getItem(LS_SPOTIFY_TOKEN);
            if (!raw) return null;
            var data = JSON.parse(raw);
            if (!data || !data.accessToken) return null;
            return data;
        } catch (_) { return null; }
    }

    function spotifyWriteToken(record) {
        try { localStorage.setItem(LS_SPOTIFY_TOKEN, JSON.stringify(record)); } catch (_) {}
    }

    function spotifyClearToken() {
        try { localStorage.removeItem(LS_SPOTIFY_TOKEN); } catch (_) {}
        try { localStorage.removeItem(LS_SPOTIFY_USER); } catch (_) {}
    }

    function spotifyClearUiState() {
        try { localStorage.removeItem(LS_SPOTIFY_UI); } catch (_) {}
        spotifyState._persistedScroll = null;
        spotifyState._listScrollMemory = {};
    }

    function spotifyEnsureListScrollMemory() {
        if (!spotifyState._listScrollMemory || typeof spotifyState._listScrollMemory !== 'object') {
            spotifyState._listScrollMemory = {};
        }
        return spotifyState._listScrollMemory;
    }

    function spotifyListScrollStorageKey(kind) {
        if (kind === 'playlistTracks' && spotifyState.selectedPlaylist && spotifyState.selectedPlaylist.id) {
            return 'playlistTracks:' + spotifyState.selectedPlaylist.id;
        }
        if (kind === 'quickHoverTracks') {
            var pl = spotifyState.quickHoverPlaylist || spotifyResolveQuickHoverPlaylist();
            if (pl && pl.id) return 'quickHoverTracks:' + pl.id;
        }
        return kind;
    }

    function spotifyRememberListScroll(kind, container) {
        if (!container) return;
        var top = container.scrollTop;
        if (top <= 0) return;
        var mem = spotifyEnsureListScrollMemory();
        mem[spotifyListScrollStorageKey(kind)] = top;
        if (!spotifyState._persistedScroll) spotifyState._persistedScroll = {};
        spotifyState._persistedScroll[kind] = top;
    }

    function spotifyGetRememberedListScroll(kind, container) {
        var mem = spotifyEnsureListScrollMemory();
        var key = spotifyListScrollStorageKey(kind);
        var remembered = typeof mem[key] === 'number' ? mem[key] : 0;
        var current = container ? container.scrollTop : 0;
        return Math.max(remembered, current);
    }

    function spotifyApplyRememberedListScroll(kind, container) {
        if (!container) return;
        var top = spotifyGetRememberedListScroll(kind, container);
        if (top > 0) {
            container.scrollTop = top;
            spotifyRememberListScroll(kind, container);
        }
    }

    function spotifyCaptureScrollPositions() {
        document.querySelectorAll('.os-spotify-playlists__list--tab').forEach(function (el) {
            spotifyRememberListScroll('playlistsList', el);
        });
        document.querySelectorAll('.os-spotify-playlists__tracks').forEach(function (el) {
            spotifyRememberListScroll('playlistTracks', el);
        });
        document.querySelectorAll('.os-spotify-search__results').forEach(function (el) {
            spotifyRememberListScroll('searchResults', el);
        });
        document.querySelectorAll('.os-spotify-add-playlist__list').forEach(function (el) {
            spotifyRememberListScroll('addPlaylistList', el);
        });
        document.querySelectorAll('[data-spotify-part="quick-hover-tracks"]').forEach(function (el) {
            spotifyRememberListScroll('quickHoverTracks', el);
        });
        var collected = spotifyCollectScrollPositions();
        if (!spotifyState._persistedScroll) spotifyState._persistedScroll = {};
        Object.keys(collected).forEach(function (k) {
            spotifyState._persistedScroll[k] = collected[k];
        });
    }

    function spotifyReadUiState() {
        try {
            var raw = localStorage.getItem(LS_SPOTIFY_UI);
            if (!raw) return null;
            var data = JSON.parse(raw);
            return data && typeof data === 'object' ? data : null;
        } catch (_) {
            return null;
        }
    }

    function spotifyCollectScrollPositions() {
        var scroll = {};
        var mem = spotifyState._listScrollMemory;
        document.querySelectorAll('.os-spotify-playlists__list--tab').forEach(function (el) {
            if (el.scrollTop > 0) scroll.playlistsList = el.scrollTop;
        });
        document.querySelectorAll('.os-spotify-playlists__tracks').forEach(function (el) {
            if (el.scrollTop > 0) scroll.playlistTracks = el.scrollTop;
        });
        document.querySelectorAll('.os-spotify-search__results').forEach(function (el) {
            if (el.scrollTop > 0) scroll.searchResults = el.scrollTop;
        });
        document.querySelectorAll('.os-spotify-add-playlist__list').forEach(function (el) {
            if (el.scrollTop > 0) scroll.addPlaylistList = el.scrollTop;
        });
        document.querySelectorAll('[data-spotify-part="quick-hover-tracks"]').forEach(function (el) {
            if (el.scrollTop > 0) scroll.quickHoverTracks = el.scrollTop;
        });
        ['playlistsList', 'playlistTracks', 'searchResults', 'addPlaylistList', 'quickHoverTracks'].forEach(function (kind) {
            if (scroll[kind] > 0) return;
            if (!mem || typeof mem !== 'object') return;
            var stored = mem[spotifyListScrollStorageKey(kind)];
            if (typeof stored === 'number' && stored > 0) scroll[kind] = stored;
        });
        return scroll;
    }

    function spotifyNormalizePlaybackContext(ctx) {
        if (!ctx || typeof ctx !== 'object') return null;
        if (ctx.mode === 'context' && ctx.contextUri) {
            return {
                mode: 'context',
                contextUri: String(ctx.contextUri),
                offsetUri: ctx.offsetUri ? String(ctx.offsetUri) : null,
                offsetPosition: typeof ctx.offsetPosition === 'number' ? ctx.offsetPosition : 0,
                source: ctx.source || ''
            };
        }
        if (ctx.mode === 'queue' && Array.isArray(ctx.queueUris) && ctx.queueUris.length) {
            return {
                mode: 'queue',
                queueUris: ctx.queueUris.filter(Boolean).map(String),
                queueIndex: Math.max(0, Number(ctx.queueIndex) || 0),
                source: ctx.source || ''
            };
        }
        return null;
    }

    function spotifyBuildQueueFromTracks(tracks, startUri, source) {
        var uris = [];
        (tracks || []).forEach(function (t) {
            if (t && t.uri) uris.push(t.uri);
        });
        if (!uris.length && startUri) uris = [startUri];
        var idx = startUri ? uris.indexOf(startUri) : 0;
        if (idx < 0) idx = 0;
        return {
            mode: 'queue',
            queueUris: uris,
            queueIndex: idx,
            source: source || ''
        };
    }

    function spotifySyncQueueIndexFromPlayer(state) {
        var ctx = spotifyState.playbackContext;
        if (!ctx || ctx.mode !== 'queue' || !ctx.queueUris || !state || !state.track_window || !state.track_window.current_track) return;
        var uri = state.track_window.current_track.uri;
        var idx = ctx.queueUris.indexOf(uri);
        if (idx >= 0) ctx.queueIndex = idx;
    }

    function spotifySyncPlaybackContextFromPlayer(state) {
        if (!state || !state.track_window || !state.track_window.current_track) return;
        var ctx = spotifyState.playbackContext;
        if (!ctx) return;
        var uri = state.track_window.current_track.uri;
        if (ctx.mode === 'context') ctx.offsetUri = uri;
        else if (ctx.mode === 'queue') spotifySyncQueueIndexFromPlayer(state);
    }

    function spotifyDetectTrackNaturalEnd(prev, state) {
        if (!prev || !state || !state.paused || prev.paused) return false;
        if (!prev.trackId || !state.track_window || !state.track_window.current_track) return false;
        if (state.track_window.current_track.id !== prev.trackId) return false;
        return prev.position >= Math.max(0, prev.duration - 2000);
    }

    function spotifyHasQueuedNextTracks(state) {
        return !!(state && state.track_window && state.track_window.next_tracks && state.track_window.next_tracks.length);
    }

    function spotifyAdvanceQueueIfNeeded(state) {
        var ctx = spotifyState.playbackContext;
        if (!ctx || ctx.mode !== 'queue' || !ctx.queueUris || !ctx.queueUris.length) return Promise.resolve();
        if (spotifyHasQueuedNextTracks(state || spotifyState.playerState)) return Promise.resolve();
        if (spotifyState._queueAdvancing) return Promise.resolve();
        var nextIdx = (ctx.queueIndex || 0) + 1;
        if (nextIdx >= ctx.queueUris.length) return Promise.resolve();
        spotifyState._queueAdvancing = true;
        ctx.queueIndex = nextIdx;
        return spotifyPlayOnDevice({ uris: ctx.queueUris.slice(nextIdx) }).then(function () {
            return spotifyWaitForPlaybackStart();
        }).catch(function () { /* ignore */ }).finally(function () {
            spotifyState._queueAdvancing = false;
        });
    }

    function spotifySnapshotSdkState(state) {
        if (!state || !state.track_window || !state.track_window.current_track) return null;
        var track = state.track_window.current_track;
        return {
            paused: !!state.paused,
            position: state.position || 0,
            trackId: track.id,
            duration: track.duration_ms || 0
        };
    }

    function spotifyClearQuickPlayerPauseHold() {
        spotifyState.quickPlayerHoldWhilePaused = false;
        spotifyState._pendingQuickPause = false;
    }

    /** Pausa desde Flexia → quick play visible; pausa externa (p. ej. widget de Windows) → ocultar. */
    function spotifyOnPlaybackPauseTransition(prevSnap, state) {
        if (!state || state.paused !== true) return;
        var wasPlaying = prevSnap && prevSnap.paused === false;
        if (!wasPlaying) return;
        if (spotifyState._pendingQuickPause) {
            spotifyState._pendingQuickPause = false;
            spotifyState.quickPlayerHoldWhilePaused = true;
        } else {
            spotifyState.quickPlayerHoldWhilePaused = false;
        }
    }

    function spotifyBuildUiPersistPayload() {
        var playback = null;
        var pb = spotifyPlaybackFromSdkState(spotifyState.playerState);
        var ctx = spotifyNormalizePlaybackContext(spotifyState.playbackContext);
        if (pb && pb.item && pb.item.id) {
            playback = {
                uri: spotifyTrackUri(pb.item.id),
                position_ms: Math.max(0, Math.floor(pb.progress_ms || 0)),
                paused: !pb.is_playing
            };
            if (ctx && ctx.mode === 'context') {
                playback.contextUri = ctx.contextUri;
                playback.offsetUri = ctx.offsetUri || playback.uri;
            }
        }
        var selectedPlaylist = null;
        if (spotifyState.selectedPlaylist && spotifyState.selectedPlaylist.id) {
            selectedPlaylist = {
                id: spotifyState.selectedPlaylist.id,
                name: spotifyState.selectedPlaylist.name || '',
                uri: spotifyState.selectedPlaylist.uri || '',
                trackCount: spotifyState.selectedPlaylist.trackCount || 0
            };
        }
        return {
            v: 2,
            uiTab: spotifyState.uiTab === 'playlists' ? 'playlists' : 'search',
            selectedPlaylist: selectedPlaylist,
            addPlaylistOpen: !!spotifyState.addPlaylistOpen,
            searchQuery: spotifyState.searchQuery || '',
            volume: spotifyState.volume,
            scroll: spotifyCollectScrollPositions(),
            playback: playback,
            playbackContext: ctx,
            artView: spotifyNormalizeArtView(spotifyState.artView),
            savedAt: Date.now()
        };
    }

    function spotifyPersistUiState() {
        if (!spotifyReadToken()) return;
        try {
            localStorage.setItem(LS_SPOTIFY_UI, JSON.stringify(spotifyBuildUiPersistPayload()));
        } catch (_) {}
    }

    function spotifySchedulePersistUiState() {
        if (spotifyPersistTimer) clearTimeout(spotifyPersistTimer);
        spotifyPersistTimer = setTimeout(function () {
            spotifyPersistTimer = null;
            spotifyPersistUiState();
        }, SPOTIFY_UI_PERSIST_MS);
    }

    function spotifyApplyPersistedUiState() {
        var saved = spotifyReadUiState();
        if (!saved) return;
        if (saved.uiTab === 'playlists' || saved.uiTab === 'search') {
            spotifyState.uiTab = saved.uiTab;
        }
        if (typeof saved.volume === 'number' && !isNaN(saved.volume)) {
            spotifyState.volume = Math.max(0, Math.min(1, saved.volume));
        }
        if (typeof saved.searchQuery === 'string') {
            spotifyState.searchQuery = saved.searchQuery;
        }
        spotifyState.addPlaylistOpen = !!saved.addPlaylistOpen;
        if (saved.selectedPlaylist && saved.selectedPlaylist.id) {
            spotifyState.selectedPlaylist = {
                id: saved.selectedPlaylist.id,
                name: saved.selectedPlaylist.name || 'Playlist',
                uri: saved.selectedPlaylist.uri || '',
                trackCount: saved.selectedPlaylist.trackCount ? Number(saved.selectedPlaylist.trackCount) : 0
            };
        }
        if (saved.scroll && typeof saved.scroll === 'object') {
            spotifyState._persistedScroll = saved.scroll;
            var mem = spotifyEnsureListScrollMemory();
            if (saved.scroll.playlistsList > 0) mem.playlistsList = saved.scroll.playlistsList;
            if (saved.scroll.searchResults > 0) mem.searchResults = saved.scroll.searchResults;
            if (saved.scroll.addPlaylistList > 0) mem.addPlaylistList = saved.scroll.addPlaylistList;
            if (saved.scroll.playlistTracks > 0) {
                var plKey = spotifyListScrollStorageKey('playlistTracks');
                mem[plKey] = saved.scroll.playlistTracks;
            }
            if (saved.scroll.quickHoverTracks > 0) {
                var qhKey = spotifyListScrollStorageKey('quickHoverTracks');
                mem[qhKey] = saved.scroll.quickHoverTracks;
            }
        }
        var ctx = spotifyNormalizePlaybackContext(saved.playbackContext);
        if (ctx) spotifyState.playbackContext = ctx;
        if (saved.artView) spotifyState.artView = spotifyNormalizeArtView(saved.artView);
    }

    function spotifyNormalizeArtView(view) {
        return view === 'vinyl' || view === 'crate' ? view : 'square';
    }

    function spotifyArtViewLabel(view) {
        if (view === 'vinyl') return 'Disco de vinilo';
        if (view === 'crate') return 'Disco en 3D';
        return 'Portada cuadrada';
    }

    function spotifyCycleArtView() {
        var order = ['square', 'vinyl', 'crate'];
        var cur = spotifyNormalizeArtView(spotifyState.artView);
        var idx = order.indexOf(cur);
        spotifyState.artView = order[(idx + 1) % order.length];
        spotifyApplyArtViewToDom();
        spotifySchedulePersistUiState();
    }

    function spotifyApplyArtViewToDom() {
        var view = spotifyNormalizeArtView(spotifyState.artView);
        var hint = spotifyArtViewLabel(view);
        document.querySelectorAll('[data-spotify-part="hero-art-wrap"]').forEach(function (wrap) {
            wrap.dataset.artView = view;
            wrap.title = 'Clic para cambiar vista (' + hint + ')';
            wrap.setAttribute('aria-label', hint + '. Clic para cambiar la vista del disco.');
        });
        document.querySelectorAll('.os-spotify-hero').forEach(function (hero) {
            hero.classList.toggle('os-spotify-hero--view-vinyl', view === 'vinyl');
            hero.classList.toggle('os-spotify-hero--view-crate', view === 'crate');
        });
        spotifySyncQuickArtLayout();
    }

    function spotifySyncQuickArtLayout() {
        var compactToggleGap = 4;
        var compactToggleSize = 20;
        var isVerticalTaskbar = !document.body.classList.contains('mobile-mode')
            && (document.body.classList.contains('taskbar-left')
                || document.body.classList.contains('taskbar-right'));

        document.querySelectorAll('#taskbarSpotifyQuick, #spotifyQuickMobileFab').forEach(function (host) {
            var art = host.querySelector('.spotify-quick-panel__art');
            if (!art) return;
            var wrap = art.querySelector('[data-spotify-part="hero-art-wrap"]');
            if (!wrap) return;
            var isCompact = host.classList.contains('taskbar-spotify-quick--compact')
                || host.classList.contains('spotify-quick-mobile-fab--compact');
            var view = spotifyNormalizeArtView(wrap.dataset.artView || spotifyState.artView);
            var size = isCompact
                ? (view === 'crate' ? 44 : (view === 'vinyl' ? 42 : 40))
                : (view === 'crate' ? 40 : (view === 'vinyl' ? 36 : 34));

            art.style.width = size + 'px';
            art.style.height = size + 'px';
            art.style.flexBasis = size + 'px';
            wrap.style.width = size + 'px';
            wrap.style.height = size + 'px';
            wrap.style.flexBasis = size + 'px';

            if (!isCompact) {
                host.style.removeProperty('width');
                host.style.removeProperty('min-width');
                host.style.removeProperty('max-width');
                host.style.removeProperty('height');
                host.style.removeProperty('min-height');
                host.style.removeProperty('max-height');
                return;
            }

            var chrome = size + compactToggleGap + compactToggleSize;
            if (host.id === 'taskbarSpotifyQuick' && isVerticalTaskbar) {
                host.style.removeProperty('width');
                host.style.removeProperty('min-width');
                host.style.removeProperty('max-width');
                host.style.height = chrome + 'px';
                host.style.minHeight = chrome + 'px';
                host.style.maxHeight = chrome + 'px';
                return;
            }

            host.style.removeProperty('height');
            host.style.removeProperty('min-height');
            host.style.removeProperty('max-height');
            host.style.width = chrome + 'px';
            host.style.minWidth = chrome + 'px';
            host.style.maxWidth = chrome + 'px';
        });

        document.querySelectorAll('#taskbarSpotifyQuick, #spotifyQuickMobileFab').forEach(function (host) {
            var mq = host.querySelector('[data-spotify-part="quick-marquee"]');
            if (mq && mq.dataset.spotifyMarqueePending) {
                spotifyRefreshQuickMarquees(host);
            }
        });
    }

    function spotifyUpdateHeroArtElement(art, artUrl) {
        if (!art) return;
        if (artUrl) {
            art.classList.remove('os-spotify-hero__art--empty');
            art.style.backgroundImage = 'url("' + artUrl.replace(/"/g, '&quot;') + '")';
            art.innerHTML = '';
        } else {
            art.classList.add('os-spotify-hero__art--empty');
            art.style.backgroundImage = '';
            art.innerHTML = '<i class="fab fa-spotify" aria-hidden="true"></i>';
        }
    }

    function spotifyCreateHeroArtWrap(artUrl) {
        var wrap = document.createElement('div');
        wrap.className = 'os-spotify-hero__art-wrap';
        wrap.setAttribute('data-spotify-part', 'hero-art-wrap');
        wrap.dataset.artView = spotifyNormalizeArtView(spotifyState.artView);
        wrap.setAttribute('role', 'button');
        wrap.setAttribute('tabindex', '0');
        wrap.title = 'Clic para cambiar vista del disco';

        var discStage = document.createElement('div');
        discStage.className = 'os-spotify-hero__disc-stage';
        discStage.setAttribute('aria-hidden', 'true');

        var discSpin = document.createElement('div');
        discSpin.className = 'os-spotify-hero__disc-spin';

        var art = document.createElement('div');
        art.setAttribute('data-spotify-part', 'hero-art');
        art.className = 'os-spotify-hero__art';
        spotifyUpdateHeroArtElement(art, artUrl);

        var groove = document.createElement('span');
        groove.className = 'os-spotify-hero__vinyl-groove';
        groove.setAttribute('aria-hidden', 'true');

        var label = document.createElement('span');
        label.className = 'os-spotify-hero__vinyl-label';
        label.setAttribute('aria-hidden', 'true');

        discSpin.appendChild(art);
        discSpin.appendChild(groove);
        discSpin.appendChild(label);
        discStage.appendChild(discSpin);
        wrap.appendChild(discStage);
        return wrap;
    }

    function spotifyRestoreScrollPositions() {
        var scroll = spotifyState._persistedScroll;
        if (!scroll) return;
        if (scroll.playlistsList) {
            document.querySelectorAll('.os-spotify-playlists__list--tab').forEach(function (el) {
                el.scrollTop = scroll.playlistsList;
                spotifyRememberListScroll('playlistsList', el);
            });
        }
        if (scroll.playlistTracks) {
            document.querySelectorAll('.os-spotify-playlists__tracks').forEach(function (el) {
                el.scrollTop = scroll.playlistTracks;
                spotifyRememberListScroll('playlistTracks', el);
            });
        }
        if (scroll.searchResults) {
            document.querySelectorAll('.os-spotify-search__results').forEach(function (el) {
                el.scrollTop = scroll.searchResults;
                spotifyRememberListScroll('searchResults', el);
            });
        }
        if (scroll.addPlaylistList) {
            document.querySelectorAll('.os-spotify-add-playlist__list').forEach(function (el) {
                el.scrollTop = scroll.addPlaylistList;
                spotifyRememberListScroll('addPlaylistList', el);
            });
        }
        if (scroll.quickHoverTracks) {
            document.querySelectorAll('[data-spotify-part="quick-hover-tracks"]').forEach(function (el) {
                el.scrollTop = scroll.quickHoverTracks;
                spotifyRememberListScroll('quickHoverTracks', el);
            });
        }
        spotifyState._persistedScroll = null;
    }

    function spotifyScheduleRestoreScrollPositions() {
        if (!spotifyState._persistedScroll) return;
        if (spotifyRestoreScrollRaf) cancelAnimationFrame(spotifyRestoreScrollRaf);
        spotifyRestoreScrollRaf = requestAnimationFrame(function () {
            requestAnimationFrame(function () {
                spotifyRestoreScrollPositions();
                spotifyRestoreScrollRaf = null;
            });
        });
    }

    function spotifyRestorePlaybackFromPersist() {
        var saved = spotifyReadUiState();
        if (!saved || !saved.playback || !saved.playback.uri) return Promise.resolve();
        var pb = saved.playback;
        var ctx = spotifyNormalizePlaybackContext(saved.playbackContext);
        if (ctx) spotifyState.playbackContext = ctx;
        else if (pb.contextUri) {
            spotifyState.playbackContext = {
                mode: 'context',
                contextUri: pb.contextUri,
                offsetUri: pb.offsetUri || pb.uri,
                offsetPosition: 0
            };
            ctx = spotifyState.playbackContext;
        }

        var body;
        if (ctx && ctx.mode === 'context' && ctx.contextUri) {
            body = { context_uri: ctx.contextUri };
            if (pb.uri) body.offset = { uri: pb.uri };
            else if (ctx.offsetUri) body.offset = { uri: ctx.offsetUri };
            else if (ctx.offsetPosition) body.offset = { position: ctx.offsetPosition };
        } else if (ctx && ctx.mode === 'queue' && ctx.queueUris.length) {
            var idx = Math.min(ctx.queueIndex || 0, ctx.queueUris.length - 1);
            ctx.queueIndex = idx;
            body = { uris: ctx.queueUris.slice(idx) };
        } else {
            body = { uris: [pb.uri] };
        }
        if (pb.position_ms) body.position_ms = Math.max(0, Math.floor(pb.position_ms));

        return spotifyPlayOnDevice(body).then(function () {
            if (pb.paused && spotifyState.player) return spotifyState.player.pause();
        }).then(function () {
            return spotifyWaitForPlaybackStart();
        }).catch(function () { /* autoplay tras recarga puede estar bloqueado */ });
    }

    function spotifyTokenExpired(token) {
        return !token || !token.expiresAt || Date.now() >= token.expiresAt;
    }

    /* Helpers PKCE: generan code_verifier + code_challenge (S256). */
    function spotifyB64UrlFromBytes(bytes) {
        var bin = '';
        for (var i = 0; i < bytes.length; i++) bin += String.fromCharCode(bytes[i]);
        return btoa(bin).replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '');
    }

    function spotifyRandomVerifier() {
        var arr = new Uint8Array(64);
        (window.crypto || window.msCrypto).getRandomValues(arr);
        return spotifyB64UrlFromBytes(arr).slice(0, 96);
    }

    function spotifyChallengeFromVerifier(verifier) {
        var enc = new TextEncoder().encode(verifier);
        return window.crypto.subtle.digest('SHA-256', enc).then(function (buf) {
            return spotifyB64UrlFromBytes(new Uint8Array(buf));
        });
    }

    /* ── Refresh transparente del access token (cuando expira). ── */
    function spotifyRefreshAccessToken() {
        var token = spotifyReadToken();
        var clientId = spotifyClientId();
        if (!token || !token.refreshToken || !clientId) return Promise.reject(new Error('no-refresh'));

        var body = new URLSearchParams();
        body.set('grant_type', 'refresh_token');
        body.set('refresh_token', token.refreshToken);
        body.set('client_id', clientId);

        return fetch(SPOTIFY_TOKEN_URL, {
            method: 'POST',
            headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
            body: body.toString()
        }).then(function (r) {
            return r.json().then(function (d) { return { status: r.status, data: d }; });
        }).then(function (r) {
            if (r.status >= 400 || !r.data || !r.data.access_token) {
                throw new Error((r.data && (r.data.error_description || r.data.error)) || ('HTTP ' + r.status));
            }
            var rec = {
                accessToken: r.data.access_token,
                refreshToken: r.data.refresh_token || token.refreshToken,
                tokenType: r.data.token_type || 'Bearer',
                scope: r.data.scope || token.scope || '',
                expiresAt: Date.now() + ((Number(r.data.expires_in) || 3600) * 1000) - 30000
            };
            spotifyWriteToken(rec);
            return rec;
        });
    }

    /* Llama a la API de Spotify con el access_token actual. Refresca + reintenta una vez ante 401. */
    function spotifyApi(path, options, isRetry) {
        var token = spotifyReadToken();
        if (!token) return Promise.reject(new Error('not-connected'));

        var doRequest = function (tk) {
            var url = path.indexOf('http') === 0 ? path : SPOTIFY_API + path;
            var opts = options || {};
            var headers = Object.assign({ 'Authorization': 'Bearer ' + tk.accessToken }, opts.headers || {});
            if (opts.body && !headers['Content-Type']) headers['Content-Type'] = 'application/json';
            return fetch(url, {
                method: opts.method || 'GET',
                headers: headers,
                body: opts.body ? (typeof opts.body === 'string' ? opts.body : JSON.stringify(opts.body)) : undefined
            });
        };

        var fire = spotifyTokenExpired(token)
            ? spotifyRefreshAccessToken().then(doRequest)
            : doRequest(token);

        return fire.then(function (resp) {
            if (resp.status === 401 && !isRetry) {
                return spotifyRefreshAccessToken().then(function () {
                    return spotifyApi(path, options, true);
                });
            }
            if (resp.status === 204) return null;
            if (!resp.ok) {
                return resp.text().then(function (t) {
                    var err = new Error('spotify-' + resp.status + ': ' + t);
                    err.status = resp.status;
                    try {
                        var parsed = JSON.parse(t);
                        if (parsed && parsed.error) {
                            err.spotifyError = parsed.error;
                            if (parsed.error.reason) err.reason = parsed.error.reason;
                        }
                    } catch (_) {}
                    throw err;
                });
            }
            var ct = resp.headers.get('Content-Type') || '';
            return ct.indexOf('application/json') >= 0 ? resp.json() : resp.text();
        });
    }

    function spotifyFriendlyError(err) {
        if (!err) return 'Error desconocido.';
        if (err.message === 'missing-streaming-scope') {
            return 'La conexión es antigua. Pulsa Desconectar y vuelve a conectar Spotify para permitir la reproducción en Flexia.';
        }
        if (err.message === 'missing-scope') {
            return 'Faltan permisos en la conexión. Pulsa Desconectar y vuelve a conectar Spotify para autorizar biblioteca y playlists.';
        }
        if (err.status === 403) {
            var api403 = (err.spotifyError && err.spotifyError.message) ? String(err.spotifyError.message) : '';
            if (/development|quota|not allowed/i.test(api403)) {
                return 'Spotify bloquea escritura en apps en modo desarrollo. Añade tu cuenta en el Dashboard de la app (User Management) o solicita ampliación de cuota.';
            }
            return 'Spotify no permitió esta acción. Si ya reconectaste, revisa el Dashboard de tu app en Spotify (modo desarrollo / usuarios permitidos).';
        }
        if (err.message === 'connect-failed' || err.message === 'device-timeout') {
            return 'No se pudo iniciar el reproductor de Spotify. Comprueba que tienes Premium y vuelve a intentar.';
        }
        if (err.message === 'sdk-load-failed') {
            return 'No se pudo cargar el reproductor de Spotify. Revisa tu conexión e inténtalo de nuevo.';
        }
        if (spotifyIsDeviceNotFound(err)) {
            return 'El reproductor aún no está listo. Espera un momento y vuelve a intentar, o pulsa Reintentar.';
        }
        if (err.spotifyError && err.spotifyError.message) return err.spotifyError.message;
        var msg = err.message || '';
        if (msg.indexOf('spotify-') === 0) {
            var body = msg.replace(/^spotify-\d+:\s*/, '');
            try {
                var j = JSON.parse(body);
                if (j && j.error && j.error.message) return j.error.message;
            } catch (_) {}
        }
        return msg || 'Error en Spotify.';
    }

    function spotifyLoadSdk() {
        if (window.Spotify && typeof window.Spotify.Player === 'function') return Promise.resolve();
        if (spotifyState.sdkLoadPromise) return spotifyState.sdkLoadPromise;
        spotifyState.sdkLoadPromise = new Promise(function (resolve, reject) {
            var settled = false;
            function done() {
                if (settled) return;
                settled = true;
                resolve();
            }
            var prevReady = window.onSpotifyWebPlaybackSDKReady;
            window.onSpotifyWebPlaybackSDKReady = function () {
                if (typeof prevReady === 'function') prevReady();
                done();
            };
            var existing = document.querySelector('script[data-flexia-spotify-sdk="1"]');
            if (existing && window.Spotify) {
                done();
                return;
            }
            if (existing) return;
            var script = document.createElement('script');
            script.src = SPOTIFY_SDK_URL;
            script.async = true;
            script.setAttribute('data-flexia-spotify-sdk', '1');
            script.onerror = function () {
                if (!settled) {
                    settled = true;
                    reject(new Error('sdk-load-failed'));
                }
            };
            document.head.appendChild(script);
        });
        return spotifyState.sdkLoadPromise;
    }

    function spotifyActivateAudio() {
        if (spotifyState.player && typeof spotifyState.player.activateElement === 'function') {
            return spotifyState.player.activateElement();
        }
        return Promise.resolve();
    }

    function spotifyStopProgressTimer() {
        if (spotifyState.progressTimer) {
            clearInterval(spotifyState.progressTimer);
            spotifyState.progressTimer = null;
        }
    }

    function spotifyStartProgressTimer() {
        spotifyStopProgressTimer();
        spotifyState.progressTimer = setInterval(function () {
            if (!spotifyState.player) return;
            spotifyState.player.getCurrentState().then(function (state) {
                if (!state) return;
                spotifyState.playerState = state;
                if (spotifyState.lastView && spotifyState.lastView.kind === 'connected') {
                    spotifySyncViewFromPlayerState(false);
                }
            }).catch(function () {});
        }, SPOTIFY_POLL_MS);
    }

    function spotifyDestroyPlayer() {
        spotifyStopProgressTimer();
        spotifyState.connectPromise = null;
        spotifyState.deviceId = null;
        spotifyState.sdkReady = false;
        spotifyState.playerState = null;
        if (spotifyState.player) {
            try { spotifyState.player.disconnect(); } catch (_) {}
            spotifyState.player = null;
        }
    }

    function spotifyWaitForDeviceId(maxMs) {
        var limit = maxMs || 8000;
        return new Promise(function (resolve, reject) {
            if (spotifyState.deviceId) {
                resolve(spotifyState.deviceId);
                return;
            }
            var elapsed = 0;
            var step = 100;
            var timer = setInterval(function () {
                if (spotifyState.deviceId) {
                    clearInterval(timer);
                    resolve(spotifyState.deviceId);
                } else if ((elapsed += step) >= limit) {
                    clearInterval(timer);
                    reject(new Error('device-timeout'));
                }
            }, step);
        });
    }

    function spotifyCreatePlayer() {
        var player = new window.Spotify.Player({
            name: 'Flexia',
            getOAuthToken: function (cb) {
                spotifyGetAccessTokenString().then(function (token) { cb(token); }, function () { cb(''); });
            },
            volume: spotifyState.volume
        });

        player.addListener('ready', function (data) {
            spotifyState.deviceId = data.device_id;
            spotifyState.sdkReady = true;
        });
        player.addListener('not_ready', function () {
            spotifyState.sdkReady = false;
        });
        player.addListener('player_state_changed', function (state) {
            var prevSnap = spotifyState._prevSdkSnapshot;
            if (spotifyDetectTrackNaturalEnd(prevSnap, state)) {
                spotifyAdvanceQueueIfNeeded(state);
            }
            if (!state || !state.track_window || !state.track_window.current_track) {
                spotifyClearQuickPlayerPauseHold();
            } else {
                spotifyOnPlaybackPauseTransition(prevSnap, state);
            }
            spotifyState.playerState = state;
            if (state) spotifySyncPlaybackContextFromPlayer(state);
            spotifyState._prevSdkSnapshot = spotifySnapshotSdkState(state);
            spotifySchedulePersistUiState();
            spotifyUpdateQuickPlayer();
            spotifySyncPlaylistTrackPlayingIndicators();
            if (spotifyState.lastView && spotifyState.lastView.kind === 'connected') {
                spotifySyncViewFromPlayerState(true);
            }
        });
        player.addListener('authentication_error', function () {
            spotifyClearToken();
            spotifyClearUiState();
            spotifyDestroyPlayer();
            spotifyRenderAll({ kind: 'disconnected' });
        });
        player.addListener('account_error', function () {
            spotifyRenderAll({
                kind: 'premium_required',
                title: 'Se requiere Spotify Premium',
                message: 'Tu cuenta de Spotify no tiene Premium activo. Contrátalo para reproducir música en Flexia.'
            });
        });
        player.addListener('initialization_error', function (e) {
            spotifyRenderAll({ kind: 'error', message: 'Error al iniciar el reproductor: ' + ((e && e.message) || 'desconocido') });
        });

        spotifyState.player = player;
        return player;
    }

    function spotifyEnsurePlayer() {
        var token = spotifyReadToken();
        if (!token) return Promise.reject(new Error('not-connected'));
        if (!spotifyHasStreamingScope(token)) return Promise.reject(new Error('missing-streaming-scope'));

        if (spotifyState.connectPromise) return spotifyState.connectPromise;

        var promise = spotifyLoadSdk().then(function () {
            if (!spotifyState.player) spotifyCreatePlayer();
            if (spotifyState.deviceId && spotifyState.sdkReady) return spotifyState.deviceId;
            if (spotifyState.deviceId && !spotifyState.sdkReady) {
                return spotifyWaitForDeviceId(10000);
            }
            return spotifyState.player.connect().then(function (success) {
                if (!success) throw new Error('connect-failed');
                return spotifyWaitForDeviceId(10000);
            });
        });

        spotifyState.connectPromise = promise;
        return promise.finally(function () {
            if (spotifyState.connectPromise === promise) spotifyState.connectPromise = null;
        });
    }

    function spotifyPlaybackFromSdkState(state) {
        if (!state || !state.track_window || !state.track_window.current_track) return null;
        var track = state.track_window.current_track;
        var pos = state.position;
        if (!state.paused && state.timestamp) {
            pos = Math.min(track.duration_ms, state.position + (Date.now() - state.timestamp));
        }
        return {
            is_playing: !state.paused,
            progress_ms: pos,
            item: {
                id: track.id,
                name: track.name,
                duration_ms: track.duration_ms,
                artists: track.artists || [],
                album: track.album || null,
                external_urls: track.external_urls || { spotify: track.uri || '' }
            },
            device: {
                name: 'Flexia',
                type: 'Computer',
                volume_percent: Math.round(spotifyState.volume * 100)
            }
        };
    }

    function spotifyWaitForPlaybackStart() {
        return new Promise(function (resolve) {
            setTimeout(function () {
                if (spotifyState.player) {
                    spotifyState.player.getCurrentState().then(function (s) {
                        spotifyState.playerState = s;
                        spotifySyncViewFromPlayerState(true).then(resolve);
                    }).catch(function () { resolve(); });
                } else {
                    resolve();
                }
            }, 450);
        });
    }

    function spotifyIsInteractingWithSearch() {
        if (spotifyState.searchFocused || spotifyState.searchBrowsing) return true;
        var ae = document.activeElement;
        return !!(ae && ae.classList && ae.classList.contains('os-spotify-search__input'));
    }

    function spotifyIsInteractingWithAddPlaylist() {
        return !!(spotifyState.addPlaylistOpen && spotifyState.addPlaylistBrowsing);
    }

    function spotifyIsInteractingWithPlaylistTracks() {
        return !!(spotifyState.selectedPlaylist && spotifyState.playlistTracksBrowsing);
    }

    /** No re-renderizar toda la tarjeta (evita reset del scroll de listas). */
    function spotifyShouldPatchOnlyConnected(viewState) {
        if (!viewState || viewState.kind !== 'connected') return false;
        if (spotifyState.addPlaylistOpen && document.querySelector('.os-spotify-add-playlist__list')) return true;
        if (spotifyState.uiTab === 'playlists' && spotifyState.selectedPlaylist
            && document.querySelector('.os-spotify-playlists__tracks')) return true;
        if (spotifyState.uiTab !== 'search') return false;
        return !!document.querySelector('.os-spotify-search__results');
    }

    function spotifyMarkSearchBrowsing(active) {
        spotifyState.searchBrowsing = !!active;
        if (spotifyState.searchBrowseTimer) {
            clearTimeout(spotifyState.searchBrowseTimer);
            spotifyState.searchBrowseTimer = null;
        }
        if (active) {
            spotifyState.searchBrowseTimer = setTimeout(function () {
                spotifyState.searchBrowsing = false;
                spotifyState.searchBrowseTimer = null;
            }, 2500);
        }
    }

    function spotifyMarkAddPlaylistBrowsing(active) {
        if (!spotifyState.addPlaylistOpen) {
            spotifyState.addPlaylistBrowsing = false;
            return;
        }
        spotifyState.addPlaylistBrowsing = !!active;
        if (spotifyState.addPlaylistBrowseTimer) {
            clearTimeout(spotifyState.addPlaylistBrowseTimer);
            spotifyState.addPlaylistBrowseTimer = null;
        }
        if (active) {
            spotifyState.addPlaylistBrowseTimer = setTimeout(function () {
                spotifyState.addPlaylistBrowsing = false;
                spotifyState.addPlaylistBrowseTimer = null;
            }, 2500);
        }
    }

    function spotifyMarkPlaylistTracksBrowsing(active) {
        if (!spotifyState.selectedPlaylist) {
            spotifyState.playlistTracksBrowsing = false;
            return;
        }
        spotifyState.playlistTracksBrowsing = !!active;
        if (spotifyState.playlistTracksBrowseTimer) {
            clearTimeout(spotifyState.playlistTracksBrowseTimer);
            spotifyState.playlistTracksBrowseTimer = null;
        }
        if (active) {
            spotifyState.playlistTracksBrowseTimer = setTimeout(function () {
                spotifyState.playlistTracksBrowsing = false;
                spotifyState.playlistTracksBrowseTimer = null;
            }, 2500);
        }
    }

    function spotifyTrackFromPlaylistItem(entry) {
        if (!entry) return null;
        var t = entry.track || entry.item;
        if (!t || !t.uri) return null;
        if (t.type && t.type !== 'track') return null;
        return t;
    }

    function spotifyPlaylistItemsPath(playlistId, offset) {
        var params = new URLSearchParams();
        params.set('limit', '50');
        params.set('offset', String(offset || 0));
        var market = spotifyGetMarket();
        if (market) params.set('market', market);
        return '/playlists/' + encodeURIComponent(playlistId) + '/items?' + params.toString();
    }

    function spotifyLoadPlaylistTracks(append) {
        var sel = spotifyState.selectedPlaylist;
        if (!sel || !sel.id) return Promise.resolve();
        if (append && spotifyState.playlistTracksLoadingMore) return Promise.resolve();
        if (!append && spotifyState.playlistTracksLoading) return Promise.resolve();

        var offset = append ? spotifyState.playlistTracks.length : 0;
        if (append) {
            spotifyState.playlistTracksLoadingMore = true;
        } else {
            spotifyState.playlistTracksLoading = true;
            spotifyState.playlistTracksError = '';
            spotifyState.playlistTracks = [];
            spotifyState.playlistTracksTotal = 0;
        }
        spotifyRefreshPlaylistTracksDom();

        return spotifyApi(spotifyPlaylistItemsPath(sel.id, offset)).then(function (data) {
            var items = (data && data.items) || [];
            spotifyState.playlistTracksTotal = (data && typeof data.total === 'number') ? data.total : spotifyState.playlistTracks.length;
            items.forEach(function (entry) {
                var track = spotifyTrackFromPlaylistItem(entry);
                if (track) spotifyState.playlistTracks.push(track);
            });
        }).catch(function (err) {
            if (!append) {
                spotifyState.playlistTracksError = spotifyFriendlyError(err);
            }
        }).then(function () {
            spotifyState.playlistTracksLoading = false;
            spotifyState.playlistTracksLoadingMore = false;
            spotifyRefreshPlaylistTracksDom();
            spotifyScheduleRestoreScrollPositions();
            spotifySchedulePersistUiState();
        });
    }

    function spotifyPopulatePlaylistTracks(container) {
        if (!container) return;
        container.innerHTML = '';
        var sel = spotifyState.selectedPlaylist;
        if (!sel) return;

        if (spotifyState.playlistTracksLoading && !spotifyState.playlistTracks.length) {
            container.innerHTML = '<p class="os-spotify-search__hint"><i class="fas fa-spinner fa-spin"></i> Cargando canciones…</p>';
            return;
        }
        if (spotifyState.playlistTracksError && !spotifyState.playlistTracks.length) {
            container.innerHTML = '<p class="os-spotify-search__hint os-spotify-search__hint--err">' + escapeHtml(spotifyState.playlistTracksError) + '</p>';
            return;
        }
        if (!spotifyState.playlistTracks.length) {
            container.innerHTML = '<p class="os-spotify-search__hint">Esta playlist no tiene canciones.</p>';
            return;
        }

        spotifyState.playlistTracks.forEach(function (track) {
            if (track && track.uri) container.appendChild(spotifyRenderTrackRow(track, 'play-track'));
        });

        var hasMore = spotifyState.playlistTracks.length < spotifyState.playlistTracksTotal;
        if (hasMore || spotifyState.playlistTracksLoadingMore) {
            var moreBtn = document.createElement('button');
            moreBtn.type = 'button';
            moreBtn.className = 'os-spotify-playlists__more';
            moreBtn.dataset.spotifyAction = 'load-more-playlist-tracks';
            if (spotifyState.playlistTracksLoadingMore) {
                moreBtn.innerHTML = '<i class="fas fa-spinner fa-spin"></i> Cargando…';
                moreBtn.disabled = true;
            } else {
                var remaining = Math.max(0, spotifyState.playlistTracksTotal - spotifyState.playlistTracks.length);
                moreBtn.textContent = 'Cargar más' + (remaining ? ' (' + remaining + ')' : '');
            }
            container.appendChild(moreBtn);
        }
    }

    function spotifyRefreshPlaylistTracksDom() {
        document.querySelectorAll('.os-spotify-playlists__tracks').forEach(function (container) {
            spotifyRememberListScroll('playlistTracks', container);
            var scrollTop = spotifyGetRememberedListScroll('playlistTracks', container);
            spotifyPopulatePlaylistTracks(container);
            if (scrollTop > 0) {
                container.scrollTop = scrollTop;
                spotifyRememberListScroll('playlistTracks', container);
            }
        });
        spotifyRefreshQuickHoverTracksDom();
    }

    function spotifyPlaylistIdFromUri(uri) {
        if (!uri) return '';
        var s = String(uri);
        var m = s.match(/spotify:playlist:([a-zA-Z0-9]+)/);
        if (m) return m[1];
        m = s.match(/\/playlist\/([a-zA-Z0-9]+)/);
        return m ? m[1] : '';
    }

    function spotifyResolveQuickHoverPlaylist() {
        var ctx = spotifyNormalizePlaybackContext(spotifyState.playbackContext);
        if (ctx && ctx.mode === 'context' && ctx.contextUri) {
            var pid = spotifyPlaylistIdFromUri(ctx.contextUri);
            if (pid) {
                if (spotifyState.selectedPlaylist && spotifyState.selectedPlaylist.id === pid) {
                    return spotifyState.selectedPlaylist;
                }
                var lists = (spotifyState.playlistsCache && spotifyState.playlistsCache.items) || [];
                for (var i = 0; i < lists.length; i++) {
                    var pl = lists[i];
                    if (pl && pl.id === pid) {
                        return {
                            id: pl.id,
                            name: pl.name || 'Playlist',
                            uri: pl.uri || ctx.contextUri,
                            trackCount: (pl.tracks && pl.tracks.total) || 0
                        };
                    }
                }
                return {
                    id: pid,
                    name: 'Playlist',
                    uri: ctx.contextUri,
                    trackCount: 0
                };
            }
        }
        if (spotifyState.selectedPlaylist && spotifyState.selectedPlaylist.id) {
            return spotifyState.selectedPlaylist;
        }
        if (ctx && ctx.mode === 'queue' && ctx.source && String(ctx.source).indexOf('playlist') >= 0
            && spotifyState.selectedPlaylist) {
            return spotifyState.selectedPlaylist;
        }
        return null;
    }

    function spotifyPopulateQuickHoverTracks(container) {
        if (!container) return;
        container.innerHTML = '';
        var pl = spotifyResolveQuickHoverPlaylist();

        if (!pl) {
            container.innerHTML = '<p class="spotify-quick-panel__hover-tracks-hint">Reproduce una playlist para ver sus canciones aquí.</p>';
            return;
        }

        if (spotifyState.quickHoverTracksLoading && !spotifyState.quickHoverTracks.length) {
            container.innerHTML = '<p class="spotify-quick-panel__hover-tracks-hint"><i class="fas fa-spinner fa-spin"></i> Cargando canciones…</p>';
            return;
        }
        if (spotifyState.quickHoverTracksError && !spotifyState.quickHoverTracks.length) {
            container.innerHTML = '<p class="spotify-quick-panel__hover-tracks-hint spotify-quick-panel__hover-tracks-hint--err">'
                + escapeHtml(spotifyState.quickHoverTracksError) + '</p>';
            return;
        }
        if (!spotifyState.quickHoverTracks.length) {
            container.innerHTML = '<p class="spotify-quick-panel__hover-tracks-hint">Esta playlist no tiene canciones.</p>';
            return;
        }

        spotifyState.quickHoverTracks.forEach(function (track) {
            if (!track || !track.uri) return;
            container.appendChild(spotifyRenderTrackRow(track, 'play-track', 'os-spotify-media-row--hover'));
        });
    }

    function spotifyRefreshQuickHoverTracksDom() {
        document.querySelectorAll('[data-spotify-part="quick-hover-tracks"]').forEach(function (container) {
            spotifyRememberListScroll('quickHoverTracks', container);
            var scrollTop = spotifyGetRememberedListScroll('quickHoverTracks', container);
            spotifyPopulateQuickHoverTracks(container);
            if (scrollTop > 0) {
                container.scrollTop = scrollTop;
                spotifyRememberListScroll('quickHoverTracks', container);
            }
        });
        requestAnimationFrame(spotifyRepaintOpenQuickPops);
    }

    function spotifyLoadQuickHoverTracks(force) {
        var pl = spotifyResolveQuickHoverPlaylist();
        if (!pl || !pl.id) {
            spotifyState.quickHoverPlaylist = null;
            spotifyState.quickHoverTracks = [];
            spotifyState.quickHoverTracksTotal = 0;
            spotifyState.quickHoverTracksError = '';
            spotifyState.quickHoverTracksLoading = false;
            spotifyRefreshQuickHoverTracksDom();
            return Promise.resolve();
        }

        if (!force
            && spotifyState.quickHoverPlaylist
            && spotifyState.quickHoverPlaylist.id === pl.id
            && spotifyState.quickHoverTracks.length
            && !spotifyState.quickHoverTracksLoading) {
            spotifyRefreshQuickHoverTracksDom();
            return Promise.resolve();
        }

        if (!force
            && spotifyState.selectedPlaylist
            && spotifyState.selectedPlaylist.id === pl.id
            && spotifyState.playlistTracks.length
            && !spotifyState.playlistTracksLoading) {
            spotifyState.quickHoverPlaylist = pl;
            spotifyState.quickHoverTracks = spotifyState.playlistTracks.slice();
            spotifyState.quickHoverTracksTotal = spotifyState.playlistTracksTotal;
            spotifyState.quickHoverTracksError = '';
            spotifyRefreshQuickHoverTracksDom();
            return Promise.resolve();
        }

        spotifyState.quickHoverPlaylist = pl;
        spotifyState.quickHoverTracksLoading = true;
        spotifyState.quickHoverTracksError = '';
        if (!force) {
            spotifyState.quickHoverTracks = [];
            spotifyState.quickHoverTracksTotal = 0;
        }
        spotifyRefreshQuickHoverTracksDom();

        return spotifyApi(spotifyPlaylistItemsPath(pl.id, 0)).then(function (data) {
            var items = (data && data.items) || [];
            spotifyState.quickHoverTracksTotal = (data && typeof data.total === 'number') ? data.total : items.length;
            spotifyState.quickHoverTracks = [];
            items.forEach(function (entry) {
                var track = spotifyTrackFromPlaylistItem(entry);
                if (track) spotifyState.quickHoverTracks.push(track);
            });
        }).catch(function (err) {
            spotifyState.quickHoverTracks = [];
            spotifyState.quickHoverTracksError = spotifyFriendlyError(err);
        }).then(function () {
            spotifyState.quickHoverTracksLoading = false;
            spotifyRefreshQuickHoverTracksDom();
        });
    }

    function spotifyIsMarqueeMeasurable(mq) {
        if (!mq) return false;
        if (mq.offsetParent !== null) return true;
        var rects = mq.getClientRects();
        return rects.length > 0 && rects[0].width > 1;
    }

    var spotifyMarqueeObservers = typeof WeakMap !== 'undefined' ? new WeakMap() : null;

    function spotifyEnsureMarqueeObserver(mq) {
        if (!mq || !window.ResizeObserver || !spotifyMarqueeObservers || spotifyMarqueeObservers.has(mq)) return;
        var ro = new ResizeObserver(function () {
            if (!mq.dataset.spotifyMarqueeText) return;
            mq.dataset.spotifyMarqueeKey = '';
            spotifyApplyTextMarquee(mq, mq.dataset.spotifyMarqueeText, {
                title: mq.dataset.spotifyMarqueeTitlePart || 'quick-title',
                clone: mq.dataset.spotifyMarqueeClonePart || '',
                force: true
            });
        });
        ro.observe(mq);
        spotifyMarqueeObservers.set(mq, ro);
    }

    function spotifyApplyTextMarquee(mq, text, opts) {
        if (!mq) return;
        opts = opts || {};
        var titlePart = opts.title || 'quick-title';
        var clonePart = opts.clone || '';
        var t1 = mq.querySelector('[data-spotify-part="' + titlePart + '"]');
        var t2 = clonePart ? mq.querySelector('[data-spotify-part="' + clonePart + '"]') : null;
        if (!t1) return;

        var str = String(text || '');
        mq.dataset.spotifyMarqueeText = str;
        mq.dataset.spotifyMarqueeTitlePart = titlePart;
        mq.dataset.spotifyMarqueeClonePart = clonePart;
        spotifyEnsureMarqueeObserver(mq);

        var cacheKey = str + '@' + Math.round(mq.clientWidth || 0);
        if (!opts.force && mq.dataset.spotifyMarqueeKey === cacheKey) return;

        t1.textContent = str;
        mq.classList.remove('is-scroll');
        mq.style.removeProperty('--spotify-quick-marquee-dur');
        if (t2) {
            t2.hidden = true;
            t2.textContent = '';
        }

        if (!spotifyIsMarqueeMeasurable(mq)) {
            mq.dataset.spotifyMarqueePending = '1';
            mq.dataset.spotifyMarqueeKey = '';
            return;
        }
        delete mq.dataset.spotifyMarqueePending;

        requestAnimationFrame(function () {
            requestAnimationFrame(function () {
                var avail = mq.clientWidth;
                if (avail < 2) {
                    mq.dataset.spotifyMarqueePending = '1';
                    mq.dataset.spotifyMarqueeKey = '';
                    return;
                }
                delete mq.dataset.spotifyMarqueePending;
                var overflow = t1.scrollWidth > avail + 1;
                if (overflow && t2 && str) {
                    t2.hidden = false;
                    t2.textContent = str;
                    mq.classList.add('is-scroll');
                    var dur = Math.max(10, Math.round((t1.scrollWidth + avail) / 28));
                    mq.style.setProperty('--spotify-quick-marquee-dur', dur + 's');
                }
                mq.dataset.spotifyMarqueeKey = str + '@' + Math.round(avail);
            });
        });
    }

    function spotifySyncQuickHoverMarquee(pop, info) {
        if (!pop) return;
        info = info || spotifyGetQuickPlaybackInfo();
        var mq = pop.querySelector('[data-spotify-part="quick-hover-marquee"]');
        if (!mq) return;
        spotifyApplyTextMarquee(mq, info ? info.title : '—', {
            title: 'quick-hover-title',
            clone: 'quick-hover-title-clone'
        });
    }

    function spotifyRefreshQuickMarquees(host, force) {
        if (!host) return;
        var card = host.querySelector('[data-spotify-quick-card]');
        if (!card) return;
        var info = spotifyGetQuickPlaybackInfo();
        var mq = card.querySelector('[data-spotify-part="quick-marquee"]');
        if (mq) {
            if (force) mq.dataset.spotifyMarqueeKey = '';
            spotifyApplyTextMarquee(mq, info ? info.title : '', {
                title: 'quick-title',
                clone: 'quick-title-clone',
                force: !!force
            });
        }
        var pop = spotifyGetQuickHoverPop(host);
        if (pop) {
            var hoverMq = pop.querySelector('[data-spotify-part="quick-hover-marquee"]');
            if (hoverMq && force) hoverMq.dataset.spotifyMarqueeKey = '';
            spotifySyncQuickHoverMarquee(pop, info);
        }
    }

    function spotifySyncQuickHoverPopMetadata(pop) {
        if (!pop) return;
        var info = spotifyGetQuickPlaybackInfo();
        var hoverArtEl = pop.querySelector('[data-spotify-part="quick-hover-art"]');
        var artistEl = pop.querySelector('[data-spotify-part="quick-hover-artist"]');
        var playlistEl = pop.querySelector('[data-spotify-part="quick-hover-playlist-name"]');

        spotifySyncQuickHoverMarquee(pop, info);
        if (artistEl) artistEl.textContent = info ? info.artist : '';

        if (hoverArtEl && info && info.artUrl) {
            spotifyUpdateHeroArtElement(hoverArtEl, info.artUrl);
        }

        var pl = spotifyResolveQuickHoverPlaylist();
        if (playlistEl) {
            playlistEl.textContent = pl ? (pl.name || 'Playlist') : '';
            playlistEl.hidden = !pl;
        }
    }

    function spotifySyncQuickHoverPopContent(pop) {
        if (!pop) return;
        spotifySyncQuickHoverPopMetadata(pop);
        spotifyLoadQuickHoverTracks(false);
    }

    function spotifyIsQuickHostCompact(host) {
        return !!(host && (
            host.classList.contains('taskbar-spotify-quick--compact')
            || host.classList.contains('spotify-quick-mobile-fab--compact')
        ));
    }

    function spotifyIsQuickHostExtended(host) {
        return !!(host && (
            host.classList.contains('taskbar-spotify-quick--extended')
            || host.classList.contains('spotify-quick-mobile-fab--extended')
        ));
    }

    function spotifySyncQuickOpenHint(host) {
        if (!host) return;
        var meta = host.querySelector('[data-spotify-quick-open]');
        if (!meta) return;
        meta.setAttribute('title', spotifyIsQuickHostExtended(host)
            ? 'Clic para ver portada, canción y lista'
            : 'Abrir Spotify en Flexia');
    }

    function spotifyCloseQuickBrowsePops(exceptPop) {
        document.querySelectorAll('.spotify-quick-panel__hover-pop.spotify-quick-panel__hover-pop--browse.is-visible').forEach(function (pop) {
            if (exceptPop && pop === exceptPop) return;
            pop.classList.remove('is-visible', 'spotify-quick-panel__hover-pop--browse');
            pop._spotifyPopHiding = false;
            if (pop._spotifyPopHideTimer) {
                clearTimeout(pop._spotifyPopHideTimer);
                pop._spotifyPopHideTimer = null;
            }
            spotifyClearQuickHoverPopStyles(pop);
            spotifyDetachQuickPopFromPortal(pop);
            var h = pop.dataset.spotifyQuickPopHost
                ? document.getElementById(pop.dataset.spotifyQuickPopHost)
                : pop.closest('#taskbarSpotifyQuick, #spotifyQuickMobileFab');
            if (h) {
                spotifySetQuickPopHostOpen(h, false);
                h._spotifyHoverPinned = false;
                h._spotifyBrowsePopOpen = false;
            }
        });
        spotifySyncQuickPopBackdrop();
    }

    function spotifyEnsureQuickPopPortal() {
        var portal = document.getElementById('spotifyQuickPopPortal');
        if (!portal) {
            portal = document.createElement('div');
            portal.id = 'spotifyQuickPopPortal';
            portal.className = 'spotify-quick-pop-portal';
            portal.setAttribute('aria-hidden', 'true');
            var backdrop = document.createElement('div');
            backdrop.className = 'spotify-quick-pop-portal__backdrop';
            backdrop.setAttribute('aria-hidden', 'true');
            portal.appendChild(backdrop);
            document.body.appendChild(portal);
        }
        var backdropEl = portal.querySelector('.spotify-quick-pop-portal__backdrop');
        if (backdropEl && backdropEl.dataset.spotifyBackdropBound !== '1') {
            backdropEl.dataset.spotifyBackdropBound = '1';
        }
        return portal;
    }

    function spotifyPurgeOrphanQuickHoverPops(host) {
        if (!host || !host.id) return;
        var keep = spotifyGetQuickHoverPop(host);
        var selector = '#spotifyQuickPopPortal [data-spotify-part="quick-hover-pop"], '
            + '#' + host.id + ' [data-spotify-part="quick-hover-pop"]';
        document.querySelectorAll(selector).forEach(function (pop) {
            if (keep && pop === keep) return;
            if (pop._spotifyPopHideTimer) {
                clearTimeout(pop._spotifyPopHideTimer);
                pop._spotifyPopHideTimer = null;
            }
            if (pop._spotifyPopShowFrame) {
                cancelAnimationFrame(pop._spotifyPopShowFrame);
                pop._spotifyPopShowFrame = null;
            }
            pop._spotifyPopHiding = false;
            pop.classList.remove(
                'is-visible',
                'spotify-quick-panel__hover-pop--browse',
                'spotify-quick-panel__hover-pop--compact',
                'spotify-quick-panel__hover-pop--portal'
            );
            pop.remove();
        });
        spotifySetQuickPopHostOpen(host, false);
        spotifySyncQuickPopBackdrop();
    }

    function spotifyPurgeAllQuickHoverPopOrphans() {
        ['taskbarSpotifyQuick', 'spotifyQuickMobileFab'].forEach(function (id) {
            spotifyPurgeOrphanQuickHoverPops(document.getElementById(id));
        });
    }

    function spotifyEnsureQuickPopActivityBinding(host, pop) {
        if (!host || !pop || pop._spotifyActivityBound) return;
        pop._spotifyActivityBound = true;
        pop.addEventListener('pointerenter', function () {
            spotifyTouchQuickHoverActivity(host);
        });
    }

    function spotifySyncQuickPopBackdrop() {
        var portal = document.getElementById('spotifyQuickPopPortal');
        if (!portal) return;
        var backdrop = portal.querySelector('.spotify-quick-pop-portal__backdrop');
        var anyPopOpen = !!document.querySelector('.spotify-quick-panel__hover-pop.is-visible');
        if (backdrop) backdrop.classList.toggle('is-visible', anyPopOpen);
        portal.setAttribute('aria-hidden', anyPopOpen ? 'false' : 'true');
    }

    function spotifyClearCompactHoverHideTimer(host) {
        if (!host || !host._spotifyQuickHoverHideTimer) return;
        clearTimeout(host._spotifyQuickHoverHideTimer);
        host._spotifyQuickHoverHideTimer = null;
    }

    function spotifyClearCompactHoverAutoCloseTimer(host) {
        if (!host || !host._spotifyQuickAutoCloseTimer) return;
        clearTimeout(host._spotifyQuickAutoCloseTimer);
        host._spotifyQuickAutoCloseTimer = null;
    }

    function spotifyClearQuickHoverAutoCloseTimer(host) {
        spotifyClearCompactHoverAutoCloseTimer(host);
    }

    function spotifyIsQuickHoverPopVisible(host) {
        var pop = spotifyGetQuickHoverPop(host);
        return !!(pop && pop.classList.contains('is-visible'));
    }

    function spotifyIsCompactHoverPopVisible(host) {
        var pop = spotifyGetQuickHoverPop(host);
        return !!(pop && pop.classList.contains('is-visible')
            && pop.classList.contains('spotify-quick-panel__hover-pop--compact'));
    }

    function spotifyScheduleQuickHoverAutoClose(host) {
        if (!host || !spotifyIsQuickHoverPopVisible(host)) return;
        if (host._spotifyQuickHoverSuspendedUntil && Date.now() < host._spotifyQuickHoverSuspendedUntil) return;
        spotifyClearQuickHoverAutoCloseTimer(host);
        host._spotifyQuickAutoCloseTimer = setTimeout(function () {
            host._spotifyQuickAutoCloseTimer = null;
            if (host._spotifyQuickHoverSuspendedUntil && Date.now() < host._spotifyQuickHoverSuspendedUntil) return;
            spotifyPurgeOrphanQuickHoverPops(host);
            if (!spotifyIsQuickHoverPopVisible(host)) return;
            spotifyHideQuickHoverPop(host);
        }, SPOTIFY_QUICK_HOVER_AUTO_CLOSE_MS);
    }

    function spotifyTouchQuickHoverActivity(host) {
        if (!host || !spotifyIsQuickHoverPopVisible(host)) return;
        host._spotifyHoverShownAt = Date.now();
        spotifyScheduleQuickHoverAutoClose(host);
    }

    function spotifyScheduleCompactHoverAutoClose(host) {
        spotifyScheduleQuickHoverAutoClose(host);
    }

    function spotifyTouchCompactHoverActivity(host) {
        spotifyTouchQuickHoverActivity(host);
    }

    function spotifyScheduleCompactHoverHide(host, delayMs) {
        if (!host || !spotifyIsQuickHostCompact(host)) return;
        if (host._spotifyBrowsePopOpen || host._spotifyHoverPinned) return;
        var pop = spotifyGetQuickHoverPop(host);
        if (!pop || !pop.classList.contains('is-visible')) return;
        if (pop.classList.contains('spotify-quick-panel__hover-pop--browse')) return;

        var hideDelay = typeof delayMs === 'number' ? delayMs : 850;
        var graceMs = 600;
        spotifyClearCompactHoverHideTimer(host);
        host._spotifyQuickHoverHideTimer = setTimeout(function () {
            host._spotifyQuickHoverHideTimer = null;
            if (host._spotifyHoverPinned || host._spotifyBrowsePopOpen) return;
            if (Date.now() - (host._spotifyHoverShownAt || 0) < graceMs) {
                spotifyScheduleCompactHoverHide(host, hideDelay);
                return;
            }
            if (spotifyPointerInQuickZone(host)) return;
            spotifyHideQuickHoverPop(host);
        }, hideDelay);
    }

    function spotifyGetQuickHoverPop(host) {
        if (!host) return null;
        var pop = host.querySelector('[data-spotify-part="quick-hover-pop"]');
        if (pop) return pop;
        var portal = document.getElementById('spotifyQuickPopPortal');
        if (!portal || !host.id) return null;
        return portal.querySelector('[data-spotify-part="quick-hover-pop"][data-spotify-quick-pop-host="' + host.id + '"]');
    }

    function spotifyIsInQuickPlayerUi(el) {
        return !!(el && el.closest && el.closest('#taskbarSpotifyQuick, #spotifyQuickMobileFab, #spotifyQuickPopPortal'));
    }

    function spotifyClosestQuickAction(el) {
        if (!el || !el.closest) return null;
        var ctrl = el.closest('[data-spotify-action]');
        return ctrl && spotifyIsInQuickPlayerUi(ctrl) ? ctrl : null;
    }

    function spotifyClosestQuickToggle(el) {
        if (!el || !el.closest) return null;
        var toggle = el.closest('[data-spotify-quick-toggle]');
        return toggle && spotifyIsInQuickPlayerUi(toggle) ? toggle : null;
    }

    function spotifyIsQuickPopToggleTarget(target) {
        if (!target || !target.closest) return false;
        var host = target.closest('#taskbarSpotifyQuick, #spotifyQuickMobileFab');
        if (!host) return false;
        var pop = spotifyGetQuickHoverPop(host);
        var popOpen = !!(pop && pop.classList.contains('is-visible'));
        if (target.closest('[data-spotify-quick-toggle]')) return true;
        if (target.closest('.spotify-quick-panel__ctrls, [data-spotify-action]')) return false;
        if (target.closest('[data-spotify-quick-open], .spotify-quick-panel__marquee, .spotify-quick-panel__title')) {
            return true;
        }
        if (spotifyIsQuickHostCompact(host) && target.closest('.spotify-quick-panel__art')) return true;
        if (popOpen) return false;
        if (spotifyIsQuickHostExtended(host)
            && target.closest('.spotify-quick-panel__body, .spotify-quick-panel__art')) return true;
        return false;
    }

    function spotifyIsClickInsideQuickHoverPop(target) {
        if (!target || !target.closest) return false;
        return !!target.closest('.spotify-quick-panel__hover-pop-inner');
    }

    function spotifyIsClickOutsideQuickHoverPop(target) {
        if (!target || !target.closest) return true;
        if (target.closest('.spotify-quick-panel__hover-pop-inner')) return false;
        if (spotifyClosestQuickToggle(target) || spotifyClosestQuickAction(target)) return false;
        return true;
    }

    function spotifyDismissQuickHoverPopFromEvent(target, force) {
        var hosts = [];
        ['taskbarSpotifyQuick', 'spotifyQuickMobileFab'].forEach(function (id) {
            var h = document.getElementById(id);
            if (h && spotifyIsQuickHoverPopVisible(h)) hosts.push(h);
        });
        if (!hosts.length) return false;
        if (!spotifyIsClickOutsideQuickHoverPop(target)) return false;
        hosts.forEach(function (h) {
            if (force) spotifyHideQuickHoverPop(h, { force: true });
            else spotifyHideQuickHoverPop(h);
        });
        return true;
    }

    function spotifyDismissOpenQuickPops(target, opts) {
        var force = !!(opts && opts.force);
        var outsideDismiss = force || target == null;
        ['taskbarSpotifyQuick', 'spotifyQuickMobileFab'].forEach(function (id) {
            var host = document.getElementById(id);
            if (!host) return;
            var pop = spotifyGetQuickHoverPop(host);
            if (!pop || !pop.classList.contains('is-visible')) return;
            if (!outsideDismiss && host._spotifyQuickPopSuppressDismissOnce) return;
            if (!outsideDismiss && target && spotifyIsQuickPopToggleTarget(target)) return;
            if (!outsideDismiss && target && spotifyIsClickInsideQuickHoverPop(target)) return;
            spotifyHideQuickHoverPop(host, { force: outsideDismiss || force });
        });
        spotifySyncQuickPopBackdrop();
    }

    function spotifyHandleQuickActionClick(quickCtrl) {
        if (!quickCtrl) return false;
        var action = quickCtrl.getAttribute('data-spotify-action');
        if (!action) return false;
        var ctx = {};
        if (action === 'play-track' || action === 'play-playlist') {
            ctx.uri = quickCtrl.getAttribute('data-spotify-uri') || '';
        }
        if (action === 'play-track' && spotifyState.quickHoverPlaylist && spotifyState.quickHoverPlaylist.id) {
            if (!spotifyState.selectedPlaylist
                || spotifyState.selectedPlaylist.id !== spotifyState.quickHoverPlaylist.id) {
                spotifyState.selectedPlaylist = {
                    id: spotifyState.quickHoverPlaylist.id,
                    name: spotifyState.quickHoverPlaylist.name || 'Playlist',
                    uri: spotifyState.quickHoverPlaylist.uri || '',
                    trackCount: spotifyState.quickHoverPlaylist.trackCount || 0
                };
                spotifyState.playlistTracks = spotifyState.quickHoverTracks.slice();
                spotifyState.playlistTracksTotal = spotifyState.quickHoverTracksTotal;
            }
        }
        handleSpotifyAction(action, ctx).catch(function (err) {
            var msg = spotifyFriendlyError(err);
            if (spotifyState.lastView && spotifyState.lastView.kind === 'connected') {
                spotifyState.inlineError = msg;
                spotifyApplyConnectedView(spotifyState.lastView, false);
            } else {
                spotifyRenderAll({ kind: 'error', message: msg });
            }
        });
        return true;
    }

    function spotifyEnsureQuickPopAnchor(host, pop) {
        var panel = host && host.querySelector('.spotify-quick-panel');
        if (!panel || !pop) return;
        if (!pop._spotifyPopAnchor) {
            pop._spotifyPopAnchor = document.createComment('spotify-quick-hover-pop-anchor');
            panel.appendChild(pop._spotifyPopAnchor);
        }
    }

    function spotifyAttachQuickPopToPortal(host, pop) {
        if (!host || !pop) return;
        spotifyPurgeOrphanQuickHoverPops(host);
        spotifyEnsureQuickPopAnchor(host, pop);
        pop.dataset.spotifyQuickPopHost = host.id;
        var portal = spotifyEnsureQuickPopPortal();
        pop.classList.add('spotify-quick-panel__hover-pop--portal');
        portal.appendChild(pop);
    }

    function spotifyDetachQuickPopFromPortal(pop) {
        if (!pop || !pop._spotifyPopAnchor) return;
        var anchor = pop._spotifyPopAnchor;
        pop.classList.remove('spotify-quick-panel__hover-pop--portal');
        delete pop.dataset.spotifyQuickPopHost;
        if (anchor.parentNode) {
            anchor.parentNode.insertBefore(pop, anchor.nextSibling);
        }
    }

    function spotifyClearQuickHoverPopStyles(pop) {
        if (!pop) return;
        ['position', 'left', 'top', 'bottom', 'right', 'transform', 'z-index', 'margin', 'visibility'].forEach(function (key) {
            pop.style.removeProperty(key);
        });
    }

    function spotifyGetQuickPopAnchor(host) {
        if (!host) return null;
        if (spotifyIsQuickHostCompact(host)) {
            return host.querySelector('.spotify-quick-panel__art') || host;
        }
        return host.querySelector('[data-spotify-quick-open]')
            || host.querySelector('.spotify-quick-panel__art')
            || host;
    }

    function spotifyPointerInQuickZone(host, clientX, clientY) {
        var pop = spotifyGetQuickHoverPop(host);
        var x = typeof clientX === 'number' ? clientX : window._spotifyQuickPointerX;
        var y = typeof clientY === 'number' ? clientY : window._spotifyQuickPointerY;
        if (typeof x !== 'number' || typeof y !== 'number' || !host) return false;

        var pad = 14;
        function hitRect(r) {
            if (!r || (!r.width && !r.height)) return false;
            return x >= r.left - pad && x <= r.right + pad && y >= r.top - pad && y <= r.bottom + pad;
        }

        var hostRect = host.getBoundingClientRect();
        if (hitRect(hostRect)) return true;
        if (pop && pop.classList.contains('is-visible')) {
            var popRect = pop.getBoundingClientRect();
            if (hitRect(popRect)) return true;
            var bridge = {
                left: Math.min(hostRect.left, popRect.left) - pad,
                right: Math.max(hostRect.right, popRect.right) + pad,
                top: Math.min(hostRect.top, popRect.top) - pad,
                bottom: Math.max(hostRect.bottom, popRect.bottom) + pad
            };
            if (x >= bridge.left && x <= bridge.right && y >= bridge.top && y <= bridge.bottom) {
                return true;
            }
        }
        return false;
    }

    function spotifyLayoutQuickHoverPop(host, pop) {
        if (!host || !pop) return;
        var anchor = spotifyGetQuickPopAnchor(host) || host;
        var rect = anchor.getBoundingClientRect();
        if (!rect.width && !rect.height) return;

        pop.style.position = 'fixed';
        pop.style.zIndex = '600010';
        pop.style.margin = '0';
        pop.style.bottom = 'auto';
        pop.style.right = 'auto';
        pop.style.transform = 'none';
        pop.style.left = '-9999px';
        pop.style.top = '0';

        var popW = pop.offsetWidth || 260;
        var popH = pop.offsetHeight || 220;
        var gap = 10;
        var left = rect.left + (rect.width - popW) / 2;
        var top = rect.top - popH - gap;
        if (top < 8) top = rect.bottom + gap;
        left = Math.max(8, Math.min(window.innerWidth - popW - 8, left));

        pop.style.left = Math.round(left) + 'px';
        pop.style.top = Math.round(top) + 'px';
    }

    function spotifyRepaintOpenQuickPops() {
        ['taskbarSpotifyQuick', 'spotifyQuickMobileFab'].forEach(function (id) {
            var host = document.getElementById(id);
            if (!host) return;
            spotifyRefreshQuickMarquees(host, true);
            var pop = spotifyGetQuickHoverPop(host);
            if (pop && pop.classList.contains('is-visible')) {
                spotifyLayoutQuickHoverPop(host, pop);
            }
        });
    }

    function spotifySetQuickPopHostOpen(host, open) {
        if (!host) return;
        host.classList.toggle('taskbar-spotify-quick--pop-open', !!open);
        host.classList.toggle('spotify-quick-mobile-fab--pop-open', !!open);
    }

    function spotifyBindQuickOpenHandlers(host) {
        if (!host) return;
        var card = host.querySelector('[data-spotify-quick-card]');
        if (!card) return;
        var openZone = card.querySelector('[data-spotify-quick-open]');
        if (!openZone || openZone.dataset.spotifyQuickOpenBound === '1') return;
        openZone.dataset.spotifyQuickOpenBound = '1';
        openZone.addEventListener('click', function (e) {
            if (e.target.closest('[data-spotify-quick-toggle]')) return;
            e.preventDefault();
            e.stopPropagation();
            host._spotifyQuickPopSuppressDismissOnce = true;
            requestAnimationFrame(function () {
                host._spotifyQuickPopSuppressDismissOnce = false;
            });
            if (spotifyIsQuickHostCompact(host)) {
                spotifyToggleCompactHoverPop(host);
            } else {
                spotifyToggleQuickBrowsePop(host);
            }
        }, true);
    }

    function spotifyResetQuickHoverHostState(host, suspendHover) {
        if (!host) return;
        host._spotifyHoverPinned = false;
        host._spotifyBrowsePopOpen = false;
        host._spotifyQuickPopSuppressDismissOnce = false;
        if (suspendHover) host._spotifyQuickHoverSuspendedUntil = Date.now() + 400;
        spotifyClearCompactHoverHideTimer(host);
        spotifyClearQuickHoverAutoCloseTimer(host);
    }

    function spotifyCloseQuickHoverPopNow(host, suspendHover) {
        if (!host) return;
        var pop = spotifyGetQuickHoverPop(host);
        spotifyResetQuickHoverHostState(host, !!suspendHover);
        if (pop) {
            if (pop._spotifyPopHideTimer) {
                clearTimeout(pop._spotifyPopHideTimer);
                pop._spotifyPopHideTimer = null;
            }
            if (pop._spotifyPopShowFrame) {
                cancelAnimationFrame(pop._spotifyPopShowFrame);
                pop._spotifyPopShowFrame = null;
            }
            pop._spotifyPopHiding = false;
            pop.classList.remove(
                'is-visible',
                'spotify-quick-panel__hover-pop--browse',
                'spotify-quick-panel__hover-pop--compact',
                'spotify-quick-panel__hover-pop--portal'
            );
            pop.style.removeProperty('pointer-events');
            pop.style.removeProperty('visibility');
            spotifyClearQuickHoverPopStyles(pop);
            spotifyDetachQuickPopFromPortal(pop);
        }
        spotifySetQuickPopHostOpen(host, false);
        spotifyPurgeOrphanQuickHoverPops(host);
        spotifySyncQuickPopBackdrop();
    }

    function spotifyCloseAllQuickHoverPopsNow(suspendHover) {
        ['taskbarSpotifyQuick', 'spotifyQuickMobileFab'].forEach(function (id) {
            spotifyCloseQuickHoverPopNow(document.getElementById(id), suspendHover);
        });
        spotifyPurgeAllQuickHoverPopOrphans();
        spotifySyncQuickPopBackdrop();
    }

    function spotifyShowQuickHoverPop(host, mode) {
        var pop = spotifyGetQuickHoverPop(host);
        if (!host || !pop) return;
        if (host._spotifyQuickHoverSuspendedUntil && Date.now() < host._spotifyQuickHoverSuspendedUntil) return;
        if (mode === 'compact' && !spotifyIsQuickHostCompact(host)) return;
        if (mode === 'browse' && spotifyIsQuickHostCompact(host)) return;
        if (pop._spotifyPopHideTimer) {
            clearTimeout(pop._spotifyPopHideTimer);
            pop._spotifyPopHideTimer = null;
        }
        if (pop._spotifyPopShowFrame) {
            cancelAnimationFrame(pop._spotifyPopShowFrame);
            pop._spotifyPopShowFrame = null;
        }
        pop._spotifyPopHiding = false;
        host._spotifyHoverHideAfter = 0;
        pop.classList.remove('spotify-quick-panel__hover-pop--browse', 'spotify-quick-panel__hover-pop--compact');
        spotifyClearQuickHoverPopStyles(pop);
        if (mode === 'browse') {
            pop.classList.add('spotify-quick-panel__hover-pop--browse');
            host._spotifyBrowsePopOpen = true;
            host._spotifyHoverPinned = false;
        } else {
            pop.classList.add('spotify-quick-panel__hover-pop--compact');
            host._spotifyBrowsePopOpen = false;
        }
        pop.style.removeProperty('pointer-events');
        spotifySyncQuickHoverPopContent(pop);
        spotifyAttachQuickPopToPortal(host, pop);
        pop.classList.add('is-visible');
        spotifySetQuickPopHostOpen(host, true);
        host._spotifyHoverShownAt = Date.now();
        pop.style.visibility = 'hidden';
        spotifyLayoutQuickHoverPop(host, pop);
        spotifyEnsureQuickPopActivityBinding(host, pop);
        pop._spotifyPopShowFrame = requestAnimationFrame(function () {
            pop._spotifyPopShowFrame = null;
            if (!pop.classList.contains('is-visible')) return;
            spotifyLayoutQuickHoverPop(host, pop);
            pop.style.removeProperty('visibility');
            spotifySyncQuickHoverMarquee(pop);
            spotifySyncQuickPopBackdrop();
        });
        spotifyScheduleQuickHoverAutoClose(host);
        spotifySyncQuickPopBackdrop();
    }

    function spotifyHideQuickHoverPop(host, opts) {
        var force = !!(opts && opts.force);
        var pop = spotifyGetQuickHoverPop(host);
        if (pop) {
            pop.querySelectorAll('[data-spotify-part="quick-hover-tracks"]').forEach(function (el) {
                spotifyRememberListScroll('quickHoverTracks', el);
            });
        }
        if (host) spotifyClearQuickHoverAutoCloseTimer(host);
        if (!pop) {
            if (host) {
                spotifySetQuickPopHostOpen(host, false);
                host._spotifyHoverPinned = false;
                host._spotifyBrowsePopOpen = false;
            }
            spotifySyncQuickPopBackdrop();
            return;
        }
        if (pop._spotifyPopHiding) {
            if (!force) return;
            if (pop._spotifyPopHideTimer) {
                clearTimeout(pop._spotifyPopHideTimer);
                pop._spotifyPopHideTimer = null;
            }
        }
        if (!pop.classList.contains('is-visible')) {
            spotifySetQuickPopHostOpen(host, false);
            if (host) {
                host._spotifyHoverPinned = false;
                host._spotifyBrowsePopOpen = false;
            }
            spotifySyncQuickPopBackdrop();
            return;
        }
        var wasCompact = pop.classList.contains('spotify-quick-panel__hover-pop--compact');
        var wasBrowse = pop.classList.contains('spotify-quick-panel__hover-pop--browse');
        var fadeMs = (wasCompact || wasBrowse) ? SPOTIFY_QUICK_HOVER_FADE_MS : 180;
        if (pop._spotifyPopShowFrame) {
            cancelAnimationFrame(pop._spotifyPopShowFrame);
            pop._spotifyPopShowFrame = null;
        }
        pop._spotifyPopHiding = true;
        pop.classList.remove('is-visible');
        pop.style.removeProperty('visibility');
        pop.style.removeProperty('pointer-events');
        spotifySetQuickPopHostOpen(host, false);
        spotifySyncQuickPopBackdrop();
        if (host) {
            host._spotifyHoverPinned = false;
            host._spotifyBrowsePopOpen = false;
            spotifyClearCompactHoverHideTimer(host);
        }
        if (pop._spotifyPopHideTimer) clearTimeout(pop._spotifyPopHideTimer);
        pop._spotifyPopHideTimer = setTimeout(function () {
            pop._spotifyPopHideTimer = null;
            if (!pop._spotifyPopHiding) return;
            pop._spotifyPopHiding = false;
            pop.classList.remove('spotify-quick-panel__hover-pop--compact', 'spotify-quick-panel__hover-pop--browse');
            spotifyClearQuickHoverPopStyles(pop);
            if (pop.classList.contains('spotify-quick-panel__hover-pop--portal')) {
                spotifyDetachQuickPopFromPortal(pop);
            }
            spotifySyncQuickPopBackdrop();
        }, fadeMs);
    }

    function spotifyToggleQuickBrowsePop(host) {
        if (!host) return;
        if (spotifyIsQuickHostCompact(host)) {
            spotifyCloseQuickHoverPopNow(host);
            return;
        }
        var pop = spotifyGetQuickHoverPop(host);
        if (!pop) return;
        var willShow = !pop.classList.contains('is-visible') || !pop.classList.contains('spotify-quick-panel__hover-pop--browse');
        if (!willShow) {
            spotifyHideQuickHoverPop(host);
            return;
        }
        spotifyCloseQuickBrowsePops(pop);
        spotifyShowQuickHoverPop(host, 'browse');
    }

    function spotifyToggleCompactHoverPop(host) {
        if (!host) return;
        if (!spotifyIsQuickHostCompact(host)) {
            spotifyCloseQuickHoverPopNow(host);
            return;
        }
        var pop = spotifyGetQuickHoverPop(host);
        if (!pop) return;
        var isOpen = pop.classList.contains('is-visible') && pop.classList.contains('spotify-quick-panel__hover-pop--compact');
        if (isOpen) {
            host._spotifyHoverPinned = false;
            spotifyHideQuickHoverPop(host);
            return;
        }
        host._spotifyHoverPinned = true;
        spotifyShowQuickHoverPop(host, 'compact');
    }

    function spotifyRenderPlaylistsTab(panel) {
        var wrap = document.createElement('div');
        wrap.className = 'os-spotify-playlists';

        if (spotifyState.selectedPlaylist) {
            var sel = spotifyState.selectedPlaylist;
            var detail = document.createElement('div');
            detail.className = 'os-spotify-playlists__detail';

            var head = document.createElement('div');
            head.className = 'os-spotify-playlists__detail-head';

            var backBtn = document.createElement('button');
            backBtn.type = 'button';
            backBtn.className = 'os-spotify-playlists__back';
            backBtn.dataset.spotifyAction = 'playlist-back';
            backBtn.setAttribute('aria-label', 'Volver a playlists');
            backBtn.title = 'Volver';
            backBtn.innerHTML = '<i class="fas fa-arrow-left" aria-hidden="true"></i>';
            head.appendChild(backBtn);

            var headMeta = document.createElement('div');
            headMeta.className = 'os-spotify-playlists__detail-meta';
            var headTitle = document.createElement('span');
            headTitle.className = 'os-spotify-playlists__detail-title';
            headTitle.textContent = sel.name || 'Playlist';
            headMeta.appendChild(headTitle);
            var count = spotifyState.playlistTracksTotal || (sel.trackCount || 0);
            if (count) {
                var headSub = document.createElement('span');
                headSub.className = 'os-spotify-playlists__detail-sub';
                headSub.textContent = count + ' canciones';
                headMeta.appendChild(headSub);
            }
            head.appendChild(headMeta);

            if (sel.uri) {
                var playAllBtn = document.createElement('button');
                playAllBtn.type = 'button';
                playAllBtn.className = 'os-spotify-playlists__play-all';
                playAllBtn.dataset.spotifyAction = 'play-playlist';
                playAllBtn.dataset.spotifyUri = sel.uri;
                playAllBtn.title = 'Reproducir toda la playlist';
                playAllBtn.innerHTML = '<i class="fas fa-play" aria-hidden="true"></i>';
                head.appendChild(playAllBtn);
            }
            detail.appendChild(head);

            var tracks = document.createElement('div');
            tracks.className = 'os-spotify-playlists__tracks';
            detail.appendChild(tracks);
            spotifyPopulatePlaylistTracks(tracks);
            wrap.appendChild(detail);
        } else {
            var plList = document.createElement('div');
            plList.className = 'os-spotify-playlists__list os-spotify-playlists__list--tab';
            var lists = spotifyState.playlistsCache && Array.isArray(spotifyState.playlistsCache.items)
                ? spotifyState.playlistsCache.items : [];
            if (spotifyState.playlistsInFlight && !lists.length) {
                plList.innerHTML = '<p class="os-spotify-search__hint"><i class="fas fa-spinner fa-spin"></i> Cargando playlists…</p>';
            } else if (!lists.length) {
                plList.innerHTML = '<p class="os-spotify-search__hint">No tienes playlists en esta cuenta.</p>';
            } else {
                spotifyDedupePlaylists(lists).slice(0, 40).forEach(function (pl) {
                    if (!pl || !pl.id) return;
                    var row = document.createElement('button');
                    row.type = 'button';
                    row.className = 'os-spotify-media-row os-spotify-media-row--playlist';
                    row.dataset.spotifyAction = 'open-playlist';
                    row.dataset.playlistId = pl.id;
                    row.dataset.playlistName = pl.name || '';
                    row.dataset.spotifyUri = pl.uri || '';
                    row.dataset.playlistTrackCount = String((pl.tracks && pl.tracks.total) || 0);
                    var img = pl.images && pl.images[0] ? pl.images[0].url : '';
                    row.innerHTML = (img
                        ? '<span class="os-spotify-media-row__art" style="background-image:url(\'' + img.replace(/'/g, '%27') + '\')"></span>'
                        : '<span class="os-spotify-media-row__art os-spotify-media-row__art--empty"><i class="fas fa-music"></i></span>')
                        + '<span class="os-spotify-media-row__body">'
                        + '<span class="os-spotify-media-row__title">' + escapeHtml(pl.name || '—') + '</span>'
                        + '<span class="os-spotify-media-row__sub">' + ((pl.tracks && pl.tracks.total) || 0) + ' canciones · elegir</span>'
                        + '</span>'
                        + '<i class="fas fa-chevron-right os-spotify-media-row__play" aria-hidden="true"></i>';
                    plList.appendChild(row);
                });
            }
            wrap.appendChild(plList);
        }

        panel.appendChild(wrap);
    }

    function spotifyPopulateSearchResults(container) {
        if (!container) return;
        container.innerHTML = '';
        if (spotifyState.searchLoading) {
            container.innerHTML = '<p class="os-spotify-search__hint"><i class="fas fa-spinner fa-spin"></i> Buscando…</p>';
            return;
        }
        if (spotifyState.searchError) {
            container.innerHTML = '<p class="os-spotify-search__hint os-spotify-search__hint--err">' + escapeHtml(spotifyState.searchError) + '</p>';
            return;
        }
        if (!spotifyState.searchQuery.trim()) {
            container.innerHTML = '<p class="os-spotify-search__hint">Escribe para buscar canciones en Spotify.</p>';
            return;
        }
        if (!spotifyState.searchResults.length) {
            container.innerHTML = '<p class="os-spotify-search__hint">Sin resultados. Prueba con otro término.</p>';
            return;
        }
        spotifyState.searchResults.forEach(function (track) {
            if (track && track.uri) container.appendChild(spotifyRenderTrackRow(track, 'play-track', '', false));
        });
    }

    function spotifyRefreshSearchResultsDom() {
        document.querySelectorAll('.os-spotify-search__results').forEach(function (container) {
            spotifyRememberListScroll('searchResults', container);
            var scrollTop = spotifyGetRememberedListScroll('searchResults', container);
            spotifyPopulateSearchResults(container);
            if (scrollTop > 0) {
                container.scrollTop = scrollTop;
                spotifyRememberListScroll('searchResults', container);
            }
        });
    }

    function spotifyPatchPlaybackUi(viewState) {
        var pb = viewState.playback;
        var hasTrack = pb && pb.item;
        var isPlaying = hasTrack && pb.is_playing;
        document.querySelectorAll('.os-spotify-card').forEach(function (card) {
            card.classList.toggle('is-playing', !!isPlaying);
            card.classList.toggle('has-track', !!hasTrack);

            var art = card.querySelector('[data-spotify-part="hero-art"]');
            if (art) {
                var artUrl = hasTrack && pb.item.album && pb.item.album.images && pb.item.album.images[0]
                    ? pb.item.album.images[0].url : '';
                spotifyUpdateHeroArtElement(art, artUrl);
            }

            var badgeWrap = card.querySelector('[data-spotify-part="hero-badge-wrap"]');
            if (badgeWrap) {
                badgeWrap.innerHTML = isPlaying
                    ? '<div class="os-spotify-hero__badge"><span class="os-spotify-hero__eq" aria-hidden="true"><i></i><i></i><i></i></span> Reproduciendo</div>'
                    : '';
            }

            var title = card.querySelector('[data-spotify-part="hero-title"]');
            if (title) {
                title.textContent = hasTrack ? (pb.item.name || '—') : 'Nada en reproducción';
                title.classList.toggle('is-link', !!(hasTrack && pb.item.external_urls && pb.item.external_urls.spotify));
            }

            var sub = card.querySelector('[data-spotify-part="hero-sub"]');
            if (sub) {
                if (hasTrack) {
                    var artistName = pb.item.artists ? pb.item.artists.map(function (a) { return a.name; }).join(', ') : '';
                    var albumName = pb.item.album ? (pb.item.album.name || '') : '';
                    sub.textContent = artistName + (albumName ? ' · ' + albumName : '');
                } else {
                    sub.textContent = 'Busca una canción o elige una playlist';
                }
            }

            var prog = card.querySelector('[data-spotify-part="progress"]');
            if (hasTrack && pb.item.duration_ms) {
                var pct = Math.max(0, Math.min(100, (pb.progress_ms / pb.item.duration_ms) * 100));
                if (!prog) {
                    prog = document.createElement('div');
                    prog.className = 'os-spotify-card__progress';
                    prog.dataset.spotifySeek = '1';
                    prog.setAttribute('data-spotify-part', 'progress');
                    var ctrls = card.querySelector('.os-spotify-card__controls');
                    if (ctrls) card.insertBefore(prog, ctrls);
                }
                prog.dataset.durationMs = String(pb.item.duration_ms);
                prog.innerHTML = '<div class="os-spotify-card__progress-bar" style="width:' + pct.toFixed(2) + '%"></div>'
                    + '<div class="os-spotify-card__progress-times">'
                    + '<span class="os-spotify-progress-cur">' + spotifyFormatMs(pb.progress_ms) + '</span>'
                    + '<span>' + spotifyFormatMs(pb.item.duration_ms) + '</span>'
                    + '</div>';
            } else if (prog) {
                prog.parentNode.removeChild(prog);
            }

            var playBtn = card.querySelector('.os-spotify-ctrl--play');
            if (playBtn) {
                playBtn.dataset.spotifyAction = isPlaying ? 'pause' : 'play';
                playBtn.setAttribute('aria-label', isPlaying ? 'Pausar' : 'Reproducir');
                playBtn.innerHTML = '<i class="fas fa-' + (isPlaying ? 'pause' : 'play') + '"></i>';
            }

            var likeBtn = card.querySelector('.os-spotify-ctrl--like');
            if (likeBtn) {
                if (viewState.liked) {
                    likeBtn.dataset.liked = '1';
                    likeBtn.innerHTML = '<i class="fas fa-heart"></i>';
                } else {
                    likeBtn.removeAttribute('data-liked');
                    likeBtn.innerHTML = '<i class="far fa-heart"></i>';
                }
            }

            var addBtn = card.querySelector('.os-spotify-ctrl--add');
            if (addBtn) {
                addBtn.classList.toggle('is-active', !!spotifyState.addPlaylistOpen);
                if (!hasTrack) addBtn.setAttribute('disabled', '');
                else addBtn.removeAttribute('disabled');
            }

            var errEl = card.querySelector('[data-spotify-part="inline-error"]');
            if (spotifyState.inlineError) {
                if (!errEl) {
                    errEl = document.createElement('div');
                    errEl.className = 'os-spotify-inline-error';
                    errEl.setAttribute('data-spotify-part', 'inline-error');
                    errEl.setAttribute('role', 'alert');
                    card.insertBefore(errEl, card.firstChild);
                }
                errEl.innerHTML = '<i class="fas fa-exclamation-circle" aria-hidden="true"></i> ' + escapeHtml(spotifyState.inlineError);
            } else if (errEl) {
                errEl.parentNode.removeChild(errEl);
            }
        });
        spotifyUpdateQuickPlayer();
    }

    function spotifyQuickAccountConnected() {
        if (!spotifyInstallReady()) return false;
        var token = spotifyReadToken();
        if (!token || !spotifyHasStreamingScope(token)) return false;
        return true;
    }

    function spotifyGetQuickPlaybackInfo() {
        var pb = spotifyPlaybackFromSdkState(spotifyState.playerState);
        if (!pb || !pb.item) return null;
        if (!pb.is_playing && !spotifyState.quickPlayerHoldWhilePaused) return null;
        var artUrl = pb.item.album && pb.item.album.images && pb.item.album.images[0]
            ? pb.item.album.images[0].url : '';
        var title = pb.item.name || 'Spotify';
        var artist = pb.item.artists ? pb.item.artists.map(function (a) { return a.name; }).join(', ') : '';
        return { artUrl: artUrl, title: title, artist: artist, isPlaying: !!pb.is_playing };
    }

    function spotifyReadQuickCompact() {
        try {
            return localStorage.getItem(LS_SPOTIFY_QUICK_COMPACT) === '1';
        } catch (_) {
            return false;
        }
    }

    function spotifyWriteQuickCompact(compact) {
        spotifyState.quickCompact = !!compact;
        try {
            localStorage.setItem(LS_SPOTIFY_QUICK_COMPACT, compact ? '1' : '0');
        } catch (_) {}
    }

    /** Celular (FAB): siempre compacto; PC/tablet (barra): preferencia guardada. */
    function spotifyResolveQuickCompact(host) {
        if (host && host.id === 'spotifyQuickMobileFab' && isMobileMode()) {
            return true;
        }
        return !!spotifyState.quickCompact;
    }

    function spotifyApplyQuickCompactUi(host) {
        if (!host) return;
        var compact = spotifyResolveQuickCompact(host);
        var modeKey = compact ? 'compact' : 'extended';
        var modeChanged = host.dataset.spotifyQuickLayoutMode !== modeKey;
        host.dataset.spotifyQuickLayoutMode = modeKey;

        host.classList.toggle('taskbar-spotify-quick--compact', compact);
        host.classList.toggle('taskbar-spotify-quick--extended', !compact);
        host.classList.toggle('spotify-quick-mobile-fab--compact', compact);
        host.classList.toggle('spotify-quick-mobile-fab--extended', !compact);
        if (!compact) {
            host.style.removeProperty('width');
            host.style.removeProperty('min-width');
            host.style.removeProperty('max-width');
        }
        if (host.id === 'taskbarSpotifyQuick') {
            var taskbar = document.getElementById('taskbar');
            if (taskbar) taskbar.classList.toggle('taskbar--spotify-quick-compact', compact);
        }
        spotifySyncQuickArtLayout();
        if (modeChanged) {
            spotifyCloseQuickHoverPopNow(host, true);
            spotifyPurgeOrphanQuickHoverPops(host);
        }
        spotifyRefreshQuickMarquees(host, modeChanged);
        spotifySyncQuickOpenHint(host);
    }

    function spotifyQuickControlsHtml() {
        return ''
            + '<button type="button" class="os-spotify-ctrl os-spotify-ctrl--quick" data-spotify-action="prev" aria-label="Anterior" title="Anterior"><i class="fas fa-step-backward"></i></button>'
            + '<button type="button" class="os-spotify-ctrl os-spotify-ctrl--play os-spotify-ctrl--quick" data-spotify-action="play" aria-label="Pausar"><i class="fas fa-pause"></i></button>'
            + '<button type="button" class="os-spotify-ctrl os-spotify-ctrl--quick" data-spotify-action="next" aria-label="Siguiente" title="Siguiente"><i class="fas fa-step-forward"></i></button>';
    }

    function spotifyBuildQuickPlayerPanel(panelClass) {
        var panel = document.createElement('div');
        panel.className = 'spotify-quick-panel ' + panelClass;

        var artHost = document.createElement('div');
        artHost.className = 'spotify-quick-panel__art os-spotify-hero';
        artHost.setAttribute('title', 'Clic para cambiar vista de portada');
        artHost.appendChild(spotifyCreateHeroArtWrap(''));

        var body = document.createElement('div');
        body.className = 'spotify-quick-panel__body';
        body.setAttribute('data-spotify-part', 'quick-body');

        var meta = document.createElement('div');
        meta.className = 'spotify-quick-panel__meta';
        meta.setAttribute('data-spotify-quick-open', '1');
        var marquee = document.createElement('div');
        marquee.className = 'spotify-quick-panel__marquee';
        marquee.setAttribute('data-spotify-part', 'quick-marquee');
        var marqueeInner = document.createElement('div');
        marqueeInner.className = 'spotify-quick-panel__marquee-inner';
        var title = document.createElement('span');
        title.className = 'spotify-quick-panel__title';
        title.setAttribute('data-spotify-part', 'quick-title');
        var titleClone = document.createElement('span');
        titleClone.className = 'spotify-quick-panel__title spotify-quick-panel__title--clone';
        titleClone.setAttribute('data-spotify-part', 'quick-title-clone');
        titleClone.setAttribute('aria-hidden', 'true');
        titleClone.hidden = true;
        marqueeInner.appendChild(title);
        marqueeInner.appendChild(titleClone);
        marquee.appendChild(marqueeInner);
        meta.appendChild(marquee);

        var ctrls = document.createElement('div');
        ctrls.className = 'spotify-quick-panel__ctrls';
        ctrls.innerHTML = spotifyQuickControlsHtml();

        body.appendChild(meta);
        body.appendChild(ctrls);

        var hoverPop = document.createElement('div');
        hoverPop.className = 'spotify-quick-panel__hover-pop';
        hoverPop.setAttribute('data-spotify-part', 'quick-hover-pop');
        hoverPop.setAttribute('role', 'dialog');
        hoverPop.setAttribute('aria-label', 'Reproductor rápido Spotify');
        var hoverInner = document.createElement('div');
        hoverInner.className = 'spotify-quick-panel__hover-pop-inner';

        var hoverHead = document.createElement('div');
        hoverHead.className = 'spotify-quick-panel__hover-head';

        var hoverArt = document.createElement('div');
        hoverArt.className = 'spotify-quick-panel__hover-art os-spotify-hero__art os-spotify-hero__art--empty';
        hoverArt.setAttribute('data-spotify-part', 'quick-hover-art');
        hoverArt.setAttribute('aria-hidden', 'true');
        hoverArt.innerHTML = '<i class="fab fa-spotify" aria-hidden="true"></i>';

        var hoverTitleMarquee = document.createElement('div');
        hoverTitleMarquee.className = 'spotify-quick-panel__marquee spotify-quick-panel__hover-marquee';
        hoverTitleMarquee.setAttribute('data-spotify-part', 'quick-hover-marquee');
        var hoverTitleMarqueeInner = document.createElement('div');
        hoverTitleMarqueeInner.className = 'spotify-quick-panel__marquee-inner';
        var hoverTitle = document.createElement('span');
        hoverTitle.className = 'spotify-quick-panel__hover-title';
        hoverTitle.setAttribute('data-spotify-part', 'quick-hover-title');
        hoverTitle.textContent = '—';
        var hoverTitleClone = document.createElement('span');
        hoverTitleClone.className = 'spotify-quick-panel__hover-title spotify-quick-panel__title--clone';
        hoverTitleClone.setAttribute('data-spotify-part', 'quick-hover-title-clone');
        hoverTitleClone.setAttribute('aria-hidden', 'true');
        hoverTitleClone.hidden = true;
        hoverTitleMarqueeInner.appendChild(hoverTitle);
        hoverTitleMarqueeInner.appendChild(hoverTitleClone);
        hoverTitleMarquee.appendChild(hoverTitleMarqueeInner);

        var hoverArtist = document.createElement('div');
        hoverArtist.className = 'spotify-quick-panel__hover-artist';
        hoverArtist.setAttribute('data-spotify-part', 'quick-hover-artist');

        var hoverPlaylistName = document.createElement('div');
        hoverPlaylistName.className = 'spotify-quick-panel__hover-playlist';
        hoverPlaylistName.setAttribute('data-spotify-part', 'quick-hover-playlist-name');
        hoverPlaylistName.hidden = true;

        hoverHead.appendChild(hoverArt);
        hoverHead.appendChild(hoverTitleMarquee);
        hoverHead.appendChild(hoverArtist);
        hoverHead.appendChild(hoverPlaylistName);

        var hoverTracks = document.createElement('div');
        hoverTracks.className = 'spotify-quick-panel__hover-tracks';
        hoverTracks.setAttribute('data-spotify-part', 'quick-hover-tracks');
        hoverTracks.setAttribute('role', 'list');
        hoverTracks.setAttribute('aria-label', 'Canciones de la playlist');

        var hoverRow = document.createElement('div');
        hoverRow.className = 'spotify-quick-panel__hover-row';
        var hoverCtrls = document.createElement('div');
        hoverCtrls.className = 'spotify-quick-panel__hover-ctrls';
        hoverCtrls.innerHTML = spotifyQuickControlsHtml();
        var hoverExpand = document.createElement('button');
        hoverExpand.type = 'button';
        hoverExpand.className = 'spotify-quick-panel__toggle spotify-quick-panel__toggle--expand spotify-quick-panel__toggle--hover';
        hoverExpand.setAttribute('data-spotify-quick-toggle', 'expand');
        hoverExpand.setAttribute('aria-label', 'Expandir reproductor');
        hoverExpand.title = 'Expandir';
        hoverExpand.innerHTML = '<i class="fas fa-chevron-right" aria-hidden="true"></i>';
        hoverRow.appendChild(hoverCtrls);
        hoverRow.appendChild(hoverExpand);

        hoverInner.appendChild(hoverHead);
        hoverInner.appendChild(hoverTracks);
        hoverInner.appendChild(hoverRow);
        hoverPop.appendChild(hoverInner);

        var compactBtn = document.createElement('button');
        compactBtn.type = 'button';
        compactBtn.className = 'spotify-quick-panel__toggle spotify-quick-panel__toggle--compact';
        compactBtn.setAttribute('data-spotify-quick-toggle', 'expand');
        compactBtn.setAttribute('aria-label', 'Restaurar tamaño del reproductor');
        compactBtn.title = 'Restaurar tamaño';
        compactBtn.innerHTML = '<i class="fas fa-chevron-right" aria-hidden="true"></i>';

        var expandBtn = document.createElement('button');
        expandBtn.type = 'button';
        expandBtn.className = 'spotify-quick-panel__toggle spotify-quick-panel__toggle--expand';
        expandBtn.setAttribute('data-spotify-quick-toggle', 'compact');
        expandBtn.setAttribute('aria-label', 'Compactar reproductor');
        expandBtn.title = 'Compactar';
        expandBtn.innerHTML = '<i class="fas fa-chevron-left" aria-hidden="true"></i>';

        panel.appendChild(artHost);
        panel.appendChild(body);
        panel.appendChild(hoverPop);
        panel.appendChild(compactBtn);
        panel.appendChild(expandBtn);
        return panel;
    }

    function spotifySyncQuickHostsOpenHint() {
        ['taskbarSpotifyQuick', 'spotifyQuickMobileFab'].forEach(function (id) {
            spotifySyncQuickOpenHint(document.getElementById(id));
        });
    }

    function spotifyEnsureQuickPlayerCard(host, panelClass) {
        if (!host) return null;
        var card = host.querySelector('[data-spotify-quick-card]');
        var existingPop = spotifyGetQuickHoverPop(host);
        var restoreBtn = card && card.querySelector('.spotify-quick-panel__toggle--compact');
        if (card && (
            !card.querySelector('[data-spotify-part="quick-body"]')
            || !existingPop
            || !existingPop.querySelector('[data-spotify-part="quick-hover-art"]')
            || !existingPop.querySelector('[data-spotify-part="quick-hover-tracks"]')
            ||             !existingPop.querySelector('[data-spotify-part="quick-hover-title"]')
            || !existingPop.querySelector('[data-spotify-part="quick-hover-marquee"]')
            || !card.querySelector('[data-spotify-part="quick-marquee"]')
            || (restoreBtn && restoreBtn.getAttribute('data-spotify-quick-toggle') !== 'expand')
        )) {
            spotifyCloseQuickHoverPopNow(host);
            spotifyPurgeOrphanQuickHoverPops(host);
            host.dataset.spotifyQuickReady = '';
            host.innerHTML = '';
            card = null;
        }
        if (card) {
            spotifySyncQuickOpenHint(host);
            spotifyBindQuickOpenHandlers(host);
            return card;
        }
        host.dataset.spotifyQuickReady = '1';
        host.innerHTML = '';
        card = document.createElement('div');
        card.className = 'os-spotify-card';
        card.setAttribute('data-spotify-quick-card', '1');
        card.appendChild(spotifyBuildQuickPlayerPanel(panelClass));
        host.appendChild(card);
        spotifySyncQuickOpenHint(host);
        spotifyBindQuickOpenHandlers(host);
        return card;
    }

    function spotifySyncQuickMarquee(card, title, force) {
        if (!card) return;
        var mq = card.querySelector('[data-spotify-part="quick-marquee"]');
        if (!mq) return;
        if (force) mq.dataset.spotifyMarqueeKey = '';
        spotifyApplyTextMarquee(mq, title, {
            title: 'quick-title',
            clone: 'quick-title-clone',
            force: !!force
        });
    }

    function spotifyPatchQuickPlayerCard(card, info) {
        if (!card || !info) return;
        card.classList.toggle('is-playing', !!info.isPlaying);
        card.classList.toggle('has-track', true);
        var art = card.querySelector('[data-spotify-part="hero-art"]');
        if (art) spotifyUpdateHeroArtElement(art, info.artUrl);
        var quickHost = card.closest('#taskbarSpotifyQuick, #spotifyQuickMobileFab');
        var hoverPop = quickHost ? spotifyGetQuickHoverPop(quickHost) : card.querySelector('[data-spotify-part="quick-hover-pop"]');
        if (hoverPop) {
            spotifySyncQuickHoverPopMetadata(hoverPop);
            spotifySyncPlaylistTrackPlayingIndicators();
        }
        spotifySyncQuickMarquee(card, info.title);
        card.querySelectorAll('.os-spotify-ctrl--play').forEach(function (playBtn) {
            playBtn.dataset.spotifyAction = info.isPlaying ? 'pause' : 'play';
            playBtn.setAttribute('aria-label', info.isPlaying ? 'Pausar' : 'Reproducir');
            playBtn.innerHTML = '<i class="fas fa-' + (info.isPlaying ? 'pause' : 'play') + '"></i>';
        });
        spotifyApplyArtViewToDom();
        spotifySyncQuickArtLayout();
    }

    function spotifyUpdateQuickPlayer() {
        var taskbarBtn = document.getElementById('taskbarSpotifyQuick');
        var mobileBtn = document.getElementById('spotifyQuickMobileFab');
        if (!spotifyQuickAccountConnected()) {
            if (taskbarBtn) taskbarBtn.hidden = true;
            if (mobileBtn) mobileBtn.hidden = true;
            return;
        }
        var info = spotifyGetQuickPlaybackInfo();
        var show = !!info;

        if (taskbarBtn && !isMobileMode()) {
            taskbarBtn.hidden = !show;
            if (show) {
                var tCard = spotifyEnsureQuickPlayerCard(taskbarBtn, 'taskbar-spotify-quick__panel');
                spotifyPatchQuickPlayerCard(tCard, info);
                spotifyApplyQuickCompactUi(taskbarBtn);
                spotifySyncQuickOpenHint(taskbarBtn);
                taskbarBtn.setAttribute('aria-label', 'Spotify: ' + info.title);
            }
        } else if (taskbarBtn) {
            taskbarBtn.hidden = true;
        }

        if (mobileBtn && isMobileMode()) {
            mobileBtn.hidden = !show;
            if (show) {
                var mCard = spotifyEnsureQuickPlayerCard(mobileBtn, 'spotify-quick-mobile-fab__panel');
                spotifyPatchQuickPlayerCard(mCard, info);
                spotifyApplyQuickCompactUi(mobileBtn);
                spotifySyncQuickOpenHint(mobileBtn);
                mobileBtn.setAttribute('aria-label', 'Spotify: ' + info.title);
            }
        } else if (mobileBtn) {
            mobileBtn.hidden = true;
        }
    }

    function openSpotifyFlexia() {
        if (isMobileMode()) {
            openSpotifyMobileModal();
            return;
        }
        if (!isStartMenuVisible()) {
            var startBtn = document.getElementById('startButton');
            if (startBtn) startBtn.click();
        }
        var section = document.getElementById('osWidgetSpotify');
        var toggle = document.getElementById('osWidgetSpotifyToggle');
        if (section && toggle) syncSpotifyWidgetCollapsedUi(section, toggle, false);
        if (section) {
            requestAnimationFrame(function () {
                try { section.scrollIntoView({ behavior: 'smooth', block: 'nearest' }); } catch (_) {}
            });
        }
        refreshSpotifyWidget(true);
    }

    function bindSpotifyQuickHoverPop(host) {
        if (!host || host.dataset.spotifyHoverPopBound === '1') return;
        host.dataset.spotifyHoverPopBound = '1';
        host._spotifyHoverPinned = false;
        host._spotifyHoverShownAt = 0;

        function getPop() {
            return spotifyGetQuickHoverPop(host);
        }

        function isCompactHost() {
            return spotifyIsQuickHostCompact(host);
        }

        function clearHideTimer() {
            spotifyClearCompactHoverHideTimer(host);
        }

        function bindPopPointerZone(pop) {
            if (!pop) return;
            if (pop._spotifyHoverZoneBound) return;
            pop._spotifyHoverZoneBound = true;
            pop.addEventListener('pointerenter', function () {
                if (!isCompactHost()) return;
                clearHideTimer();
                spotifyTouchQuickHoverActivity(host);
            });
            pop.addEventListener('pointerleave', function () {
                if (!isCompactHost()) return;
                if (pop.classList.contains('spotify-quick-panel__hover-pop--browse')) return;
                spotifyScheduleCompactHoverHide(host);
            });
        }

        function onEnterZone() {
            if (!isCompactHost()) return;
            if (host._spotifyQuickHoverSuspendedUntil && Date.now() < host._spotifyQuickHoverSuspendedUntil) return;
            if (host._spotifyBrowsePopOpen) return;
            clearHideTimer();
            host._spotifyHoverShownAt = Date.now();
            var pop = getPop();
            if (!pop || !pop.classList.contains('is-visible')) {
                host._spotifyHoverPinned = false;
                spotifyShowQuickHoverPop(host, 'compact');
                pop = getPop();
            } else {
                spotifyLayoutQuickHoverPop(host, pop);
                spotifyTouchCompactHoverActivity(host);
            }
            bindPopPointerZone(pop);
        }

        function onLeaveZone() {
            if (!isCompactHost()) return;
            if (host._spotifyBrowsePopOpen) return;
            spotifyScheduleCompactHoverHide(host);
        }

        host.addEventListener('pointerenter', onEnterZone);
        host.addEventListener('pointerleave', onLeaveZone);
        bindPopPointerZone(getPop());
    }

    function bindSpotifyQuickPlayer() {
        if (window.__spotifyQuickPlayerBound) return;
        window.__spotifyQuickPlayerBound = true;

        if (!window.__spotifyQuickPointerTrackBound) {
            window.__spotifyQuickPointerTrackBound = true;
            document.addEventListener('pointermove', function (e) {
                window._spotifyQuickPointerX = e.clientX;
                window._spotifyQuickPointerY = e.clientY;
                ['taskbarSpotifyQuick', 'spotifyQuickMobileFab'].forEach(function (id) {
                    var h = document.getElementById(id);
                    if (!h) return;
                    var p = spotifyGetQuickHoverPop(h);
                    if (!p || !p.classList.contains('is-visible')) return;
                    if (spotifyPointerInQuickZone(h, e.clientX, e.clientY)) {
                        spotifyTouchQuickHoverActivity(h);
                        if (spotifyIsQuickHostCompact(h) && !h._spotifyBrowsePopOpen && !h._spotifyHoverPinned) {
                            spotifyClearCompactHoverHideTimer(h);
                        }
                    } else if (spotifyIsQuickHostCompact(h) && !h._spotifyBrowsePopOpen && !h._spotifyHoverPinned) {
                        if (p.classList.contains('spotify-quick-panel__hover-pop--browse')) return;
                        spotifyScheduleCompactHoverHide(h, 350);
                    }
                });
            }, { passive: true });
        }
        if (!window.__spotifyQuickPopRepositionBound) {
            window.__spotifyQuickPopRepositionBound = true;
            window.addEventListener('resize', spotifyRepaintOpenQuickPops, { passive: true });
            window.addEventListener('scroll', spotifyRepaintOpenQuickPops, { passive: true, capture: true });
        }

        bindSpotifyQuickHoverPop(document.getElementById('taskbarSpotifyQuick'));
        bindSpotifyQuickHoverPop(document.getElementById('spotifyQuickMobileFab'));
        ['taskbarSpotifyQuick', 'spotifyQuickMobileFab'].forEach(function (id) {
            var h = document.getElementById(id);
            if (!h) return;
            var p = spotifyGetQuickHoverPop(h);
            if (p) spotifyEnsureQuickPopActivityBinding(h, p);
        });
        spotifyPurgeAllQuickHoverPopOrphans();
        spotifySyncQuickPopBackdrop();
        document.addEventListener('click', function (e) {
            var quickToggle = spotifyClosestQuickToggle(e.target);
            if (quickToggle) {
                e.preventDefault();
                e.stopPropagation();
                var toggleHost = quickToggle.closest('#taskbarSpotifyQuick, #spotifyQuickMobileFab');
                if (toggleHost && toggleHost.id === 'spotifyQuickMobileFab' && isMobileMode()) {
                    return;
                }
                var mode = quickToggle.getAttribute('data-spotify-quick-toggle');
                spotifyCloseAllQuickHoverPopsNow(true);
                spotifyWriteQuickCompact(mode === 'compact');
                var tb = document.getElementById('taskbarSpotifyQuick');
                var mb = document.getElementById('spotifyQuickMobileFab');
                spotifyApplyQuickCompactUi(tb);
                spotifyApplyQuickCompactUi(mb);
                spotifyUpdateQuickPlayer();
                return;
            }
            var quickCtrl = spotifyClosestQuickAction(e.target);
            if (quickCtrl) {
                e.preventDefault();
                e.stopPropagation();
                var hoverPop = quickCtrl.closest('[data-spotify-part="quick-hover-pop"]');
                if (hoverPop && hoverPop.dataset.spotifyQuickPopHost) {
                    var popHost = document.getElementById(hoverPop.dataset.spotifyQuickPopHost);
                    if (popHost) {
                        popHost._spotifyQuickPopSuppressDismissOnce = true;
                        requestAnimationFrame(function () {
                            popHost._spotifyQuickPopSuppressDismissOnce = false;
                        });
                    }
                }
                spotifyHandleQuickActionClick(quickCtrl);
                return;
            }
            if (e.target.closest && e.target.closest('#taskbarSpotifyQuick [data-spotify-part="hero-art-wrap"], #spotifyQuickMobileFab [data-spotify-part="hero-art-wrap"]')) {
                return;
            }

            var quickHost = e.target.closest && e.target.closest('#taskbarSpotifyQuick, #spotifyQuickMobileFab');
            if (!quickHost) return;

            if (e.target.closest('[data-spotify-quick-toggle], .spotify-quick-panel__ctrls')) return;
            if (e.target.closest('#spotifyQuickPopPortal .spotify-quick-panel__hover-pop')) return;

            var onTitleZone = e.target.closest('[data-spotify-quick-open], .spotify-quick-panel__marquee, .spotify-quick-panel__title');
            var onCompactArt = spotifyIsQuickHostCompact(quickHost)
                && e.target.closest('.spotify-quick-panel__art');

            if (onTitleZone || onCompactArt) {
                e.preventDefault();
                e.stopPropagation();
                quickHost._spotifyQuickPopSuppressDismissOnce = true;
                requestAnimationFrame(function () {
                    quickHost._spotifyQuickPopSuppressDismissOnce = false;
                });
                if (spotifyIsQuickHostCompact(quickHost)) {
                    spotifyToggleCompactHoverPop(quickHost);
                } else {
                    spotifyToggleQuickBrowsePop(quickHost);
                }
                return;
            }

            if (spotifyIsQuickHostExtended(quickHost)
                && e.target.closest('.spotify-quick-panel__body, .spotify-quick-panel__art')) {
                e.preventDefault();
                e.stopPropagation();
                quickHost._spotifyQuickPopSuppressDismissOnce = true;
                requestAnimationFrame(function () {
                    quickHost._spotifyQuickPopSuppressDismissOnce = false;
                });
                spotifyToggleQuickBrowsePop(quickHost);
                return;
            }
        });

        if (!window.__spotifyQuickHoverDismissBound) {
            window.__spotifyQuickHoverDismissBound = true;
            document.addEventListener('pointerdown', function (e) {
                if (e.button !== 0) return;
                if (spotifyClosestQuickToggle(e.target)) {
                    ['taskbarSpotifyQuick', 'spotifyQuickMobileFab'].forEach(function (id) {
                        var h = document.getElementById(id);
                        if (h && spotifyIsQuickHoverPopVisible(h)) spotifyCloseQuickHoverPopNow(h, true);
                    });
                    return;
                }
                spotifyDismissQuickHoverPopFromEvent(e.target, true);
            }, true);
            document.addEventListener('mousedown', function (e) {
                if (e.button !== 0) return;
                if (spotifyIsClickInsideQuickHoverPop(e.target)) {
                    if (spotifyClosestQuickToggle(e.target) || spotifyClosestQuickAction(e.target)) return;
                    return;
                }
                if (spotifyClosestQuickToggle(e.target) || spotifyClosestQuickAction(e.target)) return;
                spotifyDismissOpenQuickPops(e.target);
            }, true);
            document.addEventListener('click', function (e) {
                if (spotifyIsClickInsideQuickHoverPop(e.target)) return;
                if (spotifyClosestQuickToggle(e.target) || spotifyClosestQuickAction(e.target)) return;
                spotifyDismissOpenQuickPops(e.target);
            });
            document.addEventListener('keydown', function (e) {
                if (e.key !== 'Escape') return;
                spotifyDismissOpenQuickPops(null, { force: true });
            });
        }
    }

    function spotifyApplyConnectedView(viewState, fetchLiked) {
        spotifyState.lastView = viewState;
        var patchOnly = (spotifyShouldPatchOnlyConnected(viewState)
            || (spotifyIsInteractingWithSearch() && viewState.kind === 'connected')
            || (spotifyIsInteractingWithAddPlaylist() && viewState.kind === 'connected')
            || (spotifyIsInteractingWithPlaylistTracks() && viewState.kind === 'connected'))
            && !spotifyState.feedback
            && !spotifyState.inlineError;
        if (patchOnly) {
            spotifyPatchPlaybackUi(viewState);
            return Promise.resolve();
        }
        spotifyRenderAll(viewState);
        return Promise.resolve();
    }

    function spotifySyncViewFromPlayerState(fetchLiked) {
        var user = spotifyState.lastView && spotifyState.lastView.user;
        var pb = spotifyPlaybackFromSdkState(spotifyState.playerState);
        var trackId = pb && pb.item ? pb.item.id : null;
        var apply = function (liked) {
            return spotifyApplyConnectedView({
                kind: 'connected',
                playback: pb,
                user: user,
                liked: !!liked
            }, fetchLiked);
        };
        if (!fetchLiked || !trackId) {
            return apply(spotifyState.lastView && spotifyState.lastView.liked);
        }
        var uri = spotifyTrackUri(trackId);
        if (!uri) return apply(false);
        return spotifyApi('/me/library/contains' + spotifyLibraryUrisQuery(uri)).then(function (arr) {
            return apply(!!(arr && arr[0]));
        }).catch(function () {
            return apply(false);
        });
    }

    function spotifyIsDeviceNotFound(err) {
        if (!err) return false;
        if (err.status === 404) return true;
        if (err.reason === 'NOT_FOUND') return true;
        var msg = (err.message || '') + (err.spotifyError && err.spotifyError.message ? err.spotifyError.message : '');
        return /device not found/i.test(msg);
    }

    function spotifyNormalizeMarket(code) {
        var c = (code || '').trim().toUpperCase();
        return /^[A-Z]{2}$/.test(c) ? c : '';
    }

    function spotifyGetMarket() {
        var u = spotifyState.lastView && spotifyState.lastView.user;
        var fromUser = spotifyNormalizeMarket(u && u.country);
        if (fromUser) return fromUser;
        try {
            var cached = JSON.parse(localStorage.getItem(LS_SPOTIFY_USER) || 'null');
            if (cached && cached.country) return spotifyNormalizeMarket(cached.country);
        } catch (_) {}
        return '';
    }

    function spotifySearchApiPath(query) {
        var q = String(query || '').trim().slice(0, 100);
        var params = new URLSearchParams();
        params.set('q', q);
        params.set('type', 'track');
        params.set('limit', '10');
        var market = spotifyGetMarket();
        if (market) params.set('market', market);
        return '/search?' + params.toString();
    }

    function spotifyTransferPlayback(deviceId) {
        return spotifyApi('/me/player', {
            method: 'PUT',
            body: { device_ids: [deviceId], play: false }
        }).catch(function (err) {
            if (spotifyIsDeviceNotFound(err)) return null;
            throw err;
        });
    }

    function spotifyPlayOnDevice(playBody) {
        return spotifyEnsurePlayer().then(function (deviceId) {
            function playOn(id) {
                return spotifyTransferPlayback(id).then(function () {
                    return spotifyApi('/me/player/play?device_id=' + encodeURIComponent(id), {
                        method: 'PUT',
                        body: playBody
                    });
                });
            }
            return playOn(deviceId).catch(function (err) {
                if (!spotifyIsDeviceNotFound(err) || !spotifyState.player) throw err;
                spotifyState.deviceId = null;
                spotifyState.sdkReady = false;
                return spotifyState.player.connect().then(function (ok) {
                    if (!ok) throw err;
                    return spotifyWaitForDeviceId(12000).then(playOn);
                });
            });
        });
    }

    function spotifyRunSearch(query) {
        var q = (query || '').trim();
        spotifyState.searchQuery = q;
        spotifyState.searchError = '';
        if (!q) {
            spotifyState.searchResults = [];
            spotifyState.searchLoading = false;
            spotifyRefreshSearchResultsDom();
            return Promise.resolve();
        }
        spotifyState.searchLoading = true;
        spotifyRefreshSearchResultsDom();
        return spotifyApi(spotifySearchApiPath(q))
            .then(function (data) {
                spotifyState.searchResults = (data && data.tracks && Array.isArray(data.tracks.items))
                    ? data.tracks.items : [];
                spotifyState.searchLoading = false;
                spotifyState.searchError = '';
                spotifyRefreshSearchResultsDom();
            }).catch(function (err) {
                spotifyState.searchResults = [];
                spotifyState.searchLoading = false;
                spotifyState.searchError = spotifyFriendlyError(err);
                spotifyRefreshSearchResultsDom();
            });
    }

    function spotifyGetCurrentTrackListState() {
        var uri = spotifyCurrentTrackUri();
        var pb = spotifyPlaybackFromSdkState(spotifyState.playerState);
        return {
            uri: uri,
            isPlaying: !!(uri && pb && pb.is_playing)
        };
    }

    function spotifyNowPlayingIndicatorHtml(visible) {
        return '<span class="os-spotify-media-row__now-playing"' + (visible ? '' : ' hidden')
            + ' aria-hidden="true">'
            + '<span class="os-spotify-now-playing-bars" role="img" aria-label="Reproduciendo">'
            + '<span class="os-spotify-now-playing-bars__bar"></span>'
            + '<span class="os-spotify-now-playing-bars__bar"></span>'
            + '<span class="os-spotify-now-playing-bars__bar"></span>'
            + '</span></span>';
    }

    function spotifyApplyTrackRowPlayingState(row, currentUri, isPlaying) {
        if (!row || !row.dataset.spotifyUri) return;
        var isCurrent = !!(currentUri && row.dataset.spotifyUri === currentUri);
        row.classList.toggle('is-current', isCurrent);
        row.classList.toggle('is-playing-track', isCurrent && isPlaying);
        var indicator = row.querySelector('.os-spotify-media-row__now-playing');
        if (indicator) {
            indicator.hidden = !isCurrent;
        }
    }

    function spotifySyncPlaylistTrackPlayingIndicators() {
        var cur = spotifyGetCurrentTrackListState();
        document.querySelectorAll(
            '.os-spotify-playlists__tracks .os-spotify-media-row[data-spotify-uri], '
            + '.spotify-quick-panel__hover-tracks .os-spotify-media-row[data-spotify-uri]'
        ).forEach(function (row) {
            spotifyApplyTrackRowPlayingState(row, cur.uri, cur.isPlaying);
        });
    }

    function spotifyRenderTrackRow(track, action, extraClass, showPlayingIndicator) {
        if (showPlayingIndicator === undefined) showPlayingIndicator = true;
        var row = document.createElement('button');
        row.type = 'button';
        var cur = spotifyGetCurrentTrackListState();
        var isCurrent = !!(showPlayingIndicator && cur.uri && track.uri === cur.uri);
        row.className = 'os-spotify-media-row'
            + (extraClass ? ' ' + extraClass : '')
            + (isCurrent ? ' is-current' : '')
            + (isCurrent && cur.isPlaying ? ' is-playing-track' : '');
        row.dataset.spotifyAction = action;
        row.dataset.spotifyUri = track.uri || '';
        var img = track.album && track.album.images && track.album.images[2]
            ? track.album.images[2].url
            : (track.album && track.album.images && track.album.images[0] ? track.album.images[0].url : '');
        var artists = track.artists ? track.artists.map(function (a) { return a.name; }).join(', ') : '';
        var dur = track.duration_ms ? spotifyFormatMs(track.duration_ms) : '';
        row.innerHTML = (img
            ? '<span class="os-spotify-media-row__art" style="background-image:url(\'' + img.replace(/'/g, '%27') + '\')"></span>'
            : '<span class="os-spotify-media-row__art os-spotify-media-row__art--empty"><i class="fas fa-music"></i></span>')
            + (showPlayingIndicator ? spotifyNowPlayingIndicatorHtml(isCurrent) : '')
            + '<span class="os-spotify-media-row__body">'
            + '<span class="os-spotify-media-row__title">' + escapeHtml(track.name || '—') + '</span>'
            + '<span class="os-spotify-media-row__sub">' + escapeHtml(artists) + '</span>'
            + '</span>'
            + (dur ? '<span class="os-spotify-media-row__dur">' + dur + '</span>' : '')
            + '<i class="fas fa-play os-spotify-media-row__play" aria-hidden="true"></i>';
        return row;
    }

    function spotifyPopulateAddPlaylistList(listEl) {
        if (!listEl) return;
        listEl.innerHTML = '';
        var lists = spotifyState.playlistsCache && Array.isArray(spotifyState.playlistsCache.items)
            ? spotifyState.playlistsCache.items : [];
        spotifyDedupePlaylists(lists).slice(0, 30).forEach(function (pl) {
            if (!pl || !pl.id) return;
            var btn = document.createElement('button');
            btn.type = 'button';
            btn.className = 'os-spotify-media-row os-spotify-media-row--compact';
            btn.dataset.spotifyAction = 'add-to-playlist';
            btn.dataset.playlistId = pl.id;
            btn.dataset.playlistName = pl.name || '';
            var img = pl.images && pl.images[0] ? pl.images[0].url : '';
            btn.innerHTML = (img
                ? '<span class="os-spotify-media-row__art" style="background-image:url(\'' + img.replace(/'/g, '%27') + '\')"></span>'
                : '<span class="os-spotify-media-row__art os-spotify-media-row__art--empty"><i class="fas fa-music"></i></span>')
                + '<span class="os-spotify-media-row__body">'
                + '<span class="os-spotify-media-row__title">' + escapeHtml(pl.name || '—') + '</span>'
                + '<span class="os-spotify-media-row__sub">' + ((pl.tracks && pl.tracks.total) || 0) + ' canciones</span>'
                + '</span>'
                + '<i class="fas fa-plus os-spotify-media-row__play" aria-hidden="true"></i>';
            listEl.appendChild(btn);
        });
    }

    function spotifyRefreshAddPlaylistDom() {
        document.querySelectorAll('.os-spotify-add-playlist__list').forEach(function (listEl) {
            spotifyRememberListScroll('addPlaylistList', listEl);
            var scrollTop = spotifyGetRememberedListScroll('addPlaylistList', listEl);
            spotifyPopulateAddPlaylistList(listEl);
            if (scrollTop > 0) {
                listEl.scrollTop = scrollTop;
                spotifyRememberListScroll('addPlaylistList', listEl);
            }
        });
    }

    function spotifyRenderAddPlaylistPanel(card, hasTrack) {
        var wrap = document.createElement('div');
        wrap.className = 'os-spotify-add-playlist' + (spotifyState.addPlaylistOpen ? ' is-open' : '');
        wrap.setAttribute('data-spotify-part', 'add-playlist-panel');
        if (!spotifyState.addPlaylistOpen) {
            card.appendChild(wrap);
            return;
        }
        if (!hasTrack) {
            wrap.innerHTML = '<p class="os-spotify-search__hint">Reproduce una canción para añadirla a una playlist.</p>';
            card.appendChild(wrap);
            return;
        }
        var lists = spotifyState.playlistsCache && Array.isArray(spotifyState.playlistsCache.items)
            ? spotifyState.playlistsCache.items : [];
        if (spotifyState.playlistsInFlight && !lists.length) {
            wrap.innerHTML = '<p class="os-spotify-search__hint"><i class="fas fa-spinner fa-spin"></i> Cargando playlists…</p>';
        } else if (!lists.length) {
            wrap.innerHTML = '<p class="os-spotify-search__hint">No tienes playlists editables en esta cuenta.</p>';
        } else {
            var title = document.createElement('p');
            title.className = 'os-spotify-add-playlist__title';
            title.textContent = 'Añadir canción actual a:';
            wrap.appendChild(title);
            var listEl = document.createElement('div');
            listEl.className = 'os-spotify-add-playlist__list';
            spotifyPopulateAddPlaylistList(listEl);
            wrap.appendChild(listEl);
        }
        card.appendChild(wrap);
    }

    function spotifyCurrentTrackUri() {
        var pb = spotifyPlaybackFromSdkState(spotifyState.playerState);
        if (pb && pb.item && pb.item.external_urls && pb.item.external_urls.spotify) {
            var m = String(pb.item.external_urls.spotify).match(/track\/([a-zA-Z0-9]+)/);
            if (m) return 'spotify:track:' + m[1];
        }
        if (pb && pb.item && pb.item.id) return 'spotify:track:' + pb.item.id;
        var st = spotifyState.playerState;
        if (st && st.track_window && st.track_window.current_track && st.track_window.current_track.uri) {
            return st.track_window.current_track.uri;
        }
        return '';
    }

    function spotifyRenderConnectedCard(panel, suffix, viewState) {
        var pb = viewState.playback;
        var hasTrack = pb && pb.item;
        var isPlaying = hasTrack && pb.is_playing;

        var card = document.createElement('div');
        card.className = 'os-spotify-card' + (isPlaying ? ' is-playing' : '') + (hasTrack ? ' has-track' : '');

        if (spotifyState.feedback && spotifyState.feedback.text) {
            var fb = document.createElement('div');
            fb.className = spotifyState.feedback.ok ? 'os-spotify-inline-success' : 'os-spotify-inline-error';
            fb.setAttribute('data-spotify-part', 'feedback');
            fb.setAttribute('role', 'status');
            fb.innerHTML = '<i class="fas fa-' + (spotifyState.feedback.ok ? 'check-circle' : 'exclamation-circle') + '" aria-hidden="true"></i> '
                + escapeHtml(spotifyState.feedback.text);
            card.appendChild(fb);
        } else if (spotifyState.inlineError) {
            var errBanner = document.createElement('div');
            errBanner.className = 'os-spotify-inline-error';
            errBanner.setAttribute('data-spotify-part', 'inline-error');
            errBanner.setAttribute('role', 'alert');
            errBanner.innerHTML = '<i class="fas fa-exclamation-circle" aria-hidden="true"></i> '
                + escapeHtml(spotifyState.inlineError);
            card.appendChild(errBanner);
        }

        // ── Now playing ──
        var hero = document.createElement('div');
        hero.className = 'os-spotify-hero';

        var artUrl = hasTrack && pb.item.album && pb.item.album.images && pb.item.album.images[0]
            ? pb.item.album.images[0].url : '';
        hero.appendChild(spotifyCreateHeroArtWrap(artUrl));

        var heroMeta = document.createElement('div');
        heroMeta.className = 'os-spotify-hero__meta';

        var badgeWrap = document.createElement('div');
        badgeWrap.setAttribute('data-spotify-part', 'hero-badge-wrap');
        if (isPlaying) {
            badgeWrap.innerHTML = '<div class="os-spotify-hero__badge"><span class="os-spotify-hero__eq" aria-hidden="true"><i></i><i></i><i></i></span> Reproduciendo</div>';
        }
        heroMeta.appendChild(badgeWrap);

        var title = document.createElement('div');
        title.setAttribute('data-spotify-part', 'hero-title');
        title.className = 'os-spotify-hero__title';
        title.textContent = hasTrack ? (pb.item.name || '—') : 'Nada en reproducción';
        if (hasTrack && pb.item.external_urls && pb.item.external_urls.spotify) {
            title.classList.add('is-link');
            title.title = 'Abrir en Spotify';
            title.addEventListener('click', function () {
                window.open(pb.item.external_urls.spotify, '_blank', 'noopener,noreferrer');
            });
        }
        heroMeta.appendChild(title);

        var sub = document.createElement('div');
        sub.setAttribute('data-spotify-part', 'hero-sub');
        sub.className = 'os-spotify-hero__sub';
        if (hasTrack) {
            var artistName = pb.item.artists ? pb.item.artists.map(function (a) { return a.name; }).join(', ') : '';
            var albumName = pb.item.album ? (pb.item.album.name || '') : '';
            sub.textContent = artistName + (albumName ? ' · ' + albumName : '');
        } else {
            sub.textContent = 'Busca una canción o elige una playlist';
        }
        heroMeta.appendChild(sub);
        hero.appendChild(heroMeta);
        card.appendChild(hero);

        if (hasTrack && pb.item.duration_ms) {
            var pct = Math.max(0, Math.min(100, (pb.progress_ms / pb.item.duration_ms) * 100));
            var prog = document.createElement('div');
            prog.className = 'os-spotify-card__progress';
            prog.setAttribute('data-spotify-part', 'progress');
            prog.dataset.spotifySeek = '1';
            prog.dataset.durationMs = String(pb.item.duration_ms);
            prog.title = 'Clic para saltar en la canción';
            prog.innerHTML = '<div class="os-spotify-card__progress-bar" style="width:' + pct.toFixed(2) + '%"></div>'
                + '<div class="os-spotify-card__progress-times">'
                + '<span class="os-spotify-progress-cur">' + spotifyFormatMs(pb.progress_ms) + '</span>'
                + '<span>' + spotifyFormatMs(pb.item.duration_ms) + '</span>'
                + '</div>';
            card.appendChild(prog);
        }

        var ctrls = document.createElement('div');
        ctrls.className = 'os-spotify-card__controls';
        ctrls.innerHTML =
            '<button type="button" class="os-spotify-ctrl" data-spotify-action="prev" aria-label="Anterior" title="Anterior"><i class="fas fa-step-backward"></i></button>'
            + '<button type="button" class="os-spotify-ctrl os-spotify-ctrl--play" data-spotify-action="' + (isPlaying ? 'pause' : 'play') + '" aria-label="' + (isPlaying ? 'Pausar' : 'Reproducir') + '"><i class="fas fa-' + (isPlaying ? 'pause' : 'play') + '"></i></button>'
            + '<button type="button" class="os-spotify-ctrl" data-spotify-action="next" aria-label="Siguiente" title="Siguiente"><i class="fas fa-step-forward"></i></button>'
            + '<button type="button" class="os-spotify-ctrl os-spotify-ctrl--like" data-spotify-action="like" aria-label="Guardar en tu biblioteca" title="Me gusta (Tu biblioteca)"' + (viewState.liked ? ' data-liked="1"' : '') + '><i class="' + (viewState.liked ? 'fas' : 'far') + ' fa-heart"></i></button>'
            + '<button type="button" class="os-spotify-ctrl os-spotify-ctrl--add' + (spotifyState.addPlaylistOpen ? ' is-active' : '') + '" data-spotify-action="toggle-add-playlist" aria-label="Añadir a playlist" title="Añadir a playlist"' + (hasTrack ? '' : ' disabled') + '><i class="fas fa-plus"></i></button>';
        card.appendChild(ctrls);

        spotifyRenderAddPlaylistPanel(card, hasTrack);

        var volPct = Math.round(spotifyState.volume * 100);
        var volWrap = document.createElement('div');
        volWrap.className = 'os-spotify-card__volume';
        volWrap.innerHTML = '<i class="fas fa-volume-down" aria-hidden="true"></i>'
            + '<input type="range" min="0" max="100" value="' + volPct + '" class="os-spotify-vol" aria-label="Volumen">'
            + '<i class="fas fa-volume-up" aria-hidden="true"></i>';
        card.appendChild(volWrap);

        // ── Pestañas Buscar / Playlists ──
        var tabs = document.createElement('div');
        tabs.className = 'os-spotify-tabs';
        tabs.setAttribute('role', 'tablist');
        ['search', 'playlists'].forEach(function (tabId) {
            var tabBtn = document.createElement('button');
            tabBtn.type = 'button';
            tabBtn.className = 'os-spotify-tabs__btn' + (spotifyState.uiTab === tabId ? ' is-active' : '');
            tabBtn.setAttribute('role', 'tab');
            tabBtn.setAttribute('aria-selected', spotifyState.uiTab === tabId ? 'true' : 'false');
            tabBtn.dataset.spotifyTab = tabId;
            tabBtn.innerHTML = tabId === 'search'
                ? '<i class="fas fa-search"></i> Buscar'
                : '<i class="fas fa-list"></i> Playlists';
            tabs.appendChild(tabBtn);
        });
        card.appendChild(tabs);

        var tabPanel = document.createElement('div');
        tabPanel.className = 'os-spotify-tab-panel';

        if (spotifyState.uiTab === 'search') {
            var searchWrap = document.createElement('div');
            searchWrap.className = 'os-spotify-search';

            var searchField = document.createElement('div');
            searchField.className = 'os-spotify-search__field';
            searchField.innerHTML = '<i class="fas fa-search" aria-hidden="true"></i>';
            var searchInput = document.createElement('input');
            searchInput.type = 'search';
            searchInput.className = 'os-spotify-search__input';
            searchInput.placeholder = 'Canción, artista o álbum…';
            searchInput.autocomplete = 'off';
            searchInput.spellcheck = false;
            searchInput.value = spotifyState.searchQuery || '';
            searchInput.setAttribute('aria-label', 'Buscar en Spotify');
            searchField.appendChild(searchInput);
            searchWrap.appendChild(searchField);

            var results = document.createElement('div');
            results.className = 'os-spotify-search__results';
            searchWrap.appendChild(results);
            spotifyPopulateSearchResults(results);
            tabPanel.appendChild(searchWrap);
        } else {
            spotifyRenderPlaylistsTab(tabPanel);
        }
        card.appendChild(tabPanel);

        var foot = document.createElement('div');
        foot.className = 'os-spotify-card__foot';
        var userName = (viewState.user && viewState.user.display_name) || '';
        foot.innerHTML = '<span class="os-spotify-card__user">' + (userName ? '<i class="fas fa-user-circle"></i> ' + escapeHtml(userName) : '') + '</span>'
            + '<button type="button" class="os-spotify-disconnect" data-spotify-action="disconnect"><i class="fas fa-sign-out-alt"></i></button>';
        foot.querySelector('.os-spotify-disconnect').title = 'Desconectar';
        card.appendChild(foot);

        panel.appendChild(card);
    }

    function spotifyIsPremiumRequiredMessage(msg) {
        if (!msg) return false;
        var s = String(msg).toLowerCase();
        return s.indexOf('premium') >= 0
            || s.indexOf('suscripción') >= 0
            || s.indexOf('subscription') >= 0
            || s.indexOf('not available for users') >= 0;
    }

    function spotifyAppendPremiumActions(box, options) {
        options = options || {};
        var actions = document.createElement('div');
        actions.className = 'os-spotify-empty__actions';

        var premiumBtn = document.createElement('a');
        premiumBtn.className = 'os-spotify-connect-btn os-spotify-connect-btn--premium';
        premiumBtn.href = spotifyPremiumUrl();
        premiumBtn.target = '_blank';
        premiumBtn.rel = 'noopener noreferrer';
        premiumBtn.innerHTML = '<i class="fas fa-crown" aria-hidden="true"></i> Ver planes Premium';

        var logoutBtn = document.createElement('button');
        logoutBtn.type = 'button';
        logoutBtn.className = 'os-spotify-connect-btn os-spotify-connect-btn--ghost';
        logoutBtn.innerHTML = '<i class="fas fa-sign-out-alt" aria-hidden="true"></i> Cerrar sesión';
        logoutBtn.addEventListener('click', function () {
            handleSpotifyAction('disconnect');
        });

        actions.appendChild(premiumBtn);
        actions.appendChild(logoutBtn);

        if (options.showRetry) {
            var retryBtn = document.createElement('button');
            retryBtn.type = 'button';
            retryBtn.className = 'os-spotify-connect-btn os-spotify-connect-btn--ghost';
            retryBtn.textContent = 'Reintentar';
            retryBtn.addEventListener('click', function () { refreshSpotifyWidget(true); });
            actions.appendChild(retryBtn);
        }

        box.appendChild(actions);
    }

    function spotifyRenderPremiumRequiredPanel(panel, viewState) {
        var box = document.createElement('div');
        box.className = 'os-spotify-empty os-spotify-empty--premium';
        box.innerHTML = '<i class="fab fa-spotify os-spotify-empty__icon" aria-hidden="true"></i>'
            + '<p class="os-spotify-empty__title">' + escapeHtml((viewState && viewState.title) || 'Se requiere Spotify Premium') + '</p>'
            + '<p class="os-spotify-empty__msg">' + escapeHtml((viewState && viewState.message) || 'La reproducción en Flexia requiere una cuenta Spotify Premium activa.') + '</p>';
        spotifyAppendPremiumActions(box, { showRetry: !!(viewState && viewState.showRetry) });
        panel.appendChild(box);
    }

    /* ── Render dual desktop / mobile ── */
    function spotifyPanelSuffixes() { return ['', 'Mobile']; }

    function spotifyRenderInto(suffix, viewState) {
        var panel = document.getElementById('osSpotifyPanel' + suffix);
        if (!panel) return;
        panel.innerHTML = '';

        if (viewState.kind === 'unconfigured') {
            var box = document.createElement('div');
            box.className = 'os-spotify-empty';
            box.innerHTML = '<i class="fab fa-spotify os-spotify-empty__icon" aria-hidden="true"></i>'
                + '<p class="os-spotify-empty__title">Spotify no está configurado</p>'
                + '<p class="os-spotify-empty__msg">Añade el Client ID en <strong>Configuración → Integraciones</strong> y completa la conexión con Spotify.</p>';
            var cfgBtn = document.createElement('button');
            cfgBtn.type = 'button';
            cfgBtn.className = 'os-spotify-connect-btn';
            cfgBtn.innerHTML = '<i class="fas fa-cog" aria-hidden="true"></i> Ir a Integraciones';
            cfgBtn.addEventListener('click', function () { handleSpotifyConnectClick(); });
            box.appendChild(cfgBtn);
            panel.appendChild(box);
            return;
        }

        if (viewState.kind === 'setup_required') {
            var setup = document.createElement('div');
            setup.className = 'os-spotify-empty';
            setup.innerHTML = '<i class="fab fa-spotify os-spotify-empty__icon" aria-hidden="true"></i>'
                + '<p class="os-spotify-empty__title">Completa la configuración de Spotify</p>'
                + '<p class="os-spotify-empty__msg">Ya hay un Client ID guardado. Ve a <strong>Integraciones</strong>, revisa la Redirect URI y pulsa <strong>Probar conexión</strong> para verificar la app.</p>';
            var setupBtn = document.createElement('button');
            setupBtn.type = 'button';
            setupBtn.className = 'os-spotify-connect-btn';
            setupBtn.innerHTML = '<i class="fas fa-cog" aria-hidden="true"></i> Ir a Integraciones';
            setupBtn.addEventListener('click', function () { handleSpotifyConnectClick(); });
            setup.appendChild(setupBtn);
            panel.appendChild(setup);
            return;
        }

        if (viewState.kind === 'disconnected') {
            var disc = document.createElement('div');
            disc.className = 'os-spotify-empty';
            disc.innerHTML = '<i class="fab fa-spotify os-spotify-empty__icon" aria-hidden="true"></i>'
                + '<p class="os-spotify-empty__title">Conecta tu cuenta de Spotify</p>'
                + '<p class="os-spotify-empty__msg">Reproduce tus playlists directamente en Flexia. Requiere cuenta <strong>Spotify Premium</strong>.</p>';
            var btn = document.createElement('button');
            btn.type = 'button';
            btn.className = 'os-spotify-connect-btn';
            btn.innerHTML = '<i class="fab fa-spotify" aria-hidden="true"></i> Conectar Spotify';
            btn.addEventListener('click', function () { handleSpotifyConnectClick(); });
            disc.appendChild(btn);
            panel.appendChild(disc);
            return;
        }

        if (viewState.kind === 'connecting') {
            var loadBox = document.createElement('div');
            loadBox.className = 'os-spotify-empty';
            loadBox.innerHTML = '<i class="fab fa-spotify os-spotify-empty__icon" aria-hidden="true"></i>'
                + '<p class="os-spotify-empty__title">Iniciando reproductor…</p>'
                + '<p class="os-spotify-empty__msg">Preparando la reproducción en Flexia.</p>';
            panel.appendChild(loadBox);
            return;
        }

        if (viewState.kind === 'premium_required') {
            spotifyRenderPremiumRequiredPanel(panel, viewState);
            return;
        }

        if (viewState.kind === 'needs_reconnect') {
            var recon = document.createElement('div');
            recon.className = 'os-spotify-empty';
            recon.innerHTML = '<i class="fab fa-spotify os-spotify-empty__icon" aria-hidden="true"></i>'
                + '<p class="os-spotify-empty__title">Vuelve a conectar Spotify</p>'
                + '<p class="os-spotify-empty__msg">' + escapeHtml(viewState.message || 'Actualiza los permisos de la app.') + '</p>';
            var reconBtn = document.createElement('button');
            reconBtn.type = 'button';
            reconBtn.className = 'os-spotify-connect-btn';
            reconBtn.innerHTML = '<i class="fab fa-spotify" aria-hidden="true"></i> Reconectar';
            reconBtn.addEventListener('click', function () {
                spotifyClearToken();
                spotifyDestroyPlayer();
                handleSpotifyConnectClick();
            });
            recon.appendChild(reconBtn);
            panel.appendChild(recon);
            return;
        }

        if (viewState.kind === 'error') {
            if (spotifyIsPremiumRequiredMessage(viewState.message)) {
                spotifyRenderPremiumRequiredPanel(panel, {
                    title: 'Se requiere Spotify Premium',
                    message: viewState.message,
                    showRetry: true
                });
                return;
            }
            var ebox = document.createElement('div');
            ebox.className = 'os-spotify-empty';
            ebox.innerHTML = '<i class="fas fa-exclamation-triangle os-spotify-empty__icon" aria-hidden="true"></i>'
                + '<p class="os-spotify-empty__title">No se pudo reproducir</p>'
                + '<p class="os-spotify-empty__msg">' + escapeHtml(viewState.message || 'Error desconocido.') + '</p>';
            var rb = document.createElement('button');
            rb.type = 'button';
            rb.className = 'os-spotify-connect-btn';
            rb.textContent = 'Reintentar';
            rb.addEventListener('click', function () { refreshSpotifyWidget(true); });
            ebox.appendChild(rb);
            panel.appendChild(ebox);
            return;
        }

        spotifyRenderConnectedCard(panel, suffix, viewState);
        if (spotifyState.uiTab === 'playlists' && !spotifyState.playlistsCache && !spotifyState.playlistsInFlight) {
            refreshSpotifyPlaylists(false);
        }
    }

    function spotifyFormatMs(ms) {
        var s = Math.max(0, Math.floor(Number(ms) / 1000));
        var m = Math.floor(s / 60);
        var ss = s % 60;
        return m + ':' + (ss < 10 ? '0' + ss : ss);
    }

    function spotifyRenderAll(viewState) {
        spotifyCaptureScrollPositions();
        spotifyState.lastView = viewState;
        spotifyPanelSuffixes().forEach(function (suf) { spotifyRenderInto(suf, viewState); });
        spotifyApplyArtViewToDom();
        spotifyScheduleRestoreScrollPositions();
        spotifyUpdateQuickPlayer();
    }

    /* ── Inicializa Web Playback SDK y sincroniza UI ── */
    function refreshSpotifyWidget(force) {
        if (!spotifyClientId()) {
            spotifyDestroyPlayer();
            spotifyRenderAll({ kind: 'unconfigured' });
            return Promise.resolve();
        }
        if (!spotifyConnectionVerified()) {
            spotifyDestroyPlayer();
            spotifyRenderAll({ kind: 'setup_required' });
            return Promise.resolve();
        }
        var token = spotifyReadToken();
        if (!token) {
            spotifyDestroyPlayer();
            spotifyRenderAll({ kind: 'disconnected' });
            return Promise.resolve();
        }
        if (!spotifyHasStreamingScope(token)) {
            spotifyDestroyPlayer();
            spotifyRenderAll({
                kind: 'needs_reconnect',
                message: 'La conexión actual no permite reproducir en Flexia. Pulsa Reconectar y acepta los nuevos permisos.'
            });
            return Promise.resolve();
        }
        if (!spotifyState.lastView || spotifyState.lastView.kind !== 'connected') {
            spotifyApplyPersistedUiState();
        }
        if (spotifyState.initInFlight && !force) return Promise.resolve();
        spotifyState.initInFlight = true;

        if (!spotifyState.lastView || spotifyState.lastView.kind !== 'connected') {
            spotifyRenderAll({ kind: 'connecting' });
        }

        var userPromise = (function () {
            var cached = null;
            try { cached = JSON.parse(localStorage.getItem(LS_SPOTIFY_USER) || 'null'); } catch (_) {}
            if (cached && cached.id) return Promise.resolve(cached);
            return spotifyApi('/me').then(function (u) {
                if (u && u.id) { try { localStorage.setItem(LS_SPOTIFY_USER, JSON.stringify(u)); } catch (_) {} }
                return u;
            }).catch(function () { return null; });
        })();

        return userPromise.then(function (user) {
            spotifyState.lastView = Object.assign({}, spotifyState.lastView || {}, { user: user });
            return spotifyEnsurePlayer().then(function () {
                var preload = Promise.resolve();
                if (spotifyState.selectedPlaylist && spotifyState.selectedPlaylist.id) {
                    preload = spotifyLoadPlaylistTracks(false);
                }
                return preload.then(function () {
                    if (!spotifyState.player) return null;
                    return spotifyState.player.getCurrentState();
                });
            }).then(function (state) {
                spotifyState.playerState = state;
                spotifyStartProgressTimer();
                refreshSpotifyPlaylists(false);
                var resume = Promise.resolve();
                if (!state) resume = spotifyRestorePlaybackFromPersist();
                return resume.then(function () {
                    return spotifySyncViewFromPlayerState(true);
                }).then(function () {
                    spotifyScheduleRestoreScrollPositions();
                    if (spotifyState.uiTab === 'search' && spotifyState.searchQuery && spotifyState.searchQuery.trim()) {
                        return spotifyRunSearch(spotifyState.searchQuery);
                    }
                });
            });
        }).catch(function (e) {
            spotifyStopProgressTimer();
            if (e && e.message === 'missing-streaming-scope') {
                spotifyRenderAll({
                    kind: 'needs_reconnect',
                    message: spotifyFriendlyError(e)
                });
            } else if (e && (e.message === 'not-connected' || (e.status && e.status === 401))) {
                spotifyClearToken();
                spotifyDestroyPlayer();
                spotifyRenderAll({ kind: 'disconnected' });
            } else if (e && (e.message === 'connect-failed' || e.message === 'device-timeout')) {
                spotifyRenderAll({
                    kind: 'premium_required',
                    title: 'No se pudo iniciar el reproductor',
                    message: spotifyFriendlyError(e),
                    showRetry: true
                });
            } else {
                spotifyRenderAll({ kind: 'error', message: spotifyFriendlyError(e) });
            }
        }).then(function () {
            spotifyState.initInFlight = false;
            startSpotifyPolling();
            spotifyUpdateQuickPlayer();
        }, function () {
            spotifyState.initInFlight = false;
            spotifyUpdateQuickPlayer();
        });
    }

    function refreshSpotifyPlaylists(force) {
        if (spotifyState.playlistsInFlight) return Promise.resolve();
        var fresh = spotifyState.playlistsCache && (Date.now() - spotifyState.playlistsCacheAt < 5 * 60 * 1000);
        if (fresh && !force) return Promise.resolve();
        spotifyState.playlistsInFlight = true;
        return spotifyApi('/me/playlists?limit=20').then(function (data) {
            var raw = data || { items: [] };
            raw.items = spotifyDedupePlaylists(raw.items || []);
            spotifyState.playlistsCache = raw;
            spotifyState.playlistsCacheAt = Date.now();
            if (spotifyState.lastView && spotifyState.addPlaylistOpen) {
                spotifyRefreshAddPlaylistDom();
            } else if (spotifyState.lastView && spotifyState.uiTab === 'playlists' && !spotifyIsInteractingWithSearch()) {
                spotifyRenderAll(spotifyState.lastView);
            }
        }).catch(function () { /* ignore */ }).then(function () {
            spotifyState.playlistsInFlight = false;
        }, function () { spotifyState.playlistsInFlight = false; });
    }

    /* ── Auth flow (PKCE) ── */
    function startSpotifyAuth() {
        var clientId = spotifyClientId();
        if (!clientId) {
            openSpotifyIntegrationsConfig();
            return;
        }
        var verifier = spotifyRandomVerifier();
        spotifyChallengeFromVerifier(verifier).then(function (challenge) {
            var state = spotifyB64UrlFromBytes((function () {
                var a = new Uint8Array(16);
                window.crypto.getRandomValues(a);
                return a;
            })());
            try {
                sessionStorage.setItem(SS_SPOTIFY_VERIFIER, verifier);
                sessionStorage.setItem(SS_SPOTIFY_STATE, state);
                sessionStorage.setItem(SS_SPOTIFY_CLIENT_ID, clientId);
            } catch (_) {}

            var params = new URLSearchParams();
            params.set('response_type', 'code');
            params.set('client_id', clientId);
            params.set('scope', SPOTIFY_SCOPES);
            params.set('redirect_uri', spotifyRedirectUri());
            params.set('code_challenge_method', 'S256');
            params.set('code_challenge', challenge);
            params.set('state', state);

            var url = SPOTIFY_AUTH_URL + '?' + params.toString();
            var w = 480, h = 720;
            var dx = window.screenX || 0, dy = window.screenY || 0;
            var dw = window.outerWidth || 1024, dh = window.outerHeight || 768;
            var left = dx + Math.max(0, (dw - w) / 2);
            var top = dy + Math.max(0, (dh - h) / 2);
            try {
                spotifyState.authPopup = window.open(url, 'flexia-spotify-auth',
                    'width=' + w + ',height=' + h + ',left=' + Math.round(left) + ',top=' + Math.round(top) + ',resizable=yes,scrollbars=yes,status=yes');
            } catch (_) {
                window.location.href = url;
                return;
            }
            if (!spotifyState.authPopup) {
                window.location.href = url;
                return;
            }
            bindSpotifyAuthMessageListener();
        });
    }

    function bindSpotifyAuthMessageListener() {
        if (spotifyState.authMessageBound) return;
        spotifyState.authMessageBound = true;
        window.addEventListener('message', function (e) {
            try {
                if (!e || e.origin !== window.location.origin) return;
                if (!e.data || e.data.source !== 'flexia-spotify') return;
                if (e.data.payload && e.data.payload.ok) {
                    spotifyOnAuthSuccess();
                } else if (e.data.payload) {
                    spotifyRenderAll({ kind: 'error', message: 'Spotify rechazó la conexión: ' + (e.data.payload.error || 'desconocido') });
                }
            } catch (_) {}
        });

        // Storage event como fallback (cuando el popup escribe en localStorage en otra ventana del mismo origen).
        window.addEventListener('storage', function (e) {
            if (e && e.key === LS_SPOTIFY_TOKEN && e.newValue) {
                spotifyOnAuthSuccess();
            }
        });
    }

    /* ── Acciones (Web Playback SDK + API para playlists y favoritos) ── */
    function handleSpotifyAction(action, ctx) {
        if (action === 'disconnect') {
            spotifyDestroyPlayer();
            spotifyClearToken();
            spotifyClearUiState();
            spotifyState.playlistsCache = null;
            spotifyState.playlistsCacheAt = 0;
            spotifyState.searchQuery = '';
            spotifyState.searchResults = [];
            spotifyState.uiTab = 'search';
            spotifyState.addPlaylistOpen = false;
            spotifyState.addPlaylistBrowsing = false;
            spotifyState.selectedPlaylist = null;
            spotifyState.playlistTracks = [];
            spotifyState.playlistTracksBrowsing = false;
            spotifyState.playbackContext = null;
            spotifyState._prevSdkSnapshot = null;
            spotifyClearQuickPlayerPauseHold();
            spotifyState.feedback = null;
            spotifyRenderAll({ kind: 'disconnected' });
            return Promise.resolve();
        }

        var run = function () {
        switch (action) {
            case 'play':
                return spotifyEnsurePlayer().then(function () {
                    var player = spotifyState.player;
                    if (!player) return;
                    return player.getCurrentState().then(function (state) {
                        if (state && state.paused) return player.resume();
                        if (!state) return Promise.resolve();
                        return player.resume();
                    });
                }).then(function () { return spotifySyncViewFromPlayerState(false); });
            case 'pause':
                spotifyState._pendingQuickPause = true;
                return spotifyEnsurePlayer().then(function () {
                    if (spotifyState.player) return spotifyState.player.pause();
                }).then(function () { return spotifySyncViewFromPlayerState(false); })
                    .catch(function (err) {
                        spotifyState._pendingQuickPause = false;
                        throw err;
                    });
            case 'next':
                return spotifyEnsurePlayer().then(function () {
                    if (spotifyState.player) return spotifyState.player.nextTrack();
                }).then(function () {
                    return new Promise(function (resolve) {
                        setTimeout(function () { spotifySyncViewFromPlayerState(true).then(resolve); }, 400);
                    });
                });
            case 'prev':
                return spotifyEnsurePlayer().then(function () {
                    if (spotifyState.player) return spotifyState.player.previousTrack();
                }).then(function () {
                    return new Promise(function (resolve) {
                        setTimeout(function () { spotifySyncViewFromPlayerState(true).then(resolve); }, 400);
                    });
                });
            case 'volume':
                if (ctx && typeof ctx.value === 'number') {
                    spotifyState.volume = Math.max(0, Math.min(1, ctx.value / 100));
                    spotifySchedulePersistUiState();
                    return spotifyEnsurePlayer().then(function () {
                        if (spotifyState.player) return spotifyState.player.setVolume(spotifyState.volume);
                    }).then(function () { return spotifySyncViewFromPlayerState(false); });
                }
                return Promise.resolve();
            case 'like': {
                var pbLike = spotifyPlaybackFromSdkState(spotifyState.playerState);
                if (!pbLike || !pbLike.item || !pbLike.item.id) return Promise.resolve();
                return spotifyEnsureScopes(['user-library-modify']).then(function () {
                    var liked = !!(spotifyState.lastView && spotifyState.lastView.liked);
                    spotifyState.inlineError = '';
                    var trackUri = spotifyTrackUri(pbLike.item.id);
                    if (!trackUri) return Promise.resolve();
                    var libPath = '/me/library' + spotifyLibraryUrisQuery(trackUri);
                    if (liked) {
                        return spotifyApi(libPath, { method: 'DELETE' });
                    }
                    return spotifyApi(libPath, { method: 'PUT' });
                }).then(function () {
                    spotifySetFeedback('Actualizado en tu biblioteca de Spotify.', true);
                    return spotifySyncViewFromPlayerState(true);
                });
            }
            case 'toggle-add-playlist':
                if (!spotifyPlaybackFromSdkState(spotifyState.playerState)) {
                    spotifyState.inlineError = 'Reproduce una canción antes de añadirla a una playlist.';
                    return spotifyApplyConnectedView(spotifyState.lastView, false);
                }
                spotifyState.addPlaylistOpen = !spotifyState.addPlaylistOpen;
                if (!spotifyState.addPlaylistOpen) spotifyState.addPlaylistBrowsing = false;
                spotifyState.inlineError = '';
                if (spotifyState.addPlaylistOpen && !spotifyState.playlistsCache) {
                    return refreshSpotifyPlaylists(true).then(function () {
                        spotifyRenderAll(spotifyState.lastView);
                        spotifySchedulePersistUiState();
                    });
                }
                spotifyRenderAll(spotifyState.lastView);
                spotifySchedulePersistUiState();
                return Promise.resolve();
            case 'add-to-playlist':
                if (!ctx || !ctx.playlistId) return Promise.resolve();
                var trackUri = spotifyCurrentTrackUri();
                if (!trackUri) return Promise.resolve();
                return spotifyEnsureScopes(['playlist-modify-public', 'playlist-modify-private'], { anyOf: true }).then(function () {
                    spotifyState.inlineError = '';
                    return spotifyApi('/playlists/' + encodeURIComponent(ctx.playlistId) + '/items', {
                        method: 'POST',
                        body: { uris: [trackUri] }
                    });
                }).then(function () {
                    var plName = ctx.playlistName || 'la playlist';
                    spotifySetFeedback('Añadida a «' + plName + '».', true);
                    return spotifyApplyConnectedView(spotifyState.lastView, false);
                });
            case 'open-playlist':
                if (!ctx || !ctx.playlistId) return Promise.resolve();
                spotifyState.inlineError = '';
                spotifyState.selectedPlaylist = {
                    id: ctx.playlistId,
                    name: ctx.playlistName || 'Playlist',
                    uri: ctx.playlistUri || '',
                    trackCount: ctx.playlistTrackCount ? Number(ctx.playlistTrackCount) : 0
                };
                spotifyState.playlistTracks = [];
                spotifyState.playlistTracksTotal = 0;
                spotifyState.playlistTracksError = '';
                return spotifyLoadPlaylistTracks(false).then(function () {
                    if (spotifyState.lastView) spotifyRenderAll(spotifyState.lastView);
                    spotifySchedulePersistUiState();
                });
            case 'playlist-back':
                spotifyState.selectedPlaylist = null;
                spotifyState.playlistTracks = [];
                spotifyState.playlistTracksTotal = 0;
                spotifyState.playlistTracksError = '';
                spotifyState.playlistTracksBrowsing = false;
                spotifyState.inlineError = '';
                if (spotifyState.lastView) spotifyRenderAll(spotifyState.lastView);
                spotifySchedulePersistUiState();
                return Promise.resolve();
            case 'load-more-playlist-tracks':
                return spotifyLoadPlaylistTracks(true);
            case 'play-playlist':
                if (!ctx || !ctx.uri) return Promise.resolve();
                spotifyState.inlineError = '';
                spotifyState.playbackContext = {
                    mode: 'context',
                    contextUri: ctx.uri,
                    offsetUri: null,
                    offsetPosition: 0,
                    source: 'playlist'
                };
                return spotifyPlayOnDevice({ context_uri: ctx.uri }).then(function () {
                    return spotifyWaitForPlaybackStart();
                });
            case 'play-track':
                if (!ctx || !ctx.uri) return Promise.resolve();
                spotifyState.inlineError = '';
                var trackUri = ctx.uri;
                var playBody;
                if (spotifyState.selectedPlaylist && spotifyState.selectedPlaylist.uri) {
                    spotifyState.playbackContext = {
                        mode: 'context',
                        contextUri: spotifyState.selectedPlaylist.uri,
                        offsetUri: trackUri,
                        offsetPosition: 0,
                        source: 'playlist-tracks'
                    };
                    playBody = {
                        context_uri: spotifyState.selectedPlaylist.uri,
                        offset: { uri: trackUri }
                    };
                } else if (spotifyState.playlistTracks && spotifyState.playlistTracks.length) {
                    spotifyState.playbackContext = spotifyBuildQueueFromTracks(
                        spotifyState.playlistTracks, trackUri, 'playlist-tracks'
                    );
                    var plIdx = spotifyState.playbackContext.queueIndex;
                    playBody = { uris: spotifyState.playbackContext.queueUris.slice(plIdx) };
                } else if (spotifyState.searchResults && spotifyState.searchResults.length) {
                    spotifyState.playbackContext = spotifyBuildQueueFromTracks(
                        spotifyState.searchResults, trackUri, 'search'
                    );
                    var qIdx = spotifyState.playbackContext.queueIndex;
                    playBody = { uris: spotifyState.playbackContext.queueUris.slice(qIdx) };
                } else {
                    spotifyState.playbackContext = spotifyBuildQueueFromTracks([{ uri: trackUri }], trackUri, 'track');
                    playBody = { uris: [trackUri] };
                }
                return spotifyPlayOnDevice(playBody).then(function () {
                    return spotifyWaitForPlaybackStart();
                });
            case 'seek':
                if (!ctx || typeof ctx.position_ms !== 'number' || !spotifyState.player) return Promise.resolve();
                return spotifyState.player.seek(ctx.position_ms).then(function () {
                    return spotifySyncViewFromPlayerState(false);
                });
        }
        return Promise.resolve();
        };
        return spotifyEnsurePlayer().then(function () {
            return spotifyActivateAudio();
        }).then(run).then(function (result) {
            spotifySchedulePersistUiState();
            return result;
        });
    }

    function bindSpotifyDelegation() {
        if (window.__spotifyDelegationBound) return;
        window.__spotifyDelegationBound = true;
        document.addEventListener('click', function (e) {
            var artWrap = e.target.closest && e.target.closest('[data-spotify-part="hero-art-wrap"]');
            if (artWrap && artWrap.closest('.os-spotify-card')) {
                e.preventDefault();
                e.stopPropagation();
                spotifyCycleArtView();
                return;
            }

            var tabBtn = e.target.closest && e.target.closest('[data-spotify-tab]');
            if (tabBtn) {
                var panelTab = tabBtn.closest('.os-spotify-panel');
                if (!panelTab) return;
                var tabId = tabBtn.getAttribute('data-spotify-tab');
                if (!tabId || tabId === spotifyState.uiTab) return;
                spotifyState.uiTab = tabId;
                spotifyState.searchFocused = false;
                if (tabId === 'search') {
                    spotifyState.selectedPlaylist = null;
                    spotifyState.playlistTracks = [];
                    spotifyState.playlistTracksBrowsing = false;
                }
                if (tabId === 'playlists') refreshSpotifyPlaylists(false);
                if (spotifyState.lastView) spotifyRenderAll(spotifyState.lastView);
                spotifySchedulePersistUiState();
                return;
            }

            var seekEl = e.target.closest && e.target.closest('[data-spotify-seek]');
            if (seekEl) {
                var panelSeek = seekEl.closest('.os-spotify-panel');
                if (!panelSeek || !spotifyState.playerState) return;
                var dur = Number(seekEl.dataset.durationMs);
                if (!dur) return;
                var rect = seekEl.getBoundingClientRect();
                var ratio = Math.max(0, Math.min(1, (e.clientX - rect.left) / rect.width));
                handleSpotifyAction('seek', { position_ms: Math.floor(dur * ratio) }).catch(function (err) {
                    spotifyRenderAll({ kind: 'error', message: spotifyFriendlyError(err) });
                });
                return;
            }

            var t = e.target.closest && e.target.closest('[data-spotify-action]');
            if (!t) return;
            var panel = t.closest('.os-spotify-panel');
            if (!panel) return;
            var action = t.getAttribute('data-spotify-action');
            if (!action || action === 'volume') return;
            var ctx = {};
            if (action === 'open-playlist') {
                ctx.playlistId = t.getAttribute('data-playlist-id') || '';
                ctx.playlistName = t.getAttribute('data-playlist-name') || '';
                ctx.playlistUri = t.getAttribute('data-spotify-uri') || '';
                ctx.playlistTrackCount = t.getAttribute('data-playlist-track-count') || '';
            }
            if (action === 'play-playlist' || action === 'play-track') {
                ctx.uri = t.getAttribute('data-spotify-uri') || '';
                if (action === 'play-track') {
                    if (spotifyState.selectedPlaylist) spotifyMarkPlaylistTracksBrowsing(true);
                    else spotifyMarkSearchBrowsing(true);
                }
            }
            if (action === 'add-to-playlist') {
                ctx.playlistId = t.getAttribute('data-playlist-id') || '';
                ctx.playlistName = t.getAttribute('data-playlist-name') || '';
            }
            handleSpotifyAction(action, ctx).catch(function (err) {
                var msg = spotifyFriendlyError(err);
                if (spotifyState.lastView && spotifyState.lastView.kind === 'connected') {
                    spotifyState.inlineError = msg;
                    spotifyApplyConnectedView(spotifyState.lastView, false);
                } else {
                    spotifyRenderAll({ kind: 'error', message: msg });
                }
            });
        });
        document.addEventListener('input', function (e) {
            var t = e.target;
            if (!t || !t.classList) return;
            if (t.classList.contains('os-spotify-vol')) {
                var panelVol = t.closest('.os-spotify-panel');
                if (panelVol) handleSpotifyAction('volume', { value: Number(t.value) });
                return;
            }
            if (t.classList.contains('os-spotify-search__input')) {
                var panelSearch = t.closest('.os-spotify-panel');
                if (!panelSearch) return;
                spotifyState.searchQuery = t.value;
                if (spotifyState.searchDebounce) clearTimeout(spotifyState.searchDebounce);
                spotifyState.searchDebounce = setTimeout(function () {
                    spotifyRunSearch(spotifyState.searchQuery).then(function () {
                        spotifySchedulePersistUiState();
                    });
                }, 450);
                spotifySchedulePersistUiState();
            }
        });
        document.addEventListener('focusin', function (e) {
            var t = e.target;
            if (t && t.classList && t.classList.contains('os-spotify-search__input')) {
                spotifyState.searchFocused = true;
            }
        });
        document.addEventListener('focusout', function (e) {
            var t = e.target;
            if (!t || !t.classList || !t.classList.contains('os-spotify-search__input')) return;
            setTimeout(function () {
                var ae = document.activeElement;
                if (!ae || !ae.classList || !ae.classList.contains('os-spotify-search__input')) {
                    spotifyState.searchFocused = false;
                }
            }, 80);
        });
        function spotifyMarkListBrowsingFromEvent(e) {
            var results = e.target.closest && e.target.closest('.os-spotify-search__results');
            if (results) {
                spotifyMarkSearchBrowsing(true);
                return;
            }
            var plTracks = e.target.closest && e.target.closest('.os-spotify-playlists__tracks');
            if (plTracks) {
                spotifyMarkPlaylistTracksBrowsing(true);
                return;
            }
            var addList = e.target.closest && e.target.closest('.os-spotify-add-playlist__list');
            if (addList) spotifyMarkAddPlaylistBrowsing(true);
        }
        document.addEventListener('pointerdown', spotifyMarkListBrowsingFromEvent, true);
        document.addEventListener('wheel', spotifyMarkListBrowsingFromEvent, { passive: true, capture: true });
        document.addEventListener('touchmove', spotifyMarkListBrowsingFromEvent, { passive: true, capture: true });
        document.addEventListener('scroll', function (e) {
            var plTracks = e.target.classList && e.target.classList.contains('os-spotify-playlists__tracks')
                ? e.target
                : (e.target.closest && e.target.closest('.os-spotify-playlists__tracks'));
            if (plTracks) {
                spotifyMarkPlaylistTracksBrowsing(true);
                spotifyRememberListScroll('playlistTracks', plTracks);
                spotifySchedulePersistUiState();
                return;
            }
            var plList = e.target.classList && e.target.classList.contains('os-spotify-playlists__list--tab')
                ? e.target
                : (e.target.closest && e.target.closest('.os-spotify-playlists__list--tab'));
            if (plList) {
                spotifyRememberListScroll('playlistsList', plList);
                spotifySchedulePersistUiState();
                return;
            }
            var searchResults = e.target.classList && e.target.classList.contains('os-spotify-search__results')
                ? e.target
                : (e.target.closest && e.target.closest('.os-spotify-search__results'));
            if (searchResults) {
                spotifyRememberListScroll('searchResults', searchResults);
                spotifySchedulePersistUiState();
                return;
            }
            var addList = e.target.classList && e.target.classList.contains('os-spotify-add-playlist__list')
                ? e.target
                : (e.target.closest && e.target.closest('.os-spotify-add-playlist__list'));
            if (addList) {
                spotifyMarkAddPlaylistBrowsing(true);
                spotifyRememberListScroll('addPlaylistList', addList);
                spotifySchedulePersistUiState();
                return;
            }
            var quickTracks = e.target.getAttribute && e.target.getAttribute('data-spotify-part') === 'quick-hover-tracks'
                ? e.target
                : (e.target.closest && e.target.closest('[data-spotify-part="quick-hover-tracks"]'));
            if (quickTracks) {
                spotifyRememberListScroll('quickHoverTracks', quickTracks);
                spotifySchedulePersistUiState();
            }
        }, true);
        document.addEventListener('keydown', function (e) {
            var inp = e.target;
            if (!inp || !inp.classList || !inp.classList.contains('os-spotify-search__input')) return;
            if (e.key === 'Enter') {
                e.preventDefault();
                if (spotifyState.searchDebounce) clearTimeout(spotifyState.searchDebounce);
                spotifyRunSearch(inp.value);
            }
        });
        if (!window.__spotifyPersistUnloadBound) {
            window.__spotifyPersistUnloadBound = true;
            window.addEventListener('pagehide', spotifyPersistUiState);
            window.addEventListener('beforeunload', spotifyPersistUiState);
        }
        document.addEventListener('keydown', function (e) {
            var wrap = e.target;
            if (!wrap || wrap.getAttribute('data-spotify-part') !== 'hero-art-wrap') return;
            if (e.key === 'Enter' || e.key === ' ') {
                e.preventDefault();
                spotifyCycleArtView();
            }
        });
    }

    function syncSpotifyWidgetCollapsedUi(section, btn, collapsed) {
        if (!section || !btn) return;
        section.classList.toggle('os-widget--spotify-collapsed', collapsed);
        btn.setAttribute('aria-expanded', collapsed ? 'false' : 'true');
        btn.title = collapsed ? 'Maximizar sección' : 'Minimizar sección';
        clearOsWidgetDragInlineStyles(section);
    }

    function bindSpotifyWidgetCollapse() {
        var section = document.getElementById('osWidgetSpotify');
        var btn = document.getElementById('osWidgetSpotifyToggle');
        if (!section || !btn || btn.dataset.osCollapseBound === '1') return;
        btn.dataset.osCollapseBound = '1';
        function readCollapsed() { try { return localStorage.getItem(LS_OS_SPOTIFY_COLLAPSED) === '1'; } catch (_) { return false; } }
        function writeCollapsed(v) { try { localStorage.setItem(LS_OS_SPOTIFY_COLLAPSED, v ? '1' : '0'); } catch (_) {} }
        function applyCollapsed(c) { syncSpotifyWidgetCollapsedUi(section, btn, c); writeCollapsed(c); }
        applyCollapsed(readCollapsed());
        btn.addEventListener('click', function () {
            if (isMobileMode()) return;
            applyCollapsed(!section.classList.contains('os-widget--spotify-collapsed'));
        });
    }

    function spotifyModalVisible() {
        var m = document.getElementById('osSpotifyMobileModal');
        return !!(m && (m.style.display === 'block' || m.style.display === 'flex'));
    }

    function closeSpotifyMobileModal(skipBodyClass) {
        var modal = document.getElementById('osSpotifyMobileModal');
        if (!modal) return;
        modal.style.display = 'none';
        if (!skipBodyClass) {
            if (!notesModalVisible() && !systemModalVisible() && !weatherModalVisible() && !mediaModalVisible()) {
                document.body.classList.remove('modal-open');
            }
        }
    }

    function openSpotifyMobileModal() {
        var modal = document.getElementById('osSpotifyMobileModal');
        if (!modal) return;
        closeNotesMobileModal(true);
        closeSystemMobileModal();
        closeWeatherMobileModal(true);
        closeMediaMobileModal(true);
        modal.style.display = 'flex';
        document.body.classList.add('modal-open');
        refreshSpotifyWidget(true);
    }

    function startSpotifyPolling() {
        stopSpotifyPolling();
        spotifyState.polling = setInterval(function () {
            var menu = document.getElementById('windowsMenu');
            var disp = menu ? (menu.style.display || '').trim().toLowerCase() : '';
            var menuOpen = !!(menu && (disp === 'flex' || disp === 'block'));
            var quickActive = spotifyQuickAccountConnected();
            if ((menuOpen || spotifyModalVisible() || quickActive) && spotifyState.player && spotifyState.lastView
                && spotifyState.lastView.kind === 'connected') {
                spotifyState.player.getCurrentState().then(function (state) {
                    spotifyState.playerState = state;
                    spotifyUpdateQuickPlayer();
                    if (!state) return;
                    if (menuOpen || spotifyModalVisible()) {
                        spotifySyncViewFromPlayerState(false);
                    }
                }).catch(function () {});
            }
        }, spotifyQuickAccountConnected() ? 2500 : 8000);
    }

    function stopSpotifyPolling() {
        if (spotifyState.polling) { clearInterval(spotifyState.polling); spotifyState.polling = null; }
    }

    function notesCarouselNoteIndex(wrap) {
        if (!wrap) return { i: 0, total: 0, filtered: false, noMatch: false };
        var q = getNotesSearchQuery();
        var visible = notesCarouselVisibleCards(wrap);
        var total = visible.length;
        if (q && !total) {
            return { i: 0, total: 0, filtered: true, noMatch: true };
        }
        if (!total) return { i: 0, total: 0, filtered: !!q, noMatch: false };
        var card = notesCarouselActiveCard(wrap);
        var idx = card ? visible.indexOf(card) : 0;
        if (idx < 0) idx = 0;
        return { i: idx + 1, total: total, filtered: !!q, noMatch: false };
    }

    function setNotesCounterText(el, wrap) {
        if (!el) return;
        if (!wrap) {
            el.textContent = '';
            return;
        }
        var o = notesCarouselNoteIndex(wrap);
        if (o.noMatch) {
            el.textContent = 'Sin coincidencias';
            return;
        }
        if (!o.total) {
            el.textContent = '';
            return;
        }
        if (o.filtered) {
            el.textContent = 'Coincidencia ' + o.i + ' de ' + o.total;
            return;
        }
        el.textContent = 'Nota ' + o.i + ' de ' + o.total;
    }

    function updateNotesCounter() {
        var deskEl = document.getElementById('osNotesCounter');
        var mobEl = document.getElementById('osNotesCounterMobile');
        var deskWrap = document.getElementById('osNotesCarousel');
        var mobWrap = document.getElementById('osNotesCarouselMobile');
        if (deskEl) deskEl.textContent = '';
        if (mobEl) mobEl.textContent = '';
        if (notesModalVisible()) {
            setNotesCounterText(mobEl, mobWrap);
            return;
        }
        if (!isMobileMode()) {
            setNotesCounterText(deskEl, deskWrap);
        }
    }

    var __notesCounterResizeTimer = null;
    function scheduleUpdateNotesCounter() {
        clearTimeout(__notesCounterResizeTimer);
        __notesCounterResizeTimer = setTimeout(updateNotesCounter, 80);
    }

    function closeSystemMobileModal(skipBodyClass) {
        const modal = document.getElementById('osSystemMobileModal');
        if (!modal) return;
        modal.style.display = 'none';
        if (!skipBodyClass) {
            if (!notesModalVisible() && !weatherModalVisible() && !mediaModalVisible()) {
                document.body.classList.remove('modal-open');
            }
        }
    }

    function openSystemMobileModal() {
        const modal = document.getElementById('osSystemMobileModal');
        if (!modal) return;
        closeNotesMobileModal(true);
        closeWeatherMobileModal(true);
        closeMediaMobileModal(true);
        modal.style.display = 'flex';
        document.body.classList.add('modal-open');
        runTelemetryOnce();
    }

    function openNotesMobileModal() {
        const modal = document.getElementById('osNotesMobileModal');
        if (!modal) return;
        closeSystemMobileModal();
        closeWeatherMobileModal(true);
        closeMediaMobileModal(true);
        modal.style.display = 'flex';
        document.body.classList.add('modal-open');
        renderNotesCarousel();
    }

    /** @param {boolean} [skipBodyClass] si true, no quita modal-open (p. ej. al abrir otro modal OS). */
    function closeNotesMobileModal(skipBodyClass) {
        const modal = document.getElementById('osNotesMobileModal');
        if (!modal) return;
        modal.style.display = 'none';
        updateNotesCounter();
        if (!skipBodyClass) {
            if (!notesModalVisible() && !systemModalVisible() && !mediaModalVisible()) {
                document.body.classList.remove('modal-open');
            }
        }
    }

    function openMediaMobileModal() {
        const modal = document.getElementById('osMediaMobileModal');
        if (!modal) return;
        closeNotesMobileModal(true);
        closeSystemMobileModal();
        closeWeatherMobileModal(true);
        closeSpotifyMobileModal(true);
        modal.style.display = 'flex';
        document.body.classList.add('modal-open');
        renderMediaSlider();
    }

    function closeMediaMobileModal(skipBodyClass) {
        const modal = document.getElementById('osMediaMobileModal');
        if (!modal) return;
        modal.style.display = 'none';
        if (!skipBodyClass) {
            if (!notesModalVisible() && !systemModalVisible() && !weatherModalVisible() && !spotifyModalVisible()) {
                document.body.classList.remove('modal-open');
            }
        }
    }

    function bindSwipeOnWrap(wrap) {
        if (!wrap || wrap.dataset.osSwipe === '1') return;
        wrap.dataset.osSwipe = '1';
        let sx = 0;
        wrap.addEventListener('touchstart', function (e) {
            if (e.touches[0]) sx = e.touches[0].clientX;
        }, { passive: true });
        wrap.addEventListener('touchend', function (e) {
            if (!e.changedTouches[0]) return;
            const dx = e.changedTouches[0].clientX - sx;
            if (Math.abs(dx) > 40) scrollNotes(dx < 0 ? 1 : -1);
        }, { passive: true });
    }

    function renderSystemWidgetInto(pill, meta, meters, data, clientMeta) {
        if (!pill || !meta || !meters) return;

        const env = data && data.environment === 'local' ? 'local' : 'hosting';
        const envLabel = (data && data.labels && data.labels[env]) ? data.labels[env] : env;
        pill.textContent = envLabel;

        let metaLines = [];
        if (data && data.php) {
            metaLines.push('PHP ' + data.php.version + ' · ' + data.php.osFamily + ' · ' + data.php.sapi);
        }
        if (data && data.server && data.server.software) {
            metaLines.push(data.server.software);
        }
        if (env === 'local' && data && data.network && data.network.localIp) {
            metaLines.push('IP local: ' + data.network.localIp);
        } else if (data && data.network && data.network.clientIp) {
            metaLines.push('IP cliente: ' + data.network.clientIp);
        }
        if (data && data.paths && data.paths.xamppLike) {
            metaLines.push('Ruta tipo XAMPP/htdocs: probable');
        }
        if (data && data.services) {
            if (data.services.probeAvailable) {
                metaLines.push('Apache: ' + (data.services.apacheDetected ? 'detectado' : 'no detectado') +
                    ' · MySQL: ' + (data.services.mysqlDetected ? 'detectado' : 'no detectado'));
            } else {
                metaLines.push('Procesos Apache/MySQL: N/D (shell_exec no disponible)');
            }
        }
        if (clientMeta && clientMeta.latencyMs != null) {
            metaLines.push('Latencia (self): ~' + Math.round(clientMeta.latencyMs) + ' ms');
        }
        if (clientMeta && clientMeta.online === false) {
            metaLines.push('Red: sin conexión');
        }
        if (clientMeta && clientMeta.connection && clientMeta.connection.effectiveType) {
            metaLines.push('Red (navegador): ' + clientMeta.connection.effectiveType +
                (clientMeta.connection.downlink != null ? ' · ~' + clientMeta.connection.downlink + ' Mbps' : ''));
        }
        if (data && data.traffic && data.traffic.global) {
            var tg = data.traffic.global;
            metaLines.push('Tráfico Flexia (servidor, hoy): ↓' + formatTrafficBytes(tg.todayInBytes) +
                ' · ↑' + formatTrafficBytes(tg.todayOutBytes));
        }
        if (data && data.traffic && data.traffic.user) {
            var tu = data.traffic.user;
            metaLines.push('Tu tráfico Flexia (hoy): ↓' + formatTrafficBytes(tu.todayInBytes) +
                ' · ↑' + formatTrafficBytes(tu.todayOutBytes));
        }
        if (clientMeta && (clientMeta.clientInBytes > 0 || clientMeta.clientOutBytes > 0)) {
            metaLines.push('Esta pestaña (aprox.): ↓' + formatTrafficBytes(clientMeta.clientInBytes) +
                ' · ↑' + formatTrafficBytes(clientMeta.clientOutBytes));
        }
        if (data && data.alerts && data.alerts.length) {
            data.alerts.forEach(function (a) {
                if (a && a.message) metaLines.push('⚠ ' + a.message);
            });
        }
        meta.innerHTML = metaLines.map(function (l) { return '<div>' + String(l).replace(/</g, '&lt;') + '</div>'; }).join('');

        var ringItems = [];

        let diskPct = null;
        if (data && data.disk && data.disk.usedPercent != null) {
            diskPct = data.disk.usedPercent;
        }
        ringItems.push({
            key: 'disk',
            label: 'Disco (proyecto)',
            shortLabel: 'Disco',
            pct: diskPct,
            hint: 'Espacio usado en la raíz de la app'
        });

        let memPct = null;
        if (data && data.phpMemory && data.phpMemory.usagePercentOfLimit != null) {
            memPct = data.phpMemory.usagePercentOfLimit;
        }
        ringItems.push({
            key: 'memory',
            label: 'Memoria PHP',
            shortLabel: 'Memoria',
            pct: memPct,
            hint: (data && data.phpMemory && data.phpMemory.limitLabel)
                ? 'Pico / límite: ' + data.phpMemory.limitLabel
                : 'Memoria PHP (pico / límite)'
        });

        let cpuLike = null;
        let cpuLabel = 'CPU / carga';
        let cpuHint = 'No disponible en este entorno';
        if (data && data.loadAvg && data.loadAvg['1m'] != null) {
            cpuLike = Math.min(100, (data.loadAvg['1m'] / 4) * 100);
            cpuLabel = 'Carga servidor (1m)';
            cpuHint = 'Aprox. desde load average';
        } else if (clientMeta && clientMeta.latencyMs != null) {
            cpuLike = Math.min(100, clientMeta.latencyMs / 3);
            cpuLabel = 'Latencia';
            cpuHint = 'Sin loadavg en este host';
        }
        ringItems.push({
            key: 'cpu',
            label: cpuLabel,
            shortLabel: 'Carga',
            pct: cpuLike,
            hint: cpuHint
        });

        renderConcentricMeterRings(meters, ringItems);
    }

    function renderSystemWidget(data, clientMeta) {
        renderSystemWidgetInto(
            document.getElementById('osSystemEnvPill'),
            document.getElementById('osSystemMeta'),
            document.getElementById('osSystemMeters'),
            data,
            clientMeta
        );
        renderSystemWidgetInto(
            document.getElementById('osSystemEnvPillMobile'),
            document.getElementById('osSystemMetaMobile'),
            document.getElementById('osSystemMetersMobile'),
            data,
            clientMeta
        );
    }

    function fetchOsEnvironment() {
        const url = (typeof BASE_URL !== 'undefined' ? BASE_URL : '') + '?action=osEnvironment';
        const t0 = performance.now();
        return fetch(url, { method: 'GET', cache: 'no-store', credentials: 'same-origin' })
            .then(function (r) { return r.json(); })
            .then(function (data) {
                const latencyMs = performance.now() - t0;
                return { data: data, latencyMs: latencyMs };
            })
            .catch(function () {
                return { data: { success: false }, latencyMs: null };
            });
    }

    function updateTaskbarTray(clientMeta, battery, envData) {
        const net = document.getElementById('taskbarTrayNet');
        const bat = document.getElementById('taskbarTrayBat');
        if (net) {
            const offline = clientMeta && clientMeta.online === false;
            const hasAlert = envData && envData.alerts && envData.alerts.length > 0;
            net.classList.toggle('taskbar-tray__item--offline', offline);
            net.classList.toggle('taskbar-tray__item--warn', !offline && hasAlert);
            const i = net.querySelector('i');
            if (i) {
                i.className = offline ? 'fas fa-wifi-slash' : 'fas fa-wifi';
            }
            var parts = ['Red'];
            if (offline) {
                parts.push('sin conexión');
            } else {
                if (clientMeta && clientMeta.latencyMs != null) {
                    var lat = Math.round(clientMeta.latencyMs);
                    var quality = latencyQualityLabel(lat);
                    parts.push('~' + lat + ' ms' + (quality ? ' (' + quality + ')' : ''));
                }
                if (envData && envData.traffic && envData.traffic.user) {
                    var u = envData.traffic.user;
                    parts.push('hoy ↑' + formatTrafficBytes(u.todayOutBytes));
                } else if (envData && envData.traffic && envData.traffic.global) {
                    var g = envData.traffic.global;
                    parts.push('Flexia hoy ↑' + formatTrafficBytes(g.todayOutBytes));
                }
                if (clientMeta && (clientMeta.clientInBytes > 0 || clientMeta.clientOutBytes > 0)) {
                    parts.push('pestaña ↓' + formatTrafficBytes(clientMeta.clientInBytes));
                }
            }
            if (hasAlert) parts.push('· aviso');
            net.title = parts.join(' · ');
        }
        if (bat && battery) {
            bat.hidden = false;
            const i = bat.querySelector('i');
            if (!i) return;
            if (battery.level <= 0.2) {
                i.className = 'fas fa-battery-empty';
                bat.title = 'Batería baja';
            } else if (battery.level <= 0.55) {
                i.className = 'fas fa-battery-half';
                bat.title = 'Batería media';
            } else {
                i.className = 'fas fa-battery-full';
                bat.title = 'Batería alta';
            }
        } else if (bat) {
            bat.hidden = true;
        }
    }

    function runTelemetryOnce() {
        const A = api();
        const clientMeta = {
            online: typeof navigator.onLine === 'boolean' ? navigator.onLine : true,
            connection: null
        };
        try {
            if (navigator.connection) {
                clientMeta.connection = {
                    effectiveType: navigator.connection.effectiveType,
                    downlink: navigator.connection.downlink
                };
            }
        } catch (_) {}

        let batPromise = Promise.resolve(null);
        if (navigator.getBattery) {
            batPromise = navigator.getBattery().catch(function () { return null; });
        }

        return Promise.all([fetchOsEnvironment(), batPromise]).then(function (pair) {
            const res = pair[0];
            const battery = pair[1];
            const data = res.data || {};
            const clientMetaFull = {
                latencyMs: res.latencyMs,
                online: clientMeta.online,
                connection: clientMeta.connection,
                clientInBytes: __flexiaClientTraffic.inBytes,
                clientOutBytes: __flexiaClientTraffic.outBytes
            };
            if (data.success) {
                renderSystemWidget(data, clientMetaFull);
            } else {
                renderSystemWidget({ environment: 'hosting', labels: { local: 'Local', hosting: 'Hosting' }, php: {}, server: {}, network: {}, disk: null, phpMemory: {}, loadAvg: null, services: { probeAvailable: false } }, clientMetaFull);
            }
            updateTaskbarTray(clientMetaFull, battery, data.success ? data : null);
        });
    }

    function startTelemetryLoop() {
        const A = api();
        const sec = (A && A.getTelemetryIntervalSec) ? A.getTelemetryIntervalSec() : 22;
        clearInterval(telemetryTimer);
        telemetryTimer = setInterval(runTelemetryOnce, Math.max(12, sec) * 1000);
    }

    var __osVvTimer = null;
    /** Alto del viewport al abrir el menú en móvil; evita que el teclado virtual encoja el menú. */
    var __osStartMenuVvLockH = null;

    function isMobileStartMenuLayoutContext() {
        var body = document.body;
        if (body.classList.contains('mobile-mode')) return false;
        return body.classList.contains('mobile-pc-mode') ||
            window.matchMedia('(max-width: 768px)').matches;
    }

    /** Móvil vertical: top/bottom en CSS; no fijar height (evita hueco superior y encogimiento con teclado). */
    function shouldUseCssAnchoredStartMenuHeight() {
        if (window.matchMedia('(max-width: 768px)').matches) return true;
        var body = document.body;
        if (body.classList.contains('mobile-pc-mode') &&
            window.matchMedia('(orientation: portrait)').matches) {
            return true;
        }
        return false;
    }

    function isStartMenuVisible() {
        var menu = document.getElementById('windowsMenu');
        if (!menu) return false;
        var inline = (menu.style.display || '').trim().toLowerCase();
        if (inline === 'none') return false;
        try {
            return window.getComputedStyle(menu).display !== 'none';
        } catch (_) {
            return true;
        }
    }

    function captureStartMenuViewportLock() {
        var inner = window.innerHeight || 0;
        var vv = window.visualViewport;
        var vvh = (vv && vv.height) ? vv.height : inner;
        __osStartMenuVvLockH = Math.round(Math.max(inner, vvh, 200));
        try {
            document.documentElement.style.setProperty(
                '--os-start-menu-vv-lock',
                __osStartMenuVvLockH + 'px'
            );
        } catch (_) {}
    }

    function clearStartMenuViewportLock() {
        __osStartMenuVvLockH = null;
        try {
            document.documentElement.style.removeProperty('--os-start-menu-vv-lock');
        } catch (_) {}
    }

    function getStartMenuLayoutViewportHeight() {
        if (__osStartMenuVvLockH != null && isStartMenuVisible() && isMobileStartMenuLayoutContext()) {
            return __osStartMenuVvLockH;
        }
        var vv = window.visualViewport;
        if (vv && vv.height) return Math.round(vv.height);
        return Math.round(window.innerHeight || 600);
    }

    function getStartMenuLayoutBottomLimit(edgePad) {
        edgePad = edgePad || 5;
        if (__osStartMenuVvLockH != null && isStartMenuVisible() && isMobileStartMenuLayoutContext()) {
            return __osStartMenuVvLockH - edgePad;
        }
        var vv = window.visualViewport;
        if (vv) return (vv.height + vv.offsetTop) - edgePad;
        return (window.innerHeight || 600) - edgePad;
    }

    /**
     * Panel ancho (plegables desplegados / landscape): mosaico cuadrado en el menú inicio.
     * Portrait estrecho en móvil PC conserva la lista vertical.
     */
    function shouldUseStartMenuFoldWideLayout() {
        var body = document.body;
        if (!body.classList.contains('mobile-pc-mode')) return false;
        var w = window.innerWidth || 0;
        var h = window.innerHeight || 1;
        var landscape = w > h;
        if (!landscape && w < 540) return false;
        try {
            if (window.matchMedia('(horizontal-viewport-segments: 2)').matches) return true;
        } catch (_) {}
        try {
            if (window.matchMedia('(spanning: single-fold-vertical)').matches ||
                window.matchMedia('(spanning: single-fold-horizontal)').matches) {
                return true;
            }
        } catch (_) {}
        if (landscape && w >= 520) return true;
        return false;
    }

    function updateStartMenuFoldWideClass() {
        try {
            document.body.classList.toggle('os-start-menu-fold-wide', shouldUseStartMenuFoldWideLayout());
        } catch (_) {}
    }

    /**
     * Sincroniza --os-app-visual-height y alturas reales de topbar/taskbar (zoom, DPI, fuentes).
     * Evita huecos o desbordes cuando 100vh o los px fijos no coinciden con el layout visible.
     */
    function updateStartMenuVisualViewportVars() {
        try {
            var root = document.documentElement;
            var h = getStartMenuLayoutViewportHeight();
            if (!h || h < 200) {
                h = window.innerHeight || 600;
            }
            root.style.setProperty('--os-app-visual-height', Math.round(h) + 'px');

            var topbar = document.querySelector('.topbar');
            if (topbar) {
                var th = Math.ceil(topbar.getBoundingClientRect().height);
                if (th > 0) {
                    root.style.setProperty('--topbar-height', th + 'px');
                }
            }
            var taskbar = document.querySelector('.taskbar');
            if (taskbar) {
                var cs = window.getComputedStyle(taskbar);
                if (cs.display !== 'none' && cs.visibility !== 'hidden') {
                    var tbh = Math.ceil(taskbar.getBoundingClientRect().height);
                    if (tbh > 0) {
                        root.style.setProperty('--taskbar-height', tbh + 'px');
                    }
                }
            }
        } catch (_) {}
    }

    /**
     * Alto del menú desde coordenadas reales (compatible con document.body.style.zoom del grid).
     */
    function syncStartMenuLayout() {
        try {
            var body = document.body;
            if (body.classList.contains('mobile-mode')) return;

            var menu = document.getElementById('windowsMenu');
            if (!menu) return;

            var menuCs = window.getComputedStyle(menu);
            if (menuCs.display === 'none') {
                menu.style.removeProperty('height');
                menu.style.removeProperty('max-height');
                return;
            }

            updateStartMenuVisualViewportVars();

            if (shouldUseCssAnchoredStartMenuHeight()) {
                /* En pantalla estrecha el menú usa top/bottom en CSS; no fijar altura con visualViewport
                   (el teclado virtual lo encogía y subía el pie del menú). */
                menu.style.removeProperty('height');
                menu.style.removeProperty('max-height');
                menu.style.removeProperty('bottom');
                return;
            }

            var edgePad = 5;
            var menuTop = menu.getBoundingClientRect().top;
            var bottomLimit;
            var taskbar = document.querySelector('.taskbar');

            if (taskbar) {
                var tbCs = window.getComputedStyle(taskbar);
                if (tbCs.display !== 'none' && tbCs.visibility !== 'hidden') {
                    bottomLimit = taskbar.getBoundingClientRect().top - edgePad;
                }
            }
            if (typeof bottomLimit !== 'number') {
                bottomLimit = getStartMenuLayoutBottomLimit(edgePad);
            }

            var h = Math.floor(bottomLimit - menuTop);
            if (h < 120) return;

            menu.style.setProperty('height', h + 'px', 'important');
            menu.style.setProperty('max-height', h + 'px', 'important');
            menu.style.setProperty('bottom', 'auto', 'important');
        } catch (_) {}
    }

    function maybeRefreshStartMenuViewportLock() {
        if (!isStartMenuVisible() || !isMobileStartMenuLayoutContext()) return;
        var inner = Math.round(window.innerHeight || 0);
        if (__osStartMenuVvLockH == null || inner > __osStartMenuVvLockH + 48) {
            captureStartMenuViewportLock();
        }
    }

    function scheduleStartMenuVvUpdate() {
        clearTimeout(__osVvTimer);
        __osVvTimer = setTimeout(function () {
            maybeRefreshStartMenuViewportLock();
            updateStartMenuVisualViewportVars();
            updateStartMenuFoldWideClass();
            syncStartMenuLayout();
        }, 40);
    }

    function scheduleStartMenuLayoutRaf() {
        requestAnimationFrame(function () {
            requestAnimationFrame(syncStartMenuLayout);
        });
    }

    function bindStartMenuViewportVars() {
        updateStartMenuVisualViewportVars();
        updateStartMenuFoldWideClass();
        window.addEventListener('resize', scheduleStartMenuVvUpdate, { passive: true });
        window.addEventListener('orientationchange', scheduleStartMenuVvUpdate, { passive: true });
        document.addEventListener('fullscreenchange', scheduleStartMenuVvUpdate);
        if (window.visualViewport) {
            window.visualViewport.addEventListener('resize', scheduleStartMenuVvUpdate, { passive: true });
            window.visualViewport.addEventListener('scroll', scheduleStartMenuVvUpdate, { passive: true });
        }
        try {
            var topbar = document.querySelector('.topbar');
            var taskbar = document.querySelector('.taskbar');
            if (typeof ResizeObserver !== 'undefined') {
                var ro = new ResizeObserver(scheduleStartMenuVvUpdate);
                if (topbar) ro.observe(topbar);
                if (taskbar) ro.observe(taskbar);
            }
        } catch (_) {}
    }

    function onMenuOpened() {
        emergencyClearStuckWidgetLift();
        if (isMobileStartMenuLayoutContext()) {
            captureStartMenuViewportLock();
        }
        scheduleStartMenuVvUpdate();
        scheduleStartMenuLayoutRaf();
        runTelemetryOnce();
        renderNotesCarousel();
        renderMediaSlider();
        renderWeatherFromCache(false);
        refreshWeatherWidget(false);
        refreshSpotifyWidget(false);
    }

    function wireStartMenuHooks() {
        const menu = document.getElementById('windowsMenu');
        if (!menu || menu.dataset.osHooks === '1') return;
        menu.dataset.osHooks = '1';
        let wasVisible = false;
        const obs = new MutationObserver(function () {
            const vis = menu.style.display !== 'none' && window.getComputedStyle(menu).display !== 'none';
            if (vis && !wasVisible) onMenuOpened();
            if (!vis && wasVisible) {
                abortAllOsWidgetDrags();
                clearStartMenuViewportLock();
                scheduleStartMenuVvUpdate();
            }
            wasVisible = vis;
        });
        obs.observe(menu, { attributes: true, attributeFilter: ['style', 'class'] });
    }

    /** Mantiene la sección como región con título; en móvil el panel de widgets está oculto (CSS) y notas/sistema van por modales. */
    function syncSystemWidgetNotesHitzone() {
        const sec = document.getElementById('osWidgetSystem');
        if (!sec) return;
        if (sec.dataset.osShellLabeledBy === undefined) {
            sec.dataset.osShellLabeledBy = sec.getAttribute('aria-labelledby') || '';
        }
        const lb = sec.dataset.osShellLabeledBy;
        if (lb) sec.setAttribute('aria-labelledby', lb);
        else sec.removeAttribute('aria-labelledby');
        sec.removeAttribute('role');
        sec.removeAttribute('tabindex');
        sec.removeAttribute('aria-label');
    }

    function init() {
        fillCategoryIconSelect('categoryIconClass', function () { updateCategoryPreviewModal(); });
        fillCategoryIconSelect('newCategoryIconClass', null);
        fillCategoryIconSelect('editCategoryIconSelect', null);
        bindCategoryModalPreview();

        function wireNav(id, dir) {
            const el = document.getElementById(id);
            if (el) el.addEventListener('click', function () { scrollNotes(dir); });
        }
        wireNav('osNotesPrev', -1);
        wireNav('osNotesNext', 1);
        const add = document.getElementById('osNotesAdd');
        if (add) add.addEventListener('click', addNote);
        wireNav('osNotesPrevMobile', -1);
        wireNav('osNotesNextMobile', 1);
        const addMob = document.getElementById('osNotesAddMobile');
        if (addMob) addMob.addEventListener('click', addNote);

        const mobOpen = document.getElementById('osNotesMobileOpen');
        const mobClose = document.getElementById('osNotesMobileClose');
        if (mobOpen) {
            mobOpen.addEventListener('click', function (e) {
                e.preventDefault();
                e.stopPropagation();
                openNotesMobileModal();
            });
        }
        if (mobClose) {
            mobClose.addEventListener('click', function () {
                closeNotesMobileModal();
            });
        }
        const mobModal = document.getElementById('osNotesMobileModal');
        if (mobModal) {
            mobModal.addEventListener('click', function (e) {
                if (e.target === mobModal) closeNotesMobileModal();
            });
        }

        const sysOpen = document.getElementById('osSystemMobileOpen');
        const sysClose = document.getElementById('osSystemMobileClose');
        if (sysOpen) {
            sysOpen.addEventListener('click', function (e) {
                e.preventDefault();
                e.stopPropagation();
                openSystemMobileModal();
            });
        }
        if (sysClose) {
            sysClose.addEventListener('click', function () {
                closeSystemMobileModal();
            });
        }
        const sysModal = document.getElementById('osSystemMobileModal');
        if (sysModal) {
            sysModal.addEventListener('click', function (e) {
                if (e.target === sysModal) closeSystemMobileModal();
            });
        }

        const weatherOpen = document.getElementById('osWeatherMobileOpen');
        const weatherClose = document.getElementById('osWeatherMobileClose');
        if (weatherOpen) {
            weatherOpen.addEventListener('click', function (e) {
                e.preventDefault();
                e.stopPropagation();
                openWeatherMobileModal();
            });
        }
        if (weatherClose) {
            weatherClose.addEventListener('click', function () {
                closeWeatherMobileModal();
            });
        }
        const weatherModal = document.getElementById('osWeatherMobileModal');
        if (weatherModal) {
            weatherModal.addEventListener('click', function (e) {
                if (e.target === weatherModal) closeWeatherMobileModal();
            });
        }

        const spotifyOpen = document.getElementById('osSpotifyMobileOpen');
        const spotifyClose = document.getElementById('osSpotifyMobileClose');
        if (spotifyOpen) {
            spotifyOpen.addEventListener('click', function (e) {
                e.preventDefault();
                e.stopPropagation();
                openSpotifyMobileModal();
            });
        }
        if (spotifyClose) {
            spotifyClose.addEventListener('click', function () {
                closeSpotifyMobileModal();
            });
        }
        const spotifyModalEl = document.getElementById('osSpotifyMobileModal');
        if (spotifyModalEl) {
            spotifyModalEl.addEventListener('click', function (e) {
                if (e.target === spotifyModalEl) closeSpotifyMobileModal();
            });
        }

        const mediaOpen = document.getElementById('osMediaMobileOpen');
        const mediaClose = document.getElementById('osMediaMobileClose');
        if (mediaOpen) {
            mediaOpen.addEventListener('click', function (e) {
                e.preventDefault();
                e.stopPropagation();
                openMediaMobileModal();
            });
        }
        if (mediaClose) {
            mediaClose.addEventListener('click', function () {
                closeMediaMobileModal();
            });
        }
        const mediaModal = document.getElementById('osMediaMobileModal');
        if (mediaModal) {
            mediaModal.addEventListener('click', function (e) {
                if (e.target === mediaModal) closeMediaMobileModal();
            });
        }

        bindSwipeOnWrap(document.getElementById('osNotesCarousel'));
        bindSwipeOnWrap(document.getElementById('osNotesCarouselMobile'));

        ['osNotesCarousel', 'osNotesCarouselMobile'].forEach(function (id) {
            var nw = document.getElementById(id);
            if (nw && nw.dataset.osNotesScrollCounter !== '1') {
                nw.dataset.osNotesScrollCounter = '1';
                nw.addEventListener('scroll', updateNotesCounter, { passive: true });
            }
        });

        window.addEventListener('resize', scheduleUpdateNotesCounter, { passive: true });

        ensureOsWidgetsCollapsedOnFirstRun();
        bindSystemWidgetCollapse();
        bindNotesWidgetCollapse();
        bindMediaWidgetCollapse();
        bindWeatherWidgetCollapse();
        bindSpotifyWidgetCollapse();
        spotifyState.quickCompact = spotifyReadQuickCompact();
        spotifyApplyQuickCompactUi(document.getElementById('taskbarSpotifyQuick'));
        spotifyApplyQuickCompactUi(document.getElementById('spotifyQuickMobileFab'));
        bindSpotifyQuickPlayer();
        bindNotesSearch();
        bindWeatherRefreshButtons();
        bindSpotifyDelegation();
        bindSpotifyAuthMessageListener();
        bindMediaWidgetControls();
        syncMediaWidgetItemsWithExplorerFolder().finally(function () {
            renderMediaSlider();
        });
        ensureOsWidgetOrderOnInit();
        bindOsWidgetReorder();

        wireStartMenuHooks();
        if (!window.__osWidgetDragBlurBound) {
            window.__osWidgetDragBlurBound = true;
            window.addEventListener('blur', abortAllOsWidgetDrags);
        }
        bindStartMenuViewportVars();
        syncSystemWidgetNotesHitzone();
        installFlexiaFetchMeter();
        runTelemetryOnce();
        startTelemetryLoop();
        renderWeatherFromCache(false);
        refreshWeatherWidget(false);
        refreshSpotifyWidget(false);
        spotifyUpdateQuickPlayer();
        startSpotifyPolling();

        try {
            const mqCompact = window.matchMedia('(max-width: 768px)');
            mqCompact.addEventListener('change', function () {
                scheduleStartMenuVvUpdate();
                renderNotesCarousel();
                renderMediaSlider();
                syncSystemWidgetNotesHitzone();
                updateNotesCounter();
            });
        } catch (_) {}
    }

    window.OsShell = {
        init: init,
        refreshNotes: renderNotesCarousel,
        refreshMediaSlider: renderMediaSlider,
        restoreMediaWidgetFromTrash: restoreMediaWidgetItemFromTrash,
        onMenuOpened: onMenuOpened,
        syncStartMenuLayout: syncStartMenuLayout,
        updateStartMenuFoldWideClass: updateStartMenuFoldWideClass,
        resetCategoryModal: resetCategoryModal,
        ensureCategoryIconSelect: function (selectId) {
            fillCategoryIconSelect(selectId, null);
        },
        closeNotesMobileModal: closeNotesMobileModal,
        closeSystemMobileModal: closeSystemMobileModal,
        closeWeatherMobileModal: closeWeatherMobileModal,
        closeSpotifyMobileModal: closeSpotifyMobileModal,
        closeMediaMobileModal: closeMediaMobileModal,
        refreshWeather: function (force) { return refreshWeatherWidget(!!force); },
        refreshSpotify: function (force) { return refreshSpotifyWidget(!!force); },
        startSpotifyAuth: startSpotifyAuth,
        openSpotifyIntegrationsConfig: openSpotifyIntegrationsConfig,
        openSpotifyFlexia: openSpotifyFlexia,
        updateSpotifyQuickPlayer: spotifyUpdateQuickPlayer
    };

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