        // ========== VARIABLES GLOBALES ==========
        let settings = typeof _phpSettings !== 'undefined' ? _phpSettings : {};
        const GRID_SESSION_USER_ID = (typeof window !== 'undefined' && window.GRID_SESSION_USER && window.GRID_SESSION_USER.id)
            ? String(window.GRID_SESSION_USER.id)
            : '';
        const GRID_SETTINGS_REDUNDANCY_KEY = 'grid_settings_redundancy_v1';
        const GRID_TASKBAR_OPEN_ORDER_KEY = 'grid_taskbar_open_order_v1';
        const GRID_TASKBAR_PINNED_ORDER_KEY = 'grid_taskbar_pinned_order_v1';
        const GRID_ASSET_BUST_KEY = 'grid_asset_bust_v1';

        function gridScopedLsKey(key) {
            return GRID_SESSION_USER_ID ? (String(key) + '_u_' + GRID_SESSION_USER_ID) : String(key);
        }

        function gridGetAssetBustToken() {
            try {
                const t = localStorage.getItem(GRID_ASSET_BUST_KEY);
                return t ? String(t) : '';
            } catch (_) {
                return '';
            }
        }
        function gridSetAssetBustToken(token) {
            try { localStorage.setItem(GRID_ASSET_BUST_KEY, String(token || '')); } catch (_) {}
        }
        function gridApplyAssetBust(urlLike) {
            if (!urlLike) return urlLike;
            try {
                const token = gridGetAssetBustToken();
                if (!token) return urlLike;
                // Solo misma-origen (incluye rutas relativas). No tocar URLs externas.
                const base = window.location.href;
                const u = new URL(String(urlLike), base);
                const sameOrigin = (u.origin === window.location.origin);
                if (!sameOrigin) return urlLike;
                u.searchParams.set('__cb', token);
                return u.toString();
            } catch (_) {
                return urlLike;
            }
        }

        function gridReadJsonLs(key) {
            try {
                const raw = localStorage.getItem(gridScopedLsKey(key));
                if (!raw) return null;
                return JSON.parse(raw);
            } catch (_) {
                return null;
            }
        }
        function gridWriteJsonLs(key, value) {
            try {
                localStorage.setItem(gridScopedLsKey(key), JSON.stringify(value));
            } catch (_) {}
        }

        /**
         * Rutas del aplicativo SkechIA (arquitect-app) y dependencias compartidas, relativas al origen del sitio.
         * Se fuerza recarga de red (cache: 'reload') al usar «Borrar caché de CSS/JS» en configuración.
         */
        const GRID_SKETCH_CACHE_PATHS = [
            'arquitect-app/index.html',
            'arquitect-app/icon.png',
            'assets/vendor/font-awesome/css/all.min.css',
            'arquitect-app/assets/css/app.css',
            'arquitect-app/assets/css/layout.css',
            'arquitect-app/assets/css/dashboard.css',
            'arquitect-app/assets/css/editor.css',
            'arquitect-app/assets/css/dialogs.css',
            'arquitect-app/assets/css/responsive.css',
            'assets/vendor/pdfjs/pdf.min.js',
            'assets/vendor/pdfjs/pdf.worker.min.js',
            'arquitect-app/assets/vendor/jquery/jquery.min.js',
            'arquitect-app/assets/js/core/constants.js',
            'arquitect-app/assets/js/core/utils.js',
            'arquitect-app/assets/js/core/guards.js',
            'arquitect-app/assets/js/core/event.bus.js',
            'arquitect-app/assets/js/core/state.store.js',
            'arquitect-app/assets/js/app.config.js',
            'arquitect-app/assets/js/data/schemas.js',
            'arquitect-app/assets/js/data/serializers.js',
            'arquitect-app/assets/js/data/validators.js',
            'arquitect-app/assets/js/data/migrations.js',
            'arquitect-app/assets/js/system/system.storage.js',
            'arquitect-app/assets/js/system/system.projects.js',
            'arquitect-app/assets/js/editor/editor.transform.js',
            'arquitect-app/assets/js/input/pointer.manager.js',
            'arquitect-app/assets/js/input/gesture.manager.js',
            'arquitect-app/assets/js/input/keyboard.manager.js',
            'arquitect-app/assets/js/editor/editor.viewport.js',
            'arquitect-app/assets/js/editor/editor.canvas.js',
            'arquitect-app/assets/js/editor/editor.tools.js',
            'arquitect-app/assets/js/editor/editor.history.js',
            'arquitect-app/assets/js/editor/editor.layers.js',
            'arquitect-app/assets/js/editor/editor.selection.js',
            'arquitect-app/assets/js/editor/editor.text.js',
            'arquitect-app/assets/js/editor/editor.assets.js',
            'arquitect-app/assets/js/editor/editor.materials.js',
            'arquitect-app/assets/js/editor/editor.sheets.js',
            'arquitect-app/assets/js/editor/editor.init.js',
            'arquitect-app/assets/js/dialogs/dialog.base.js',
            'arquitect-app/assets/js/dialogs/dialog.project.js',
            'arquitect-app/assets/js/dialogs/dialog.export.js',
            'arquitect-app/assets/js/ai/ai.transport.js',
            'arquitect-app/assets/js/ai/ai.chat.js',
            'arquitect-app/assets/vendor/jspdf/jspdf.umd.min.js',
            'arquitect-app/assets/js/export/export.png.js',
            'arquitect-app/assets/js/export/export.pdf.js',
            'arquitect-app/assets/js/export/export.html.js',
            'arquitect-app/assets/js/app.router.js',
            'arquitect-app/assets/js/app.boot.js',
        ];

        async function gridReloadSketchAppAssetsCaches() {
            const extra = (typeof settings !== 'undefined' && settings && settings.explorerUi
                && Array.isArray(settings.explorerUi.assetCacheSketchExtraPaths))
                ? settings.explorerUi.assetCacheSketchExtraPaths.filter((p) => typeof p === 'string' && p.trim())
                : [];
            const paths = GRID_SKETCH_CACHE_PATHS.concat(extra);
            const origin = window.location.origin;
            await Promise.all(paths.map((rel) => {
                try {
                    const u = new URL(String(rel).trim(), window.location.href);
                    if (u.origin !== origin) return Promise.resolve();
                    return fetch(u.toString(), { cache: 'reload', mode: 'same-origin', credentials: 'same-origin' }).catch(() => {});
                } catch (_) {
                    return Promise.resolve();
                }
            }));
        }

        async function gridClearAssetCacheAndReload() {
            // Borra Cache Storage y desregistra Service Workers (si existieran) para evitar que
            // CSS/JS viejos se sirvan desde caché. Luego recarga con un query param único.
            const bust = String(Date.now());
            gridSetAssetBustToken(bust);
            try {
                if ('caches' in window && window.caches && typeof window.caches.keys === 'function') {
                    const keys = await window.caches.keys();
                    await Promise.all(keys.map((k) => window.caches.delete(k)));
                }
            } catch (_) {}
            try {
                if ('serviceWorker' in navigator && navigator.serviceWorker && typeof navigator.serviceWorker.getRegistrations === 'function') {
                    const regs = await navigator.serviceWorker.getRegistrations();
                    await Promise.all(regs.map((r) => r.unregister().catch(() => false)));
                }
            } catch (_) {}
            try {
                await gridReloadSketchAppAssetsCaches();
            } catch (_) {}
            try {
                const url = new URL(window.location.href);
                url.searchParams.set('__cb', bust);
                window.location.replace(url.toString());
            } catch (_) {
                window.location.reload();
            }
        }
        function gridSettingsStructureOk(s) {
            return !!(s && typeof s === 'object'
                && Array.isArray(s.categories) && s.categories.length > 0
                && Array.isArray(s.apps));
        }
        function gridNormalizeCategoryEntry(raw) {
            if (raw == null) return { name: 'General', icon: '', color: '', description: '' };
            if (typeof raw === 'string') {
                const name = String(raw).trim() || 'General';
                return { name, icon: '', color: '', description: '' };
            }
            if (typeof raw === 'object') {
                const name = String(raw.name || raw.title || '').trim() || 'General';
                return {
                    name,
                    icon: typeof raw.icon === 'string' ? raw.icon : '',
                    color: typeof raw.color === 'string' ? raw.color : '',
                    description: typeof raw.description === 'string' ? raw.description : '',
                };
            }
            return { name: 'General', icon: '', color: '', description: '' };
        }
        function gridCategoryNameFromEntry(raw) {
            return gridNormalizeCategoryEntry(raw).name;
        }
        function gridSanitizeFaClass(s) {
            const parts = String(s || '').trim().split(/\s+/).filter(function (p) {
                return /^fa[a-z0-9-]+$/i.test(p);
            });
            return parts.length ? parts.join(' ') : 'fas fa-folder';
        }
        function gridCategoryListIncludesName(list, name) {
            const n = String(name || '').trim();
            return Array.isArray(list) && list.some((e) => gridCategoryNameFromEntry(e) === n);
        }
        function gridFindCategoryEntryByName(name) {
            const arr = (settings && Array.isArray(settings.categories)) ? settings.categories : [];
            const n = String(name || '').trim();
            for (let i = 0; i < arr.length; i++) {
                if (gridCategoryNameFromEntry(arr[i]) === n) return gridNormalizeCategoryEntry(arr[i]);
            }
            return gridNormalizeCategoryEntry(n);
        }
        function gridNormalizeSettingsCategoriesInPlace() {
            if (!settings || typeof settings !== 'object') return;
            if (!Array.isArray(settings.categories) || settings.categories.length === 0) {
                settings.categories = [gridNormalizeCategoryEntry('General')];
                return;
            }
            settings.categories = settings.categories.map((e) => gridNormalizeCategoryEntry(e));
        }
        function gridPersistSettingsRedundancy(s) {
            try {
                localStorage.setItem(gridScopedLsKey(GRID_SETTINGS_REDUNDANCY_KEY), JSON.stringify(s));
            } catch (_) {}
        }
        function gridTryRecoverSettingsFromLocalStorage() {
            try {
                const raw = localStorage.getItem(gridScopedLsKey(GRID_SETTINGS_REDUNDANCY_KEY));
                if (!raw) return null;
                const parsed = JSON.parse(raw);
                if (gridSettingsStructureOk(parsed)) return parsed;
            } catch (_) {}
            return null;
        }
        (async function gridSettingsRedundancyBootstrap() {
            const fromLs = gridTryRecoverSettingsFromLocalStorage();
            const recoveryAlert = (typeof GRID_SETTINGS_RECOVERY_ALERT !== 'undefined' && GRID_SETTINGS_RECOVERY_ALERT);
            if (gridSettingsStructureOk(settings)) {
                gridPersistSettingsRedundancy(settings);
            } else if (fromLs) {
                const msg = recoveryAlert
                    ? 'Se detectó daño en la configuración del servidor. Hay una copia válida en este navegador.\n\n¿Deseas restaurarla ahora?'
                    : 'La configuración del servidor no es válida. Hay una copia válida en este navegador.\n\n¿Deseas restaurarla ahora?';
                if (await appDialog.confirm(msg)) {
                    settings = fromLs;
                    fetch('?action=saveSettings', {
                        method: 'POST',
                        headers: { 'Content-Type': 'application/json' },
                        body: JSON.stringify(fromLs),
                        credentials: 'same-origin',
                    }).catch(() => {});
                }
            }

            // Respaldo local: orden de barra de tareas (por usuario cuando hay sesión).
            // Solo usamos localStorage como *fallback* si el servidor no trae estas claves (o vienen vacías).
            try {
                const localPinned = gridReadJsonLs(GRID_TASKBAR_PINNED_ORDER_KEY);
                if (Array.isArray(localPinned) && (!Array.isArray(settings.pinnedApps) || settings.pinnedApps.length === 0)) {
                    settings.pinnedApps = localPinned;
                }
                const localOpenOrder = gridReadJsonLs(GRID_TASKBAR_OPEN_ORDER_KEY);
                if (Array.isArray(localOpenOrder) && (!Array.isArray(settings.taskbarOpenOrder) || settings.taskbarOpenOrder.length === 0)) {
                    settings.taskbarOpenOrder = localOpenOrder;
                }
            } catch (_) {}

            const lines = [];
            if (recoveryAlert) {
                lines.push(
                    'La configuración en el servidor (settings.json y/o settings.json.bak) estaba dañada o no se pudo leer.',
                    'Se ha aplicado la configuración por defecto. Puede que hayas perdido aplicaciones y ajustes personalizados.',
                    '',
                    'Qué puedes hacer:',
                    '• Si tienes una copia de seguridad (exportación ZIP), impórtala desde los ajustes de la aplicación.',
                    '• Revisa que la carpeta del proyecto tenga permisos de lectura y escritura para el servidor web.',
                    '• Si guardabas otra copia de settings.json fuera del servidor, súbela y sustituye el archivo en la raíz del proyecto.'
                );
            }
            if (typeof GRID_SETTINGS_PERSIST_FAILED !== 'undefined' && GRID_SETTINGS_PERSIST_FAILED) {
                if (lines.length) lines.push('');
                lines.push(
                    'No se pudo guardar la configuración en disco (settings.json).',
                    'Revisa permisos de la carpeta del proyecto, espacio en disco y que el antivirus no bloquee el archivo.',
                    'Hasta que se pueda escribir, los cambios pueden no persistir al recargar la página.'
                );
            }
            if (!gridSettingsStructureOk(settings) && !fromLs) {
                if (lines.length) lines.push('');
                lines.push(
                    'No se pudo validar la configuración y no hay una copia recuperable en este navegador (localStorage).',
                    'Prueba en otro navegador o equipo donde hubieras usado la app, o restaura desde copia de seguridad ZIP.'
                );
            }
            if (lines.length) {
                await appDialog.alert(lines.join('\n'));
            }
        })();
        function getSetting(key, fallback = null) {
            return settings[key] !== undefined ? settings[key] : fallback;
        }
        /** Clave estable para apps de archivo o enlace (papelera, taskbar, anclajes). */
        function getAppLinkKey(app) {
            if (!app) return '';
            if (app.explorerOpen && app.explorerOpen.bucket) {
                return 'gridexplorer:' + app.explorerOpen.bucket + ':' + (app.explorerOpen.relPath || '');
            }
            if (app.fileInfo && app.fileInfo.url) return String(app.fileInfo.url);
            return app.link || '';
        }
        function getAppStableId(app) {
            return app.title + getAppLinkKey(app);
        }
        function pinMatchesApp(p, app) {
            return p.title === app.title && (p.link || '') === getAppLinkKey(app);
        }
        function buildSecureFileUrl(url) {
            try {
                const u = new URL(url, window.location.origin);
                const mediaIdx = u.pathname.toLowerCase().indexOf('/media/');
                if (mediaIdx === -1) return url;
                const relative = decodeURIComponent(u.pathname.slice(mediaIdx + 1)); // media/...
                return `${BASE_URL}file.php?f=${encodeURIComponent(btoa(relative))}`;
            } catch (_) {
                return url;
            }
        }

        function resolveWallpaperMediaUrl(url) {
            if (!url) return '';
            const s = String(url).trim();
            if (/^https?:\/\//i.test(s) || s.includes('file.php?')) return s;
            if (s.toLowerCase().startsWith('media/')) return buildSecureFileUrl(s);
            return s;
        }

        function getExplorerWallpaperMediaKind(item) {
            if (!item || item.type === 'dir' || !item.url) return null;
            const mime = (item.mimeType || '').toLowerCase();
            const name = (item.name || '').toLowerCase();
            if (mime.startsWith('image/') || /\.(jpe?g|png|gif|webp|bmp|svg)$/i.test(name)) return 'image';
            if (mime.startsWith('video/') || /\.(mp4|webm|ogg|mov)$/i.test(name)) return 'video';
            return null;
        }

        function isExplorerManagedWallpaperPath(url) {
            return String(url || '').toLowerCase().includes('media/explorer/');
        }

        async function applyExplorerWallpaper(item) {
            const kind = getExplorerWallpaperMediaKind(item);
            if (!kind || !item.url) return;
            const mediaPath = item.url;
            applyBackgroundFromUrl(mediaPath);
            await saveSettings({ backgroundType: 'file', backgroundUrl: mediaPath });
            showToast(kind === 'image' ? tr('wallpaperSetFixedDone') : tr('wallpaperApplyDesktopDone'));
        }

        async function addExplorerWallpaperToList(item) {
            const kind = getExplorerWallpaperMediaKind(item);
            if (!kind || !item.url) return;
            const mediaPath = item.url;
            if (kind === 'image') {
                let images = getSetting('backgroundImages', []);
                if (images.includes(mediaPath)) {
                    await appDialog.alert(tr('wallpaperAlreadyInList'));
                    return;
                }
                images.push(mediaPath);
                settings.backgroundImages = images;
                renderImageList();
                await saveSettings({ backgroundType: 'images', backgroundImages: images });
                if (configModal && configModal.style.display === 'block') {
                    useImagesRadio.checked = true;
                    toggleBackgroundInputs();
                }
                showToast(tr('wallpaperAddedToListImages'));
            } else {
                let videos = getSetting('backgroundVideos', []);
                if (videos.includes(mediaPath)) {
                    await appDialog.alert(tr('wallpaperAlreadyInList'));
                    return;
                }
                videos.push(mediaPath);
                settings.backgroundVideos = videos;
                renderVideoList();
                await saveSettings({ backgroundType: 'videos', backgroundVideos: videos });
                if (configModal && configModal.style.display === 'block') {
                    useVideosRadio.checked = true;
                    toggleBackgroundInputs();
                }
                showToast(tr('wallpaperAddedToListVideos'));
            }
        }

        /** Atributo HTML seguro (src, etc.) */
        function escapeHtmlAttr(s) {
            return String(s).replace(/&/g, '&amp;').replace(/"/g, '&quot;').replace(/</g, '&lt;');
        }

        /** Texto en HTML (nodos de contenido) */
        function escapeHtml(s) {
            return String(s)
                .replace(/&/g, '&amp;')
                .replace(/</g, '&lt;')
                .replace(/>/g, '&gt;');
        }

        /** Texto plano/código: muchos servidores envían .txt como application/octet-stream. */
        function isTextLikeMimeOrName(mime, name) {
            const m = (mime || '').toLowerCase();
            const n = (name || '').toLowerCase();
            if (m.startsWith('text/')) return true;
            return /\.(txt|csv|md|json|xml|html?|css|js|mjs|cjs|php|sql|py|ts|tsx|jsx|vue|svg|log|ini|env|yaml|yml|sh|bat|rtf|toml)$/i.test(n);
        }

        /** HTML/HTM: vista previa renderizada (iframe), alternable con código fuente. */
        function isHtmlLikeMimeOrName(mime, name) {
            const m = (mime || '').toLowerCase();
            const n = (name || '').toLowerCase();
            if (m.includes('text/html')) return true;
            return /\.(html|htm)$/i.test(n);
        }

        function applyFeExplorerHtmlPreviewView(fePreviewBody) {
            if (!fePreviewBody) return;
            const ctx = fePreviewBody._feSplitCtx;
            if (!ctx || ctx.kind !== 'html') return;
            const frameUrl = ctx.frameUrl || ctx.secureUrl || ctx.fileUrl || '';
            const name = ctx.fileName || '';
            const src = ctx.sourceText != null ? String(ctx.sourceText) : '';
            const view = ctx.htmlView === 'text' ? 'text' : 'render';
            fePreviewBody.innerHTML = '';
            const surface = document.createElement('div');
            if (view === 'text') {
                surface.className = 'fe-preview-surface fe-preview-surface--text';
                const pre = document.createElement('pre');
                pre.className = 'fe-preview-text';
                pre.textContent = src || tr('explorerHtmlPreviewTextUnavailable');
                surface.appendChild(pre);
            } else {
                surface.className = 'fe-preview-surface fe-preview-surface--html';
                const ifr = document.createElement('iframe');
                ifr.className = 'fe-preview-html-frame';
                ifr.setAttribute('src', frameUrl);
                ifr.setAttribute('sandbox', 'allow-scripts allow-same-origin allow-popups allow-forms allow-modals');
                ifr.setAttribute('referrerpolicy', 'no-referrer');
                ifr.title = name || 'HTML';
                surface.appendChild(ifr);
            }
            fePreviewBody.appendChild(surface);
        }

        /** Descarga fiable en móvil (el atributo download a veces no aplica a PDF). */
        function downloadFileViaBlob(url, filename) {
            const name = filename || 'archivo';
            fetch(url, { credentials: 'same-origin' })
                .then((r) => {
                    if (!r.ok) throw new Error('fetch');
                    return r.blob();
                })
                .then((blob) => {
                    const blobUrl = URL.createObjectURL(blob);
                    const a = document.createElement('a');
                    a.href = blobUrl;
                    a.download = name;
                    document.body.appendChild(a);
                    a.click();
                    document.body.removeChild(a);
                    setTimeout(() => URL.revokeObjectURL(blobUrl), 2000);
                })
                .catch(() => {
                    const a = document.createElement('a');
                    a.href = url;
                    a.download = name;
                    document.body.appendChild(a);
                    a.click();
                    document.body.removeChild(a);
                });
        }

        /**
         * Imprimir PDF: si hay visor pdf.js (canvas) en previewHost, se abre ventana con imágenes
         * en la misma acción del usuario (print() fiable). Si no, fallback con URL en iframe.
         * @param {string} url URL segura del PDF (file.php…)
         * @param {HTMLElement|null} [previewHost] Contenedor del visor (p. ej. fePreviewBody con .pdf-canvas-scroll-wrap)
         */
        function printPdfFromUrl(url, previewHost) {
            if (!url) return;
            const canvases =
                previewHost && previewHost.querySelectorAll
                    ? previewHost.querySelectorAll('.pdf-canvas-scroll-wrap canvas')
                    : [];
            if (canvases.length) {
                const chunks = [
                    '<!DOCTYPE html><html><head><meta charset="utf-8"><title>Print</title>',
                    '<style>body{margin:0;background:#fff}img{display:block;max-width:100%;height:auto;margin:0 auto 8px;page-break-after:always}img:last-child{page-break-after:auto}</style></head><body>'
                ];
                let nImg = 0;
                for (let i = 0; i < canvases.length; i++) {
                    try {
                        const data = canvases[i].toDataURL('image/jpeg', 0.92);
                        chunks.push(`<img src="${data}" alt="" />`);
                        nImg++;
                    } catch (_) {}
                }
                chunks.push('</body></html>');
                if (nImg > 0) {
                    const w = window.open('', '_blank', 'noopener,noreferrer');
                    if (!w) {
                        window.open(url, '_blank', 'noopener,noreferrer');
                        return;
                    }
                    w.document.open();
                    w.document.write(chunks.join(''));
                    w.document.close();
                    try {
                        w.focus();
                        w.print();
                    } catch (_) {}
                    try {
                        w.addEventListener(
                            'afterprint',
                            () => {
                                try {
                                    w.close();
                                } catch (_) {}
                            },
                            { once: true }
                        );
                    } catch (_) {}
                    setTimeout(() => {
                        try {
                            if (w && !w.closed) w.close();
                        } catch (_) {}
                    }, 120000);
                    return;
                }
            }
            const w = window.open('', '_blank', 'noopener,noreferrer');
            if (!w) {
                window.open(url, '_blank', 'noopener,noreferrer');
                return;
            }
            const src = escapeHtmlAttr(url);
            w.document.open();
            w.document.write(
                '<!DOCTYPE html><html><head><meta charset="utf-8"><title>Print</title>' +
                    '<style>html,body{margin:0;height:100%}iframe{border:0;width:100%;height:100%}</style></head><body>' +
                    '<iframe id="fePrintPdfFrame" src="' +
                    src +
                    '"></iframe><scr' +
                    'ipt>(function(){var f=document.getElementById("fePrintPdfFrame");var done=0;function p(){if(done)return;try{if(!f||!f.contentWindow)return;}catch(e){return}done=1;try{f.contentWindow.focus();f.contentWindow.print();}catch(e){try{window.print();}catch(_){}}}f.addEventListener("load",function(){setTimeout(p,400);});setTimeout(p,4500);})();</scr' +
                    'ipt></body></html>'
            );
            w.document.close();
        }

        /**
         * Pellizco (pinch) en el visor PDF: mismo zoom que los botones.
         * - Móvil/tablet (maxTouchPoints): dos dedos vía touch (iOS + viewport maximum-scale=1 rompe gesture*).
         * - Escritorio: Pointer Events con dos punteros táctiles o pen (sin superficie táctil no hay pinch).
         */
        function setupPdfPinchZoom(scrollWrap, getZoom, setZoom, applyZoom) {
            if (!scrollWrap || typeof getZoom !== 'function' || typeof setZoom !== 'function' || typeof applyZoom !== 'function') return;

            let pinchRaf = null;
            function schedulePinchZoom(next) {
                const z = Math.min(2.5, Math.max(0.5, next));
                setZoom(z);
                if (pinchRaf !== null) return;
                pinchRaf = requestAnimationFrame(() => {
                    pinchRaf = null;
                    applyZoom();
                });
            }

            const pdfScrollTouchAction = 'pan-x pan-y';
            const hasCoarseTouch = (navigator.maxTouchPoints || 0) > 0;

            /** Pellizco con dos dedos vía touch (iOS/Android; gesture* de WebKit falla con viewport maximum-scale=1). */
            function attachTwoFingerTouchPinch() {
                let touchPinchStartDist = 0;
                let touchPinchStartZoom = 1;
                scrollWrap.addEventListener('touchstart', (e) => {
                    if (e.touches.length === 2) {
                        e.preventDefault();
                        scrollWrap.style.touchAction = 'none';
                        const dx = e.touches[0].clientX - e.touches[1].clientX;
                        const dy = e.touches[0].clientY - e.touches[1].clientY;
                        touchPinchStartDist = Math.max(Math.hypot(dx, dy), 8);
                        touchPinchStartZoom = getZoom();
                    }
                }, { passive: false });
                scrollWrap.addEventListener('touchmove', (e) => {
                    if (e.touches.length === 2 && touchPinchStartDist > 0) {
                        e.preventDefault();
                        const dx = e.touches[0].clientX - e.touches[1].clientX;
                        const dy = e.touches[0].clientY - e.touches[1].clientY;
                        const dist = Math.hypot(dx, dy);
                        const ratio = dist / touchPinchStartDist;
                        schedulePinchZoom(touchPinchStartZoom * ratio);
                    }
                }, { passive: false });
                scrollWrap.addEventListener('touchend', (e) => {
                    if (e.touches.length < 2) {
                        touchPinchStartDist = 0;
                        scrollWrap.style.touchAction = pdfScrollTouchAction;
                    }
                });
                scrollWrap.addEventListener('touchcancel', () => {
                    touchPinchStartDist = 0;
                    scrollWrap.style.touchAction = pdfScrollTouchAction;
                });
            }

            if (hasCoarseTouch) {
                attachTwoFingerTouchPinch();
                return;
            }

            const pointers = new Map();
            let pinchStartDist = 0;
            let pinchStartZoom = 1;

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

            function pointerDown(e) {
                if (e.pointerType !== 'touch') return;
                updatePointer(e);
                if (pointers.size === 2) {
                    scrollWrap.style.touchAction = 'none';
                    const pts = [...pointers.values()];
                    const dx = pts[0].x - pts[1].x;
                    const dy = pts[0].y - pts[1].y;
                    pinchStartDist = Math.max(Math.hypot(dx, dy), 8);
                    pinchStartZoom = getZoom();
                    try { e.preventDefault(); } catch (_) {}
                }
            }

            function pointerMove(e) {
                if (e.pointerType !== 'touch') return;
                if (!pointers.has(e.pointerId)) return;
                updatePointer(e);
                if (pointers.size === 2 && pinchStartDist > 0) {
                    const pts = [...pointers.values()];
                    const dx = pts[0].x - pts[1].x;
                    const dy = pts[0].y - pts[1].y;
                    const dist = Math.hypot(dx, dy);
                    const ratio = dist / pinchStartDist;
                    const next = pinchStartZoom * ratio;
                    schedulePinchZoom(next);
                    try { e.preventDefault(); } catch (_) {}
                }
            }

            function pointerUp(e) {
                pointers.delete(e.pointerId);
                if (pointers.size < 2) {
                    pinchStartDist = 0;
                    scrollWrap.style.touchAction = pdfScrollTouchAction;
                }
            }

            if (typeof window.PointerEvent !== 'undefined') {
                scrollWrap.addEventListener('pointerdown', pointerDown, { passive: false });
                scrollWrap.addEventListener('pointermove', pointerMove, { passive: false });
                scrollWrap.addEventListener('pointerup', pointerUp);
                scrollWrap.addEventListener('pointercancel', pointerUp);
            } else {
                attachTwoFingerTouchPinch();
            }
        }

        /** Zoom del PDF: preferir CSS `zoom` (sí afecta al scroll; `transform:scale` no y rompe zoom in). */
        function pdfLayerSupportsCssZoom() {
            return typeof CSS !== 'undefined' && CSS.supports && CSS.supports('zoom', '1');
        }

        const PDF_ZOOM_HUD_AUTOHIDE_MS = 3000;

        function schedulePdfZoomHudAutoHide(scrollWrap) {
            try {
                const root = scrollWrap && typeof scrollWrap.closest === 'function' ? scrollWrap.closest('.pdf-canvas-root') : null;
                const hud = root ? root.querySelector('.pdf-zoom-hud') : null;
                if (!hud) return;
                hud.classList.remove('pdf-zoom-hud--dim');
                const prevT = hud._pdfHudFadeTimer;
                if (prevT) clearTimeout(prevT);
                hud._pdfHudFadeTimer = setTimeout(() => {
                    hud._pdfHudFadeTimer = null;
                    hud.classList.add('pdf-zoom-hud--dim');
                }, PDF_ZOOM_HUD_AUTOHIDE_MS);
            } catch (_) {}
        }

        function updatePdfZoomHudUIFromScrollWrap(scrollWrap, zoom) {
            try {
                const root = scrollWrap && typeof scrollWrap.closest === 'function' ? scrollWrap.closest('.pdf-canvas-root') : null;
                const hud = root ? root.querySelector('.pdf-zoom-hud') : null;
                if (!hud) return;
                const valueEl = hud.querySelector('.pdf-zoom-hud-value');
                if (valueEl) valueEl.textContent = `${Math.round(zoom * 100)}%`;
                const rangeEl = hud.querySelector('.pdf-zoom-hud-range');
                if (rangeEl) {
                    const minP = parseFloat(rangeEl.min || '0');
                    const maxP = parseFloat(rangeEl.max || '100');
                    const p = Math.round(zoom * 100);
                    const clampedP = Math.max(minP, Math.min(maxP, p));
                    rangeEl.value = String(clampedP);
                }
            } catch (_) {}
        }

        function ensurePdfZoomHud(mountEl, pinchOpts) {
            try {
                if (!mountEl || !pinchOpts || !pinchOpts.hud || !pinchOpts.hud.enabled) return;
                const existing = mountEl.querySelector('.pdf-zoom-hud');
                if (existing) return;

                const minZoom = typeof pinchOpts.hud.minZoom === 'number' ? pinchOpts.hud.minZoom : 0.5;
                const maxZoom = typeof pinchOpts.hud.maxZoom === 'number' ? pinchOpts.hud.maxZoom : 2.5;
                const stepPercent = typeof pinchOpts.hud.stepPercent === 'number' ? pinchOpts.hud.stepPercent : 1;

                const getZoom = typeof pinchOpts.getZoom === 'function' ? pinchOpts.getZoom : () => 1;
                const z0 = typeof getZoom === 'function' ? getZoom() : 1;
                const p0 = Math.round(z0 * 100);

                mountEl.style.position = 'relative';

                const hud = document.createElement('div');
                hud.className = 'pdf-zoom-hud';
                hud.innerHTML = `
                    <div class="pdf-zoom-hud-row">
                        <div class="pdf-zoom-hud-value">${p0}%</div>
                        <button type="button" class="pdf-zoom-hud-reset" title="Restaurar zoom">Reset</button>
                    </div>
                    <input class="pdf-zoom-hud-range" type="range"
                        min="${Math.round(minZoom * 100)}"
                        max="${Math.round(maxZoom * 100)}"
                        step="${stepPercent}"
                        value="${Math.max(Math.round(minZoom * 100), Math.min(Math.round(maxZoom * 100), p0))}">
                `;

                const valueEl = hud.querySelector('.pdf-zoom-hud-value');
                const rangeEl = hud.querySelector('.pdf-zoom-hud-range');
                const resetBtn = hud.querySelector('.pdf-zoom-hud-reset');

                if (rangeEl) {
                    rangeEl.addEventListener('input', (e) => {
                        const p = parseFloat(e.target.value);
                        const z = p / 100;
                        if (valueEl) valueEl.textContent = `${Math.round(z * 100)}%`;
                        if (pinchOpts.hud && typeof pinchOpts.hud.onSetZoom === 'function') {
                            pinchOpts.hud.onSetZoom(z);
                        } else {
                            try {
                                if (typeof pinchOpts.setZoom === 'function') pinchOpts.setZoom(z);
                                if (typeof pinchOpts.applyZoom === 'function') pinchOpts.applyZoom();
                            } catch (_) {}
                        }
                    });
                }
                if (resetBtn) {
                    resetBtn.addEventListener('click', (e) => {
                        e.stopPropagation();
                        if (pinchOpts.hud && typeof pinchOpts.hud.onReset === 'function') {
                            pinchOpts.hud.onReset();
                        } else {
                            try {
                                if (typeof pinchOpts.setZoom === 'function') pinchOpts.setZoom(1);
                                if (typeof pinchOpts.applyZoom === 'function') pinchOpts.applyZoom();
                            } catch (_) {}
                        }
                    });
                }

                mountEl.appendChild(hud);
            } catch (_) {}
        }

        /**
         * @param {HTMLElement|null} scrollWrap
         * @param {number} zoom
         * @param {{ preserveScroll?: boolean, scheduleHudFade?: boolean }} [opts]
         */
        function applyPdfCanvasLayerZoom(scrollWrap, zoom, opts) {
            if (!scrollWrap) return;
            const zl = scrollWrap.querySelector('.pdf-canvas-zoom-layer');
            scrollWrap.style.zoom = '';
            if (!zl) return;
            const z = Math.min(2.5, Math.max(0.5, typeof zoom === 'number' && zoom > 0 ? zoom : 1));
            const preserveScroll = !opts || opts.preserveScroll !== false;
            const oldZ = parseFloat(scrollWrap.dataset.pdfZoom || '1') || 1;
            const zoomChanged = Math.abs(z - oldZ) > 0.001;

            const prevW = scrollWrap.scrollWidth;
            const prevH = scrollWrap.scrollHeight;
            const u = prevW > 0 ? (scrollWrap.scrollLeft + scrollWrap.clientWidth / 2) / prevW : 0.5;
            const v = prevH > 0 ? (scrollWrap.scrollTop + scrollWrap.clientHeight / 2) / prevH : 0.5;

            zl.style.transformOrigin = '';
            zl.style.transform = '';
            zl.style.width = '';

            if (pdfLayerSupportsCssZoom()) {
                zl.style.zoom = z === 1 ? '' : String(z);
            } else {
                zl.style.transformOrigin = 'top center';
                zl.style.transform = z !== 1 ? `scale(${z})` : '';
                zl.style.width = z !== 1 ? `${100 / z}%` : '';
            }

            scrollWrap.dataset.pdfZoom = String(z);
            updatePdfZoomHudUIFromScrollWrap(scrollWrap, z);
            if (zoomChanged || (opts && opts.scheduleHudFade)) {
                schedulePdfZoomHudAutoHide(scrollWrap);
            }

            if (!preserveScroll || !zoomChanged) return;
            requestAnimationFrame(() => {
                const nw = scrollWrap.scrollWidth;
                const nh = scrollWrap.scrollHeight;
                if (nw <= 0 || nh <= 0) return;
                scrollWrap.scrollLeft = Math.max(0, Math.min(u * nw - scrollWrap.clientWidth / 2, nw - scrollWrap.clientWidth));
                scrollWrap.scrollTop = Math.max(0, Math.min(v * nh - scrollWrap.clientHeight / 2, nh - scrollWrap.clientHeight));
            });
        }
        const PDFJS_VENDOR_BASE = (typeof BASE_URL !== 'undefined' ? BASE_URL : '') + 'assets/vendor/pdfjs/';
        let pdfJsLoadPromise = null;
        function ensurePdfJs() {
            if (pdfJsLoadPromise) return pdfJsLoadPromise;
            pdfJsLoadPromise = new Promise((resolve, reject) => {
                if (window.pdfjsLib) {
                    window.pdfjsLib.GlobalWorkerOptions.workerSrc = PDFJS_VENDOR_BASE + 'pdf.worker.min.js';
                    resolve(window.pdfjsLib);
                    return;
                }
                const s = document.createElement('script');
                s.src = PDFJS_VENDOR_BASE + 'pdf.min.js';
                s.onload = () => {
                    try {
                        const lib = window.pdfjsLib;
                        if (!lib) {
                            reject(new Error('pdf.js'));
                            return;
                        }
                        lib.GlobalWorkerOptions.workerSrc = PDFJS_VENDOR_BASE + 'pdf.worker.min.js';
                        resolve(lib);
                    } catch (e) { reject(e); }
                };
                s.onerror = () => reject(new Error('pdf.js load'));
                document.head.appendChild(s);
            });
            return pdfJsLoadPromise;
        }
        /** Marcador inicial hasta que PDF.js pinte las páginas. */
        function pdfCanvasMountPlaceholderHtml() {
            const wait = escapeHtml(tr('explorerPdfWait'));
            return `<div class="pdf-canvas-root" style="width:100%;height:100%;min-height:0;flex:1;align-self:stretch;display:flex;flex-direction:column;overflow:hidden;background:#3a3a3a;">
                <div class="pdf-canvas-loading" style="padding:16px;color:#ccc;text-align:center;display:flex;flex-direction:column;gap:10px;justify-content:center;flex:1;min-height:0;">
                    <div class="pdf-canvas-loading-line1" style="font-size:13px;opacity:0.92;">${wait}</div>
                    <div class="pdf-canvas-loading-line2 pdf-canvas-loading-pct" style="font-size:clamp(18px,4.5vw,26px);font-weight:700;letter-spacing:0.02em;">0%</div>
                </div>
            </div>`;
        }
        async function mountPdfCanvasViewer(mountEl, url, pinchOpts = null) {
            if (!mountEl || !url) return;
            const setPdfLoadingUi = (line1, pctLine) => {
                const l1 = mountEl.querySelector('.pdf-canvas-loading-line1');
                const l2 = mountEl.querySelector('.pdf-canvas-loading-pct');
                if (l1 && line1 != null) l1.textContent = line1;
                if (l2 && pctLine != null) l2.textContent = pctLine;
            };
            try {
                const pdfjsLib = await ensurePdfJs();
                const pdf = await pdfjsLib.getDocument({
                    url,
                    onProgress: (st) => {
                        const total = st && typeof st.total === 'number' ? st.total : 0;
                        const loaded = st && typeof st.loaded === 'number' ? st.loaded : 0;
                        if (total > 0) {
                            const pct = Math.min(100, Math.round((loaded / total) * 100));
                            setPdfLoadingUi(tr('explorerPdfDownloadProgress').replace('%p%', String(pct)), String(pct) + '%');
                        } else {
                            setPdfLoadingUi(tr('explorerPdfWait'), '…');
                        }
                    }
                }).promise;
                mountEl.innerHTML = '';
                mountEl.style.position = 'relative';
                const scroll = document.createElement('div');
                scroll.className = 'pdf-canvas-scroll-wrap';
                scroll.style.cssText = 'display:flex;flex-direction:column;align-items:center;overflow:auto;width:100%;height:100%;-webkit-overflow-scrolling:touch;touch-action:pan-x pan-y;padding:8px;box-sizing:border-box;';
                const zoomLayer = document.createElement('div');
                zoomLayer.className = 'pdf-canvas-zoom-layer';
                zoomLayer.style.cssText = 'display:block;width:100%;max-width:100%;box-sizing:border-box;text-align:center;';
                scroll.appendChild(zoomLayer);
                mountEl.appendChild(scroll);
                const dpr = Math.min(window.devicePixelRatio || 1, 2);
                const scale = 1.2 * dpr;
                const numPages = pdf.numPages || 0;
                const renderOverlay = document.createElement('div');
                renderOverlay.setAttribute('aria-hidden', 'true');
                renderOverlay.style.cssText = 'position:absolute;inset:0;display:flex;flex-direction:column;align-items:center;justify-content:center;background:#3a3a3a;z-index:4;pointer-events:none;';
                mountEl.appendChild(renderOverlay);
                const paintRenderOverlay = (pageNum) => {
                    const overallPct = numPages > 0 ? Math.round((pageNum / numPages) * 100) : 0;
                    renderOverlay.innerHTML = `<div style="color:#ccc;text-align:center;padding:16px;">
                        <div style="font-size:13px;opacity:0.92;">${escapeHtml(tr('explorerPdfRenderProgress').replace('%c%', String(pageNum)).replace('%t%', String(numPages)))}</div>
                        <div style="font-size:clamp(18px,4.5vw,26px);font-weight:700;margin-top:8px;">${overallPct}%</div>
                    </div>`;
                };
                for (let pageNum = 1; pageNum <= numPages; pageNum++) {
                    paintRenderOverlay(pageNum);
                    const page = await pdf.getPage(pageNum);
                    const viewport = page.getViewport({ scale });
                    const canvas = document.createElement('canvas');
                    const ctx = canvas.getContext('2d', { alpha: false });
                    canvas.width = viewport.width;
                    canvas.height = viewport.height;
                    canvas.style.cssText = 'max-width:100%;height:auto;display:block;margin:0 auto 12px;box-shadow:0 2px 8px rgba(0,0,0,0.45);';
                    const renderTask = page.render({ canvasContext: ctx, viewport });
                    await renderTask.promise;
                    zoomLayer.appendChild(canvas);
                }
                renderOverlay.remove();
                ensurePdfZoomHud(mountEl, pinchOpts);
                delete scroll.dataset.pdfZoom;
                if (pinchOpts && typeof pinchOpts.getZoom === 'function') {
                    applyPdfCanvasLayerZoom(scroll, pinchOpts.getZoom(), { preserveScroll: false, scheduleHudFade: true });
                } else if (pinchOpts && typeof pinchOpts.applyZoom === 'function') {
                    try { pinchOpts.applyZoom(); } catch (_) {}
                }
                if (pinchOpts && typeof pinchOpts.getZoom === 'function' && typeof pinchOpts.setZoom === 'function' && typeof pinchOpts.applyZoom === 'function') {
                    setupPdfPinchZoom(scroll, pinchOpts.getZoom, pinchOpts.setZoom, pinchOpts.applyZoom);
                }
            } catch (err) {
                console.error('PDF canvas:', err);
                mountEl.innerHTML = `<div style="padding:16px;color:#eee;text-align:center;">No se pudo mostrar el PDF aquí. <a href="${escapeHtmlAttr(url)}" target="_blank" rel="noopener" style="color:#8cf;">Abrir</a></div>`;
            }
        }
        function setPdfViewerContent(container, url, pinchOpts = null) {
            if (!container || !url) return;
            container.innerHTML = pdfCanvasMountPlaceholderHtml();
            const root = container.querySelector('.pdf-canvas-root');
            if (root) mountPdfCanvasViewer(root, url, pinchOpts);
        }

        function logActivity(type, data = {}) {
            try {
                const payload = { type: type, data: data, ts: Date.now() };
                if (navigator.sendBeacon) {
                    const blob = new Blob([JSON.stringify(payload)], { type: 'application/json' });
                    navigator.sendBeacon('?action=logEvent', blob);
                } else {
                    fetch('?action=logEvent', {
                        method: 'POST',
                        headers: { 'Content-Type': 'application/json' },
                        body: JSON.stringify(payload)
                    }).catch(() => {});
                }
            } catch (_) {}
        }
        async function shareSecureUrl(url, title = 'Compartir archivo', skipBuildSecure = false) {
            let secure = skipBuildSecure ? url : buildSecureFileUrl(url);
            try {
                if (!/^https?:\/\//i.test(secure)) {
                    // Rutas absolutas del sitio (/grid/share.php) deben resolverse solo contra el origen;
                    // si se concatenan con BASE_URL (.../grid/) se duplica el segmento (p. ej. grid/grid).
                    if (secure.startsWith('/')) {
                        secure = new URL(secure, window.location.origin).href;
                    } else {
                        secure = new URL(secure.replace(/^\//, ''), BASE_URL).href;
                    }
                }
            } catch (_) {}
            if (navigator.share) {
                await navigator.share({ title, text: title, url: secure });
            } else {
                await appDialog.prompt('URL segura para compartir:', secure);
            }
        }

        const UI_STRINGS = {
            es: {
                winSearch: 'Buscar en el menú...',
                f2Hint: 'F2: Editar seleccionada | Enter: Abrir | Supr: Eliminar (papelera) | Mayús+Supr: Borrar permanente | ESC: Cerrar | Flechas: Navegar',
                editApp: 'Editar App',
                uninstallApp: 'Desinstalar aplicación',
                pinTaskbar: 'Anclar a la barra de tareas',
                unpinTaskbar: 'Desanclar de la barra de tareas',
                addDesktop: 'Añadir al escritorio',
                removeDesktop: 'Quitar del escritorio',
                desktopAppAlreadyThere: 'Esta aplicación ya está en el escritorio',
                desktopToastAdded: 'Añadido al escritorio',
                desktopToastRemoved: 'Quitado del escritorio',
                removeShortcut: 'Eliminar acceso directo',
                open: 'Abrir',
                openNewWindow: 'Abrir nueva ventana',
                restoreAll: 'Restaurar todas',
                minimizeAll: 'Minimizar todas',
                closeAll: 'Cerrar todas las ventanas',
                openWindowsPreview: 'Ventanas abiertas',
                windowMinimized: 'Minimizada',
                windowActive: 'Activa',
                share: 'Compartir',
                download: 'Descargar',
                print: 'Imprimir',
                zoomIn: 'Acercar',
                zoomOut: 'Alejar',
                zoomReset: 'Restaurar zoom'
                ,
                minimizeWindow: 'Minimizar',
                maximizeRestoreWindow: 'Maximizar/Restaurar',
                fullscreenWindow: 'Pantalla completa',
                closeWindow: 'Cerrar',
                windowTitleIconReload: 'Recargar contenido de la ventana',
                closeWindowEscConfirmTitle: 'Cerrar ventana',
                closeWindowEscConfirmMessage: '¿Seguro que deseas cerrar «%title%»?',
                closeWindowEscConfirmYes: 'Sí, cerrar',
                closeWindowEscConfirmCancel: 'Cancelar',
                explorerUp: 'Subir',
                explorerNewFolder: 'Nueva carpeta',
                explorerUploadFiles: 'Subir archivos',
                explorerRefresh: 'Refrescar',
                explorerTrash: 'Papelera',
                explorerNoItems: 'No hay elementos.',
                explorerFolder: 'Carpeta',
                explorerRename: 'Renombrar',
                explorerCopy: 'Duplicar',
                explorerDelete: 'Eliminar',
                explorerConfirmDeleteToTrash: '¿Eliminar y mover a la papelera?',
                explorerConfirmDeleteTrashMany: '¿Eliminar %n elementos y moverlos a la papelera?',
                explorerConfirmDeletePermanent: '¿Eliminar permanentemente? No se enviará a la papelera.',
                explorerConfirmDeletePermanentMany: '¿Eliminar %n elementos permanentemente? No se enviarán a la papelera.',
                explorerRestore: 'Restaurar',
                explorerDeletePerm: 'Eliminar permanentemente',
                explorerPromptNewName: 'Nuevo nombre:',
                explorerRenameModalTitle: 'Renombrar',
                explorerRenameHintFile: 'Edita el nombre y la extensión por separado.',
                explorerRenameHintFolder: 'Introduce el nuevo nombre de la carpeta.',
                explorerRenameLabelBase: 'Nombre base',
                explorerRenameLabelFolder: 'Nombre',
                explorerRenameExtHelp: 'Escribe la extensión sin punto; se añade automáticamente.',
                explorerRenamePreviewLabel: 'Vista previa',
                explorerRenameCancel: 'Cancelar',
                explorerRenameSave: 'Aplicar',
                explorerPromptDestinationName: 'Nombre destino:',
                explorerPromptNewDirName: 'Nombre de la carpeta:',
                explorerNewFolderChoiceTitle: 'Carpeta creada',
                explorerNewFolderChoiceMessage: 'Se creó «%s». ¿Desea entrar en la carpeta o permanecer aquí?',
                explorerNewFolderChoiceEnter: 'Entrar',
                explorerNewFolderChoiceStay: 'Permanecer aquí',
                explorerLoading: 'Cargando...',
                explorerPdfWait: 'Cargando PDF…',
                explorerPdfDownloadProgress: 'Descargando %p%',
                explorerPdfRenderProgress: 'Página %c% de %t%',
                explorerNavCancel: 'Cancelar y volver',
                explorerOpeningRemote: 'Abriendo archivo…',
                explorerStatusOk: 'OK',
                explorerStatusError: 'Error',
                explorerRestoreFailed: 'No se pudo restaurar',
                recycleTrashEmpty: 'La papelera está vacía.',
                recycleRestore: 'Restaurar',
                recycleView: 'Ver',
                recycleDelete: 'Eliminar',
                recycleDeleteFailed: 'No se pudo eliminar',
                recycleEmptyConfirm: '¿Vaciar la papelera? Los elementos se borrarán de forma permanente y no podrán recuperarse.',
                recyclePreviewQuickNote: 'Nota rápida',
                recyclePreviewNoPreview: 'No hay vista previa para este elemento.',
                recyclePreviewExplorerShortcut: 'Acceso al explorador (restáuralo para abrirlo de nuevo).',
                recyclePreviewFolderEmpty: 'Carpeta vacía'
                ,
                backupLogsTitle: 'Backup y logs',
                exportBackupBtn: 'Descargar ZIP',
                importBackupBtn: 'Subir ZIP',
                openingsHistoryTitle: 'Historial de aperturas',
                refreshLogBtn: 'Actualizar log',
                clearLogBtn: 'Eliminar historial',
                rotateLogBtn: 'Crear log nuevo',
                formatReinstallTitle: 'Formateo y reinstalación',
                formatReinstallHint: 'Devuelve Flexia a su estado inicial y abre de nuevo el asistente de configuración. Solo puede usarlo el superadministrador.',
                formatModeLabel: 'Activar modo formateo (habilita el botón «Formatear»)',
                formatLaunchInstallBtn: 'Formatear y abrir instalador',
                formatReinstallConfirm1: '¿Formatear Flexia y empezar de nuevo? Se borrarán los ajustes del escritorio, los fondos, los logos y la configuración de usuarios. Esta acción no se puede deshacer.',
                formatReinstallConfirmTitle: 'Formatear Flexia',
                formatKeepUserDocsLabel: 'Conservar documentos de usuarios',
                formatKeepUserDocsHintOff: 'Desactivado: se borrarán todos los documentos del explorador (comunitarios, por departamento y de cada usuario).',
                formatKeepUserDocsHintOn: 'Activado: antes de formatear, los documentos se moverán a la carpeta de recuperación «flexia disk».',
                formatReinstallSuccess: 'Listo. Se abrirá el instalador.',
                formatReinstallError: 'No se pudo preparar la reinstalación.',
                recycleBinModalTitle: 'Papelera de reciclaje',
                emptyTrashBtn: 'Vaciar papelera',
                windowsMosaicCloseAllConfirm: '¿Cerrar todas las ventanas?',
                windowsMosaicTitle: 'Ventanas abiertas',
                closeAllWindowsBtn: 'Cerrar todo',
                windowDefaultTitle: 'Ventana',
                explorerConnections: 'Conexiones',
                explorerAddWebdav: 'Añadir WebDAV',
                explorerAddFtp: 'Añadir FTP',
                explorerPromptConnName: 'Nombre de la conexión:',
                explorerPromptWebdavBaseUrl: 'Base URL WebDAV:',
                explorerPromptWebdavUser: 'Usuario WebDAV (opcional):',
                explorerPromptWebdavPass: 'Password WebDAV (opcional):',
                explorerPromptFtpHost: 'Host FTP:',
                explorerPromptFtpPort: 'Puerto FTP (21):',
                explorerPromptFtpUser: 'Usuario FTP:',
                explorerPromptFtpPass: 'Password FTP:',
                explorerPromptFtpRoot: 'Root FTP (opcional):',
                explorerRemotePathLabel: 'Remoto',
                explorerNoConnection: 'Sin conexión',
                explorerRemoteLoadFailed: 'No se pudo cargar el archivo remoto',
                explorerConfirmDeleteRemoteDir: '¿Eliminar carpeta remota?',
                explorerConnTest: 'Conectar',
                explorerConnEdit: 'Editar',
                explorerConnDelete: 'Eliminar',
                explorerConnNewTab: 'Nueva pestaña remota',
                explorerConnOpenTab: 'Abrir',
                explorerConnTestOk: 'Conexión correcta',
                explorerConnTestFail: 'No se pudo conectar',
                explorerMoveFailed: 'No se pudo mover el elemento',
                explorerDropToMove: 'Suelta aquí para mover',
                explorerHome: 'Inicio',
                explorerSharedSpaceCommunityKind: 'Espacio comunitario',
                explorerSharedSpaceDepartmentKind: 'Departamento',
                explorerBack: 'Atrás',
                explorerForward: 'Adelante',
                explorerViewList: 'Vista de lista',
                explorerViewGrid: 'Vista de iconos',
                explorerViewMenu: 'Vista del explorador',
                explorerViewIconsXL: 'Iconos extra grandes',
                explorerViewIconsLG: 'Iconos grandes',
                explorerViewIconsMD: 'Iconos medianos',
                explorerViewIconsSM: 'Iconos pequeños',
                explorerViewDetails: 'Detalles',
                explorerViewContent: 'Contenido',
                explorerViewTiles: 'Mosaicos',
                explorerViewCompact: 'Compacta (técnica)',
                explorerViewSplitPreview: 'Dividida (vista previa a la derecha)',
                explorerViewSplitPreviewLeft: 'Dividida (vista previa a la izquierda)',
                explorerViewSplitPreviewTop: 'Dividida (vista previa arriba)',
                explorerViewSplitPreviewBottom: 'Dividida (vista previa abajo)',
                explorerViewSplitPreviewHScroll: 'Dividida (navegación horizontal + vista previa)',
                explorerViewMultimedia: 'Multimedia',
                explorerProperties: 'Propiedades',
                explorerPreview: 'Vista previa',
                explorerHtmlPreviewShowCode: 'Ver código',
                explorerHtmlPreviewShowWeb: 'Ver vista web',
                explorerHtmlPreviewTextUnavailable: 'El código no está disponible (archivo demasiado grande para cargarlo en el visor).',
                explorerPreviewUnsupported: 'Vista previa no disponible para este tipo.',
                explorerColName: 'Nombre',
                explorerColType: 'Tipo',
                explorerColSize: 'Tamaño',
                explorerColModified: 'Modificación',
                explorerColCreated: 'Creación / metadatos',
                explorerColExt: 'Extensión',
                explorerColAuthor: 'Autor',
                explorerColPages: 'Páginas',
                explorerFolderContains: 'Contiene',
                explorerFolderFiles: 'archivos',
                explorerFolderSubfolders: 'carpetas',
                explorerSelectForProps: 'Seleccione un elemento para ver sus propiedades.',
                explorerPropPath: 'Ruta',
                explorerPropMime: 'MIME',
                explorerContentSnippet: 'Vista resumida',
                explorerSplitResizeHint: 'Arrastrar para ajustar el ancho de la vista previa',
                explorerTabLocal: 'Mis documentos',
                explorerTabClose: 'Cerrar pestaña',
                explorerSidebarTitle: 'Mis documentos',
                explorerSharedSpaceFilesKind: 'Mis documentos',
                explorerSidebarOpen: 'Mostrar carpetas',
                explorerSidebarClose: 'Ocultar panel de carpetas',
                explorerToolbarMoreActions: 'Más acciones del explorador',
                explorerToolbarCollapseActions: 'Ocultar acciones',
                explorerToolbarPinBar: 'Fijar barra (mostrar siempre todas las acciones)',
                explorerToolbarUnpinBar: 'Desfijar barra (ahorrar espacio)',
                explorerPaste: 'Pegar',
                explorerClipCopy: 'Copiar',
                explorerShareSecure: 'Compartir',
                explorerShareFailed: 'No se pudo generar el enlace de compartición',
                explorerClipCut: 'Cortar',
                explorerSelectAll: 'Seleccionar todo',
                explorerMultiSelectMode: 'Selección múltiple',
                explorerMultiSelectExit: 'Salir de selección múltiple',
                explorerSelectedCount: '%n seleccionado(s)',
                explorerSearchPlaceholder: 'Buscar…',
                explorerSearchGlobal: 'Todo',
                explorerSearchGlobalHint: 'Buscar en todas las carpetas',
                explorerSearchRecursiveHint: 'Busca por nombre en todo el árbol de carpetas (local y conexión remota)',
                explorerSearchClear: 'Limpiar búsqueda',
                explorerExitSearch: 'Salir de la búsqueda',
                explorerTreeFilterPlaceholder: 'Filtrar carpetas…',
                explorerSearchNoResults: 'Sin resultados',
                explorerSearching: 'Buscando…',
                explorerPasting: 'Pegando…',
                explorerConflictIntro: 'Ya existe "%s" en el destino.',
                explorerConflictModalTitle: 'Nombre duplicado',
                explorerConflictApplyAllLabel: 'Aplicar la misma acción a los siguientes conflictos',
                explorerConflictReplaceBtn: 'Reemplazar el existente',
                explorerConflictMergeBtn: 'Fusionar carpetas',
                explorerConflictRenameBtn: 'Copiar con otro nombre',
                explorerConflictSkipBtn: 'Omitir este elemento',
                explorerConflictCancelBtn: 'Cancelar operación',
                explorerConflictRenameOk: 'Usar este nombre',
                explorerConflictRenameBack: 'Volver',
                explorerConflictNewName: 'Nuevo nombre:',
                explorerUndoConfirm: '¿Desea deshacer los cambios del último pegado u operación?',
                explorerUndoDone: 'Cambios deshechos',
                explorerSearchResults: 'resultados',
                nextWallpaper: 'Cambiar al siguiente fondo de pantalla',
                wallpaperApplyDesktop: 'Aplicar como fondo de escritorio',
                wallpaperApplyDesktopDone: 'Fondo de escritorio aplicado',
                wallpaperAddToListVideos: 'Añadir a lista de fondo',
                wallpaperSetFixed: 'Fijar de fondo de pantalla',
                wallpaperSetFixedDone: 'Fondo de pantalla fijado',
                wallpaperAddToListImages: 'Añadir a lista de fondos de pantalla',
                wallpaperAddedToListVideos: 'Vídeo añadido a la lista de fondos',
                wallpaperAddedToListImages: 'Imagen añadida a la lista de fondos',
                wallpaperAlreadyInList: 'Este archivo ya está en la lista de fondos',
                wallpaperRemoveFromListConfirm: '¿Quitar este archivo de la lista de fondos? (no se eliminará del explorador)',
                explorerStorageTabShort: 'Caché y papelera',
                explorerStorageTabTitle: 'Explorador: caché remota y papelera',
                explorerStorageEnvironment: 'Entorno detectado',
                explorerEnvXampp: 'SINAPSYS TECNOLOGY',
                explorerEnvWindows: 'Windows (servidor o local)',
                explorerEnvUnix: 'Hosting Linux / Unix',
                explorerEnvUnknown: 'No identificado',
                explorerLabelRemoteCache: 'Caché remota (remote-cache)',
                explorerLabelTrash: 'Papelera (trash)',
                explorerLabelLocalFiles: 'Archivos locales (files)',
                explorerLabelTotalExplorer: 'Total carpeta explorador',
                explorerFilesLabel: 'archivos',
                explorerChartPieTitle: 'Distribución del disco (partición)',
                explorerChartPieTitleExplorerOnly: 'Distribución (carpeta explorador)',
                explorerLabelDiskFreePie: 'Espacio libre',
                explorerLabelDiskOtherPie: 'Otro uso del disco',
                explorerChartDiskTitle: 'Disco del proyecto (partición del servidor)',
                explorerDiskUnavailable: 'No disponible: el hosting puede ocultar el espacio en disco.',
                explorerDiskUsed: 'usado',
                explorerDiskFree: 'Libre',
                explorerStorageNoData: 'Sin datos',
                explorerStorageLoadError: 'No se pudieron cargar las estadísticas.',
                explorerStorageProgressPreparing: 'Iniciando análisis de carpetas…',
                explorerStorageProgressMeta: 'Leyendo volumen (espacio en disco)…',
                explorerStorageProgressRemote: 'Recorriendo caché remota (remote-cache)…',
                explorerStorageProgressTrash: 'Recorriendo papelera (trash)…',
                explorerStorageProgressFiles: 'Recorriendo archivos locales',
                explorerStorageProgressRendering: 'Componiendo tablas y gráficas…',
                explorerStorageFromCache: 'Caché local (sin recorrer ahora las carpetas)',
                explorerStorageCacheLine: 'Los tamaños de carpetas se guardan unos minutos. El espacio en disco y el entorno se actualizan ahora. Pulsa «Actualizar medidas» para un recuento completo del explorador.',
                explorerStorageContacting: 'Consultando al servidor (si no hay caché, puede tardar)…',
                explorerBtnEmptyRemoteCache: 'Vaciar caché remota',
                explorerBtnEmptyExplorerTrash: 'Vaciar papelera del explorador',
                explorerBtnRefreshStats: 'Actualizar medidas',
                explorerStorageShortcutFiles: 'Abrir biblioteca local (Archivos)',
                explorerStorageShortcutCache: 'Abrir caché remota',
                explorerStorageShortcutTrash: 'Abrir papelera del explorador',
                explorerStorageShortcutsHint: 'Abre el explorador en cada ubicación para mover o recuperar archivos.',
                diskRecoveryTabShort: 'Archivos en disco',
                diskRecoveryFormatCrossHint: 'La misma opción sigue disponible en la pestaña «Seguridad».',
                systemCreditsTabShort: 'Créditos y hosting',
                systemCreditsMainTitle: 'Propiedad y aviso legal',
                systemCreditsOwner: 'El licenciamiento de este software pertenece a Quadrata. Sinapsys DevOps es el proveedor exclusivo autorizado para modificaciones al código interno.',
                systemCreditsPrivacyTitle: 'Privacidad y seguridad de los datos',
                systemCreditsPrivacyBody: 'Flexia no se hace responsable de las vulnerabilidades relativas a los datos almacenados o tratados con este software. La confidencialidad, integridad y disponibilidad de la información dependen al 100 % de la seguridad del hosting, de la configuración del servidor y de las buenas prácticas del operador. Elija un proveedor de alojamiento fiable, mantenga el sistema actualizado, use HTTPS y realice copias de seguridad periódicas.',
                systemCreditsHostingHeading: 'Información del servidor (consulta en tiempo real)',
                systemCreditsHostingLoading: 'Consultando al servidor…',
                systemCreditsHostingError: 'No se pudo obtener la información del hosting.',
                systemCreditsLogoMissing: 'En la carpeta logos_sinapsys: el primer PNG distinto de logo_cliente.png se muestra arriba (Sinapsys); logo_cliente.png se muestra en la licencia debajo del texto legal.',
                systemCreditsHostHttp: 'Host HTTP',
                systemCreditsServerSoftware: 'Software del servidor',
                systemCreditsPhpVersion: 'Versión PHP',
                systemCreditsPhpSapi: 'Interfaz PHP (SAPI)',
                systemCreditsOsFamily: 'Familia de SO (PHP)',
                systemCreditsEnvironment: 'Tipo de entorno',
                systemCreditsDisk: 'Disco del proyecto',
                systemCreditsPhpMemory: 'Memoria PHP (pico / límite)',
                systemCreditsLoadAvg: 'Carga del sistema (1 min)',
                systemCreditsServerTime: 'Marca de tiempo (servidor)',
                systemCreditsHttps: 'HTTPS activo',
                systemCreditsTrafficGlobalToday: 'Tráfico Flexia hoy (servidor, global)',
                systemCreditsTrafficGlobalTotal: 'Tráfico Flexia acumulado (servidor, global)',
                systemCreditsTrafficUserToday: 'Tu tráfico Flexia hoy (servidor)',
                systemCreditsTrafficUserTotal: 'Tu tráfico Flexia acumulado (servidor)',
                systemCreditsTrafficScopeNote: 'Solo peticiones HTTP de Flexia (API); no es el ancho de banda total del VPS.',
                systemCreditsHostingPanel: 'Panel del proveedor de hosting',
                systemCreditsAlerts: 'Avisos',
                explorerConfirmEmptyRemoteCache: '¿Borrar todo el contenido de media/explorer/remote-cache? Los archivos remotos se volverán a descargar al abrirlos.',
                explorerConfirmEmptyTrashExplorer: '¿Vaciar la papelera del explorador? Los archivos se borrarán de forma permanente y no podrán recuperarse.',
                explorerBtnClearAssetCache: 'Borrar caché de CSS/JS',
                explorerConfirmClearAssetCache: 'Esto borrará la caché del navegador (CSS/JS) para este sitio, forzará la recarga de los assets de SkechIA (boceto) y volverá a cargar la página.\n\n¿Continuar?',
                explorerOperationFailed: 'No se pudo completar la operación.',
                explorerShortcutCategory: 'Biblioteca',
                explorerShortcutPinTaskbar: 'Anclar a la barra de tareas',
                explorerShortcutAddDesktop: 'Añadir al escritorio',
                explorerShortcutAddStartMenu: 'Añadir al menú Inicio',
                explorerShortcutAddedStartMenu: 'Acceso añadido al menú Inicio',
                explorerShortcutAlreadyInApps: 'Este elemento ya está en aplicaciones',
                explorerShortcutFolderHint: 'Acceso directo a carpeta en la biblioteca',
                launcherFolderColor: 'Color'
            },
            en: {
                winSearch: 'Search in menu...',
                f2Hint: 'F2: Edit selected | Enter: Open | Del: Delete (recycle bin) | Shift+Del: Permanent delete | ESC: Close | Arrows: Navigate',
                editApp: 'Edit app',
                uninstallApp: 'Uninstall application',
                pinTaskbar: 'Pin to taskbar',
                unpinTaskbar: 'Unpin from taskbar',
                addDesktop: 'Add to desktop',
                removeDesktop: 'Remove from desktop',
                desktopAppAlreadyThere: 'This app is already on the desktop',
                desktopToastAdded: 'Added to desktop',
                desktopToastRemoved: 'Removed from desktop',
                removeShortcut: 'Remove shortcut',
                open: 'Open',
                openNewWindow: 'Open new window',
                restoreAll: 'Restore all',
                minimizeAll: 'Minimize all',
                closeAll: 'Close all windows',
                openWindowsPreview: 'Open windows',
                windowMinimized: 'Minimized',
                windowActive: 'Active',
                share: 'Share',
                download: 'Download',
                print: 'Print',
                zoomIn: 'Zoom in',
                zoomOut: 'Zoom out',
                zoomReset: 'Reset zoom'
                ,
                minimizeWindow: 'Minimize',
                maximizeRestoreWindow: 'Maximize/Restore',
                fullscreenWindow: 'Fullscreen',
                closeWindow: 'Close',
                windowTitleIconReload: 'Reload window content',
                closeWindowEscConfirmTitle: 'Close window',
                closeWindowEscConfirmMessage: 'Are you sure you want to close «%title%»?',
                closeWindowEscConfirmYes: 'Yes, close',
                closeWindowEscConfirmCancel: 'Cancel',
                explorerUp: 'Up',
                explorerNewFolder: 'New folder',
                explorerUploadFiles: 'Upload files',
                explorerRefresh: 'Refresh',
                explorerTrash: 'Recycle Bin',
                explorerNoItems: 'No items.',
                explorerFolder: 'Folder',
                explorerRename: 'Rename',
                explorerCopy: 'Duplicate',
                explorerDelete: 'Delete',
                explorerConfirmDeleteToTrash: 'Delete and move to the recycle bin?',
                explorerConfirmDeleteTrashMany: 'Delete %n items and move them to the recycle bin?',
                explorerConfirmDeletePermanent: 'Delete permanently? This will not go to the recycle bin.',
                explorerConfirmDeletePermanentMany: 'Delete %n items permanently? They will not go to the recycle bin.',
                explorerRestore: 'Restore',
                explorerDeletePerm: 'Delete permanently',
                explorerPromptNewName: 'New name:',
                explorerRenameModalTitle: 'Rename',
                explorerRenameHintFile: 'Edit the base name and extension separately.',
                explorerRenameHintFolder: 'Enter the new folder name.',
                explorerRenameLabelBase: 'Base name',
                explorerRenameLabelFolder: 'Name',
                explorerRenameExtHelp: 'Type the extension without a leading dot; it is added automatically.',
                explorerRenamePreviewLabel: 'Preview',
                explorerRenameCancel: 'Cancel',
                explorerRenameSave: 'Apply',
                explorerPromptDestinationName: 'Destination name:',
                explorerPromptNewDirName: 'Folder name:',
                explorerNewFolderChoiceTitle: 'Folder created',
                explorerNewFolderChoiceMessage: '«%s» was created. Open the folder or stay here?',
                explorerNewFolderChoiceEnter: 'Open folder',
                explorerNewFolderChoiceStay: 'Stay here',
                explorerLoading: 'Loading...',
                explorerPdfWait: 'Loading PDF…',
                explorerPdfDownloadProgress: 'Downloading %p%',
                explorerPdfRenderProgress: 'Page %c% of %t%',
                explorerNavCancel: 'Cancel and go back',
                explorerOpeningRemote: 'Opening file…',
                explorerStatusOk: 'OK',
                explorerStatusError: 'Error',
                explorerRestoreFailed: 'Could not restore',
                recycleTrashEmpty: 'Recycle bin is empty.',
                recycleRestore: 'Restore',
                recycleView: 'View',
                recycleDelete: 'Delete',
                recycleDeleteFailed: 'Could not delete',
                recycleEmptyConfirm: 'Empty the recycle bin? Items will be permanently deleted and cannot be recovered.',
                recyclePreviewQuickNote: 'Quick note',
                recyclePreviewNoPreview: 'No preview available for this item.',
                recyclePreviewExplorerShortcut: 'Explorer shortcut (restore it to open again).',
                recyclePreviewFolderEmpty: 'Empty folder'
                ,
                backupLogsTitle: 'Backup and logs',
                exportBackupBtn: 'Download ZIP',
                importBackupBtn: 'Upload ZIP',
                openingsHistoryTitle: 'Openings history',
                refreshLogBtn: 'Refresh log',
                clearLogBtn: 'Clear history',
                rotateLogBtn: 'Create new log',
                formatReinstallTitle: 'Factory reset & reinstall',
                formatReinstallHint: 'Restores Flexia to its initial state and opens the setup wizard again. Super administrator only.',
                formatModeLabel: 'Enable format mode (shows the Format button)',
                formatLaunchInstallBtn: 'Format and open installer',
                formatReinstallConfirm1: 'Format Flexia and start over? Desktop settings, wallpapers, logos, and user accounts will be removed. This cannot be undone.',
                formatReinstallConfirmTitle: 'Format Flexia',
                formatKeepUserDocsLabel: 'Keep user documents',
                formatKeepUserDocsHintOff: 'Off: all explorer documents will be deleted (community, departments, and per-user files).',
                formatKeepUserDocsHintOn: 'On: documents will be moved to the «flexia disk» recovery folder before formatting.',
                formatReinstallSuccess: 'Done. Opening the installer.',
                formatReinstallError: 'Could not prepare reinstall.',
                recycleBinModalTitle: 'Recycle bin',
                emptyTrashBtn: 'Empty recycle bin',
                windowsMosaicCloseAllConfirm: 'Close all windows?',
                windowsMosaicTitle: 'Open windows',
                closeAllWindowsBtn: 'Close all',
                windowDefaultTitle: 'Window',
                explorerConnections: 'Connections',
                explorerAddWebdav: 'Add WebDAV',
                explorerAddFtp: 'Add FTP',
                explorerPromptConnName: 'Connection name:',
                explorerPromptWebdavBaseUrl: 'WebDAV base URL:',
                explorerPromptWebdavUser: 'WebDAV username (optional):',
                explorerPromptWebdavPass: 'WebDAV password (optional):',
                explorerPromptFtpHost: 'FTP host:',
                explorerPromptFtpPort: 'FTP port (21):',
                explorerPromptFtpUser: 'FTP username:',
                explorerPromptFtpPass: 'FTP password:',
                explorerPromptFtpRoot: 'FTP root (optional):',
                explorerRemotePathLabel: 'Remote',
                explorerNoConnection: 'No connection selected',
                explorerRemoteLoadFailed: 'Failed to load remote file',
                explorerConfirmDeleteRemoteDir: 'Delete remote directory?',
                explorerConnTest: 'Connect',
                explorerConnEdit: 'Edit',
                explorerConnDelete: 'Delete',
                explorerConnNewTab: 'New remote tab',
                explorerConnOpenTab: 'Open',
                explorerConnTestOk: 'Connection OK',
                explorerConnTestFail: 'Connection failed',
                explorerMoveFailed: 'Could not move item',
                explorerDropToMove: 'Drop here to move',
                explorerHome: 'Home',
                explorerSharedSpaceCommunityKind: 'Community space',
                explorerSharedSpaceDepartmentKind: 'Department',
                explorerBack: 'Back',
                explorerForward: 'Forward',
                explorerViewList: 'List view',
                explorerViewGrid: 'Icon view',
                explorerViewMenu: 'Explorer layout',
                explorerViewIconsXL: 'Extra large icons',
                explorerViewIconsLG: 'Large icons',
                explorerViewIconsMD: 'Medium icons',
                explorerViewIconsSM: 'Small icons',
                explorerViewDetails: 'Details',
                explorerViewContent: 'Content',
                explorerViewTiles: 'Tiles',
                explorerViewCompact: 'Compact (technical)',
                explorerViewSplitPreview: 'Split (preview on the right)',
                explorerViewSplitPreviewLeft: 'Split (preview on the left)',
                explorerViewSplitPreviewTop: 'Split (preview on top)',
                explorerViewSplitPreviewBottom: 'Split (preview on bottom)',
                explorerViewSplitPreviewHScroll: 'Split (horizontal strip + preview)',
                explorerViewMultimedia: 'Multimedia',
                explorerProperties: 'Properties',
                explorerPreview: 'Preview',
                explorerHtmlPreviewShowCode: 'View source',
                explorerHtmlPreviewShowWeb: 'View web page',
                explorerHtmlPreviewTextUnavailable: 'Source is not available (file too large to load in the viewer).',
                explorerPreviewUnsupported: 'Preview not available for this file type.',
                explorerColName: 'Name',
                explorerColType: 'Type',
                explorerColSize: 'Size',
                explorerColModified: 'Modified',
                explorerColCreated: 'Created / metadata',
                explorerColExt: 'Extension',
                explorerColAuthor: 'Author',
                explorerColPages: 'Pages',
                explorerFolderContains: 'Contains',
                explorerFolderFiles: 'files',
                explorerFolderSubfolders: 'folders',
                explorerSelectForProps: 'Select an item to see its properties.',
                explorerPropPath: 'Path',
                explorerPropMime: 'MIME',
                explorerContentSnippet: 'Summary',
                explorerSplitResizeHint: 'Drag to resize the preview pane',
                explorerTabLocal: 'My documents',
                explorerTabClose: 'Close tab',
                explorerSidebarTitle: 'My documents',
                explorerSharedSpaceFilesKind: 'My documents',
                explorerSidebarOpen: 'Show folders',
                explorerSidebarClose: 'Hide folder panel',
                explorerToolbarMoreActions: 'More explorer actions',
                explorerToolbarCollapseActions: 'Hide actions',
                explorerToolbarPinBar: 'Pin toolbar (always show all actions)',
                explorerToolbarUnpinBar: 'Unpin toolbar (save space)',
                explorerPaste: 'Paste',
                explorerClipCopy: 'Copy',
                explorerShareSecure: 'Share',
                explorerShareFailed: 'Could not generate share link',
                explorerClipCut: 'Cut',
                explorerSelectAll: 'Select all',
                explorerMultiSelectMode: 'Multiple selection',
                explorerMultiSelectExit: 'Exit multiple selection',
                explorerSelectedCount: '%n selected',
                explorerSearchPlaceholder: 'Search…',
                explorerSearchGlobal: 'All',
                explorerSearchGlobalHint: 'Search entire library',
                explorerSearchRecursiveHint: 'Search by name across the whole folder tree (local and remote)',
                explorerSearchClear: 'Clear search',
                explorerExitSearch: 'Exit search',
                explorerTreeFilterPlaceholder: 'Filter folders…',
                explorerSearchNoResults: 'No results',
                explorerSearching: 'Searching…',
                explorerPasting: 'Pasting…',
                explorerConflictIntro: 'A file or folder named "%s" already exists here.',
                explorerConflictModalTitle: 'Duplicate name',
                explorerConflictApplyAllLabel: 'Apply the same action to further conflicts',
                explorerConflictReplaceBtn: 'Replace existing',
                explorerConflictMergeBtn: 'Merge folders',
                explorerConflictRenameBtn: 'Copy with a different name',
                explorerConflictSkipBtn: 'Skip this item',
                explorerConflictCancelBtn: 'Cancel operation',
                explorerConflictRenameOk: 'Use this name',
                explorerConflictRenameBack: 'Back',
                explorerConflictNewName: 'New name:',
                explorerUndoConfirm: 'Undo the last paste or operation?',
                explorerUndoDone: 'Undone',
                explorerSearchResults: 'results',
                nextWallpaper: 'Change to next wallpaper',
                wallpaperApplyDesktop: 'Apply as desktop wallpaper',
                wallpaperApplyDesktopDone: 'Desktop wallpaper applied',
                wallpaperAddToListVideos: 'Add to background list',
                wallpaperSetFixed: 'Set as wallpaper',
                wallpaperSetFixedDone: 'Wallpaper set',
                wallpaperAddToListImages: 'Add to wallpaper list',
                wallpaperAddedToListVideos: 'Video added to background list',
                wallpaperAddedToListImages: 'Image added to wallpaper list',
                wallpaperAlreadyInList: 'This file is already in the background list',
                wallpaperRemoveFromListConfirm: 'Remove this file from the background list? (it will not be deleted from the explorer)',
                explorerStorageTabShort: 'Cache & trash',
                explorerStorageTabTitle: 'Explorer: remote cache and trash',
                explorerStorageEnvironment: 'Detected environment',
                explorerEnvXampp: 'SINAPSYS TECNOLOGY',
                explorerEnvWindows: 'Windows (server or local)',
                explorerEnvUnix: 'Linux / Unix hosting',
                explorerEnvUnknown: 'Unknown',
                explorerLabelRemoteCache: 'Remote cache (remote-cache)',
                explorerLabelTrash: 'Trash',
                explorerLabelLocalFiles: 'Local files (files)',
                explorerLabelTotalExplorer: 'Explorer folder total',
                explorerFilesLabel: 'files',
                explorerChartPieTitle: 'Disk space (partition)',
                explorerChartPieTitleExplorerOnly: 'Breakdown (explorer folder only)',
                explorerLabelDiskFreePie: 'Free space',
                explorerLabelDiskOtherPie: 'Other disk usage',
                explorerChartDiskTitle: 'Server partition (project disk)',
                explorerDiskUnavailable: 'Unavailable: hosting may hide disk space.',
                explorerDiskUsed: 'used',
                explorerDiskFree: 'Free',
                explorerStorageNoData: 'No data',
                explorerStorageLoadError: 'Could not load statistics.',
                explorerStorageProgressPreparing: 'Starting folder analysis…',
                explorerStorageProgressMeta: 'Reading volume (disk space)…',
                explorerStorageProgressRemote: 'Scanning remote cache (remote-cache)…',
                explorerStorageProgressTrash: 'Scanning trash…',
                explorerStorageProgressFiles: 'Scanning local files',
                explorerStorageProgressRendering: 'Building tables and charts…',
                explorerStorageFromCache: 'Local cache (no full folder walk)',
                explorerStorageCacheLine: 'Folder sizes are remembered for a few minutes. Disk and environment are refreshed. Use “Refresh stats” to recount everything.',
                explorerStorageContacting: 'Contacting server (first scan can take a while)…',
                explorerBtnEmptyRemoteCache: 'Empty remote cache',
                explorerBtnEmptyExplorerTrash: 'Empty explorer trash',
                explorerBtnRefreshStats: 'Refresh stats',
                explorerStorageShortcutFiles: 'Open local library (Files)',
                explorerStorageShortcutCache: 'Open remote cache',
                explorerStorageShortcutTrash: 'Open explorer recycle bin',
                explorerStorageShortcutsHint: 'Opens the file explorer at each location to move or restore files.',
                diskRecoveryTabShort: 'Files on disk',
                diskRecoveryFormatCrossHint: 'The same option remains under the «Security» tab.',
                systemCreditsTabShort: 'Credits & hosting',
                systemCreditsMainTitle: 'Ownership & legal notice',
                systemCreditsOwner: 'Software licensing belongs to Quadrata. Sinapsys DevOps is the exclusive authorized vendor for modifications to the internal source code.',
                systemCreditsPrivacyTitle: 'Privacy and data security',
                systemCreditsPrivacyBody: 'Flexia is not liable for vulnerabilities affecting data stored or processed with this software. Confidentiality, integrity, and availability depend entirely on hosting security, server configuration, and the operator’s practices. Choose a reliable host, keep the stack patched, use HTTPS, and maintain regular backups.',
                systemCreditsHostingHeading: 'Server information (live query)',
                systemCreditsHostingLoading: 'Contacting server…',
                systemCreditsHostingError: 'Could not load hosting information.',
                systemCreditsLogoMissing: 'In logos_sinapsys: the first PNG other than logo_cliente.png is shown at the top (Sinapsys); logo_cliente.png is shown in the license section below the legal text.',
                systemCreditsHostHttp: 'HTTP host',
                systemCreditsServerSoftware: 'Server software',
                systemCreditsPhpVersion: 'PHP version',
                systemCreditsPhpSapi: 'PHP SAPI',
                systemCreditsOsFamily: 'OS family (PHP)',
                systemCreditsEnvironment: 'Environment type',
                systemCreditsDisk: 'Project disk',
                systemCreditsPhpMemory: 'PHP memory (peak / limit)',
                systemCreditsLoadAvg: 'Load average (1 min)',
                systemCreditsServerTime: 'Server timestamp',
                systemCreditsHttps: 'HTTPS active',
                systemCreditsTrafficGlobalToday: 'Flexia traffic today (server, global)',
                systemCreditsTrafficGlobalTotal: 'Flexia traffic total (server, global)',
                systemCreditsTrafficUserToday: 'Your Flexia traffic today (server)',
                systemCreditsTrafficUserTotal: 'Your Flexia traffic total (server)',
                systemCreditsTrafficScopeNote: 'Flexia HTTP requests only (API); not total VPS bandwidth.',
                systemCreditsHostingPanel: 'Hosting provider panel',
                systemCreditsAlerts: 'Alerts',
                explorerConfirmEmptyRemoteCache: 'Delete all of media/explorer/remote-cache? Remote files will download again when opened.',
                explorerConfirmEmptyTrashExplorer: 'Empty the explorer recycle bin? Files will be permanently deleted and cannot be recovered.',
                explorerBtnClearAssetCache: 'Clear CSS/JS cache',
                explorerConfirmClearAssetCache: 'This will clear the browser cache (CSS/JS) for this site, force-reload SkechIA (sketch app) assets, and reload the page.\n\nContinue?',
                explorerOperationFailed: 'Operation failed.',
                explorerShortcutCategory: 'Library',
                explorerShortcutPinTaskbar: 'Pin to taskbar',
                explorerShortcutAddDesktop: 'Add to desktop',
                explorerShortcutAddStartMenu: 'Add to Start menu',
                explorerShortcutAddedStartMenu: 'Shortcut added to the Start menu',
                explorerShortcutAlreadyInApps: 'This item is already in your apps',
                explorerShortcutFolderHint: 'Folder shortcut in library',
                launcherFolderColor: 'Color'
            },
            fr: {
                winSearch: 'Rechercher dans le menu...',
                f2Hint: 'F2: Modifier la sélection | Entrée: Ouvrir | Suppr: Supprimer',
                editApp: "Modifier l'app",
                uninstallApp: "Désinstaller l'application",
                pinTaskbar: 'Épingler à la barre',
                unpinTaskbar: 'Désépingler de la barre',
                addDesktop: 'Ajouter au bureau',
                removeDesktop: 'Retirer du bureau',
                desktopAppAlreadyThere: 'Cette application est déjà sur le bureau',
                desktopToastAdded: 'Ajouté au bureau',
                desktopToastRemoved: 'Retiré du bureau',
                open: 'Ouvrir',
                share: 'Partager',
                download: 'Télécharger'
            },
            de: {
                winSearch: 'Im Menü suchen...',
                f2Hint: 'F2: Bearbeiten | Enter: Öffnen | Entf: Löschen | ESC: Schließen',
                editApp: 'App bearbeiten',
                uninstallApp: 'Anwendung deinstallieren',
                pinTaskbar: 'An Taskleiste anheften',
                unpinTaskbar: 'Von Taskleiste lösen',
                addDesktop: 'Zum Desktop hinzufügen',
                removeDesktop: 'Vom Desktop entfernen',
                desktopAppAlreadyThere: 'Diese App ist bereits auf dem Desktop',
                desktopToastAdded: 'Zum Desktop hinzugefügt',
                desktopToastRemoved: 'Vom Desktop entfernt',
                open: 'Öffnen',
                share: 'Teilen',
                download: 'Herunterladen'
            },
            pt: {
                winSearch: 'Pesquisar no menu...',
                f2Hint: 'F2: Editar | Enter: Abrir | Del: Excluir | ESC: Fechar',
                editApp: 'Editar app',
                uninstallApp: 'Desinstalar aplicativo',
                pinTaskbar: 'Fixar na barra de tarefas',
                unpinTaskbar: 'Desafixar da barra de tarefas',
                addDesktop: 'Adicionar ao desktop',
                removeDesktop: 'Remover do desktop',
                desktopAppAlreadyThere: 'Este aplicativo já está na área de trabalho',
                desktopToastAdded: 'Adicionado à área de trabalho',
                desktopToastRemoved: 'Removido da área de trabalho',
                open: 'Abrir',
                share: 'Compartilhar',
                download: 'Baixar'
            },
            it: {
                winSearch: 'Cerca nel menu...',
                f2Hint: 'F2: Modifica | Invio: Apri | Canc: Elimina | ESC: Chiudi',
                editApp: 'Modifica app',
                uninstallApp: 'Disinstalla applicazione',
                pinTaskbar: 'Aggiungi alla barra',
                unpinTaskbar: 'Rimuovi dalla barra',
                addDesktop: 'Aggiungi al desktop',
                removeDesktop: 'Rimuovi dal desktop',
                desktopAppAlreadyThere: 'Questa app è già sul desktop',
                desktopToastAdded: 'Aggiunto al desktop',
                desktopToastRemoved: 'Rimosso dal desktop',
                open: 'Apri',
                share: 'Condividi',
                download: 'Scarica'
            },
            ru: {
                winSearch: 'Поиск в меню...',
                f2Hint: 'F2: Правка | Enter: Открыть | Del: Удалить | ESC: Закрыть',
                editApp: 'Редактировать приложение',
                uninstallApp: 'Удалить приложение',
                pinTaskbar: 'Закрепить на панели',
                unpinTaskbar: 'Открепить от панели',
                addDesktop: 'Добавить на рабочий стол',
                removeDesktop: 'Убрать с рабочего стола',
                desktopAppAlreadyThere: 'Это приложение уже на рабочем столе',
                desktopToastAdded: 'Добавлено на рабочий стол',
                desktopToastRemoved: 'Убрано с рабочего стола',
                open: 'Открыть',
                share: 'Поделиться',
                download: 'Скачать'
            },
            zh: {
                winSearch: '在菜单中搜索...',
                f2Hint: 'F2: 编辑 | Enter: 打开 | Del: 删除 | ESC: 关闭',
                editApp: '编辑应用',
                uninstallApp: '卸载应用',
                pinTaskbar: '固定到任务栏',
                unpinTaskbar: '取消固定任务栏',
                addDesktop: '添加到桌面',
                removeDesktop: '从桌面移除',
                desktopAppAlreadyThere: '此应用已在桌面',
                desktopToastAdded: '已添加到桌面',
                desktopToastRemoved: '已从桌面移除',
                open: '打开',
                share: '分享',
                download: '下载'
            },
            ja: {
                winSearch: 'メニューを検索...',
                f2Hint: 'F2: 編集 | Enter: 開く | Del: 削除 | ESC: 閉じる',
                editApp: 'アプリを編集',
                uninstallApp: 'アプリをアンインストール',
                pinTaskbar: 'タスクバーにピン留め',
                unpinTaskbar: 'タスクバーから解除',
                addDesktop: 'デスクトップに追加',
                removeDesktop: 'デスクトップから削除',
                desktopAppAlreadyThere: 'このアプリは既にデスクトップにあります',
                desktopToastAdded: 'デスクトップに追加しました',
                desktopToastRemoved: 'デスクトップから削除しました',
                open: '開く',
                share: '共有',
                download: 'ダウンロード'
            },
            ko: {
                winSearch: '메뉴에서 검색...',
                f2Hint: 'F2: 편집 | Enter: 열기 | Del: 삭제 | ESC: 닫기',
                editApp: '앱 편집',
                uninstallApp: '앱 제거',
                pinTaskbar: '작업 표시줄 고정',
                unpinTaskbar: '작업 표시줄에서 해제',
                addDesktop: '바탕 화면에 추가',
                removeDesktop: '바탕 화면에서 제거',
                desktopAppAlreadyThere: '이 앱은 이미 바탕 화면에 있습니다',
                desktopToastAdded: '바탕 화면에 추가됨',
                desktopToastRemoved: '바탕 화면에서 제거됨',
                open: '열기',
                share: '공유',
                download: '다운로드'
            },
            ar: {
                winSearch: 'البحث في القائمة...',
                f2Hint: 'F2: تحرير | Enter: فتح | Del: حذف | ESC: إغلاق',
                editApp: 'تعديل التطبيق',
                uninstallApp: 'إلغاء تثبيت التطبيق',
                pinTaskbar: 'تثبيت على شريط المهام',
                unpinTaskbar: 'إلغاء تثبيت شريط المهام',
                addDesktop: 'إضافة إلى سطح المكتب',
                removeDesktop: 'إزالة من سطح المكتب',
                desktopAppAlreadyThere: 'هذا التطبيق موجود بالفعل على سطح المكتب',
                desktopToastAdded: 'تمت الإضافة إلى سطح المكتب',
                desktopToastRemoved: 'تمت الإزالة من سطح المكتب',
                open: 'فتح',
                share: 'مشاركة',
                download: 'تنزيل'
            },
            hi: {
                winSearch: 'मेनू में खोजें...',
                f2Hint: 'F2: संपादन | Enter: खोलें | Del: हटाएं | ESC: बंद',
                editApp: 'ऐप संपादित करें',
                uninstallApp: 'एप अनइंस्टॉल करें',
                pinTaskbar: 'टास्कबार पर पिन करें',
                unpinTaskbar: 'टास्कबार से अनपिन',
                addDesktop: 'डेस्कटॉप पर जोड़ें',
                removeDesktop: 'डेस्कटॉप से हटाएं',
                desktopAppAlreadyThere: 'यह ऐप पहले से डेस्कटॉप पर है',
                desktopToastAdded: 'डेस्कटॉप पर जोड़ा गया',
                desktopToastRemoved: 'डेस्कटॉप से हटाया गया',
                open: 'खोलें',
                share: 'शेयर',
                download: 'डाउनलोड'
            }
        };
        let __currentLocale = getSetting('appLocale', 'es');
        function tr(key) {
            const L = UI_STRINGS[__currentLocale] || UI_STRINGS.es;
            return L[key] || UI_STRINGS.en[key] || UI_STRINGS.es[key] || key;
        }
        function applyAppLocale(lang) {
            __currentLocale = lang || 'es';
            const L = UI_STRINGS[lang] || UI_STRINGS.en;
            document.documentElement.setAttribute('lang', lang);
            document.documentElement.setAttribute('dir', lang === 'ar' ? 'rtl' : 'ltr');
            const ws = document.getElementById('windowsSearch');
            if (ws) ws.placeholder = L.winSearch;
            const f2 = document.getElementById('f2Hint');
            if (f2) f2.textContent = L.f2Hint;
            // Botones globales de accesos rápidos (si están visibles/representados).
            const shareBtnPreview = document.getElementById('shareBtn');
            const downloadBtnPreview = document.getElementById('downloadBtn');
            if (shareBtnPreview) shareBtnPreview.textContent = tr('share');
            if (downloadBtnPreview) downloadBtnPreview.textContent = tr('download');
            const previewPrintBtnTr = document.getElementById('previewPrintBtn');
            if (previewPrintBtnTr) previewPrintBtnTr.textContent = tr('print');

            try {
                // Strip menu (contexto en escritorio).
                const stripEdit = document.getElementById('editAppBtn');
                if (stripEdit) stripEdit.innerHTML = `<i class="fas fa-edit"></i> ${tr('editApp')}`;
                const stripDelete = document.getElementById('deleteAppBtn');
                if (stripDelete) stripDelete.innerHTML = `<i class="fas fa-trash"></i> ${tr('uninstallApp')}`;

                const stripToggleGrid = document.getElementById('toggleGridBtn');
                if (stripToggleGrid && typeof editingIndex === 'number' && editingIndex >= 0 && apps && apps[editingIndex]) {
                    const app = apps[editingIndex];
                    const onGrid = appIsVisibleOnWindowsLauncherGrid(app);
                    stripToggleGrid.innerHTML = onGrid
                        ? `<i class="fas fa-eye-slash"></i> ${tr('removeDesktop')}`
                        : `<i class="fas fa-eye"></i> ${tr('addDesktop')}`;
                }
                const stripPin = document.getElementById('pinToTaskbarBtn');
                if (stripPin && typeof editingIndex === 'number' && editingIndex >= 0 && apps && apps[editingIndex]) {
                    const app = apps[editingIndex];
                    const isPinned = pinnedApps.some(p => pinMatchesApp(p, app));
                    stripPin.innerHTML = isPinned
                        ? `<i class="far fa-star"></i> ${tr('unpinTaskbar')}`
                        : `<i class="fas fa-star"></i> ${tr('pinTaskbar')}`;
                }
                const nextWpLbl = document.getElementById('desktopNextWallpaperLabel');
                const nextWpBtn = document.getElementById('desktopNextWallpaperBtn');
                if (nextWpLbl) nextWpLbl.textContent = tr('nextWallpaper');
                if (nextWpBtn) nextWpBtn.setAttribute('title', tr('nextWallpaper'));
                const folderInfoStrip = document.getElementById('folderInfoBtn');
                if (folderInfoStrip) folderInfoStrip.innerHTML = `<i class="fas fa-info-circle"></i> ${tr('explorerProperties')}`;
                // Preview modal (botones inferiores si están renderizados).
                const previewShareBottom = document.getElementById('previewShareBottomBtn');
                const previewDownloadBottom = document.getElementById('previewDownloadBottomBtn');
                if (previewShareBottom) previewShareBottom.querySelector('span') && (previewShareBottom.querySelector('span').textContent = tr('share'));
                if (previewDownloadBottom) previewDownloadBottom.querySelector('span') && (previewDownloadBottom.querySelector('span').textContent = tr('download'));

                // Backup/Logs, Papelera y Mosaico de ventanas.
                const backupLogsTitle = document.getElementById('backupLogsTitle');
                if (backupLogsTitle) backupLogsTitle.textContent = tr('backupLogsTitle');
                const exportBackupBtn = document.getElementById('exportBackupBtn');
                if (exportBackupBtn) exportBackupBtn.textContent = tr('exportBackupBtn');
                const importBackupBtn = document.getElementById('importBackupBtn');
                if (importBackupBtn) importBackupBtn.textContent = tr('importBackupBtn');

                const openingsHistoryTitle = document.getElementById('openingsHistoryTitle');
                if (openingsHistoryTitle) openingsHistoryTitle.textContent = tr('openingsHistoryTitle');
                const refreshLogBtn = document.getElementById('refreshLogBtn');
                if (refreshLogBtn) refreshLogBtn.textContent = tr('refreshLogBtn');
                const clearLogBtn = document.getElementById('clearLogBtn');
                if (clearLogBtn) clearLogBtn.textContent = tr('clearLogBtn');
                const rotateLogBtn = document.getElementById('rotateLogBtn');
                if (rotateLogBtn) rotateLogBtn.textContent = tr('rotateLogBtn');

                const formatReinstallTitle = document.getElementById('formatReinstallTitle');
                if (formatReinstallTitle) formatReinstallTitle.textContent = tr('formatReinstallTitle');
                const formatReinstallHint = document.getElementById('formatReinstallHint');
                if (formatReinstallHint) formatReinstallHint.textContent = tr('formatReinstallHint');
                const formatModeLabel = document.getElementById('formatModeLabel');
                if (formatModeLabel) formatModeLabel.textContent = tr('formatModeLabel');
                const formatLaunchInstallBtn = document.getElementById('formatLaunchInstallBtn');
                if (formatLaunchInstallBtn) formatLaunchInstallBtn.textContent = tr('formatLaunchInstallBtn');

                const recycleBinModalTitle = document.getElementById('recycleBinModalTitle');
                if (recycleBinModalTitle) recycleBinModalTitle.textContent = tr('recycleBinModalTitle');
                const emptyTrashBtn = document.getElementById('emptyTrashBtn');
                if (emptyTrashBtn) emptyTrashBtn.textContent = tr('emptyTrashBtn');
                const emptyTrashBtnTop = document.getElementById('emptyTrashBtnTop');
                if (emptyTrashBtnTop) emptyTrashBtnTop.textContent = tr('emptyTrashBtn');

                const explorerRenameModalTitle = document.getElementById('explorerRenameModalTitle');
                if (explorerRenameModalTitle) explorerRenameModalTitle.textContent = tr('explorerRenameModalTitle');
                const explorerRenameModalHint = document.getElementById('explorerRenameModalHint');
                if (explorerRenameModalHint) {
                    const fb = document.getElementById('explorerRenameFolderBlock');
                    const isFolderUi = fb && fb.style.display !== 'none';
                    explorerRenameModalHint.textContent = isFolderUi ? tr('explorerRenameHintFolder') : tr('explorerRenameHintFile');
                }
                const explorerRenameBaseLabel = document.getElementById('explorerRenameBaseLabel');
                if (explorerRenameBaseLabel) explorerRenameBaseLabel.textContent = tr('explorerRenameLabelBase');
                const explorerRenameFolderLabel = document.getElementById('explorerRenameFolderLabel');
                if (explorerRenameFolderLabel) explorerRenameFolderLabel.textContent = tr('explorerRenameLabelFolder');
                const explorerRenameExtHelp = document.getElementById('explorerRenameExtHelp');
                if (explorerRenameExtHelp) explorerRenameExtHelp.textContent = tr('explorerRenameExtHelp');
                const explorerRenamePreviewLabel = document.getElementById('explorerRenamePreviewLabel');
                if (explorerRenamePreviewLabel) explorerRenamePreviewLabel.textContent = tr('explorerRenamePreviewLabel');
                const explorerRenameModalSave = document.getElementById('explorerRenameModalSave');
                if (explorerRenameModalSave) explorerRenameModalSave.textContent = tr('explorerRenameSave');
                const explorerRenameModalCancel = document.getElementById('explorerRenameModalCancel');
                if (explorerRenameModalCancel) explorerRenameModalCancel.textContent = tr('explorerRenameCancel');

                const recycleBinEl = document.getElementById('recycleBin');
                if (recycleBinEl) recycleBinEl.title = tr('recycleBinModalTitle');

                const windowsMosaicTitle = document.getElementById('windowsMosaicTitle');
                if (windowsMosaicTitle) windowsMosaicTitle.textContent = tr('windowsMosaicTitle');
                const closeAllWindowsBtn = document.getElementById('closeAllWindowsBtn');
                if (closeAllWindowsBtn) closeAllWindowsBtn.textContent = tr('closeAllWindowsBtn');

                const explorerStorageTitle = document.getElementById('explorerStorageTitle');
                if (explorerStorageTitle) explorerStorageTitle.textContent = tr('explorerStorageTabTitle');
                const explorerStorageTabBtn = document.getElementById('explorerStorageTabBtn');
                if (explorerStorageTabBtn) explorerStorageTabBtn.textContent = tr('explorerStorageTabShort');
                const explorerEmptyRemoteCacheBtn = document.getElementById('explorerEmptyRemoteCacheBtn');
                if (explorerEmptyRemoteCacheBtn) explorerEmptyRemoteCacheBtn.textContent = tr('explorerBtnEmptyRemoteCache');
                const explorerEmptyTrashFromConfigBtn = document.getElementById('explorerEmptyTrashFromConfigBtn');
                if (explorerEmptyTrashFromConfigBtn) explorerEmptyTrashFromConfigBtn.textContent = tr('explorerBtnEmptyExplorerTrash');
                const explorerStorageRefreshBtn = document.getElementById('explorerStorageRefreshBtn');
                if (explorerStorageRefreshBtn) explorerStorageRefreshBtn.textContent = tr('explorerBtnRefreshStats');
                const explorerStorageShortcutsHint = document.getElementById('explorerStorageShortcutsHint');
                if (explorerStorageShortcutsHint) explorerStorageShortcutsHint.textContent = tr('explorerStorageShortcutsHint');
                const explorerStorageOpenFilesBtnLabel = document.getElementById('explorerStorageOpenFilesBtnLabel');
                if (explorerStorageOpenFilesBtnLabel) explorerStorageOpenFilesBtnLabel.textContent = tr('explorerStorageShortcutFiles');
                const explorerStorageOpenCacheBtnLabel = document.getElementById('explorerStorageOpenCacheBtnLabel');
                if (explorerStorageOpenCacheBtnLabel) explorerStorageOpenCacheBtnLabel.textContent = tr('explorerStorageShortcutCache');
                const explorerStorageOpenTrashBtnLabel = document.getElementById('explorerStorageOpenTrashBtnLabel');
                if (explorerStorageOpenTrashBtnLabel) explorerStorageOpenTrashBtnLabel.textContent = tr('explorerStorageShortcutTrash');
                const explorerStorageOpenFilesBtn = document.getElementById('explorerStorageOpenFilesBtn');
                if (explorerStorageOpenFilesBtn) explorerStorageOpenFilesBtn.setAttribute('title', tr('explorerStorageShortcutFiles'));
                const explorerStorageOpenCacheBtn = document.getElementById('explorerStorageOpenCacheBtn');
                if (explorerStorageOpenCacheBtn) explorerStorageOpenCacheBtn.setAttribute('title', tr('explorerStorageShortcutCache'));
                const explorerStorageOpenTrashBtn = document.getElementById('explorerStorageOpenTrashBtn');
                if (explorerStorageOpenTrashBtn) explorerStorageOpenTrashBtn.setAttribute('title', tr('explorerStorageShortcutTrash'));

                const gridClearAssetCacheBtn = document.getElementById('gridClearAssetCacheBtn');
                if (gridClearAssetCacheBtn) gridClearAssetCacheBtn.textContent = tr('explorerBtnClearAssetCache');

                const diskRecoveryTabBtn = document.getElementById('diskRecoveryTabBtn');
                if (diskRecoveryTabBtn) diskRecoveryTabBtn.textContent = tr('diskRecoveryTabShort');
                const diskRecoveryFormatTitle = document.getElementById('diskRecoveryFormatTitle');
                if (diskRecoveryFormatTitle) diskRecoveryFormatTitle.textContent = tr('formatReinstallTitle');
                const diskRecoveryFormatHint = document.getElementById('diskRecoveryFormatHint');
                if (diskRecoveryFormatHint) diskRecoveryFormatHint.textContent = tr('formatReinstallHint');
                const diskRecoveryFormatModeLabel = document.getElementById('diskRecoveryFormatModeLabel');
                if (diskRecoveryFormatModeLabel) diskRecoveryFormatModeLabel.textContent = tr('formatModeLabel');
                const diskRecoveryFormatLaunchBtnTr = document.getElementById('diskRecoveryFormatLaunchBtn');
                if (diskRecoveryFormatLaunchBtnTr) diskRecoveryFormatLaunchBtnTr.textContent = tr('formatLaunchInstallBtn');
                const diskRecoveryFormatCrossHint = document.getElementById('diskRecoveryFormatCrossHint');
                if (diskRecoveryFormatCrossHint) diskRecoveryFormatCrossHint.textContent = tr('diskRecoveryFormatCrossHint');

                const systemCreditsTabBtn = document.getElementById('systemCreditsTabBtn');
                if (systemCreditsTabBtn) systemCreditsTabBtn.textContent = tr('systemCreditsTabShort');
                const systemCreditsMainTitle = document.getElementById('systemCreditsMainTitle');
                if (systemCreditsMainTitle) systemCreditsMainTitle.textContent = tr('systemCreditsMainTitle');
                const systemCreditsOwnerLine = document.getElementById('systemCreditsOwnerLine');
                if (systemCreditsOwnerLine) systemCreditsOwnerLine.textContent = tr('systemCreditsOwner');
                const systemCreditsPrivacyTitle = document.getElementById('systemCreditsPrivacyTitle');
                if (systemCreditsPrivacyTitle) systemCreditsPrivacyTitle.textContent = tr('systemCreditsPrivacyTitle');
                const systemCreditsPrivacyText = document.getElementById('systemCreditsPrivacyText');
                if (systemCreditsPrivacyText) systemCreditsPrivacyText.textContent = tr('systemCreditsPrivacyBody');
                const systemCreditsHostingHeading = document.getElementById('systemCreditsHostingHeading');
                if (systemCreditsHostingHeading) systemCreditsHostingHeading.textContent = tr('systemCreditsHostingHeading');
                const systemCreditsLogoMissing = document.getElementById('systemCreditsLogoMissing');
                if (systemCreditsLogoMissing) systemCreditsLogoMissing.textContent = tr('systemCreditsLogoMissing');
                const systemCreditsTabEl = document.getElementById('systemCreditsTab');
                if (systemCreditsTabEl && systemCreditsTabEl.classList.contains('active')) {
                    refreshSystemCreditsPanel();
                }
            } catch (_) {}
        }

        async function saveSettings(patch = {}) {
            const currentApps = apps || [];
            const mergedSettings = { ...settings, ...patch, apps: currentApps };
            if (!mergedSettings.categories || mergedSettings.categories.length === 0) {
                mergedSettings.categories = ['General'];
            }
            try {
                // Solo enviar las claves del patch. Si enviamos todo `settings`, pisamos en el
                // servidor campos que solo actualiza la API (p. ej. fileExplorerTrash al borrar archivos).
                const payload = { ...patch };
                delete payload.pageZoom;
                delete payload.iframeZoom;
                if (payload.launcherLayout === null) delete payload.launcherLayout;

                const response = await fetch('?action=saveSettings', {
                    method: 'POST',
                    headers: { 'Content-Type': 'application/json' },
                    body: JSON.stringify(payload)
                });
                const result = await response.json();
                if (result.success) {
                    settings = result.data ? { ...mergedSettings, ...result.data } : mergedSettings;
                    // Prioridad a reordenamientos del cliente: evita que una respuesta vieja del servidor "pise" el orden.
                    if (patch && patch.pinnedApps !== undefined) settings.pinnedApps = patch.pinnedApps;
                    if (patch && patch.taskbarOpenOrder !== undefined) settings.taskbarOpenOrder = patch.taskbarOpenOrder;
                    if (gridSettingsStructureOk(settings)) gridPersistSettingsRedundancy(settings);
                    if (Array.isArray(settings.pinnedApps)) gridWriteJsonLs(GRID_TASKBAR_PINNED_ORDER_KEY, settings.pinnedApps);
                    if (Array.isArray(settings.taskbarOpenOrder)) gridWriteJsonLs(GRID_TASKBAR_OPEN_ORDER_KEY, settings.taskbarOpenOrder);
                    if (settings.categories) {
                        gridNormalizeSettingsCategoriesInPlace();
                        categories = settings.categories;
                    }
                    if (settings.taskbarPosition) applyTaskbarPosition(settings.taskbarPosition);
                    if (settings.startButtonIcon !== undefined) updateStartButtonIcon(settings.startButtonIcon);
                    if (settings.mobileDesktopMode !== undefined) updateMobileMode(settings.mobileDesktopMode);
                    if (settings.desktopShortcuts !== undefined) desktopShortcuts = settings.desktopShortcuts;
                    if (settings.appLocale !== undefined) applyAppLocale(settings.appLocale);
                    if (Array.isArray(settings.fileExplorerTrash)) fileExplorerTrash = settings.fileExplorerTrash;
                    if (Array.isArray(settings.fileExplorerConnections)) fileExplorerConnections = settings.fileExplorerConnections;
                    if (patch && patch.trash !== undefined) {
                        trash = Array.isArray(settings.trash) ? settings.trash.slice() : [];
                    }
                    if (patch && patch.quickNotes !== undefined) {
                        settings.quickNotes = Array.isArray(patch.quickNotes) ? patch.quickNotes : [];
                    }
                    if (patch && patch.mediaWidgetItems !== undefined) {
                        settings.mediaWidgetItems = Array.isArray(patch.mediaWidgetItems) ? patch.mediaWidgetItems : [];
                    }
                    if (patch && patch.osWidgetOrder !== undefined) {
                        settings.osWidgetOrder = Array.isArray(patch.osWidgetOrder) ? patch.osWidgetOrder : ['spotify', 'system', 'weather', 'notes', 'media'];
                    }
                    if (Array.isArray(settings.taskbarOpenOrder)) taskbarOpenOrder = settings.taskbarOpenOrder.slice();
                    if (settings.launcherLayout && window.GridLauncher && window.GridLauncher.layoutModel) {
                        launcherLayout = GridLauncher.layoutModel.normalizeLayout(settings.launcherLayout);
                    }
                    applyThemeToTaskbar();
                    renderDesktopShortcuts();
                    return result;
                }
            } catch (error) {
                console.error('Error de red al guardar settings:', error);
            }
        }

        const QUICK_NOTE_TRASH_ICON =
            'data:image/svg+xml,' + encodeURIComponent(
                '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32">' +
                '<rect x="4" y="3" width="22" height="26" rx="3" fill="#facc15" stroke="#ca8a04" stroke-width="1.2"/>' +
                '<path d="M10 3h10l4 4h-14z" fill="#fde047"/>' +
                '<line x1="9" y1="14" x2="21" y2="14" stroke="#854d0e" stroke-width="1.2" stroke-linecap="round"/>' +
                '<line x1="9" y1="18" x2="18" y2="18" stroke="#854d0e" stroke-width="1.2" stroke-linecap="round"/>' +
                '</svg>'
            );

        function gridTrashItemIsQuickNote(item) {
            return !!(item && item.trashKind === 'quickNote' && item.quickNote);
        }

        function gridQuickNoteTrashExcerpt(text, maxLen) {
            const raw = String(text || '').trim().replace(/\s+/g, ' ');
            if (!raw) return '';
            const n = Math.max(8, maxLen || 48);
            return raw.length > n ? raw.slice(0, n) + '…' : raw;
        }

        /** Etiqueta en papelera: título; si falta, extracto del cuerpo; si hay ambos, título + extracto. */
        function gridQuickNoteTrashLabel(note) {
            const title = String((note && note.title) || '').trim();
            const excerpt = gridQuickNoteTrashExcerpt(note && note.text, 48);
            if (title && excerpt) {
                return title + ' — ' + excerpt;
            }
            if (title) return title;
            if (excerpt) return excerpt;
            return 'Nota rápida (sin contenido)';
        }

        function gridQuickNoteTrashEntry(note) {
            return {
                trashKind: 'quickNote',
                quickNote: Object.assign({}, note),
                title: gridQuickNoteTrashLabel(note),
                icon: QUICK_NOTE_TRASH_ICON
            };
        }

        function moveQuickNoteToTrash(noteId, snapshot) {
            const notes = Array.isArray(settings.quickNotes) ? settings.quickNotes.slice() : [];
            const idx = notes.findIndex(function (n) { return n && n.id === noteId; });
            if (idx < 0) return Promise.resolve(false);
            const note = Object.assign({}, notes[idx], snapshot || {});
            notes.splice(idx, 1);
            trash.push(gridQuickNoteTrashEntry(note));
            settings.quickNotes = notes;
            return saveSettings({ quickNotes: notes, trash: trash }).then(function (result) {
                if (result && result.success) {
                    updateRecycleBinIcon();
                    renderTrashItems();
                }
                return !!(result && result.success);
            });
        }

        function restoreQuickNoteFromTrash(trashIndex) {
            const item = trash[trashIndex];
            if (!gridTrashItemIsQuickNote(item)) return Promise.resolve(false);
            const notes = Array.isArray(settings.quickNotes) ? settings.quickNotes.slice() : [];
            notes.push(item.quickNote);
            trash.splice(trashIndex, 1);
            settings.quickNotes = notes;
            return saveSettings({ quickNotes: notes, trash: trash }).then(function (result) {
                if (result && result.success) {
                    updateRecycleBinIcon();
                    renderTrashItems();
                    if (window.OsShell && typeof window.OsShell.refreshNotes === 'function') {
                        window.OsShell.refreshNotes();
                    }
                }
                return !!(result && result.success);
            });
        }

        window.__antaraGridDesktop = {
            saveSettings,
            getSetting,
            getQuickNotes: function () {
                return Array.isArray(settings.quickNotes) ? settings.quickNotes : [];
            },
            setQuickNotes: function (arr) {
                settings.quickNotes = Array.isArray(arr) ? arr : [];
            },
            getMediaWidgetItems: function () {
                return Array.isArray(settings.mediaWidgetItems) ? settings.mediaWidgetItems : [];
            },
            setMediaWidgetItems: function (arr) {
                settings.mediaWidgetItems = Array.isArray(arr) ? arr : [];
            },
            moveQuickNoteToTrash: moveQuickNoteToTrash,
            getTelemetryIntervalSec: function () {
                const v = Number(getSetting('osTelemetryIntervalSec', 22));
                return isFinite(v) && v > 0 ? v : 22;
            },
            resolveWallpaperMediaUrl: resolveWallpaperMediaUrl,
            getExplorerWallpaperMediaKind: getExplorerWallpaperMediaKind,
            applyExplorerWallpaper: applyExplorerWallpaper,
            addExplorerWallpaperToList: addExplorerWallpaperToList,
            showContextMenu: showContextMenu,
            showFilePreview: showFilePreview,
            shareSecureUrl: shareSecureUrl,
            explorerDeleteToTrash: explorerDelete,
            syncExplorerTrashUi: async function () {
                await fetchExplorerListTrash();
                updateRecycleBinIcon();
            },
            tr: tr,
        };

        async function deleteMediaFile(data) {
            try {
                const response = await fetch('?action=deleteMedia', {
                    method: 'POST',
                    headers: { 'Content-Type': 'application/json' },
                    body: JSON.stringify(data)
                });
                return await response.json();
            } catch (error) {
                console.error('Error al eliminar archivo de media:', error);
                return { success: false };
            }
        }

        /** Accesos creados desde el explorador: al desinstalar / vaciar papelera no se borra el archivo en la biblioteca. */
        function gridTrashItemSkipsMediaDelete(item) {
            if (!item) return false;
            if (item.libraryExplorerShortcut) return true;
            if (item.explorerOpen && item.explorerOpen.bucket) return true;
            return false;
        }

        const appGrid = document.getElementById('appGrid');
        const windowsMenu = document.getElementById('windowsMenu');
        const windowsMenuBackdrop = document.getElementById('windowsMenuBackdrop');
        const windowsMenuClose = document.getElementById('windowsMenuClose');
        const taskbar = document.getElementById('taskbar');
        const windowsCategoryList = document.getElementById('windowsCategoryList');
        const windowsAppsContainer = document.getElementById('windowsAppsContainer');
        const windowsSearch = document.getElementById('windowsSearch');
        const windowsSearchClear = document.getElementById('windowsSearchClear');
        const allCategoriesOption = document.getElementById('allCategoriesOption');
        const addAppModal = document.getElementById('addAppModal');
        const configModal = document.getElementById('configModal');
        const appForm = document.getElementById('appForm');
        const deleteBtn = document.getElementById('deleteBtn');
        const modalTitle = document.getElementById('modalTitle');
        const configBtn = document.getElementById('configBtn');
        const addAppFloatBtn = document.getElementById('addAppFloatBtn');
        const stripMenu = document.getElementById('stripMenu');
        const desktopStripMenu = document.getElementById('desktopStripMenu');
        const stripMenuBackdrop = document.getElementById('stripMenuBackdrop');
        const editAppBtn = document.getElementById('editAppBtn');
        const toggleGridBtn = document.getElementById('toggleGridBtn');
        const pinToTaskbarBtn = document.getElementById('pinToTaskbarBtn');
        const deleteAppBtn = document.getElementById('deleteAppBtn');
        const folderOpenBtn = document.getElementById('folderOpenBtn');
        const folderInfoBtn = document.getElementById('folderInfoBtn');
        const folderRenameBtn = document.getElementById('folderRenameBtn');
        const folderColorBtn = document.getElementById('folderColorBtn');
        const folderDeleteBtn = document.getElementById('folderDeleteBtn');
        const launcherFolderModal = document.getElementById('launcherFolderModal');
        const launcherFolderModalClose = document.getElementById('launcherFolderModalClose');
        const launcherFolderGrid = document.getElementById('launcherFolderGrid');
        const launcherFolderModalTitle = document.getElementById('launcherFolderModalTitle');
        const desktopAddAppBtn = document.getElementById('desktopAddAppBtn');
        const desktopNextWallpaperBtn = document.getElementById('desktopNextWallpaperBtn');
        const desktopConfigBtn = document.getElementById('desktopConfigBtn');
        const searchInput = document.getElementById('searchInput');
        const searchClear = document.getElementById('searchClear');
        const muteToggle = document.getElementById('muteToggle');
        const youtubeLinkBtn = document.getElementById('youtubeLinkBtn');
        const startButton = document.getElementById('startButton');
        const recycleBin = document.getElementById('recycleBin');
        const recycleBinModal = document.getElementById('recycleBinModal');
        const trashItems = document.getElementById('trashItems');
        const windowsMosaicBtn = document.getElementById('windowsMosaicBtn');
        const mobileAppHubBtn = document.getElementById('mobileAppHubBtn');
        const mobileAppHubCount = document.getElementById('mobileAppHubCount');
        const mobileAppHubBackdrop = document.getElementById('mobileAppHubBackdrop');
        const mobileAppHubPanel = document.getElementById('mobileAppHubPanel');
        const mobileAppHubCloseBtn = document.getElementById('mobileAppHubCloseBtn');
        const mobileAppHubPinned = document.getElementById('mobileAppHubPinned');
        const mobileAppHubOpen = document.getElementById('mobileAppHubOpen');
        const mobileAppHubSectionOpen = document.getElementById('mobileAppHubSectionOpen');
        const windowsMosaicModal = document.getElementById('windowsMosaicModal');
        const windowsMosaicGrid = document.getElementById('windowsMosaicGrid');
        const closeAllWindowsBtn = document.getElementById('closeAllWindowsBtn');
        const clock = document.getElementById('clock');
        const zoomInBtn = document.getElementById('zoomInBtn');
        const zoomOutBtn = document.getElementById('zoomOutBtn');
        const fullscreenBtn = document.getElementById('fullscreenBtn');
        const previewWindowFrame = document.getElementById('previewWindowFrame');
        const previewWindowTitlebar = document.getElementById('previewWindowTitlebar');
        const previewMaximizeBtn = document.getElementById('previewMaximizeBtn');
        const previewZoomInBtn = document.getElementById('previewZoomInBtn');
        const previewZoomOutBtn = document.getElementById('previewZoomOutBtn');

        // Evitar drag nativo del navegador (HTML5 DnD) en tiles del launcher.
        // El reordenamiento se gestiona por el controlador de long-press.
        if (appGrid) {
            appGrid.addEventListener('dragstart', (e) => {
                if (e && e.target && e.target.closest && e.target.closest('#appGrid .app-item')) {
                    e.preventDefault();
                    e.stopPropagation();
                }
            }, true);
        }
        const previewZoomResetBtn = document.getElementById('previewZoomResetBtn');
        const previewExternalBtn = document.getElementById('previewExternalBtn');
        const previewCloseBtn = document.getElementById('previewCloseBtn');
        const pinnedAppsContainer = document.getElementById('pinnedAppsContainer');
        const taskbarAppsContainer = document.getElementById('taskbarAppsContainer');
        const mobileDesktopModeCheck = document.getElementById('mobileDesktopMode');
        const desktopShortcutsContainer = document.getElementById('desktopShortcuts');

        const addCategoryModal = document.getElementById('addCategoryModal');
        const editCategoryIconModal = document.getElementById('editCategoryIconModal');
        const categoryForm = document.getElementById('categoryForm');
        const addCategoryBtn = document.getElementById('addCategoryBtn');
        const appCategorySelect = document.getElementById('appCategory');
        const categoryFilterSelect = document.getElementById('categoryFilter');

        const textColorPicker = document.getElementById('textColorPicker');
        const gridTextColorPicker = document.getElementById('gridTextColorPicker');

        const useYoutubeRadio = document.getElementById('useYoutube');
        const useFileRadio = document.getElementById('useFile');
        const useImagesRadio = document.getElementById('useImages');
        const useVideosRadio = document.getElementById('useVideos');
        const youtubeInputGroup = document.getElementById('youtubeInputGroup');
        const fileInputGroup = document.getElementById('fileInputGroup');
        const imagesInputGroup = document.getElementById('imagesInputGroup');
        const videosInputGroup = document.getElementById('videosInputGroup');
        const youtubeLinkInput = document.getElementById('youtubeLink');
        const youtubeLinkMobileInput = document.getElementById('youtubeLinkMobile');
        const bgFileInput = document.getElementById('bgFile');
        const previewContainer = document.getElementById('previewContainer');
        const youtubeFeedback = document.getElementById('youtubeFeedback');
        const imageFilesInput = document.getElementById('imageFiles');
        const imageListContainer = document.getElementById('imageListContainer');
        const videoFilesInput = document.getElementById('videoFiles');
        const videoListContainer = document.getElementById('videoListContainer');
        const slideshowEnabledCheck = document.getElementById('slideshowEnabled');
        const slideshowIntervalInput = document.getElementById('slideshowInterval');
        const slideshowIntervalGroup = document.getElementById('slideshowIntervalGroup');

        const inspectBlockCheck = document.getElementById('inspectBlock');
        const exportBackupBtn = document.getElementById('exportBackupBtn');
        const importBackupZipInput = document.getElementById('importBackupZipInput');
        const importBackupBtn = document.getElementById('importBackupBtn');
        const refreshLogBtn = document.getElementById('refreshLogBtn');
        const clearLogBtn = document.getElementById('clearLogBtn');
        const rotateLogBtn = document.getElementById('rotateLogBtn');
        const formatModeCheckbox = document.getElementById('formatModeCheckbox');
        const formatLaunchInstallBtn = document.getElementById('formatLaunchInstallBtn');
        const diskRecoveryFormatModeCheckbox = document.getElementById('diskRecoveryFormatModeCheckbox');
        const diskRecoveryFormatLaunchBtn = document.getElementById('diskRecoveryFormatLaunchBtn');
        const activityLogBox = document.getElementById('activityLogBox');
        const showTypeBadgeCheck = document.getElementById('showTypeBadge');
        const typeBadgeModeRadios = document.querySelectorAll('input[name="typeBadgeMode"]');
        const startButtonIconInput = document.getElementById('startButtonIcon');
        const fontSelect = document.getElementById('fontSelect');
        const fontSizeInput = document.getElementById('fontSizeInput');

        let apps = settings.apps && Array.isArray(settings.apps) ? settings.apps : [];
        let trash = settings.trash && Array.isArray(settings.trash) ? settings.trash : [];
        let fileExplorerTrash = settings.fileExplorerTrash && Array.isArray(settings.fileExplorerTrash) ? settings.fileExplorerTrash : [];
        let fileExplorerConnections = settings.fileExplorerConnections && Array.isArray(settings.fileExplorerConnections) ? settings.fileExplorerConnections : [];
        let pinnedApps = settings.pinnedApps && Array.isArray(settings.pinnedApps) ? settings.pinnedApps : [];
        let taskbarOpenOrder = settings.taskbarOpenOrder && Array.isArray(settings.taskbarOpenOrder) ? settings.taskbarOpenOrder.slice() : [];
        let desktopShortcuts = settings.desktopShortcuts && Array.isArray(settings.desktopShortcuts) ? settings.desktopShortcuts : [];
        let launcherLayout = null;
        let launcherContextTarget = { type: 'none' };
        let __launcherGridControllers = [];
        let __gridLauncherSuppressClickUntil = 0;
        if (window.GridLauncher && window.GridLauncher.layoutModel) {
            launcherLayout = GridLauncher.layoutModel.ensure(settings, getAppStableId);
            settings.launcherLayout = launcherLayout;
        }
        gridNormalizeSettingsCategoriesInPlace();
        if (!Array.isArray(settings.quickNotes)) settings.quickNotes = [];
        if (!Array.isArray(settings.mediaWidgetItems)) settings.mediaWidgetItems = [];
        let categories = settings.categories;
        let editingIndex = -1;
        let tempSelectedFile = null;
        let currentFile = null;
        let currentTheme = getSetting('selectedTheme', 'neumorphism');
        let previewThemeActive = false;
        let activePreviewImg = null;
        let isImageZoomed = false;
        let selectedAppIndex = -1;
        let selectedGridVisibleIndex = -1;
        let isDragging = false;
        let startX, startY, translateX = 0, translateY = 0, scale = 1;
        let touchStartTime = 0;
        let touchStartY = 0;
        let isScrolling = false;
        let slideshowTimer = null;
        let currentImageIndex = 0;
        let videoPlayIndex = 0;
        let currentVideoElement = null;
        let windowsMenuVisible = false;
        let activeWindowsCategory = 'all';
        let selectedCategoryIndex = 0;
        let selectedWindowsAppIndex = -1;
        let windowsSearchQuery = '';
        function updateWindowsSearchClearVisibility() {
            if (!windowsSearchClear || !windowsSearch) return;
            windowsSearchClear.classList.toggle('visible', !!(windowsSearch.value || '').trim());
        }
        let previewActive = false;
        /** Ancho del visualViewport al abrir el visor o tras reset; para detectar split / cambio de layout. */
        let previewViewportWidthSnapshot = 0;
        // Evitar "resets" de zoom durante interacción táctil (pan/pinch).
        // En móviles, visualViewport puede cambiar durante gestos y disparar resets falsos.
        let __imageInteractionActiveUntil = 0;
        function markImageInteractionActive(ms = 400) {
            try {
                const until = Date.now() + Math.max(0, ms | 0);
                if (until > __imageInteractionActiveUntil) __imageInteractionActiveUntil = until;
            } catch (_) {}
        }

        // Evitar "resets" de zoom durante interacción táctil SOLO en el visor FTP (previewModal).
        let __ftpImageInteractionActiveUntil = 0;
        function isFtpPreviewImageActive() {
            try {
                if (!previewActive) return false;
                if (previewZoomMode !== 'image') return false;
                if (!activePreviewImg || activePreviewImg.id !== 'previewImage') return false;
                const modal = document.getElementById('previewModal');
                return !!(modal && modal.style.display === 'block');
            } catch (_) {
                return false;
            }
        }
        function markFtpImageInteractionActive(ms = 400) {
            try {
                if (!isFtpPreviewImageActive()) return;
                const until = Date.now() + Math.max(0, ms | 0);
                if (until > __ftpImageInteractionActiveUntil) __ftpImageInteractionActiveUntil = until;
            } catch (_) {}
        }
        let previewFeTabsObserver = null;
        let openWindows = [];
        let windowZIndex = 10000;
        let activeTaskbarApps = [];
        let hoverPreviewTimeout = null;
        /** Tras reordenar iconos de la barra, evita el clic que dispara minimizar/abrir. */
        let taskbarDragSuppressClickUntil = 0;
        /** Tras reordenar en el hub móvil, evita click/abrir accidental. */
        let mobileHubSuppressClickUntil = 0;
        /** Tras swipe-to-close en mosaico, evita click accidental. */
        let mosaicSwipeSuppressClickUntil = 0;
        let taskbarIconReorderBound = false;
        let mobileHubPinnedReorderBound = false;
        let mobileHubOpenSwipeCloseBound = false;
        let windowsMosaicSwipeCloseBound = false;
        let currentContextMenu = null;
        let currentHoverPreview = null;

        /** Por encima de .window-frame y de overlays del explorador (p. ej. progreso ~700k). Usa !important por si el modal fija z-index en CSS. */
        function bumpShellModalAboveWindows(modalEl, minZ = 800000) {
            if (!modalEl) return;
            let nextZ = ++windowZIndex;
            try {
                (openWindows || []).forEach((w) => {
                    if (!w || !w.frame || w.frame.style.display === 'none' || w.frame.classList.contains('minimized')) return;
                    const zi = parseInt(String(w.frame.style.zIndex || '0'), 10);
                    if (!isNaN(zi) && zi >= nextZ) nextZ = zi + 1;
                });
            } catch (_) {}
            nextZ = Math.max(nextZ, minZ);
            modalEl.style.setProperty('z-index', String(nextZ), 'important');
            windowZIndex = Math.max(windowZIndex, nextZ);
        }
        let isResizing = false;
        let resizeStartX, resizeStartY, resizeStartWidth, resizeStartHeight;
        let globalIframeZoom = typeof SAVED_IFRAME_ZOOM !== 'undefined' ? SAVED_IFRAME_ZOOM : 1;
        
        // Identificador único por dispositivo/navegador (zoom persistente por dispositivo).
        let zoomDeviceId = localStorage.getItem('zoom_device_id_v1');
        if (!zoomDeviceId) {
            zoomDeviceId = (window.crypto && crypto.randomUUID) ? crypto.randomUUID() : ('dev_' + Math.random().toString(16).slice(2));
            localStorage.setItem('zoom_device_id_v1', zoomDeviceId);
        }
        const gridZoomStorageKey = `gridZoom_${zoomDeviceId}`;
        const iframeZoomStorageKey = `iframeZoom_${zoomDeviceId}`;
        const appZoomStoragePrefix = `appZoom_${zoomDeviceId}_`;
        const OPEN_WINDOWS_SESSION_KEY = `grid_open_windows_session_${GRID_SESSION_USER_ID || 'anon'}_${zoomDeviceId}`;
        const SHARED_OPEN_WINDOWS_SETTINGS_KEY = 'openWindowsSessionShared';
        let __restoringOpenWindowsSession = false;
        let __persistOpenWindowsTimer = null;
        let __persistOpenWindowsSharedTimer = null;
        let __lastSharedOpenWindowsPayload = '';

        function clampZoom(value, min = 0.5, max = 2.5, fallback = 1) {
            const n = Number(value);
            if (!Number.isFinite(n)) return fallback;
            return Math.max(min, Math.min(max, n));
        }

        function getAppZoomStorageKey(app, kind = 'generic') {
            let sid = '';
            try {
                sid = encodeURIComponent(getAppStableId(app));
            } catch (_) {
                sid = encodeURIComponent((app && (app.id || app.title || app.link)) || 'app');
            }
            return `${appZoomStoragePrefix}${kind}_${sid}`;
        }

        function readZoomFromStorage(key, fallback = 1, min = 0.5, max = 2.5) {
            try {
                const raw = localStorage.getItem(key);
                if (raw === null) return clampZoom(fallback, min, max, fallback);
                return clampZoom(raw, min, max, fallback);
            } catch (_) {
                return clampZoom(fallback, min, max, fallback);
            }
        }

        function writeZoomToStorage(key, value, min = 0.5, max = 2.5) {
            const safe = clampZoom(value, min, max, 1);
            try { localStorage.setItem(key, String(safe)); } catch (_) {}
            return safe;
        }

        function getTopVisibleExplorerWindowEntry() {
            const visible = openWindows
                .filter(w => w && w.frame && document.body.contains(w.frame) && w.frame.style.display !== 'none' && !w.frame.classList.contains('minimized'))
                .filter(w => w.app && w.app.id === 'file-explorer-system')
                .sort((a, b) => (parseInt(b.frame.style.zIndex || '0', 10) - parseInt(a.frame.style.zIndex || '0', 10)));
            return visible[0] || null;
        }

        function getTopVisibleExplorerFrame() {
            const entry = getTopVisibleExplorerWindowEntry();
            return entry ? entry.frame : null;
        }

        function isExplorerFocusLockActive() {
            return !!getTopVisibleExplorerFrame();
        }

        function isTargetInsideExplorerScope(target) {
            if (!target || !target.closest) return false;
            /** Cualquier ventana del explorador (no solo la de mayor z-index): evita falsos "fuera" con varias ventanas. */
            const wf = target.closest('.window-frame');
            if (wf && wf.querySelector('.file-explorer')) {
                return true;
            }
            if (currentContextMenu && currentContextMenu.dataset && currentContextMenu.dataset.owner === 'explorer' && currentContextMenu.contains(target)) {
                return true;
            }
            return false;
        }

        /** Diálogos en document.body que deben recibir foco y teclado aunque el explorador tenga "focus lock". */
        function isExplorerKeyboardLockExemptTarget(target) {
            if (!target || !target.closest) return false;
            return !!target.closest(
                '.grid-app-dialog, #explorerRenameModal, #explorerConflictModal, #explorerNewFolderChoiceModal, #explorerThemeModal, #explorerCustModal, #explorerToolbarCustModal, #addAppModal, #configModal, #addCategoryModal, #editCategoryIconModal, #recycleBinModal, #windowsMosaicModal, #previewModal'
            );
        }

        function focusExplorerPrimaryElement() {
            const explorerFrame = getTopVisibleExplorerFrame();
            if (!explorerFrame) return;
            const ae = document.activeElement;
            if (ae && ae.closest && ae.closest('.window-frame .file-explorer')) {
                const tag = (ae.nodeName || '').toLowerCase();
                if (tag === 'input' || tag === 'textarea' || tag === 'select') return;
                if (ae.closest('.fe-search-wrap')) return;
            }
            const primary = explorerFrame.querySelector('.explorer-items, .fe-search-input, .fe-tree, .fe-toolbar button, .fe-sidebar-toggle, .window-titlebar .close-btn');
            if (primary && typeof primary.focus === 'function') {
                try { primary.focus(); } catch (_) {}
            }
        }

        if (!window.__gridExplorerDocKeydownCaptureInstalled) {
            window.__gridExplorerDocKeydownCaptureInstalled = true;
            document.addEventListener('keydown', (e) => {
                if (!isExplorerFocusLockActive()) return;
                if (isExplorerKeyboardLockExemptTarget(e.target)) return;
                if (isTargetInsideExplorerScope(e.target)) {
                    if ((e.key === 'Control' || e.code === 'ControlLeft' || e.code === 'ControlRight') && !isTextEditingTarget(e.target)) {
                        e.preventDefault();
                        e.stopPropagation();
                    }
                    return;
                }
                const k = e.key || '';
                const code = e.code || '';
                const isExplorerShortcut = (e.ctrlKey || e.metaKey) && (
                    k === 'a' || k === 'A' || k === 'c' || k === 'C' || k === 'x' || k === 'X' || k === 'v' || k === 'V' ||
                    code === 'KeyA' || code === 'KeyC' || code === 'KeyX' || code === 'KeyV'
                );
                const isExplorerNavKey = k === 'ArrowRight' || k === 'ArrowLeft' || k === 'ArrowDown' || k === 'ArrowUp' ||
                    k === 'Enter' || k === 'Backspace' || k === 'Delete' || k === 'F2';
                const isExplorerTypeAheadPrintable = !e.ctrlKey && !e.metaKey && !e.altKey && !e.isComposing &&
                    k.length === 1 && k >= ' ' && k !== '\x7f';
                if (!isExplorerShortcut && !isExplorerNavKey && !isExplorerTypeAheadPrintable) return;
                const frame = getTopVisibleExplorerFrame();
                if (!frame) return;
                const hostFe = frame.querySelector('.file-explorer');
                const itemsEl0 = hostFe && hostFe.querySelector('.explorer-items');
                if (!itemsEl0) return;
                e.preventDefault();
                e.stopPropagation();
                focusExplorerPrimaryElement();
                queueMicrotask(() => {
                    itemsEl0.dispatchEvent(new KeyboardEvent('keydown', {
                        key: e.key,
                        code: e.code || '',
                        ctrlKey: e.ctrlKey,
                        metaKey: e.metaKey,
                        shiftKey: e.shiftKey,
                        altKey: e.altKey,
                        bubbles: true,
                        cancelable: true
                    }));
                });
            }, true);
        }

        function blockIfExplorerFocusLock(event) {
            if (!isExplorerFocusLockActive()) return false;
            if (isTargetInsideExplorerScope(event.target)) return false;
            if (isExplorerKeyboardLockExemptTarget(event.target)) return false;
            event.preventDefault();
            event.stopPropagation();
            focusExplorerPrimaryElement();
            return true;
        }

        function writeOpenWindowsSessionPayload(json) {
            try {
                localStorage.setItem(OPEN_WINDOWS_SESSION_KEY, json);
                return true;
            } catch (_) {
                try {
                    sessionStorage.setItem(OPEN_WINDOWS_SESSION_KEY, json);
                    return true;
                } catch (_) {}
            }
            return false;
        }

        /** Devuelve JSON string solo si el payload es un array con al menos una ventana (sesión útil). */
        function normalizeOpenWindowsSessionPayload(raw) {
            if (raw == null || raw === '') return null;
            const str = typeof raw === 'string' ? raw : (() => {
                try {
                    return JSON.stringify(raw);
                } catch (_) {
                    return '';
                }
            })();
            if (!str) return null;
            try {
                const arr = JSON.parse(str);
                if (!Array.isArray(arr) || arr.length === 0) return null;
                return str;
            } catch (_) {
                return null;
            }
        }

        /**
         * Orden: snapshot en servidor (settings.json) si tiene ventanas → mismo navegador (localStorage) → sessionStorage.
         * Así otra PC ve el escritorio guardado en el servidor; si el servidor aún está en [] pero esta máquina tenía sesión local, se restaura lo local.
         */
        function readOpenWindowsSessionRaw() {
            const fromSettings = settings && settings[SHARED_OPEN_WINDOWS_SETTINGS_KEY];
            const server = normalizeOpenWindowsSessionPayload(fromSettings);
            if (server) return server;
            try {
                const a = localStorage.getItem(OPEN_WINDOWS_SESSION_KEY);
                const local = normalizeOpenWindowsSessionPayload(a);
                if (local) return local;
            } catch (_) {}
            try {
                const b = sessionStorage.getItem(OPEN_WINDOWS_SESSION_KEY);
                return normalizeOpenWindowsSessionPayload(b);
            } catch (_) {
                return null;
            }
        }

        function buildOpenWindowsSnapshot() {
            return (openWindows || [])
                .map((w) => {
                    const frame = w?.frame || document.getElementById(w?.id || '');
                    if (!frame || !w?.app) return null;
                    const app = w.app;
                    const appGroup = activeTaskbarApps.find(a => a.appId === getAppStableId(app));
                    const winObj = appGroup?.windows?.find(aw => aw.id === w.id);
                    const kind = (app.id === 'file-explorer-system' || app.lastAction === 'openExplorer') ? 'explorer' : (app.fileInfo ? 'file' : 'link');
                    let explorerState = null;
                    if (kind === 'explorer') {
                        try {
                            const host = frame.querySelector('[id^="explorerHost_"]');
                            if (host && typeof host.__gridGetState === 'function') {
                                explorerState = host.__gridGetState();
                            }
                        } catch (_) {}
                    }
                    return {
                        kind,
                        appStableId: getAppStableId(app),
                        title: app.title || '',
                        link: app.link || '',
                        linkTarget: app.linkTarget || 'window',
                        icon: app.icon || '',
                        fileInfo: app.fileInfo ? {
                            url: app.fileInfo.url || '',
                            type: app.fileInfo.type || '',
                            name: app.fileInfo.name || ''
                        } : null,
                        explorerOpen: app.explorerOpen && app.explorerOpen.bucket ? {
                            bucket: app.explorerOpen.bucket,
                            relPath: app.explorerOpen.relPath || ''
                        } : null,
                        libraryExplorerShortcut: !!(app.libraryExplorerShortcut || (app.explorerOpen && app.explorerOpen.bucket)),
                        explorerState,
                        zIndex: parseInt(frame.style.zIndex || '0', 10) || 0,
                        top: frame.style.top || '',
                        left: frame.style.left || '',
                        width: frame.style.width || '',
                        height: frame.style.height || '',
                        maximized: frame.classList.contains('maximized'),
                        minimized: frame.classList.contains('minimized') || frame.style.display === 'none' || !!winObj?.isMinimized,
                        savedRect: {
                            top: frame.dataset.savedTop || '',
                            left: frame.dataset.savedLeft || '',
                            width: frame.dataset.savedWidth || '',
                            height: frame.dataset.savedHeight || ''
                        },
                        windowFrameClass: app.windowFrameClass || ''
                    };
                })
                .filter(Boolean)
                .sort((a, b) => (a.zIndex || 0) - (b.zIndex || 0));
        }

        function schedulePersistSharedOpenWindows(payload) {
            if (__restoringOpenWindowsSession) return;
            if (__persistOpenWindowsSharedTimer) clearTimeout(__persistOpenWindowsSharedTimer);
            __persistOpenWindowsSharedTimer = setTimeout(async () => {
                try {
                    const normalized = String(payload || '[]');
                    if (normalized === __lastSharedOpenWindowsPayload) return;
                    if (settings && settings[SHARED_OPEN_WINDOWS_SETTINGS_KEY] === normalized) {
                        __lastSharedOpenWindowsPayload = normalized;
                        return;
                    }
                    __lastSharedOpenWindowsPayload = normalized;
                    await saveSettings({ [SHARED_OPEN_WINDOWS_SETTINGS_KEY]: normalized });
                } catch (_) {}
            }, 400);
        }

        function persistSharedOpenWindowsNow(payload) {
            try {
                const normalized = String(payload || '[]');
                if (settings) settings[SHARED_OPEN_WINDOWS_SETTINGS_KEY] = normalized;
                __lastSharedOpenWindowsPayload = normalized;
                fetch('?action=saveSettings', {
                    method: 'POST',
                    headers: { 'Content-Type': 'application/json' },
                    body: JSON.stringify({ [SHARED_OPEN_WINDOWS_SETTINGS_KEY]: normalized }),
                    keepalive: true
                }).catch(() => {});
            } catch (_) {}
        }

        function schedulePersistOpenWindowsSession() {
            if (__restoringOpenWindowsSession) return;
            const run = () => {
                try {
                    const snapshot = buildOpenWindowsSnapshot();
                    const payload = JSON.stringify(snapshot);
                    writeOpenWindowsSessionPayload(payload);
                    schedulePersistSharedOpenWindows(payload);
                } catch (_) {}
            };
            if (__persistOpenWindowsTimer) clearTimeout(__persistOpenWindowsTimer);
            __persistOpenWindowsTimer = setTimeout(run, 120);
        }

        function resolveAppFromWindowSnapshot(s) {
            if (!s) return null;
            if (s.kind === 'explorer') {
                const sid = s.appStableId || '';
                const fromApps = (apps || []).find(a => getAppStableId(a) === sid);
                if (fromApps && fromApps.explorerOpen && fromApps.explorerOpen.bucket) return fromApps;
                if (s.explorerOpen && s.explorerOpen.bucket) {
                    return {
                        title: s.title || (s.explorerOpen.relPath ? s.explorerOpen.relPath.split('/').pop() : '') || 'Explorador',
                        icon: s.icon || FILE_EXPLORER_APP.icon,
                        category: tr('explorerShortcutCategory'),
                        lastAction: 'openExplorer',
                        link: '#',
                        linkTarget: '_self',
                        showInGrid: false,
                        explorerOpen: {
                            bucket: s.explorerOpen.bucket,
                            relPath: s.explorerOpen.relPath || ''
                        },
                        libraryExplorerShortcut: true
                    };
                }
                return FILE_EXPLORER_APP;
            }
            if (s.kind === 'file' && s.fileInfo && s.fileInfo.url) {
                return {
                    title: s.title || (s.fileInfo.name || 'Archivo'),
                    icon: s.icon || FILE_EXPLORER_APP.icon,
                    lastAction: 'file',
                    fileInfo: {
                        url: s.fileInfo.url,
                        type: s.fileInfo.type || '',
                        name: s.fileInfo.name || s.title || 'archivo'
                    },
                    libraryExplorerShortcut: !!s.libraryExplorerShortcut
                };
            }
            const fromApps = (apps || []).find(a => getAppStableId(a) === s.appStableId || ((a.title || '') === (s.title || '') && (a.link || '') === (s.link || '')));
            if (fromApps) return fromApps;
            if (s.link) {
                return {
                    title: s.title || s.link,
                    icon: s.icon || FILE_EXPLORER_APP.icon,
                    lastAction: 'link',
                    link: s.link,
                    linkTarget: s.linkTarget || 'window',
                    windowFrameClass: s.windowFrameClass || ''
                };
            }
            return null;
        }

        function restoreOpenWindowsSession() {
            const raw = readOpenWindowsSessionRaw();
            if (!raw) return;
            let arr = [];
            try {
                arr = JSON.parse(raw);
            } catch (_) {
                return;
            }
            if (!Array.isArray(arr) || arr.length === 0) return;

            __restoringOpenWindowsSession = true;
            try {
                arr.forEach((s) => {
                    const app = resolveAppFromWindowSnapshot(s);
                    if (!app) return;
                    const restoreState = {
                        zIndex: s.zIndex || 0,
                        top: s.top || '',
                        left: s.left || '',
                        width: s.width || '',
                        height: s.height || '',
                        maximized: !!s.maximized,
                        minimized: !!s.minimized,
                        savedRect: s.savedRect || null,
                        explorerState: s.explorerState || null
                    };
                    if (s.kind === 'explorer') {
                        openFileExplorerWindow(app, restoreState);
                    } else if (s.kind === 'file') {
                        openFileWindowFrame(app, restoreState);
                    } else {
                        openWindowFrame(app, restoreState);
                    }
                });
            } catch (_) {}
            __restoringOpenWindowsSession = false;
            schedulePersistOpenWindowsSession();
        }

        window.addEventListener('beforeunload', () => {
            if (__persistOpenWindowsTimer) clearTimeout(__persistOpenWindowsTimer);
            if (__persistOpenWindowsSharedTimer) clearTimeout(__persistOpenWindowsSharedTimer);
            try {
                const snapshot = buildOpenWindowsSnapshot();
                const payload = JSON.stringify(snapshot);
                writeOpenWindowsSessionPayload(payload);
                persistSharedOpenWindowsNow(payload);
            } catch (_) {}
        });
        window.addEventListener('pagehide', () => {
            if (__persistOpenWindowsTimer) clearTimeout(__persistOpenWindowsTimer);
            if (__persistOpenWindowsSharedTimer) clearTimeout(__persistOpenWindowsSharedTimer);
            try {
                const snapshot = buildOpenWindowsSnapshot();
                const payload = JSON.stringify(snapshot);
                writeOpenWindowsSessionPayload(payload);
                persistSharedOpenWindowsNow(payload);
            } catch (_) {}
        });

        document.addEventListener('visibilitychange', () => {
            if (document.visibilityState !== 'hidden' || __restoringOpenWindowsSession) return;
            try {
                const snapshot = buildOpenWindowsSnapshot();
                const payload = JSON.stringify(snapshot);
                writeOpenWindowsSessionPayload(payload);
                persistSharedOpenWindowsNow(payload);
            } catch (_) {}
        });

        const actionTypeRadios = document.querySelectorAll('input[name="actionType"]');
        const linkInput = document.getElementById('linkInput');
        const fileInput = document.getElementById('fileInput');
        const appLinkInput = document.getElementById('appLink');
        const appFileInput = document.getElementById('appFile');
        const currentFileInfo = document.getElementById('currentFileInfo');
        const currentFileName = document.getElementById('currentFileName');
        const previewLinkBtn = document.getElementById('previewLinkBtn');
        const linkTargetRadios = document.querySelectorAll('input[name="linkTarget"]');
        const showInGridCheckbox = document.getElementById('showInGrid');
        
        // App de papelera para móvil (no se puede borrar)
        const RECYCLE_BIN_APP = {
            id: 'recycle-bin-system',
            title: 'Papelera',
            icon: 'data:image/svg+xml,%3Csvg xmlns=\'http://www.w3.org/2000/svg\' viewBox=\'0 0 24 24\'%3E%3Cpath fill=\'%23999\' d=\'M6 19c0 1.1.9 2 2 2h8c1.1 0 2-.9 2-2V7H6v12zM19 4h-3.5l-1-1h-5l-1 1H5v2h14V4z\'/%3E%3C/svg%3E',
            description: 'Papelera de reciclaje',
            category: 'Sistema',
            lastAction: 'link',
            link: '#',
            linkTarget: '_self',
            showInGrid: true,
            isSystemApp: true,
            action: 'openRecycleBin'
        };

        // App del explorador tipo Windows (siempre anclada a la barra de tareas).
        const FILE_EXPLORER_APP = {
            id: 'file-explorer-system',
            title: 'Explorador',
            icon: 'data:image/svg+xml,%3Csvg xmlns=\'http://www.w3.org/2000/svg\' viewBox=\'0 0 24 24\'%3E%3Cpath fill=\'%23999\' d=\'M10 4H4c-1.1 0-2 .9-2 2v12c0 1.1.9 2 2 2h16c1.1 0 2-.9 2-2V8l-5-4h-4zM10 6h3.6l3.4 2.7V18H4V6h6z\'/%3E%3Cpath fill=\'%23999\' d=\'M6 10h12v2H6v-2zm0 4h8v2H6v-2z\'/%3E%3C/svg%3E',
            description: 'Explorador de archivos',
            category: 'Sistema',
            lastAction: 'openExplorer',
            link: '#',
            linkTarget: '_self',
            showInGrid: true,
            isSystemApp: true,
            action: 'openFileExplorer',
            isPinned: true
        };

        const SKETCHIA_APP_ID = 'sketchia-system';
        const SKETCHIA_APP_LINK = 'arquitect-app/index.html';
        /** App de sistema Sketchia (mismo icono que el Explorador); anclaje inicial solo en instalación nueva. */
        const SKETCHIA_APP = {
            id: SKETCHIA_APP_ID,
            title: 'SkechIA',
            icon: 'arquitect-app/icon.png',
            description: 'Dibujo y diseño con capas e IA',
            category: 'Sistema',
            lastAction: 'link',
            link: SKETCHIA_APP_LINK,
            linkTarget: 'window',
            showInGrid: true,
            isSystemApp: true,
            isPinned: true
        };

        function isStartMenuSystemApp(app) {
            return !!(app && (app.id === 'file-explorer-system' || app.id === SKETCHIA_APP_ID));
        }

        function sketchiaLinkMatchesStored(link) {
            if (link == null) return false;
            const lk = String(link).replace(/^\.\//, '').replace(/\\/g, '/');
            return lk === SKETCHIA_APP_LINK || lk.endsWith(SKETCHIA_APP_LINK);
        }

        // Asegurar que el explorador esté siempre disponible (grid + windows menu + barra).
        (function ensureFileExplorerSystemApp() {
            try {
                if (!apps.some(a => a && a.id === FILE_EXPLORER_APP.id)) {
                    apps.unshift({ ...FILE_EXPLORER_APP });
                }
                const needPin = !pinnedApps.some(p => p && p.title === FILE_EXPLORER_APP.title && (p.link || '') === (FILE_EXPLORER_APP.link || ''));
                if (needPin) {
                    pinnedApps.unshift({ title: FILE_EXPLORER_APP.title, link: FILE_EXPLORER_APP.link || '', icon: FILE_EXPLORER_APP.icon });
                    saveSettings({ pinnedApps: pinnedApps }).then(() => {
                        renderPinnedApps();
                        renderTaskbarApps();
                    });
                }
            } catch (_) {}
        })();

        // Sketchia: app fija de sistema (no desinstalable); escritorio y barra anclada solo la primera vez.
        (function ensureSketchiaSystemApp() {
            try {
                let appsMutated = false;
                let pinsMutated = false;
                let wasBrandNew = false;

                let legacyIdx = apps.findIndex(a => a && a.id === 'arquitect-studio-app');
                let sketchIdx = apps.findIndex(a => a && a.id === SKETCHIA_APP_ID);

                if (legacyIdx >= 0 && sketchIdx >= 0) {
                    apps.splice(legacyIdx, 1);
                    appsMutated = true;
                    legacyIdx = apps.findIndex(a => a && a.id === 'arquitect-studio-app');
                    sketchIdx = apps.findIndex(a => a && a.id === SKETCHIA_APP_ID);
                }

                if (legacyIdx >= 0 && sketchIdx < 0) {
                    const leg = apps[legacyIdx];
                    Object.assign(leg, {
                        id: SKETCHIA_APP_ID,
                        title: SKETCHIA_APP.title,
                        icon: SKETCHIA_APP.icon,
                        description: SKETCHIA_APP.description,
                        category: SKETCHIA_APP.category,
                        lastAction: 'link',
                        link: SKETCHIA_APP.link,
                        linkTarget: 'window',
                        isSystemApp: true
                    });
                    if (leg.showInGrid === undefined) leg.showInGrid = true;
                    appsMutated = true;
                } else if (sketchIdx < 0) {
                    wasBrandNew = true;
                    const expIdx = apps.findIndex(a => a && a.id === FILE_EXPLORER_APP.id);
                    if (expIdx >= 0) {
                        apps.splice(expIdx + 1, 0, { ...SKETCHIA_APP });
                    } else {
                        apps.unshift({ ...SKETCHIA_APP });
                    }
                    appsMutated = true;
                } else {
                    sketchIdx = apps.findIndex(a => a && a.id === SKETCHIA_APP_ID);
                    if (sketchIdx >= 0) {
                        const sk = apps[sketchIdx];
                        const c = SKETCHIA_APP;
                        if (sk.icon !== c.icon || sk.title !== c.title || sk.description !== c.description ||
                            sk.link !== c.link || sk.linkTarget !== c.linkTarget || sk.lastAction !== c.lastAction ||
                            sk.category !== c.category || sk.isSystemApp !== true) {
                            Object.assign(sk, {
                                title: c.title,
                                icon: c.icon,
                                description: c.description,
                                category: c.category,
                                lastAction: c.lastAction,
                                link: c.link,
                                linkTarget: c.linkTarget,
                                isSystemApp: true
                            });
                            appsMutated = true;
                        }
                    }
                }

                pinnedApps.forEach((p) => {
                    if (!p || !sketchiaLinkMatchesStored(p.link)) return;
                    if (p.title !== SKETCHIA_APP.title || p.icon !== SKETCHIA_APP.icon) {
                        p.title = SKETCHIA_APP.title;
                        p.icon = SKETCHIA_APP.icon;
                        pinsMutated = true;
                    }
                });

                if (wasBrandNew) {
                    const pinKey = getAppLinkKey(SKETCHIA_APP);
                    const alreadyPinned = pinnedApps.some(p => p && pinMatchesApp(p, SKETCHIA_APP));
                    if (!alreadyPinned) {
                        const exTitle = FILE_EXPLORER_APP.title;
                        const exLink = FILE_EXPLORER_APP.link || '';
                        const exIdx = pinnedApps.findIndex(p => p && p.title === exTitle && (p.link || '') === exLink);
                        const entry = { title: SKETCHIA_APP.title, link: pinKey, icon: SKETCHIA_APP.icon };
                        if (exIdx >= 0) {
                            pinnedApps.splice(exIdx + 1, 0, entry);
                        } else {
                            pinnedApps.unshift(entry);
                        }
                        pinsMutated = true;
                    }
                }

                if (appsMutated || pinsMutated) {
                    saveSettings({ apps: apps, pinnedApps: pinnedApps }).then(() => {
                        renderPinnedApps();
                        renderTaskbarApps();
                        renderApps();
                        if (windowsMenuVisible) renderWindowsMenu(apps);
                    });
                }
            } catch (_) {}
        })();

        // ========== FUNCIONES DE ZOOM Y FULLSCREEN ==========
        let currentZoom = getSetting('pageZoom', 1);
        
        function applyPageZoom(zoom) {
            currentZoom = zoom;
            document.body.style.zoom = currentZoom;
            localStorage.setItem(gridZoomStorageKey, currentZoom);
            try {
                if (window.OsShell && typeof window.OsShell.syncStartMenuLayout === 'function') {
                    requestAnimationFrame(function () {
                        requestAnimationFrame(function () {
                            window.OsShell.syncStartMenuLayout();
                        });
                    });
                }
            } catch (_) {}
        }
        
        function zoomPageIn() {
            currentZoom = Math.min(currentZoom + 0.1, 2);
            applyPageZoom(currentZoom);
        }
        
        function zoomPageOut() {
            currentZoom = Math.max(currentZoom - 0.1, 0.5);
            applyPageZoom(currentZoom);
        }
        
        function toggleFullscreen() {
            if (!document.fullscreenElement) {
                document.documentElement.requestFullscreen();
                fullscreenBtn.innerHTML = '<i class="fas fa-compress"></i>';
            } else {
                document.exitFullscreen();
                fullscreenBtn.innerHTML = '<i class="fas fa-expand"></i>';
            }
        }
        
        document.addEventListener('fullscreenchange', () => {
            if (document.fullscreenElement) {
                fullscreenBtn.innerHTML = '<i class="fas fa-compress"></i>';
            } else {
                fullscreenBtn.innerHTML = '<i class="fas fa-expand"></i>';
            }
        });
        
        // Funciones de zoom solo para imagen en preview modal
        const IMAGE_ZOOM_MIN = 1;
        const IMAGE_ZOOM_MAX = 3;
        const IMAGE_ZOOM_STEP = 0.2;
        let previewZoomMode = 'none';
        let previewPdfZoom = 1;

        function applyPreviewPdfZoom() {
            const el = document.querySelector('#previewContent .pdf-canvas-scroll-wrap');
            applyPdfCanvasLayerZoom(el, previewPdfZoom);
        }

        /** Vista previa dividida del explorador: zoom PDF (PDF.js en el panel). */
        function applyFeExplorerPdfZoom(fePreviewBody) {
            if (!fePreviewBody) return;
            const el = fePreviewBody.querySelector('.pdf-canvas-scroll-wrap');
            if (el) applyPdfCanvasLayerZoom(el, fePreviewBody._fePdfZoom || 1);
        }

        function feExplorerImgZoomToAt(state, imgEl, wrapEl, newScale, clientX, clientY) {
            if (!imgEl || !wrapEl) return;
            const clampedScale = Math.max(IMAGE_ZOOM_MIN, Math.min(newScale, IMAGE_ZOOM_MAX));
            const oldScale = state.scale || 1;
            if (clampedScale === IMAGE_ZOOM_MIN) {
                state.scale = 1;
                state.translateX = 0;
                state.translateY = 0;
                state.isZoomed = false;
                imgEl.classList.remove('fe-preview-img--zoomed');
                imgEl.style.transform = `translate(calc(-50% + 0px), calc(-50% + 0px)) scale(1)`;
                return;
            }
            const rect = wrapEl.getBoundingClientRect();
            const cx = rect.left + rect.width / 2;
            const cy = rect.top + rect.height / 2;
            const px = (clientX - cx - state.translateX) / oldScale;
            const 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 > IMAGE_ZOOM_MIN;
            imgEl.classList.toggle('fe-preview-img--zoomed', state.isZoomed);
            imgEl.style.transform = `translate(calc(-50% + ${state.translateX}px), calc(-50% + ${state.translateY}px)) scale(${state.scale})`;
        }

        function feExplorerImgZoomTo(state, imgEl, wrapEl, newScale) {
            const clamped = Math.max(IMAGE_ZOOM_MIN, Math.min(newScale, IMAGE_ZOOM_MAX));
            if (!wrapEl) {
                state.scale = clamped;
                state.isZoomed = clamped > IMAGE_ZOOM_MIN;
                if (!state.isZoomed) {
                    state.translateX = 0;
                    state.translateY = 0;
                }
                if (imgEl) {
                    imgEl.classList.toggle('fe-preview-img--zoomed', state.isZoomed);
                    imgEl.style.transform = `translate(calc(-50% + ${state.translateX}px), calc(-50% + ${state.translateY}px)) scale(${state.scale})`;
                }
                return;
            }
            const r = wrapEl.getBoundingClientRect();
            feExplorerImgZoomToAt(state, imgEl, wrapEl, clamped, r.left + r.width / 2, r.top + r.height / 2);
        }

        function handleFeExplorerImageWheel(e, wrapEl, state, imgEl) {
            const dy = typeof e.deltaY === 'number' ? e.deltaY : 0;
            const dx = typeof e.deltaX === 'number' ? e.deltaX : 0;
            if (!dx && !dy) return;
            const verticalDominant = Math.abs(dy) >= Math.abs(dx);
            if (verticalDominant) {
                e.preventDefault();
                const intensity = 0.0015;
                const factor = Math.exp(-dy * intensity);
                feExplorerImgZoomToAt(state, imgEl, wrapEl, state.scale * factor, e.clientX, e.clientY);
                return;
            }
            if (state.scale <= IMAGE_ZOOM_MIN) return;
            e.preventDefault();
            state.translateX -= dx;
            state.translateY -= dy;
            imgEl.style.transform = `translate(calc(-50% + ${state.translateX}px), calc(-50% + ${state.translateY}px)) scale(${state.scale})`;
        }

        function setupFeExplorerSplitPinchZoom(wrapEl, imgEl, state) {
            if (!wrapEl || !imgEl) return;
            const PINCH_EXP = 0.9;
            const RATIO_DEADZONE = 0.006;
            const pointers = new Map();
            let pinchStartDist = 0;
            let pinchStartZoom = 1;
            let pinchAnchor = null;
            function updatePointer(ev) {
                pointers.set(ev.pointerId, { x: ev.clientX, y: ev.clientY });
            }
            function pointerDown(ev) {
                if (ev.pointerType !== 'touch') return;
                updatePointer(ev);
                if (pointers.size === 2) {
                    wrapEl.style.touchAction = 'none';
                    const pts = [...pointers.values()];
                    const dx = pts[0].x - pts[1].x;
                    const 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 (_) {}
                }
            }
            function pointerMove(ev) {
                if (ev.pointerType !== 'touch') return;
                if (!pointers.has(ev.pointerId)) return;
                updatePointer(ev);
                if (pointers.size === 2 && pinchStartDist > 0) {
                    const pts = [...pointers.values()];
                    const dx = pts[0].x - pts[1].x;
                    const dy = pts[0].y - pts[1].y;
                    const dist = Math.hypot(dx, dy);
                    const rawRatio = dist / pinchStartDist;
                    const ratio = Math.pow(rawRatio, PINCH_EXP);
                    if (Math.abs(ratio - 1) < RATIO_DEADZONE) return;
                    pinchAnchor = { x: (pts[0].x + pts[1].x) / 2, y: (pts[0].y + pts[1].y) / 2 };
                    const next = pinchStartZoom * ratio;
                    feExplorerImgZoomToAt(state, imgEl, wrapEl, next, pinchAnchor.x, pinchAnchor.y);
                    try { ev.preventDefault(); } catch (_) {}
                }
            }
            function pointerUp(ev) {
                pointers.delete(ev.pointerId);
                if (pointers.size < 2) {
                    pinchStartDist = 0;
                    pinchAnchor = null;
                    wrapEl.style.touchAction = '';
                }
            }
            if (typeof window.PointerEvent !== 'undefined') {
                wrapEl.addEventListener('pointerdown', pointerDown, { passive: false });
                wrapEl.addEventListener('pointermove', pointerMove, { passive: false });
                wrapEl.addEventListener('pointerup', pointerUp);
                wrapEl.addEventListener('pointercancel', pointerUp);
            }
        }

        function mountFeExplorerSplitImageViewer(fePreviewBody, displayUrl, secureUrl, fileName) {
            const state = { scale: 1, translateX: 0, translateY: 0, isZoomed: false };
            fePreviewBody._feSplitCtx = { kind: 'image', secureUrl, fileName, fileUrl: secureUrl || displayUrl, imageState: state, imageEls: null };
            fePreviewBody.innerHTML = '';
            const surface = document.createElement('div');
            surface.className = 'fe-preview-surface fe-preview-surface--image';
            const box = document.createElement('div');
            box.className = 'fe-preview-image-box';
            const img = document.createElement('img');
            img.src = displayUrl;
            img.className = 'fe-preview-img';
            img.draggable = false;
            img.alt = '';
            img.style.transformOrigin = 'center center';
            img.style.transform = 'translate(-50%, -50%) scale(1)';
            box.appendChild(img);
            surface.appendChild(box);
            fePreviewBody.appendChild(surface);
            let pid = null;
            let dragging = false;
            let ox = 0;
            let oy = 0;
            box.addEventListener('wheel', (e) => handleFeExplorerImageWheel(e, box, state, img), { passive: false });
            box.addEventListener(
                'pointerdown',
                (e) => {
                    if (state.scale <= IMAGE_ZOOM_MIN) return;
                    if (e.pointerType === 'mouse' && e.button !== 0) return;
                    e.preventDefault();
                    pid = e.pointerId;
                    dragging = true;
                    try {
                        box.setPointerCapture(pid);
                    } catch (_) {}
                    ox = e.clientX - state.translateX;
                    oy = e.clientY - state.translateY;
                },
                { passive: false }
            );
            box.addEventListener(
                'pointermove',
                (e) => {
                    if (!dragging || pid === null || e.pointerId !== pid) return;
                    if (state.scale <= IMAGE_ZOOM_MIN) return;
                    e.preventDefault();
                    state.translateX = e.clientX - ox;
                    state.translateY = e.clientY - oy;
                    img.style.transform = `translate(calc(-50% + ${state.translateX}px), calc(-50% + ${state.translateY}px)) scale(${state.scale})`;
                },
                { passive: false }
            );
            const endPan = (e) => {
                if (pid === null || e.pointerId !== pid) return;
                dragging = false;
                try {
                    box.releasePointerCapture(pid);
                } catch (_) {}
                pid = null;
            };
            box.addEventListener('pointerup', endPan);
            box.addEventListener('pointercancel', endPan);
            setupFeExplorerSplitPinchZoom(box, img, state);
            fePreviewBody._feSplitCtx.imageEls = { img, box };
        }

        /** Área visible real (Chrome/Android con teclado virtual: visualViewport). */
        function getLayoutViewportRect() {
            const vv = window.visualViewport;
            if (vv) {
                return {
                    width: vv.width,
                    height: vv.height,
                    left: vv.offsetLeft,
                    top: vv.offsetTop
                };
            }
            return {
                width: window.innerWidth,
                height: window.innerHeight,
                left: 0,
                top: 0
            };
        }

        let previewViewportLayoutRaf = null;
        function schedulePreviewVisualViewportLayout() {
            if (previewViewportLayoutRaf !== null) cancelAnimationFrame(previewViewportLayoutRaf);
            previewViewportLayoutRaf = requestAnimationFrame(() => {
                previewViewportLayoutRaf = null;
                applyPreviewVisualViewportLayout();
            });
        }

        function initPreviewViewportWidthSnapshot() {
            // Usamos un ancho "estable" que no cambia con el pinch-zoom del navegador.
            // visualViewport.width disminuye cuando el usuario pellizca (scale>1) y eso
            // podría disparar resets de zoom falsos.
            const vv = window.visualViewport;
            const stableW = vv ? (vv.width * (vv.scale || 1)) : window.innerWidth;
            previewViewportWidthSnapshot = stableW;
        }

        function detachPreviewFeTabsObserver() {
            if (previewFeTabsObserver) {
                previewFeTabsObserver.disconnect();
                previewFeTabsObserver = null;
            }
        }

        /**
         * Si aparece una segunda pestaña del explorador (p. ej. remota) con zoom activo, restaurar escala.
         */
        function attachPreviewFeTabsObserver() {
            detachPreviewFeTabsObserver();
            if (!activePreviewImg) return;
            if (previewZoomMode === 'pdf') return;
            let debounce = null;
            const run = () => {
                clearTimeout(debounce);
                debounce = setTimeout(() => {
                    if (!activePreviewImg || !isImageZoomed) return;
                    if (document.querySelectorAll('.fe-tab').length >= 2) {
                        resetImageZoom();
                    }
                }, 50);
            };
            previewFeTabsObserver = new MutationObserver(run);
            previewFeTabsObserver.observe(document.body, { childList: true, subtree: true });
        }

        /**
         * Pantalla dividida (dos apps) o ≥2 pestañas en el explorador + cambio de ancho: quitar zoom de imagen.
         */
        function maybeResetImageZoomOnViewportResize() {
            if (!activePreviewImg || !isImageZoomed) return;
            if (previewZoomMode === 'pdf') return;
            // Si el usuario está interactuando (pan/pinch), no resetear.
            if (__imageInteractionActiveUntil && Date.now() < __imageInteractionActiveUntil) return;
            // En el visor FTP, NO hacer zoom-out automático al panear (visualViewport cambia en móviles).
            // Mantener el auto-reset (split screen / 2 tabs) para otros contextos.
            if (isFtpPreviewImageActive()) return;
            const vv = window.visualViewport;
            const w = vv ? (vv.width * (vv.scale || 1)) : window.innerWidth;
            if (previewViewportWidthSnapshot <= 0) {
                previewViewportWidthSnapshot = w;
                return;
            }
            const dw = Math.abs(w - previewViewportWidthSnapshot);
            if (dw <= 48) return;
            const twoExplorerTabs = document.querySelectorAll('.fe-tab').length >= 2;
            const isMobileUA = /Android|iPhone|iPad|iPod/i.test(navigator.userAgent || '');
            let splitScreenLikely = false;
            if (isMobileUA && window.screen && window.screen.width > 0) {
                splitScreenLikely = w <= window.screen.width * 0.51;
            }
            if (twoExplorerTabs || splitScreenLikely) {
                resetImageZoom();
                return;
            }
            previewViewportWidthSnapshot = w;
        }

        function applyPreviewVisualViewportLayout() {
            const modal = document.getElementById('previewModal');
            const frame = document.getElementById('previewWindowFrame');
            if (!modal || !frame || modal.style.display !== 'block') return;
            const vv = window.visualViewport;
            if (!vv) {
                modal.style.top = '';
                modal.style.left = '';
                modal.style.width = '';
                modal.style.height = '';
                return;
            }
            modal.style.boxSizing = 'border-box';
            modal.style.top = vv.offsetTop + 'px';
            modal.style.left = vv.offsetLeft + 'px';
            modal.style.width = vv.width + 'px';
            modal.style.height = vv.height + 'px';

            const pad = 6;
            const maxW = Math.max(120, vv.width - pad * 2);
            const maxH = Math.max(120, vv.height - pad * 2);

            if (frame.classList.contains('maximized')) {
                frame.style.top = vv.offsetTop + 'px';
                frame.style.left = vv.offsetLeft + 'px';
                frame.style.width = vv.width + 'px';
                frame.style.height = vv.height + 'px';
                frame.style.right = 'auto';
                frame.style.bottom = 'auto';
            } else {
                let fw = parseInt(frame.style.width, 10);
                let fh = parseInt(frame.style.height, 10);
                if (!Number.isFinite(fw) || fw <= 0) fw = Math.min(920, maxW);
                if (!Number.isFinite(fh) || fh <= 0) fh = Math.min(680, maxH);
                fw = Math.min(fw, maxW);
                fh = Math.min(fh, maxH);
                let fl = parseInt(frame.style.left, 10);
                let ft = parseInt(frame.style.top, 10);
                if (!Number.isFinite(fl)) fl = vv.offsetLeft + 50;
                if (!Number.isFinite(ft)) ft = vv.offsetTop + 60;
                fl = Math.max(vv.offsetLeft + pad, Math.min(fl, vv.offsetLeft + vv.width - fw - pad));
                ft = Math.max(vv.offsetTop + pad, Math.min(ft, vv.offsetTop + vv.height - fh - pad));
                frame.style.width = fw + 'px';
                frame.style.height = fh + 'px';
                frame.style.left = fl + 'px';
                frame.style.top = ft + 'px';
            }
            if (previewZoomMode === 'pdf') applyPreviewPdfZoom();
        }

        function resetPreviewModalViewportStyles() {
            const modal = document.getElementById('previewModal');
            const frame = document.getElementById('previewWindowFrame');
            if (modal) {
                modal.style.boxSizing = '';
                modal.style.top = '';
                modal.style.left = '';
                modal.style.width = '';
                modal.style.height = '';
            }
            if (frame && frame.classList.contains('maximized')) {
                frame.style.top = '';
                frame.style.left = '';
                frame.style.width = '';
                frame.style.height = '';
                frame.style.right = '';
                frame.style.bottom = '';
            }
        }

        function zoomImageTo(newScale) {
            // Mantener compatibilidad: zoom hacia el centro del contenedor actual.
            if (!activePreviewImg) return;
            const wrapEl = activePreviewImg.parentElement;
            if (!wrapEl || typeof wrapEl.getBoundingClientRect !== 'function') {
                const clampedScale = Math.max(IMAGE_ZOOM_MIN, Math.min(newScale, IMAGE_ZOOM_MAX));
                scale = clampedScale;
                if (scale === IMAGE_ZOOM_MIN) {
                    isImageZoomed = false;
                    translateX = 0;
                    translateY = 0;
                } else {
                    isImageZoomed = scale > IMAGE_ZOOM_MIN;
                }
                activePreviewImg.classList.toggle('zoomed', scale > IMAGE_ZOOM_MIN);
                updateImageTransform();
                return;
            }
            const r = wrapEl.getBoundingClientRect();
            zoomImageToAt(newScale, r.left + r.width / 2, r.top + r.height / 2, wrapEl);
        }

        function zoomImageToAt(newScale, clientX, clientY, wrapEl) {
            if (!activePreviewImg) return;
            const clampedScale = Math.max(IMAGE_ZOOM_MIN, Math.min(newScale, IMAGE_ZOOM_MAX));
            const oldScale = scale || 1;
            const nextScale = clampedScale;

            if (nextScale === IMAGE_ZOOM_MIN) {
                scale = nextScale;
                isImageZoomed = false;
                translateX = 0;
                translateY = 0;
                activePreviewImg.classList.toggle('zoomed', false);
                updateImageTransform();
                return;
            }

            const rect = wrapEl && typeof wrapEl.getBoundingClientRect === 'function'
                ? wrapEl.getBoundingClientRect()
                : activePreviewImg.getBoundingClientRect();

            // La imagen está centrada en el contenedor con translate(-50%,-50%) + translateX/Y.
            // Anclamos el zoom al punto (clientX, clientY) para evitar saltos/descentrado.
            const cx = rect.left + rect.width / 2;
            const cy = rect.top + rect.height / 2;
            const px = (clientX - cx - translateX) / oldScale;
            const py = (clientY - cy - translateY) / oldScale;

            scale = nextScale;
            translateX = (clientX - cx) - px * scale;
            translateY = (clientY - cy) - py * scale;
            isImageZoomed = scale > IMAGE_ZOOM_MIN;
            activePreviewImg.classList.toggle('zoomed', true);
            updateImageTransform();
        }

        /**
         * Rueda vertical: zoom in/out hacia el cursor. Desplazamiento horizontal dominante (trackpad) con zoom: pan.
         */
        function handleImageViewerWheel(e, wrapEl) {
            const dy = typeof e.deltaY === 'number' ? e.deltaY : 0;
            const dx = typeof e.deltaX === 'number' ? e.deltaX : 0;
            if (!dx && !dy) return;
            const verticalDominant = Math.abs(dy) >= Math.abs(dx);
            if (verticalDominant) {
                e.preventDefault();
                const intensity = 0.0015;
                const factor = Math.exp(-dy * intensity);
                zoomImageToAt(scale * factor, e.clientX, e.clientY, wrapEl);
                return;
            }
            if (!isImageZoomed) return;
            e.preventDefault();
            translateX -= dx;
            translateY -= dy;
            updateImageTransform();
        }
        
        function zoomImageIn() {
            if (previewZoomMode === 'pdf') {
                previewPdfZoom = Math.min(previewPdfZoom + 0.1, 2.5);
                applyPreviewPdfZoom();
                return;
            }
            zoomImageTo(scale + IMAGE_ZOOM_STEP);
        }
        
        function zoomImageOut() {
            if (previewZoomMode === 'pdf') {
                previewPdfZoom = Math.max(previewPdfZoom - 0.1, 0.5);
                applyPreviewPdfZoom();
                return;
            }
            zoomImageTo(scale - IMAGE_ZOOM_STEP);
        }
        
        function resetImageZoom() {
            if (previewZoomMode === 'pdf') {
                previewPdfZoom = 1;
                applyPreviewPdfZoom();
                return;
            }
            zoomImageTo(IMAGE_ZOOM_MIN);
            initPreviewViewportWidthSnapshot();
        }

        /**
         * Pellizco en visor de imagen (misma idea que PDF): usa `scale` global y `zoomImageTo`.
         */
        function setupImagePinchZoom(wrapEl, imgEl) {
            if (!wrapEl || !imgEl) return;
            const clampZ = (z) => Math.max(IMAGE_ZOOM_MIN, Math.min(IMAGE_ZOOM_MAX, z));
            const PINCH_EXP = 0.9; // <1 = menos sensibilidad (más natural)
            const RATIO_DEADZONE = 0.006; // ignorar micro-variaciones (ruido del gesto)
            const applyPinch = (next, anchor) => {
                if (activePreviewImg !== imgEl) return;
                if (anchor && typeof anchor.x === 'number' && typeof anchor.y === 'number') {
                    zoomImageToAt(clampZ(next), anchor.x, anchor.y, wrapEl);
                } else {
                    zoomImageTo(clampZ(next));
                }
            };
            let pinchRaf = null;
            let pendingAnchor = null;
            function schedulePinch(next, anchor) {
                if (pinchRaf !== null) return;
                pendingAnchor = anchor || pendingAnchor;
                pinchRaf = requestAnimationFrame(() => {
                    pinchRaf = null;
                    const a = pendingAnchor;
                    pendingAnchor = null;
                    applyPinch(next, a);
                });
            }

            const ua = navigator.userAgent || '';
            const isIOS = /iPad|iPhone|iPod/i.test(ua) || (navigator.platform === 'MacIntel' && (navigator.maxTouchPoints || 0) > 1);

            if (isIOS) {
                let gestureBase = 1;
                wrapEl.addEventListener('gesturestart', (e) => {
                    e.preventDefault();
                    gestureBase = scale;
                    markImageInteractionActive(650);
                    markFtpImageInteractionActive(650);
                }, { passive: false });
                wrapEl.addEventListener('gesturechange', (e) => {
                    e.preventDefault();
                    const raw = (e.scale || 1);
                    const ratio = Math.pow(raw, PINCH_EXP);
                    if (Math.abs(ratio - 1) < RATIO_DEADZONE) return;
                    const next = gestureBase * ratio;
                    // iOS no siempre provee el centro del gesto; usamos el centro del contenedor.
                    const r = wrapEl.getBoundingClientRect();
                    schedulePinch(next, { x: r.left + r.width / 2, y: r.top + r.height / 2 });
                    markImageInteractionActive(650);
                    markFtpImageInteractionActive(650);
                }, { passive: false });
                wrapEl.addEventListener('gestureend', (e) => {
                    e.preventDefault();
                    markImageInteractionActive(250);
                    markFtpImageInteractionActive(250);
                }, { passive: false });
            }

            const pointers = new Map();
            let pinchStartDist = 0;
            let pinchStartZoom = 1;
            const imgTouchAction = 'pan-x pan-y';
            let pinchAnchor = null;

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

            function pointerDown(e) {
                if (e.pointerType !== 'touch') return;
                updatePointer(e);
                if (pointers.size === 2) {
                    wrapEl.style.touchAction = 'none';
                    const pts = [...pointers.values()];
                    const dx = pts[0].x - pts[1].x;
                    const dy = pts[0].y - pts[1].y;
                    pinchStartDist = Math.max(Math.hypot(dx, dy), 8);
                    pinchStartZoom = scale;
                    pinchAnchor = { x: (pts[0].x + pts[1].x) / 2, y: (pts[0].y + pts[1].y) / 2 };
                    try { e.preventDefault(); } catch (_) {}
                    markImageInteractionActive(650);
                    markFtpImageInteractionActive(650);
                }
            }

            function pointerMove(e) {
                if (e.pointerType !== 'touch') return;
                if (!pointers.has(e.pointerId)) return;
                updatePointer(e);
                if (pointers.size === 2 && pinchStartDist > 0) {
                    const pts = [...pointers.values()];
                    const dx = pts[0].x - pts[1].x;
                    const dy = pts[0].y - pts[1].y;
                    const dist = Math.hypot(dx, dy);
                    const rawRatio = dist / pinchStartDist;
                    const ratio = Math.pow(rawRatio, PINCH_EXP);
                    if (Math.abs(ratio - 1) < RATIO_DEADZONE) return;
                    pinchAnchor = { x: (pts[0].x + pts[1].x) / 2, y: (pts[0].y + pts[1].y) / 2 };
                    schedulePinch(pinchStartZoom * ratio, pinchAnchor);
                    try { e.preventDefault(); } catch (_) {}
                    markImageInteractionActive(650);
                    markFtpImageInteractionActive(650);
                }
            }

            function pointerUp(e) {
                pointers.delete(e.pointerId);
                if (pointers.size < 2) {
                    pinchStartDist = 0;
                    pinchAnchor = null;
                    wrapEl.style.touchAction = '';
                    markImageInteractionActive(250);
                    markFtpImageInteractionActive(250);
                }
            }

            if (typeof window.PointerEvent !== 'undefined') {
                wrapEl.addEventListener('pointerdown', pointerDown, { passive: false });
                wrapEl.addEventListener('pointermove', pointerMove, { passive: false });
                wrapEl.addEventListener('pointerup', pointerUp);
                wrapEl.addEventListener('pointercancel', pointerUp);
            } else {
                let tPinchDist = 0;
                let tPinchZoom = 1;
                let tAnchor = null;
                wrapEl.addEventListener('touchstart', (e) => {
                    if (e.touches.length === 2) {
                        e.preventDefault();
                        wrapEl.style.touchAction = 'none';
                        const dx = e.touches[0].clientX - e.touches[1].clientX;
                        const dy = e.touches[0].clientY - e.touches[1].clientY;
                        tPinchDist = Math.max(Math.hypot(dx, dy), 8);
                        tPinchZoom = scale;
                        tAnchor = { x: (e.touches[0].clientX + e.touches[1].clientX) / 2, y: (e.touches[0].clientY + e.touches[1].clientY) / 2 };
                        markImageInteractionActive(650);
                        markFtpImageInteractionActive(650);
                    }
                }, { passive: false });
                wrapEl.addEventListener('touchmove', (e) => {
                    if (e.touches.length === 2 && tPinchDist > 0) {
                        e.preventDefault();
                        const dx = e.touches[0].clientX - e.touches[1].clientX;
                        const dy = e.touches[0].clientY - e.touches[1].clientY;
                        const dist = Math.hypot(dx, dy);
                        tAnchor = { x: (e.touches[0].clientX + e.touches[1].clientX) / 2, y: (e.touches[0].clientY + e.touches[1].clientY) / 2 };
                        const rawRatio = dist / tPinchDist;
                        const ratio = Math.pow(rawRatio, PINCH_EXP);
                        if (Math.abs(ratio - 1) < RATIO_DEADZONE) return;
                        schedulePinch(tPinchZoom * ratio, tAnchor);
                        markImageInteractionActive(650);
                        markFtpImageInteractionActive(650);
                    }
                }, { passive: false });
                wrapEl.addEventListener('touchend', (e) => {
                    if (e.touches.length < 2) {
                        tPinchDist = 0;
                        tAnchor = null;
                        wrapEl.style.touchAction = '';
                        markImageInteractionActive(250);
                        markFtpImageInteractionActive(250);
                    }
                });
                wrapEl.addEventListener('touchcancel', () => {
                    tPinchDist = 0;
                    tAnchor = null;
                    wrapEl.style.touchAction = '';
                    markImageInteractionActive(250);
                    markFtpImageInteractionActive(250);
                });
            }
        }

        function setupGallerySwipeNav(wrapEl, getCurrentIndex, navigateToIndex, len) {
            if (!wrapEl || typeof navigateToIndex !== 'function') return;
            let sx = 0;
            let sy = 0;
            let swipePtrId = null;
            wrapEl.addEventListener('pointerdown', (e) => {
                if (e.pointerType === 'mouse' && e.button !== 0) return;
                if (e.pointerType !== 'mouse' && e.pointerType !== 'pen' && e.pointerType !== 'touch') return;
                if (scale > IMAGE_ZOOM_MIN + 0.02) return;
                swipePtrId = e.pointerId;
                sx = e.clientX;
                sy = e.clientY;
            });
            wrapEl.addEventListener('pointerup', (e) => {
                if (swipePtrId === null || e.pointerId !== swipePtrId) return;
                swipePtrId = null;
                if (scale > IMAGE_ZOOM_MIN + 0.02) return;
                const dx = e.clientX - sx;
                const dy = e.clientY - sy;
                if (len <= 1) return;
                if (Math.abs(dx) < 36 || Math.abs(dy) > 100) return;
                const idx = getCurrentIndex();
                if (dx < 0) navigateToIndex((idx + 1) % len);
                else navigateToIndex((idx - 1 + len) % len);
            });
            wrapEl.addEventListener('pointercancel', (e) => {
                if (e.pointerId === swipePtrId) swipePtrId = null;
            });
        }

        /** Pan con zoom en galería: el área útil es el wrap (no solo el img). */
        function setupGalleryImagePanZoom(wrapEl, imgEl) {
            if (!wrapEl || !imgEl) return;
            wrapEl.addEventListener('pointerdown', (e) => {
                if (!isImageZoomed) return;
                if (e.pointerType === 'mouse' && e.button !== 0) return;
                e.preventDefault();
                const pid = e.pointerId;
                try { wrapEl.setPointerCapture(pid); } catch (_) {}
                const ox = e.clientX - translateX;
                const oy = e.clientY - translateY;
                const move = (ev) => {
                    if (ev.pointerId !== pid) return;
                    ev.preventDefault();
                    translateX = ev.clientX - ox;
                    translateY = ev.clientY - oy;
                    updateImageTransform();
                };
                const up = (ev) => {
                    if (ev.pointerId !== pid) return;
                    wrapEl.removeEventListener('pointermove', move);
                    wrapEl.removeEventListener('pointerup', up);
                    wrapEl.removeEventListener('pointercancel', up);
                    try { wrapEl.releasePointerCapture(pid); } catch (_) {}
                    if (imgEl) imgEl.style.cursor = isImageZoomed ? 'grab' : 'grab';
                };
                wrapEl.addEventListener('pointermove', move, { passive: false });
                wrapEl.addEventListener('pointerup', up);
                wrapEl.addEventListener('pointercancel', up);
                imgEl.style.cursor = 'grabbing';
            });
            wrapEl.addEventListener('wheel', (e) => {
                handleImageViewerWheel(e, wrapEl);
            }, { passive: false });
        }
        
        if (zoomInBtn) zoomInBtn.addEventListener('click', zoomPageIn);
        if (zoomOutBtn) zoomOutBtn.addEventListener('click', zoomPageOut);
        if (fullscreenBtn) fullscreenBtn.addEventListener('click', toggleFullscreen);

        // ========== Controles de previsualización (ventana tipo iframe) ==========
        let previewIsMaximized = false;
        let previewSavedRect = null;

        function applyPreviewWindowMaximized() {
            if (!previewWindowFrame) return;
            if (!previewIsMaximized) {
                previewSavedRect = {
                    top: previewWindowFrame.style.top,
                    left: previewWindowFrame.style.left,
                    width: previewWindowFrame.style.width,
                    height: previewWindowFrame.style.height
                };
            }
            previewWindowFrame.classList.add('maximized');
            const rh = document.getElementById('previewResizeHandle');
            if (rh) rh.style.display = 'none';
            previewIsMaximized = true;
        }

        if (previewZoomInBtn) previewZoomInBtn.addEventListener('click', zoomImageIn);
        if (previewZoomOutBtn) previewZoomOutBtn.addEventListener('click', zoomImageOut);
        if (previewZoomResetBtn) previewZoomResetBtn.addEventListener('click', resetImageZoom);
        if (previewExternalBtn) {
            previewExternalBtn.addEventListener('click', () => {
                if (previewExternalBtn.dataset.externalUrl) {
                    window.open(previewExternalBtn.dataset.externalUrl, '_blank');
                }
            });
        }
        if (previewCloseBtn) previewCloseBtn.addEventListener('click', closePreviewModal);
        if (previewMaximizeBtn && previewWindowFrame) {
            previewMaximizeBtn.addEventListener('click', () => {
                if (!previewIsMaximized) {
                    applyPreviewWindowMaximized();
                } else {
                    previewWindowFrame.classList.remove('maximized');
                    if (previewSavedRect) {
                        previewWindowFrame.style.top = previewSavedRect.top || '';
                        previewWindowFrame.style.left = previewSavedRect.left || '';
                        previewWindowFrame.style.width = previewSavedRect.width || '';
                        previewWindowFrame.style.height = previewSavedRect.height || '';
                    }
                    const rh = document.getElementById('previewResizeHandle');
                    if (rh) rh.style.display = 'block';
                    previewIsMaximized = false;
                }
                schedulePreviewVisualViewportLayout();
            });
        }

        // ========== Drag de la ventana de previsualización ==========
        let previewIsDragging = false;
        let previewDragStartX = 0, previewDragStartY = 0, previewStartLeft = 0, previewStartTop = 0;
        if (previewWindowTitlebar && previewWindowFrame) {
            previewWindowTitlebar.addEventListener('mousedown', (e) => {
                if (previewIsMaximized) return;
                previewIsDragging = true;
                previewDragStartX = e.clientX;
                previewDragStartY = e.clientY;
                previewStartLeft = parseInt(previewWindowFrame.style.left, 10) || 0;
                previewStartTop = parseInt(previewWindowFrame.style.top, 10) || 0;
                previewWindowFrame.style.cursor = 'grabbing';
                e.preventDefault();
            });
            document.addEventListener('mousemove', (e) => {
                if (!previewIsDragging) return;
                const dx = e.clientX - previewDragStartX;
                const dy = e.clientY - previewDragStartY;
                previewWindowFrame.style.left = (previewStartLeft + dx) + 'px';
                previewWindowFrame.style.top = (previewStartTop + dy) + 'px';
            });
            document.addEventListener('mouseup', (e) => {
                if (!previewIsDragging) return;
                previewIsDragging = false;
                if (previewWindowFrame) previewWindowFrame.style.cursor = '';
                const snap = applyWindows11Snap(previewWindowFrame, e.clientX, e.clientY, { resizeHandle: previewResizeHandle, maximizeBtn: previewMaximizeBtn });
                if (snap) previewIsMaximized = !!snap.maximized;
            });
        }

        // ========== Redimensionar ventana de previsualización ==========
        let previewIsResizing = false;
        let previewResizeStartX = 0, previewResizeStartY = 0, previewStartWidth = 0, previewStartHeight = 0;
        const previewResizeHandle = document.getElementById('previewResizeHandle');
        if (previewResizeHandle && previewWindowFrame) {
            previewResizeHandle.addEventListener('mousedown', (e) => {
                if (previewIsMaximized) return;
                previewIsResizing = true;
                previewIsDragging = false;
                previewResizeStartX = e.clientX;
                previewResizeStartY = e.clientY;
                previewStartWidth = parseInt(previewWindowFrame.style.width, 10) || 800;
                previewStartHeight = parseInt(previewWindowFrame.style.height, 10) || 600;
                previewWindowFrame.style.cursor = 'se-resize';
                e.preventDefault();
                e.stopPropagation();
            });
            document.addEventListener('mousemove', (e) => {
                if (!previewIsResizing) return;
                const dx = e.clientX - previewResizeStartX;
                const dy = e.clientY - previewResizeStartY;
                previewWindowFrame.style.width = Math.max(320, previewStartWidth + dx) + 'px';
                previewWindowFrame.style.height = Math.max(240, previewStartHeight + dy) + 'px';
            });
            document.addEventListener('mouseup', () => {
                previewIsResizing = false;
                if (previewWindowFrame) previewWindowFrame.style.cursor = '';
            });
        }

        (function setupPreviewVisualViewportListeners() {
            const onScroll = () => {
                if (!previewActive) return;
                const modal = document.getElementById('previewModal');
                if (!modal || modal.style.display !== 'block') return;
                schedulePreviewVisualViewportLayout();
            };
            const onResize = () => {
                if (previewActive) {
                    const modal = document.getElementById('previewModal');
                    if (modal && modal.style.display === 'block') {
                        schedulePreviewVisualViewportLayout();
                    }
                }
                maybeResetImageZoomOnViewportResize();
            };
            if (window.visualViewport) {
                window.visualViewport.addEventListener('resize', onResize);
                window.visualViewport.addEventListener('scroll', onScroll);
            }
            window.addEventListener('resize', onResize);
            window.addEventListener('orientationchange', onResize);
        })();

        // ========== FUNCIONES DE MODO MÓVIL ==========
        function updateMobileMode(enable) {
            const isMobileDevice = IS_MOBILE === true || /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent);
            if (isMobileDevice) {
                if (enable) {
                    document.body.classList.remove('mobile-mode');
                    document.body.classList.add('mobile-pc-mode');
                    document.body.classList.remove('taskbar-top', 'taskbar-left', 'taskbar-right');
                    document.body.classList.add('taskbar-bottom');
                } else {
                    document.body.classList.remove('mobile-pc-mode');
                    document.body.classList.add('mobile-mode');
                }
            }
            applyThemeToTaskbar();
            try {
                if (window.OsShell && typeof window.OsShell.updateStartMenuFoldWideClass === 'function') {
                    window.OsShell.updateStartMenuFoldWideClass();
                }
                if (window.OsShell && typeof window.OsShell.updateSpotifyQuickPlayer === 'function') {
                    window.OsShell.updateSpotifyQuickPlayer();
                }
            } catch (_) {}
        }
        
        function isMobile() {
            return /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent);
        }
        
        function isMobilePC() {
            return isMobile() && getSetting('mobileDesktopMode', false);
        }
        
        // ========== FUNCIONES DE TEMA ==========
        function applyThemeToTaskbar() {
            // No dependemos del "primer" className para no romper el layout (mobile/taskbar).
            const themeClasses = ['neumorphism', 'glassmorphism', 'skeuomorphism', 'flat', 'material', 'brutalism', 'retro', 'dark', 'minimalism', 'morphing', 'isometric', 'cyberpunk', 'gradient', 'glassgradient', 'auroracrystal3d', 'card', 'typography'];
            const targetTheme = currentTheme || getSetting('selectedTheme', 'neumorphism');
            document.body.classList.remove(...themeClasses);
            document.body.classList.add(targetTheme);
            applyThemePaletteToDocument(targetTheme);
            updateAuroraCrystal3dLayer();
            applyExplorerUiOverridesFromSettings();
            refreshThemeContrast();
        }
        
        // ========== FUNCIONES DE ACCESOS DIRECTOS EN ESCRITORIO ==========
        function getWindowPreviewThumb(win, appGroup) {
            // Cache calculada al minimizar (ideal para ventanas con iframe).
            if (win && win.previewCache && typeof win.previewCache === 'object') return win.previewCache;

            const fallback = appGroup && appGroup.icon
                ? { type: 'img', src: appGroup.icon }
                : { type: 'img', src: "data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 64 40'%3E%3Crect width='64' height='40' fill='%23cbd5e1'/%3E%3Cpath d='M8 28h48v4H8z' fill='%2394a3b8'/%3E%3C/svg%3E" };

            if (win && win.frame) {
                const iframe = win.frame.querySelector('.window-content iframe, iframe');
                if (iframe && iframe.src) {
                    return { type: 'iframe', src: iframe.src };
                }
                const img = win.frame.querySelector('img#previewImage, img[id^="fwimg_"], .window-content img');
                if (img && img.src) return { type: 'img', src: img.src };
                const video = win.frame.querySelector('.window-content video');
                if (video && video.src) {
                    return {
                        type: 'video',
                        src: video.src,
                        poster: video.poster || ''
                    };
                }
                if (video && video.poster) return { type: 'img', src: video.poster };
            }

            return fallback;
        }

        function appIsVisibleOnWindowsLauncherGrid(app) {
            if (!app) return false;
            if (app.isSystemApp) return app.showInGrid === true;
            return app.showInGrid !== false;
        }

        function addToDesktop(app) {
            if (!app || app.isSystemApp) return false;
            if (appIsVisibleOnWindowsLauncherGrid(app)) {
                showToast(tr('desktopAppAlreadyThere'));
                return false;
            }
            app.showInGrid = true;
            if (window.GridLauncher && window.GridLauncher.layoutModel && launcherLayout) {
                launcherLayout = GridLauncher.layoutModel.appendAppToOrder(launcherLayout, getAppStableId(app), getAppStableId);
            }
            saveSettings({ apps: apps, launcherLayout: launcherLayout }).then(() => {
                renderApps();
                if (windowsMenuVisible) renderWindowsMenu(apps);
                showToast(tr('desktopToastAdded'));
            });
            return true;
        }

        function renderDesktopShortcuts() {
            if (!desktopShortcutsContainer) return;
            desktopShortcutsContainer.innerHTML = '';
            const savedGridTextColor = getSetting('gridTextColor', '#000000');
            desktopShortcuts.forEach(shortcut => {
                const app = apps.find(a => a.title === shortcut.title && getAppLinkKey(a) === (shortcut.link || ''));
                if (!app) return;
                const shortcutDiv = document.createElement('div');
                shortcutDiv.className = 'app-item';
                shortcutDiv.setAttribute('data-appid', shortcut.appId);
                shortcutDiv.innerHTML = `
                    <img src="${app.icon}" alt="${app.title}" class="app-icon">
                    <p style="color: ${savedGridTextColor};">${app.title}</p>
                `;
                shortcutDiv.addEventListener('dblclick', (e) => {
                    e.stopPropagation();
                    launchDesktopApp(app);
                });
                shortcutDiv.addEventListener('contextmenu', (e) => {
                    e.preventDefault();
                    e.stopPropagation();
                    const contextMenu = document.createElement('div');
                    contextMenu.className = 'context-menu';
                    contextMenu.innerHTML = `
                        <button type="button" class="strip-menu-link remove-shortcut"><i class="fas fa-trash"></i> ${tr('removeShortcut')}</button>
                        <button type="button" class="strip-menu-link open-app"><i class="fas fa-external-link-alt"></i> ${tr('open')}</button>
                    `;
                    contextMenu.style.left = e.clientX + 'px';
                    contextMenu.style.top = (e.clientY - 80) + 'px';
                    document.body.appendChild(contextMenu);
                    
                    contextMenu.querySelector('.remove-shortcut').addEventListener('click', () => {
                        desktopShortcuts = desktopShortcuts.filter(s => !(s.title === shortcut.title && (s.link || '') === (shortcut.link || '')));
                        saveSettings({ desktopShortcuts: desktopShortcuts });
                        contextMenu.remove();
                        if (currentContextMenu === contextMenu) currentContextMenu = null;
                    });
                    contextMenu.querySelector('.open-app').addEventListener('click', () => {
                        launchDesktopApp(app);
                        contextMenu.remove();
                        if (currentContextMenu === contextMenu) currentContextMenu = null;
                    });
                    currentContextMenu = contextMenu;
                    setTimeout(() => {
                        document.addEventListener('click', function removeMenu(e) {
                            if (contextMenu && !contextMenu.contains(e.target)) {
                                contextMenu.remove();
                                if (currentContextMenu === contextMenu) currentContextMenu = null;
                                document.removeEventListener('click', removeMenu);
                            }
                        });
                    }, 100);
                });
                desktopShortcutsContainer.appendChild(shortcutDiv);
            });
        }
        
        function addDesktopShortcut(app) { return addToDesktop(app); }
        
        function removeDesktopShortcut(app) {
            const lk = getAppLinkKey(app);
            desktopShortcuts = desktopShortcuts.filter(s => !(s.title === app.title && (s.link || '') === lk));
            saveSettings({ desktopShortcuts: desktopShortcuts });
            renderDesktopShortcuts();
        }

        // ========== FUNCIONES DE BLOQUEO DE INSPECCIÓN ==========
        function updateInspectBlock(enable) {
            if (enable) {
                document.addEventListener('keydown', keyBlockHandler);
            } else {
                document.removeEventListener('keydown', keyBlockHandler);
            }
        }

        function contextMenuHandler(e) {
            if (blockIfExplorerFocusLock(e)) return;
            // El explorador de archivos maneja su propio menú contextual
            // (items, árbol y área vacía). Evitar que el menú global lo pise.
            if (e.target && e.target.closest && e.target.closest('.file-explorer')) {
                return;
            }
            if (e.target.closest('.window-frame') || e.target.closest('#previewModal') || e.target.closest('#desktopShortcuts .app-item') || e.target.closest('#appGrid .app-item')) {
                e.preventDefault();
                return;
            }
            // Si el clic derecho es dentro del menú Windows, dejamos que su manejador propio renderice
            // su `context-menu` (para evitar doble menú: stripMenu + context-menu).
            if (e.target.closest('#windowsMenu')) {
                return;
            }
            // Si el manejador del propio elemento ya hizo preventDefault, no duplicamos menú.
            if (e.defaultPrevented) {
                return;
            }
            if (e.target.closest('.app-item')) {
                e.preventDefault();
                const appItem = e.target.closest('.app-item');
                const index = parseInt(appItem.dataset.index);
                if (!isNaN(index) && index >= 0 && index < apps.length) {
                    editingIndex = index;
                    selectedAppIndex = index;
                    const visibleItems = Array.from(document.querySelectorAll('#appGrid .app-item'));
                    selectedGridVisibleIndex = visibleItems.findIndex(item => parseInt(item.dataset.index) === index);
                    showStripMenu(e, document.getElementById('stripMenu'));
                }
                return;
            }
            if (!e.target.closest('.topbar') && !e.target.closest('button') && !e.target.closest('.config-btn') && !e.target.closest('.add-app-btn') && !e.target.closest('.taskbar')) {
                e.preventDefault();
                showStripMenu(e, document.getElementById('desktopStripMenu'));
            } else {
                e.preventDefault();
            }
        }

        async function keyBlockHandler(e) {
            if (e.key === 'F12' || 
                (e.ctrlKey && e.shiftKey && e.key === 'I') || 
                (e.ctrlKey && e.shiftKey && e.key === 'J') ||
                (e.ctrlKey && e.key === 'U')) {
                e.preventDefault();
                await appDialog.alert('Acción no permitida.');
            }
        }

        document.addEventListener('contextmenu', contextMenuHandler);
        document.addEventListener('mousedown', (e) => { blockIfExplorerFocusLock(e); }, true);
        document.addEventListener('touchstart', (e) => { blockIfExplorerFocusLock(e); }, true);
        document.addEventListener('focusin', (e) => { blockIfExplorerFocusLock(e); }, true);
        let inspectBlockEnabled = getSetting('inspectBlock', true);
        updateInspectBlock(inspectBlockEnabled);

        // ========== FUNCIONES DE TASKBAR PARA VENTANAS ABIERTAS ==========
        function taskbarStripIsVertical() {
            return document.body.classList.contains('taskbar-left') || document.body.classList.contains('taskbar-right');
        }

        function mergeTaskbarOpenOrder(visibleIds) {
            const seen = new Set();
            const next = [];
            for (const id of taskbarOpenOrder) {
                if (visibleIds.includes(id) && !seen.has(id)) {
                    next.push(id);
                    seen.add(id);
                }
            }
            for (const id of visibleIds) {
                if (!seen.has(id)) {
                    next.push(id);
                    seen.add(id);
                }
            }
            taskbarOpenOrder = next;
        }

        function sortTaskbarGroupsByOpenOrder(groups) {
            const order = taskbarOpenOrder;
            const rank = (id) => {
                const i = order.indexOf(id);
                return i === -1 ? 999999 : i;
            };
            return groups.slice().sort((a, b) => rank(a.appId) - rank(b.appId));
        }

        function getTaskbarDragAfterElement(container, clientX, clientY) {
            const vertical = taskbarStripIsVertical();
            const selector = container.id === 'pinnedAppsContainer' ? '.pinned-app' : '.taskbar-app';
            const els = [...container.querySelectorAll(selector)].filter((el) => !el.classList.contains('taskbar-drag-deny') && !el.classList.contains('taskbar-dragging'));
            return els.reduce((closest, child) => {
                const box = child.getBoundingClientRect();
                const offset = vertical
                    ? clientY - box.top - box.height / 2
                    : clientX - box.left - box.width / 2;
                if (offset < 0 && offset > closest.offset) {
                    return { offset, element: child };
                }
                return closest;
            }, { offset: Number.NEGATIVE_INFINITY, element: null }).element;
        }

        function bindTaskbarIconReorder() {
            if (taskbarIconReorderBound || !pinnedAppsContainer || !taskbarAppsContainer) return;
            taskbarIconReorderBound = true;

            function onPinnedDragOver(e) {
                e.preventDefault();
                e.dataTransfer.dropEffect = 'move';
                const dragging = document.querySelector('.taskbar-dragging');
                if (!dragging || !dragging.classList.contains('pinned-app')) return;
                const after = getTaskbarDragAfterElement(pinnedAppsContainer, e.clientX, e.clientY);
                if (after == null) pinnedAppsContainer.appendChild(dragging);
                else pinnedAppsContainer.insertBefore(dragging, after);
            }

            function onTaskbarDragOver(e) {
                e.preventDefault();
                e.dataTransfer.dropEffect = 'move';
                const dragging = document.querySelector('.taskbar-dragging');
                if (!dragging || !dragging.classList.contains('taskbar-app')) return;
                const after = getTaskbarDragAfterElement(taskbarAppsContainer, e.clientX, e.clientY);
                if (after == null) taskbarAppsContainer.appendChild(dragging);
                else taskbarAppsContainer.insertBefore(dragging, after);
            }

            function applyPinnedOrderFromDom() {
                const nodes = [...pinnedAppsContainer.querySelectorAll('.pinned-app')];
                const newOrder = [];
                const seenKeys = new Set();
                nodes.forEach((node) => {
                    const t = node.dataset.pinTitle;
                    const l = node.dataset.pinLink || '';
                    const key = `${t}::${l}`;
                    if (seenKeys.has(key)) return;
                    const p = pinnedApps.find((x) => x && x.title === t && (x.link || '') === l);
                    if (p) {
                        newOrder.push(p);
                        seenKeys.add(key);
                    }
                });
                // Si hay pins persistidos que no están en el DOM (p. ej. app eliminada o filtrada),
                // no bloqueamos el guardado: los conservamos al final en su orden previo.
                const remainder = pinnedApps.filter((p) => p && !seenKeys.has(`${p.title}::${p.link || ''}`));
                pinnedApps = newOrder.concat(remainder);
                try { gridWriteJsonLs(GRID_TASKBAR_PINNED_ORDER_KEY, pinnedApps); } catch (_) {}
                saveSettings({ pinnedApps });
            }

            function applyTaskbarOpenOrderFromDom() {
                const nodes = [...taskbarAppsContainer.querySelectorAll('.taskbar-app')];
                const ids = nodes.map((n) => n.dataset.appid).filter(Boolean);
                taskbarOpenOrder = ids;
                try { gridWriteJsonLs(GRID_TASKBAR_OPEN_ORDER_KEY, taskbarOpenOrder); } catch (_) {}
                saveSettings({ taskbarOpenOrder });
            }

            pinnedAppsContainer.addEventListener('dragover', onPinnedDragOver);
            pinnedAppsContainer.addEventListener('drop', (e) => {
                e.preventDefault();
                const dragging = document.querySelector('.taskbar-dragging');
                if (dragging) dragging.classList.remove('taskbar-dragging');
                if (dragging && dragging.classList.contains('pinned-app')) {
                    taskbarDragSuppressClickUntil = Date.now() + 400;
                    applyPinnedOrderFromDom();
                }
            });

            taskbarAppsContainer.addEventListener('dragover', onTaskbarDragOver);
            taskbarAppsContainer.addEventListener('drop', (e) => {
                e.preventDefault();
                const dragging = document.querySelector('.taskbar-dragging');
                if (dragging) dragging.classList.remove('taskbar-dragging');
                if (dragging && dragging.classList.contains('taskbar-app')) {
                    taskbarDragSuppressClickUntil = Date.now() + 400;
                    applyTaskbarOpenOrderFromDom();
                }
            });
        }

        function bindMobileHubPinnedReorder() {
            if (mobileHubPinnedReorderBound || !mobileAppHubPinned) return;
            mobileHubPinnedReorderBound = true;

            let dragEl = null;
            let dragging = false;
            let startX = 0, startY = 0;
            let pointerId = null;
            let longPressTimer = null;
            let longPressFired = false;

            const clearLongPress = () => {
                if (longPressTimer) clearTimeout(longPressTimer);
                longPressTimer = null;
                longPressFired = false;
            };

            function applyPinnedOrderFromHubDom() {
                const nodes = [...mobileAppHubPinned.querySelectorAll('.mobile-app-hub-item[data-pin-title]')];
                const next = [];
                const seen = new Set();
                for (const n of nodes) {
                    const t = n.dataset.pinTitle || '';
                    const l = n.dataset.pinLink || '';
                    const key = `${t}::${l}`;
                    if (seen.has(key)) continue;
                    const p = pinnedApps.find((x) => x && x.title === t && (x.link || '') === l);
                    if (p) {
                        next.push(p);
                        seen.add(key);
                    }
                }
                const remainder = pinnedApps.filter((p) => p && !seen.has(`${p.title}::${p.link || ''}`));
                pinnedApps = next.concat(remainder);
                try { gridWriteJsonLs(GRID_TASKBAR_PINNED_ORDER_KEY, pinnedApps); } catch (_) {}
                saveSettings({ pinnedApps });
            }

            function hubItemFromPoint(x, y) {
                const el = document.elementFromPoint(x, y);
                if (!el) return null;
                const item = el.closest && el.closest('.mobile-app-hub-item[data-pin-title]');
                if (!item) return null;
                if (!mobileAppHubPinned.contains(item)) return null;
                return item;
            }

            function onPointerDown(e) {
                const item = e.target && e.target.closest && e.target.closest('.mobile-app-hub-item[data-pin-title]');
                if (!item || !mobileAppHubPinned.contains(item)) return;
                if (Date.now() < mobileHubSuppressClickUntil) return;

                dragEl = item;
                dragging = false;
                startX = e.clientX;
                startY = e.clientY;
                pointerId = e.pointerId;
                longPressFired = false;

                clearLongPress();
                longPressTimer = setTimeout(() => {
                    longPressFired = true;
                    // Abrir menú contextual (equivalente a strip menu) en móvil.
                    try {
                        const t = dragEl?.dataset?.pinTitle || '';
                        const l = dragEl?.dataset?.pinLink || '';
                        const idx = apps.findIndex(a => a && a.title === t && getAppLinkKey(a) === l);
                        if (idx !== -1) {
                            editingIndex = idx;
                            selectedAppIndex = idx;
                            selectedGridVisibleIndex = -1;
                            const evt = { clientX: startX, clientY: startY, preventDefault() {}, stopPropagation() {} };
                            showStripMenu(evt, document.getElementById('stripMenu'));
                        }
                    } catch (_) {}
                }, 560);

                try { dragEl.setPointerCapture(pointerId); } catch (_) {}
            }

            function onPointerMove(e) {
                if (!dragEl || pointerId === null || e.pointerId !== pointerId) return;
                const dx = e.clientX - startX;
                const dy = e.clientY - startY;
                const dist = Math.hypot(dx, dy);
                if (!dragging) {
                    if (dist < 10) return;
                    if (longPressTimer) clearTimeout(longPressTimer);
                    longPressTimer = null;
                    if (longPressFired) return;
                    dragging = true;
                    dragEl.classList.add('mobile-hub-dragging');
                }
                e.preventDefault();

                const over = hubItemFromPoint(e.clientX, e.clientY);
                if (over && over !== dragEl) {
                    const r = over.getBoundingClientRect();
                    const after = (e.clientY > (r.top + r.height / 2)) || (e.clientX > (r.left + r.width / 2));
                    if (after) {
                        mobileAppHubPinned.insertBefore(dragEl, over.nextSibling);
                    } else {
                        mobileAppHubPinned.insertBefore(dragEl, over);
                    }
                }
            }

            function onPointerUp(e) {
                if (pointerId === null || e.pointerId !== pointerId) return;
                clearLongPress();

                if (dragEl) {
                    try { dragEl.releasePointerCapture(pointerId); } catch (_) {}
                }

                if (dragEl && dragging) {
                    dragEl.classList.remove('mobile-hub-dragging');
                    mobileHubSuppressClickUntil = Date.now() + 500;
                    applyPinnedOrderFromHubDom();
                    // Re-render para reflejar contadores/estados.
                    renderPinnedApps();
                    renderMobileAppHub();
                }

                dragEl = null;
                dragging = false;
                pointerId = null;
            }

            mobileAppHubPinned.addEventListener('pointerdown', onPointerDown, { passive: true });
            mobileAppHubPinned.addEventListener('pointermove', onPointerMove, { passive: false });
            mobileAppHubPinned.addEventListener('pointerup', onPointerUp, { passive: true });
            mobileAppHubPinned.addEventListener('pointercancel', onPointerUp, { passive: true });
        }

        function bindMobileHubOpenSwipeToClose() {
            if (mobileHubOpenSwipeCloseBound || !mobileAppHubOpen) return;
            mobileHubOpenSwipeCloseBound = true;

            let dragTile = null;
            let pointerId = null;
            let startX = 0, startY = 0;
            let lastDx = 0;
            let isSwiping = false;

            const resetTileStyle = (tile) => {
                if (!tile) return;
                tile.classList.remove('mobile-hub-swipe-active', 'mobile-hub-swipe-closing');
                tile.style.transform = '';
                tile.style.opacity = '';
                tile.style.position = '';
                tile.style.zIndex = '';
            };

            const getThreshold = (tile) => {
                try {
                    const w = tile.getBoundingClientRect().width || 0;
                    return Math.max(90, Math.min(160, Math.round(w * 0.35)));
                } catch (_) {
                    return 120;
                }
            };

            function onPointerDown(e) {
                const tile = e.target && e.target.closest && e.target.closest('.windows-mosaic-tile');
                if (!tile || !mobileAppHubOpen.contains(tile)) return;
                if (e.target && e.target.closest && e.target.closest('.windows-mosaic-close')) return;
                if (Date.now() < mobileHubSuppressClickUntil) return;

                dragTile = tile;
                pointerId = e.pointerId;
                startX = e.clientX;
                startY = e.clientY;
                lastDx = 0;
                isSwiping = false;
            }

            function onPointerMove(e) {
                if (!dragTile || pointerId === null || e.pointerId !== pointerId) return;
                const dx = e.clientX - startX;
                const dy = e.clientY - startY;
                lastDx = dx;

                if (!isSwiping) {
                    // Solo iniciar swipe si es principalmente horizontal.
                    if (Math.abs(dx) < 10) return;
                    // Si el usuario se está desplazando verticalmente, dejamos que scrollee.
                    if (Math.abs(dy) > Math.abs(dx) * 0.9) return;
                    isSwiping = true;
                    dragTile.classList.add('mobile-hub-swipe-active');
                    // Subir encima de otros tiles durante el swipe (evita sensación de “choque”).
                    dragTile.style.position = 'relative';
                    dragTile.style.zIndex = '6';
                    // Capturar el puntero SOLO cuando confirmamos swipe horizontal.
                    try { dragTile.setPointerCapture(pointerId); } catch (_) {}
                }

                e.preventDefault();
                const thr = getThreshold(dragTile);
                let maxDx = thr;
                try {
                    const w = dragTile.getBoundingClientRect().width || 0;
                    // Limitar desplazamiento para que el tile no invada otras secciones.
                    maxDx = Math.max(thr, Math.min(220, Math.round(w * 0.65)));
                } catch (_) {}
                const clampedDx = Math.max(-maxDx, Math.min(maxDx, dx));
                const progress = Math.min(1, Math.abs(clampedDx) / thr);
                const opacity = 1 - progress * 0.65;
                dragTile.style.transform = `translateX(${clampedDx}px)`;
                dragTile.style.opacity = String(Math.max(0.18, opacity));
            }

            function onPointerUp(e) {
                if (pointerId === null || e.pointerId !== pointerId) return;
                try { dragTile && dragTile.releasePointerCapture(pointerId); } catch (_) {}

                const tile = dragTile;
                const dx = lastDx;
                const wasSwiping = isSwiping;
                dragTile = null;
                pointerId = null;
                isSwiping = false;

                if (!tile) return;
                if (!wasSwiping) return;

                const thr = getThreshold(tile);
                if (Math.abs(dx) >= thr) {
                    const winId = tile.dataset.windowId;
                    tile.classList.add('mobile-hub-swipe-closing');
                    const sign = dx >= 0 ? 1 : -1;
                    try {
                        const w = tile.getBoundingClientRect().width || 320;
                        // Cierre dentro del contenedor (sin cruzar secciones).
                        tile.style.transform = `translateX(${sign * Math.round(w * 0.7)}px)`;
                        tile.style.opacity = '0';
                    } catch (_) {}
                    mobileHubSuppressClickUntil = Date.now() + 700;
                    setTimeout(() => {
                        if (winId) closeWindowFromMosaic(winId);
                    }, 180);
                } else {
                    // No alcanzó umbral: volver a normal.
                    tile.classList.add('mobile-hub-swipe-closing');
                    tile.style.transform = 'translateX(0px)';
                    tile.style.opacity = '1';
                    setTimeout(() => resetTileStyle(tile), 220);
                }
            }

            mobileAppHubOpen.addEventListener('pointerdown', onPointerDown, { passive: false });
            mobileAppHubOpen.addEventListener('pointermove', onPointerMove, { passive: false });
            mobileAppHubOpen.addEventListener('pointerup', onPointerUp, { passive: true });
            mobileAppHubOpen.addEventListener('pointercancel', onPointerUp, { passive: true });
        }

        function bindWindowsMosaicSwipeToClose() {
            if (windowsMosaicSwipeCloseBound || !windowsMosaicGrid) return;
            windowsMosaicSwipeCloseBound = true;

            let dragTile = null;
            let pointerId = null;
            let startX = 0, startY = 0;
            let lastDx = 0, lastDy = 0;
            let isSwiping = false;
            let axis = 'none'; // 'x' | 'y'

            const resetTile = (tile) => {
                if (!tile) return;
                tile.classList.remove('mobile-hub-swipe-active', 'mobile-hub-swipe-closing');
                tile.style.transform = '';
                tile.style.opacity = '';
                tile.style.position = '';
                tile.style.zIndex = '';
            };

            const isPhoneLike = () => {
                // En el mosaico:
                // - Celular: swipe izquierda/derecha
                // - Tablet/PC: swipe hacia arriba
                // isMobile() detecta también tablets por UA, así que usamos tamaño como heurística.
                try {
                    const shortSide = Math.min(window.innerWidth || 0, window.innerHeight || 0);
                    return !!(isMobile && typeof isMobile === 'function' && isMobile() && shortSide > 0 && shortSide < 700);
                } catch (_) {
                    return false;
                }
            };

            const thresholdFor = (tile) => {
                try {
                    const r = tile.getBoundingClientRect();
                    const base = axis === 'y' ? r.height : r.width;
                    return Math.max(90, Math.min(180, Math.round((base || 320) * 0.30)));
                } catch (_) {
                    return 120;
                }
            };

            function onPointerDown(e) {
                const tile = e.target && e.target.closest && e.target.closest('.windows-mosaic-tile');
                if (!tile || !windowsMosaicGrid.contains(tile)) return;
                if (e.target && e.target.closest && e.target.closest('.windows-mosaic-close')) return;
                if (Date.now() < mosaicSwipeSuppressClickUntil) return;

                dragTile = tile;
                pointerId = e.pointerId;
                startX = e.clientX;
                startY = e.clientY;
                lastDx = 0;
                lastDy = 0;
                isSwiping = false;
                axis = 'none';
            }

            function onPointerMove(e) {
                if (!dragTile || pointerId === null || e.pointerId !== pointerId) return;
                const dx = e.clientX - startX;
                const dy = e.clientY - startY;
                lastDx = dx;
                lastDy = dy;

                if (!isSwiping) {
                    // decidir eje según dispositivo
                    const wantAxis = isPhoneLike() ? 'x' : 'y';
                    if (wantAxis === 'x') {
                        if (Math.abs(dx) < 10) return;
                        if (Math.abs(dy) > Math.abs(dx) * 0.9) return;
                        axis = 'x';
                    } else {
                        if (Math.abs(dy) < 10) return;
                        if (Math.abs(dx) > Math.abs(dy) * 0.9) return;
                        axis = 'y';
                    }
                    isSwiping = true;
                    dragTile.classList.add('mobile-hub-swipe-active');
                    dragTile.style.position = 'relative';
                    dragTile.style.zIndex = '6';
                    try { dragTile.setPointerCapture(pointerId); } catch (_) {}
                }

                e.preventDefault();
                const thr = thresholdFor(dragTile);
                if (axis === 'x') {
                    const clamped = Math.max(-thr * 1.2, Math.min(thr * 1.2, dx));
                    const progress = Math.min(1, Math.abs(clamped) / thr);
                    dragTile.style.transform = `translateX(${clamped}px)`;
                    dragTile.style.opacity = String(Math.max(0.18, 1 - progress * 0.65));
                } else if (axis === 'y') {
                    // solo swipe hacia ARRIBA para cerrar
                    const up = Math.min(0, dy);
                    const clamped = Math.max(-thr * 1.2, up);
                    const progress = Math.min(1, Math.abs(clamped) / thr);
                    dragTile.style.transform = `translateY(${clamped}px)`;
                    dragTile.style.opacity = String(Math.max(0.18, 1 - progress * 0.65));
                }
            }

            function onPointerUp(e) {
                if (pointerId === null || e.pointerId !== pointerId) return;
                try { dragTile && dragTile.releasePointerCapture(pointerId); } catch (_) {}

                const tile = dragTile;
                const dx = lastDx;
                const dy = lastDy;
                const wasSwiping = isSwiping;
                const usedAxis = axis;

                dragTile = null;
                pointerId = null;
                isSwiping = false;
                axis = 'none';

                if (!tile || !wasSwiping) return;

                const thr = thresholdFor(tile);
                const winId = tile.dataset.windowId;

                const shouldClose = usedAxis === 'x'
                    ? Math.abs(dx) >= thr
                    : (usedAxis === 'y' ? (dy <= -thr) : false);

                if (shouldClose) {
                    tile.classList.add('mobile-hub-swipe-closing');
                    try {
                        if (usedAxis === 'x') {
                            const sign = dx >= 0 ? 1 : -1;
                            const w = tile.getBoundingClientRect().width || 320;
                            tile.style.transform = `translateX(${sign * Math.round(w * 0.7)}px)`;
                        } else {
                            const h = tile.getBoundingClientRect().height || 220;
                            tile.style.transform = `translateY(${-Math.round(h * 0.7)}px)`;
                        }
                        tile.style.opacity = '0';
                    } catch (_) {}
                    mosaicSwipeSuppressClickUntil = Date.now() + 700;
                    setTimeout(() => {
                        if (winId) closeWindowFromMosaic(winId);
                    }, 180);
                } else {
                    tile.classList.add('mobile-hub-swipe-closing');
                    tile.style.transform = 'translateX(0px) translateY(0px)';
                    tile.style.opacity = '1';
                    setTimeout(() => resetTile(tile), 220);
                }
            }

            windowsMosaicGrid.addEventListener('pointerdown', onPointerDown, { passive: false });
            windowsMosaicGrid.addEventListener('pointermove', onPointerMove, { passive: false });
            windowsMosaicGrid.addEventListener('pointerup', onPointerUp, { passive: true });
            windowsMosaicGrid.addEventListener('pointercancel', onPointerUp, { passive: true });
        }

        function addTaskbarApp(windowId, app) {
            const sid = getAppStableId(app);
            const existing = activeTaskbarApps.find(a => a.appId === sid);
            if (existing) {
                if (!existing.windows.find(w => w.id === windowId)) {
                    existing.windows.push({ id: windowId, frame: document.getElementById(windowId), isMinimized: false, previewCache: null });
                }
                updateTaskbarIcon(existing);
            } else {
                const isPin = pinnedApps.some(p => pinMatchesApp(p, app));
                activeTaskbarApps.push({
                    appId: sid,
                    title: app.title,
                    icon: app.icon,
                    windows: [{ id: windowId, frame: document.getElementById(windowId), isMinimized: false, previewCache: null }],
                    activeWindowId: windowId,
                    isPinned: isPin
                });
                if (!isPin && !taskbarOpenOrder.includes(sid)) {
                    taskbarOpenOrder.push(sid);
                }
                renderTaskbarApps();
            }
        }

        function removeTaskbarApp(windowId, app) {
            const sid = getAppStableId(app);
            const existing = activeTaskbarApps.find(a => a.appId === sid);
            if (existing) {
                existing.windows = existing.windows.filter(w => w.id !== windowId);
                if (existing.windows.length === 0 && !existing.isPinned) {
                    activeTaskbarApps = activeTaskbarApps.filter(a => a.appId !== sid);
                    taskbarOpenOrder = taskbarOpenOrder.filter((id) => id !== sid);
                } else if (existing.activeWindowId === windowId) {
                    existing.activeWindowId = existing.windows[0]?.id;
                }
                renderTaskbarApps();
            }
        }

        function updateTaskbarIcon(appGroup) {
            const iconElement = document.querySelector(`.taskbar-app[data-appid="${appGroup.appId}"]`);
            if (iconElement) {
                const countBadge = iconElement.querySelector('.window-count');
                if (countBadge) {
                    countBadge.textContent = appGroup.windows.length;
                    countBadge.style.display = appGroup.windows.length > 1 ? 'flex' : 'none';
                }
                if (appGroup.activeWindowId) {
                    iconElement.classList.add('active');
                } else {
                    iconElement.classList.remove('active');
                }
            }
        }
        
        function showWindowPreview(appGroup, targetElement, preservedPreviewRect = null) {
            if (currentHoverPreview) {
                currentHoverPreview.remove();
                currentHoverPreview = null;
            }
            if (hoverPreviewTimeout) clearTimeout(hoverPreviewTimeout);
            
            const previewDiv = document.createElement('div');
            previewDiv.className = 'window-preview';
            
            let previewHtml = `<div class="preview-header">${tr('openWindowsPreview')}</div><div class="window-preview-scroll">`;
            appGroup.windows.forEach((win, idx) => {
                const isActive = win.id === appGroup.activeWindowId;
                const isMinimized = win.isMinimized;
                const thumb = getWindowPreviewThumb(win, appGroup);
                let thumbHtml = '';
                if (thumb && thumb.type === 'iframe') {
                    thumbHtml = `
                        <div class="preview-thumb-iframe-wrap">
                            <iframe class="preview-thumb-iframe" src="${thumb.src}" sandbox="allow-scripts allow-popups allow-forms allow-modals" loading="lazy"></iframe>
                        </div>
                    `;
                } else if (thumb && thumb.type === 'video') {
                    thumbHtml = `<video class="preview-thumb-video" src="${thumb.src}" ${thumb.poster ? `poster="${thumb.poster}"` : ''} muted autoplay loop playsinline preload="metadata"></video>`;
                } else if (thumb && thumb.type === 'img') {
                    thumbHtml = `<img class="preview-thumb" src="${thumb.src}" alt="">`;
                } else {
                    thumbHtml = `<img class="preview-thumb preview-thumb-fallback" src="${(thumb && thumb.src) ? thumb.src : (appGroup.icon || '')}" alt="">`;
                }
                previewHtml += `
                    <div class="preview-item" data-window-id="${win.id}" style="background: ${isActive ? 'rgba(255,255,255,0.12)' : 'rgba(255,255,255,0.04)'}">
                        ${thumbHtml}
                        <div class="preview-info">
                            <div class="preview-info-text">
                                <div class="preview-title">${appGroup.title} ${appGroup.windows.length > 1 ? (idx + 1) : ''}</div>
                                <div class="preview-state">${isMinimized ? tr('windowMinimized') : tr('windowActive')}</div>
                            </div>
                            <button type="button" class="close-preview" data-window-id="${win.id}" title="${tr('closeWindow')}"><i class="fas fa-times"></i></button>
                        </div>
                    </div>
                `;
            });
            previewHtml += `</div>`;
            
            previewDiv.innerHTML = previewHtml;
            // Posicionamiento inteligente - al lado del icono (usando tamaño real)
            previewDiv.style.position = 'fixed';
            previewDiv.style.left = '-9999px';
            previewDiv.style.top = '-9999px';
            previewDiv.style.visibility = 'hidden';
            document.body.appendChild(previewDiv);
            const pvRect = previewDiv.getBoundingClientRect();
            previewDiv.style.visibility = '';

            let top;
            let left;
            if (preservedPreviewRect && typeof preservedPreviewRect.left === 'number' && typeof preservedPreviewRect.top === 'number') {
                left = preservedPreviewRect.left;
                top = preservedPreviewRect.top;
            } else {
                const rect = targetElement.getBoundingClientRect();
                const taskbarPos = isMobilePC() ? 'bottom' : getSetting('taskbarPosition', 'bottom');
                if (taskbarPos === 'bottom') {
                    top = rect.top - pvRect.height - 8;
                    left = rect.left + rect.width / 2 - pvRect.width / 2;
                    if (top < 10) top = rect.bottom + 8;
                } else if (taskbarPos === 'top') {
                    top = rect.bottom + 8;
                    left = rect.left + rect.width / 2 - pvRect.width / 2;
                } else if (taskbarPos === 'left') {
                    left = rect.right + 8;
                    top = rect.top + rect.height / 2 - pvRect.height / 2;
                    if (left + pvRect.width > window.innerWidth) left = rect.left - pvRect.width - 8;
                } else {
                    left = rect.left - pvRect.width - 8;
                    top = rect.top + rect.height / 2 - pvRect.height / 2;
                    if (left < 10) left = rect.right + 8;
                }
            }

            previewDiv.style.top = Math.max(10, Math.min(top, window.innerHeight - pvRect.height - 10)) + 'px';
            previewDiv.style.left = Math.max(10, Math.min(left, window.innerWidth - pvRect.width - 10)) + 'px';
            previewDiv.style.zIndex = '10001';
            
            previewDiv.addEventListener('mouseenter', () => {
                if (hoverPreviewTimeout) clearTimeout(hoverPreviewTimeout);
            });
            previewDiv.addEventListener('mouseleave', () => {
                setTimeout(() => {
                    if (currentHoverPreview && !currentHoverPreview.matches(':hover')) {
                        currentHoverPreview.remove();
                        currentHoverPreview = null;
                    }
                }, 200);
            });
            
            previewDiv.querySelectorAll('.preview-item').forEach(item => {
                item.addEventListener('click', (e) => {
                    e.stopPropagation();
                    const windowId = item.dataset.windowId;
                    const targetWin = appGroup.windows.find(w => w.id === windowId);
                    if (targetWin) {
                        targetWin.frame.classList.remove('minimized');
                        targetWin.frame.style.display = 'flex';
                        targetWin.frame.style.zIndex = ++windowZIndex;
                        targetWin.isMinimized = false;
                        appGroup.activeWindowId = targetWin.id;
                        renderTaskbarApps();
                        renderPinnedApps();
                    }
                    if (currentHoverPreview) {
                        currentHoverPreview.remove();
                        currentHoverPreview = null;
                    }
                });
                const closeBtn = item.querySelector('.close-preview');
                if (closeBtn) {
                    closeBtn.addEventListener('click', (e) => {
                        e.stopPropagation();
                        const windowId = closeBtn.dataset.windowId;
                        const targetWin = appGroup.windows.find(w => w.id === windowId);
                        if (targetWin) {
                            const ow = openWindows.find(w => w.id === windowId);
                            const prevRect = currentHoverPreview ? currentHoverPreview.getBoundingClientRect() : null;
                            targetWin.frame.remove();
                            openWindows = openWindows.filter(w => w.id !== windowId);
                            schedulePersistOpenWindowsSession();
                            removeTaskbarApp(windowId, ow && ow.app ? ow.app : { title: appGroup.title, link: appGroup.appId.substring(appGroup.title.length) });
                            renderTaskbarApps();
                            renderPinnedApps();
                            // Mantener posición del menú y refrescar miniaturas si quedan ventanas.
                            if (appGroup.windows.length > 0) {
                                showWindowPreview(appGroup, targetElement, prevRect);
                            } else {
                                if (currentHoverPreview) {
                                    currentHoverPreview.remove();
                                    currentHoverPreview = null;
                                }
                            }
                        }
                    });
                }
            });
            
            currentHoverPreview = previewDiv;
        }
        
        function showContextMenu(options, event) {
            if (!options || !Array.isArray(options) || options.length === 0) return;
            if (currentContextMenu) {
                currentContextMenu.remove();
                currentContextMenu = null;
            }
            // Evitar que quede abierto el stripMenu (duplicación de menús).
            if (stripMenu && stripMenu.style.display === 'block') hideStripMenu();
            if (desktopStripMenu && desktopStripMenu.style.display === 'block') hideStripMenu();
            
            const menu = document.createElement('div');
            menu.className = 'context-menu';
            const menuOwnerIsExplorer = !!(event && event.target && event.target.closest && event.target.closest('.file-explorer'));
            if (menuOwnerIsExplorer) {
                menu.dataset.owner = 'explorer';
                menu.classList.add('context-menu--explorer');
            }
            let menuHtml = '';
            options.forEach(opt => {
                if (opt === 'separator') {
                    menuHtml += '<hr>';
                } else {
                    menuHtml += `<button type="button" class="strip-menu-link ${opt.className}"><i class="${opt.icon}"></i> ${opt.label}</button>`;
                }
            });
            menu.innerHTML = menuHtml;
            const anchor = (event && event.target && event.target.closest)
                ? (event.target.closest('.taskbar-app, .pinned-app, .recycle-bin, .clock, #taskbar, .start-button, .taskbar-apps') || event.target)
                : null;
            const rect = anchor && anchor.getBoundingClientRect ? anchor.getBoundingClientRect() : (event && event.target && event.target.getBoundingClientRect ? event.target.getBoundingClientRect() : null);
            const taskbarPos = getSetting('taskbarPosition', 'bottom');

            // Medir tamaño real del menú para pegarlo al icono (evita desplazamientos).
            menu.style.left = '0px';
            menu.style.top = '0px';
            menu.style.zIndex = '10002';
            menu.style.visibility = 'hidden';
            document.body.appendChild(menu);

            const menuRect = menu.getBoundingClientRect();
            const menuWidth = menuRect.width;
            const menuHeight = menuRect.height;

            let left = 10;
            let top = 10;
            // Explorador / menú junto al cursor: la lógica tipo barra de tareas desplazaba el menú
            // fuera de la vista o sobre el borde de la ventana.
            if (menuOwnerIsExplorer && event && typeof event.clientX === 'number' && typeof event.clientY === 'number') {
                left = event.clientX;
                top = event.clientY;
                if (left + menuWidth > window.innerWidth - 8) left = event.clientX - menuWidth;
                if (top + menuHeight > window.innerHeight - 8) top = event.clientY - menuHeight;
                if (left < 8) left = 8;
                if (top < 8) top = 8;
            } else if (rect) {
                if (taskbarPos === 'bottom') {
                    left = rect.left;
                    top = rect.top - menuHeight - 6;
                } else if (taskbarPos === 'top') {
                    left = rect.left;
                    top = rect.bottom + 6;
                } else if (taskbarPos === 'left') {
                    left = rect.right + 6;
                    top = rect.top + rect.height / 2 - menuHeight / 2;
                } else if (taskbarPos === 'right') {
                    left = rect.left - menuWidth - 6;
                    top = rect.top + rect.height / 2 - menuHeight / 2;
                } else {
                    left = rect.left;
                    top = rect.bottom + 6;
                }
            } else {
                left = (typeof event.clientX === 'number') ? event.clientX - (menuWidth / 2) : 10;
                top = (typeof event.clientY === 'number') ? event.clientY - menuHeight - 10 : 10;
                if (top < 10 && typeof event.clientY === 'number') top = event.clientY + 10;
            }

            left = Math.max(10, Math.min(left, window.innerWidth - menuWidth - 10));
            top = Math.max(10, Math.min(top, window.innerHeight - menuHeight - 10));
            menu.style.left = left + 'px';
            menu.style.top = top + 'px';
            menu.style.visibility = '';

            currentContextMenu = menu;
            
            options.forEach(opt => {
                if (opt !== 'separator') {
                    const esc = (typeof CSS !== 'undefined' && CSS.escape) ? CSS.escape(opt.className) : opt.className.replace(/\\/g, '\\\\').replace(/"/g, '\\"');
                    const btn = menu.querySelector('button.' + esc);
                    if (btn) {
                        btn.addEventListener('click', (e) => {
                            opt.action(e);
                            menu.remove();
                            if (currentContextMenu === menu) currentContextMenu = null;
                        });
                    }
                }
            });
            
            const closeMenu = (e) => {
                if (!menu.contains(e.target)) {
                    menu.remove();
                    if (currentContextMenu === menu) currentContextMenu = null;
                    document.removeEventListener('click', closeMenu, true);
                    document.removeEventListener('contextmenu', closeMenu, true);
                }
            };
            setTimeout(() => {
                document.addEventListener('click', closeMenu, true);
                document.addEventListener('contextmenu', closeMenu, true);
            }, 100);
        }
        
        function showWindowContextMenu(appGroup, targetElement, event) {
            const options = [
                { className: 'open-new', icon: 'fas fa-plus', label: tr('openNewWindow'), action: () => {
                    const appLink = appGroup.appId.substring(appGroup.title.length);
                    const app = apps.find(a => a.title === appGroup.title && getAppLinkKey(a) === appLink);
                    if (app) launchDesktopApp(app);
                }},
                { className: 'restore-all', icon: 'fas fa-window-restore', label: tr('restoreAll'), action: () => {
                    appGroup.windows.forEach(win => {
                        win.frame.classList.remove('minimized');
                        win.frame.style.display = 'flex';
                        win.frame.style.zIndex = ++windowZIndex;
                        win.isMinimized = false;
                    });
                    if (appGroup.windows.length > 0) appGroup.activeWindowId = appGroup.windows[0].id;
                    renderTaskbarApps();
                }},
                { className: 'minimize-all', icon: 'fas fa-window-minimize', label: tr('minimizeAll'), action: () => {
                    appGroup.windows.forEach(win => {
                        win.frame.classList.add('minimized');
                        win.frame.style.display = 'none';
                        win.isMinimized = true;
                    });
                    appGroup.activeWindowId = null;
                    renderTaskbarApps();
                }},
                'separator',
                { className: 'pin-btn', icon: `${appGroup.isPinned ? 'fas fa-star' : 'far fa-star'}`, label: appGroup.isPinned ? tr('unpinTaskbar') : tr('pinTaskbar'), action: () => {
                    if (appGroup && appGroup.appId === getAppStableId(FILE_EXPLORER_APP)) {
                        showToast('Explorador no se puede desanclar');
                        return;
                    }
                    const appLink = appGroup.appId.substring(appGroup.title.length);
                    if (appGroup.isPinned) {
                        pinnedApps = pinnedApps.filter(p => !(p.title === appGroup.title && (p.link || '') === appLink));
                        appGroup.isPinned = false;
                        showToast(`${appGroup.title} desanclado de la barra de tareas`);
                    } else {
                        pinnedApps.push({ title: appGroup.title, link: appLink, icon: appGroup.icon });
                        appGroup.isPinned = true;
                        showToast(`${appGroup.title} anclado a la barra de tareas`);
                    }
                    saveSettings({ pinnedApps: pinnedApps });
                    renderPinnedApps();
                    renderTaskbarApps();
                }},
                'separator',
                { className: 'close-all', icon: 'fas fa-times-circle', label: tr('closeAll'), action: () => {
                    appGroup.windows.forEach(win => {
                        win.frame.remove();
                        removeTaskbarApp(win.id, { title: appGroup.title, link: appGroup.appId.substring(appGroup.title.length) });
                    });
                }}
            ];
            showContextMenu(options, event);
        }

        function renderTaskbarApps() {
            if (!taskbarAppsContainer) return;
            taskbarAppsContainer.innerHTML = '';
            // Evitar duplicar el icono: las apps ancladas se muestran en `pinned-apps`.
            const appsWithWindows = activeTaskbarApps.filter(app => {
                const appLink = app.appId.substring(app.title.length);
                const isPinnedNow = pinnedApps.some(p => p.title === app.title && (p.link || '') === appLink);
                return app.windows.length > 0 && !isPinnedNow;
            });
            mergeTaskbarOpenOrder(appsWithWindows.map((g) => g.appId));
            const sortedApps = sortTaskbarGroupsByOpenOrder(appsWithWindows);
            sortedApps.forEach(appGroup => {
                const appDiv = document.createElement('div');
                appDiv.className = `taskbar-app ${appGroup.activeWindowId ? 'active' : ''}`;
                appDiv.dataset.appid = appGroup.appId;
                appDiv.draggable = true;
                appDiv.addEventListener('dragstart', (e) => {
                    e.dataTransfer.effectAllowed = 'move';
                    e.dataTransfer.setData('text/plain', 'taskbar');
                    appDiv.classList.add('taskbar-dragging');
                });
                appDiv.addEventListener('dragend', () => {
                    appDiv.classList.remove('taskbar-dragging');
                });
                const hasMinimized = appGroup.windows.some(w => w.isMinimized);
                appDiv.innerHTML = `
                    <img src="${appGroup.icon}" alt="${appGroup.title}">
                    <span class="window-count" style="display: ${appGroup.windows.length > 1 ? 'flex' : 'none'}">${appGroup.windows.length}</span>
                    ${hasMinimized ? '<span class="minimized-indicator" style="position:absolute; bottom:-2px; left:50%; transform:translateX(-50%); width:6px; height:6px; background:#ffbd2e; border-radius:50%;"></span>' : ''}
                `;
                appDiv.setAttribute('title', `${appGroup.title} (${appGroup.windows.length} ventana${appGroup.windows.length !== 1 ? 's' : ''})`);
                const taskbarAppImg = appDiv.querySelector('img');
                if (taskbarAppImg) taskbarAppImg.draggable = false;
                
                appDiv.addEventListener('mouseenter', (e) => {
                    if (appGroup.windows.length === 0) return;
                    if (hoverPreviewTimeout) clearTimeout(hoverPreviewTimeout);
                    hoverPreviewTimeout = setTimeout(() => {
                        showWindowPreview(appGroup, e.target);
                    }, 400);
                });
                appDiv.addEventListener('mouseleave', () => {
                    if (hoverPreviewTimeout) clearTimeout(hoverPreviewTimeout);
                    setTimeout(() => {
                        if (currentHoverPreview && !currentHoverPreview.matches(':hover')) {
                            currentHoverPreview.remove();
                            currentHoverPreview = null;
                        }
                    }, 300);
                });
                
                appDiv.addEventListener('click', () => {
                    if (Date.now() < taskbarDragSuppressClickUntil) return;
                    if (appGroup.windows.length === 1) {
                        const activeWindow = appGroup.windows[0];
                        if (activeWindow.isMinimized) {
                            activeWindow.frame.classList.remove('minimized');
                            activeWindow.frame.style.display = 'flex';
                            activeWindow.frame.style.zIndex = ++windowZIndex;
                            activeWindow.isMinimized = false;
                            appGroup.activeWindowId = activeWindow.id;
                        } else if (appGroup.activeWindowId === activeWindow.id) {
                            activeWindow.frame.classList.add('minimized');
                            activeWindow.frame.style.display = 'none';
                            activeWindow.isMinimized = true;
                            appGroup.activeWindowId = null;
                        } else {
                            activeWindow.frame.style.zIndex = ++windowZIndex;
                            appGroup.activeWindowId = activeWindow.id;
                        }
                    } else if (appGroup.windows.length > 1) {
                        showWindowPreview(appGroup, appDiv);
                    }
                    renderTaskbarApps();
                });
                
                appDiv.addEventListener('contextmenu', (e) => {
                    e.preventDefault();
                    showWindowContextMenu(appGroup, e.target, e);
                });
                taskbarAppsContainer.appendChild(appDiv);
            });
            // Mantener contadores/badges de apps ancladas sincronizados.
            renderPinnedApps();
            renderMobileAppHub();
            updateMobileHubBadge();
        }

        function updateMobileHubBadge() {
            if (!mobileAppHubCount) return;
            const openAppsCount = activeTaskbarApps.filter(g => g && g.windows && g.windows.length > 0).length;
            mobileAppHubCount.textContent = String(openAppsCount);
            mobileAppHubCount.style.display = openAppsCount > 0 ? 'inline-flex' : 'none';
        }

        function closeMobileAppHub() {
            if (!mobileAppHubBackdrop) return;
            mobileAppHubBackdrop.classList.remove('visible');
            mobileAppHubBackdrop.setAttribute('aria-hidden', 'true');
        }

        function openMobileAppHub() {
            if (!mobileAppHubBackdrop) return;
            renderMobileAppHub();
            mobileAppHubBackdrop.classList.add('visible');
            mobileAppHubBackdrop.setAttribute('aria-hidden', 'false');
        }

        function toggleMobileAppHub() {
            if (!mobileAppHubBackdrop) return;
            if (mobileAppHubBackdrop.classList.contains('visible')) {
                closeMobileAppHub();
            } else {
                openMobileAppHub();
            }
        }

        function openAppFromHub(app) {
            if (!app) return;
            const appGroup = activeTaskbarApps.find(a => a.appId === getAppStableId(app));
            if (appGroup && appGroup.windows.length > 0) {
                if (appGroup.windows.length === 1) {
                    const w = appGroup.windows[0];
                    if (w.isMinimized) {
                        w.frame.classList.remove('minimized');
                        w.frame.style.display = 'flex';
                        w.isMinimized = false;
                    }
                    w.frame.style.zIndex = ++windowZIndex;
                    appGroup.activeWindowId = w.id;
                    renderTaskbarApps();
                    renderPinnedApps();
                    return;
                }
                showWindowPreview(appGroup, mobileAppHubBtn || taskbar);
                return;
            }
            launchDesktopApp(app);
        }

        function renderMobileAppHub() {
            if (!mobileAppHubPinned || !mobileAppHubOpen) return;
            mobileAppHubPinned.innerHTML = '';
            mobileAppHubOpen.innerHTML = '';

            const pinnedList = [];
            pinnedApps.forEach((p) => {
                const app = apps.find(a => a.title === p.title && getAppLinkKey(a) === (p.link || ''));
                if (app) pinnedList.push(app);
            });

            const winList = openWindows
                .filter(w => w && w.frame && document.body.contains(w.frame))
                .sort((a, b) => (b.frame.style.zIndex || 0) - (a.frame.style.zIndex || 0));

            const makePinnedItem = (app, metaText = '') => {
                const item = document.createElement('button');
                item.type = 'button';
                item.className = 'mobile-app-hub-item';
                item.dataset.pinTitle = app.title;
                item.dataset.pinLink = getAppLinkKey(app);
                item.innerHTML = `
                    <img src="${app.icon}" alt="${app.title}">
                    <span class="name">${app.title}</span>
                    <span class="meta">${metaText}</span>
                `;
                item.addEventListener('click', () => {
                    if (Date.now() < mobileHubSuppressClickUntil) return;
                    openAppFromHub(app);
                    closeMobileAppHub();
                });
                item.addEventListener('contextmenu', (e) => {
                    e.preventDefault();
                    e.stopPropagation();
                    // Contextual igual que en escritorio: usa el stripMenu.
                    const idx = apps.findIndex(a => a && a.title === app.title && getAppLinkKey(a) === getAppLinkKey(app));
                    if (idx !== -1) {
                        editingIndex = idx;
                        selectedAppIndex = idx;
                        selectedGridVisibleIndex = -1;
                        showStripMenu(e, document.getElementById('stripMenu'));
                    }
                });
                return item;
            };

            if (pinnedList.length === 0) {
                const empty = document.createElement('div');
                empty.className = 'mobile-app-hub-item mobile-app-hub-item--empty';
                empty.innerHTML = '<span class="name">Sin apps ancladas</span>';
                mobileAppHubPinned.appendChild(empty);
            } else {
                pinnedList.forEach((app) => {
                    const group = activeTaskbarApps.find(g => g.appId === getAppStableId(app));
                    const count = group ? group.windows.length : 0;
                    mobileAppHubPinned.appendChild(makePinnedItem(app, count > 0 ? `${count} abierta(s)` : 'anclada'));
                });
            }

            if (mobileAppHubSectionOpen) {
                mobileAppHubSectionOpen.classList.toggle('is-hidden', winList.length === 0);
            }

            if (winList.length === 0) {
                return;
            }

            winList.forEach((winEntry) => {
                const app = winEntry.app;
                const frame = winEntry.frame;
                const appGroup = activeTaskbarApps.find(a => a.appId === getAppStableId(app));
                const windowObj = appGroup ? appGroup.windows.find(w => w.id === winEntry.id) : null;
                const minimized = isFrameMinimized(frame);
                const maximized = isFrameMaximized(frame);
                const thumb = windowObj ? getWindowPreviewThumb(windowObj, appGroup) : null;
                const thumbHtml = buildMosaicThumbInnerHtml(thumb, app);

                const tile = document.createElement('div');
                tile.className = 'windows-mosaic-tile';
                tile.dataset.windowId = winEntry.id;
                tile.setAttribute('role', 'button');
                tile.setAttribute('tabindex', '0');
                tile.innerHTML = `
                    <button type="button" class="windows-mosaic-close" title="${tr('closeWindow')}" aria-label="${tr('closeWindow')}"><i class="fas fa-times"></i></button>
                    <div class="windows-mosaic-thumb">${thumbHtml}</div>
                    <div class="windows-mosaic-meta">
                        <div>
                            <div class="windows-mosaic-title">${appGroup?.title || app?.title || tr('windowDefaultTitle')}</div>
                            <div class="windows-mosaic-sub">${minimized ? tr('windowMinimized') : (maximized ? tr('windowActive') : '')}</div>
                        </div>
                        <i class="fas fa-chevron-right" aria-hidden="true"></i>
                    </div>
                `;
                const closeBtn = tile.querySelector('.windows-mosaic-close');
                if (closeBtn) {
                    closeBtn.addEventListener('click', (ev) => {
                        ev.stopPropagation();
                        ev.preventDefault();
                        closeWindowFromMosaic(winEntry.id);
                    });
                }
                tile.addEventListener('click', () => {
                    selectWindowFromMosaic(winEntry.id);
                });
                tile.addEventListener('keydown', (e) => {
                    if (e.key === 'Enter') selectWindowFromMosaic(winEntry.id);
                });
                mobileAppHubOpen.appendChild(tile);
            });

            // En móvil, permitir ordenar ancladas dentro del hub.
            bindMobileHubPinnedReorder();
            // En móvil, swipe para cerrar ventanas abiertas.
            bindMobileHubOpenSwipeToClose();
        }

        /** Acomodo tipo Windows 11 al soltar el arrastre cerca de bordes */
        function applyWindows11Snap(frame, clientX, clientY, ui) {
            const margin = 12;
            const ww = window.innerWidth;
            const wh = window.innerHeight;
            const halfW = Math.floor(ww / 2);
            const halfH = Math.floor(wh / 2);
            const fr = frame.getBoundingClientRect();
            const touchLeftHalf = fr.left <= margin && fr.width >= ww * 0.45;
            const touchRightHalf = fr.right >= ww - margin && fr.width >= ww * 0.45;
            const rh = ui && ui.resizeHandle;
            const mb = ui && ui.maximizeBtn;
            let applied = false;

            const unmax = () => {
                frame.classList.remove('maximized');
                if (rh) rh.style.display = 'block';
                if (mb) mb.innerHTML = '<i class="fas fa-square"></i>';
            };

            if (clientY < margin && clientX > ww * 0.2 && clientX < ww * 0.8) {
                // Guardar rect original para que el botón Restaurar funcione con 1 clic,
                // incluso cuando el maximizado se hizo por snap (sin pasar por el botón).
                const cs = getComputedStyle(frame);
                frame.dataset.savedTop = frame.style.top || cs.top || '';
                frame.dataset.savedLeft = frame.style.left || cs.left || '';
                frame.dataset.savedWidth = frame.style.width || cs.width || '';
                frame.dataset.savedHeight = frame.style.height || cs.height || '';

                frame.classList.add('maximized');
                frame.style.top = '';
                frame.style.left = '';
                frame.style.width = '';
                frame.style.height = '';
                if (rh) rh.style.display = 'none';
                if (mb) mb.innerHTML = '<i class="fas fa-window-restore"></i>';
                return { maximized: true };
            }

            if ((clientX < margin || touchLeftHalf) && clientY >= margin && clientY <= wh - margin) {
                unmax();
                frame.style.left = '0px';
                frame.style.top = '0px';
                frame.style.width = halfW + 'px';
                frame.style.height = wh + 'px';
                applied = true;
            } else if ((clientX > ww - margin || touchRightHalf) && clientY >= margin && clientY <= wh - margin) {
                unmax();
                frame.style.left = halfW + 'px';
                frame.style.top = '0px';
                frame.style.width = halfW + 'px';
                frame.style.height = wh + 'px';
                applied = true;
            } else if (clientX < margin && clientY < margin) {
                unmax();
                frame.style.left = '0px';
                frame.style.top = '0px';
                frame.style.width = halfW + 'px';
                frame.style.height = halfH + 'px';
                applied = true;
            } else if (clientX > ww - margin && clientY < margin) {
                unmax();
                frame.style.left = halfW + 'px';
                frame.style.top = '0px';
                frame.style.width = halfW + 'px';
                frame.style.height = halfH + 'px';
                applied = true;
            } else if (clientX < margin && clientY > wh - margin) {
                unmax();
                frame.style.left = '0px';
                frame.style.top = halfH + 'px';
                frame.style.width = halfW + 'px';
                frame.style.height = halfH + 'px';
                applied = true;
            } else if (clientX > ww - margin && clientY > wh - margin) {
                unmax();
                frame.style.left = halfW + 'px';
                frame.style.top = halfH + 'px';
                frame.style.width = halfW + 'px';
                frame.style.height = halfH + 'px';
                applied = true;
            }

            if (applied) return { maximized: false };
            return null;
        }

        function refreshThemeContrast() {
            const parseBg = (str) => {
                if (!str || str === 'transparent') return null;
                const m = str.match(/rgba?\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)(?:\s*,\s*([\d.]+))?\s*\)/);
                if (!m) return null;
                return {
                    rgb: [parseInt(m[1], 10), parseInt(m[2], 10), parseInt(m[3], 10)],
                    alpha: m[4] !== undefined ? parseFloat(m[4]) : 1
                };
            };
            const relLum = (r, g, b) => {
                const lin = [r, g, b].map(v => {
                    v /= 255;
                    return v <= 0.03928 ? v / 12.92 : Math.pow((v + 0.055) / 1.055, 2.4);
                });
                return 0.2126 * lin[0] + 0.7152 * lin[1] + 0.0722 * lin[2];
            };
            const dark = '#0f172a';
            const light = '#f8fafc';
            const lightSurface = [224, 229, 236];
            const rawBg = getComputedStyle(document.body).backgroundColor;
            let parsed = parseBg(rawBg);
            if (!parsed) parsed = { rgb: lightSurface.slice(), alpha: 1 };
            let rgb = parsed.rgb;
            let alpha = parsed.alpha;
            if (alpha < 0.14) {
                rgb = lightSurface.slice();
            } else if (alpha < 0.45 && relLum(rgb[0], rgb[1], rgb[2]) < 0.12) {
                rgb = lightSurface.slice();
            }
            const lum = relLum(rgb[0], rgb[1], rgb[2]);
            let text = lum > 0.52 ? dark : light;
            let muted = lum > 0.52 ? '#334155' : '#cbd5e1';
            let shadow = lum > 0.52 ? 'rgba(0,0,0,0.12)' : 'rgba(0,0,0,0.55)';
            /* Glassmorfismo: paneles tipo vidrio claro; texto oscuro para contraste automático (árbol, lista, barra). */
            if (document.body.classList.contains('glassmorphism') || document.body.classList.contains('glassgradient') || document.body.classList.contains('auroracrystal3d')) {
                text = dark;
                muted = '#475569';
                shadow = 'rgba(0,0,0,0.14)';
            }
            document.documentElement.style.setProperty('--theme-contrast-text', text);
            document.documentElement.style.setProperty('--theme-contrast-muted', muted);
            document.documentElement.style.setProperty('--theme-contrast-shadow', shadow);
        }

        /**
         * Arrastre por la barra de título: la ventana solo se mueve si el botón principal
         * sigue pulsado (e.buttons) y el puntero supera un pequeño umbral, para no confundir clic con drag.
         */
        function bindTitlebarDragToMoveWindow(frame, titlebar, options) {
            const getIsMaximized = options.getIsMaximized;
            const getSnapUi = options.getSnapUi;
            const onSnapResult = options.onSnapResult;
            const schedulePersist = options.schedulePersist;
            const THRESHOLD_PX = 5;
            let pointerDown = false;
            let dragging = false;
            let startClientX = 0;
            let startClientY = 0;
            let originLeft = 0;
            let originTop = 0;

            const onMove = (e) => {
                if (!pointerDown) return;
                if (!(e.buttons & 1)) {
                    pointerDown = false;
                    dragging = false;
                    frame.style.cursor = '';
                    return;
                }
                if (!dragging) {
                    if (Math.abs(e.clientX - startClientX) < THRESHOLD_PX && Math.abs(e.clientY - startClientY) < THRESHOLD_PX) return;
                    dragging = true;
                    frame.style.cursor = 'grabbing';
                }
                frame.style.left = (originLeft + (e.clientX - startClientX)) + 'px';
                frame.style.top = (originTop + (e.clientY - startClientY)) + 'px';
            };

            const onUp = (ev) => {
                if (!pointerDown) return;
                const wasDragging = dragging;
                pointerDown = false;
                dragging = false;
                frame.style.cursor = '';
                if (wasDragging) {
                    const snap = applyWindows11Snap(frame, ev.clientX, ev.clientY, getSnapUi());
                    if (snap && typeof onSnapResult === 'function') onSnapResult(snap);
                    if (typeof schedulePersist === 'function') schedulePersist();
                }
            };

            titlebar.addEventListener('mousedown', (e) => {
                if (getIsMaximized()) return;
                if (e.button !== 0) return;
                if (e.target.closest('.window-controls')) return;
                pointerDown = true;
                dragging = false;
                startClientX = e.clientX;
                startClientY = e.clientY;
                originLeft = frame.offsetLeft;
                originTop = frame.offsetTop;
                e.preventDefault();
            });

            document.addEventListener('mousemove', onMove);
            document.addEventListener('mouseup', onUp);
        }

        // ========== FUNCIONES DE VENTANAS ESTILO MACOS ==========
        function openWindowFrame(app, restoreState = null) {
            const windowId = 'window_' + Date.now() + '_' + Math.random().toString(36).substr(2, 8);
            const frame = document.createElement('div');
            frame.className = 'window-frame maximized';
            if (app && app.windowFrameClass) {
                String(app.windowFrameClass).split(/\s+/).filter(Boolean).forEach((c) => frame.classList.add(c));
            }
            frame.id = windowId;
            frame.style.zIndex = ++windowZIndex;
            frame.style.top = '50px';
            frame.style.left = '50px';
            frame.style.width = '800px';
            frame.style.height = '600px';
            
            frame.innerHTML = `
                <div class="window-titlebar">
                    <div class="window-title">
                        <img src="${app.icon}" onerror="this.src='data:image/svg+xml,%3Csvg xmlns=\'http://www.w3.org/2000/svg\' viewBox=\'0 0 24 24\'%3E%3Cpath fill=\'%23999\' d=\'M4 6h16v2H4V6zm0 4h16v2H4v-2zm0 4h16v2H4v-2z\'/%3E%3C/svg%3E'">
                        <span>${app.title}</span>
                    </div>
                    <div class="window-controls">
                        <button class="minimize-btn" title="Minimizar"><i class="fas fa-minus"></i></button>
                        <button class="maximize-btn" title="Maximizar/Restaurar"><i class="fas fa-window-restore"></i></button>
                        <button class="fullscreen-btn" title="Pantalla completa"><i class="fas fa-expand"></i></button>
                        <button class="zoom-in-btn" title="Acercar iframe"><i class="fas fa-search-plus"></i></button>
                        <button class="zoom-out-btn" title="Alejar iframe"><i class="fas fa-search-minus"></i></button>
                        <button class="zoom-reset-btn" title="Restaurar zoom"><i class="fas fa-undo"></i></button>
                        <button class="external-btn" title="Abrir en nueva pestaña"><i class="fas fa-external-link-alt"></i></button>
                        <button class="close-btn" title="Cerrar"><i class="fas fa-times"></i></button>
                    </div>
                </div>
                <div class="window-content">
                    <iframe src="${escapeHtmlAttr(gridApplyAssetBust(app.link))}" sandbox="allow-same-origin allow-scripts allow-popups allow-forms allow-modals allow-top-navigation"></iframe>
                </div>
                <div class="window-resize-handle"></div>
            `;
            
            document.body.appendChild(frame);

            logActivity('window_open', { title: app.title, action: app.lastAction || 'link', url: app.link || '' });
            
            const titlebar = frame.querySelector('.window-titlebar');
            const content = frame.querySelector('.window-content');
            const iframe = content.querySelector('iframe');
            const titleIcon = frame.querySelector('.window-title img');
            if (titleIcon && iframe) {
                titleIcon.style.cursor = 'pointer';
                titleIcon.setAttribute('title', tr('windowTitleIconReload'));
                titleIcon.addEventListener('mousedown', (e) => e.stopPropagation());
                titleIcon.addEventListener('click', (e) => {
                    e.stopPropagation();
                    try {
                        iframe.contentWindow.location.reload();
                        return;
                    } catch (_) {}
                    const u = String(gridApplyAssetBust(app.link));
                    const sep = u.indexOf('?') >= 0 ? '&' : '?';
                    iframe.src = u + sep + '_gridWinReload=' + Date.now();
                });
            }
            const minimizeBtn = frame.querySelector('.minimize-btn');
            const maximizeBtn = frame.querySelector('.maximize-btn');
            const fullscreenBtn = frame.querySelector('.fullscreen-btn');
            const closeBtn = frame.querySelector('.close-btn');
            const zoomInBtnIframe = frame.querySelector('.zoom-in-btn');
            const zoomOutBtnIframe = frame.querySelector('.zoom-out-btn');
            const zoomResetBtnIframe = frame.querySelector('.zoom-reset-btn');
            const externalBtn = frame.querySelector('.external-btn');
            const resizeHandle = frame.querySelector('.window-resize-handle');
            resizeHandle.style.display = 'none';
            
            let isMaximized = true;
            let isFullscreen = false;
            let savedRect = null;
            const appIframeZoomStorageKey = getAppZoomStorageKey(app, 'iframe');
            let iframeZoom = readZoomFromStorage(appIframeZoomStorageKey, globalIframeZoom, 0.5, 2);
            
            iframe.style.transformOrigin = '0 0';
            iframe.style.transform = `scale(${iframeZoom})`;
            iframe.style.width = `${100 / iframeZoom}%`;
            iframe.style.height = `${100 / iframeZoom}%`;
            
            zoomInBtnIframe.addEventListener('click', () => {
                iframeZoom = Math.min(iframeZoom + 0.1, 2);
                iframe.style.transform = `scale(${iframeZoom})`;
                iframe.style.width = `${100 / iframeZoom}%`;
                iframe.style.height = `${100 / iframeZoom}%`;
                writeZoomToStorage(appIframeZoomStorageKey, iframeZoom, 0.5, 2);
            });
            
            zoomOutBtnIframe.addEventListener('click', () => {
                iframeZoom = Math.max(iframeZoom - 0.1, 0.5);
                iframe.style.transform = `scale(${iframeZoom})`;
                iframe.style.width = `${100 / iframeZoom}%`;
                iframe.style.height = `${100 / iframeZoom}%`;
                writeZoomToStorage(appIframeZoomStorageKey, iframeZoom, 0.5, 2);
            });
            
            zoomResetBtnIframe.addEventListener('click', () => {
                iframeZoom = 1;
                iframe.style.transform = `scale(1)`;
                iframe.style.width = `100%`;
                iframe.style.height = `100%`;
                writeZoomToStorage(appIframeZoomStorageKey, 1, 0.5, 2);
            });
            
            externalBtn.addEventListener('click', () => {
                window.open(gridApplyAssetBust(app.link), '_blank');
            });
            
            iframe.onerror = () => {
                const overlay = document.createElement('div');
                overlay.className = 'iframe-error-overlay';
                overlay.innerHTML = `
                    <p>No se puede cargar el sitio web</p>
                    <button onclick="window.open('${app.link}', '_blank')">Abrir en nueva pestaña</button>
                `;
                content.appendChild(overlay);
            };
            
            minimizeBtn.addEventListener('click', () => {
                const appGroup = activeTaskbarApps.find(a => a.appId === getAppStableId(app));
                if (appGroup) {
                    const windowObj = appGroup.windows.find(w => w.id === windowId);
                    if (windowObj) {
                        // Captura/stock del preview al minimizar (mejor momento para thumbnails/miniiframes).
                        windowObj.previewCache = getWindowPreviewThumb(windowObj, appGroup);
                        windowObj.isMinimized = true;
                    }
                    appGroup.activeWindowId = null;
                    renderTaskbarApps();
                }
                frame.classList.add('minimized');
                frame.style.display = 'none';
                schedulePersistOpenWindowsSession();
            });
            
            maximizeBtn.addEventListener('click', () => {
                if (isMaximized) {
                    frame.classList.remove('maximized');
                    // Restaurar también desde maximizado por snap.
                    if (!savedRect) {
                        const ds = frame.dataset || {};
                        if (ds.savedTop || ds.savedLeft || ds.savedWidth || ds.savedHeight) {
                            savedRect = {
                                top: ds.savedTop,
                                left: ds.savedLeft,
                                width: ds.savedWidth,
                                height: ds.savedHeight
                            };
                        }
                    }
                    if (savedRect) {
                        frame.style.top = savedRect.top;
                        frame.style.left = savedRect.left;
                        frame.style.width = savedRect.width;
                        frame.style.height = savedRect.height;
                    }
                    maximizeBtn.innerHTML = '<i class="fas fa-square"></i>';
                } else {
                    savedRect = {
                        top: frame.style.top,
                        left: frame.style.left,
                        width: frame.style.width,
                        height: frame.style.height
                    };
                    // Compartir con snap/otros caminos.
                    frame.dataset.savedTop = savedRect.top;
                    frame.dataset.savedLeft = savedRect.left;
                    frame.dataset.savedWidth = savedRect.width;
                    frame.dataset.savedHeight = savedRect.height;
                    frame.classList.add('maximized');
                    maximizeBtn.innerHTML = '<i class="fas fa-window-restore"></i>';
                }
                isMaximized = !isMaximized;
                resizeHandle.style.display = isMaximized ? 'none' : 'block';
                schedulePersistOpenWindowsSession();
            });
            
            fullscreenBtn.addEventListener('click', () => {
                if (!isFullscreen) {
                    frame.requestFullscreen();
                    fullscreenBtn.innerHTML = '<i class="fas fa-compress"></i>';
                } else {
                    document.exitFullscreen();
                    fullscreenBtn.innerHTML = '<i class="fas fa-expand"></i>';
                }
                isFullscreen = !isFullscreen;
            });
            
            closeBtn.addEventListener('click', () => {
                frame.remove();
                removeTaskbarApp(windowId, app);
                openWindows = openWindows.filter(w => w.id !== windowId);
                schedulePersistOpenWindowsSession();
            });
            
            let isResizingWindow = false;
            resizeHandle.addEventListener('mousedown', (e) => {
                e.preventDefault();
                e.stopPropagation();
                isResizingWindow = true;
                resizeStartX = e.clientX;
                resizeStartY = e.clientY;
                resizeStartWidth = parseInt(frame.style.width, 10);
                resizeStartHeight = parseInt(frame.style.height, 10);
                
                document.addEventListener('mousemove', resizeMove);
                document.addEventListener('mouseup', resizeStop);
            });
            
            function resizeMove(e) {
                if (!isResizingWindow) return;
                const dx = e.clientX - resizeStartX;
                const dy = e.clientY - resizeStartY;
                frame.style.width = Math.max(300, resizeStartWidth + dx) + 'px';
                frame.style.height = Math.max(200, resizeStartHeight + dy) + 'px';
            }
            
            function resizeStop() {
                isResizingWindow = false;
                document.removeEventListener('mousemove', resizeMove);
                document.removeEventListener('mouseup', resizeStop);
                schedulePersistOpenWindowsSession();
            }

            bindTitlebarDragToMoveWindow(frame, titlebar, {
                getIsMaximized: () => isMaximized,
                getSnapUi: () => ({ resizeHandle, maximizeBtn }),
                onSnapResult: (snap) => { if (snap) isMaximized = !!snap.maximized; },
                schedulePersist: schedulePersistOpenWindowsSession,
            });

            frame.addEventListener('mousedown', () => {
                frame.style.zIndex = ++windowZIndex;
                const appGroup = activeTaskbarApps.find(a => a.appId === getAppStableId(app));
                if (appGroup) {
                    appGroup.activeWindowId = windowId;
                    const windowObj = appGroup.windows.find(w => w.id === windowId);
                    if (windowObj) windowObj.isMinimized = false;
                    renderTaskbarApps();
                }
                schedulePersistOpenWindowsSession();
            });

            if (restoreState) {
                if (restoreState.savedRect) {
                    frame.dataset.savedTop = restoreState.savedRect.top || '';
                    frame.dataset.savedLeft = restoreState.savedRect.left || '';
                    frame.dataset.savedWidth = restoreState.savedRect.width || '';
                    frame.dataset.savedHeight = restoreState.savedRect.height || '';
                }
                if (restoreState.zIndex) frame.style.zIndex = String(restoreState.zIndex);
                if (!restoreState.maximized) {
                    isMaximized = false;
                    frame.classList.remove('maximized');
                    if (restoreState.top) frame.style.top = restoreState.top;
                    if (restoreState.left) frame.style.left = restoreState.left;
                    if (restoreState.width) frame.style.width = restoreState.width;
                    if (restoreState.height) frame.style.height = restoreState.height;
                    maximizeBtn.innerHTML = '<i class="fas fa-square"></i>';
                    resizeHandle.style.display = 'block';
                } else {
                    isMaximized = true;
                    frame.classList.add('maximized');
                    maximizeBtn.innerHTML = '<i class="fas fa-window-restore"></i>';
                    resizeHandle.style.display = 'none';
                }
                if (restoreState.minimized) {
                    frame.classList.add('minimized');
                    frame.style.display = 'none';
                }
            }

            openWindows.push({ id: windowId, frame, app });
            addTaskbarApp(windowId, app);
            if (restoreState && restoreState.minimized) {
                const appGroup = activeTaskbarApps.find(a => a.appId === getAppStableId(app));
                if (appGroup) {
                    const windowObj = appGroup.windows.find(w => w.id === windowId);
                    if (windowObj) windowObj.isMinimized = true;
                    appGroup.activeWindowId = null;
                    renderTaskbarApps();
                }
            }
            schedulePersistOpenWindowsSession();
        }

        /** Icono del panel integrado (admin / usuarios). */
        const BUILTIN_ADMIN_USERS_ICON =
            "data:image/svg+xml," +
            encodeURIComponent(
                '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><rect width="24" height="24" rx="4" fill="#0078d4"/><path fill="#fff" d="M12 11a2.5 2.5 0 110-5 2.5 2.5 0 010 5zm-6 8.5v-.5c0-2.2 2.2-4 6-4s6 1.8 6 4v.5H6z"/></svg>'
            );

        /**
         * Abre el panel de usuarios/admin en una ventana con iframe (misma barra de tareas y mismo marco que apps / explorador).
         */
        function openBuiltInAdminUsersWindow(url) {
            if (!url || typeof url !== 'string') return;
            const resolved = gridApplyAssetBust(url.trim());
            const syntheticApp = {
                title: 'Panel de usuarios y acceso',
                link: resolved,
                lastAction: 'link',
                icon: BUILTIN_ADMIN_USERS_ICON,
            };
            const sid = getAppStableId(syntheticApp);
            try {
                const existing = openWindows.find((w) => w.app && getAppStableId(w.app) === sid);
                if (existing && existing.frame && document.body.contains(existing.frame)) {
                    existing.frame.style.zIndex = ++windowZIndex;
                    existing.frame.classList.remove('minimized');
                    existing.frame.style.display = 'flex';
                    const ag = activeTaskbarApps.find((a) => a.appId === sid);
                    if (ag) {
                        ag.activeWindowId = existing.id;
                        ag.windows.forEach((wo) => {
                            if (wo.id === existing.id) wo.isMinimized = false;
                        });
                    }
                    renderTaskbarApps();
                    renderPinnedApps();
                    return;
                }
            } catch (_) {}
            openWindowFrame(syntheticApp, null);
        }

        /** Ventana maximizada para archivos (mismo comportamiento que iframe: minimizar, snap, barra de tareas). */
        function openFileWindowFrame(app, restoreState = null) {
            if (!app || !app.fileInfo) return;
            if (!restoreState) {
                try {
                    const sid = getAppStableId(app);
                    const existing = openWindows.find(w => w.app && getAppStableId(w.app) === sid);
                    if (existing && existing.frame && document.body.contains(existing.frame)) {
                        existing.frame.style.zIndex = ++windowZIndex;
                        existing.frame.classList.remove('minimized');
                        existing.frame.style.display = 'flex';
                        const ag = activeTaskbarApps.find(a => a.appId === sid);
                        if (ag) {
                            ag.activeWindowId = existing.id;
                            ag.windows.forEach(wo => { if (wo.id === existing.id) wo.isMinimized = false; });
                        }
                        renderTaskbarApps();
                        renderPinnedApps();
                        return;
                    }
                } catch (_) {}
            }
            const windowId = 'filewin_' + Date.now() + '_' + Math.random().toString(36).substr(2, 8);
            const frame = document.createElement('div');
            frame.className = 'window-frame maximized';
            frame.id = windowId;
            frame.style.zIndex = ++windowZIndex;
            frame.style.top = '0';
            frame.style.left = '0';
            frame.style.width = '100%';
            frame.style.height = '100%';
            const iconFallback = "data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath fill='%23999' d='M4 6h16v2H4V6zm0 4h16v2H4v-2zm0 4h16v2H4v-2z'/%3E%3C/svg%3E";
            frame.innerHTML = `
                <div class="window-titlebar">
                    <div class="window-title">
                        <img src="${app.icon || iconFallback}" alt="">
                        <span>${app.title}</span>
                    </div>
                    <div class="window-controls">
                        <button class="minimize-btn" title="Minimizar"><i class="fas fa-minus"></i></button>
                        <button class="maximize-btn" title="Maximizar/Restaurar"><i class="fas fa-window-restore"></i></button>
                        <button class="fullscreen-btn" title="Pantalla completa"><i class="fas fa-expand"></i></button>
                        <button class="close-btn" title="Cerrar"><i class="fas fa-times"></i></button>
                    </div>
                </div>
                <div class="window-content" style="position:relative;">
                    <div class="file-viewer-float-toolbar" id="fileViewerToolbar_${windowId}">
                        <button type="button" class="zoom-in-btn" title="${tr('zoomIn')}"><i class="fas fa-search-plus"></i></button>
                        <button type="button" class="zoom-out-btn" title="${tr('zoomOut')}"><i class="fas fa-search-minus"></i></button>
                        <button type="button" class="zoom-reset-btn" title="${tr('zoomReset')}"><i class="fas fa-undo"></i></button>
                        <button type="button" class="share-btn" title="${tr('share')}"><i class="fas fa-share-alt"></i></button>
                        <button type="button" class="download-btn" title="${tr('download')}"><i class="fas fa-download"></i></button>
                        <button type="button" class="print-btn" title="${tr('print')}"><i class="fas fa-print"></i></button>
                        <button type="button" class="external-btn" title="${tr('openNewWindow') || 'Abrir en nueva pestaña'}"><i class="fas fa-external-link-alt"></i></button>
                    </div>
                    <div id="fileWinContent_${windowId}" style="width:100%;height:100%;position:relative;box-sizing:border-box;"></div>
                </div>
                <div class="window-resize-handle"></div>
            `;
            document.body.appendChild(frame);
            logActivity('window_open', { title: app.title, action: 'file', url: app.fileInfo?.url || '' });
            const titleImg = frame.querySelector('.window-title img');
            if (titleImg) {
                titleImg.addEventListener('error', function te() {
                    titleImg.removeEventListener('error', te);
                    titleImg.src = iconFallback;
                }, { once: true });
            }
            const titlebar = frame.querySelector('.window-titlebar');
            const contentHost = frame.querySelector(`#fileWinContent_${windowId}`);
            const minimizeBtn = frame.querySelector('.minimize-btn');
            const maximizeBtn = frame.querySelector('.maximize-btn');
            const fullscreenBtn = frame.querySelector('.fullscreen-btn');
            const closeBtn = frame.querySelector('.close-btn');
            const zoomInBtnIframe = frame.querySelector('.zoom-in-btn');
            const zoomOutBtnIframe = frame.querySelector('.zoom-out-btn');
            const zoomResetBtnIframe = frame.querySelector('.zoom-reset-btn');
            const shareBtnWin = frame.querySelector('.share-btn');
            const downloadBtnWin = frame.querySelector('.download-btn');
            const printBtnWin = frame.querySelector('.print-btn');
            const externalBtn = frame.querySelector('.external-btn');
            const resizeHandle = frame.querySelector('.window-resize-handle');
            resizeHandle.style.display = 'none';

            let isMaximized = true;
            let isFullscreen = false;
            let savedRect = null;

            const fi = app.fileInfo;
            const fileUrl = fi.url.startsWith('http') ? fi.url : BASE_URL + fi.url;
            const name = fi.name || '';
            const officeEmbed = (u) => 'https://view.officeapps.live.com/op/embed.aspx?src=' + encodeURIComponent(u);
            const googleEmb = (u) => 'https://docs.google.com/viewer?url=' + encodeURIComponent(u) + '&embedded=true';
            const secureFileUrl = buildSecureFileUrl(fileUrl);
            const ext = (name.split('.').pop() || '').toLowerCase();
            const isPdfFile = fi.type === 'application/pdf' || /\.pdf$/i.test(name);
            if (printBtnWin) printBtnWin.style.display = isPdfFile ? '' : 'none';
            const appFileZoomStorageKey = getAppZoomStorageKey(app, 'file');
            let genericZoom = readZoomFromStorage(appFileZoomStorageKey, 1, 0.5, 2.5);
            const applyGenericZoom = () => {
                const scrollWrap = contentHost.querySelector('.pdf-canvas-scroll-wrap');
                if (scrollWrap) {
                    applyPdfCanvasLayerZoom(scrollWrap, genericZoom);
                    return;
                }
                const target = contentHost.querySelector('iframe, embed, pre, video');
                if (!target) return;
                target.style.transformOrigin = '0 0';
                target.style.transform = `scale(${genericZoom})`;
                target.style.width = `${100 / genericZoom}%`;
                target.style.height = `${100 / genericZoom}%`;
            };

            [zoomInBtnIframe, zoomOutBtnIframe, zoomResetBtnIframe].forEach(b => { b.style.display = 'none'; });
            externalBtn.dataset.externalUrl = fileUrl;

            const wireImageZoom = () => {
                zoomInBtnIframe.onclick = () => zoomImageIn();
                zoomOutBtnIframe.onclick = () => zoomImageOut();
                zoomResetBtnIframe.onclick = () => resetImageZoom();
            };
            const wireGenericZoom = () => {
                [zoomInBtnIframe, zoomOutBtnIframe, zoomResetBtnIframe].forEach(b => { b.style.display = ''; });
                applyGenericZoom();
                zoomInBtnIframe.onclick = () => {
                    genericZoom = Math.min(genericZoom + 0.1, 2.5);
                    writeZoomToStorage(appFileZoomStorageKey, genericZoom, 0.5, 2.5);
                    applyGenericZoom();
                };
                zoomOutBtnIframe.onclick = () => {
                    genericZoom = Math.max(genericZoom - 0.1, 0.5);
                    writeZoomToStorage(appFileZoomStorageKey, genericZoom, 0.5, 2.5);
                    applyGenericZoom();
                };
                zoomResetBtnIframe.onclick = () => {
                    genericZoom = 1;
                    writeZoomToStorage(appFileZoomStorageKey, 1, 0.5, 2.5);
                    applyGenericZoom();
                };
            };

            const isGalleryMedia = (fi.type.startsWith('image/') || fi.type.startsWith('video/')) && app.galleryContext;
            if (isGalleryMedia) {
                scale = 1; translateX = 0; translateY = 0; isImageZoomed = false;
                [zoomInBtnIframe, zoomOutBtnIframe, zoomResetBtnIframe].forEach(b => { b.style.display = 'none'; });
                contentHost.innerHTML = `<div style="display:flex;align-items:center;justify-content:center;height:100%;color:#aaa;">${tr('explorerLoading')}</div>`;
                const titleSpanEl = frame.querySelector('.window-title span');
                (async () => {
                    const gc = app.galleryContext;
                    const isRemoteGallery = gc && gc.source === 'remote' && gc.connId;
                    const startName = gc.name || name;
                    let data = null;
                    const isMediaFile = (it) => {
                        if (!it || it.type !== 'file') return false;
                        const mt = (it.mimeType || '').toLowerCase();
                        const nm = (it.name || '').toLowerCase();
                        if (mt.startsWith('image/') || mt.startsWith('video/')) return true;
                        return /\.(jpe?g|png|gif|webp|bmp|svg|ico|mp4|webm|ogg|mov|mkv|m4v)$/i.test(nm);
                    };
                    let itemsList = [];
                    if (isRemoteGallery) {
                        try {
                            data = await fetchExplorerConnList(gc.connId, gc.path || '');
                        } catch (_) { data = null; }
                        itemsList = (data && data.success && Array.isArray(data.items)) ? data.items : [];
                    } else {
                        const parentPath = gc.parentPath || '';
                        try {
                            data = await fetchExplorerList('files', parentPath);
                        } catch (_) { data = null; }
                        itemsList = (data && data.success && Array.isArray(data.items)) ? data.items : [];
                    }
                    let media = itemsList.filter(isMediaFile);
                    media.sort((a, b) => (a.name || '').localeCompare(b.name || '', undefined, { sensitivity: 'base' }));
                    if (media.length === 0) {
                        media = [{ name: fi.name, url: fi.url, mimeType: fi.type }];
                    }
                    let startIdx = media.findIndex(m => m.name === startName);
                    if (startIdx < 0) startIdx = 0;
                    const isVideoItem = (m) => {
                        const mt = (m.mimeType || '').toLowerCase();
                        return mt.startsWith('video/') || /\.(mp4|webm|ogg|mov|mkv|m4v)$/i.test(m.name || '');
                    };
                    const itemUrlLocal = (m) => {
                        const u = m.url || '';
                        return u.startsWith('http') ? u : BASE_URL + u;
                    };
                    const urlCache = new Map();
                    if (isRemoteGallery && fi.url) {
                        const raw = fi.url.startsWith('http') ? fi.url : BASE_URL + fi.url;
                        urlCache.set(startName, buildSecureFileUrl(raw));
                    }
                    const resolveDisplayUrl = async (m) => {
                        if (isRemoteGallery) {
                            if (urlCache.has(m.name)) return urlCache.get(m.name);
                            const r = await explorerConnFetch(gc.connId, gc.path || '', m.name);
                            if (!r || !r.success) return '';
                            const raw = r.url.startsWith('http') ? r.url : BASE_URL + r.url;
                            const u = buildSecureFileUrl(raw);
                            urlCache.set(m.name, u);
                            return u;
                        }
                        return itemUrlLocal(m);
                    };
                    const thumbUrlFor = (m) => {
                        if (isRemoteGallery) {
                            return buildExplorerRemotePreviewUrl(gc.connId, gc.path || '', m.name, m.size);
                        }
                        return itemUrlLocal(m);
                    };
                    const root = document.createElement('div');
                    root.className = 'file-gallery-root';
                    root.tabIndex = 0;
                    root.style.outline = 'none';
                    root.innerHTML = `
                        <div class="file-gallery-main" id="gmain_${windowId}"></div>
                        <button type="button" class="file-gallery-nav file-gallery-prev" aria-label="Prev"><i class="fas fa-chevron-left"></i></button>
                        <button type="button" class="file-gallery-nav file-gallery-next" aria-label="Next"><i class="fas fa-chevron-right"></i></button>
                        <div class="file-gallery-thumbs" id="gthumbs_${windowId}"></div>
                    `;
                    contentHost.innerHTML = '';
                    contentHost.appendChild(root);
                    const gmain = root.querySelector('.file-gallery-main');
                    const thumbs = root.querySelector('.file-gallery-thumbs');
                    const prevBtn = root.querySelector('.file-gallery-prev');
                    const nextBtn = root.querySelector('.file-gallery-next');
                    let current = startIdx;
                    let showSeq = 0;
                    const show = async (i) => {
                        if (i < 0 || i >= media.length) return;
                        const seq = ++showSeq;
                        current = i;
                        const m = media[i];
                        const needFetch = isRemoteGallery && !urlCache.has(m.name);
                        if (needFetch) {
                            gmain.innerHTML = `<div style="display:flex;align-items:center;justify-content:center;height:100%;color:#aaa;">${tr('explorerLoading')}</div>`;
                        }
                        const u = await resolveDisplayUrl(m);
                        if (seq !== showSeq) return;
                        if (!u) {
                            gmain.innerHTML = `<div style="display:flex;align-items:center;justify-content:center;height:100%;color:#c00;">${tr('explorerRemoteLoadFailed')}</div>`;
                            return;
                        }
                        gmain.innerHTML = '';
                        if (isVideoItem(m)) {
                            const v = document.createElement('video');
                            v.src = u;
                            v.controls = true;
                            v.autoplay = true;
                            v.muted = true;
                            v.playsInline = true;
                            v.style.cssText = 'width:100%;height:100%;max-height:100%;object-fit:contain;background:#000;';
                            gmain.appendChild(v);
                            [zoomInBtnIframe, zoomOutBtnIframe, zoomResetBtnIframe].forEach(b => { b.style.display = 'none'; });
                            if (activePreviewImg && frame.contains(activePreviewImg)) {
                                detachPreviewFeTabsObserver();
                                previewViewportWidthSnapshot = 0;
                                activePreviewImg = null;
                            }
                            scale = 1; translateX = 0; translateY = 0; isImageZoomed = false;
                        } else {
                            scale = 1;
                            translateX = 0;
                            translateY = 0;
                            isImageZoomed = false;
                            const wrap = document.createElement('div');
                            wrap.className = 'file-gallery-image-wrap';
                            const img = document.createElement('img');
                            img.id = 'fwimg_' + windowId;
                            img.src = u;
                            img.alt = m.name || '';
                            img.className = 'file-gallery-zoom-img';
                            wrap.appendChild(img);
                            gmain.appendChild(wrap);
                            [zoomInBtnIframe, zoomOutBtnIframe, zoomResetBtnIframe].forEach(b => { b.style.display = ''; });
                            activePreviewImg = img;
                            initPreviewViewportWidthSnapshot();
                            setTimeout(() => attachPreviewFeTabsObserver(), 0);
                            wireImageZoom();
                            setupGalleryImagePanZoom(wrap, img);
                            setupImagePinchZoom(wrap, img);
                            setupGallerySwipeNav(wrap, () => current, (ni) => { void show(ni); }, media.length);
                        }
                        thumbs.querySelectorAll('.file-gallery-thumb').forEach((el, j) => el.classList.toggle('active', j === current));
                        const th = thumbs.children[current];
                        if (th) th.scrollIntoView({ inline: 'nearest', block: 'nearest', behavior: 'smooth' });
                        if (titleSpanEl) titleSpanEl.textContent = m.name || app.title;
                    };
                    media.forEach((m, i) => {
                        const t = document.createElement('div');
                        t.className = 'file-gallery-thumb' + (i === startIdx ? ' active' : '');
                        const tu = thumbUrlFor(m);
                        const tuEsc = String(tu).replace(/"/g, '&quot;');
                        if (isVideoItem(m)) {
                            t.innerHTML = `<video src="${tuEsc}" muted playsinline preload="metadata" style="width:100%;height:100%;object-fit:cover;"></video>`;
                        } else {
                            t.innerHTML = `<img src="${tuEsc}" alt="" style="width:100%;height:100%;object-fit:cover;">`;
                        }
                        t.addEventListener('click', () => { void show(i); });
                        thumbs.appendChild(t);
                    });
                    prevBtn.addEventListener('click', (e) => { e.stopPropagation(); void show((current - 1 + media.length) % media.length); });
                    nextBtn.addEventListener('click', (e) => { e.stopPropagation(); void show((current + 1) % media.length); });
                    root.addEventListener('keydown', (e) => {
                        // Con zoom: flechas panean. Sin zoom: navegan entre medios.
                        if (e.key === 'ArrowLeft') {
                            e.preventDefault();
                            e.stopPropagation();
                            if (isImageZoomed) { translateX += 40; updateImageTransform(); }
                            else prevBtn.click();
                        } else if (e.key === 'ArrowRight') {
                            e.preventDefault();
                            e.stopPropagation();
                            if (isImageZoomed) { translateX -= 40; updateImageTransform(); }
                            else nextBtn.click();
                        } else if (e.key === 'ArrowUp') {
                            if (!isImageZoomed) return;
                            e.preventDefault();
                            e.stopPropagation();
                            translateY += 40; updateImageTransform();
                        } else if (e.key === 'ArrowDown') {
                            if (!isImageZoomed) return;
                            e.preventDefault();
                            e.stopPropagation();
                            translateY -= 40; updateImageTransform();
                        }
                    });
                    setTimeout(() => { try { root.focus(); } catch (_) {} }, 0);
                    void show(startIdx);
                })();
            } else if (fi.type.startsWith('image/')) {
                scale = 1; translateX = 0; translateY = 0; isImageZoomed = false;
                [zoomInBtnIframe, zoomOutBtnIframe, zoomResetBtnIframe].forEach(b => { b.style.display = ''; });
                wireImageZoom();
                contentHost.innerHTML = `<div class="image-preview-container"><img id="fwimg_${windowId}" src="${fileUrl}" alt=""></div>`;
                activePreviewImg = document.getElementById('fwimg_' + windowId);
                if (activePreviewImg) {
                    initPreviewViewportWidthSnapshot();
                    setTimeout(() => attachPreviewFeTabsObserver(), 0);
                    const imgBox = contentHost.querySelector('.image-preview-container');
                    activePreviewImg.addEventListener('mousedown', startDrag);
                    activePreviewImg.addEventListener('touchstart', startDrag, { passive: false });
                    activePreviewImg.addEventListener('wheel', (e) => {
                        const box = imgBox || activePreviewImg.parentElement;
                        handleImageViewerWheel(e, box);
                    }, { passive: false });
                    if (imgBox) setupImagePinchZoom(imgBox, activePreviewImg);
                }
            } else if (fi.type.startsWith('video/')) {
                contentHost.innerHTML = `<video src="${fileUrl}" controls autoplay muted style="width:100%;height:100%;object-fit:contain;background:#000;"></video>`;
            } else if (fi.type.startsWith('audio/')) {
                contentHost.innerHTML = `
                    <div style="display:flex;flex-direction:column;align-items:center;justify-content:center;height:100%;background:#111;">
                        <div class="audio-animation"><span></span><span></span><span></span></div>
                        <audio src="${fileUrl}" controls autoplay style="width:80%;margin-top:16px;"></audio>
                    </div>`;
            } else if (fi.type === 'application/pdf' || /\.pdf$/i.test(name)) {
                setPdfViewerContent(contentHost, fileUrl, {
                    getZoom: () => genericZoom,
                    setZoom: (z) => { genericZoom = z; },
                    applyZoom: applyGenericZoom,
                    hud: {
                        enabled: true,
                        minZoom: 0.5,
                        maxZoom: 2.5,
                        stepPercent: 1,
                        onSetZoom: (z) => {
                            genericZoom = z;
                            writeZoomToStorage(appFileZoomStorageKey, genericZoom, 0.5, 2.5);
                            applyGenericZoom();
                        },
                        onReset: () => {
                            genericZoom = 1;
                            writeZoomToStorage(appFileZoomStorageKey, 1, 0.5, 2.5);
                            applyGenericZoom();
                        }
                    }
                });
                wireGenericZoom();
            } else if (/\.(ppt|pptx)$/i.test(name) || (fi.type && (fi.type.includes('presentation') || fi.type.includes('powerpoint')))) {
                externalBtn.dataset.externalUrl = officeEmbed(secureFileUrl);
                contentHost.innerHTML = `<iframe src="${officeEmbed(secureFileUrl)}" width="100%" height="100%" frameborder="0" allowfullscreen></iframe>`;
                wireGenericZoom();
            } else if (fi.type && (fi.type.includes('spreadsheet') || fi.type.includes('excel')) || /\.(xls|xlsx|csv)$/i.test(name)) {
                const wantsOffice = ['xls', 'xlsx'].includes(ext) || (fi.type && fi.type.toLowerCase().includes('excel'));
                if (wantsOffice) {
                    externalBtn.dataset.externalUrl = officeEmbed(secureFileUrl);
                    contentHost.innerHTML = `<iframe src="${officeEmbed(secureFileUrl)}" width="100%" height="100%" frameborder="0" allowfullscreen></iframe>`;
                } else {
                    externalBtn.dataset.externalUrl = googleEmb(fileUrl);
                    contentHost.innerHTML = `<iframe src="${googleEmb(fileUrl)}" width="100%" height="100%" frameborder="0"></iframe>`;
                }
                wireGenericZoom();
            } else if ((fi.type && (fi.type.includes('word') || fi.type.includes('document'))) || /\.(doc|docx)$/i.test(name)) {
                externalBtn.dataset.externalUrl = officeEmbed(secureFileUrl);
                contentHost.innerHTML = `<iframe src="${officeEmbed(secureFileUrl)}" width="100%" height="100%" frameborder="0" allowfullscreen></iframe>`;
                wireGenericZoom();
            } else if (isTextLikeMimeOrName(fi.type, name)) {
                fetch(fileUrl, { credentials: 'same-origin' }).then(r => r.text()).then(text => {
                    contentHost.innerHTML = `<pre class="file-win-text-pre" style="white-space:pre-wrap;word-wrap:break-word;overflow:auto;flex:1 1 auto;min-height:0;align-self:stretch;width:100%;box-sizing:border-box;padding:12px;margin:0;background:#fff;color:#000;">${escapeHtml(text)}</pre>`;
                    wireGenericZoom();
                }).catch(() => {
                    contentHost.innerHTML = `<p style="padding:12px;color:#eee;">No se pudo cargar el texto.</p>`;
                });
            } else {
                externalBtn.dataset.externalUrl = googleEmb(fileUrl);
                contentHost.innerHTML = `<iframe src="${googleEmb(fileUrl)}" width="100%" height="100%" frameborder="0" sandbox="allow-scripts allow-same-origin allow-popups"></iframe>`;
                wireGenericZoom();
            }

            externalBtn.addEventListener('click', () => {
                const u = externalBtn.dataset.externalUrl || fileUrl;
                const iframeViewer = !!contentHost.querySelector('iframe');
                if (iframeViewer) {
                    window.open(u, '_blank');
                } else {
                    window.open(secureFileUrl, '_blank');
                }
            });
            if (shareBtnWin) {
                shareBtnWin.addEventListener('click', async () => {
                    try { await shareSecureUrl(fileUrl, app.title); } catch (_) {}
                });
            }
            if (downloadBtnWin) {
                downloadBtnWin.addEventListener('click', () => {
                    if (isPdfFile) {
                        downloadFileViaBlob(secureFileUrl, fi.name || 'documento.pdf');
                    } else {
                        const a = document.createElement('a');
                        a.href = secureFileUrl;
                        a.download = fi.name || 'archivo';
                        document.body.appendChild(a);
                        a.click();
                        document.body.removeChild(a);
                    }
                });
            }
            if (printBtnWin && isPdfFile) {
                printBtnWin.addEventListener('click', () => printPdfFromUrl(secureFileUrl, contentHost));
            }

            minimizeBtn.addEventListener('click', () => {
                const appGroup = activeTaskbarApps.find(a => a.appId === getAppStableId(app));
                if (appGroup) {
                    const windowObj = appGroup.windows.find(w => w.id === windowId);
                    if (windowObj) {
                        // Captura/stock del preview al minimizar.
                        windowObj.previewCache = getWindowPreviewThumb(windowObj, appGroup);
                        windowObj.isMinimized = true;
                    }
                    appGroup.activeWindowId = null;
                    renderTaskbarApps();
                }
                frame.classList.add('minimized');
                frame.style.display = 'none';
                schedulePersistOpenWindowsSession();
            });

            maximizeBtn.addEventListener('click', () => {
                if (isMaximized) {
                    frame.classList.remove('maximized');
                    // Restaurar también desde maximizado por snap.
                    if (!savedRect) {
                        const ds = frame.dataset || {};
                        if (ds.savedTop || ds.savedLeft || ds.savedWidth || ds.savedHeight) {
                            savedRect = {
                                top: ds.savedTop,
                                left: ds.savedLeft,
                                width: ds.savedWidth,
                                height: ds.savedHeight
                            };
                        }
                    }
                    if (savedRect) {
                        frame.style.top = savedRect.top;
                        frame.style.left = savedRect.left;
                        frame.style.width = savedRect.width;
                        frame.style.height = savedRect.height;
                    }
                    maximizeBtn.innerHTML = '<i class="fas fa-square"></i>';
                } else {
                    savedRect = {
                        top: frame.style.top,
                        left: frame.style.left,
                        width: frame.style.width,
                        height: frame.style.height
                    };
                    // Compartir con snap/otros caminos.
                    frame.dataset.savedTop = savedRect.top;
                    frame.dataset.savedLeft = savedRect.left;
                    frame.dataset.savedWidth = savedRect.width;
                    frame.dataset.savedHeight = savedRect.height;
                    frame.classList.add('maximized');
                    maximizeBtn.innerHTML = '<i class="fas fa-window-restore"></i>';
                }
                isMaximized = !isMaximized;
                resizeHandle.style.display = isMaximized ? 'none' : 'block';
                schedulePersistOpenWindowsSession();
            });

            fullscreenBtn.addEventListener('click', () => {
                if (!isFullscreen) {
                    frame.requestFullscreen();
                    fullscreenBtn.innerHTML = '<i class="fas fa-compress"></i>';
                } else {
                    document.exitFullscreen();
                    fullscreenBtn.innerHTML = '<i class="fas fa-expand"></i>';
                }
                isFullscreen = !isFullscreen;
            });

            closeBtn.addEventListener('click', () => {
                if (activePreviewImg && frame.contains(activePreviewImg)) {
                    detachPreviewFeTabsObserver();
                    previewViewportWidthSnapshot = 0;
                    activePreviewImg = null;
                    scale = 1; translateX = 0; translateY = 0; isImageZoomed = false;
                }
                frame.remove();
                removeTaskbarApp(windowId, app);
                openWindows = openWindows.filter(w => w.id !== windowId);
                schedulePersistOpenWindowsSession();
            });

            let isResizingWindow = false;
            resizeHandle.addEventListener('mousedown', (e) => {
                e.preventDefault();
                e.stopPropagation();
                isResizingWindow = true;
                resizeStartX = e.clientX;
                resizeStartY = e.clientY;
                resizeStartWidth = parseInt(frame.style.width, 10);
                resizeStartHeight = parseInt(frame.style.height, 10);
                document.addEventListener('mousemove', resizeMoveFw);
                document.addEventListener('mouseup', resizeStopFw);
            });
            function resizeMoveFw(e) {
                if (!isResizingWindow) return;
                const dx = e.clientX - resizeStartX;
                const dy = e.clientY - resizeStartY;
                frame.style.width = Math.max(300, resizeStartWidth + dx) + 'px';
                frame.style.height = Math.max(200, resizeStartHeight + dy) + 'px';
            }
            function resizeStopFw() {
                isResizingWindow = false;
                document.removeEventListener('mousemove', resizeMoveFw);
                document.removeEventListener('mouseup', resizeStopFw);
                schedulePersistOpenWindowsSession();
            }

            bindTitlebarDragToMoveWindow(frame, titlebar, {
                getIsMaximized: () => isMaximized,
                getSnapUi: () => ({ resizeHandle, maximizeBtn }),
                onSnapResult: (snap) => { if (snap) isMaximized = !!snap.maximized; },
                schedulePersist: schedulePersistOpenWindowsSession,
            });

            frame.addEventListener('mousedown', () => {
                frame.style.zIndex = ++windowZIndex;
                const appGroup = activeTaskbarApps.find(a => a.appId === getAppStableId(app));
                if (appGroup) {
                    appGroup.activeWindowId = windowId;
                    const windowObj = appGroup.windows.find(w => w.id === windowId);
                    if (windowObj) windowObj.isMinimized = false;
                    renderTaskbarApps();
                }
                schedulePersistOpenWindowsSession();
            });

            if (restoreState) {
                if (restoreState.savedRect) {
                    frame.dataset.savedTop = restoreState.savedRect.top || '';
                    frame.dataset.savedLeft = restoreState.savedRect.left || '';
                    frame.dataset.savedWidth = restoreState.savedRect.width || '';
                    frame.dataset.savedHeight = restoreState.savedRect.height || '';
                }
                if (restoreState.zIndex) frame.style.zIndex = String(restoreState.zIndex);
                if (!restoreState.maximized) {
                    isMaximized = false;
                    frame.classList.remove('maximized');
                    if (restoreState.top) frame.style.top = restoreState.top;
                    if (restoreState.left) frame.style.left = restoreState.left;
                    if (restoreState.width) frame.style.width = restoreState.width;
                    if (restoreState.height) frame.style.height = restoreState.height;
                    maximizeBtn.innerHTML = '<i class="fas fa-square"></i>';
                    resizeHandle.style.display = 'block';
                } else {
                    isMaximized = true;
                    frame.classList.add('maximized');
                    maximizeBtn.innerHTML = '<i class="fas fa-window-restore"></i>';
                    resizeHandle.style.display = 'none';
                }
                if (restoreState.minimized) {
                    frame.classList.add('minimized');
                    frame.style.display = 'none';
                }
            }

            openWindows.push({ id: windowId, frame, app });
            addTaskbarApp(windowId, app);
            if (restoreState && restoreState.minimized) {
                const appGroup = activeTaskbarApps.find(a => a.appId === getAppStableId(app));
                if (appGroup) {
                    const windowObj = appGroup.windows.find(w => w.id === windowId);
                    if (windowObj) windowObj.isMinimized = true;
                    appGroup.activeWindowId = null;
                    renderTaskbarApps();
                }
            }
            schedulePersistOpenWindowsSession();
        }

        // ========== FUNCIONES COMUNES ==========
        function clearSearch() {
            searchInput.value = '';
            searchClear.classList.remove('visible');
            renderApps();
            searchInput.focus();
        }

        searchInput.addEventListener('input', function() {
            searchClear.classList.toggle('visible', this.value.length > 0);
            searchApps(this.value);
        });

        addAppFloatBtn.addEventListener('click', () => openModal());

        actionTypeRadios.forEach(radio => {
            radio.addEventListener('change', () => {
                if (radio.value === 'link') {
                    linkInput.style.display = 'block';
                    fileInput.style.display = 'none';
                    document.getElementById('linkTargetSelector').style.display = 'block';
                    updateLinkPreviewButton();
                } else {
                    linkInput.style.display = 'none';
                    fileInput.style.display = 'block';
                    document.getElementById('linkTargetSelector').style.display = 'none';
                    updateFileInfo();
                }
            });
        });

        function updateLinkPreviewButton() {
            previewLinkBtn.style.display = appLinkInput.value.trim() ? 'block' : 'none';
        }
        appLinkInput.addEventListener('input', updateLinkPreviewButton);

        function updateFileInfo() {
            if (currentFile) {
                currentFileName.textContent = `Archivo: ${currentFile.name}`;
                currentFileInfo.style.display = 'flex';
            } else if (editingIndex > -1 && apps[editingIndex] && apps[editingIndex].fileInfo) {
                currentFileName.textContent = `Archivo: ${apps[editingIndex].fileInfo.name}`;
                currentFileInfo.style.display = 'flex';
            } else {
                currentFileInfo.style.display = 'none';
            }
        }

        function previewCurrentLink() {
            const url = appLinkInput.value.trim();
            if (url) showLinkPreview(url);
        }

        /** Centra la ventana de previsualización y la dimensiona según la pantalla (p. ej. al pulsar «Ver» enlace). */
        function layoutPreviewWindowCenteredProportional() {
            if (!previewWindowFrame) return;
            previewIsMaximized = false;
            previewSavedRect = null;
            previewWindowFrame.classList.remove('maximized');
            const rh = document.getElementById('previewResizeHandle');
            if (rh) rh.style.display = 'block';

            const margin = 28;
            const vr = getLayoutViewportRect();
            const vw = vr.width;
            const vh = vr.height;
            let w = Math.round(Math.min(vw * 0.92, 1400));
            let h = Math.round(Math.min(vh * 0.86, 1000));
            w = Math.max(320, Math.min(w, vw - margin * 2));
            h = Math.max(240, Math.min(h, vh - margin * 2));
            const left = Math.round(vr.left + (vw - w) / 2);
            const top = Math.round(vr.top + (vh - h) / 2);
            previewWindowFrame.style.left = left + 'px';
            previewWindowFrame.style.top = top + 'px';
            previewWindowFrame.style.width = w + 'px';
            previewWindowFrame.style.height = h + 'px';
        }

        function showLinkPreview(url) {
            const modalTitle = document.getElementById('previewModalTitle');
            const previewContent = document.getElementById('previewContent');
            const shareBtn = document.getElementById('shareBtn');
            const downloadBtn = document.getElementById('downloadBtn');
            
            modalTitle.textContent = 'Vista previa del enlace';
            
            const yt = window.GridYoutubeUrl;
            const ytPreviewId = yt && yt.extractYoutubeId(url);
            if (ytPreviewId && yt.buildYoutubeWallpaperEmbedUrl) {
                const embedSrc = yt.buildYoutubeWallpaperEmbedUrl(ytPreviewId);
                const ytIframe = yt.youtubeWallpaperIframeHtml
                    ? yt.youtubeWallpaperIframeHtml(embedSrc, 'width="100%" height="100%"')
                    : `<iframe width="100%" height="100%" src="${embedSrc}" referrerpolicy="strict-origin-when-cross-origin" frameborder="0" allow="autoplay; fullscreen; encrypted-media" allowfullscreen></iframe>`;
                previewContent.innerHTML = ytIframe;
            } else {
                previewContent.innerHTML = `<iframe src="${url}" style="width:100%; height:100%; border:none;"></iframe>`;
            }
            
            shareBtn.style.display = 'inline-block';
            downloadBtn.style.display = 'none';
            const previewPrintLink = document.getElementById('previewPrintBtn');
            if (previewPrintLink) previewPrintLink.style.display = 'none';
            if (previewExternalBtn) previewExternalBtn.dataset.externalUrl = url;
            layoutPreviewWindowCenteredProportional();
            document.getElementById('previewModal').style.display = 'block';
            document.body.classList.add('modal-open');
            previewActive = true;
            schedulePreviewVisualViewportLayout();
            setTimeout(schedulePreviewVisualViewportLayout, 120);
            setTimeout(schedulePreviewVisualViewportLayout, 450);
        }

        function getTypeBadge(app) {
            if (!getSetting('showTypeBadge', true)) return '';
            if (app.isSystemApp) return '';
            let type = '';
            let icon = '';
            if (app.explorerOpen && app.explorerOpen.bucket) {
                type = 'folder';
                icon = 'fas fa-folder';
            } else if (app.lastAction === 'link') {
                type = 'link';
                icon = 'fas fa-link';
            } else if (app.fileInfo) {
                const ext = app.fileInfo.name.split('.').pop().toLowerCase();
                if (['mp4', 'webm', 'ogg', 'mov'].includes(ext)) {
                    type = 'video';
                    icon = 'fas fa-video';
                } else if (['mp3', 'wav', 'ogg'].includes(ext)) {
                    type = 'audio';
                    icon = 'fas fa-music';
                } else if (['jpg', 'jpeg', 'png', 'gif', 'webp'].includes(ext)) {
                    type = 'img';
                    icon = 'fas fa-image';
                } else if (['pdf'].includes(ext)) {
                    type = 'pdf';
                    icon = 'fas fa-file-pdf';
                } else if (['doc', 'docx'].includes(ext)) {
                    type = 'doc';
                    icon = 'fas fa-file-word';
                } else if (['xls', 'xlsx'].includes(ext)) {
                    type = 'xls';
                    icon = 'fas fa-file-excel';
                } else if (['ppt', 'pptx'].includes(ext)) {
                    type = 'ppt';
                    icon = 'fas fa-file-powerpoint';
                } else {
                    type = 'file';
                    icon = 'fas fa-file';
                }
            }
            if (!type) return '';
            const mode = getSetting('typeBadgeMode', 'icon');
            if (mode === 'icon') {
                return `<span class="app-type-badge"><i class="${icon}"></i></span>`;
            } else {
                return `<span class="app-type-badge">${type}</span>`;
            }
        }

        function showToast(message, duration = 2000, type) {
            const toast = document.createElement('div');
            toast.className = 'toast';
            let resolvedType = type;
            if (!resolvedType && window.appDialog && typeof window.appDialog.detectFeedbackType === 'function') {
                resolvedType = window.appDialog.detectFeedbackType(message, 'alert');
            }
            resolvedType = resolvedType || 'info';
            toast.dataset.type = resolvedType;
            const iconClass = (window.appDialog && typeof window.appDialog.feedbackIconClass === 'function')
                ? window.appDialog.feedbackIconClass(resolvedType)
                : 'fas fa-circle-info';
            const iconEl = document.createElement('i');
            iconEl.className = iconClass + ' toast-icon';
            iconEl.setAttribute('aria-hidden', 'true');
            const textEl = document.createElement('span');
            textEl.className = 'toast-text';
            textEl.textContent = String(message == null ? '' : message);
            toast.appendChild(iconEl);
            toast.appendChild(textEl);
            document.body.appendChild(toast);
            setTimeout(() => {
                toast.remove();
            }, duration);
        }

        function applyTaskbarPosition(position) {
            if (isMobilePC()) {
                document.body.classList.remove('taskbar-top', 'taskbar-left', 'taskbar-right', 'taskbar-bottom');
                document.body.classList.add('taskbar-bottom');
                return;
            }
            if (isMobile() && !getSetting('mobileDesktopMode', false)) return;
            document.body.classList.remove('taskbar-top', 'taskbar-bottom', 'taskbar-left', 'taskbar-right');
            document.body.classList.add(`taskbar-${position}`);
            setTimeout(() => {
                if (windowsMenuVisible) {
                    windowsMenu.style.display = 'flex';
                }
            }, 50);
        }

        function updateStartButtonIcon(iconUrl) {
            if (iconUrl) {
                startButton.innerHTML = `<img src="${iconUrl}" alt="inicio">`;
            } else {
                startButton.innerHTML = '<i class="fas fa-th" aria-hidden="true"></i>';
            }
            const removeStartBtn = document.getElementById('removeStartIconBtn');
            if (removeStartBtn) removeStartBtn.style.display = iconUrl ? '' : 'none';
        }
        
        function updateActionButtonsVisibility() {
            const hasLocalVideo = getSetting('backgroundType', 'file') === 'videos' && getSetting('backgroundVideos', []).length > 0;
            const hasLocalVideoFile = getSetting('backgroundType', 'file') === 'file' && getSetting('backgroundUrl', '').match(/\.(mp4|webm|ogg)(\?.*)?$/i);
            const showMute = hasLocalVideo || hasLocalVideoFile;
            muteToggle.style.display = showMute ? 'inline-flex' : 'none';
            
            const hasYoutube = getSetting('backgroundType', 'file') === 'youtube';
            const hasYoutubeUrl = getSetting('youtubeBackground', '') || getSetting('youtubeBackgroundMobile', '');
            youtubeLinkBtn.style.display = (hasYoutube && hasYoutubeUrl) ? 'inline-flex' : 'none';
        }

        function getDisplayApps() {
            const isMobileDevice = isMobile();
            const mobileDesktopMode = getSetting('mobileDesktopMode', false);
            
            if (isMobileDevice && !mobileDesktopMode) {
                const hasRecycleBin = apps.some(a => a.isSystemApp && a.id === 'recycle-bin-system');
                if (!hasRecycleBin) {
                    return [RECYCLE_BIN_APP, ...apps];
                }
            }
            return apps;
        }

        function __destroyLauncherGridControllers() {
            __launcherGridControllers.forEach(function (c) {
                try { c.destroy(); } catch (_) {}
            });
            __launcherGridControllers = [];
        }
        function gridLauncherSuppressNextClick(ms) {
            __gridLauncherSuppressClickUntil = Date.now() + (ms || 400);
        }
        function gridLauncherGetSuppressUntil() {
            return __gridLauncherSuppressClickUntil;
        }

        function launchDesktopApp(app) {
            if (!app) return;
            if (app.isSystemApp && app.action === 'openRecycleBin') {
                openRecycleBin();
                return;
            }
            if (app.isSystemApp && app.action === 'openFileExplorer') {
                openFileExplorerWindow(app);
                return;
            }
            if (app.explorerOpen && app.explorerOpen.bucket) {
                openFileExplorerWindow(app, null, { bucket: app.explorerOpen.bucket, relPath: app.explorerOpen.relPath || '' });
                return;
            }
            if (app.lastAction === 'link' && app.link && app.link !== '#') {
                const target = app.linkTarget || '_blank';
                if (target === 'window') {
                    openWindowFrame(app);
                } else {
                    const link = app.link;
                    if (link.includes('twitter.com') || link.includes('x.com')) {
                        const twitterIntent = link.replace(/^https?:\/\/(www\.)?(twitter\.com|x\.com)/, 'twitter://status');
                        window.open(twitterIntent, target) || window.open(link, target);
                    } else if (link.includes('instagram.com')) {
                        const instagramIntent = link.replace(/^https?:\/\/(www\.)?instagram\.com/, 'instagram://');
                        window.open(instagramIntent, target) || window.open(link, target);
                    } else if (link.includes('youtube.com') || link.includes('youtu.be')) {
                        const youtubeIntent = link.replace(/^https?:\/\/(www\.)?(youtube\.com|youtu\.be)/, 'youtube://');
                        window.open(youtubeIntent, target) || window.open(link, target);
                    } else if (link.includes('spotify.com')) {
                        const spotifyIntent = link.replace(/^https?:\/\/(www\.)?spotify\.com/, 'spotify://');
                        window.open(spotifyIntent, target) || window.open(link, target);
                    } else {
                        window.open(link, target);
                    }
                }
                return;
            }
            if (app.lastAction === 'file' && app.fileInfo) {
                openFileWindowFrame(app);
            }
        }

        function openAppFromGrid(app) {
            if (previewActive) return;
            launchDesktopApp(app);
        }

        function prepareLauncherStripForApp(app, realIndex) {
            launcherContextTarget = { type: 'app', appStableId: getAppStableId(app), indexInApps: realIndex };
            editingIndex = realIndex;
            selectedAppIndex = realIndex;
            const visibleItems = Array.from(document.querySelectorAll('#appGrid .app-item'));
            selectedGridVisibleIndex = visibleItems.findIndex(item => parseInt(item.dataset.index, 10) === realIndex);
            if (editAppBtn) editAppBtn.style.display = '';
            if (toggleGridBtn) toggleGridBtn.style.display = 'none';
            if (pinToTaskbarBtn) pinToTaskbarBtn.style.display = 'none';
            if (deleteAppBtn) deleteAppBtn.style.display = '';
            if (folderOpenBtn) folderOpenBtn.style.display = 'none';
            if (folderInfoBtn) folderInfoBtn.style.display = 'none';
            if (folderRenameBtn) folderRenameBtn.style.display = 'none';
            if (folderColorBtn) folderColorBtn.style.display = 'none';
            if (folderDeleteBtn) folderDeleteBtn.style.display = 'none';
            const appData = app;
            const isMobileDevice = isMobile();
            const mobileDesktopMode = getSetting('mobileDesktopMode', false);
            const desktopMode = getSetting('desktopMode', 'grid');
            const showWindowsMode = (!isMobileDevice && desktopMode === 'windows') || (isMobileDevice && mobileDesktopMode && desktopMode === 'windows');
            if (isMobileDevice || !showWindowsMode) {
                if (toggleGridBtn) toggleGridBtn.style.display = 'none';
            } else {
                if (toggleGridBtn) {
                    toggleGridBtn.style.display = 'block';
                    const onGrid = appIsVisibleOnWindowsLauncherGrid(appData);
                    toggleGridBtn.innerHTML = `<i class="fas ${onGrid ? 'fa-eye-slash' : 'fa-eye'}"></i> ${onGrid ? tr('removeDesktop') : tr('addDesktop')}`;
                }
            }
            if (pinToTaskbarBtn) {
                pinToTaskbarBtn.style.display = (showWindowsMode && ((appData.lastAction === 'link' && appData.link && appData.link !== '#') || (appData.lastAction === 'file' && appData.fileInfo) || (appData.explorerOpen && appData.explorerOpen.bucket))) ? 'block' : 'none';
            }
            if (deleteAppBtn) deleteAppBtn.style.display = 'block';
        }

        function prepareLauncherStripForFolder(folderId) {
            launcherContextTarget = { type: 'folder', folderId: folderId };
            editingIndex = -1;
            if (editAppBtn) editAppBtn.style.display = 'none';
            if (toggleGridBtn) toggleGridBtn.style.display = 'none';
            if (pinToTaskbarBtn) pinToTaskbarBtn.style.display = 'none';
            if (deleteAppBtn) deleteAppBtn.style.display = 'none';
            if (folderOpenBtn) folderOpenBtn.style.display = 'block';
            if (folderInfoBtn) folderInfoBtn.style.display = 'block';
            if (folderRenameBtn) folderRenameBtn.style.display = 'block';
            if (folderColorBtn) folderColorBtn.style.display = 'block';
            if (folderDeleteBtn) folderDeleteBtn.style.display = 'block';
        }

        function handleLauncherGridDrop(payload) {
            if (!payload || !launcherLayout || !window.GridLauncher) return;
            const sourceEl = payload.sourceEl;
            const targetEl = payload.targetEl;
            if (!targetEl || !sourceEl || targetEl === sourceEl) return;
            const gd = GridLauncher.gridDrag;
            const LM = GridLauncher.layoutModel;
            const FS = GridLauncher.folderService;
            const sr = sourceEl.getBoundingClientRect();
            const tr = targetEl.getBoundingClientRect();
            const merge = gd.rectOverlapRatio(sr, tr) >= gd.OVERLAP_RATIO;
            const fromOrder = parseInt(sourceEl.dataset.orderIndex, 10);
            const toOrder = parseInt(targetEl.dataset.orderIndex, 10);
            const sidSource = sourceEl.dataset.appStableId || '';
            const sidTarget = targetEl.dataset.appStableId || '';
            const folderSource = sourceEl.dataset.folderId || '';
            const folderTarget = targetEl.dataset.folderId || '';

            if (!merge) {
                if (fromOrder >= 0 && toOrder >= 0 && fromOrder !== toOrder) {
                    launcherLayout = LM.moveOrderIndex(launcherLayout, fromOrder, toOrder);
                    saveSettings({ launcherLayout: launcherLayout }).then(() => {
                        renderApps();
                    });
                }
                return;
            }
            if (sidSource && sidTarget && sidSource !== sidTarget) {
                const r = FS.createFolderFromApps(launcherLayout, sidSource, sidTarget, LM.DEFAULT_FOLDER_NAME);
                if (!r.error) {
                    launcherLayout = r.layout;
                    saveSettings({ launcherLayout: launcherLayout }).then(() => renderApps());
                }
                return;
            }
            if (sidSource && folderTarget) {
                const r2 = FS.insertAppIntoFolder(launcherLayout, folderTarget, sidSource);
                if (!r2.error) {
                    launcherLayout = r2.layout;
                    saveSettings({ launcherLayout: launcherLayout }).then(() => renderApps());
                }
                return;
            }
            if (folderSource && sidTarget) {
                const r3 = FS.insertAppIntoFolder(launcherLayout, folderSource, sidTarget);
                if (!r3.error) {
                    launcherLayout = r3.layout;
                    saveSettings({ launcherLayout: launcherLayout }).then(() => renderApps());
                }
                return;
            }
            if (fromOrder >= 0 && toOrder >= 0 && fromOrder !== toOrder) {
                launcherLayout = LM.moveOrderIndex(launcherLayout, fromOrder, toOrder);
                saveSettings({ launcherLayout: launcherLayout }).then(() => renderApps());
            }
        }

        function openLauncherFolderModal(folderId) {
            if (!launcherFolderModal || !launcherFolderGrid || !launcherLayout) return;
            const folder = launcherLayout.folders && launcherLayout.folders[folderId];
            if (!folder) return;
            if (launcherFolderModalTitle) launcherFolderModalTitle.textContent = folder.name || 'Carpeta';
            launcherFolderModal.style.display = 'flex';
            document.body.classList.add('modal-open');
            launcherFolderGrid.innerHTML = '';
            const savedGridTextColor = getSetting('gridTextColor', '#000000');
            (folder.appStableIds || []).forEach(function (sid) {
                const found = GridLauncher.layoutModel.findAppByStableId(apps, sid, getAppStableId);
                if (!found || !found.app) return;
                const app = found.app;
                const tile = document.createElement('div');
                tile.className = 'app-item';
                tile.dataset.index = String(found.index);
                tile.innerHTML = `
                    <img src="${app.icon}" alt="${app.title}" class="app-icon">
                    <p style="color: ${savedGridTextColor};">${app.title}</p>
                    ${getTypeBadge(app)}
                `;
                tile.addEventListener('click', function (e) {
                    e.preventDefault();
                    const ev = e;
                    launcherFolderModal.style.display = 'none';
                    document.body.classList.remove('modal-open');
                    openAppFromGrid(app);
                });
                launcherFolderGrid.appendChild(tile);
            });
        }

        function renderApps(filteredApps = getDisplayApps()) {
            const isMobileDevice = isMobile();
            const mobileDesktopMode = getSetting('mobileDesktopMode', false);
            const desktopMode = getSetting('desktopMode', 'grid');
            
            const showWindowsMode = (!isMobileDevice && desktopMode === 'windows') || (isMobileDevice && mobileDesktopMode && desktopMode === 'windows');
            
            if (showWindowsMode) {
                document.body.classList.add('windows-mode');
                const taskbarPos = getSetting('taskbarPosition', 'bottom');
                applyTaskbarPosition(taskbarPos);
                taskbar.style.display = 'flex';
                renderPinnedApps();
                renderTaskbarApps();
                if (windowsMenuVisible) {
                    windowsMenu.style.display = 'flex';
                    windowsMenuBackdrop.style.display = 'block';
                    document.querySelectorAll('.app-item').forEach(el => el.setAttribute('tabindex', '-1'));
                } else {
                    windowsMenu.style.display = 'none';
                    windowsMenuBackdrop.style.display = 'none';
                    document.querySelectorAll('.app-item').forEach(el => el.setAttribute('tabindex', '0'));
                }
            } else {
                document.body.classList.remove('windows-mode');
                taskbar.style.display = 'none';
                windowsMenu.style.display = 'none';
                windowsMenuBackdrop.style.display = 'none';
                windowsMenuVisible = false;
                document.querySelectorAll('.app-item').forEach(el => el.setAttribute('tabindex', '0'));
            }
            renderGrid(filteredApps);
        }

        function renderGrid(filteredApps) {
            __destroyLauncherGridControllers();
            appGrid.innerHTML = '';
            const savedGridTextColor = getSetting('gridTextColor', '#000000');

            const isMobileDevice = isMobile();
            const mobileDesktopMode = getSetting('mobileDesktopMode', false);
            const desktopMode = getSetting('desktopMode', 'grid');
            const isFiltering = searchInput.value.trim() !== '' || categoryFilterSelect.value !== '';
            const searchQuery = searchInput.value.trim();

            let appsToShow;
            if (isMobileDevice && !mobileDesktopMode) {
                appsToShow = filteredApps;
            } else if (isMobileDevice && mobileDesktopMode) {
                appsToShow = filteredApps.filter(app => app.showInGrid !== false || app.isSystemApp);
            } else if (isFiltering || desktopMode === 'grid') {
                appsToShow = filteredApps;
            } else {
                // Modo mosaico/lista: ocultar no-sistema con showInGrid === false, pero las apps sistema
                // deben seguir en appsToShow para que resolveGridRows / prependSystem las muestre.
                const sidSeen = new Set();
                appsToShow = [];
                filteredApps.forEach((app) => {
                    if (!app) return;
                    if (app.isSystemApp) {
                        const sid = getAppStableId(app);
                        if (sidSeen.has(sid)) return;
                        sidSeen.add(sid);
                        appsToShow.push(app);
                    } else if (app.showInGrid !== false) {
                        const sid = getAppStableId(app);
                        if (sidSeen.has(sid)) return;
                        sidSeen.add(sid);
                        appsToShow.push(app);
                    }
                });
            }

            if (!window.GridLauncher || !window.GridLauncher.integration || !launcherLayout) {
                const noAppsMessage = document.createElement('div');
                noAppsMessage.innerHTML = 'Error de módulos del escritorio. Recargue la página.';
                noAppsMessage.style.gridColumn = '1 / -1';
                appGrid.appendChild(noAppsMessage);
                return;
            }

            const integ = GridLauncher.integration;
            const display = integ.buildDisplayRows(apps, appsToShow, launcherLayout, getAppStableId, isFiltering, searchQuery);
            const displayRows = display.prependSystem.concat(display.rows);

            function highlightDropTarget(el) {
                document.querySelectorAll('#appGrid .app-item.launcher-drop-target').forEach(function (n) {
                    n.classList.remove('launcher-drop-target');
                });
                if (el) el.classList.add('launcher-drop-target');
            }

            const launcherApi = {
                appGrid: appGrid,
                previewActive: function () {
                    return previewActive;
                },
                suppressNextClick: gridLauncherSuppressNextClick,
                getSuppressUntil: gridLauncherGetSuppressUntil,
                longPressMs: function () {
                    return getSetting('launcherLongPressMs', GridLauncher.pointerInteraction.DEFAULT_LONG_PRESS_MS);
                },
                moveThresholdPx: function () {
                    return getSetting('launcherMoveThresholdPx', GridLauncher.pointerInteraction.DEFAULT_MOVE_PX);
                },
                isDragDisabled: function () {
                    return isFiltering;
                },
                onDragHighlight: highlightDropTarget,
                onGridDrop: handleLauncherGridDrop,
                getAppStableId: getAppStableId,
                onAppClick: function (e, app, realIndex) {
                    openAppFromGrid(app);
                },
                onAppContextMenu: function (e, app, realIndex) {
                    if (app.isSystemApp) return;
                    prepareLauncherStripForApp(app, realIndex);
                    showStripMenu(e, stripMenu);
                },
                onFolderContextMenu: function (e, folderId, folder) {
                    prepareLauncherStripForFolder(folderId);
                    showStripMenu(e, stripMenu);
                },
                onFolderOpen: function (folderId, folder) {
                    openLauncherFolderModal(folderId);
                },
                onTileFocus: function (realIndex, el) {
                    selectedAppIndex = realIndex;
                    const visibleItems = Array.from(document.querySelectorAll('#appGrid .app-item'));
                    selectedGridVisibleIndex = visibleItems.indexOf(el);
                }
            };

            if (displayRows.length === 0) {
                const noAppsMessage = document.createElement('div');
                noAppsMessage.innerHTML = 'No hay aplicaciones en el escritorio. Haga clic en el botón <button class="inline-add-btn" onclick="document.getElementById(\'addAppFloatBtn\').click()">+</button> para agregar.';
                noAppsMessage.style.gridColumn = '1 / -1';
                noAppsMessage.style.textAlign = 'center';
                noAppsMessage.style.padding = '20px';
                noAppsMessage.style.color = savedGridTextColor;
                appGrid.appendChild(noAppsMessage);
            } else {
                let visibleIdx = 0;
                displayRows.forEach(function (row) {
                    let el;
                    if (row.kind === 'app') {
                        el = integ.createAppTile(row.app, row.indexInApps, row.orderIndex, savedGridTextColor, getTypeBadge, getAppStableId);
                    } else {
                        el = integ.createFolderTile(row.folderId, row.folder, row.orderIndex, savedGridTextColor, apps, getAppStableId);
                    }
                    const ctrl = integ.bindLauncherToTile(el, row, launcherApi);
                    if (ctrl) __launcherGridControllers.push(ctrl);
                    appGrid.appendChild(el);
                    visibleIdx++;
                });
            }

            if (!isMobile() && selectedGridVisibleIndex === -1 && appGrid.firstChild && appGrid.firstChild.tabIndex !== -1) {
                appGrid.firstChild.focus();
                selectedGridVisibleIndex = 0;
            }
        }

        const GRID_DESKTOP_RECOVER_COUNT_KEY = 'gridDesktopRecoverCount';
        const GRID_DESKTOP_RECOVER_MAX = 3;

        function gridRecoverDesktopUi() {
            let n = 0;
            try {
                n = parseInt(sessionStorage.getItem(GRID_DESKTOP_RECOVER_COUNT_KEY) || '0', 10);
                if (!Number.isFinite(n) || n < 0) n = 0;
                if (n >= GRID_DESKTOP_RECOVER_MAX) return false;
                sessionStorage.setItem(GRID_DESKTOP_RECOVER_COUNT_KEY, String(n + 1));
            } catch (_) {
                return false;
            }

            try { document.body.classList.remove('modal-open'); } catch (_) {}

            const hideModal = (el) => {
                try {
                    if (el && el.style) el.style.display = 'none';
                } catch (_) {}
            };
            [
                'addAppModal',
                'configModal',
                'addCategoryModal',
                'editCategoryIconModal',
                'previewModal',
                'explorerRenameModal',
                'explorerConflictModal',
                'explorerNewFolderChoiceModal',
                'explorerThemeModal',
                'explorerCustModal',
                'explorerToolbarCustModal',
                'recycleBinModal',
                'windowsMosaicModal'
            ].forEach((id) => hideModal(document.getElementById(id)));

            if (typeof closePreviewModal === 'function') {
                try {
                    const pm = document.getElementById('previewModal');
                    if (pm && (pm.style.display === 'block' || pm.style.display === 'flex')) closePreviewModal();
                } catch (_) {}
            }

            if (previewThemeActive && typeof applyTheme === 'function' && currentTheme) {
                try {
                    applyTheme(currentTheme);
                    const ts = document.getElementById('themeSelect');
                    if (ts) ts.value = currentTheme;
                } catch (_) {}
                previewThemeActive = false;
            }

            try { settings.desktopMode = 'grid'; } catch (_) {}

            if (window.GridLauncher && window.GridLauncher.layoutModel && settings && typeof settings === 'object') {
                try {
                    launcherLayout = GridLauncher.layoutModel.ensure(settings, getAppStableId);
                    settings.launcherLayout = launcherLayout;
                } catch (_) {}
            }

            try { renderCategories(); } catch (_) {}
            try { renderApps(); } catch (_) {}
            try { showToast('Escritorio restaurado'); } catch (_) {}
            return true;
        }

        function scheduleGridDesktopHealthCheck() {
            requestAnimationFrame(() => {
                requestAnimationFrame(() => {
                    try {
                        if (document.body.classList.contains('windows-mode')) return;
                        const grid = document.getElementById('appGrid');
                        if (!grid) return;
                        const hasTiles = grid.querySelectorAll('.app-item').length > 0;
                        const txt = (grid.innerText || grid.textContent || '');
                        const errModules = txt.indexOf('Error de módulos del escritorio') !== -1;
                        if (errModules || (!hasTiles && Array.isArray(apps) && apps.length > 0)) {
                            gridRecoverDesktopUi();
                        }
                    } catch (_) {}
                });
            });
        }

        let __gridRecoverErrTimer = null;
        function triggerGridRecoverFromGlobalError() {
            if (__gridRecoverErrTimer) return;
            __gridRecoverErrTimer = setTimeout(() => { __gridRecoverErrTimer = null; }, 12000);
            gridRecoverDesktopUi();
        }

        window.addEventListener('error', (e) => {
            try {
                const err = e && e.error;
                const msg = err && err.message ? String(err.message) : String((e && e.message) || '');
                if (/renderGrid|renderApps|renderCategories|GridLauncher|launcherLayout|layoutModel|grid-integration|layout-model|folder-service|pointer-interaction/i.test(msg)) {
                    triggerGridRecoverFromGlobalError();
                }
            } catch (_) {}
        });
        window.addEventListener('unhandledrejection', (e) => {
            try {
                const r = e && e.reason;
                const msg = r && r.message ? String(r.message) : String(r || '');
                if (/renderGrid|renderApps|renderCategories|GridLauncher|launcherLayout|layoutModel|grid-integration|layout-model|folder-service|pointer-interaction/i.test(msg)) {
                    triggerGridRecoverFromGlobalError();
                }
            } catch (_) {}
        });

        window.gridRecoverDesktopUi = gridRecoverDesktopUi;

        function renderWindowsMenu(filteredApps = getDisplayApps()) {
            let appsToShow = filteredApps.filter(app => !app.isSystemApp || isStartMenuSystemApp(app));
            if (windowsSearchQuery) {
                const q = windowsSearchQuery.toLowerCase();
                appsToShow = appsToShow.filter(app => 
                    app.title.toLowerCase().includes(q) || 
                    (app.description && app.description.toLowerCase().includes(q))
                );
            }
            
            const catSet = new Set();
            appsToShow.forEach(app => catSet.add(app.category || 'General'));
            const categoryArray = Array.from(catSet).sort();
            
            windowsCategoryList.innerHTML = '';
            windowsCategoryList.appendChild(allCategoriesOption);
            
            allCategoriesOption.onclick = () => {
                document.querySelectorAll('#windowsCategoryList li').forEach(l => l.classList.remove('active'));
                allCategoriesOption.classList.add('active');
                activeWindowsCategory = 'all';
                selectedCategoryIndex = 0;
                renderWindowsAppsByCategory('all', appsToShow);
            };
            
            categoryArray.forEach(catName => {
                const li = document.createElement('li');
                const entry = gridFindCategoryEntryByName(catName);
                const iconClass = gridSanitizeFaClass(entry.icon || 'fas fa-folder');
                const color = (entry.color && String(entry.color).trim()) ? String(entry.color).trim() : '';
                const iconSpan = document.createElement('span');
                iconSpan.className = 'os-cat-icon';
                if (color) {
                    iconSpan.style.background = color + '22';
                    iconSpan.style.color = color;
                    iconSpan.style.border = '1px solid ' + color + '44';
                }
                const ic = document.createElement('i');
                ic.setAttribute('aria-hidden', 'true');
                ic.className = iconClass;
                iconSpan.appendChild(ic);
                const lab = document.createElement('span');
                lab.className = 'os-cat-label';
                lab.textContent = catName;
                li.appendChild(iconSpan);
                li.appendChild(lab);
                li.dataset.category = catName;
                li.setAttribute('tabindex', '0');
                li.addEventListener('click', () => {
                    document.querySelectorAll('#windowsCategoryList li').forEach(l => l.classList.remove('active'));
                    li.classList.add('active');
                    activeWindowsCategory = catName;
                    selectedCategoryIndex = Array.from(windowsCategoryList.children).indexOf(li);
                    renderWindowsAppsByCategory(catName, appsToShow);
                });
                li.addEventListener('contextmenu', (e) => {
                    e.preventDefault();
                    e.stopPropagation();
                    const appToPin = apps.find(a => a.category === catName);
                    if (appToPin && !appToPin.isSystemApp) {
                        const isPinned = pinnedApps.some(p => pinMatchesApp(p, appToPin));
                        const options = [
                            { className: 'pin-btn', icon: `${isPinned ? 'fas fa-star' : 'far fa-star'}`, label: isPinned ? tr('unpinTaskbar') : tr('pinTaskbar'), action: () => {
                                if (isPinned) {
                                    pinnedApps = pinnedApps.filter(p => !pinMatchesApp(p, appToPin));
                                    showToast(`${appToPin.title} desanclado de la barra de tareas`);
                                } else {
                                    pinnedApps.push({ title: appToPin.title, link: getAppLinkKey(appToPin), icon: appToPin.icon });
                                    showToast(`${appToPin.title} anclado a la barra de tareas`);
                                }
                                saveSettings({ pinnedApps: pinnedApps });
                                renderPinnedApps();
                            }},
                        ];
                        showContextMenu(options, e);
                    }
                });
                windowsCategoryList.appendChild(li);
            });
            
            let targetCat = activeWindowsCategory;
            if (targetCat === 'all' || !categoryArray.includes(targetCat)) {
                targetCat = 'all';
            }
            if (targetCat === 'all') {
                allCategoriesOption.classList.add('active');
                selectedCategoryIndex = 0;
                renderWindowsAppsByCategory('all', appsToShow);
            } else {
                const targetLi = Array.from(windowsCategoryList.children).find(li => li.dataset.category === targetCat);
                if (targetLi) {
                    targetLi.classList.add('active');
                    selectedCategoryIndex = Array.from(windowsCategoryList.children).indexOf(targetLi);
                    renderWindowsAppsByCategory(targetCat, appsToShow);
                } else {
                    allCategoriesOption.classList.add('active');
                    selectedCategoryIndex = 0;
                    renderWindowsAppsByCategory('all', appsToShow);
                }
            }
            
        }

        function renderWindowsAppsByCategory(category, filteredApps) {
            let appsInCat;
            if (category === 'all') {
                appsInCat = filteredApps;
            } else {
                appsInCat = filteredApps.filter(app => (app.category || 'General') === category);
            }
            windowsAppsContainer.innerHTML = '';
            const savedGridTextColor = getSetting('gridTextColor', '#000000');
            
            appsInCat.forEach((app, idx) => {
                const realIndex = apps.indexOf(app);
                const appItem = document.createElement('div');
                appItem.className = 'app-item os-shell__app-tile';
                appItem.dataset.index = realIndex;
                appItem.tabIndex = 0;
                
                let tooltipText = app.title;
                if (app.description && app.description.trim() !== '') {
                    tooltipText += `\n${app.description}`;
                }
                appItem.setAttribute('data-tooltip', tooltipText);
                
                appItem.innerHTML = `
                    <img src="${app.icon}" alt="${app.title}" class="app-icon">
                    <p style="color: ${savedGridTextColor};">${app.title}</p>
                    ${getTypeBadge(app)}
                `;
                
                const handleClick = (e) => {
                    if (!previewActive) {
                        e.preventDefault();
                        launchDesktopApp(app);
                    }
                };
                appItem.addEventListener('click', handleClick);
                
                appItem.addEventListener('contextmenu', (e) => {
                    e.preventDefault();
                    e.stopPropagation();
                    const isPinned = pinnedApps.some(p => pinMatchesApp(p, app));
                    const options = [];
                    if (!app.isSystemApp) {
                        options.push({ className: 'edit-app-win', icon: 'fas fa-edit', label: tr('editApp'), action: () => {
                            openModal(app, realIndex);
                        }});
                    }
                    options.push(
                        { className: 'pin-btn', icon: `${isPinned ? 'fas fa-star' : 'far fa-star'}`, label: isPinned ? tr('unpinTaskbar') : tr('pinTaskbar'), action: () => {
                            if (isPinned) {
                                pinnedApps = pinnedApps.filter(p => !pinMatchesApp(p, app));
                                showToast(`${app.title} desanclado de la barra de tareas`);
                            } else {
                                pinnedApps.push({ title: app.title, link: getAppLinkKey(app), icon: app.icon });
                                showToast(`${app.title} anclado a la barra de tareas`);
                            }
                            saveSettings({ pinnedApps: pinnedApps });
                            renderPinnedApps();
                        }},
                        { className: 'toggle-grid-btn', icon: `fas ${appIsVisibleOnWindowsLauncherGrid(app) ? 'fa-eye-slash' : 'fa-eye'}`, label: appIsVisibleOnWindowsLauncherGrid(app) ? tr('removeDesktop') : tr('addDesktop'), action: () => {
                            const onGrid = appIsVisibleOnWindowsLauncherGrid(app);
                            if (onGrid) {
                                app.showInGrid = false;
                                saveSettings({ apps: apps }).then(() => {
                                    renderApps();
                                    if (windowsMenuVisible) renderWindowsMenu(apps);
                                    showToast(tr('desktopToastRemoved'));
                                });
                            } else {
                                app.showInGrid = true;
                                const savePayload = { apps: apps };
                                if (window.GridLauncher && window.GridLauncher.layoutModel && launcherLayout) {
                                    launcherLayout = GridLauncher.layoutModel.appendAppToOrder(launcherLayout, getAppStableId(app), getAppStableId);
                                    savePayload.launcherLayout = launcherLayout;
                                }
                                saveSettings(savePayload).then(() => {
                                    renderApps();
                                    if (windowsMenuVisible) renderWindowsMenu(apps);
                                    showToast(tr('desktopToastAdded'));
                                });
                            }
                        }},
                        { className: 'open-btn', icon: 'fas fa-external-link-alt', label: tr('open'), action: () => { launchDesktopApp(app); }}
                    );
                    if (!app.isSystemApp) {
                        options.push({ className: 'delete-app-win', icon: 'fas fa-trash-alt', label: tr('uninstallApp'), action: () => {
                            const idx = apps.indexOf(app);
                            if (idx > -1) moveToTrash(idx);
                        }});
                    }
                    showContextMenu(options, e);
                });
                
                appItem.addEventListener('focus', () => {
                    selectedWindowsAppIndex = idx;
                });

                windowsAppsContainer.appendChild(appItem);
            });
            
            selectedWindowsAppIndex = -1;
        }

        windowsSearch.addEventListener('input', function() {
            windowsSearchQuery = this.value;
            updateWindowsSearchClearVisibility();
            if (windowsMenuVisible) {
                renderWindowsMenu(apps);
            }
        });
        if (windowsSearchClear) {
            windowsSearchClear.addEventListener('click', () => {
                windowsSearch.value = '';
                windowsSearchQuery = '';
                updateWindowsSearchClearVisibility();
                if (windowsMenuVisible) renderWindowsMenu(apps);
                windowsSearch.focus();
            });
        }

        function isWindowsMenuOpenInDom() {
            if (!windowsMenu) return false;
            const inline = (windowsMenu.style.display || '').trim().toLowerCase();
            if (inline === 'none') return false;
            if (inline === 'flex' || inline === 'block') return true;
            try {
                return window.getComputedStyle(windowsMenu).display !== 'none';
            } catch (_) {
                return windowsMenuVisible;
            }
        }

        function toggleWindowsMenu() {
            const open = isWindowsMenuOpenInDom();
            if (!open) {
                windowsMenu.style.display = 'flex';
                windowsMenuBackdrop.style.display = 'block';
                renderWindowsMenu(apps);
                updateWindowsSearchClearVisibility();
                windowsMenuVisible = true;
                try {
                    if (window.OsShell && typeof window.OsShell.syncStartMenuLayout === 'function') {
                        requestAnimationFrame(function () {
                            requestAnimationFrame(window.OsShell.syncStartMenuLayout);
                        });
                    }
                } catch (_) {}
                document.querySelectorAll('.app-item').forEach(el => el.setAttribute('tabindex', '-1'));
                setTimeout(() => {
                    const firstCat = windowsCategoryList.querySelector('li');
                    if (firstCat) firstCat.focus();
                }, 100);
            } else {
                windowsMenu.style.display = 'none';
                windowsMenuBackdrop.style.display = 'none';
                windowsMenuVisible = false;
                document.querySelectorAll('.app-item').forEach(el => el.setAttribute('tabindex', '0'));
            }
        }

        if (windowsMenuClose) {
            windowsMenuClose.addEventListener('click', (e) => {
                e.stopPropagation();
                toggleWindowsMenu();
            });
        }
        if (windowsMenuBackdrop) {
            windowsMenuBackdrop.addEventListener('click', (e) => {
                e.stopPropagation();
                toggleWindowsMenu();
            });
        }
        if (startButton) {
            startButton.addEventListener('click', (e) => {
                e.stopPropagation();
                toggleWindowsMenu();
            });
        }

        function gridAuthLogoutFlow() {
            const G = typeof window.GRID_AUTH === 'object' && window.GRID_AUTH ? window.GRID_AUTH : null;
            const loginUrl = (G && G.loginUrl) ? G.loginUrl : (typeof BASE_URL === 'string' ? BASE_URL.replace(/\/?$/, '/') + 'login/' : '/login/');
            const apiUrl = (G && G.apiUrl) ? G.apiUrl : (typeof BASE_URL === 'string' ? BASE_URL.replace(/\/?$/, '/') + 'core/auth/api.php' : '/core/auth/api.php');
            const go = function () {
                if (window.AntaraAuthTabSync && typeof window.AntaraAuthTabSync.notifyLogout === 'function') {
                    window.AntaraAuthTabSync.notifyLogout(loginUrl);
                }
                window.location.href = loginUrl + (loginUrl.indexOf('?') >= 0 ? '&' : '?') + 'reason=logout';
            };
            fetch(apiUrl + (apiUrl.indexOf('?') >= 0 ? '&' : '?') + 'action=authLogout', {
                method: 'POST',
                credentials: 'same-origin',
                headers: { 'Content-Type': 'application/json', 'Accept': 'application/json' },
                body: '{}',
            }).then(go).catch(go);
        }
        const windowsMenuLogoutBtn = document.getElementById('windowsMenuLogoutBtn');
        if (windowsMenuLogoutBtn) windowsMenuLogoutBtn.addEventListener('click', gridAuthLogoutFlow);
        const mobileAppHubLogoutBtn = document.getElementById('mobileAppHubLogoutBtn');
        if (mobileAppHubLogoutBtn) mobileAppHubLogoutBtn.addEventListener('click', gridAuthLogoutFlow);
        const desktopStripLogoutBtn = document.getElementById('desktopStripLogoutBtn');
        if (desktopStripLogoutBtn) desktopStripLogoutBtn.addEventListener('click', gridAuthLogoutFlow);

        document.querySelectorAll('a.js-admin-users-window').forEach((adminLink) => {
            adminLink.addEventListener('click', (e) => {
                const url = adminLink.getAttribute('href');
                if (!url || url === '#' || url.toLowerCase().startsWith('javascript:')) return;
                if (e.defaultPrevented) return;
                if (e.ctrlKey || e.metaKey || e.shiftKey || e.altKey) return;
                if (e.button !== 0) return;
                e.preventDefault();
                hideStripMenu();
                if (windowsMenuVisible) toggleWindowsMenu();
                if (mobileAppHubBackdrop && mobileAppHubBackdrop.classList.contains('visible')) closeMobileAppHub();
                openBuiltInAdminUsersWindow(url);
            });
        });

        function isTextEditingTarget(target) {
            if (!target || !(target instanceof Element)) return false;
            if (target.isContentEditable) return true;
            const editable = target.closest('input, textarea, select, [contenteditable="true"], [contenteditable=""], [role="textbox"]');
            if (!editable) return false;
            if (!(editable instanceof HTMLInputElement)) return true;
            const t = (editable.type || '').toLowerCase();
            const nonTextInputTypes = new Set(['button', 'checkbox', 'color', 'file', 'hidden', 'image', 'radio', 'range', 'reset', 'submit']);
            return !nonTextInputTypes.has(t);
        }

        document.addEventListener('keydown', function(e) {
            if (isExplorerFocusLockActive() && !isTargetInsideExplorerScope(e.target) && !isExplorerKeyboardLockExemptTarget(e.target)) {
                e.preventDefault();
                e.stopPropagation();
                focusExplorerPrimaryElement();
                return;
            }
            if (e.key === 'Control' && !isMobile() && getSetting('desktopMode', 'grid') === 'windows' && !isTextEditingTarget(e.target)) {
                if (isExplorerFocusLockActive()) {
                    e.preventDefault();
                    e.stopPropagation();
                    return;
                }
                e.preventDefault();
                toggleWindowsMenu();
            }
        });

        recycleBin.addEventListener('click', () => {
            openRecycleBin();
        });
        recycleBin.addEventListener('contextmenu', async (e) => {
            e.preventDefault();
            if (await appDialog.confirm(tr('recycleEmptyConfirm'))) {
                emptyTrash();
            }
        });

        // ========== MOSAICO DE VENTANAS (reemplazo tipo Windows) ==========
        function isFrameMinimized(frame) {
            return !frame || frame.classList.contains('minimized') || frame.style.display === 'none';
        }

        function isFrameMaximized(frame) {
            return !!(frame && frame.classList.contains('maximized'));
        }

        function setFrameToFront(frame) {
            if (!frame) return;
            frame.style.zIndex = ++windowZIndex;
        }

        function selectWindowFromMosaic(windowId) {
            if (Date.now() < mosaicSwipeSuppressClickUntil) return;
            const winEntry = openWindows.find(w => w.id === windowId);
            if (!winEntry || !winEntry.frame) return;

            const frame = winEntry.frame;
            const appGroup = activeTaskbarApps.find(a => a.appId === getAppStableId(winEntry.app));
            const windowObj = appGroup ? appGroup.windows.find(w => w.id === windowId) : null;
            if (appGroup && windowObj) appGroup.activeWindowId = windowId;

            // Mantener el estado de las demás ventanas: solo modificar la seleccionada.
            const wasMinimized = isFrameMinimized(frame);
            if (wasMinimized && appGroup && windowObj) {
                windowObj.isMinimized = false;
            }
            if (wasMinimized) {
                frame.classList.remove('minimized');
                frame.style.display = 'flex';
            }

            setFrameToFront(frame);

            // Maximizar después de restaurar (si aplica) y sin alternar a restaurar.
            const needsMax = !isFrameMaximized(frame);
            if (needsMax) {
                const maximizeBtn = frame.querySelector('.maximize-btn');
                if (maximizeBtn) maximizeBtn.click();
            }

            renderTaskbarApps();
            renderPinnedApps();

            windowsMosaicModal.style.display = 'none';
            document.body.classList.remove('modal-open');
            closeMobileAppHub();
        }

        function buildMosaicThumbInnerHtml(thumb, app) {
            if (thumb && thumb.type === 'iframe') {
                return `
                        <div class="thumb-iframe-wrap">
                            <iframe class="preview-thumb-iframe" src="${thumb.src}" sandbox="allow-scripts allow-popups allow-forms allow-modals" loading="lazy"></iframe>
                        </div>
                    `;
            }
            if (thumb && thumb.type === 'video') {
                return `<video src="${thumb.src}" ${thumb.poster ? `poster="${thumb.poster}"` : ''} muted autoplay loop playsinline preload="metadata"></video>`;
            }
            if (thumb && thumb.type === 'img') {
                return `<img src="${thumb.src}" alt="">`;
            }
            return `<img src="${app?.icon || ''}" alt="">`;
        }

        function renderWindowsMosaic() {
            windowsMosaicGrid.innerHTML = '';
            const list = openWindows
                .filter(w => w && w.frame && document.body.contains(w.frame))
                .sort((a, b) => (b.frame.style.zIndex || 0) - (a.frame.style.zIndex || 0));

            if (list.length === 0) {
                windowsMosaicGrid.innerHTML = '<p style="margin:0;">No hay ventanas abiertas.</p>';
                return;
            }

            list.forEach(winEntry => {
                const app = winEntry.app;
                const frame = winEntry.frame;
                const appGroup = activeTaskbarApps.find(a => a.appId === getAppStableId(app));
                const windowObj = appGroup ? appGroup.windows.find(w => w.id === winEntry.id) : null;

                const minimized = isFrameMinimized(frame);
                const maximized = isFrameMaximized(frame);
                const thumb = windowObj ? getWindowPreviewThumb(windowObj, appGroup) : null;
                const thumbHtml = buildMosaicThumbInnerHtml(thumb, app);

                const tile = document.createElement('div');
                tile.className = 'windows-mosaic-tile';
                tile.dataset.windowId = winEntry.id;
                tile.setAttribute('role', 'button');
                tile.setAttribute('tabindex', '0');
                tile.innerHTML = `
                    <button type="button" class="windows-mosaic-close" title="${tr('closeWindow')}" aria-label="${tr('closeWindow')}"><i class="fas fa-times"></i></button>
                    <div class="windows-mosaic-thumb">${thumbHtml}</div>
                    <div class="windows-mosaic-meta">
                        <div>
                            <div class="windows-mosaic-title">${appGroup?.title || app?.title || tr('windowDefaultTitle')}</div>
                            <div class="windows-mosaic-sub">${minimized ? tr('windowMinimized') : (maximized ? tr('windowActive') : '')}</div>
                        </div>
                        <i class="fas fa-chevron-right" style="opacity:0.55;"></i>
                    </div>
                `;
                const closeBtn = tile.querySelector('.windows-mosaic-close');
                if (closeBtn) {
                    closeBtn.addEventListener('click', (ev) => {
                        ev.stopPropagation();
                        ev.preventDefault();
                        closeWindowFromMosaic(winEntry.id);
                    });
                }
                tile.addEventListener('click', () => {
                    if (Date.now() < mosaicSwipeSuppressClickUntil) return;
                    selectWindowFromMosaic(winEntry.id);
                });
                tile.addEventListener('keydown', (e) => {
                    if (e.key === 'Enter') selectWindowFromMosaic(winEntry.id);
                });
                windowsMosaicGrid.appendChild(tile);
            });

            // Swipe para cerrar en el mosaico:
            // - Celular: izquierda/derecha
            // - Tablet/PC: hacia arriba
            bindWindowsMosaicSwipeToClose();
        }

        function openWindowsMosaic() {
            if (!windowsMosaicModal) return;
            renderWindowsMosaic();
            windowsMosaicModal.style.display = 'block';
            document.body.classList.add('modal-open');
        }

        async function closeAllWindows() {
            if (openWindows.length === 0) return;
            if (!(await appDialog.confirm(tr('windowsMosaicCloseAllConfirm')))) return;

            // Copia para no romper el array mientras removemos.
            const toClose = [...openWindows];
            toClose.forEach(w => {
                try {
                    if (w.frame) w.frame.remove();
                    if (w.app) removeTaskbarApp(w.id, w.app);
                } catch (_) {}
            });
            openWindows = [];
            activeTaskbarApps = activeTaskbarApps.filter(a => a.windows && a.windows.length > 0);
            renderTaskbarApps();
            renderPinnedApps();
            schedulePersistOpenWindowsSession();
            if (windowsMosaicModal) {
                windowsMosaicModal.style.display = 'none';
                document.body.classList.remove('modal-open');
            }
        }

        if (windowsMosaicBtn) {
            windowsMosaicBtn.addEventListener('click', () => openWindowsMosaic());
            windowsMosaicBtn.addEventListener('keydown', (e) => {
                if (e.key === 'Enter') openWindowsMosaic();
            });
        }
        if (mobileAppHubBtn) {
            mobileAppHubBtn.addEventListener('click', () => toggleMobileAppHub());
            mobileAppHubBtn.addEventListener('keydown', (e) => {
                if (e.key === 'Enter' || e.key === ' ') {
                    e.preventDefault();
                    toggleMobileAppHub();
                }
            });
        }
        if (mobileAppHubCloseBtn) {
            mobileAppHubCloseBtn.addEventListener('click', () => closeMobileAppHub());
        }
        if (mobileAppHubBackdrop) {
            mobileAppHubBackdrop.addEventListener('click', (e) => {
                if (e.target === mobileAppHubBackdrop) closeMobileAppHub();
            });
        }
        if (closeAllWindowsBtn) {
            closeAllWindowsBtn.addEventListener('click', closeAllWindows);
        }

        // ========== EXPLORADOR DE ARCHIVOS TIPO WINDOWS (local) ==========
        function formatBytes(bytes) {
            if (!bytes && bytes !== 0) return '';
            const u = ['B', 'KB', 'MB', 'GB', 'TB'];
            let i = 0;
            let b = Number(bytes);
            while (b >= 1024 && i < u.length - 1) { b /= 1024; i++; }
            const val = (i === 0) ? String(Math.round(b)) : (Math.round(b * 10) / 10).toFixed(1);
            return `${val} ${u[i]}`;
        }

        function buildExplorerRemotePreviewUrl(connId, remoteRelPath, name, sizeHint) {
            try {
                const u = new URL(window.location.href);
                const params = new URLSearchParams();
                params.set('action', 'explorerConnPreview');
                params.set('connId', connId);
                params.set('path', remoteRelPath || '');
                params.set('name', name);
                if (sizeHint && sizeHint > 0) params.set('size', String(sizeHint));
                u.search = '';
                return u.pathname + '?' + params.toString();
            } catch (_) {
                return '';
            }
        }

        function getFileThumbHtml(item, thumbCtx) {
            thumbCtx = thumbCtx || {};
            const wrapCls = (base) => {
                const extra = thumbCtx.thumbWrapClass ? (' ' + thumbCtx.thumbWrapClass) : '';
                return base + extra;
            };
            if (thumbCtx.isRemote && thumbCtx.connId && item && item.type !== 'dir') {
                const mime = item.mimeType || '';
                const maxPrev = 15 * 1024 * 1024;
                if (!(item.size && item.size > maxPrev) && (mime.startsWith('image/') || mime.startsWith('video/'))) {
                    const prevUrl = buildExplorerRemotePreviewUrl(thumbCtx.connId, thumbCtx.remotePath, item.name, item.size);
                    if (prevUrl) {
                        if (mime.startsWith('image/')) {
                            return `<div class="${wrapCls('explorer-item-thumb explorer-item-thumb-remote')}"><img src="${escapeHtmlAttr(prevUrl)}" alt="" loading="lazy" draggable="false"></div>`;
                        }
                        return `<div class="${wrapCls('explorer-item-thumb explorer-item-thumb-remote')}"><video src="${escapeHtmlAttr(prevUrl)}" muted playsinline preload="metadata" draggable="false"></video></div>`;
                    }
                }
            }
            if (!item) return '';
            if (item.type === 'dir') {
                if (thumbCtx.ecBucket && window.GridExplorerCustomization) {
                    const g = window.GridExplorerCustomization;
                    const key = g.folderRelKey(thumbCtx.ecBucket, thumbCtx.ecParentRel || '', item.name, item);
                    const st = g.resolveFolderStyle(thumbCtx.ecBucket, key);
                    return g.folderThumbHtml(st, wrapCls('explorer-item-thumb'), false);
                }
                return `<div class="${wrapCls('explorer-item-thumb')}"><i class="fas fa-folder fe-thumb-folder-ico"></i></div>`;
            }
            const mime = item.mimeType || '';
            const secureUrl = item.url ? buildSecureFileUrl(item.url) : '';
            if (mime.startsWith('image/')) {
                if (secureUrl) {
                    return `<div class="${wrapCls('explorer-item-thumb')}"><img src="${secureUrl}" alt="" loading="lazy" draggable="false"><\/div>`;
                }
                return `<div class="${wrapCls('explorer-item-thumb')}"><i class="fas fa-image fe-thumb-fallback-ico"></i></div>`;
            }
            if (mime.startsWith('video/')) {
                if (secureUrl) {
                    return `<div class="${wrapCls('explorer-item-thumb')}"><video src="${secureUrl}" muted playsinline preload="metadata" draggable="false"><\/video><\/div>`;
                }
                return `<div class="${wrapCls('explorer-item-thumb')}"><i class="fas fa-video fe-thumb-fallback-ico"></i></div>`;
            }
            const lowerName = (item.name || '').toLowerCase();
            if (mime.startsWith('audio/') || lowerName.match(/\.(mp3|wav|ogg|m4a|aac|flac)$/i)) {
                return `<div class="${wrapCls('explorer-item-thumb')}"><i class="fas fa-file-audio fe-thumb-fallback-ico"></i></div>`;
            }
            if (mime === 'application/pdf' || (item.name || '').toLowerCase().endsWith('.pdf')) {
                return `<div class="${wrapCls('explorer-item-thumb')}"><i class="fas fa-file-pdf fe-thumb-fallback-ico"></i></div>`;
            }
            if (mime.includes('word') || (item.name || '').toLowerCase().match(/\.(doc|docx)$/i)) {
                return `<div class="${wrapCls('explorer-item-thumb')}"><i class="fas fa-file-word fe-thumb-fallback-ico"></i></div>`;
            }
            if (mime.includes('excel') || (item.name || '').toLowerCase().match(/\.(xls|xlsx|csv)$/i)) {
                return `<div class="${wrapCls('explorer-item-thumb')}"><i class="fas fa-file-excel fe-thumb-fallback-ico"></i></div>`;
            }
            if (mime.includes('presentation') || (item.name || '').toLowerCase().match(/\.(ppt|pptx)$/i)) {
                return `<div class="${wrapCls('explorer-item-thumb')}"><i class="fas fa-file-powerpoint fe-thumb-fallback-ico"></i></div>`;
            }
            return `<div class="${wrapCls('explorer-item-thumb')}"><i class="fas fa-file-alt fe-thumb-fallback-ico"></i></div>`;
        }

        function buildExplorerMultiDragPreview(names, displayItems) {
            const maxCells = 4;
            const preview = document.createElement('div');
            preview.className = 'explorer-drag-preview';

            const grid = document.createElement('div');
            grid.className = 'explorer-drag-preview-grid';

            const map = new Map();
            if (Array.isArray(displayItems)) {
                displayItems.forEach((it) => {
                    if (it && it.name) map.set(it.name, it);
                });
            }

            const picked = [];
            for (const n of (names || [])) {
                if (!n) continue;
                if (picked.length >= maxCells) break;
                picked.push(n);
            }

            picked.forEach((n) => {
                const cell = document.createElement('div');
                cell.className = 'explorer-drag-preview-cell';
                const it = map.get(n);
                cell.innerHTML = getFileThumbHtml(it || { name: n, type: 'file', mimeType: 'application/octet-stream' });
                grid.appendChild(cell);
            });

            while (grid.children.length < Math.min(maxCells, Math.max(1, (names || []).length))) {
                const cell = document.createElement('div');
                cell.className = 'explorer-drag-preview-cell';
                cell.innerHTML = `<div class="explorer-item-thumb"><i class="fas fa-file-alt" style="font-size:26px; opacity:0.75; color: rgba(255,255,255,0.92);"></i></div>`;
                grid.appendChild(cell);
            }

            const count = document.createElement('div');
            count.className = 'explorer-drag-preview-count';
            count.textContent = String((names || []).length || 0);

            const label = document.createElement('div');
            label.className = 'explorer-drag-preview-label';
            label.textContent = ((names || []).length === 2)
                ? `${names[0]} + 1`
                : `${(names || []).length} elementos`;

            preview.appendChild(grid);
            preview.appendChild(count);
            preview.appendChild(label);
            return preview;
        }

        function explorerPseudoAppForFile(item, parentRelPath) {
            const mime = item.mimeType || '';
            const app = {
                title: item.name,
                icon: FILE_EXPLORER_APP.icon,
                lastAction: 'file',
                fileInfo: {
                    url: item.url,
                    type: mime,
                    name: item.name
                }
            };
            if (parentRelPath !== undefined && parentRelPath !== null) {
                app.galleryContext = { parentPath: parentRelPath, name: item.name };
            }
            return app;
        }

        function openExplorerFile(item, parentRelPath) {
            if (!item || item.type === 'dir') return;
            const pseudoApp = explorerPseudoAppForFile(item, parentRelPath);
            openFileWindowFrame(pseudoApp);
        }

        async function fetchExplorerList(bucket, path, departmentId) {
            const body = { bucket: bucket, path: path || '' };
            if (window.GridExplorerSharedSpaces && typeof window.GridExplorerSharedSpaces.withDepartmentId === 'function') {
                Object.assign(body, window.GridExplorerSharedSpaces.withDepartmentId({}, bucket, departmentId || ''));
            } else if (bucket === 'department' && departmentId) {
                body.departmentId = departmentId;
            }
            const res = await fetch('?action=explorerList', {
                method: 'POST',
                headers: { 'Content-Type': 'application/json' },
                body: JSON.stringify(body)
            });
            return await res.json();
        }

        async function fetchExplorerStat(bucket, parentPath, name, type) {
            const res = await fetch('?action=explorerStat', {
                method: 'POST',
                headers: { 'Content-Type': 'application/json' },
                body: JSON.stringify({ bucket: bucket || 'files', path: parentPath || '', name: name || '', type: type || 'file' })
            });
            try {
                return await res.json();
            } catch (_) {
                return { success: false };
            }
        }

        async function explorerShareCreate(bucket, path, name, type) {
            const res = await fetch('?action=shareCreate', {
                method: 'POST',
                headers: { 'Content-Type': 'application/json' },
                body: JSON.stringify({
                    bucket: bucket || 'files',
                    path: path || '',
                    name: name || '',
                    type: type === 'dir' ? 'dir' : 'file'
                })
            });
            return await res.json();
        }

        async function fetchExplorerConnList(connId, path) {
            const res = await fetch('?action=explorerConnList', {
                method: 'POST',
                headers: { 'Content-Type': 'application/json' },
                body: JSON.stringify({ connId, path: path || '' })
            });
            return await res.json();
        }

        async function explorerWaitForRemoteItemsVisible(connId, path, names, timeoutMs = 9000) {
            const wanted = Array.isArray(names) ? names.map((x) => String(x || '')).filter(Boolean) : [];
            if (!connId || wanted.length === 0) return true;
            const started = Date.now();
            let delay = 250;
            while (Date.now() - started < timeoutMs) {
                let data = null;
                try { data = await fetchExplorerConnList(connId, path || ''); } catch (_) { data = null; }
                const list = (data && data.success && Array.isArray(data.items)) ? data.items : [];
                const set = new Set(list.map((it) => String(it?.name || '')).filter(Boolean));
                const allThere = wanted.every((n) => set.has(n));
                if (allThere) return true;
                await new Promise((r) => setTimeout(r, delay));
                delay = Math.min(1200, Math.round(delay * 1.35));
            }
            return false;
        }

        async function explorerSearchGlobal(q, searchBucket = 'files') {
            const res = await fetch('?action=explorerSearch', {
                method: 'POST',
                headers: { 'Content-Type': 'application/json' },
                body: JSON.stringify({ q: (q || '').trim(), bucket: searchBucket === 'cache' ? 'cache' : 'files' })
            });
            return await res.json();
        }

        async function explorerConnFetch(connId, path, name) {
            const res = await fetch('?action=explorerConnFetch', {
                method: 'POST',
                headers: { 'Content-Type': 'application/json' },
                body: JSON.stringify({ connId, path: path || '', name })
            });
            return await res.json();
        }

        async function explorerImportFromRemote(connId, fromPath, name, toPath, opts = {}) {
            const res = await fetch('?action=explorerImportFromRemote', {
                method: 'POST',
                headers: { 'Content-Type': 'application/json' },
                body: JSON.stringify({
                    connId,
                    fromPath: fromPath || '',
                    name,
                    toPath: toPath || '',
                    autoRename: !!opts.autoRename,
                    overwrite: !!opts.overwrite,
                    destName: opts.destName || undefined,
                    toBucket: opts.toBucket === 'cache' ? 'cache' : 'files'
                })
            });
            return await res.json();
        }

        async function explorerExportToRemote(connId, toPath, fromPath, name, opts = {}) {
            const res = await fetch('?action=explorerExportToRemote', {
                method: 'POST',
                headers: { 'Content-Type': 'application/json' },
                body: JSON.stringify({
                    connId,
                    toPath: toPath || '',
                    fromPath: fromPath || '',
                    name,
                    autoRename: !!opts.autoRename,
                    remoteName: opts.remoteName
                })
            });
            return await res.json();
        }

        async function fetchExplorerListTrash() {
            const res = await fetch('?action=explorerListTrash', { method: 'POST', cache: 'no-store' });
            const data = await res.json();
            if (data && data.success) {
                fileExplorerTrash = data.items || [];
                if (typeof settings === 'object' && settings) settings.fileExplorerTrash = fileExplorerTrash;
            }
            return data;
        }

        async function fetchExplorerTrashListDir(id, name, relPath) {
            const res = await fetch('?action=explorerTrashListDir', {
                method: 'POST',
                headers: { 'Content-Type': 'application/json' },
                body: JSON.stringify({ id: id || '', name: name || '', path: relPath || '' })
            });
            return await res.json();
        }

        async function explorerRestoreTrashPath(id, rootName, relPath, name, opts = {}) {
            const res = await fetch('?action=explorerTrashRestorePath', {
                method: 'POST',
                headers: { 'Content-Type': 'application/json' },
                body: JSON.stringify({
                    id: id || '',
                    rootName: rootName || '',
                    path: relPath || '',
                    name: name || '',
                    overwrite: !!opts.overwrite,
                    merge: !!opts.merge,
                    newName: opts.newName ? String(opts.newName) : ''
                })
            });
            return await res.json();
        }

        function explorerApiJsonBody(base) {
            const body = Object.assign({}, base || {});
            const ctx = window.__gridExplorerActiveContext || {};
            const bucket = body.bucket || ctx.bucket || 'files';
            body.bucket = bucket;
            if (window.GridExplorerSharedSpaces && typeof window.GridExplorerSharedSpaces.withDepartmentId === 'function') {
                return window.GridExplorerSharedSpaces.withDepartmentId(body, bucket, body.departmentId || ctx.departmentId || '');
            }
            if (bucket === 'department' && (body.departmentId || ctx.departmentId)) {
                body.departmentId = body.departmentId || ctx.departmentId;
            }
            return body;
        }

        async function explorerCreateDir(path, name, bucket) {
            const res = await fetch('?action=explorerCreateDir', {
                method: 'POST',
                headers: { 'Content-Type': 'application/json' },
                body: JSON.stringify(explorerApiJsonBody({ path: path || '', name: name, bucket: bucket || 'files' }))
            });
            return await res.json();
        }

        async function explorerRename(path, oldName, newName, bucket = 'files') {
            const res = await fetch('?action=explorerRename', {
                method: 'POST',
                headers: { 'Content-Type': 'application/json' },
                body: JSON.stringify(explorerApiJsonBody({ path: path || '', oldName: oldName, newName: newName, bucket: bucket || 'files' }))
            });
            return await res.json();
        }

        function splitExplorerFileNameForRename(fullName, isDirectory) {
            if (isDirectory || !fullName) return { base: fullName || '', ext: '' };
            const lastDot = fullName.lastIndexOf('.');
            if (lastDot <= 0) return { base: fullName, ext: '' };
            return { base: fullName.slice(0, lastDot), ext: fullName.slice(lastDot + 1) };
        }

        function joinExplorerRenamedFile(base, extRaw) {
            const b = (base || '').trim();
            let e = (extRaw || '').trim();
            if (e.startsWith('.')) e = e.slice(1);
            if (!b && !e) return '';
            if (!e) return b;
            return b + '.' + e;
        }

        function isValidExplorerItemName(name) {
            if (!name || name === '.' || name === '..') return false;
            return !/[\\/]/.test(name);
        }

        function showExplorerRenameModal(initialName, options) {
            const isDir = !!(options && options.isDirectory);
            return new Promise((resolve) => {
                const modal = document.getElementById('explorerRenameModal');
                if (!modal) {
                    appDialog.prompt(tr('explorerPromptNewName'), initialName || '').then((fb) => {
                        resolve(fb && String(fb).trim() && String(fb).trim() !== initialName ? String(fb).trim() : null);
                    });
                    return;
                }
                const titleEl = document.getElementById('explorerRenameModalTitle');
                const hintEl = document.getElementById('explorerRenameModalHint');
                const iconEl = document.getElementById('explorerRenameModalIcon');
                const folderBlock = document.getElementById('explorerRenameFolderBlock');
                const fileBlock = document.getElementById('explorerRenameFileBlock');
                const folderInput = document.getElementById('explorerRenameFolderInput');
                const baseInput = document.getElementById('explorerRenameBase');
                const extInput = document.getElementById('explorerRenameExt');
                const baseLabel = document.getElementById('explorerRenameBaseLabel');
                const folderLabel = document.getElementById('explorerRenameFolderLabel');
                const extHelp = document.getElementById('explorerRenameExtHelp');
                const previewLabel = document.getElementById('explorerRenamePreviewLabel');
                const previewVal = document.getElementById('explorerRenamePreviewValue');
                const btnSave = document.getElementById('explorerRenameModalSave');
                const btnCancel = document.getElementById('explorerRenameModalCancel');
                const btnCloseX = document.getElementById('explorerRenameModalCloseX');

                const syncLabels = () => {
                    if (titleEl) titleEl.textContent = tr('explorerRenameModalTitle');
                    if (hintEl) hintEl.textContent = isDir ? tr('explorerRenameHintFolder') : tr('explorerRenameHintFile');
                    if (baseLabel) baseLabel.textContent = tr('explorerRenameLabelBase');
                    if (folderLabel) folderLabel.textContent = tr('explorerRenameLabelFolder');
                    if (extHelp) extHelp.textContent = tr('explorerRenameExtHelp');
                    if (previewLabel) previewLabel.textContent = tr('explorerRenamePreviewLabel');
                    if (btnSave) btnSave.textContent = tr('explorerRenameSave');
                    if (btnCancel) btnCancel.textContent = tr('explorerRenameCancel');
                    if (btnCloseX) btnCloseX.setAttribute('aria-label', tr('explorerRenameCancel'));
                };
                syncLabels();

                if (iconEl) iconEl.className = 'fas ' + (isDir ? 'fa-folder' : 'fa-file-alt');
                if (folderBlock && fileBlock) {
                    folderBlock.style.display = isDir ? 'block' : 'none';
                    fileBlock.style.display = isDir ? 'none' : 'block';
                }

                const parts = splitExplorerFileNameForRename(initialName || '', isDir);
                if (isDir) {
                    if (folderInput) folderInput.value = parts.base;
                } else {
                    if (baseInput) baseInput.value = parts.base;
                    if (extInput) extInput.value = parts.ext;
                }

                const getFinalName = () => {
                    if (isDir) return (folderInput && folderInput.value || '').trim();
                    return joinExplorerRenamedFile(baseInput && baseInput.value, extInput && extInput.value);
                };

                const updatePreview = () => {
                    if (previewVal) previewVal.textContent = getFinalName() || '—';
                };
                updatePreview();

                let finished = false;
                const cleanup = () => {
                    try {
                        window.__gridCloseExplorerRenameModal = null;
                    } catch (_) {}
                    document.body.classList.remove('modal-open');
                    modal.style.display = 'none';
                    modal.setAttribute('aria-hidden', 'true');
                    if (btnSave) btnSave.removeEventListener('click', onSave);
                    if (btnCancel) btnCancel.removeEventListener('click', onCancel);
                    if (btnCloseX) btnCloseX.removeEventListener('click', onCancel);
                    modal.removeEventListener('click', onBackdropClick);
                    if (folderInput) {
                        folderInput.removeEventListener('input', updatePreview);
                        folderInput.removeEventListener('keydown', onEnterKey);
                    }
                    if (baseInput) {
                        baseInput.removeEventListener('input', updatePreview);
                        baseInput.removeEventListener('keydown', onEnterKey);
                    }
                    if (extInput) {
                        extInput.removeEventListener('input', updatePreview);
                        extInput.removeEventListener('keydown', onEnterKey);
                    }
                };

                const finish = (ok, resolvedName) => {
                    if (finished) return;
                    finished = true;
                    cleanup();
                    resolve(ok ? resolvedName : null);
                };

                function onSave() {
                    const name = getFinalName();
                    if (!name || !isValidExplorerItemName(name)) return;
                    if (name === initialName) {
                        finish(false);
                        return;
                    }
                    finish(true, name);
                }

                function onCancel() {
                    finish(false);
                }

                function onBackdropClick(e) {
                    if (e.target === modal) onCancel();
                }

                function onEnterKey(e) {
                    if (e.key !== 'Enter') return;
                    e.preventDefault();
                    onSave();
                }

                try {
                    window.__gridCloseExplorerRenameModal = onCancel;
                } catch (_) {}

                if (btnSave) btnSave.addEventListener('click', onSave);
                if (btnCancel) btnCancel.addEventListener('click', onCancel);
                if (btnCloseX) btnCloseX.addEventListener('click', onCancel);
                modal.addEventListener('click', onBackdropClick);
                if (folderInput) {
                    folderInput.addEventListener('input', updatePreview);
                    folderInput.addEventListener('keydown', onEnterKey);
                }
                if (baseInput) {
                    baseInput.addEventListener('input', updatePreview);
                    baseInput.addEventListener('keydown', onEnterKey);
                }
                if (extInput) {
                    extInput.addEventListener('input', updatePreview);
                    extInput.addEventListener('keydown', onEnterKey);
                }

                modal.style.display = 'block';
                modal.setAttribute('aria-hidden', 'false');
                document.body.classList.add('modal-open');

                requestAnimationFrame(() => {
                    if (isDir && folderInput) {
                        folderInput.focus();
                        folderInput.select();
                    } else if (baseInput) {
                        baseInput.focus();
                        baseInput.select();
                    }
                });
            });
        }

        async function explorerCopy(path, oldName, newName, bucket = 'files', opts = {}) {
            const res = await fetch('?action=explorerCopy', {
                method: 'POST',
                headers: { 'Content-Type': 'application/json' },
                body: JSON.stringify({
                    path: path || '',
                    oldName: oldName,
                    newName: newName,
                    bucket: bucket || 'files',
                    overwrite: !!opts.overwrite
                })
            });
            return await res.json();
        }

        async function explorerMove(fromPath, toPath, name, opts = {}) {
            let body = {
                fromPath: fromPath || '',
                toPath: toPath || '',
                name: name || '',
                fromBucket: opts.fromBucket || 'files',
                toBucket: opts.toBucket || 'files',
                overwrite: !!opts.overwrite,
                destName: opts.destName || undefined
            };
            if (window.GridExplorerSharedSpaces && typeof window.GridExplorerSharedSpaces.withMoveDepartments === 'function') {
                body = window.GridExplorerSharedSpaces.withMoveDepartments(
                    body,
                    body.fromBucket,
                    body.toBucket,
                    opts.fromDepartmentId || '',
                    opts.toDepartmentId || ''
                );
            }
            const res = await fetch('?action=explorerMove', {
                method: 'POST',
                headers: { 'Content-Type': 'application/json' },
                body: JSON.stringify(body)
            });
            return await res.json();
        }

        async function explorerCopyTo(fromPath, toPath, name, opts = {}) {
            let body = {
                fromPath: fromPath || '',
                toPath: toPath || '',
                name: name || '',
                autoRename: !!opts.autoRename,
                overwrite: !!opts.overwrite,
                destName: opts.destName || undefined,
                fromBucket: opts.fromBucket || 'files',
                toBucket: opts.toBucket || 'files'
            };
            if (window.GridExplorerSharedSpaces && typeof window.GridExplorerSharedSpaces.withMoveDepartments === 'function') {
                body = window.GridExplorerSharedSpaces.withMoveDepartments(
                    body,
                    body.fromBucket,
                    body.toBucket,
                    opts.fromDepartmentId || '',
                    opts.toDepartmentId || ''
                );
            }
            const res = await fetch('?action=explorerCopyTo', {
                method: 'POST',
                headers: { 'Content-Type': 'application/json' },
                body: JSON.stringify(body)
            });
            return await res.json();
        }

        function getFileClipboard() {
            if (!window.__gridFileClipboard) window.__gridFileClipboard = { items: [], action: 'copy' };
            return window.__gridFileClipboard;
        }

        function fileExplorerClipboardHasPasteableItems() {
            const c = getFileClipboard();
            return Array.isArray(c.items) && c.items.length > 0;
        }

        function closeWindowFromMosaic(windowId) {
            const winEntry = openWindows.find(w => w.id === windowId);
            if (!winEntry || !winEntry.frame) return;
            try {
                if (activePreviewImg && winEntry.frame.contains(activePreviewImg)) {
                    detachPreviewFeTabsObserver();
                    previewViewportWidthSnapshot = 0;
                    activePreviewImg = null;
                    scale = 1;
                    translateX = 0;
                    translateY = 0;
                    isImageZoomed = false;
                }
                winEntry.frame.remove();
            } catch (_) {}
            if (winEntry.app) removeTaskbarApp(windowId, winEntry.app);
            openWindows = openWindows.filter(w => w.id !== windowId);
            schedulePersistOpenWindowsSession();
            renderTaskbarApps();
            renderPinnedApps();
            if (windowsMosaicModal && windowsMosaicModal.style.display === 'block') {
                renderWindowsMosaic();
            }
            if (mobileAppHubBackdrop && mobileAppHubBackdrop.classList.contains('visible')) {
                renderMobileAppHub();
            }
        }

        async function explorerDelete(path, name, bucket = 'files') {
            const res = await fetch('?action=explorerDelete', {
                method: 'POST',
                headers: { 'Content-Type': 'application/json' },
                body: JSON.stringify(explorerApiJsonBody({ path: path || '', name: name, bucket: bucket || 'files' }))
            });
            return await res.json();
        }

        async function explorerDeletePermanent(path, name, bucket = 'files') {
            const res = await fetch('?action=explorerDeletePermanent', {
                method: 'POST',
                headers: { 'Content-Type': 'application/json' },
                body: JSON.stringify(explorerApiJsonBody({ path: path || '', name: name, bucket: bucket || 'files' }))
            });
            return await res.json();
        }

        async function explorerRestoreTrashItem(id, opts = {}) {
            const res = await fetch('?action=explorerRestoreTrashItem', {
                method: 'POST',
                headers: { 'Content-Type': 'application/json' },
                body: JSON.stringify({
                    id: id,
                    overwrite: !!opts.overwrite,
                    merge: !!opts.merge,
                    newName: opts.newName ? String(opts.newName) : ''
                })
            });
            return await res.json();
        }

        async function explorerDeleteTrashItem(id) {
            const res = await fetch('?action=explorerDeleteTrashItem', {
                method: 'POST',
                headers: { 'Content-Type': 'application/json' },
                body: JSON.stringify({ id: id })
            });
            return await res.json();
        }

        async function explorerEmptyTrash() {
            const res = await fetch('?action=explorerEmptyTrash', { method: 'POST' });
            return await res.json();
        }

        async function explorerEmptyRemoteCache() {
            const res = await fetch('?action=explorerEmptyRemoteCache', { method: 'POST' });
            return await res.json();
        }

        function setExplorerStorageProgressUI(statsEl, message, percent) {
            if (!statsEl) return;
            const p = Math.max(0, Math.min(100, Math.round(percent)));
            const msg = (message != null && message !== '') ? message : tr('explorerStorageProgressPreparing');
            statsEl.innerHTML =
                '<span class="explorer-storage-loading explorer-storage-loading--active" role="status" aria-live="polite">' +
                '<span class="explorer-storage-loading-spin" aria-hidden="true"></span>' +
                '<span class="explorer-storage-loading-text">[ ' + escapeHtml(msg) + ' — ' + p + '% ]' +
                '</span></span>';
        }

        function truncateExplorerParen(s, maxLen) {
            const t = s == null ? '' : String(s);
            if (t.length <= maxLen) return t;
            if (maxLen <= 1) return '…';
            return t.slice(0, maxLen - 1) + '…';
        }

        function formatExplorerCacheAge(sec) {
            const s = Math.max(0, Math.floor(Number(sec) || 0));
            if (s < 60) return String(s) + 's';
            if (s < 3600) return String(Math.floor(s / 60)) + 'm';
            const h = Math.floor(s / 3600);
            if (h < 48) return String(h) + 'h';
            return String(Math.floor(s / 86400)) + 'd';
        }

        async function readJsonRes(res) {
            if (!res) return null;
            if (!res.ok) return null;
            const raw = (await res.text()).replace(/^\uFEFF/, '');
            const t = raw.trim();
            if (!t) return null;
            try {
                return JSON.parse(t);
            } catch (_) {
                return null;
            }
        }

        async function fetchExplorerStoragePart(part, forceRefresh) {
            let u = '?action=explorerStorageStats&part=' + encodeURIComponent(part);
            if (forceRefresh) u += '&refresh=1';
            const res = await fetch(u, { cache: 'no-store' });
            return await readJsonRes(res);
        }

        async function fetchExplorerStorageStatsAll(forceRefresh) {
            let u = '?action=explorerStorageStats';
            if (forceRefresh) u += '&refresh=1';
            const res = await fetch(u, { cache: 'no-store' });
            return await readJsonRes(res);
        }

        async function fetchExplorerStorageFilesWithStream(onEntry, forceRefresh) {
            let res;
            let url = '?action=explorerStorageFilesStream';
            if (forceRefresh) {
                url += '&refresh=1';
            }
            try {
                res = await fetch(url, { cache: 'no-store' });
            } catch (_) {
                return { ok: false, files: null };
            }
            if (!res.ok || !res.body || typeof res.body.getReader !== 'function') {
                return { ok: false, files: null };
            }
            const reader = res.body.getReader();
            const dec = new TextDecoder();
            let buf = '';
            let outFiles = null;
            for (;;) {
                let r;
                try {
                    r = await reader.read();
                } catch (_) {
                    return { ok: false, files: null };
                }
                const { done, value } = r;
                if (value) buf += dec.decode(value, { stream: !done });
                let ix;
                while ((ix = buf.indexOf('\n')) >= 0) {
                    const line = buf.slice(0, ix);
                    buf = buf.slice(ix + 1);
                    if (!line.trim()) continue;
                    let j;
                    try {
                        j = JSON.parse(line);
                    } catch (e) {
                        continue;
                    }
                    if (j.type === 'done' && j.success && j.files) {
                        outFiles = j.files;
                    } else if (j.type === 'entry' && (j.name != null || j.pathRel) && onEntry) {
                        onEntry(j);
                    }
                }
                if (done) break;
            }
            if (outFiles) {
                return { ok: true, files: { bytes: Number(outFiles.bytes) || 0, files: Number(outFiles.files) || 0 } };
            }
            return { ok: false, files: null };
        }

        async function fetchExplorerStorageStatsWithProgress(onProgress, options) {
            const forceRefresh = options && options.forceRefresh === true;
            onProgress(2, tr('explorerStorageProgressPreparing'));
            onProgress(4, tr('explorerStorageContacting'));
            const uFast = '?action=explorerStorageStats' + (forceRefresh ? '&refresh=1' : '');
            const resFast = await fetch(uFast, { cache: 'no-store' });
            const dataFast = await readJsonRes(resFast);
            if (
                dataFast &&
                dataFast.success &&
                dataFast.remoteCache != null &&
                dataFast.trash != null &&
                dataFast.files != null
            ) {
                if (dataFast.fromCache) {
                    onProgress(
                        100,
                        tr('explorerStorageFromCache') +
                            (typeof dataFast.cacheAgeSeconds === 'number'
                                ? ' · ~' + formatExplorerCacheAge(dataFast.cacheAgeSeconds)
                                : '')
                    );
                } else {
                    onProgress(100, tr('explorerStorageProgressRendering'));
                }
                return dataFast;
            }
            onProgress(6, tr('explorerStorageProgressMeta'));
            const meta = await fetchExplorerStoragePart('meta', forceRefresh);
            if (!meta || !meta.success) {
                const all = await fetchExplorerStorageStatsAll(forceRefresh);
                if (all && all.success) {
                    onProgress(100, tr('explorerStorageProgressRendering'));
                }
                return all && all.success ? all : null;
            }
            onProgress(16, tr('explorerStorageProgressRemote'));
            const rC = await fetchExplorerStoragePart('remoteCache', forceRefresh);
            if (!rC || !rC.success) {
                const all = await fetchExplorerStorageStatsAll(forceRefresh);
                if (all && all.success) {
                    onProgress(100, tr('explorerStorageProgressRendering'));
                }
                return all && all.success ? all : null;
            }
            onProgress(38, tr('explorerStorageProgressTrash'));
            const tR = await fetchExplorerStoragePart('trash', forceRefresh);
            if (!tR || !tR.success) {
                const all = await fetchExplorerStorageStatsAll(forceRefresh);
                if (all && all.success) {
                    onProgress(100, tr('explorerStorageProgressRendering'));
                }
                return all && all.success ? all : null;
            }
            onProgress(60, tr('explorerStorageProgressFiles') + ' (…)');
            let fl;
            let streamCount = 0;
            const streamR = await fetchExplorerStorageFilesWithStream((j) => {
                const paren = j.pathRel != null && j.pathRel !== '' ? String(j.pathRel) : String(j.name || '');
                if (!paren) {
                    onProgress(62, tr('explorerStorageProgressFiles') + ' (…)');
                    return;
                }
                streamCount++;
                const detail = paren + (j.isDir ? '/' : '');
                onProgress(60 + Math.min(35, streamCount * 0.3), tr('explorerStorageProgressFiles') + ' (' + truncateExplorerParen(detail, 80) + ')');
            }, forceRefresh);
            if (streamR && streamR.ok && streamR.files) {
                fl = { success: true, part: 'files', files: streamR.files };
            } else {
                onProgress(62, tr('explorerStorageProgressFiles') + ' (…)');
                const fPart = await fetchExplorerStoragePart('files', forceRefresh);
                if (!fPart || !fPart.success) {
                    const all = await fetchExplorerStorageStatsAll(forceRefresh);
                    if (all && all.success) {
                        onProgress(100, tr('explorerStorageProgressRendering'));
                    }
                    return all && all.success ? all : null;
                }
                fl = fPart;
            }
            onProgress(100, tr('explorerStorageProgressRendering'));
            const rc = rC.remoteCache || { bytes: 0, files: 0 };
            const trs = tR.trash || { bytes: 0, files: 0 };
            const fStat = (fl && fl.files) || { bytes: 0, files: 0 };
            const merged = {
                success: true,
                remoteCache: rc,
                trash: trs,
                files: fStat,
                explorerTotal: { bytes: rc.bytes + trs.bytes + fStat.bytes, files: rc.files + trs.files + fStat.files },
                disk: meta.disk,
                diskUnavailableReason: meta.diskUnavailableReason,
                environment: meta.environment,
                phpOs: meta.phpOs
            };
            try {
                await fetch('?action=explorerStorageStatsCacheSave', {
                    method: 'POST',
                    headers: { 'Content-Type': 'application/json' },
                    body: JSON.stringify({ remoteCache: rc, trash: trs, files: fStat })
                });
            } catch (_) {}
            return merged;
        }

        function formatExplorerBytes(n) {
            const v = Number(n) || 0;
            if (v < 1024) return String(v) + ' B';
            const u = ['KB', 'MB', 'GB', 'TB'];
            let x = v;
            let i = -1;
            do {
                x /= 1024;
                i++;
            } while (x >= 1024 && i < u.length - 1);
            const dec = x >= 100 || i === 0 ? 0 : (x >= 10 ? 1 : 2);
            return x.toFixed(dec) + ' ' + u[i];
        }

        function drawExplorerPieChart(canvas, parts) {
            const ctx = canvas.getContext('2d');
            if (ctx && typeof ctx.ellipse !== 'function') {
                ctx.ellipse = function (cx, cy, rx, ry, rotation, a0, a1) {
                    this.save();
                    this.beginPath();
                    this.translate(cx, cy);
                    this.rotate(rotation);
                    const s = Math.max(1e-6, rx);
                    this.scale(1, ry / s);
                    this.arc(0, 0, s, a0, a1, false);
                    this.restore();
                };
            }
            const w = canvas.width;
            const h = canvas.height;
            ctx.clearRect(0, 0, w, h);
            const cx = w / 2;
            const cy = h / 2 - 4;
            const r = Math.min(w, h) * 0.34;
            const safeParts = (parts || [])
                .map(p => ({
                    value: Number(p?.value) || 0,
                    color: p?.color || 'rgba(128,128,128,0.45)',
                    label: (p?.label ?? '').toString()
                }))
                .filter(p => p.value > 0);
            const total = safeParts.reduce((a, p) => a + p.value, 0);
            if (total <= 0) {
                ctx.fillStyle = 'rgba(128,128,128,0.35)';
                ctx.beginPath();
                ctx.arc(cx, cy, r, 0, Math.PI * 2);
                ctx.fill();
                ctx.fillStyle = getComputedStyle(document.body).getPropertyValue('--text-color') || '#ccc';
                ctx.font = '13px sans-serif';
                ctx.textAlign = 'center';
                ctx.textBaseline = 'middle';
                ctx.fillText(tr('explorerStorageNoData'), cx, cy);
                return;
            }

            function darkerColor(c, factor = 0.65) {
                const s = (c || '').toString().trim();
                const m = s.match(/^rgba?\(\s*([0-9.]+)\s*,\s*([0-9.]+)\s*,\s*([0-9.]+)(?:\s*,\s*([0-9.]+))?\s*\)$/i);
                if (!m) return 'rgba(0,0,0,0.35)';
                const r0 = Math.max(0, Math.min(255, Number(m[1]) || 0));
                const g0 = Math.max(0, Math.min(255, Number(m[2]) || 0));
                const b0 = Math.max(0, Math.min(255, Number(m[3]) || 0));
                const a0 = m[4] == null ? 1 : Math.max(0, Math.min(1, Number(m[4]) || 0));
                const r1 = Math.round(r0 * factor);
                const g1 = Math.round(g0 * factor);
                const b1 = Math.round(b0 * factor);
                return `rgba(${r1}, ${g1}, ${b1}, ${Math.max(0.25, a0)})`;
            }

            // Isometric/3D depth settings
            const depth = Math.max(10, Math.round(r * 0.22));
            const tiltX = 1.0;
            const tiltY = 0.72; // squash Y for isometric look

            let a0 = -Math.PI / 2;
            const slices = [];
            safeParts.forEach((p) => {
                const slice = (p.value / total) * Math.PI * 2;
                slices.push({ ...p, aStart: a0, aEnd: a0 + slice });
                a0 += slice;
            });

            // Draw bottom face first (darker), using ellipse to avoid edge "gaps" from scaling.
            for (const s of slices) {
                ctx.beginPath();
                ctx.moveTo(cx, cy + depth);
                ctx.ellipse(cx, cy + depth, r * tiltX, r * tiltY, 0, s.aStart, s.aEnd);
                ctx.closePath();
                ctx.fillStyle = darkerColor(s.color, 0.55);
                ctx.fill();
            }

            // Draw side walls (front half only) to "close" the isometric volume and avoid open/overlapped areas.
            for (const s of slices) {
                const mid = (s.aStart + s.aEnd) / 2;
                // Front half in screen coords: sin(mid) > 0
                if (Math.sin(mid) <= 0) continue;
                ctx.beginPath();
                // Top outer arc
                ctx.ellipse(cx, cy, r * tiltX, r * tiltY, 0, s.aStart, s.aEnd);
                // Down to bottom outer arc (same angles)
                ctx.ellipse(cx, cy + depth, r * tiltX, r * tiltY, 0, s.aEnd, s.aStart, true);
                ctx.closePath();
                ctx.fillStyle = darkerColor(s.color, 0.62);
                ctx.fill();
            }

            // Draw top face last (original colors), using ellipse for consistent geometry.
            for (const s of slices) {
                ctx.beginPath();
                ctx.moveTo(cx, cy);
                ctx.ellipse(cx, cy, r * tiltX, r * tiltY, 0, s.aStart, s.aEnd);
                ctx.closePath();
                ctx.fillStyle = s.color;
                ctx.fill();
            }

            // Outline
            ctx.strokeStyle = 'rgba(255,255,255,0.22)';
            ctx.lineWidth = 1;
            ctx.beginPath();
            ctx.ellipse(cx, cy, r * tiltX, r * tiltY, 0, 0, Math.PI * 2);
            ctx.stroke();

            // Connector lines + labels (outside), similar to the reference image
            const textColor = (getComputedStyle(document.body).getPropertyValue('--text-color') || '#d7d7d7').trim();
            ctx.font = '12px sans-serif';
            ctx.fillStyle = textColor;
            ctx.lineWidth = 1;

            const minGap = 16;
            const pad = 10;
            const elbowR = r + 14;
            const labelXOffset = r + 70;

            const left = [];
            const right = [];
            for (const s of slices) {
                const mid = (s.aStart + s.aEnd) / 2;
                const cos = Math.cos(mid);
                const sin = Math.sin(mid);
                // Apply tilt to Y coordinates so connectors match isometric face
                const anchorX = cx + cos * (r * 0.92);
                const anchorY = cy + (sin * (r * 0.92)) * tiltY;
                const elbowX = cx + cos * elbowR;
                const elbowY = cy + (sin * elbowR) * tiltY;
                const pct = total > 0 ? (s.value / total) * 100 : 0;
                const label = s.label ? `${s.label} (${pct.toFixed(pct >= 10 ? 0 : 1)}%)` : `${pct.toFixed(1)}%`;
                const item = { ...s, mid, cos, sin, anchorX, anchorY, elbowX, elbowY, label };
                (cos >= 0 ? right : left).push(item);
            }

            function layoutSide(items, isRight) {
                items.sort((a, b) => a.elbowY - b.elbowY);
                let lastY = -Infinity;
                for (const it of items) {
                    if (it.elbowY < lastY + minGap) it.elbowY = lastY + minGap;
                    it.elbowY = Math.max(pad, Math.min(h - pad, it.elbowY));
                    lastY = it.elbowY;
                    it.labelX = cx + (isRight ? labelXOffset : -labelXOffset);
                    it.labelY = it.elbowY;
                }
                // Back-pass to keep within bounds
                for (let i = items.length - 2; i >= 0; i--) {
                    if (items[i].labelY > items[i + 1].labelY - minGap) {
                        items[i].labelY = items[i + 1].labelY - minGap;
                    }
                    items[i].labelY = Math.max(pad, Math.min(h - pad, items[i].labelY));
                }
            }

            layoutSide(left, false);
            layoutSide(right, true);

            function drawLabelWithBg(text, x, y, align, labelColor) {
                ctx.save();
                ctx.textAlign = align;
                ctx.textBaseline = 'middle';
                const m = ctx.measureText(text);
                const tw = m.width;
                const th = 14;
                const bx = align === 'left' ? x - 2 : x - tw - 2;
                const by = y - th / 2 - 2;
                const bw = tw + 10;
                const bh = th + 4;

                // Keep label inside canvas
                const bx2 = Math.max(2, Math.min(w - bw - 2, bx));
                const by2 = Math.max(2, Math.min(h - bh - 2, by));
                const tx = align === 'left' ? (bx2 + 6) : (bx2 + bw - 6);
                const ty = by2 + bh / 2;

                // Background pill + outline text so it never "disappears"
                ctx.fillStyle = 'rgba(0,0,0,0.38)';
                ctx.beginPath();
                const rad = 6;
                ctx.moveTo(bx2 + rad, by2);
                ctx.arcTo(bx2 + bw, by2, bx2 + bw, by2 + bh, rad);
                ctx.arcTo(bx2 + bw, by2 + bh, bx2, by2 + bh, rad);
                ctx.arcTo(bx2, by2 + bh, bx2, by2, rad);
                ctx.arcTo(bx2, by2, bx2 + bw, by2, rad);
                ctx.closePath();
                ctx.fill();

                ctx.lineWidth = 3;
                ctx.strokeStyle = 'rgba(0,0,0,0.55)';
                ctx.strokeText(text, tx, ty);
                ctx.fillStyle = labelColor || textColor || '#fff';
                ctx.fillText(text, tx, ty);
                ctx.restore();
                return { tx, ty };
            }

            function drawItem(it, isRight) {
                ctx.strokeStyle = 'rgba(180,190,210,0.65)';
                ctx.beginPath();
                ctx.moveTo(it.anchorX, it.anchorY);
                ctx.lineTo(it.elbowX, it.elbowY);
                const endX = it.labelX + (isRight ? -10 : 10);
                const endY = it.labelY;
                ctx.lineTo(endX, endY);
                ctx.stroke();

                // Arrow head on last segment ("flecha")
                const angle = Math.atan2(endY - it.elbowY, endX - it.elbowX);
                const ah = 8;
                const aw = 5;
                ctx.fillStyle = 'rgba(180,190,210,0.75)';
                ctx.beginPath();
                ctx.moveTo(endX, endY);
                ctx.lineTo(endX - ah * Math.cos(angle) + aw * Math.sin(angle), endY - ah * Math.sin(angle) - aw * Math.cos(angle));
                ctx.lineTo(endX - ah * Math.cos(angle) - aw * Math.sin(angle), endY - ah * Math.sin(angle) + aw * Math.cos(angle));
                ctx.closePath();
                ctx.fill();

                drawLabelWithBg(it.label, it.labelX, it.labelY, isRight ? 'left' : 'right', it.color);
            }

            left.forEach(it => drawItem(it, false));
            right.forEach(it => drawItem(it, true));
        }

        function drawExplorerDiskBar(canvas, disk) {
            const ctx = canvas.getContext('2d');
            const w = canvas.width;
            const h = canvas.height;
            ctx.clearRect(0, 0, w, h);
            const barH = 28;
            const y = 28;
            const pad = 8;
            const bw = w - pad * 2;
            if (!disk || !disk.totalBytes) {
                ctx.fillStyle = 'rgba(128,128,128,0.25)';
                ctx.fillRect(pad, y, bw, barH);
                ctx.fillStyle = getComputedStyle(document.body).getPropertyValue('--text-color') || '#aaa';
                ctx.font = '12px sans-serif';
                ctx.textAlign = 'center';
                ctx.textBaseline = 'middle';
                ctx.fillText(tr('explorerDiskUnavailable'), w / 2, y + barH / 2);
                return;
            }
            const usedR = Math.min(1, disk.usedBytes / disk.totalBytes);
            ctx.fillStyle = 'rgba(80,80,90,0.5)';
            ctx.fillRect(pad, y, bw, barH);
            ctx.fillStyle = 'rgba(70, 130, 180, 0.88)';
            ctx.fillRect(pad, y, bw * usedR, barH);
            ctx.strokeStyle = 'rgba(255,255,255,0.2)';
            ctx.strokeRect(pad, y, bw, barH);
            ctx.font = '12px sans-serif';
            ctx.textAlign = 'left';
            ctx.textBaseline = 'bottom';
            const pct = (usedR * 100).toFixed(1);
            ctx.fillStyle = 'rgba(70, 130, 180, 0.98)';
            ctx.fillText(`${pct}% ${tr('explorerDiskUsed')} — ${formatExplorerBytes(disk.usedBytes)} / ${formatExplorerBytes(disk.totalBytes)}`, pad, y - 4);
            ctx.textAlign = 'right';
            ctx.textBaseline = 'top';
            ctx.fillStyle = 'rgba(95, 105, 118, 0.98)';
            ctx.fillText(`${tr('explorerDiskFree')}: ${formatExplorerBytes(disk.freeBytes)}`, pad + bw, y + barH + 6);
        }

        function explorerEnvDescription(env) {
            const map = {
                local_xampp: tr('explorerEnvXampp'),
                windows_server: tr('explorerEnvWindows'),
                unix_hosting: tr('explorerEnvUnix'),
                unknown: tr('explorerEnvUnknown')
            };
            return map[env] || map.unknown;
        }

        async function refreshExplorerStoragePanel(opts) {
            const forceRefresh = !!(opts && opts.forceRefresh);
            const statsEl = document.getElementById('explorerStorageStats');
            const envEl = document.getElementById('explorerStorageEnv');
            const pieCanvas = document.getElementById('explorerStoragePieCanvas');
            const diskCanvas = document.getElementById('explorerStorageDiskCanvas');
            if (!statsEl || !pieCanvas || !diskCanvas) return;
            setExplorerStorageProgressUI(statsEl, tr('explorerStorageProgressPreparing'), 0);
            try {
                const data = await fetchExplorerStorageStatsWithProgress(
                    (pct, msg) => {
                        setExplorerStorageProgressUI(statsEl, msg, pct);
                    },
                    { forceRefresh: forceRefresh }
                );
                if (!data || !data.success) {
                    statsEl.textContent = tr('explorerStorageLoadError');
                    return;
                }
                const rc = data.remoteCache || { bytes: 0, files: 0 };
                const trs = data.trash || { bytes: 0, files: 0 };
                const fl = data.files || { bytes: 0, files: 0 };
                const tot = data.explorerTotal || { bytes: 0, files: 0 };
                const disk = data.disk && data.disk.totalBytes ? data.disk : null;
                const explorerBytes = rc.bytes + trs.bytes + fl.bytes;
                let otherBytes = 0;
                let freeBytes = 0;
                if (disk) {
                    otherBytes = Math.max(0, disk.usedBytes - explorerBytes);
                    freeBytes = Math.max(0, disk.freeBytes);
                }
                if (envEl) {
                    const envLine1 = `${tr('explorerStorageEnvironment')}: ${explorerEnvDescription(data.environment)} (${data.phpOs || ''})`;
                    if (data.fromCache) {
                        const age = typeof data.cacheAgeSeconds === 'number' ? formatExplorerCacheAge(data.cacheAgeSeconds) : '';
                        envEl.textContent =
                            envLine1 +
                            '\n' +
                            tr('explorerStorageCacheLine') +
                            (age ? ' (~' + age + ')' : '');
                        envEl.setAttribute('title', tr('explorerStorageCacheLine'));
                    } else {
                        envEl.textContent = envLine1;
                        envEl.removeAttribute('title');
                    }
                }
                let statGridHtml =
                    '<div class="explorer-storage-stat-grid">' +
                    '<div><strong class="explorer-stat-label-rc">' + tr('explorerLabelRemoteCache') + '</strong><br>' + formatExplorerBytes(rc.bytes) + ' · ' + rc.files + ' ' + tr('explorerFilesLabel') + '</div>' +
                    '<div><strong class="explorer-stat-label-tr">' + tr('explorerLabelTrash') + '</strong><br>' + formatExplorerBytes(trs.bytes) + ' · ' + trs.files + ' ' + tr('explorerFilesLabel') + '</div>' +
                    '<div><strong class="explorer-stat-label-fl">' + tr('explorerLabelLocalFiles') + '</strong><br>' + formatExplorerBytes(fl.bytes) + ' · ' + fl.files + ' ' + tr('explorerFilesLabel') + '</div>' +
                    '<div><strong class="explorer-stat-label-tot">' + tr('explorerLabelTotalExplorer') + '</strong><br>' + formatExplorerBytes(tot.bytes) + ' · ' + tot.files + ' ' + tr('explorerFilesLabel') + '</div>';
                if (disk) {
                    statGridHtml +=
                        '<div><strong class="explorer-stat-label-other">' + tr('explorerLabelDiskOtherPie') + '</strong><br>' + formatExplorerBytes(otherBytes) + '</div>' +
                        '<div><strong class="explorer-stat-label-free">' + tr('explorerLabelDiskFreePie') + '</strong><br>' + formatExplorerBytes(freeBytes) + '</div>';
                }
                statGridHtml += '</div>';
                const EXPLORER_STORAGE_PIE_VIS_KEY = 'explorerStoragePieVisibilityV1';
                function normalizeExplorerStoragePieVisibility(raw, hasDisk) {
                    const base = {
                        rc: true,
                        tr: true,
                        fl: true,
                        other: hasDisk ? true : false,
                        free: hasDisk ? true : false
                    };
                    if (!raw || typeof raw !== 'object') return base;
                    const out = { ...base };
                    for (const k of Object.keys(base)) {
                        if (typeof raw[k] === 'boolean') out[k] = raw[k];
                    }
                    return out;
                }
                const vis = normalizeExplorerStoragePieVisibility(getSetting(EXPLORER_STORAGE_PIE_VIS_KEY, null), !!disk);

                function legendItemHtml(key, className, labelText) {
                    const isOn = !!vis[key];
                    const offClass = isOn ? '' : ' explorer-leg--off';
                    const pressed = isOn ? 'true' : 'false';
                    return `<span class="${className} explorer-leg-toggle${offClass}" role="button" tabindex="0" data-explorer-pie-key="${key}" aria-pressed="${pressed}"><span class="explorer-leg-dot" aria-hidden="true">●</span> ${labelText}</span>`;
                }

                let legendHtml =
                    '<p class="explorer-storage-legend explorer-storage-legend--chart" aria-label="' + tr('explorerStorageTabShort') + '">' +
                    legendItemHtml('rc', 'explorer-leg-rc', tr('explorerLabelRemoteCache')) +
                    ' · ' + legendItemHtml('tr', 'explorer-leg-tr', tr('explorerLabelTrash')) +
                    ' · ' + legendItemHtml('fl', 'explorer-leg-fl', tr('explorerLabelLocalFiles'));
                if (disk) {
                    legendHtml +=
                        ' · ' + legendItemHtml('other', 'explorer-leg-other', tr('explorerLabelDiskOtherPie')) +
                        ' · ' + legendItemHtml('free', 'explorer-leg-free', tr('explorerLabelDiskFreePie'));
                }
                legendHtml += '</p>';
                statsEl.innerHTML = statGridHtml + legendHtml;
                const pieLegendEl = document.getElementById('explorerStoragePieLegend');
                if (pieLegendEl) pieLegendEl.innerHTML = legendHtml;

                const pieLabel = document.getElementById('explorerStoragePieLabel');
                if (pieLabel) {
                    pieLabel.textContent = disk ? tr('explorerChartPieTitle') : tr('explorerChartPieTitleExplorerOnly');
                    pieLabel.className = 'explorer-storage-chart-label explorer-chart-label-pie' + (disk ? ' explorer-chart-label-pie--disk' : '');
                }
                const barLabel = document.getElementById('explorerStorageBarLabel');
                if (barLabel) {
                    barLabel.textContent = tr('explorerChartDiskTitle');
                    barLabel.className = 'explorer-storage-chart-label explorer-chart-label-disk';
                }
                const allPieParts = disk
                    ? [
                        { key: 'rc', value: rc.bytes, color: 'rgba(230, 126, 34, 0.92)', label: tr('explorerLabelRemoteCache') },
                        { key: 'tr', value: trs.bytes, color: 'rgba(192, 57, 43, 0.92)', label: tr('explorerLabelTrash') },
                        { key: 'fl', value: fl.bytes, color: 'rgba(41, 128, 185, 0.92)', label: tr('explorerLabelLocalFiles') },
                        { key: 'other', value: otherBytes, color: 'rgba(118, 122, 138, 0.9)', label: tr('explorerLabelDiskOtherPie') },
                        { key: 'free', value: freeBytes, color: 'rgba(39, 174, 96, 0.92)', label: tr('explorerLabelDiskFreePie') }
                    ]
                    : [
                        { key: 'rc', value: rc.bytes, color: 'rgba(230, 126, 34, 0.92)', label: tr('explorerLabelRemoteCache') },
                        { key: 'tr', value: trs.bytes, color: 'rgba(192, 57, 43, 0.92)', label: tr('explorerLabelTrash') },
                        { key: 'fl', value: fl.bytes, color: 'rgba(41, 128, 185, 0.92)', label: tr('explorerLabelLocalFiles') }
                    ];
                const pieParts = allPieParts.filter(p => vis[p.key] !== false);
                await new Promise((resolve) => {
                    if (typeof requestAnimationFrame === 'function') {
                        requestAnimationFrame(() => requestAnimationFrame(() => resolve()));
                    } else {
                        resolve();
                    }
                });
                try {
                    drawExplorerPieChart(pieCanvas, pieParts);
                } catch (ePie) {
                    if (typeof console !== 'undefined' && console.warn) {
                        console.warn('drawExplorerPieChart', ePie);
                    }
                }
                try {
                    drawExplorerDiskBar(diskCanvas, data.disk);
                } catch (eBar) {
                    if (typeof console !== 'undefined' && console.warn) {
                        console.warn('drawExplorerDiskBar', eBar);
                    }
                }

                function attachLegendToggleHandlers(rootEl) {
                    if (!rootEl) return;
                    const toggles = rootEl.querySelectorAll('[data-explorer-pie-key]');
                    toggles.forEach(el => {
                        const key = el.getAttribute('data-explorer-pie-key');
                        if (!key) return;
                        const onToggle = async () => {
                            const next = { ...vis, [key]: !vis[key] };
                            // Evitar "todo apagado": si se apaga el último rubro visible, revertir.
                            const anyOn = Object.keys(next).some(k => next[k]);
                            if (!anyOn) return;
                            try {
                                await saveSettings({ [EXPLORER_STORAGE_PIE_VIS_KEY]: next });
                            } catch (_) {}
                            // Reflejar inmediatamente en memoria y UI actual
                            vis[key] = next[key];
                            el.classList.toggle('explorer-leg--off', !vis[key]);
                            el.setAttribute('aria-pressed', vis[key] ? 'true' : 'false');
                            drawExplorerPieChart(
                                pieCanvas,
                                allPieParts.filter(p => vis[p.key] !== false)
                            );
                        };
                        el.addEventListener('click', (e) => {
                            e.preventDefault();
                            onToggle();
                        });
                        el.addEventListener('keydown', (e) => {
                            if (e.key === 'Enter' || e.key === ' ') {
                                e.preventDefault();
                                onToggle();
                            }
                        });
                    });
                }
                attachLegendToggleHandlers(statsEl);
                attachLegendToggleHandlers(pieLegendEl);
            } catch (e) {
                statsEl.textContent = tr('explorerStorageLoadError');
            }
        }

        function openFileExplorerWindow(app, restoreState = null, initialTarget = null) {
            if (app && app.id === 'file-explorer-system' && !restoreState) {
                try {
                    const sid = getAppStableId(app);
                    const ag = activeTaskbarApps.find(a => a.appId === sid);
                    if (ag && ag.windows.length > 0) {
                        const w = ag.windows.find(x => !x.isMinimized) || ag.windows[0];
                        if (w && w.frame && document.body.contains(w.frame)) {
                            const host = w.frame.querySelector('[id^="explorerHost_"]');
                            if (initialTarget && host && typeof host.__gridNavigateTo === 'function') {
                                host.__gridNavigateTo(initialTarget);
                            }
                            w.frame.style.zIndex = ++windowZIndex;
                            w.frame.classList.remove('minimized');
                            w.frame.style.display = 'flex';
                            w.isMinimized = false;
                            ag.activeWindowId = w.id;
                            renderTaskbarApps();
                            renderPinnedApps();
                            setTimeout(() => focusExplorerPrimaryElement(), 0);
                            return;
                        }
                    }
                } catch (_) {}
            }
            const windowId = 'explorerwin_' + Date.now() + '_' + Math.random().toString(36).substr(2, 8);
            const frame = document.createElement('div');
            frame.className = 'window-frame maximized';
            frame.id = windowId;
            frame.style.zIndex = ++windowZIndex;
            frame.style.top = '50px';
            frame.style.left = '50px';
            frame.style.width = '900px';
            frame.style.height = '650px';

            frame.innerHTML = `
                <div class="window-titlebar">
                    <div class="window-title">
                        <img src="${app.icon}" alt="">
                        <span>${app.title}</span>
                    </div>
                    <div class="window-controls">
                        <button class="minimize-btn" title="${tr('minimizeWindow')}"><i class="fas fa-minus"></i></button>
                        <button class="maximize-btn" title="${tr('maximizeRestoreWindow')}"><i class="fas fa-window-restore"></i></button>
                        <button class="fullscreen-btn" title="${tr('fullscreenWindow')}"><i class="fas fa-expand"></i></button>
                        <button class="close-btn" title="${tr('closeWindow')}"><i class="fas fa-times"></i></button>
                    </div>
                </div>
                <div class="window-content">
                    <div class="file-explorer" id="explorerHost_${windowId}"></div>
                </div>
                <div class="window-resize-handle"></div>
            `;

            document.body.appendChild(frame);
            logActivity('window_open', { title: app.title, action: 'explorer', url: app.fileInfo?.url || '' });

            const titlebar = frame.querySelector('.window-titlebar');
            const minimizeBtn = frame.querySelector('.minimize-btn');
            const maximizeBtn = frame.querySelector('.maximize-btn');
            const fullscreenBtn = frame.querySelector('.fullscreen-btn');
            const closeBtn = frame.querySelector('.close-btn');
            const resizeHandle = frame.querySelector('.window-resize-handle');
            resizeHandle.style.display = 'none';

            let isMaximized = true;
            let isFullscreen = false;
            let savedRect = null;

            const host = frame.querySelector(`#explorerHost_${windowId}`);
            initFileExplorerUI(host, { windowId, app, initialTarget: initialTarget || null });
            try {
                if (restoreState && restoreState.explorerState && typeof host.__gridApplyState === 'function') {
                    host.__gridApplyState(restoreState.explorerState);
                }
            } catch (_) {}
            setTimeout(() => focusExplorerPrimaryElement(), 0);

            minimizeBtn.addEventListener('click', () => {
                const appGroup = activeTaskbarApps.find(a => a.appId === getAppStableId(app));
                if (appGroup) {
                    const windowObj = appGroup.windows.find(w => w.id === windowId);
                    if (windowObj) {
                        windowObj.previewCache = getWindowPreviewThumb(windowObj, appGroup);
                        windowObj.isMinimized = true;
                    }
                    appGroup.activeWindowId = null;
                    renderTaskbarApps();
                }
                frame.classList.add('minimized');
                frame.style.display = 'none';
                schedulePersistOpenWindowsSession();
                setTimeout(() => focusExplorerPrimaryElement(), 0);
            });

            maximizeBtn.addEventListener('click', () => {
                if (isMaximized) {
                    frame.classList.remove('maximized');
                    if (!savedRect) {
                        const ds = frame.dataset || {};
                        if (ds.savedTop || ds.savedLeft || ds.savedWidth || ds.savedHeight) {
                            savedRect = { top: ds.savedTop, left: ds.savedLeft, width: ds.savedWidth, height: ds.savedHeight };
                        }
                    }
                    if (savedRect) {
                        frame.style.top = savedRect.top;
                        frame.style.left = savedRect.left;
                        frame.style.width = savedRect.width;
                        frame.style.height = savedRect.height;
                    }
                    maximizeBtn.innerHTML = '<i class="fas fa-square"></i>';
                } else {
                    savedRect = { top: frame.style.top, left: frame.style.left, width: frame.style.width, height: frame.style.height };
                    frame.dataset.savedTop = savedRect.top;
                    frame.dataset.savedLeft = savedRect.left;
                    frame.dataset.savedWidth = savedRect.width;
                    frame.dataset.savedHeight = savedRect.height;
                    frame.classList.add('maximized');
                    maximizeBtn.innerHTML = '<i class="fas fa-window-restore"></i>';
                }
                isMaximized = !isMaximized;
                resizeHandle.style.display = isMaximized ? 'none' : 'block';
                schedulePersistOpenWindowsSession();
            });

            fullscreenBtn.addEventListener('click', () => {
                if (!isFullscreen) {
                    frame.requestFullscreen();
                    fullscreenBtn.innerHTML = '<i class="fas fa-compress"></i>';
                } else {
                    document.exitFullscreen();
                    fullscreenBtn.innerHTML = '<i class="fas fa-expand"></i>';
                }
                isFullscreen = !isFullscreen;
            });

            closeBtn.addEventListener('click', () => {
                frame.remove();
                removeTaskbarApp(windowId, app);
                openWindows = openWindows.filter(w => w.id !== windowId);
                schedulePersistOpenWindowsSession();
                setTimeout(() => focusExplorerPrimaryElement(), 0);
            });

            bindTitlebarDragToMoveWindow(frame, titlebar, {
                getIsMaximized: () => isMaximized,
                getSnapUi: () => ({ resizeHandle, maximizeBtn }),
                onSnapResult: (snap) => { if (snap) isMaximized = !!snap.maximized; },
                schedulePersist: schedulePersistOpenWindowsSession,
            });

            frame.addEventListener('mousedown', () => {
                frame.style.zIndex = ++windowZIndex;
                const appGroup = activeTaskbarApps.find(a => a.appId === getAppStableId(app));
                if (appGroup) {
                    appGroup.activeWindowId = windowId;
                    const windowObj = appGroup.windows.find(w => w.id === windowId);
                    if (windowObj) windowObj.isMinimized = false;
                    renderTaskbarApps();
                }
                schedulePersistOpenWindowsSession();
                setTimeout(() => focusExplorerPrimaryElement(), 0);
            });

            if (restoreState) {
                if (restoreState.savedRect) {
                    frame.dataset.savedTop = restoreState.savedRect.top || '';
                    frame.dataset.savedLeft = restoreState.savedRect.left || '';
                    frame.dataset.savedWidth = restoreState.savedRect.width || '';
                    frame.dataset.savedHeight = restoreState.savedRect.height || '';
                }
                if (restoreState.zIndex) frame.style.zIndex = String(restoreState.zIndex);
                if (!restoreState.maximized) {
                    isMaximized = false;
                    frame.classList.remove('maximized');
                    if (restoreState.top) frame.style.top = restoreState.top;
                    if (restoreState.left) frame.style.left = restoreState.left;
                    if (restoreState.width) frame.style.width = restoreState.width;
                    if (restoreState.height) frame.style.height = restoreState.height;
                    maximizeBtn.innerHTML = '<i class="fas fa-square"></i>';
                    resizeHandle.style.display = 'block';
                } else {
                    isMaximized = true;
                    frame.classList.add('maximized');
                    maximizeBtn.innerHTML = '<i class="fas fa-window-restore"></i>';
                    resizeHandle.style.display = 'none';
                }
                if (restoreState.minimized) {
                    frame.classList.add('minimized');
                    frame.style.display = 'none';
                }
            }

            openWindows.push({ id: windowId, frame, app });
            addTaskbarApp(windowId, app);
            if (restoreState && restoreState.minimized) {
                const appGroup = activeTaskbarApps.find(a => a.appId === getAppStableId(app));
                if (appGroup) {
                    const windowObj = appGroup.windows.find(w => w.id === windowId);
                    if (windowObj) windowObj.isMinimized = true;
                    appGroup.activeWindowId = null;
                    renderTaskbarApps();
                }
            }
            schedulePersistOpenWindowsSession();
        }

        function initFileExplorerUI(host, ctx) {
            if (!host) return;

            let bucket = 'files'; // files | cache | trash | remote | community | department
            let relPath = ''; // local path for files/trash
            let feHomeLanding = true; // Inicio: accesos a espacios; false = dentro de Mis documentos
            let sharedDepartmentId = '';
            let explorerSharedSpaces = null;
            let remoteRelPath = ''; // remote path inside connection
            let remoteCtx = null; // { id, name, type }
            let lastNonTrashBucket = 'files';
            let items = [];
            let explorerDragPayload = null;
            let pathHistory = [''];
            let historyCursor = 0;
            let remotePathHistory = [''];
            let remoteHistoryCursor = 0;
            let viewMode = 'icons_md';
            let feDetailsSort = { col: 'name', dir: 'asc' };
            let fePropertiesVisible = false;
            const FE_VALID_VIEW_MODES = ['icons_xl', 'icons_lg', 'icons_md', 'icons_sm', 'list', 'details', 'content', 'tiles', 'compact', 'split_preview', 'split_preview_left', 'split_preview_top', 'split_preview_bottom', 'split_preview_hscroll', 'multimedia'];
            const selectedNames = new Set();
            /** Tras renombrar, `scheduleExplorerKeyboardFocus` prioriza este nombre aunque la selección apunte a otro índice. */
            let fePostRenderFocusItemName = null;
            const FE_EXPLORER_TYPEAHEAD_MS = 750;
            let feExplorerTypeAheadBuffer = '';
            let feExplorerTypeAheadTimer = null;
            function feResetExplorerTypeAhead() {
                feExplorerTypeAheadBuffer = '';
                if (feExplorerTypeAheadTimer != null) {
                    clearTimeout(feExplorerTypeAheadTimer);
                    feExplorerTypeAheadTimer = null;
                }
            }
            function feExplorerTypeAheadPrintableKey(e) {
                if (!e || e.ctrlKey || e.metaKey || e.altKey) return false;
                if (e.isComposing) return false;
                const k = e.key || '';
                if (k.length !== 1) return false;
                if (k < ' ' || k === '\x7f') return false;
                return true;
            }
            function feFindExplorerTypeAheadIndex(disp, buffer, startIdx) {
                const n = Array.isArray(disp) ? disp.length : 0;
                if (!n || !buffer) return -1;
                const b = buffer.toLowerCase();
                if (buffer.length === 1) {
                    for (let s = 0; s < n; s++) {
                        const i = (startIdx + 1 + s) % n;
                        const it = disp[i];
                        if (!it || !it.name) continue;
                        if (String(it.name).toLowerCase().startsWith(b)) return i;
                    }
                    return -1;
                }
                for (let i = 0; i < n; i++) {
                    const it = disp[i];
                    if (!it || !it.name) continue;
                    if (String(it.name).toLowerCase().startsWith(b)) return i;
                }
                return -1;
            }
            function feSyncSelectionAfterItemRename(oldName, newName) {
                if (!oldName || !newName || oldName === newName) return;
                if (!selectedNames.has(oldName)) return;
                selectedNames.delete(oldName);
                selectedNames.add(newName);
            }
            let lastSelIdx = 0;
            let keyboardSelAnchorIdx = null;
            let feClipboard = null;
            let marqueeState = null;
            let feExplorerSkipNextClick = false;
            // Navegación dentro de la papelera (sin restaurar)
            // null = raíz virtual (lista de elementos eliminados)
            let feTrashCtx = null; // { id: string, name: string }
            let feTrashRelPath = ''; // ruta dentro del elemento eliminado (solo si feTrashCtx != null)
            // Selección táctil / stylus (tablet / móvil)
            let feLastPointerType = 'mouse'; // 'mouse' | 'touch' | 'pen'
            let feTouchMultiSelectActive = false;
            let feTouchLongPressTimer = null;
            let feTouchLongPressFired = false;
            let feTouchLongPressPointerId = null;
            /** Modo selección múltiple desde la barra (mouse/teclado). */
            let feToolbarMultiSelectActive = false;
            let feSelectionPill = null;
            let feSelectionCount = null;
            let feSelectionExitBtn = null;
            /** Texto de búsqueda guardado para la pestaña Local (archivos/papelera). */
            let localTabSearchQuery = '';
            /** True cuando la búsqueda de la barra ya se ejecutó (no solo texto sin buscar). */
            let explorerToolbarSearchCommitted = false;
            /** Persistido al cambiar a remoto para restaurar al volver a Local. */
            let localTabSearchCommitted = false;
            let searchDebounceTimer = null;
            let treeFilterDebounceTimer = null;
            let treeFilterExpandSeq = 0;
            let searchRequestSeq = 0;
            /** Resultados de búsqueda recursiva (local vía API o remoto recorriendo carpetas). */
            let globalSearchRaw = null;
            let globalSearchActive = false;
            let globalSearchPending = false;
            let treeRefreshSeq = 0;
            let explorerBusy = false;
            let remoteNavSeq = 0;
            let localNavSeq = 0;
            let lastRenderedLocalState = { relPath: '', pathHistory: [''], historyCursor: 0 };
            let localNavCancelSnapshot = null;
            function abortExplorerPendingLoads() {
                localNavSeq++;
                remoteNavSeq++;
                setExplorerBusy(false);
            }
            let feActiveTab = 'local';
            /** Pestañas remotas independientes (misma conexión puede repetirse). */
            let remoteTabList = [];
            let activeRemoteTabId = null;

            // Persistencia cross-dispositivo: cuando cambie el estado interno del explorador,
            // persistimos el snapshot de ventanas abiertas en settings.json.
            let __explorerStateChangedTimer = null;
            function notifyExplorerStateChanged() {
                if (__restoringOpenWindowsSession) return;
                if (__explorerStateChangedTimer) clearTimeout(__explorerStateChangedTimer);
                __explorerStateChangedTimer = setTimeout(() => {
                    try { schedulePersistOpenWindowsSession(); } catch (_) {}
                }, 140);
            }

            function buildRemoteHistoryFromPath(p) {
                const safe = explorerNormalizeRelPath(p || '');
                const hist = [''];
                if (!safe) return { rel: '', history: hist, cursor: 0 };
                let acc = '';
                safe.split('/').filter(Boolean).forEach((seg) => {
                    acc = acc ? acc + '/' + seg : seg;
                    hist.push(acc);
                });
                return { rel: safe, history: hist, cursor: hist.length - 1 };
            }

            function buildLocalHistoryFromPath(p) {
                const safe = explorerNormalizeRelPath(p || '');
                const hist = [''];
                if (!safe) return { rel: '', history: hist, cursor: 0 };
                let acc = '';
                safe.split('/').filter(Boolean).forEach((seg) => {
                    acc = acc ? acc + '/' + seg : seg;
                    hist.push(acc);
                });
                return { rel: safe, history: hist, cursor: hist.length - 1 };
            }

            // Exponer API mínima al host para snapshot/restauración (usada por openWindowsSessionShared).
            host.__gridGetState = () => {
                try {
                    const tabs = (remoteTabList || []).map((t) => ({
                        connId: t.connId || '',
                        connName: t.connName || '',
                        connType: t.connType || '',
                        remoteRelPath: t.remoteRelPath || ''
                    })).filter((t) => !!t.connId);
                    const activeIdx = tabs.findIndex((t) => {
                        const rt = remoteTabList.find((x) => x.tabId === activeRemoteTabId);
                        return !!rt && rt.connId === t.connId && (rt.remoteRelPath || '') === (t.remoteRelPath || '');
                    });
                    return {
                        v: 1,
                        feActiveTab: feActiveTab,
                        bucket: bucket,
                        relPath: relPath,
                        feHomeLanding: !!(bucket === 'files' && !relPath && feHomeLanding),
                        departmentId: bucket === 'department' ? (sharedDepartmentId || '') : '',
                        lastNonTrashBucket: lastNonTrashBucket,
                        remoteTabs: tabs,
                        activeRemoteTabIndex: activeIdx
                    };
                } catch (_) {
                    return { v: 1 };
                }
            };

            host.__gridApplyState = (st) => {
                try {
                    if (!st || typeof st !== 'object') return;
                    // Restaurar local (por defecto).
                    const wantBucket = (['cache', 'trash', 'files', 'community', 'department'].includes(st.bucket)) ? st.bucket : 'files';
                    lastNonTrashBucket = (['remote', 'cache', 'files', 'community', 'department'].includes(st.lastNonTrashBucket)) ? st.lastNonTrashBucket : lastNonTrashBucket;
                    sharedDepartmentId = (wantBucket === 'department' && st.departmentId) ? String(st.departmentId) : '';

                    if (wantBucket === 'trash') {
                        bucket = 'trash';
                    } else if (wantBucket === 'cache') {
                        bucket = 'cache';
                    } else if (wantBucket === 'community') {
                        bucket = 'community';
                    } else if (wantBucket === 'department') {
                        bucket = 'department';
                    } else {
                        bucket = 'files';
                    }

                    const localHist = buildLocalHistoryFromPath(st.relPath || '');
                    relPath = localHist.rel;
                    pathHistory = localHist.history;
                    historyCursor = localHist.cursor;
                    lastRenderedLocalState = { relPath, pathHistory: [...pathHistory], historyCursor };
                    feHomeLanding = wantBucket === 'files' && !relPath && !!st.feHomeLanding;

                    // Restaurar pestañas remotas (FTP/WebDAV).
                    const list = Array.isArray(st.remoteTabs) ? st.remoteTabs : [];
                    remoteTabList = [];
                    activeRemoteTabId = null;
                    if (list.length) {
                        list.forEach((t) => {
                            const cid = t && t.connId ? String(t.connId) : '';
                            if (!cid) return;
                            const def = findExplorerConnectionByIdConn(cid);
                            if (!def) return;
                            const rh = buildRemoteHistoryFromPath(t.remoteRelPath || '');
                            const tabId = 'rt_restore_' + Date.now() + '_' + Math.random().toString(36).slice(2, 8);
                            remoteTabList.push({
                                tabId,
                                connId: cid,
                                connName: (t.connName || def.name || cid || '').trim() || cid,
                                connType: t.connType || def.type || '',
                                remoteRelPath: rh.rel,
                                remotePathHistory: rh.history,
                                remoteHistoryCursor: rh.cursor,
                                searchQuery: '',
                                searchCommitted: false
                            });
                        });
                    }

                    // Activar pestaña correcta.
                    const wantActive = st.feActiveTab || 'local';
                    renderExplorerTabs();
                    if (wantActive !== 'local' && remoteTabList.length) {
                        const idx = (typeof st.activeRemoteTabIndex === 'number' && st.activeRemoteTabIndex >= 0)
                            ? Math.min(st.activeRemoteTabIndex, remoteTabList.length - 1)
                            : (remoteTabList.length - 1);
                        activateRemoteTab(remoteTabList[idx].tabId);
                    } else {
                        feActiveTab = 'local';
                        if (bucket !== 'trash' && bucket !== 'cache' && bucket !== 'files' && bucket !== 'community' && bucket !== 'department') bucket = 'files';
                        syncExplorerActiveContext();
                        renderFilesView({ refreshTree: true });
                    }
                    notifyExplorerStateChanged();
                } catch (_) {}
            };

            if (ctx && ctx.initialTarget) {
                const it = ctx.initialTarget;
                if (it.bucket === 'trash') {
                    feHomeLanding = false;
                    bucket = 'trash';
                } else if (it.bucket === 'cache') {
                    feHomeLanding = false;
                    bucket = 'cache';
                    relPath = it.relPath || '';
                    pathHistory = [''];
                    let acc = '';
                    if (relPath) {
                        relPath.split('/').filter(Boolean).forEach((p) => {
                            acc = acc ? acc + '/' + p : p;
                            pathHistory.push(acc);
                        });
                    }
                    historyCursor = pathHistory.length - 1;
                } else if (it.bucket === 'community') {
                    feHomeLanding = false;
                    bucket = 'community';
                    sharedDepartmentId = '';
                    relPath = it.relPath || '';
                    pathHistory = [''];
                    let accC = '';
                    if (relPath) {
                        relPath.split('/').filter(Boolean).forEach((p) => {
                            accC = accC ? accC + '/' + p : p;
                            pathHistory.push(accC);
                        });
                    }
                    historyCursor = pathHistory.length - 1;
                } else if (it.bucket === 'department') {
                    feHomeLanding = false;
                    bucket = 'department';
                    sharedDepartmentId = it.departmentId ? String(it.departmentId) : '';
                    relPath = it.relPath || '';
                    pathHistory = [''];
                    let accD = '';
                    if (relPath) {
                        relPath.split('/').filter(Boolean).forEach((p) => {
                            accD = accD ? accD + '/' + p : p;
                            pathHistory.push(accD);
                        });
                    }
                    historyCursor = pathHistory.length - 1;
                } else if (it.bucket === 'files') {
                    feHomeLanding = !(it.relPath || '');
                    relPath = it.relPath || '';
                    pathHistory = [''];
                    let acc2 = '';
                    if (relPath) {
                        relPath.split('/').filter(Boolean).forEach((p) => {
                            acc2 = acc2 ? acc2 + '/' + p : p;
                            pathHistory.push(acc2);
                        });
                    }
                    historyCursor = pathHistory.length - 1;
                }
            }

            function syncExplorerActiveContext() {
                try {
                    window.__gridExplorerActiveContext = {
                        bucket: localListBucketName(),
                        departmentId: bucket === 'department' ? (sharedDepartmentId || '') : ''
                    };
                } catch (_) {}
            }

            function activeDepartmentId() {
                return bucket === 'department' ? (sharedDepartmentId || '') : '';
            }

            function fetchExplorerListActive(path) {
                return fetchExplorerList(localListBucketName(), path, activeDepartmentId());
            }

            async function loadExplorerSharedSpaces(force) {
                if (!window.GridExplorerSharedSpaces || typeof window.GridExplorerSharedSpaces.fetch !== 'function') {
                    explorerSharedSpaces = null;
                    return null;
                }
                try {
                    explorerSharedSpaces = await window.GridExplorerSharedSpaces.fetch(!!force);
                } catch (_) {
                    explorerSharedSpaces = null;
                }
                return explorerSharedSpaces;
            }

            function communityDisplayName() {
                const n = explorerSharedSpaces && explorerSharedSpaces.community && explorerSharedSpaces.community.displayName;
                return (n && String(n).trim()) || 'Espacio comunitario';
            }

            function buildSharedSpaceShortcutItems() {
                const out = [{
                    name: '__fe_shortcut:files',
                    type: 'dir',
                    mimeType: '',
                    size: 0,
                    mtime: 0,
                    __sharedShortcut: true,
                    __sharedBucket: 'files',
                    __departmentId: '',
                    __shortcutLabel: tr('explorerSidebarTitle'),
                    __shortcutSub: tr('explorerSharedSpaceFilesKind'),
                    __shortcutIcon: 'fa-hdd'
                }];
                if (!explorerSharedSpaces) return out;
                if (explorerSharedSpaces.community && explorerSharedSpaces.community.visible) {
                    out.push({
                        name: '__fe_shortcut:community',
                        type: 'dir',
                        mimeType: '',
                        size: 0,
                        mtime: 0,
                        __sharedShortcut: true,
                        __sharedBucket: 'community',
                        __departmentId: '',
                        __shortcutLabel: communityDisplayName(),
                        __shortcutSub: tr('explorerSharedSpaceCommunityKind'),
                        __shortcutIcon: 'fa-users'
                    });
                }
                const deptList = (explorerSharedSpaces.departments && explorerSharedSpaces.departments.visibleList) || [];
                for (const dept of deptList) {
                    if (!dept || !dept.id) continue;
                    out.push({
                        name: '__fe_shortcut:department:' + dept.id,
                        type: 'dir',
                        mimeType: '',
                        size: 0,
                        mtime: 0,
                        __sharedShortcut: true,
                        __sharedBucket: 'department',
                        __departmentId: dept.id,
                        __shortcutLabel: dept.name || tr('explorerSharedSpaceDepartmentKind'),
                        __shortcutSub: tr('explorerSharedSpaceDepartmentKind'),
                        __shortcutIcon: 'fa-building'
                    });
                }
                return out;
            }

            function appendSharedSpaceShortcutsIfRoot(list) {
                if (bucket !== 'files' || relPath) return list;
                if (globalSearchActive || globalSearchPending) return list;
                const shortcuts = buildSharedSpaceShortcutItems();
                if (!shortcuts.length) return list;
                const sq = feSearchInput ? (feSearchInput.value || '').trim().toLowerCase() : '';
                const filtered = sq
                    ? shortcuts.filter((it) => {
                        const label = (it.__shortcutLabel || '').toLowerCase();
                        const sub = (it.__shortcutSub || '').toLowerCase();
                        return label.includes(sq) || sub.includes(sq);
                    })
                    : shortcuts;
                if (feHomeLanding) return filtered;
                return list;
            }

            function feIsSharedShortcutItem(item) {
                return !!(item && item.__sharedShortcut);
            }

            function feFilterDeletableLocalRows(disp, rows) {
                return (rows || []).filter((it) => {
                    const row = (disp || []).find((x) => x && x.name === it.name);
                    return row && !feIsSharedShortcutItem(row);
                });
            }

            function feGetEffectiveSelectedNames(fallbackName) {
                const fromDom = itemsEl
                    ? Array.from(itemsEl.querySelectorAll('.explorer-item.explorer-item-selected'))
                        .map((el) => el.dataset.name)
                        .filter(Boolean)
                    : [];
                const selectedSet = new Set(fromDom.length ? fromDom : Array.from(selectedNames));
                if (selectedSet.size && fallbackName && selectedSet.has(fallbackName)) {
                    return Array.from(selectedSet);
                }
                if (selectedSet.size) return Array.from(selectedSet);
                return fallbackName ? [fallbackName] : [];
            }

            function feResolveLocalDeleteRows(disp, fallbackName) {
                const names = feGetEffectiveSelectedNames(fallbackName);
                const rows = names.map((n) => {
                    const row = (disp || []).find((x) => x && x.name === n);
                    if (!row) return null;
                    const rp = row.__fromGlobal ? row.__parentPath : relPath;
                    return { path: rp, name: n };
                }).filter(Boolean);
                return feFilterDeletableLocalRows(disp, rows);
            }

            function feResolveRemoteDeleteRows(disp, fallbackName) {
                const names = feGetEffectiveSelectedNames(fallbackName);
                return names.map((n) => {
                    const row = (disp || []).find((x) => x && x.name === n);
                    if (!row) return null;
                    const rp = row.__fromGlobal ? (row.__parentPath ?? '') : (remoteRelPath || '');
                    return { path: rp, name: n, type: row.type || 'file' };
                }).filter(Boolean);
            }

            function feLocalDeleteNavigateAfterRows(rows) {
                for (const it of rows || []) {
                    const delFull = normalizeJoin(it.path, it.name);
                    if (relPath === delFull || (relPath && String(relPath).startsWith(delFull + '/'))) {
                        navigateToPath(it.path || '', { fromTree: false });
                        return true;
                    }
                }
                return false;
            }

            async function feDeleteLocalRowsToTrash(rows, lb) {
                if (!rows || !rows.length) return false;
                const msg = rows.length === 1
                    ? tr('explorerConfirmDeleteToTrash')
                    : tr('explorerConfirmDeleteTrashMany').replace('%n', String(rows.length));
                if (!(await appDialog.confirm(msg))) return false;
                const results = await Promise.all(rows.map((it) => explorerDelete(it.path, it.name, lb).then((r) => ({ r, it }))));
                results.forEach(({ r, it }) => {
                    if (r && r.success) logActivity('explorer_delete_to_trash', { path: it.path, name: it.name });
                });
                selectedNames.clear();
                if (!feLocalDeleteNavigateAfterRows(rows)) renderFilesView();
                renderTrashItems();
                fetchExplorerListTrash();
                updateRecycleBinIcon();
                return true;
            }

            async function feDeleteLocalRowsPermanent(rows, lb) {
                if (!rows || !rows.length) return false;
                const msg = rows.length === 1
                    ? tr('explorerConfirmDeletePermanent')
                    : tr('explorerConfirmDeletePermanentMany').replace('%n', String(rows.length));
                if (!(await appDialog.confirm(msg))) return false;
                const results = await Promise.all(rows.map((it) => explorerDeletePermanent(it.path, it.name, lb).then((r) => ({ r, it }))));
                results.forEach(({ r, it }) => {
                    if (r && r.success) logActivity('explorer_delete_permanent', { path: it.path, name: it.name });
                });
                selectedNames.clear();
                if (!feLocalDeleteNavigateAfterRows(rows)) renderFilesView();
                return true;
            }

            async function feDeleteRemoteRows(rows, connId, options = {}) {
                if (!rows || !rows.length || !connId) return false;
                const permanent = !!options.permanent;
                const dirs = rows.filter((it) => it.type === 'dir');
                const files = rows.filter((it) => it.type !== 'dir');
                if (dirs.length && !(await appDialog.confirm(tr('explorerConfirmDeleteRemoteDir')))) return false;
                if (files.length) {
                    const msg = files.length === 1
                        ? (permanent ? tr('explorerConfirmDeletePermanent') : tr('explorerConfirmDeleteToTrash'))
                        : (permanent ? tr('explorerConfirmDeletePermanentMany') : tr('explorerConfirmDeleteTrashMany')).replace('%n', String(files.length));
                    if (!(await appDialog.confirm(msg))) return false;
                }
                for (const it of dirs) {
                    const r = await explorerConnDelete(connId, it.path, it.name, 'dir');
                    if (!r || !r.success) {
                        if (r && r.error) showToast(String(r.error));
                        return false;
                    }
                }
                for (const it of files) {
                    const r = permanent
                        ? await explorerConnDelete(connId, it.path, it.name, 'file')
                        : await explorerConnDeleteToTrash(connId, it.path, it.name);
                    if (!r || !r.success) {
                        if (r && r.error) showToast(String(r.error));
                        return false;
                    }
                }
                selectedNames.clear();
                if (!permanent && files.length) {
                    renderTrashItems();
                    fetchExplorerListTrash();
                    updateRecycleBinIcon();
                }
                renderRemoteView();
                return true;
            }

            function navigateToExplorerHome(options = {}) {
                feActiveTab = 'local';
                if (bucket === 'trash') {
                    bucket = lastNonTrashBucket || 'files';
                }
                bucket = 'files';
                sharedDepartmentId = '';
                feHomeLanding = true;
                relPath = '';
                pathHistory = [''];
                historyCursor = 0;
                selectedNames.clear();
                clearExplorerGlobalSearch();
                syncExplorerActiveContext();
                if (options && options.fromTree) updateSidebarActivePath();
                renderFilesView({ refreshTree: !(options && options.fromTree) });
                maybeCloseExplorerSidebar();
                notifyExplorerStateChanged();
            }

            function navigateToBreadcrumbSpaceRoot(targetBucket, deptId) {
                const b = targetBucket || bucket;
                if (b === 'community') navigateToSharedRoot('community', '', {});
                else if (b === 'department') navigateToSharedRoot('department', deptId || sharedDepartmentId || '', {});
                else navigateToSharedRoot('files', '', {});
            }

            async function executeExplorerDropOnShortcut(shortcut, names, isCopy) {
                if (!shortcut || !shortcut.__sharedShortcut) return false;
                const prevBucket = bucket;
                const prevDept = sharedDepartmentId;
                const prevHome = feHomeLanding;
                feHomeLanding = false;
                if (shortcut.__sharedBucket === 'department') {
                    bucket = 'department';
                    sharedDepartmentId = shortcut.__departmentId || '';
                } else if (shortcut.__sharedBucket === 'community') {
                    bucket = 'community';
                    sharedDepartmentId = '';
                } else {
                    bucket = 'files';
                    sharedDepartmentId = '';
                }
                syncExplorerActiveContext();
                const ok = await executeExplorerDropToPath('', names, isCopy);
                if (!ok) {
                    bucket = prevBucket;
                    sharedDepartmentId = prevDept;
                    feHomeLanding = prevHome;
                    syncExplorerActiveContext();
                }
                await renderFilesView();
                return ok;
            }

            function openSharedSpaceShortcut(item) {
                if (!item || !item.__sharedShortcut) return;
                feHomeLanding = false;
                if (item.__sharedBucket === 'files') {
                    navigateToSharedRoot('files', '', {});
                } else if (item.__sharedBucket === 'community') {
                    navigateToSharedRoot('community', '', {});
                } else if (item.__sharedBucket === 'department') {
                    navigateToSharedRoot('department', item.__departmentId || '', {});
                }
            }

            function navigateToSharedRoot(targetBucket, deptId, options) {
                feActiveTab = 'local';
                if (bucket === 'trash') {
                    bucket = lastNonTrashBucket || 'files';
                }
                feHomeLanding = false;
                bucket = targetBucket;
                if (targetBucket === 'department') {
                    sharedDepartmentId = deptId || '';
                } else {
                    sharedDepartmentId = '';
                }
                lastNonTrashBucket = targetBucket;
                relPath = '';
                pathHistory = [''];
                historyCursor = 0;
                selectedNames.clear();
                clearExplorerGlobalSearch();
                syncExplorerActiveContext();
                if (options && options.fromTree) updateSidebarActivePath();
                renderFilesView({ refreshTree: !(options && options.fromTree) });
                maybeCloseExplorerSidebar();
                notifyExplorerStateChanged();
            }

            host.__gridRefreshSharedSpaces = async function () {
                await loadExplorerSharedSpaces(true);
                await refreshSidebarTree();
                if (bucket === 'community' || bucket === 'department' || (bucket === 'files' && !relPath)) {
                    await renderFilesView({ refreshTree: true });
                }
            };

            const isLocalLibraryBucket = () => ['files', 'cache', 'community', 'department'].includes(bucket);
            const isPersonalFilesBucket = () => bucket === 'files';
            const localListBucketName = () => {
                if (bucket === 'cache') return 'cache';
                if (bucket === 'community') return 'community';
                if (bucket === 'department') return 'department';
                return 'files';
            };

            host.innerHTML = `
                <div class="fe-root">
                <div class="fe-body">
                <div class="fe-sidebar-backdrop" id="feSidebarBackdrop_${ctx.windowId}" aria-hidden="true"></div>
                <aside class="fe-sidebar" id="feSidebar_${ctx.windowId}">
                    <div class="fe-sidebar-head-row">
                        <div class="fe-sidebar-head">${tr('explorerSidebarTitle')}</div>
                        <button type="button" class="fe-sidebar-close-btn" title="${tr('explorerSidebarClose')}" aria-label="${tr('explorerSidebarClose')}"><i class="fas fa-chevron-left"></i></button>
                    </div>
                    <div class="fe-tree-filter-row">
                        <input type="search" class="fe-tree-filter-input" id="feTreeFilter_${ctx.windowId}" placeholder="${tr('explorerTreeFilterPlaceholder')}" autocomplete="off" aria-label="${tr('explorerTreeFilterPlaceholder')}">
                        <button type="button" class="fe-tree-filter-clear" title="${tr('explorerSearchClear')}" aria-label="${tr('explorerSearchClear')}">&times;</button>
                    </div>
                    <div class="fe-tree" id="feTree_${ctx.windowId}" role="tree"></div>
                </aside>
                <div class="fe-main">
                <div class="explorer-toolbar fe-toolbar">
                    <button type="button" class="fe-sidebar-toggle" title="${tr('explorerSidebarOpen')}" aria-label="${tr('explorerSidebarOpen')}"><i class="fas fa-bars"></i></button>
                    <button type="button" class="fe-toolbar-more-btn" id="feToolbarMore_${ctx.windowId}" title="${tr('explorerToolbarMoreActions')}" aria-label="${tr('explorerToolbarMoreActions')}" aria-expanded="false" aria-controls="feToolbarActions_${ctx.windowId}"><i class="fas fa-ellipsis-h"></i></button>
                    <div class="fe-toolbar-actions-cluster" id="feToolbarActions_${ctx.windowId}">
                    <button type="button" class="fe-back-btn" title="${tr('explorerBack')}" disabled><i class="fas fa-arrow-left"></i></button>
                    <button type="button" class="fe-fwd-btn" title="${tr('explorerForward')}" disabled><i class="fas fa-arrow-right"></i></button>
                    <button type="button" class="explorer-up-btn" title="${tr('explorerUp')}"><i class="fas fa-level-up-alt"></i></button>
                    <button type="button" class="explorer-refresh-btn" title="${tr('explorerRefresh')}"><i class="fas fa-sync"></i></button>
                    <button type="button" class="explorer-newfolder-btn" title="${tr('explorerNewFolder')}"><i class="fas fa-folder-plus"></i></button>
                    <button type="button" class="explorer-upload-btn" title="${tr('explorerUploadFiles')}"><i class="fas fa-upload"></i></button>
                    <button type="button" class="explorer-trash-toggle-btn" title="${tr('explorerTrash')}"><i class="fas fa-trash-restore"></i></button>
                    <button type="button" class="explorer-connections-btn" title="${tr('explorerConnections')}"><i class="fas fa-network-wired"></i></button>
                    <button type="button" class="fe-multi-select-btn" title="${tr('explorerMultiSelectMode')}" aria-label="${tr('explorerMultiSelectMode')}" aria-pressed="false"><i class="fas fa-check-double"></i></button>
                    <div class="fe-selection-pill" hidden>
                        <span class="fe-selection-count">0</span>
                        <button type="button" class="fe-selection-exit" title="${tr('explorerMultiSelectExit')}" aria-label="${tr('explorerMultiSelectExit')}">&times;</button>
                    </div>
                    <div class="fe-view-menu-wrap">
                        <button type="button" class="fe-view-menu-btn" title="${tr('explorerViewMenu')}" aria-haspopup="true" aria-expanded="false"><i class="fas fa-th-large"></i></button>
                        <div class="fe-view-menu-dropdown" id="feViewMenu_${ctx.windowId}" hidden role="menu"></div>
                    </div>
                    <button type="button" class="fe-properties-btn" title="${tr('explorerProperties')}" aria-label="${tr('explorerProperties')}"><i class="fas fa-info-circle"></i></button>
                    <button type="button" class="fe-theme-btn" title="Personalizar explorador" aria-label="Personalizar explorador"><i class="fas fa-cog"></i></button>
                    <button type="button" class="fe-toolbar-pin-btn" title="${tr('explorerToolbarPinBar')}" aria-label="${tr('explorerToolbarPinBar')}" aria-pressed="false"><i class="fas fa-thumbtack"></i></button>
                    </div>
                    <div class="fe-toolbar-search-cluster">
                    <div class="fe-search-wrap">
                        <input type="search" class="fe-search-input" placeholder="${tr('explorerSearchPlaceholder')}" title="${tr('explorerSearchRecursiveHint')}" autocomplete="off" aria-label="${tr('explorerSearchPlaceholder')}">
                    </div>
                    <input type="file" id="explorerUploadInput_${ctx.windowId}" style="display:none;" multiple accept=".pdf,.doc,.docx,.xls,.xlsx,.ppt,.pptx,.txt,.jpg,.jpeg,.png,.gif,.webp,.mp4,.webm,.ogg,.mp3,.wav">
                    </div>
                </div>
                <div class="fe-tabs" id="feTabs_${ctx.windowId}" role="tablist" aria-label="Explorador"></div>
                <div class="fe-path-row">
                    <div class="fe-breadcrumb" id="feBC_${ctx.windowId}"></div>
                    <div class="explorer-path" id="explorerPath_${ctx.windowId}" style="display:none;">/</div>
                </div>
                <div class="fe-items-wrap" id="feItemsWrap_${ctx.windowId}">
                    <div class="fe-explorer-stage" id="feExplorerStage_${ctx.windowId}">
                        <div class="fe-explorer-main-pane" id="feExplorerMainPane_${ctx.windowId}">
                            <div class="explorer-items" id="explorerItems_${ctx.windowId}" tabindex="0"></div>
                        </div>
                        <div class="fe-split-resizer" id="feSplitResizer_${ctx.windowId}" hidden aria-hidden="true" title=""></div>
                        <div class="fe-preview-pane" id="fePreviewPane_${ctx.windowId}" hidden>
                            <div class="fe-preview-body" id="fePreviewBody_${ctx.windowId}"></div>
                            <div class="fe-preview-head">
                                <div class="fe-preview-head-titles">
                                    <span class="fe-preview-title">${tr('explorerPreview')}</span>
                                    <span class="fe-preview-filename" id="fePreviewFileName_${ctx.windowId}" hidden></span>
                                </div>
                            </div>
                            <div class="fe-preview-toolbar" id="fePreviewToolbar_${ctx.windowId}" role="toolbar" aria-label="${escapeHtmlAttr(tr('explorerPreview'))}" hidden>
                                <button type="button" class="fe-preview-tb-btn" data-fe-split-action="zoom-in" title="${escapeHtmlAttr(tr('zoomIn'))}" aria-label="${escapeHtmlAttr(tr('zoomIn'))}"><i class="fas fa-search-plus"></i></button>
                                <button type="button" class="fe-preview-tb-btn" data-fe-split-action="zoom-out" title="${escapeHtmlAttr(tr('zoomOut'))}" aria-label="${escapeHtmlAttr(tr('zoomOut'))}"><i class="fas fa-search-minus"></i></button>
                                <button type="button" class="fe-preview-tb-btn" data-fe-split-action="zoom-reset" title="${escapeHtmlAttr(tr('zoomReset'))}" aria-label="${escapeHtmlAttr(tr('zoomReset'))}"><i class="fas fa-undo"></i></button>
                                <span class="fe-preview-tb-sep" aria-hidden="true"></span>
                                <button type="button" class="fe-preview-tb-btn" data-fe-split-action="share" title="${escapeHtmlAttr(tr('share'))}" aria-label="${escapeHtmlAttr(tr('share'))}"><i class="fas fa-share-alt"></i></button>
                                <button type="button" class="fe-preview-tb-btn" data-fe-split-action="download" title="${escapeHtmlAttr(tr('download'))}" aria-label="${escapeHtmlAttr(tr('download'))}"><i class="fas fa-download"></i></button>
                                <button type="button" class="fe-preview-tb-btn" data-fe-split-action="external" title="${escapeHtmlAttr(tr('openNewWindow'))}" aria-label="${escapeHtmlAttr(tr('openNewWindow'))}"><i class="fas fa-external-link-alt"></i></button>
                                <button type="button" class="fe-preview-tb-btn" data-fe-split-action="print" title="${escapeHtmlAttr(tr('print'))}" hidden><i class="fas fa-print"></i></button>
                                <span class="fe-preview-tb-sep fe-preview-tb-sep--html" aria-hidden="true" hidden></span>
                                <button type="button" class="fe-preview-tb-btn" data-fe-split-action="html-toggle" title="${escapeHtmlAttr(tr('explorerHtmlPreviewShowCode'))}" aria-label="${escapeHtmlAttr(tr('explorerHtmlPreviewShowCode'))}" aria-pressed="false" hidden><i class="fas fa-code"></i></button>
                            </div>
                        </div>
                    </div>
                    <aside class="fe-properties-panel" id="fePropertiesPanel_${ctx.windowId}" hidden aria-label="${tr('explorerProperties')}">
                        <div class="fe-properties-head">
                            <span>${tr('explorerProperties')}</span>
                            <button type="button" class="fe-properties-close" title="${tr('closeWindow')}" aria-label="${tr('closeWindow')}">&times;</button>
                        </div>
                        <div class="fe-properties-body" id="fePropertiesBody_${ctx.windowId}"></div>
                    </aside>
                </div>
                <div class="explorer-upload-list" id="explorerUploadList_${ctx.windowId}"></div>
                </div>
                </div>
                <div class="explorer-connections-panel" id="explorerConnectionsPanel_${ctx.windowId}" style="display:none;">
                    <div class="explorer-connections-header">
                        <div class="explorer-connections-title">${tr('explorerConnections')}</div>
                        <button type="button" class="explorer-connections-close-btn" id="explorerConnectionsCloseBtn_${ctx.windowId}">&times;</button>
                    </div>
                    <div class="explorer-connections-list" id="explorerConnectionsList_${ctx.windowId}"></div>
                    <div class="explorer-connections-actions">
                        <button type="button" class="explorer-add-webdav-btn" id="explorerAddWebdavBtn_${ctx.windowId}">${tr('explorerAddWebdav')}</button>
                        <button type="button" class="explorer-add-ftp-btn" id="explorerAddFtpBtn_${ctx.windowId}">${tr('explorerAddFtp')}</button>
                    </div>
                </div>
                </div>
            `;

            try {
                refreshThemeContrast();
                requestAnimationFrame(() => refreshThemeContrast());
            } catch (_) {}

            const pathEl = host.querySelector(`#explorerPath_${ctx.windowId}`);
            const itemsEl = host.querySelector(`#explorerItems_${ctx.windowId}`);
            const uploadInput = host.querySelector(`#explorerUploadInput_${ctx.windowId}`);
            const uploadListEl = host.querySelector(`#explorerUploadList_${ctx.windowId}`);
            // Guardar tipo de puntero más reciente para condicionar clicks (touch vs mouse vs pen)
            if (itemsEl && typeof window.PointerEvent !== 'undefined') {
                itemsEl.addEventListener('pointerdown', (e) => {
                    try {
                        if (e && e.pointerType) feLastPointerType = e.pointerType;
                    } catch (_) {}
                }, true);
            }

            const connectionsBtn = host.querySelector('.explorer-connections-btn');
            const connectionsPanel = host.querySelector(`#explorerConnectionsPanel_${ctx.windowId}`);
            const connectionsListEl = host.querySelector(`#explorerConnectionsList_${ctx.windowId}`);
            const closeConnectionsBtn = host.querySelector(`#explorerConnectionsCloseBtn_${ctx.windowId}`);
            const addWebdavBtn = host.querySelector(`#explorerAddWebdavBtn_${ctx.windowId}`);
            const addFtpBtn = host.querySelector(`#explorerAddFtpBtn_${ctx.windowId}`);
            const feBC = host.querySelector(`#feBC_${ctx.windowId}`);
            const feTree = host.querySelector(`#feTree_${ctx.windowId}`);
            const feSidebar = host.querySelector(`#feSidebar_${ctx.windowId}`);
            const feRoot = host.querySelector('.fe-root');
            const feSidebarBackdrop = host.querySelector(`#feSidebarBackdrop_${ctx.windowId}`);
            const feSidebarToggle = host.querySelector('.fe-sidebar-toggle');
            const feToolbarMoreBtn = host.querySelector('.fe-toolbar-more-btn');
            const feToolbarPinBtn = host.querySelector('.fe-toolbar-pin-btn');
            const FE_MTOOLBAR_PIN_KEY = 'grid_explorer_toolbar_mobile_pinned_v1';
            const mqFeMobileToolbar = typeof window.matchMedia === 'function' ? window.matchMedia('(max-width: 768px)') : { matches: false, addEventListener: () => {}, removeEventListener: () => {} };
            function feMobileToolbarReadPinned() {
                try {
                    return localStorage.getItem(FE_MTOOLBAR_PIN_KEY) === '1';
                } catch (_) {
                    return false;
                }
            }
            function feMobileToolbarWritePinned(on) {
                try {
                    if (on) localStorage.setItem(FE_MTOOLBAR_PIN_KEY, '1');
                    else localStorage.removeItem(FE_MTOOLBAR_PIN_KEY);
                } catch (_) {}
            }
            function syncFeMobileToolbarUi() {
                if (!feRoot) return;
                const mobile = mqFeMobileToolbar.matches;
                const pinned = mobile && feMobileToolbarReadPinned();
                feRoot.classList.toggle('fe-mtoolbar-pinned', pinned);
                const expanded = feRoot.classList.contains('fe-mtoolbar-expanded');
                if (feToolbarMoreBtn) {
                    const showMore = mobile && !pinned;
                    feToolbarMoreBtn.hidden = !showMore;
                    feToolbarMoreBtn.setAttribute('aria-expanded', mobile && (pinned || expanded) ? 'true' : 'false');
                    const ic = feToolbarMoreBtn.querySelector('i');
                    if (ic) ic.className = expanded && !pinned ? 'fas fa-chevron-up' : 'fas fa-ellipsis-h';
                    feToolbarMoreBtn.title = expanded && !pinned ? tr('explorerToolbarCollapseActions') : tr('explorerToolbarMoreActions');
                    feToolbarMoreBtn.setAttribute('aria-label', feToolbarMoreBtn.title);
                }
                if (feToolbarPinBtn) {
                    feToolbarPinBtn.hidden = !mobile;
                    feToolbarPinBtn.setAttribute('aria-pressed', pinned ? 'true' : 'false');
                    feToolbarPinBtn.title = pinned ? tr('explorerToolbarUnpinBar') : tr('explorerToolbarPinBar');
                    feToolbarPinBtn.setAttribute('aria-label', feToolbarPinBtn.title);
                }
            }
            if (feToolbarMoreBtn && feRoot) {
                feToolbarMoreBtn.addEventListener('click', (e) => {
                    e.stopPropagation();
                    if (!mqFeMobileToolbar.matches || feRoot.classList.contains('fe-mtoolbar-pinned')) return;
                    feRoot.classList.toggle('fe-mtoolbar-expanded');
                    syncFeMobileToolbarUi();
                });
            }
            if (feToolbarPinBtn && feRoot) {
                feToolbarPinBtn.addEventListener('click', (e) => {
                    e.stopPropagation();
                    if (!mqFeMobileToolbar.matches) return;
                    const next = !feMobileToolbarReadPinned();
                    feMobileToolbarWritePinned(next);
                    if (!next) feRoot.classList.remove('fe-mtoolbar-expanded');
                    syncFeMobileToolbarUi();
                });
            }
            if (typeof mqFeMobileToolbar.addEventListener === 'function') {
                mqFeMobileToolbar.addEventListener('change', () => {
                    if (!mqFeMobileToolbar.matches && feRoot) feRoot.classList.remove('fe-mtoolbar-expanded');
                    syncFeMobileToolbarUi();
                });
            } else if (typeof mqFeMobileToolbar.addListener === 'function') {
                mqFeMobileToolbar.addListener(() => {
                    if (!mqFeMobileToolbar.matches && feRoot) feRoot.classList.remove('fe-mtoolbar-expanded');
                    syncFeMobileToolbarUi();
                });
            }
            syncFeMobileToolbarUi();
            const feSidebarCloseBtn = host.querySelector('.fe-sidebar-close-btn');
            const feItemsWrap = host.querySelector(`#feItemsWrap_${ctx.windowId}`);
            const feBackBtn = host.querySelector('.fe-back-btn');
            const feFwdBtn = host.querySelector('.fe-fwd-btn');
            const feViewMenuBtn = host.querySelector('.fe-view-menu-btn');
            const feViewMenuDropdown = host.querySelector(`#feViewMenu_${ctx.windowId}`);
            const fePropertiesBtn = host.querySelector('.fe-properties-btn');
            const fePropertiesPanel = host.querySelector(`#fePropertiesPanel_${ctx.windowId}`);
            const fePropertiesBody = host.querySelector(`#fePropertiesBody_${ctx.windowId}`);
            const fePropertiesClose = host.querySelector(`#fePropertiesPanel_${ctx.windowId} .fe-properties-close`);
            const feExplorerStage = host.querySelector(`#feExplorerStage_${ctx.windowId}`);
            const feExplorerMainPane = host.querySelector(`#feExplorerMainPane_${ctx.windowId}`) || host.querySelector('.fe-explorer-main-pane');
            const feSplitResizer = host.querySelector(`#feSplitResizer_${ctx.windowId}`);
            const fePreviewPane = host.querySelector(`#fePreviewPane_${ctx.windowId}`);
            const fePreviewBody = host.querySelector(`#fePreviewBody_${ctx.windowId}`);
            const fePreviewToolbar = host.querySelector(`#fePreviewToolbar_${ctx.windowId}`);
            const fePreviewFileName = host.querySelector(`#fePreviewFileName_${ctx.windowId}`);
            const feThemeBtn = host.querySelector('.fe-theme-btn');
            const feMultiSelectBtn = host.querySelector('.fe-multi-select-btn');
            feSelectionPill = host.querySelector('.fe-selection-pill');
            feSelectionCount = host.querySelector('.fe-selection-pill .fe-selection-count');
            feSelectionExitBtn = host.querySelector('.fe-selection-pill .fe-selection-exit');
            const feSearchInput = host.querySelector('.fe-search-input');
            const feSearchClearBtn = host.querySelector('.fe-search-clear');
            const feTreeFilterInput = host.querySelector('.fe-tree-filter-input');
            const feTreeFilterClearBtn = host.querySelector('.fe-tree-filter-clear');
            const feTabsEl = host.querySelector(`#feTabs_${ctx.windowId}`);
            const VIEW_MODE_KEY = 'grid_explorer_view_mode_v2';
            const LEGACY_VIEW_KEY = 'grid_explorer_view_mode_v1';
            const DETAILS_SORT_KEY = 'grid_explorer_details_sort_v1';
            const PROPS_KEY = 'grid_explorer_props_open_v1';
            const SPLIT_PREVIEW_W_KEY = 'grid_explorer_split_preview_w_v1';
            const SPLIT_RATIO_STORE_KEY = 'grid_explorer_split_ratios_v1';
            /** Fracción mínima del escenario para la vista previa (panel derecho/izquierdo según modo). */
            const SPLIT_MIN_PREVIEW_RATIO = 0.25;
            /** Fracción mínima del escenario para la lista de archivos (panel principal). */
            const SPLIT_MIN_NAV_RATIO = 0.15;
            const SPLIT_RESIZER_PX = 8;
            const SPLIT_DEFAULT_RATIO = 0.50;
            let feSplitPreviewDrag = null;
            const FE_SPLIT_MODES = ['split_preview', 'split_preview_left', 'split_preview_top', 'split_preview_bottom', 'split_preview_hscroll'];
            const FE_SPLIT_ORIENT_CLASSES = ['fe-split-orient-right', 'fe-split-orient-left', 'fe-split-orient-top', 'fe-split-orient-bottom', 'fe-split-orient-hscroll'];
            function isFeSplitMode(vm) {
                return FE_SPLIT_MODES.includes(vm);
            }
            function feSplitOrientation(vm) {
                if (vm === 'split_preview' || vm === 'split_preview_left') return 'row';
                return 'column';
            }
            function readSplitRatios() {
                try {
                    const o = JSON.parse(localStorage.getItem(SPLIT_RATIO_STORE_KEY) || '{}');
                    return o && typeof o === 'object' ? o : {};
                } catch (_) {
                    return {};
                }
            }
            function clampSplitRatio(r) {
                const x = Number(r);
                if (x !== x) return SPLIT_DEFAULT_RATIO;
                return Math.max(SPLIT_MIN_PREVIEW_RATIO, Math.min(1 - SPLIT_MIN_NAV_RATIO, x));
            }
            function getSplitRatio(vm) {
                const o = readSplitRatios();
                let r = typeof o[vm] === 'number' ? o[vm] : null;
                if (r == null && vm === 'split_preview') {
                    try {
                        const legacy = parseInt(localStorage.getItem(SPLIT_PREVIEW_W_KEY), 10);
                        if (legacy >= 120 && feExplorerStage) {
                            const sw = feExplorerStage.getBoundingClientRect().width || 800;
                            if (sw > 0) r = legacy / sw;
                        }
                    } catch (_) {}
                }
                return clampSplitRatio(r == null ? SPLIT_DEFAULT_RATIO : r);
            }
            function persistSplitRatio(vm, ratio) {
                const r = clampSplitRatio(ratio);
                try {
                    const o = readSplitRatios();
                    o[vm] = r;
                    localStorage.setItem(SPLIT_RATIO_STORE_KEY, JSON.stringify(o));
                } catch (_) {}
                return r;
            }
            const FE_VM_ORDER = ['icons_xl', 'icons_lg', 'icons_md', 'icons_sm', 'list', 'details', 'content', 'tiles', 'compact', 'split_preview', 'split_preview_left', 'split_preview_top', 'split_preview_bottom', 'split_preview_hscroll', 'multimedia'];
            const FE_VM_CLASS = {
                icons_xl: 'fe-vm-icons-xl',
                icons_lg: 'fe-vm-icons-lg',
                icons_md: 'fe-vm-icons-md',
                icons_sm: 'fe-vm-icons-sm',
                list: 'fe-vm-list',
                details: 'fe-vm-details',
                content: 'fe-vm-content',
                tiles: 'fe-vm-tiles',
                compact: 'fe-vm-compact',
                split_preview: 'fe-vm-split',
                split_preview_left: 'fe-vm-split',
                split_preview_top: 'fe-vm-split',
                split_preview_bottom: 'fe-vm-split',
                split_preview_hscroll: 'fe-vm-split fe-vm-icons-sm fe-split-hscroll-strip',
                multimedia: 'fe-vm-multimedia'
            };
            const FE_VM_TR = {
                icons_xl: 'explorerViewIconsXL',
                icons_lg: 'explorerViewIconsLG',
                icons_md: 'explorerViewIconsMD',
                icons_sm: 'explorerViewIconsSM',
                list: 'explorerViewList',
                details: 'explorerViewDetails',
                content: 'explorerViewContent',
                tiles: 'explorerViewTiles',
                compact: 'explorerViewCompact',
                split_preview: 'explorerViewSplitPreview',
                split_preview_left: 'explorerViewSplitPreviewLeft',
                split_preview_top: 'explorerViewSplitPreviewTop',
                split_preview_bottom: 'explorerViewSplitPreviewBottom',
                split_preview_hscroll: 'explorerViewSplitPreviewHScroll',
                multimedia: 'explorerViewMultimedia'
            };
            function feListLike(vm) {
                // Vista dividida: mismo layout que iconos (nombre bajo el icono); no usar lista para evitar
                // reglas adaptativas que ocultan el nombre cuando el panel es estrecho.
                if (isFeSplitMode(vm)) return false;
                return vm === 'list' || vm === 'compact';
            }
            function feDetailsLike(vm) {
                return vm === 'details';
            }
            function feIconGridLike(vm) {
                return vm === 'icons_xl' || vm === 'icons_lg' || vm === 'icons_md' || vm === 'icons_sm' || vm === 'multimedia' || vm === 'tiles';
            }
            function syncExplorerItemsViewClasses() {
                if (!itemsEl) return;
                Object.values(FE_VM_CLASS).forEach((c) => {
                    String(c).split(/\s+/).filter(Boolean).forEach((cls) => itemsEl.classList.remove(cls));
                });
                const add = FE_VM_CLASS[viewMode];
                if (add) String(add).split(/\s+/).filter(Boolean).forEach((cls) => itemsEl.classList.add(cls));
                itemsEl.classList.toggle('explorer-items-list', feListLike(viewMode));
                itemsEl.classList.toggle('explorer-items-details', feDetailsLike(viewMode));
                itemsEl.classList.toggle('explorer-items-content', viewMode === 'content');
            }
            function clearSplitPaneInlineStyles() {
                if (fePreviewPane) {
                    fePreviewPane.style.width = '';
                    fePreviewPane.style.height = '';
                    fePreviewPane.style.minWidth = '';
                    fePreviewPane.style.minHeight = '';
                    fePreviewPane.style.maxWidth = '';
                    fePreviewPane.style.maxHeight = '';
                    fePreviewPane.style.flex = '';
                }
                if (feExplorerMainPane) {
                    feExplorerMainPane.style.flex = '';
                    feExplorerMainPane.style.minWidth = '';
                    feExplorerMainPane.style.maxWidth = '';
                    feExplorerMainPane.style.minHeight = '';
                    feExplorerMainPane.style.maxHeight = '';
                }
            }
            const SPLIT_MOSAIC_MAX_MAIN_W = 200;
            /** Panel de archivos muy bajo en split vertical (vista previa arriba/abajo). */
            const SPLIT_MOSAIC_MAX_MAIN_H = 112;
            function syncExplorerItemsSplitAdaptive() {
                if (!itemsEl || !feExplorerMainPane) return;
                if (!isFeSplitMode(viewMode)) {
                    itemsEl.removeAttribute('data-fe-split-adaptive');
                    itemsEl.removeAttribute('data-fe-split-mosaic');
                    return;
                }
                const rect = feExplorerMainPane.getBoundingClientRect();
                const w = rect.width || 0;
                const h = rect.height || 0;
                if (viewMode === 'split_preview_hscroll') {
                    if (w < SPLIT_MOSAIC_MAX_MAIN_W) {
                        itemsEl.setAttribute('data-fe-split-mosaic', '1');
                        itemsEl.removeAttribute('data-fe-split-adaptive');
                    } else {
                        itemsEl.removeAttribute('data-fe-split-mosaic');
                        let lvl = 'lg';
                        if (w < 88) lvl = 'xs';
                        else if (w < 140) lvl = 'sm';
                        else if (w < 220) lvl = 'md';
                        itemsEl.setAttribute('data-fe-split-adaptive', lvl);
                    }
                    return;
                }
                if (viewMode === 'split_preview_top' || viewMode === 'split_preview_bottom') {
                    if (w < SPLIT_MOSAIC_MAX_MAIN_W || h < SPLIT_MOSAIC_MAX_MAIN_H) {
                        itemsEl.setAttribute('data-fe-split-mosaic', '1');
                        itemsEl.removeAttribute('data-fe-split-adaptive');
                        return;
                    }
                    itemsEl.removeAttribute('data-fe-split-mosaic');
                    let lvl = 'lg';
                    if (w < 88) lvl = 'xs';
                    else if (w < 140) lvl = 'sm';
                    else if (w < 220) lvl = 'md';
                    itemsEl.setAttribute('data-fe-split-adaptive', lvl);
                    return;
                }
                if (w < SPLIT_MOSAIC_MAX_MAIN_W) {
                    itemsEl.setAttribute('data-fe-split-mosaic', '1');
                    itemsEl.removeAttribute('data-fe-split-adaptive');
                    return;
                }
                itemsEl.removeAttribute('data-fe-split-mosaic');
                let lvl = 'lg';
                if (w < 88) lvl = 'xs';
                else if (w < 140) lvl = 'sm';
                else if (w < 220) lvl = 'md';
                itemsEl.setAttribute('data-fe-split-adaptive', lvl);
            }
            function applySplitPaneLayout() {
                if (!fePreviewPane || !feExplorerStage || !isFeSplitMode(viewMode)) return;
                const ratio = getSplitRatio(viewMode);
                const rect = feExplorerStage.getBoundingClientRect();
                const sw = Math.max(1, rect.width || 800);
                const sh = Math.max(1, rect.height || 400);
                const wPreview = Math.round(sw * ratio);
                const hPreview = Math.round(sh * ratio);
                const minW = Math.round(sw * SPLIT_MIN_PREVIEW_RATIO);
                const maxW = Math.round(sw * (1 - SPLIT_MIN_NAV_RATIO));
                const minH = Math.round(sh * SPLIT_MIN_PREVIEW_RATIO);
                const maxH = Math.round(sh * (1 - SPLIT_MIN_NAV_RATIO));
                fePreviewPane.style.flex = '0 0 auto';
                if (viewMode === 'split_preview' || viewMode === 'split_preview_left') {
                    fePreviewPane.style.width = wPreview + 'px';
                    fePreviewPane.style.height = '';
                    fePreviewPane.style.minWidth = minW + 'px';
                    fePreviewPane.style.maxWidth = maxW + 'px';
                    fePreviewPane.style.minHeight = '';
                    fePreviewPane.style.maxHeight = '';
                    if (feExplorerMainPane) {
                        feExplorerMainPane.style.flex = '1 1 0';
                        feExplorerMainPane.style.minHeight = '';
                        feExplorerMainPane.style.maxHeight = '';
                        const minMainW = Math.max(0, Math.round(sw * SPLIT_MIN_NAV_RATIO - SPLIT_RESIZER_PX));
                        const maxMainW = Math.max(minMainW, Math.round(sw * (1 - SPLIT_MIN_PREVIEW_RATIO) - SPLIT_RESIZER_PX));
                        feExplorerMainPane.style.minWidth = minMainW + 'px';
                        feExplorerMainPane.style.maxWidth = maxMainW + 'px';
                    }
                } else {
                    fePreviewPane.style.width = '100%';
                    fePreviewPane.style.height = hPreview + 'px';
                    fePreviewPane.style.minWidth = '';
                    fePreviewPane.style.maxWidth = '';
                    fePreviewPane.style.minHeight = minH + 'px';
                    fePreviewPane.style.maxHeight = maxH + 'px';
                    if (feExplorerMainPane) {
                        feExplorerMainPane.style.minWidth = '';
                        feExplorerMainPane.style.maxWidth = '';
                        feExplorerMainPane.style.flex = '1 1 0';
                        feExplorerMainPane.style.minHeight = Math.round(sh * SPLIT_MIN_NAV_RATIO) + 'px';
                        if (viewMode === 'split_preview_hscroll') {
                            feExplorerMainPane.style.maxHeight = Math.round(sh * (1 - SPLIT_MIN_PREVIEW_RATIO)) + 'px';
                        } else {
                            feExplorerMainPane.style.maxHeight = '';
                        }
                    }
                }
                syncExplorerItemsSplitAdaptive();
                try {
                    if (fePreviewBody && fePreviewBody._feSplitCtx && fePreviewBody._feSplitCtx.kind === 'pdf') {
                        requestAnimationFrame(() => applyFeExplorerPdfZoom(fePreviewBody));
                    }
                } catch (_) {}
            }
            function applyExplorerChromeLayout() {
                if (feItemsWrap) feItemsWrap.classList.toggle('fe-has-properties', fePropertiesVisible);
                if (fePropertiesPanel) fePropertiesPanel.hidden = !fePropertiesVisible;
                if (fePropertiesBtn) fePropertiesBtn.classList.toggle('active', fePropertiesVisible);
                const split = isFeSplitMode(viewMode);
                if (feExplorerStage) {
                    feExplorerStage.classList.toggle('fe-split-active', split);
                    FE_SPLIT_ORIENT_CLASSES.forEach((c) => feExplorerStage.classList.remove(c));
                    if (split) {
                        if (viewMode === 'split_preview') feExplorerStage.classList.add('fe-split-orient-right');
                        else if (viewMode === 'split_preview_left') feExplorerStage.classList.add('fe-split-orient-left');
                        else if (viewMode === 'split_preview_top') feExplorerStage.classList.add('fe-split-orient-top');
                        else if (viewMode === 'split_preview_bottom') feExplorerStage.classList.add('fe-split-orient-bottom');
                        else if (viewMode === 'split_preview_hscroll') feExplorerStage.classList.add('fe-split-orient-hscroll');
                    }
                    feExplorerStage.classList.toggle('fe-split-vertical', split && feSplitOrientation(viewMode) === 'column');
                }
                if (feSplitResizer) {
                    feSplitResizer.hidden = !split;
                    feSplitResizer.title = split ? (tr('explorerSplitResizeHint') || '') : '';
                }
                if (fePreviewPane) fePreviewPane.hidden = !split;
                if (split) {
                    requestAnimationFrame(() => applySplitPaneLayout());
                } else {
                    clearSplitPaneInlineStyles();
                }
                if (!split && fePreviewBody) {
                    fePreviewBody.querySelectorAll('video, audio').forEach((m) => {
                        try { m.pause(); } catch (_) {}
                    });
                    fePreviewBody.innerHTML = '';
                    try {
                        delete fePreviewBody._feSplitCtx;
                        delete fePreviewBody._fePdfZoom;
                    } catch (_) {}
                }
                if (!split && fePreviewToolbar) fePreviewToolbar.hidden = true;
                if (!split && fePreviewFileName) {
                    fePreviewFileName.textContent = '';
                    fePreviewFileName.hidden = true;
                }
                syncExplorerItemsViewClasses();
            }
            function closeFeViewMenu() {
                if (!feViewMenuDropdown || !feViewMenuBtn) return;
                feViewMenuDropdown.hidden = true;
                feViewMenuBtn.setAttribute('aria-expanded', 'false');
            }
            function buildFeViewMenu() {
                if (!feViewMenuDropdown) return;
                feViewMenuDropdown.innerHTML = FE_VM_ORDER.map((vm) => {
                    const trk = FE_VM_TR[vm];
                    const label = trk ? tr(trk) : vm;
                    const cur = vm === viewMode ? ' fe-view-menu-item-current' : '';
                    return `<button type="button" role="menuitem" class="fe-view-menu-item${cur}" data-fe-vm="${escapeHtmlAttr(vm)}">${escapeHtml(label)}</button>`;
                }).join('');
            }
            function setExplorerViewMode(vm, opts) {
                if (!FE_VM_ORDER.includes(vm)) return;
                viewMode = vm;
                try { localStorage.setItem(VIEW_MODE_KEY, viewMode); } catch (_) {}
                try { localStorage.removeItem(LEGACY_VIEW_KEY); } catch (_) {}
                buildFeViewMenu();
                applyExplorerChromeLayout();
                if (!opts || !opts.skipRender) renderItems();
                void refreshExplorerPropertiesPanel();
                void updateExplorerPreviewPane();
            }
            function rowPathForItem(it) {
                if (!it) return bucket === 'remote' ? (remoteRelPath || '') : relPath;
                if (it.__fromGlobal) return it.__parentPath ?? '';
                return bucket === 'remote' ? (remoteRelPath || '') : relPath;
            }
            async function openExplorerFolderInfoFromPath(parentPath, folderName) {
                if (!fePropertiesBody) return;
                fePropertiesVisible = true;
                try { localStorage.setItem(PROPS_KEY, '1'); } catch (_) {}
                applyExplorerChromeLayout();
                const lb = localListBucketName();
                const rp = parentPath || '';
                const pushRow = (k, v) => {
                    return `<div class="fe-prop-row"><span class="fe-prop-k">${escapeHtml(k)}</span><span class="fe-prop-v">${escapeHtml(v == null ? '—' : String(v))}</span></div>`;
                };
                let html = '';
                html += pushRow(tr('explorerColName'), folderName);
                html += pushRow(tr('explorerPropPath'), (rp ? rp + '/' : '') + folderName);
                html += pushRow(tr('explorerColType'), tr('explorerFolder'));
                html += pushRow(tr('explorerColSize'), '—');
                const stat = await fetchExplorerStat(lb, rp, folderName, 'dir');
                if (stat && stat.success && stat.kind === 'dir') {
                    html += pushRow(tr('explorerColModified'), formatModDate(stat.mtime));
                    const line = `${stat.fileCount || 0} ${tr('explorerFolderFiles')}, ${stat.dirCount || 0} ${tr('explorerFolderSubfolders')} · ${formatBytes(stat.totalBytes || 0)}`;
                    html += `<div class="fe-prop-row"><span class="fe-prop-k">${escapeHtml(tr('explorerFolderContains'))}</span><span class="fe-prop-v">${escapeHtml(line)}</span></div>`;
                    if (stat.ctime) {
                        html += pushRow(tr('explorerColCreated'), formatModDate(stat.ctime));
                    }
                } else {
                    html += pushRow(tr('explorerColModified'), '—');
                }
                fePropertiesBody.innerHTML = html;
            }
            async function openExplorerRemoteFolderInfoFromPath(parentPath, folderName) {
                if (!fePropertiesBody || !remoteCtx?.id) return;
                fePropertiesVisible = true;
                try { localStorage.setItem(PROPS_KEY, '1'); } catch (_) {}
                applyExplorerChromeLayout();
                const rp = parentPath || '';
                const full = normalizeJoin(rp, folderName);
                const pushRow = (k, v) => {
                    return `<div class="fe-prop-row"><span class="fe-prop-k">${escapeHtml(k)}</span><span class="fe-prop-v">${escapeHtml(v == null ? '—' : String(v))}</span></div>`;
                };
                let html = '';
                html += pushRow(tr('explorerColName'), folderName);
                html += pushRow(tr('explorerPropPath'), full || folderName);
                html += pushRow(tr('explorerColType'), tr('explorerFolder'));
                fePropertiesBody.innerHTML = html + `<div class="fe-properties-empty fe-prop-loading">${escapeHtml(tr('explorerLoading'))}</div>`;
                try {
                    const data = await fetchExplorerConnList(remoteCtx.id, full);
                    const items = (data && data.items) ? data.items : [];
                    let dirs = 0;
                    let files = 0;
                    items.forEach((it) => {
                        if (it && it.type === 'dir') dirs++;
                        else files++;
                    });
                    fePropertiesBody.querySelector('.fe-prop-loading')?.remove();
                    const line = `${files} ${tr('explorerFilesLabel')}, ${dirs} ${tr('explorerFolder').toLowerCase()}`;
                    fePropertiesBody.insertAdjacentHTML('beforeend', `<div class="fe-prop-row"><span class="fe-prop-k">${escapeHtml(tr('explorerFolderContains'))}</span><span class="fe-prop-v">${escapeHtml(line)}</span></div>`);
                } catch (_) {
                    fePropertiesBody.querySelector('.fe-prop-loading')?.remove();
                }
            }
            async function refreshExplorerPropertiesPanel() {
                if (!fePropertiesBody || !fePropertiesVisible) return;
                const disp = itemsEl && itemsEl._feDisplayItems ? itemsEl._feDisplayItems : items;
                const names = Array.from(selectedNames).filter(Boolean);
                if (!disp || names.length === 0) {
                    fePropertiesBody.innerHTML = `<div class="fe-properties-empty">${escapeHtml(tr('explorerSelectForProps'))}</div>`;
                    return;
                }
                if (names.length > 1) {
                    let total = 0;
                    let dirs = 0;
                    let files = 0;
                    names.forEach((n) => {
                        const row = disp.find((x) => x && x.name === n);
                        if (!row) return;
                        if (row.type === 'dir') dirs++;
                        else {
                            files++;
                            total += Number(row.size) || 0;
                        }
                    });
                    fePropertiesBody.innerHTML = `
                        <div class="fe-prop-row"><span class="fe-prop-k">${escapeHtml(tr('explorerSelectedCount').replace('%n', String(names.length)))}</span></div>
                        <div class="fe-prop-row"><span class="fe-prop-k">${escapeHtml(tr('explorerColType'))}</span><span class="fe-prop-v">${files} ${escapeHtml(tr('explorerFilesLabel'))}, ${dirs} ${escapeHtml(tr('explorerFolder').toLowerCase())}</span></div>
                        <div class="fe-prop-row"><span class="fe-prop-k">${escapeHtml(tr('explorerColSize'))}</span><span class="fe-prop-v">${escapeHtml(formatBytes(total))}</span></div>`;
                    return;
                }
                const n = names[0];
                const item = disp.find((x) => x && x.name === n);
                if (!item) {
                    fePropertiesBody.innerHTML = `<div class="fe-properties-empty">${escapeHtml(tr('explorerSelectForProps'))}</div>`;
                    return;
                }
                const rp = rowPathForItem(item);
                const lb = localListBucketName();
                const ext = (item.name || '').includes('.') ? item.name.slice(item.name.lastIndexOf('.') + 1) : '';
                let html = '';
                const pushRow = (k, v) => {
                    html += `<div class="fe-prop-row"><span class="fe-prop-k">${escapeHtml(k)}</span><span class="fe-prop-v">${escapeHtml(v == null ? '—' : String(v))}</span></div>`;
                };
                pushRow(tr('explorerColName'), item.name);
                pushRow(tr('explorerPropPath'), (rp ? rp + '/' : '') + item.name);
                pushRow(tr('explorerColType'), item.type === 'dir' ? tr('explorerFolder') : (item.mimeType || '—'));
                if (item.type !== 'dir') {
                    pushRow(tr('explorerColSize'), formatBytes(item.size));
                    pushRow(tr('explorerColModified'), formatModDate(item.mtime));
                    pushRow(tr('explorerColExt'), ext || '—');
                } else {
                    pushRow(tr('explorerColSize'), '—');
                    pushRow(tr('explorerColModified'), formatModDate(item.mtime));
                }
                fePropertiesBody.innerHTML = html;
                if (isLocalLibraryBucket() && item.type === 'dir') {
                    const stat = await fetchExplorerStat(lb, rp, item.name, 'dir');
                    if (stat && stat.success && stat.kind === 'dir') {
                        const line = `${stat.fileCount || 0} ${tr('explorerFolderFiles')}, ${stat.dirCount || 0} ${tr('explorerFolderSubfolders')} · ${formatBytes(stat.totalBytes || 0)}`;
                        fePropertiesBody.insertAdjacentHTML('beforeend', `<div class="fe-prop-row"><span class="fe-prop-k">${escapeHtml(tr('explorerFolderContains'))}</span><span class="fe-prop-v">${escapeHtml(line)}</span></div>`);
                        if (stat.ctime) {
                            fePropertiesBody.insertAdjacentHTML('beforeend', `<div class="fe-prop-row"><span class="fe-prop-k">${escapeHtml(tr('explorerColCreated'))}</span><span class="fe-prop-v">${escapeHtml(formatModDate(stat.ctime))}</span></div>`);
                        }
                    }
                } else if (isLocalLibraryBucket() && item.type === 'file') {
                    const stat = await fetchExplorerStat(lb, rp, item.name, 'file');
                    if (stat && stat.success && stat.kind === 'file') {
                        fePropertiesBody.insertAdjacentHTML('beforeend', `<div class="fe-prop-row"><span class="fe-prop-k">${escapeHtml(tr('explorerColCreated'))}</span><span class="fe-prop-v">${escapeHtml(formatModDate(stat.ctime))}</span></div>`);
                    }
                }
            }
            function configureFePreviewToolbar(mode) {
                if (!fePreviewToolbar) return;
                const showBar = mode !== 'none';
                const showZ = mode === 'img' || mode === 'pdf';
                const showHtml = mode === 'html';
                if (!showBar) {
                    fePreviewToolbar.hidden = true;
                    return;
                }
                fePreviewToolbar.hidden = false;
                fePreviewToolbar.querySelectorAll('[data-fe-split-action="zoom-in"],[data-fe-split-action="zoom-out"],[data-fe-split-action="zoom-reset"]').forEach((b) => {
                    b.hidden = !showZ;
                });
                fePreviewToolbar.querySelectorAll('[data-fe-split-action="share"],[data-fe-split-action="download"],[data-fe-split-action="external"]').forEach((b) => {
                    b.hidden = false;
                });
                const printBtn = fePreviewToolbar.querySelector('[data-fe-split-action="print"]');
                if (printBtn) printBtn.hidden = mode !== 'pdf';
                fePreviewToolbar.querySelectorAll('.fe-preview-tb-sep--html, [data-fe-split-action="html-toggle"]').forEach((el) => {
                    if (el) el.hidden = !showHtml;
                });
                if (showHtml && fePreviewBody && fePreviewBody._feSplitCtx && fePreviewBody._feSplitCtx.kind === 'html') {
                    const hasSrc = fePreviewBody._feSplitCtx.htmlSourceAvailable !== false;
                    const toggleBtn = fePreviewToolbar.querySelector('[data-fe-split-action="html-toggle"]');
                    const sepHtml = fePreviewToolbar.querySelector('.fe-preview-tb-sep--html');
                    if (!hasSrc) {
                        if (toggleBtn) toggleBtn.hidden = true;
                        if (sepHtml) sepHtml.hidden = true;
                    }
                }
            }
            function syncFeHtmlPreviewToolbar() {
                if (!fePreviewToolbar || !fePreviewBody || !fePreviewBody._feSplitCtx || fePreviewBody._feSplitCtx.kind !== 'html') return;
                const inText = fePreviewBody._feSplitCtx.htmlView === 'text';
                const btn = fePreviewToolbar.querySelector('[data-fe-split-action="html-toggle"]');
                if (!btn) return;
                const ico = btn.querySelector('i');
                if (inText) {
                    btn.classList.add('fe-preview-tb-btn--active');
                    btn.setAttribute('aria-pressed', 'true');
                    const lab = tr('explorerHtmlPreviewShowWeb');
                    btn.title = lab;
                    btn.setAttribute('aria-label', lab);
                    if (ico) ico.className = 'fas fa-desktop';
                } else {
                    btn.classList.remove('fe-preview-tb-btn--active');
                    btn.setAttribute('aria-pressed', 'false');
                    const lab = tr('explorerHtmlPreviewShowCode');
                    btn.title = lab;
                    btn.setAttribute('aria-label', lab);
                    if (ico) ico.className = 'fas fa-code';
                }
            }
            if (fePreviewPane && !fePreviewPane._feSplitToolbarBound) {
                fePreviewPane._feSplitToolbarBound = true;
                fePreviewPane.addEventListener('click', (e) => {
                    const btn = e.target && e.target.closest ? e.target.closest('[data-fe-split-action]') : null;
                    if (!btn || !fePreviewPane.contains(btn)) return;
                    e.preventDefault();
                    e.stopPropagation();
                    if (!fePreviewBody || !fePreviewBody._feSplitCtx) return;
                    const ctx = fePreviewBody._feSplitCtx;
                    const act = btn.getAttribute('data-fe-split-action');
                    const secure = ctx.secureUrl || '';
                    const fname = ctx.fileName || 'file';
                    if (act === 'share') {
                        shareSecureUrl(ctx.fileUrl || secure, fname).catch(() => {});
                        return;
                    }
                    if (act === 'download') {
                        const a = document.createElement('a');
                        a.href = secure || ctx.fileUrl || '#';
                        a.download = fname;
                        a.rel = 'noopener';
                        document.body.appendChild(a);
                        a.click();
                        document.body.removeChild(a);
                        return;
                    }
                    if (act === 'external') {
                        const u = ctx.viewerUrl || ctx.externalUrl || secure || ctx.fileUrl;
                        if (u) window.open(u, '_blank', 'noopener,noreferrer');
                        return;
                    }
                    if (act === 'print' && ctx.kind === 'pdf' && secure) {
                        printPdfFromUrl(secure, fePreviewBody);
                        return;
                    }
                    if (act === 'zoom-in') {
                        if (ctx.kind === 'pdf') {
                            fePreviewBody._fePdfZoom = Math.min((fePreviewBody._fePdfZoom || 1) + 0.1, 2.5);
                            applyFeExplorerPdfZoom(fePreviewBody);
                        } else if (ctx.kind === 'image' && ctx.imageState && ctx.imageEls) {
                            feExplorerImgZoomTo(ctx.imageState, ctx.imageEls.img, ctx.imageEls.box, ctx.imageState.scale + IMAGE_ZOOM_STEP);
                        }
                        return;
                    }
                    if (act === 'zoom-out') {
                        if (ctx.kind === 'pdf') {
                            fePreviewBody._fePdfZoom = Math.max((fePreviewBody._fePdfZoom || 1) - 0.1, 0.5);
                            applyFeExplorerPdfZoom(fePreviewBody);
                        } else if (ctx.kind === 'image' && ctx.imageState && ctx.imageEls) {
                            feExplorerImgZoomTo(ctx.imageState, ctx.imageEls.img, ctx.imageEls.box, ctx.imageState.scale - IMAGE_ZOOM_STEP);
                        }
                        return;
                    }
                    if (act === 'zoom-reset') {
                        if (ctx.kind === 'pdf') {
                            fePreviewBody._fePdfZoom = 1;
                            applyFeExplorerPdfZoom(fePreviewBody);
                        } else if (ctx.kind === 'image' && ctx.imageState && ctx.imageEls) {
                            feExplorerImgZoomTo(ctx.imageState, ctx.imageEls.img, ctx.imageEls.box, IMAGE_ZOOM_MIN);
                        }
                        return;
                    }
                    if (act === 'html-toggle') {
                        if (ctx.kind !== 'html' || ctx.htmlSourceAvailable === false) return;
                        ctx.htmlView = ctx.htmlView === 'text' ? 'render' : 'text';
                        applyFeExplorerHtmlPreviewView(fePreviewBody);
                        syncFeHtmlPreviewToolbar();
                        return;
                    }
                });
            }
            async function updateExplorerPreviewPane() {
                if (!isFeSplitMode(viewMode) || !fePreviewBody) return;
                const disp = itemsEl && itemsEl._feDisplayItems ? itemsEl._feDisplayItems : items;
                const names = Array.from(selectedNames).filter(Boolean);
                fePreviewBody.innerHTML = '';
                try {
                    delete fePreviewBody._feSplitCtx;
                    delete fePreviewBody._fePdfZoom;
                } catch (_) {}
                configureFePreviewToolbar('none');
                if (fePreviewFileName) {
                    fePreviewFileName.textContent = '';
                    fePreviewFileName.hidden = true;
                }
                if (names.length !== 1 || !disp) {
                    fePreviewBody.innerHTML = `<div class="fe-preview-fallback">${escapeHtml(tr('explorerSelectForProps'))}</div>`;
                    configureFePreviewToolbar('none');
                    return;
                }
                const item = disp.find((x) => x && x.name === names[0]);
                if (!item || item.type === 'dir') {
                    fePreviewBody.innerHTML = `<div class="fe-preview-fallback">${escapeHtml(tr('explorerFolder'))}: ${escapeHtml(item ? item.name : '')}</div>`;
                    configureFePreviewToolbar('none');
                    return;
                }
                if (fePreviewFileName) {
                    fePreviewFileName.textContent = item.name || '';
                    fePreviewFileName.hidden = false;
                }
                const mime = item.mimeType || '';
                const url = item.url ? buildSecureFileUrl(item.url) : '';
                if (bucket === 'remote' && remoteCtx && remoteCtx.id) {
                    const maxPrev = 15 * 1024 * 1024;
                    if (!(item.size && item.size > maxPrev) && mime.startsWith('image/')) {
                        const u = buildExplorerRemotePreviewUrl(remoteCtx.id, remoteRelPath || '', item.name, item.size);
                        if (u) {
                            mountFeExplorerSplitImageViewer(fePreviewBody, u, u, item.name);
                            configureFePreviewToolbar('img');
                            return;
                        }
                    }
                    if (!(item.size && item.size > maxPrev) && mime.startsWith('video/')) {
                        const u = buildExplorerRemotePreviewUrl(remoteCtx.id, remoteRelPath || '', item.name, item.size);
                        if (u) {
                            fePreviewBody._feSplitCtx = { kind: 'video', secureUrl: u, fileName: item.name, fileUrl: u };
                            fePreviewBody.innerHTML = `<div class="fe-preview-surface fe-preview-surface--media"><video class="fe-preview-video" src="${escapeHtmlAttr(u)}" controls playsinline preload="metadata"></video></div>`;
                            configureFePreviewToolbar('actions');
                            return;
                        }
                    }
                    if (!(item.size && item.size > maxPrev) && mime.startsWith('audio/')) {
                        const u = buildExplorerRemotePreviewUrl(remoteCtx.id, remoteRelPath || '', item.name, item.size);
                        if (u) {
                            fePreviewBody._feSplitCtx = { kind: 'audio', secureUrl: u, fileName: item.name, fileUrl: u };
                            fePreviewBody.innerHTML = `
                        <div class="fe-preview-surface fe-preview-surface--audio">
                            <div class="audio-animation" aria-hidden="true"><span></span><span></span><span></span></div>
                            <audio class="fe-preview-audio-el" src="${escapeHtmlAttr(u)}" controls preload="metadata" playsinline></audio>
                        </div>`;
                            configureFePreviewToolbar('actions');
                            return;
                        }
                    }
                    if (!(item.size && item.size > maxPrev) && isHtmlLikeMimeOrName(mime, item.name)) {
                        const u = buildExplorerRemotePreviewUrl(remoteCtx.id, remoteRelPath || '', item.name, item.size);
                        if (u) {
                            const maxText = 512 * 1024;
                            let source = null;
                            if (!(item.size && item.size > maxText)) {
                                try {
                                    const r = await fetch(u, { credentials: 'same-origin' });
                                    if (r.ok) source = await r.text();
                                } catch (_) {
                                    source = null;
                                }
                            }
                            const hasSrc = source != null;
                            fePreviewBody._feSplitCtx = {
                                kind: 'html',
                                secureUrl: u,
                                frameUrl: u,
                                fileName: item.name,
                                fileUrl: u,
                                sourceText: hasSrc ? source : '',
                                htmlView: 'render',
                                htmlSourceAvailable: hasSrc
                            };
                            applyFeExplorerHtmlPreviewView(fePreviewBody);
                            configureFePreviewToolbar(hasSrc ? 'html' : 'actions');
                            if (hasSrc) syncFeHtmlPreviewToolbar();
                            return;
                        }
                    }
                    fePreviewBody.innerHTML = `<div class="fe-preview-fallback">${escapeHtml(tr('explorerPreviewUnsupported'))}</div>`;
                    configureFePreviewToolbar('none');
                    return;
                }
                if (mime.startsWith('image/') && url) {
                    mountFeExplorerSplitImageViewer(fePreviewBody, url, url, item.name);
                    configureFePreviewToolbar('img');
                    return;
                }
                if (mime.startsWith('video/') && url) {
                    fePreviewBody._feSplitCtx = { kind: 'video', secureUrl: url, fileName: item.name, fileUrl: url };
                    fePreviewBody.innerHTML = `<div class="fe-preview-surface fe-preview-surface--media"><video class="fe-preview-video" src="${escapeHtmlAttr(url)}" controls playsinline preload="metadata"></video></div>`;
                    configureFePreviewToolbar('actions');
                    return;
                }
                if (mime.startsWith('audio/') && url) {
                    fePreviewBody._feSplitCtx = { kind: 'audio', secureUrl: url, fileName: item.name, fileUrl: url };
                    fePreviewBody.innerHTML = `
                        <div class="fe-preview-surface fe-preview-surface--audio">
                            <div class="audio-animation" aria-hidden="true"><span></span><span></span><span></span></div>
                            <audio class="fe-preview-audio-el" src="${escapeHtmlAttr(url)}" controls preload="metadata" playsinline></audio>
                        </div>`;
                    configureFePreviewToolbar('actions');
                    return;
                }
                if ((mime === 'application/pdf' || (item.name || '').toLowerCase().endsWith('.pdf')) && url) {
                    fePreviewBody._feSplitCtx = { kind: 'pdf', secureUrl: url, fileName: item.name, fileUrl: url };
                    fePreviewBody.innerHTML = '';
                    const root = document.createElement('div');
                    root.className = 'fe-preview-content-root fe-preview-content-root--pdf';
                    fePreviewBody.appendChild(root);
                    fePreviewBody._fePdfZoom = 1;
                    setPdfViewerContent(root, url, {
                        getZoom: () => fePreviewBody._fePdfZoom,
                        setZoom: (z) => {
                            fePreviewBody._fePdfZoom = z;
                        },
                        applyZoom: () => applyFeExplorerPdfZoom(fePreviewBody),
                        hud: {
                            enabled: true,
                            minZoom: 0.5,
                            maxZoom: 2.5,
                            stepPercent: 1,
                            onSetZoom: (z) => {
                                fePreviewBody._fePdfZoom = z;
                                applyFeExplorerPdfZoom(fePreviewBody);
                            },
                            onReset: () => {
                                fePreviewBody._fePdfZoom = 1;
                                applyFeExplorerPdfZoom(fePreviewBody);
                            }
                        }
                    });
                    configureFePreviewToolbar('pdf');
                    setTimeout(() => applyFeExplorerPdfZoom(fePreviewBody), 0);
                    setTimeout(() => applyFeExplorerPdfZoom(fePreviewBody), 400);
                    return;
                }
                if (url) {
                    const nm = item.name || '';
                    const nmLower = nm.toLowerCase();
                    const ext = nmLower.includes('.') ? nmLower.slice(nmLower.lastIndexOf('.') + 1) : '';
                    const mimeL = (mime || '').toLowerCase();
                    let embedSrc = '';
                    let viewerUrl = '';
                    if (/\.(ppt|pptx)$/i.test(nm) || mimeL.includes('presentation') || mimeL.includes('powerpoint')) {
                        viewerUrl = `https://view.officeapps.live.com/op/embed.aspx?src=${encodeURIComponent(url)}`;
                        embedSrc = viewerUrl;
                    } else if (mimeL.includes('spreadsheet') || mimeL.includes('excel') || /\.(xls|xlsx|csv)$/i.test(nm)) {
                        const isExcelBin = ['xls', 'xlsx'].includes(ext) || mimeL.includes('excel');
                        if (isExcelBin) {
                            viewerUrl = `https://view.officeapps.live.com/op/embed.aspx?src=${encodeURIComponent(url)}`;
                            embedSrc = viewerUrl;
                        } else if (/\.csv$/i.test(nm)) {
                            viewerUrl = `https://docs.google.com/viewer?url=${encodeURIComponent(url)}&embedded=true`;
                            embedSrc = viewerUrl;
                        }
                    } else if (mimeL.includes('word') || mimeL.includes('document') || /\.(doc|docx)$/i.test(nm)) {
                        viewerUrl = `https://view.officeapps.live.com/op/embed.aspx?src=${encodeURIComponent(url)}`;
                        embedSrc = viewerUrl;
                    }
                    if (embedSrc) {
                        const sandbox = embedSrc.includes('docs.google.com')
                            ? ' sandbox="allow-scripts allow-same-origin allow-popups"'
                            : '';
                        fePreviewBody._feSplitCtx = {
                            kind: 'office',
                            secureUrl: url,
                            fileName: item.name,
                            fileUrl: url,
                            viewerUrl: viewerUrl || embedSrc
                        };
                        fePreviewBody.innerHTML = `<div class="fe-preview-surface fe-preview-surface--office"><iframe src="${escapeHtmlAttr(embedSrc)}" width="100%" height="100%" frameborder="0" allowfullscreen${sandbox}></iframe></div>`;
                        configureFePreviewToolbar('actions');
                        return;
                    }
                }
                if (isHtmlLikeMimeOrName(mime, item.name || '') && url) {
                    const maxText = 512 * 1024;
                    if (item.size < maxText) {
                        try {
                            const r = await fetch(url, { credentials: 'same-origin' });
                            const t = await r.text();
                            fePreviewBody._feSplitCtx = {
                                kind: 'html',
                                secureUrl: url,
                                frameUrl: url,
                                fileName: item.name,
                                fileUrl: url,
                                sourceText: t,
                                htmlView: 'render',
                                htmlSourceAvailable: true
                            };
                            applyFeExplorerHtmlPreviewView(fePreviewBody);
                            configureFePreviewToolbar('html');
                            syncFeHtmlPreviewToolbar();
                        } catch (_) {
                            fePreviewBody.innerHTML = `<div class="fe-preview-fallback">${escapeHtml(tr('explorerPreviewUnsupported'))}</div>`;
                            configureFePreviewToolbar('none');
                        }
                        return;
                    }
                    fePreviewBody._feSplitCtx = {
                        kind: 'html',
                        secureUrl: url,
                        frameUrl: url,
                        fileName: item.name,
                        fileUrl: url,
                        sourceText: '',
                        htmlView: 'render',
                        htmlSourceAvailable: false
                    };
                    applyFeExplorerHtmlPreviewView(fePreviewBody);
                    configureFePreviewToolbar('actions');
                    return;
                }
                const textish = isTextLikeMimeOrName(mime, item.name || '');
                if (textish && url && item.size < 512 * 1024) {
                    try {
                        const r = await fetch(url, { credentials: 'same-origin' });
                        const t = await r.text();
                        const slice = t.slice(0, 6000);
                        fePreviewBody._feSplitCtx = { kind: 'text', secureUrl: url, fileName: item.name, fileUrl: url };
                        fePreviewBody.innerHTML = `<div class="fe-preview-surface fe-preview-surface--text"><pre class="fe-preview-text">${escapeHtml(slice)}</pre></div>`;
                        configureFePreviewToolbar('actions');
                    } catch (_) {
                        fePreviewBody.innerHTML = `<div class="fe-preview-fallback">${escapeHtml(tr('explorerPreviewUnsupported'))}</div>`;
                        configureFePreviewToolbar('none');
                    }
                    return;
                }
                fePreviewBody.innerHTML = `<div class="fe-preview-fallback">${escapeHtml(tr('explorerPreviewUnsupported'))}<div class="fe-preview-meta">${escapeHtml(formatBytes(item.size))} · ${escapeHtml(formatModDate(item.mtime))}</div></div>`;
                try {
                    delete fePreviewBody._feSplitCtx;
                } catch (_) {}
                configureFePreviewToolbar('none');
            }
            const feTreeExpanded = new Set();
            const feTreeHasChildrenCache = new Map();
            const feTreeRemoteExpanded = new Set();
            const feTreeRemoteHasChildrenCache = new Map();
            if (feBC) {
                // UX: permitir "subir" un nivel al hacer click en el fondo del breadcrumb (no en un segmento).
                feBC.addEventListener('click', (e) => {
                    if (e.target !== feBC) return;
                    if (bucket === 'remote') {
                        if (!remoteRelPath) return;
                        const parent = remoteRelPath.split('/').filter(Boolean).slice(0, -1).join('/');
                        remoteNavigateToPath(parent, { pushHistory: true });
                        return;
                    }
                    if (isLocalLibraryBucket()) {
                        if (!relPath) return;
                        goUpFolder();
                    }
                });

                // Delegación: asegura navegación en segmentos del breadcrumb (incluye FTP/WebDAV).
                feBC.addEventListener('click', (e) => {
                    const seg = e.target && e.target.closest ? e.target.closest('.fe-bc-seg') : null;
                    if (!seg || !feBC.contains(seg)) return;
                    if (seg.classList.contains('fe-bc-exit-search')) return;
                    if (seg.classList.contains('fe-bc-current')) return;
                    const action = seg.dataset.feBcAction || 'path';
                    const targetBucket = seg.dataset.feBucket ? String(seg.dataset.feBucket) : '';
                    const targetPath = seg.dataset.fePath !== undefined ? String(seg.dataset.fePath) : '';
                    const targetDept = seg.dataset.feDepartmentId ? String(seg.dataset.feDepartmentId) : '';
                    e.preventDefault();
                    e.stopPropagation();
                    if (action === 'home') {
                        navigateToExplorerHome();
                        return;
                    }
                    if (action === 'spaceRoot') {
                        navigateToBreadcrumbSpaceRoot(targetBucket, targetDept);
                        return;
                    }
                    if (targetBucket === 'remote') {
                        remoteNavigateToPath(targetPath, { pushHistory: true });
                        return;
                    }
                    if (targetBucket === 'community' || targetBucket === 'department') {
                        if (targetBucket !== bucket || (targetBucket === 'department' && targetDept && targetDept !== sharedDepartmentId)) {
                            navigateToTreePath(targetPath, { bucket: targetBucket, departmentId: targetDept });
                        } else {
                            navigateToPath(targetPath);
                        }
                        return;
                    }
                    if (targetBucket === 'files' || targetBucket === 'cache') {
                        navigateToPath(targetPath);
                    }
                }, true);
            }
            const feUndoStack = [];
            function feExplorerIsTextInputEl(t) {
                if (!t) return false;
                const tag = (t.tagName || '').toLowerCase();
                if (tag === 'input' || tag === 'textarea' || tag === 'select') return true;
                if (t.isContentEditable) return true;
                return false;
            }
            function feCommitUndoSequential(steps) {
                if (!steps || !steps.length) return;
                feUndoStack.push({
                    run: async () => {
                        for (const fn of steps) {
                            try {
                                if (typeof fn === 'function') await fn();
                            } catch (_) {}
                        }
                    }
                });
            }
            try {
                const rawV2 = localStorage.getItem(VIEW_MODE_KEY);
                if (rawV2 === 'documents') {
                    viewMode = 'details';
                    try { localStorage.setItem(VIEW_MODE_KEY, 'details'); } catch (_) {}
                } else if (rawV2 && FE_VM_ORDER.includes(rawV2)) {
                    viewMode = rawV2;
                } else {
                    const leg = localStorage.getItem(LEGACY_VIEW_KEY);
                    if (leg === 'list') viewMode = 'list';
                    else if (leg === 'grid') viewMode = 'icons_md';
                }
                const hadV2 = !!rawV2;
                const leg = localStorage.getItem(LEGACY_VIEW_KEY);
                const hadLegacyUseful = leg === 'list' || leg === 'grid';
                if (!hadV2 && !hadLegacyUseful) {
                    const narrowMobile = typeof window.matchMedia === 'function' && window.matchMedia('(max-width: 720px)').matches;
                    viewMode = narrowMobile ? 'icons_md' : 'split_preview';
                    try { localStorage.setItem(VIEW_MODE_KEY, viewMode); } catch (_) {}
                }
            } catch (_) {}
            try {
                const ds = JSON.parse(localStorage.getItem(DETAILS_SORT_KEY) || 'null');
                if (ds && ds.col && (ds.dir === 'asc' || ds.dir === 'desc')) feDetailsSort = { col: ds.col, dir: ds.dir };
            } catch (_) {}
            try {
                fePropertiesVisible = localStorage.getItem(PROPS_KEY) === '1';
            } catch (_) {}
            buildFeViewMenu();
            applyExplorerChromeLayout();
            if (feViewMenuBtn && feViewMenuDropdown) {
                feViewMenuBtn.addEventListener('click', (e) => {
                    e.preventDefault();
                    e.stopPropagation();
                    const open = feViewMenuDropdown.hidden;
                    closeFeViewMenu();
                    if (open) {
                        buildFeViewMenu();
                        feViewMenuDropdown.hidden = false;
                        feViewMenuBtn.setAttribute('aria-expanded', 'true');
                    }
                });
                feViewMenuDropdown.addEventListener('click', (e) => {
                    const btn = e.target && e.target.closest ? e.target.closest('[data-fe-vm]') : null;
                    if (!btn || !feViewMenuDropdown.contains(btn)) return;
                    e.preventDefault();
                    const vm = btn.getAttribute('data-fe-vm');
                    if (vm) setExplorerViewMode(vm);
                    closeFeViewMenu();
                });
            }
            document.addEventListener('click', (e) => {
                if (!feViewMenuDropdown || feViewMenuDropdown.hidden) return;
                if (e.target && feViewMenuBtn && (e.target === feViewMenuBtn || feViewMenuBtn.contains(e.target))) return;
                if (e.target && feViewMenuDropdown.contains(e.target)) return;
                closeFeViewMenu();
            });
            if (fePropertiesBtn && fePropertiesPanel) {
                fePropertiesBtn.addEventListener('click', (e) => {
                    e.preventDefault();
                    e.stopPropagation();
                    fePropertiesVisible = !fePropertiesVisible;
                    try { localStorage.setItem(PROPS_KEY, fePropertiesVisible ? '1' : '0'); } catch (_) {}
                    applyExplorerChromeLayout();
                    void refreshExplorerPropertiesPanel();
                });
            }
            if (fePropertiesClose) {
                fePropertiesClose.addEventListener('click', (e) => {
                    e.preventDefault();
                    fePropertiesVisible = false;
                    try { localStorage.setItem(PROPS_KEY, '0'); } catch (_) {}
                    applyExplorerChromeLayout();
                });
            }
            if (feSplitResizer && fePreviewPane && feExplorerStage) {
                const endSplitDrag = (ev) => {
                    if (!feSplitPreviewDrag) return;
                    if (ev && feSplitPreviewDrag.pointerId != null && ev.pointerId !== feSplitPreviewDrag.pointerId) return;
                    feSplitResizer.classList.remove('fe-split-dragging');
                    try {
                        if (ev && feSplitPreviewDrag.pointerId != null) feSplitResizer.releasePointerCapture(feSplitPreviewDrag.pointerId);
                    } catch (_) {}
                    const vm = feSplitPreviewDrag.mode;
                    if (vm && isFeSplitMode(vm)) {
                        const rect = feExplorerStage.getBoundingClientRect();
                        const sw = Math.max(1, rect.width);
                        const sh = Math.max(1, rect.height);
                        let r = feSplitPreviewDrag.ratio;
                        if (feSplitPreviewDrag.axis === 'x') {
                            const pw = fePreviewPane.getBoundingClientRect().width;
                            r = pw / sw;
                        } else {
                            const ph = fePreviewPane.getBoundingClientRect().height;
                            r = ph / sh;
                        }
                        persistSplitRatio(vm, r);
                    }
                    feSplitPreviewDrag = null;
                    if (isFeSplitMode(viewMode)) applySplitPaneLayout();
                };
                feSplitResizer.addEventListener('pointerdown', (e) => {
                    if (!isFeSplitMode(viewMode) || e.button !== 0) return;
                    e.preventDefault();
                    const rect = feExplorerStage.getBoundingClientRect();
                    const sw = Math.max(1, rect.width);
                    const sh = Math.max(1, rect.height);
                    const pr = fePreviewPane.getBoundingClientRect();
                    const axis = feSplitOrientation(viewMode) === 'row' ? 'x' : 'y';
                    feSplitPreviewDrag = {
                        mode: viewMode,
                        axis,
                        startX: e.clientX,
                        startY: e.clientY,
                        startPreviewW: pr.width,
                        startPreviewH: pr.height,
                        ratio: getSplitRatio(viewMode),
                        pointerId: e.pointerId,
                        stageW: sw,
                        stageH: sh
                    };
                    feSplitResizer.classList.add('fe-split-dragging');
                    try { feSplitResizer.setPointerCapture(e.pointerId); } catch (_) {}
                });
                feSplitResizer.addEventListener('pointermove', (e) => {
                    if (!feSplitPreviewDrag || e.pointerId !== feSplitPreviewDrag.pointerId) return;
                    const sw = Math.max(1, feExplorerStage.getBoundingClientRect().width);
                    const sh = Math.max(1, feExplorerStage.getBoundingClientRect().height);
                    const minPxW = sw * SPLIT_MIN_PREVIEW_RATIO;
                    const maxPxW = sw * (1 - SPLIT_MIN_NAV_RATIO);
                    const minPxH = sh * SPLIT_MIN_PREVIEW_RATIO;
                    const maxPxH = sh * (1 - SPLIT_MIN_NAV_RATIO);
                    if (feSplitPreviewDrag.axis === 'x') {
                        const dw = e.clientX - feSplitPreviewDrag.startX;
                        // Mismo sentido en split_preview (previa derecha) y split_preview_left: arrastrar a la
                        // derecha reduce el ancho de la vista previa; a la izquierda lo aumenta (más iconos / menos iconos).
                        let nw = feSplitPreviewDrag.startPreviewW - dw;
                        nw = Math.max(minPxW, Math.min(nw, maxPxW));
                        fePreviewPane.style.flex = '0 0 auto';
                        fePreviewPane.style.width = nw + 'px';
                        fePreviewPane.style.minWidth = minPxW + 'px';
                        fePreviewPane.style.maxWidth = maxPxW + 'px';
                    } else {
                        const dh = e.clientY - feSplitPreviewDrag.startY;
                        const vmDrag = feSplitPreviewDrag.mode;
                        const rawH = vmDrag === 'split_preview_top'
                            ? feSplitPreviewDrag.startPreviewH - dh
                            : feSplitPreviewDrag.startPreviewH + dh;
                        const nh = Math.max(minPxH, Math.min(rawH, maxPxH));
                        fePreviewPane.style.flex = '0 0 auto';
                        fePreviewPane.style.width = '100%';
                        fePreviewPane.style.height = nh + 'px';
                        fePreviewPane.style.minHeight = minPxH + 'px';
                        fePreviewPane.style.maxHeight = maxPxH + 'px';
                    }
                    syncExplorerItemsSplitAdaptive();
                });
                feSplitResizer.addEventListener('pointerup', endSplitDrag);
                feSplitResizer.addEventListener('pointercancel', endSplitDrag);
                window.addEventListener('resize', () => {
                    if (isFeSplitMode(viewMode)) applySplitPaneLayout();
                });
                let feSplitAdaptiveRo = null;
                if (feExplorerMainPane && typeof ResizeObserver !== 'undefined') {
                    feSplitAdaptiveRo = new ResizeObserver(() => {
                        if (isFeSplitMode(viewMode)) syncExplorerItemsSplitAdaptive();
                    });
                    feSplitAdaptiveRo.observe(feExplorerMainPane);
                }
                if (feExplorerMainPane) {
                    feExplorerMainPane.addEventListener(
                        'wheel',
                        (e) => {
                            if (!feExplorerStage || !feExplorerStage.classList.contains('fe-split-orient-hscroll')) return;
                            if (Math.abs(e.deltaY) <= Math.abs(e.deltaX)) return;
                            e.preventDefault();
                            feExplorerMainPane.scrollLeft += e.deltaY;
                        },
                        { passive: false }
                    );
                }
            }

            if (feThemeBtn) {
                feThemeBtn.addEventListener('click', (e) => {
                    e.preventDefault();
                    e.stopPropagation();
                    if (window.GridExplorerCustomization && typeof window.GridExplorerCustomization.openToolbarCustModal === 'function') {
                        window.GridExplorerCustomization.fetchCustomization(false).then(() => {
                            window.GridExplorerCustomization.openToolbarCustModal();
                        }).catch(() => {
                            initExplorerThemeModalOnce();
                            if (typeof window.openExplorerThemeModal === 'function') window.openExplorerThemeModal();
                        });
                    } else {
                        initExplorerThemeModalOnce();
                        if (typeof window.openExplorerThemeModal === 'function') window.openExplorerThemeModal();
                    }
                });
            }
            function feExitMultiSelect(opts = {}) {
                const clear = opts.clearSelection !== false;
                feToolbarMultiSelectActive = false;
                feTouchMultiSelectActive = false;
                if (clear) selectedNames.clear();
                if (feMultiSelectBtn) {
                    feMultiSelectBtn.classList.remove('active');
                    feMultiSelectBtn.title = tr('explorerMultiSelectMode');
                    feMultiSelectBtn.setAttribute('aria-pressed', 'false');
                }
                if (itemsEl) {
                    itemsEl.querySelectorAll('.explorer-item').forEach((el) => el.classList.remove('explorer-item-selected'));
                }
                refreshMultiSelectUi();
            }
            function refreshMultiSelectUi() {
                if (!feSelectionPill || !feSelectionCount) return;
                const activeMode = !!(feToolbarMultiSelectActive || feTouchMultiSelectActive);
                const n = selectedNames.size;
                const shouldShow = isLocalLibraryBucket() && n > 0 && (activeMode || n > 1);
                feSelectionPill.hidden = !shouldShow;
                if (shouldShow) feSelectionCount.textContent = tr('explorerSelectedCount').replace('%n', String(n));
            }
            if (feSelectionExitBtn) {
                feSelectionExitBtn.addEventListener('click', (e) => {
                    e.preventDefault();
                    e.stopPropagation();
                    feExitMultiSelect({ clearSelection: true });
                });
            }
            if (feMultiSelectBtn) {
                feMultiSelectBtn.addEventListener('click', (ev) => {
                    ev.stopPropagation();
                    if (!isLocalLibraryBucket()) return;
                    feToolbarMultiSelectActive = !feToolbarMultiSelectActive;
                    feMultiSelectBtn.classList.toggle('active', feToolbarMultiSelectActive);
                    feMultiSelectBtn.title = feToolbarMultiSelectActive ? tr('explorerMultiSelectExit') : tr('explorerMultiSelectMode');
                    feMultiSelectBtn.setAttribute('aria-pressed', feToolbarMultiSelectActive ? 'true' : 'false');
                    if (!feToolbarMultiSelectActive) {
                        // Al salir del modo, limpiar selección para continuar navegación normal.
                        feTouchMultiSelectActive = false;
                        selectedNames.clear();
                        itemsEl?.querySelectorAll?.('.explorer-item')?.forEach?.((el) => el.classList.remove('explorer-item-selected'));
                    }
                    refreshMultiSelectUi();
                });
            }

            function closeExplorerSidebar() {
                if (!feRoot) return;
                feRoot.classList.remove('fe-sidebar-visible');
                if (feSidebarBackdrop) feSidebarBackdrop.setAttribute('aria-hidden', 'true');
            }
            function openExplorerSidebar() {
                if (!feRoot) return;
                feRoot.classList.add('fe-sidebar-visible');
                if (feSidebarBackdrop) feSidebarBackdrop.setAttribute('aria-hidden', 'false');
            }
            function maybeCloseExplorerSidebar() {
                if (window.matchMedia('(max-width: 768px)').matches) closeExplorerSidebar();
            }
            if (feSidebarToggle) {
                feSidebarToggle.addEventListener('click', (e) => {
                    e.stopPropagation();
                    openExplorerSidebar();
                });
            }
            if (feSidebarCloseBtn) feSidebarCloseBtn.addEventListener('click', () => closeExplorerSidebar());
            if (feSidebarBackdrop) feSidebarBackdrop.addEventListener('click', () => closeExplorerSidebar());
            window.addEventListener('resize', () => {
                if (!window.matchMedia('(max-width: 768px)').matches) closeExplorerSidebar();
            });
            document.addEventListener('keydown', (e) => {
                if (e.key !== 'Escape') return;
                if (!feRoot || !feRoot.classList.contains('fe-sidebar-visible')) return;
                closeExplorerSidebar();
            });

            function formatModDate(ts) {
                if (!ts && ts !== 0) return '—';
                try {
                    const d = new Date(ts * 1000);
                    return d.toLocaleString(undefined, { year: 'numeric', month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit' });
                } catch (_) {
                    return '—';
                }
            }

            /** Ruta visible tipo árbol del explorador (no URL del servidor). */
            function formatExplorerTreeLabel(relPath, parentPath, type) {
                const pathForTree = (type === 'dir' ? relPath : parentPath) || '';
                const parts = pathForTree.split('/').filter(Boolean);
                const home = tr('explorerHome');
                if (!parts.length) return home;
                return `${home} › ${parts.join(' › ')}`;
            }

            function updateNavButtons() {
                if (bucket === 'remote') {
                    if (feBackBtn) feBackBtn.disabled = remoteHistoryCursor <= 0;
                    if (feFwdBtn) feFwdBtn.disabled = remoteHistoryCursor >= remotePathHistory.length - 1;
                    return;
                }
                if (feBackBtn) feBackBtn.disabled = historyCursor <= 0;
                if (feFwdBtn) feFwdBtn.disabled = historyCursor >= pathHistory.length - 1;
            }

            function renderBreadcrumb() {
                if (!feBC) return;
                feBC.innerHTML = '';
                if (bucket === 'trash') {
                    const addSep = () => {
                        const sep = document.createElement('span');
                        sep.className = 'fe-bc-sep';
                        sep.textContent = '›';
                        feBC.appendChild(sep);
                    };
                    const addSeg = (label, onClick, isCurrent) => {
                        const s = document.createElement('span');
                        s.className = 'fe-bc-seg' + (isCurrent ? ' fe-bc-current' : '');
                        s.textContent = label;
                        if (!isCurrent && typeof onClick === 'function') s.addEventListener('click', onClick);
                        feBC.appendChild(s);
                        if (!isCurrent) addSep();
                    };
                    addSeg(tr('explorerHome'), () => navigateToPath(''), false);
                    addSep();
                    // "Papelera" siempre vuelve a la raíz virtual de la papelera (lista de eliminados)
                    if (!feTrashCtx) {
                        const cur = document.createElement('span');
                        cur.className = 'fe-bc-seg fe-bc-current';
                        cur.textContent = tr('explorerTrash');
                        feBC.appendChild(cur);
                        return;
                    }
                    addSeg(tr('explorerTrash'), async () => {
                        feTrashCtx = null;
                        feTrashRelPath = '';
                        selectedNames.clear();
                        await renderTrashView();
                    }, false);
                    addSep();
                    // Elemento eliminado (carpeta raíz)
                    addSeg(feTrashCtx.name || '', async () => {
                        feTrashRelPath = '';
                        selectedNames.clear();
                        await renderTrashView();
                    }, !feTrashRelPath);
                    if (feTrashRelPath) {
                        const parts = feTrashRelPath.split('/').filter(Boolean);
                        let acc = '';
                        parts.forEach((p, idx) => {
                            acc = acc ? acc + '/' + p : p;
                            const isCur = idx === parts.length - 1;
                            addSep();
                            addSeg(p, async () => {
                                feTrashRelPath = acc;
                                selectedNames.clear();
                                await renderTrashView();
                            }, isCur);
                        });
                    }
                    return;
                }
                if (bucket === 'remote') {
                    const addSeg = (label, onClick, isCurrent) => {
                        const s = document.createElement('span');
                        s.className = 'fe-bc-seg' + (isCurrent ? ' fe-bc-current' : '');
                        s.textContent = label;
                        s.dataset.feBucket = 'remote';
                        // fePath se asigna por el llamador vía cierre (ver abajo) usando el onClick.
                        // Para delegación: el llamador setea fePath después.
                        if (!isCurrent) s.addEventListener('click', onClick);
                        feBC.appendChild(s);
                        const sep = document.createElement('span');
                        sep.className = 'fe-bc-sep';
                        sep.textContent = '›';
                        feBC.appendChild(sep);
                    };
                    const rootLabel = remoteCtx?.name || tr('explorerRemotePathLabel');
                    addSeg(rootLabel, () => remoteNavigateToPath('', { pushHistory: true }), !remoteRelPath);
                    if (feBC.lastChild && feBC.lastChild.previousSibling && feBC.lastChild.previousSibling.classList && feBC.lastChild.previousSibling.classList.contains('fe-bc-seg')) {
                        feBC.lastChild.previousSibling.dataset.fePath = '';
                    }
                    const parts = remoteRelPath ? remoteRelPath.split('/').filter(Boolean) : [];
                    let acc = '';
                    parts.forEach((p, idx) => {
                        acc = acc ? acc + '/' + p : p;
                        const isCurrent = idx === parts.length - 1;
                        addSeg(p, () => remoteNavigateToPath(acc, { pushHistory: true }), isCurrent);
                        // Asignar destino al último segmento agregado (el span, no el separador).
                        if (feBC.lastChild && feBC.lastChild.previousSibling && feBC.lastChild.previousSibling.classList && feBC.lastChild.previousSibling.classList.contains('fe-bc-seg')) {
                            feBC.lastChild.previousSibling.dataset.fePath = acc;
                        }
                    });
                    if (feBC.lastChild && feBC.lastChild.classList.contains('fe-bc-sep')) feBC.removeChild(feBC.lastChild);
                    return;
                }
                if (!isLocalLibraryBucket()) return;
                let rootLabel;
                if (bucket === 'community') {
                    rootLabel = communityDisplayName();
                } else if (bucket === 'department') {
                    const dept = ((explorerSharedSpaces && explorerSharedSpaces.departments && explorerSharedSpaces.departments.visibleList) || [])
                        .find((d) => d && d.id === sharedDepartmentId);
                    rootLabel = (dept && dept.name) || 'Departamento';
                } else if (bucket === 'files' && feHomeLanding) {
                    rootLabel = tr('explorerHome');
                } else if (bucket === 'files') {
                    rootLabel = tr('explorerSidebarTitle');
                } else {
                    rootLabel = tr('explorerHome');
                }
                const addSeg = (label, pathTarget, segOpts = {}) => {
                    const s = document.createElement('span');
                    s.className = 'fe-bc-seg' + (segOpts.current ? ' fe-bc-current' : '');
                    s.textContent = label;
                    s.dataset.feBucket = bucket;
                    s.dataset.fePath = pathTarget || '';
                    if (bucket === 'department') s.dataset.feDepartmentId = sharedDepartmentId || '';
                    if (segOpts.home) s.dataset.feBcAction = 'home';
                    else if (segOpts.spaceRoot) s.dataset.feBcAction = 'spaceRoot';
                    else s.dataset.feBcAction = 'path';
                    feBC.appendChild(s);
                    const sep = document.createElement('span');
                    sep.className = 'fe-bc-sep';
                    sep.textContent = '›';
                    feBC.appendChild(sep);
                };
                const showInicioPrefix = !feHomeLanding && (bucket === 'files' || bucket === 'community' || bucket === 'department');
                const q = (feSearchInput ? (feSearchInput.value || '').trim() : '');
                const showExit = !!q && explorerToolbarSearchCommitted;
                const parts = relPath ? relPath.split('/').filter(Boolean) : [];
                const atSpaceRoot = parts.length === 0;

                if (feHomeLanding && bucket === 'files') {
                    addSeg(tr('explorerHome'), '', { home: true, current: !showExit });
                } else {
                    if (showInicioPrefix) {
                        addSeg(tr('explorerHome'), '', { home: true });
                    }
                    const spaceRootClickable = bucket === 'files' || bucket === 'community' || bucket === 'department';
                    addSeg(rootLabel, '', {
                        spaceRoot: spaceRootClickable,
                        current: atSpaceRoot && !showExit
                    });
                }
                if (showExit) {
                    const exitSeg = document.createElement('span');
                    exitSeg.className = 'fe-bc-seg fe-bc-exit-search';
                    exitSeg.textContent = tr('explorerExitSearch');
                    exitSeg.addEventListener('click', () => {
                        const wasHome = feHomeLanding;
                        clearExplorerGlobalSearch();
                        if (wasHome) navigateToExplorerHome();
                        else navigateToPath('');
                    });
                    feBC.appendChild(exitSeg);
                    const sep = document.createElement('span');
                    sep.className = 'fe-bc-sep';
                    sep.textContent = '›';
                    feBC.appendChild(sep);
                }
                let acc = '';
                parts.forEach((p, idx) => {
                    acc = acc ? acc + '/' + p : p;
                    const pathTarget = acc;
                    const isCurrent = idx === parts.length - 1 && !showExit;
                    addSeg(p, pathTarget, { current: isCurrent });
                });
                if (feBC.lastChild && feBC.lastChild.classList.contains('fe-bc-sep')) feBC.removeChild(feBC.lastChild);
                const bcSegs = feBC.querySelectorAll('.fe-bc-seg:not(.fe-bc-exit-search)');
                if (bcSegs.length && !feBC.querySelector('.fe-bc-seg.fe-bc-current')) {
                    bcSegs[bcSegs.length - 1].classList.add('fe-bc-current');
                }
            }

            function updateSidebarActivePath() {
                if (!feTree) return;
                if (bucket === 'remote' && remoteCtx && remoteCtx.id) {
                    feTree.querySelectorAll('.fe-tree-node[data-remote-path]').forEach((node) => {
                        const p = node.dataset.remotePath !== undefined ? String(node.dataset.remotePath) : '';
                        node.classList.toggle('active', p === (remoteRelPath || ''));
                    });
                    const trashNode = feTree.querySelector('.fe-tree-node[data-fe-role="trash"]');
                    if (trashNode) trashNode.classList.remove('active');
                    return;
                }
                if (bucket === 'trash') {
                    feTree.querySelectorAll('.fe-tree-node[data-fe-role="root"]').forEach((n) => n.classList.remove('active'));
                    feTree.querySelectorAll('.fe-tree-node[data-path]').forEach((n) => n.classList.remove('active'));
                    const trashNode = feTree.querySelector('.fe-tree-node[data-fe-role="trash"]');
                    if (trashNode) trashNode.classList.add('active');
                    return;
                }
                const trashNode = feTree.querySelector('.fe-tree-node[data-fe-role="trash"]');
                if (trashNode) trashNode.classList.remove('active');
                const target = relPath || '';
                feTree.querySelectorAll('.fe-tree-node[data-fe-role="root"], .fe-tree-node[data-fe-role="community"], .fe-tree-node[data-fe-role="department"]').forEach((n) => n.classList.remove('active'));
                if (bucket === 'community' && !target) {
                    const commNode = feTree.querySelector('.fe-tree-node[data-fe-role="community"]');
                    if (commNode) commNode.classList.add('active');
                } else if (bucket === 'department' && !target) {
                    const deptNode = feTree.querySelector('.fe-tree-node[data-fe-role="department"][data-department-id="' + CSS.escape(sharedDepartmentId || '') + '"]');
                    if (deptNode) deptNode.classList.add('active');
                } else if (bucket === 'files' && !target && !feHomeLanding) {
                    const rootNode = feTree.querySelector('.fe-tree-node[data-fe-role="root"]');
                    if (rootNode) rootNode.classList.add('active');
                }
                feTree.querySelectorAll('.fe-tree-node[data-path]').forEach((node) => {
                    const nodeBucket = node.dataset.feTreeBucket || 'files';
                    const nodeDept = node.dataset.feTreeDepartmentId || '';
                    let bucketMatch = false;
                    if (bucket === 'community') bucketMatch = nodeBucket === 'community';
                    else if (bucket === 'department') bucketMatch = nodeBucket === 'department' && nodeDept === (sharedDepartmentId || '');
                    else if (bucket === 'files' || bucket === 'cache') bucketMatch = nodeBucket === 'files' || nodeBucket === 'cache';
                    node.classList.toggle('active', bucketMatch && node.dataset.path === target);
                });
            }

            function treeCtxMatchesNav(treeCtx) {
                if (!treeCtx || !treeCtx.bucket) return bucket === 'files' || bucket === 'cache';
                if (treeCtx.bucket !== bucket) return false;
                if (treeCtx.bucket === 'department') {
                    return (treeCtx.departmentId || '') === (sharedDepartmentId || '');
                }
                return true;
            }

            function navigateToTreePath(path, treeCtx, options = {}) {
                if (!treeCtx || !treeCtx.bucket) {
                    navigateToPath(path, options);
                    return;
                }
                feActiveTab = 'local';
                if (bucket === 'trash') {
                    bucket = lastNonTrashBucket || 'files';
                }
                bucket = treeCtx.bucket;
                if (treeCtx.bucket === 'department') {
                    sharedDepartmentId = treeCtx.departmentId || '';
                } else {
                    sharedDepartmentId = '';
                }
                lastNonTrashBucket = treeCtx.bucket;
                syncExplorerActiveContext();
                navigateToPath(path, options);
            }

            function navigateToPath(path, options = {}) {
                feActiveTab = 'local';
                if (bucket === 'trash') {
                    bucket = lastNonTrashBucket || 'files';
                }
                if (!isLocalLibraryBucket()) return;
                if (explorerBusy) return;
                const fromTree = !!(options && options.fromTree);
                const keepHomeLanding = !!(options && options.keepHomeLanding);
                if (bucket === 'files') {
                    if (path) feHomeLanding = false;
                    else if (!keepHomeLanding) feHomeLanding = false;
                }
                relPath = path || '';
                pathHistory = [''];
                let acc = '';
                if (relPath) {
                    relPath.split('/').filter(Boolean).forEach((p) => {
                        acc = acc ? acc + '/' + p : p;
                        pathHistory.push(acc);
                    });
                }
                historyCursor = pathHistory.length - 1;
                selectedNames.clear();
                clearExplorerGlobalSearch();
                syncExplorerActiveContext();
                if (fromTree) updateSidebarActivePath();
                renderFilesView({ refreshTree: !fromTree });
                maybeCloseExplorerSidebar();
                notifyExplorerStateChanged();
            }

            function clearExplorerGlobalSearch() {
                if (feSearchInput) feSearchInput.value = '';
                localTabSearchQuery = '';
                explorerToolbarSearchCommitted = false;
                localTabSearchCommitted = false;
                globalSearchRaw = null;
                globalSearchActive = false;
                globalSearchPending = false;
                searchRequestSeq++;
                if (feSearchClearBtn) feSearchClearBtn.classList.remove('visible');
                if (feRoot) feRoot.classList.remove('fe-searching');
                if (bucket === 'remote' && activeRemoteTabId) {
                    const t = remoteTabList.find((x) => x.tabId === activeRemoteTabId);
                    if (t) {
                        t.searchQuery = '';
                        t.searchCommitted = false;
                    }
                }
            }

            function updateFeSearchClearVisibility() {
                if (!feSearchClearBtn || !feSearchInput) return;
                const q = (feSearchInput.value || '').trim();
                const show = q.length > 0 && explorerToolbarSearchCommitted;
                feSearchClearBtn.classList.toggle('visible', show);
                renderBreadcrumb();
            }

            function updateFeTreeFilterClearVisibility() {
                if (!feTreeFilterClearBtn || !feTreeFilterInput) return;
                // UX: el usuario pidió que el botón "× Limpiar búsqueda" no aparezca en el árbol.
                // Mantener el filtro funcional (Escape / borrar manualmente) pero ocultar el botón.
                feTreeFilterClearBtn.classList.remove('visible');
                feTreeFilterClearBtn.setAttribute('aria-hidden', 'true');
            }

            /** Recorre FTP/WebDAV y devuelve coincidencias por nombre (carpetas y archivos). */
            async function explorerConnRecursiveSearch(connId, query, max) {
                const ql = (query || '').trim().toLowerCase();
                if (!ql) return [];
                const out = [];
                const walk = async (relPath) => {
                    if (out.length >= max) return;
                    const data = await fetchExplorerConnList(connId, relPath);
                    if (!data || !data.success || !Array.isArray(data.items)) return;
                    for (const it of data.items) {
                        if (out.length >= max) break;
                        if (!it || !it.name) continue;
                        const name = it.name;
                        if (name === '.' || name === '..') continue;
                        const fullRel = relPath ? relPath + '/' + name : name;
                        if ((name || '').toLowerCase().includes(ql)) {
                            const typ = it.type === 'dir' ? 'dir' : 'file';
                            out.push({
                                name,
                                type: typ,
                                mimeType: it.mimeType || '',
                                size: it.size || 0,
                                mtime: it.mtime || 0,
                                parentPath: relPath || '',
                                relPath: fullRel,
                                __pathLabel: formatExplorerTreeLabel(fullRel, relPath || '', typ)
                            });
                        }
                        if (it.type === 'dir') {
                            await walk(fullRel);
                        }
                    }
                };
                await walk('');
                return out;
            }

            /** Recorre el servidor y expande ramas para que existan nodos de subcarpetas al filtrar el árbol. */
            async function expandLocalTreePathsForFilter(ql, seq) {
                const lb = localListBucketName();
                const matches = [];
                const walk = async (parentPath) => {
                    if (seq !== treeFilterExpandSeq) return;
                    let data;
                    try {
                        data = await fetchExplorerList(lb, parentPath);
                    } catch (_) {
                        return;
                    }
                    if (seq !== treeFilterExpandSeq) return;
                    if (!data || !data.success || !Array.isArray(data.items)) return;
                    const dirs = data.items.filter((i) => i && i.type === 'dir');
                    for (const d of dirs) {
                        const full = normalizeJoin(parentPath, d.name);
                        const nameLow = (d.name || '').toLowerCase();
                        const fullLow = full.toLowerCase();
                        if (nameLow.includes(ql) || fullLow.includes(ql)) {
                            matches.push(full);
                        }
                        await walk(full);
                    }
                };
                await walk('');
                if (seq !== treeFilterExpandSeq) return;
                for (const p of matches) {
                    const parts = p.split('/').filter(Boolean);
                    let acc = '';
                    for (const part of parts) {
                        acc = acc ? acc + '/' + part : part;
                        feTreeExpanded.add(acc);
                    }
                }
            }

            async function expandRemoteTreePathsForFilter(ql, seq) {
                if (!remoteCtx?.id) return;
                const cid = remoteCtx.id;
                const matches = [];
                const walk = async (parentPath) => {
                    if (seq !== treeFilterExpandSeq) return;
                    let data;
                    try {
                        data = await fetchExplorerConnList(cid, parentPath);
                    } catch (_) {
                        return;
                    }
                    if (seq !== treeFilterExpandSeq) return;
                    if (!data || !Array.isArray(data.items)) return;
                    const dirs = data.items.filter((i) => i && i.type === 'dir');
                    for (const d of dirs) {
                        const full = normalizeJoin(parentPath, d.name);
                        const nameLow = (d.name || '').toLowerCase();
                        const fullLow = full.toLowerCase();
                        if (nameLow.includes(ql) || fullLow.includes(ql)) {
                            matches.push(full);
                        }
                        await walk(full);
                    }
                };
                await walk('');
                if (seq !== treeFilterExpandSeq) return;
                for (const p of matches) {
                    const parts = p.split('/').filter(Boolean);
                    let acc = '';
                    for (const part of parts) {
                        acc = acc ? acc + '/' + part : part;
                        feTreeRemoteExpanded.add(acc);
                    }
                }
            }

            async function runTreeFolderFilterExpand() {
                const seq = ++treeFilterExpandSeq;
                const q = ((feTreeFilterInput && feTreeFilterInput.value) || '').trim().toLowerCase();
                if (!q) {
                    applyFeTreeSearchFilter();
                    return;
                }
                try {
                    if (bucket === 'remote' && remoteCtx?.id) {
                        await expandRemoteTreePathsForFilter(q, seq);
                    } else if (isLocalLibraryBucket() || bucket === 'trash') {
                        await expandLocalTreePathsForFilter(q, seq);
                    } else {
                        applyFeTreeSearchFilter();
                        return;
                    }
                } catch (_) {
                    if (seq !== treeFilterExpandSeq) return;
                }
                if (seq !== treeFilterExpandSeq) return;
                await refreshSidebarTree();
            }

            /** Oculta ramas del árbol que no coinciden con el filtro de directorios del panel lateral (independiente de la búsqueda global). */
            function applyFeTreeSearchFilter() {
                if (!feTree) return;
                const sq = ((feTreeFilterInput && feTreeFilterInput.value) || '').trim().toLowerCase();
                const walk = (nodeEl) => {
                    if (!nodeEl || !nodeEl.classList || !nodeEl.classList.contains('fe-tree-node')) return false;
                    const labelEl = nodeEl.querySelector(':scope > .fe-tree-label');
                    const label = (labelEl && labelEl.textContent) ? labelEl.textContent.toLowerCase() : '';
                    const pathStr = (nodeEl.dataset.path || nodeEl.dataset.remotePath || '').toLowerCase();
                    const childrenWrap = nodeEl.querySelector(':scope > .fe-tree-children');
                    let childVisible = false;
                    if (childrenWrap) {
                        for (const ch of childrenWrap.children) {
                            if (ch.classList && ch.classList.contains('fe-tree-node')) {
                                if (walk(ch)) childVisible = true;
                            }
                        }
                    }
                    const selfMatch = !sq || label.includes(sq) || pathStr.includes(sq);
                    const visible = selfMatch || childVisible;
                    nodeEl.style.display = visible ? '' : 'none';
                    return visible;
                };
                if (!sq) {
                    feTree.querySelectorAll('.fe-tree-node').forEach((el) => {
                        el.style.display = '';
                    });
                    return;
                }
                const trash = feTree.querySelector('.fe-tree-node[data-fe-role="trash"]');
                if (trash) trash.style.display = '';
                const rootLocal = feTree.querySelector('.fe-tree-node[data-fe-role="root"]');
                if (rootLocal) rootLocal.style.display = '';
                const connRootRemote = feTree.querySelector('.fe-tree-node[data-remote-path=""]');
                if (connRootRemote && !connRootRemote.dataset.feRole) connRootRemote.style.display = '';
                const rootChildren = feTree.querySelector('.fe-tree-root-children');
                if (rootChildren) {
                    for (const ch of rootChildren.children) {
                        if (ch.classList && ch.classList.contains('fe-tree-node')) walk(ch);
                    }
                }
            }

            function setExplorerBusy(on, label, cancelOpts) {
                explorerBusy = !!on;
                if (!feRoot || !feItemsWrap) return;
                feRoot.classList.toggle('fe-busy', explorerBusy);
                const overlayId = `feBusyOverlay_${ctx.windowId}`;
                let overlay = feItemsWrap.querySelector(`#${overlayId}`);
                const cancelable = !!(explorerBusy && cancelOpts && typeof cancelOpts.onCancel === 'function');
                if (explorerBusy) {
                    if (!overlay) {
                        overlay = document.createElement('div');
                        overlay.id = overlayId;
                        overlay.className = 'fe-busy-overlay';
                        feItemsWrap.appendChild(overlay);
                    }
                    const cancelHtml = cancelable
                        ? `<button type="button" class="fe-busy-cancel-btn">${escapeHtml(tr('explorerNavCancel'))}</button>`
                        : '';
                    overlay.innerHTML = `
                        <div class="fe-busy-card" role="status" aria-live="polite">
                            <div class="fe-busy-main">
                                <div class="fe-busy-spinner" aria-hidden="true"></div>
                                <div class="fe-busy-text">${escapeHtml(label || tr('explorerLoading'))}</div>
                            </div>
                            ${cancelHtml}
                        </div>
                    `;
                    if (cancelable) {
                        const btn = overlay.querySelector('.fe-busy-cancel-btn');
                        if (btn) btn.addEventListener('click', () => { try { cancelOpts.onCancel(); } catch (_) {} });
                    }
                } else {
                    if (overlay && overlay.parentNode) overlay.parentNode.removeChild(overlay);
                }

                // Bloquear interacciones mientras carga/navega.
                if (itemsEl) itemsEl.style.pointerEvents = explorerBusy ? 'none' : '';
                if (feSearchInput) feSearchInput.disabled = explorerBusy;
                if (feTreeFilterInput) feTreeFilterInput.disabled = explorerBusy;
                host.querySelectorAll('.fe-toolbar button:not(.fe-search-clear):not(.fe-toolbar-more-btn):not(.fe-toolbar-pin-btn)').forEach((b) => {
                    if (!b) return;
                    if (explorerBusy) b.dataset.prevDisabled = b.disabled ? '1' : '0';
                    const prev = b.dataset.prevDisabled === '1';
                    b.disabled = explorerBusy ? true : prev;
                    if (!explorerBusy) delete b.dataset.prevDisabled;
                });
                host.querySelectorAll('.fe-tab-main, .fe-tab-close').forEach((b) => {
                    if (!b) return;
                    if (explorerBusy) b.dataset.prevTabDisabled = b.disabled ? '1' : '0';
                    const prev = b.dataset.prevTabDisabled === '1';
                    b.disabled = explorerBusy ? true : prev;
                    if (!explorerBusy) delete b.dataset.prevTabDisabled;
                });
                updateNavButtons();
            }

            function goHistoryBack() {
                if (explorerBusy) return;
                if (bucket === 'remote') {
                    if (remoteHistoryCursor <= 0) return;
                    remoteHistoryCursor--;
                    remoteRelPath = remotePathHistory[remoteHistoryCursor] || '';
                    clearExplorerGlobalSearch();
                    persistActiveRemoteTabState();
                    renderRemoteView();
                    return;
                }
                if (!isLocalLibraryBucket() || historyCursor <= 0) return;
                historyCursor--;
                relPath = pathHistory[historyCursor];
                selectedNames.clear();
                clearExplorerGlobalSearch();
                renderFilesView();
                maybeCloseExplorerSidebar();
            }

            function goHistoryForward() {
                if (explorerBusy) return;
                if (bucket === 'remote') {
                    if (remoteHistoryCursor >= remotePathHistory.length - 1) return;
                    remoteHistoryCursor++;
                    remoteRelPath = remotePathHistory[remoteHistoryCursor] || '';
                    clearExplorerGlobalSearch();
                    persistActiveRemoteTabState();
                    renderRemoteView();
                    return;
                }
                if (!isLocalLibraryBucket() || historyCursor >= pathHistory.length - 1) return;
                historyCursor++;
                relPath = pathHistory[historyCursor];
                selectedNames.clear();
                clearExplorerGlobalSearch();
                renderFilesView();
                maybeCloseExplorerSidebar();
            }

            function goUpFolder() {
                if (explorerBusy) return;
                if (bucket === 'remote') {
                    if (!remoteRelPath) return;
                    const parts = remoteRelPath.split('/').filter(Boolean);
                    parts.pop();
                    remoteNavigateToPath(parts.join('/'), { pushHistory: true });
                    return;
                }
                if (!isLocalLibraryBucket() || !relPath) {
                    if (!feHomeLanding && !relPath && (bucket === 'files' || bucket === 'community' || bucket === 'department')) {
                        navigateToExplorerHome();
                        return;
                    }
                    return;
                }
                const parts = relPath.split('/').filter(Boolean);
                parts.pop();
                navigateToPath(parts.join('/'));
            }

            function enterFolder(name) {
                if (!isLocalLibraryBucket()) return;
                if (explorerBusy) return;
                feActiveTab = 'local';
                feHomeLanding = false;
                const next = normalizeJoin(relPath, name);
                pathHistory = pathHistory.slice(0, historyCursor + 1);
                pathHistory.push(next);
                historyCursor = pathHistory.length - 1;
                relPath = next;
                selectedNames.clear();
                clearExplorerGlobalSearch();
                renderFilesView();
                maybeCloseExplorerSidebar();
                notifyExplorerStateChanged();
            }

            function remoteNavigateToPath(path, options = {}) {
                if (bucket !== 'remote') return;
                if (explorerBusy) return;
                clearExplorerGlobalSearch();
                const next = explorerNormalizeRelPath(String(path || ''));
                remoteRelPath = next;
                if (options.pushHistory) {
                    remotePathHistory = remotePathHistory.slice(0, remoteHistoryCursor + 1);
                    remotePathHistory.push(next);
                    remoteHistoryCursor = remotePathHistory.length - 1;
                }
                persistActiveRemoteTabState();
                renderRemoteView();
                notifyExplorerStateChanged();
            }

            function enterRemoteFolder(name) {
                if (bucket !== 'remote') return;
                if (explorerBusy) return;
                clearExplorerGlobalSearch();
                const next = normalizeJoin(remoteRelPath, name);
                remotePathHistory = remotePathHistory.slice(0, remoteHistoryCursor + 1);
                remotePathHistory.push(next);
                remoteHistoryCursor = remotePathHistory.length - 1;
                remoteRelPath = next;
                persistActiveRemoteTabState();
                renderRemoteView();
                notifyExplorerStateChanged();
            }

            async function openTrashBucketFromTree() {
                if (bucket === 'trash') {
                    feTrashCtx = null;
                    feTrashRelPath = '';
                    await renderTrashView();
                    return;
                }
                abortExplorerPendingLoads();
                if (bucket === 'remote') {
                    persistActiveRemoteTabState();
                    lastNonTrashBucket = 'remote';
                } else {
                    lastNonTrashBucket = bucket === 'cache' ? 'cache' : 'files';
                }
                bucket = 'trash';
                feTrashCtx = null;
                feTrashRelPath = '';
                await renderTrashView();
                notifyExplorerStateChanged();
            }

            /** Árbol lateral para conexión remota (FTP/WebDAV): carpetas bajo la ruta actual. */
            async function buildRemoteSidebarTreeLevel(container, parentPath, refreshSeq) {
                if (!remoteCtx?.id) return;
                const data = await fetchExplorerConnList(remoteCtx.id, parentPath);
                if (refreshSeq !== treeRefreshSeq) return;
                const dirs = (data.items || []).filter((i) => i && i.type === 'dir').sort((a, b) =>
                    (a.name || '').localeCompare(b.name || '', undefined, { sensitivity: 'base' }));
                for (const d of dirs) {
                    const full = normalizeJoin(parentPath, d.name);
                    const row = document.createElement('div');
                    const isActive = (remoteRelPath || '') === full;
                    const isBranch = remoteRelPath && (remoteRelPath === full || remoteRelPath.startsWith(full + '/'));
                    const shouldExpand = isBranch || (feTreeRemoteExpanded && feTreeRemoteExpanded.has(full));
                    row.className = 'fe-tree-node' + (isActive ? ' active' : '') + (shouldExpand && !isActive ? ' fe-tree-branch-open' : '') + (shouldExpand ? ' fe-tree-open' : '');
                    row.dataset.remotePath = full;
                    const exp = document.createElement('span');
                    exp.className = 'fe-tree-expander';
                    exp.setAttribute('role', 'button');
                    exp.setAttribute('tabindex', '0');
                    exp.textContent = shouldExpand ? '−' : '+';
                    exp.setAttribute('aria-disabled', 'true');
                    const icon = document.createElement('i');
                    icon.className = 'fas fa-folder fe-tree-icon';
                    icon.setAttribute('aria-hidden', 'true');
                    const lab = document.createElement('span');
                    lab.className = 'fe-tree-label';
                    lab.textContent = d.name;
                    row.appendChild(exp);
                    row.appendChild(icon);
                    row.appendChild(lab);
                    row.addEventListener('click', (e) => {
                        e.stopPropagation();
                        remoteNavigateToPath(full, { pushHistory: true });
                        maybeCloseExplorerSidebar();
                    });
                    row.addEventListener('contextmenu', (e) => {
                        if (!remoteCtx?.id) return;
                        e.preventDefault();
                        e.stopPropagation();
                        const opts = [
                            { className: 'open', icon: 'fas fa-folder-open', label: tr('open'), action: () => {
                                remoteNavigateToPath(full, { pushHistory: true });
                                maybeCloseExplorerSidebar();
                            }},
                            { className: 'fe-folder-info', icon: 'fas fa-info-circle', label: tr('explorerProperties'), action: () => { void openExplorerRemoteFolderInfoFromPath(parentPath, d.name); } },
                            { className: 'rename', icon: 'fas fa-edit', label: tr('explorerRename'), action: async () => {
                                const newName = await showExplorerRenameModal(d.name, { isDirectory: true });
                                if (!newName || newName === d.name) return;
                                explorerConnRename(remoteCtx.id, parentPath, d.name, newName).then((r) => {
                                    if (r && r.success) {
                                        logActivity('explorer_remote_rename', { connId: remoteCtx.id, path: parentPath, from: d.name, to: newName });
                                        remapRemotePathsAfterDirRename(parentPath, d.name, newName);
                                        persistActiveRemoteTabState();
                                        feSyncSelectionAfterItemRename(d.name, newName);
                                        fePostRenderFocusItemName = newName;
                                        void renderRemoteView();
                                    }
                                });
                            }}
                        ];
                        showContextMenu(opts, e);
                    }, true);
                    const ensureHasChildren = async () => {
                        if (feTreeRemoteHasChildrenCache.has(full)) return !!feTreeRemoteHasChildrenCache.get(full);
                        try {
                            const child = await fetchExplorerConnList(remoteCtx.id, full);
                            if (refreshSeq !== treeRefreshSeq) return false;
                            const has = (child.items || []).some((i) => i && i.type === 'dir');
                            feTreeRemoteHasChildrenCache.set(full, has);
                            return has;
                        } catch (_) {
                            feTreeRemoteHasChildrenCache.set(full, false);
                            return false;
                        }
                    };
                    const updateExpanderVisibility = async () => {
                        const has = await ensureHasChildren();
                        exp.setAttribute('aria-disabled', has ? 'false' : 'true');
                        if (!has) {
                            if (feTreeRemoteExpanded) feTreeRemoteExpanded.delete(full);
                            row.classList.remove('fe-tree-open');
                        }
                    };
                    await updateExpanderVisibility();

                    const toggleExpand = async () => {
                        const has = await ensureHasChildren();
                        if (!has) return;
                        const open = !(feTreeRemoteExpanded && feTreeRemoteExpanded.has(full));
                        if (feTreeRemoteExpanded) {
                            if (open) feTreeRemoteExpanded.add(full);
                            else feTreeRemoteExpanded.delete(full);
                        }
                        refreshSidebarTree();
                    };
                    exp.addEventListener('click', async (e) => {
                        e.preventDefault();
                        e.stopPropagation();
                        await toggleExpand();
                    });
                    exp.addEventListener('keydown', async (e) => {
                        if (e.key !== 'Enter' && e.key !== ' ') return;
                        e.preventDefault();
                        e.stopPropagation();
                        await toggleExpand();
                    });
                    container.appendChild(row);

                    if (shouldExpand) {
                        const childWrap = document.createElement('div');
                        childWrap.className = 'fe-tree-children';
                        await buildRemoteSidebarTreeLevel(childWrap, full, refreshSeq);
                        if (refreshSeq !== treeRefreshSeq) return;
                        container.appendChild(childWrap);
                    }
                }
            }

            function restoreFeTreeScroll(savedTop, savedLeft) {
                if (!feTree) return;
                const top = Math.max(0, Number(savedTop) || 0);
                const left = Math.max(0, Number(savedLeft) || 0);
                const apply = () => {
                    if (!feTree) return;
                    feTree.scrollTop = top;
                    feTree.scrollLeft = left;
                };
                apply();
                requestAnimationFrame(() => {
                    apply();
                    requestAnimationFrame(apply);
                });
            }

            async function refreshSidebarTree() {
                if (!feTree) return;
                const refreshSeq = ++treeRefreshSeq;
                const prevScrollTop = feTree.scrollTop;
                const prevScrollLeft = feTree.scrollLeft;
                if (bucket === 'remote' && remoteCtx && remoteCtx.id) {
                    const trashRow = document.createElement('div');
                    trashRow.dataset.feRole = 'trash';
                    trashRow.className = 'fe-tree-node fe-tree-trash-node';
                    trashRow.innerHTML = '<i class="fas fa-trash-alt fe-tree-icon" aria-hidden="true"></i><span class="fe-tree-label">' + tr('explorerTrash') + '</span>';
                    trashRow.addEventListener('click', (e) => {
                        e.stopPropagation();
                        maybeCloseExplorerSidebar();
                        void openTrashBucketFromTree();
                    });
                    const connRoot = document.createElement('div');
                    connRoot.className = 'fe-tree-node';
                    connRoot.setAttribute('data-remote-path', '');
                    connRoot.innerHTML = '<i class="fas fa-network-wired fe-tree-icon" aria-hidden="true"></i><span class="fe-tree-label">' + (remoteCtx.name || tr('explorerConnections')) + '</span>';
                    connRoot.addEventListener('click', (e) => {
                        e.stopPropagation();
                        remoteNavigateToPath('', { pushHistory: true });
                        maybeCloseExplorerSidebar();
                    });
                    try {
                        const sub = document.createElement('div');
                        sub.className = 'fe-tree-children fe-tree-root-children';
                        await buildRemoteSidebarTreeLevel(sub, '', refreshSeq);
                        if (refreshSeq !== treeRefreshSeq) return;
                        feTree.replaceChildren(trashRow, connRoot, sub);
                    } catch (_) {
                        feTree.replaceChildren(trashRow, connRoot);
                    }
                    restoreFeTreeScroll(prevScrollTop, prevScrollLeft);
                    updateSidebarActivePath();
                    applyFeTreeSearchFilter();
                    return;
                }

                const trashRow = document.createElement('div');
                trashRow.dataset.feRole = 'trash';
                trashRow.className = 'fe-tree-node fe-tree-trash-node';
                trashRow.innerHTML = '<i class="fas fa-trash-alt fe-tree-icon" aria-hidden="true"></i><span class="fe-tree-label">' + tr('explorerTrash') + '</span>';
                trashRow.addEventListener('click', (e) => {
                    e.stopPropagation();
                    maybeCloseExplorerSidebar();
                    void openTrashBucketFromTree();
                });
                trashRow.addEventListener('dragover', (e) => {
                    if (!explorerDragPayload || !isLocalLibraryBucket()) return;
                    e.preventDefault();
                    e.dataTransfer.dropEffect = 'move';
                    trashRow.classList.add('explorer-drop-target');
                });
                trashRow.addEventListener('dragenter', (e) => {
                    if (!explorerDragPayload || !isLocalLibraryBucket()) return;
                    trashRow.classList.add('explorer-drop-target');
                });
                trashRow.addEventListener('dragleave', () => trashRow.classList.remove('explorer-drop-target'));
                trashRow.addEventListener('drop', async (e) => {
                    e.preventDefault();
                    e.stopPropagation();
                    trashRow.classList.remove('explorer-drop-target');
                    if (!explorerDragPayload || !isLocalLibraryBucket()) return;
                    const names = explorerDragPayload.names && explorerDragPayload.names.length
                        ? explorerDragPayload.names
                        : [explorerDragPayload.name];
                    const fromB = explorerDragPayload && explorerDragPayload.fromBucket ? explorerDragPayload.fromBucket : localListBucketName();
                    const fromPath = explorerDragPayload && explorerDragPayload.fromPath !== undefined ? explorerDragPayload.fromPath : (relPath || '');
                    const list = Array.isArray(names) ? names.filter(Boolean) : [];
                    if (!list.length) return;
                    const okMsg = list.length === 1
                        ? tr('explorerConfirmDeleteToTrash')
                        : tr('explorerConfirmDeleteTrashMany').replace('%n', String(list.length));
                    if (!(await appDialog.confirm(okMsg))) return;
                    for (const nm of list) {
                        try {
                            const r = await explorerDelete(fromPath, nm, fromB);
                            if (r && r.success) logActivity('explorer_delete_to_trash', { path: fromPath, name: nm });
                        } catch (_) {}
                    }
                    selectedNames.clear();
                    await fetchExplorerListTrash().catch(() => {});
                    renderTrashItems();
                    updateRecycleBinIcon();
                    await renderFilesView();
                });
                trashRow.addEventListener('contextmenu', (e) => {
                    e.preventDefault();
                    e.stopPropagation();
                    const opts = [
                        { className: 'open', icon: 'fas fa-external-link-alt', label: tr('open'), action: () => {
                            maybeCloseExplorerSidebar();
                            void openTrashBucketFromTree();
                        }},
                        { className: 'refresh', icon: 'fas fa-sync', label: tr('explorerRefresh'), action: async () => {
                            await fetchExplorerListTrash();
                            if (bucket === 'trash') await renderTrashView();
                            renderTrashItems();
                            updateRecycleBinIcon();
                        }}
                    ];
                    if (Array.isArray(fileExplorerTrash) && fileExplorerTrash.length > 0) {
                        opts.push({
                            className: 'empty-explorer-trash',
                            icon: 'fas fa-trash-alt',
                            label: tr('emptyTrashBtn'),
                            action: async () => {
                                if (!(await appDialog.confirm(tr('recycleEmptyConfirm')))) return;
                                const r = await explorerEmptyTrash();
                                if (r && r.success) {
                                    await fetchExplorerListTrash();
                                    if (bucket === 'trash') await renderTrashView();
                                    renderTrashItems();
                                    updateRecycleBinIcon();
                                    logActivity('explorer_empty_trash', {});
                                    showToast(tr('explorerStatusOk'));
                                }
                            }
                        });
                    }
                    showContextMenu(opts, e);
                }, true);
                const root = document.createElement('div');
                root.dataset.feRole = 'root';
                root.className = 'fe-tree-node';
                root.innerHTML = '<i class="fas fa-hdd fe-tree-icon" aria-hidden="true"></i><span class="fe-tree-label">' + tr('explorerSidebarTitle') + '</span>';
                root.addEventListener('click', () => navigateToSharedRoot('files', '', { fromTree: true }));
                root.addEventListener('dragover', (e) => {
                    if (!explorerDragPayload || !isLocalLibraryBucket()) return;
                    if (!feExplorerLocalDropAllowedOnPath('')) return;
                    e.preventDefault();
                    e.dataTransfer.dropEffect = explorerDragPayload.mode === 'copy' ? 'copy' : 'move';
                    root.classList.add('explorer-drop-target');
                });
                root.addEventListener('dragenter', (e) => {
                    if (!explorerDragPayload || !isLocalLibraryBucket()) return;
                    if (!feExplorerLocalDropAllowedOnPath('')) return;
                    root.classList.add('explorer-drop-target');
                });
                root.addEventListener('dragleave', () => root.classList.remove('explorer-drop-target'));
                root.addEventListener('drop', async (e) => {
                    e.preventDefault();
                    e.stopPropagation();
                    root.classList.remove('explorer-drop-target');
                    if (!explorerDragPayload || !isLocalLibraryBucket()) return;
                    const names = explorerDragPayload.names && explorerDragPayload.names.length
                        ? explorerDragPayload.names
                        : [explorerDragPayload.name];
                    const isCopy = explorerDragPayload.mode === 'copy';
                    await executeExplorerDropToPath('', names, isCopy);
                });
                root.addEventListener('contextmenu', (e) => {
                    if (!isLocalLibraryBucket()) return;
                    e.preventDefault();
                    e.stopPropagation();
                    const opts = [];
                    if (isPersonalFilesBucket()) {
                        opts.push({ className: 'explorer-new-folder-ctx', icon: 'fas fa-folder-plus', label: tr('explorerNewFolder'), action: () => { void promptAndCreateLocalFolder(''); } });
                    }
                    if (fileExplorerClipboardHasPasteableItems()) {
                        opts.push({ className: 'clip-paste', icon: 'fas fa-paste', label: tr('explorerPaste'), action: () => { pasteExplorerClipboard(''); } });
                    }
                    showContextMenu(opts, e);
                }, true);
                try {
                    await loadExplorerSharedSpaces(false);
                    const treeNodes = [trashRow, root];
                    const sub = document.createElement('div');
                    sub.className = 'fe-tree-children fe-tree-root-children';
                    const prevBucket = bucket;
                    const prevDept = sharedDepartmentId;
                    bucket = 'files';
                    sharedDepartmentId = '';
                    syncExplorerActiveContext();
                    await buildSidebarTreeLevel(sub, '', { bucket: 'files', departmentId: '' });
                    bucket = prevBucket;
                    sharedDepartmentId = prevDept;
                    syncExplorerActiveContext();
                    if (refreshSeq !== treeRefreshSeq) return;
                    treeNodes.push(sub);

                    const showCommunity = explorerSharedSpaces && explorerSharedSpaces.community && explorerSharedSpaces.community.visible;
                    if (showCommunity) {
                        const communityRoot = document.createElement('div');
                        communityRoot.dataset.feRole = 'community';
                        communityRoot.className = 'fe-tree-node fe-tree-shared-node';
                        communityRoot.innerHTML = '<i class="fas fa-users fe-tree-icon" aria-hidden="true"></i><span class="fe-tree-label"></span>';
                        const commLabel = communityRoot.querySelector('.fe-tree-label');
                        if (commLabel) commLabel.textContent = communityDisplayName();
                        communityRoot.addEventListener('click', () => navigateToSharedRoot('community', '', { fromTree: true }));
                        communityRoot.addEventListener('contextmenu', (e) => {
                            e.preventDefault();
                            e.stopPropagation();
                            const opts = [
                                { className: 'open', icon: 'fas fa-external-link-alt', label: tr('open'), action: () => navigateToSharedRoot('community', '', { fromTree: true }) },
                            ];
                            if (explorerSharedSpaces && explorerSharedSpaces.canRenameCommunity) {
                                opts.push({
                                    className: 'rename',
                                    icon: 'fas fa-i-cursor',
                                    label: 'Renombrar espacio comunitario',
                                    action: async () => {
                                        const cur = communityDisplayName();
                                        const next = await appDialog.prompt('Nombre visible del espacio comunitario (para todos los usuarios):', cur);
                                        if (!next || String(next).trim() === '' || String(next).trim() === cur) return;
                                        if (!window.GridExplorerSharedSpaces) return;
                                        try {
                                            await window.GridExplorerSharedSpaces.save({ communityDisplayName: String(next).trim() });
                                            await loadExplorerSharedSpaces(true);
                                            await refreshSidebarTree();
                                            if (bucket === 'community') renderBreadcrumb();
                                        } catch (err) {
                                            await appDialog.alert(String(err && err.message ? err.message : err));
                                        }
                                    }
                                });
                            }
                            if (bucket === 'community') {
                                opts.push({ className: 'explorer-new-folder-ctx', icon: 'fas fa-folder-plus', label: tr('explorerNewFolder'), action: () => { void promptAndCreateLocalFolder(''); } });
                            }
                            showContextMenu(opts, e);
                        }, true);
                        const commSub = document.createElement('div');
                        commSub.className = 'fe-tree-children fe-tree-root-children';
                        const pb = bucket; const pd = sharedDepartmentId;
                        bucket = 'community'; sharedDepartmentId = ''; syncExplorerActiveContext();
                        await buildSidebarTreeLevel(commSub, '', { bucket: 'community', departmentId: '' });
                        bucket = pb; sharedDepartmentId = pd; syncExplorerActiveContext();
                        treeNodes.push(communityRoot, commSub);
                    }

                    const deptList = (explorerSharedSpaces && explorerSharedSpaces.departments && explorerSharedSpaces.departments.visibleList) || [];
                    for (const dept of deptList) {
                        if (!dept || !dept.id) continue;
                        const deptRoot = document.createElement('div');
                        deptRoot.dataset.feRole = 'department';
                        deptRoot.dataset.departmentId = dept.id;
                        deptRoot.className = 'fe-tree-node fe-tree-shared-node';
                        deptRoot.innerHTML = '<i class="fas fa-building fe-tree-icon" aria-hidden="true"></i><span class="fe-tree-label"></span>';
                        const deptLabel = deptRoot.querySelector('.fe-tree-label');
                        if (deptLabel) deptLabel.textContent = dept.name || 'Departamento';
                        const deptId = dept.id;
                        deptRoot.addEventListener('click', () => navigateToSharedRoot('department', deptId, { fromTree: true }));
                        deptRoot.addEventListener('contextmenu', (e) => {
                            e.preventDefault();
                            e.stopPropagation();
                            const opts = [
                                { className: 'open', icon: 'fas fa-external-link-alt', label: tr('open'), action: () => navigateToSharedRoot('department', deptId, { fromTree: true }) },
                            ];
                            if (bucket === 'department' && sharedDepartmentId === deptId) {
                                opts.push({ className: 'explorer-new-folder-ctx', icon: 'fas fa-folder-plus', label: tr('explorerNewFolder'), action: () => { void promptAndCreateLocalFolder(''); } });
                            }
                            showContextMenu(opts, e);
                        }, true);
                        const deptSub = document.createElement('div');
                        deptSub.className = 'fe-tree-children fe-tree-root-children';
                        const pb2 = bucket; const pd2 = sharedDepartmentId;
                        bucket = 'department'; sharedDepartmentId = deptId; syncExplorerActiveContext();
                        await buildSidebarTreeLevel(deptSub, '', { bucket: 'department', departmentId: deptId });
                        bucket = pb2; sharedDepartmentId = pd2; syncExplorerActiveContext();
                        treeNodes.push(deptRoot, deptSub);
                    }

                    feTree.replaceChildren(...treeNodes);
                } catch (_) {
                    feTree.replaceChildren(trashRow, root);
                }
                restoreFeTreeScroll(prevScrollTop, prevScrollLeft);
                updateSidebarActivePath();
                applyFeTreeSearchFilter();
            }

            /** Árbol recursivo: lista todas las carpetas del nivel y expande la rama hasta la carpeta actual. */
            async function buildSidebarTreeLevel(container, parentPath, treeCtx) {
                const ctx = treeCtx && treeCtx.bucket ? treeCtx : { bucket: 'files', departmentId: '' };
                const data = await fetchExplorerList(
                    ctx.bucket === 'cache' ? 'cache' : (ctx.bucket === 'community' ? 'community' : (ctx.bucket === 'department' ? 'department' : 'files')),
                    parentPath,
                    ctx.bucket === 'department' ? (ctx.departmentId || '') : ''
                );
                const dirs = (data.items || []).filter((i) => i.type === 'dir').sort((a, b) =>
                    (a.name || '').localeCompare(b.name || '', undefined, { sensitivity: 'base' }));
                for (const d of dirs) {
                    const full = normalizeJoin(parentPath, d.name);
                    const row = document.createElement('div');
                    const isActive = treeCtxMatchesNav(ctx) && relPath === full;
                    const isBranch = treeCtxMatchesNav(ctx) && relPath && (relPath === full || relPath.startsWith(full + '/'));
                    const shouldExpand = isBranch || (feTreeExpanded && feTreeExpanded.has(full));
                    row.className = 'fe-tree-node' + (isActive ? ' active' : '') + (shouldExpand && !isActive ? ' fe-tree-branch-open' : '') + (shouldExpand ? ' fe-tree-open' : '');
                    row.dataset.path = full;
                    row.dataset.feTreeBucket = ctx.bucket;
                    if (ctx.bucket === 'department') row.dataset.feTreeDepartmentId = ctx.departmentId || '';
                    const exp = document.createElement('span');
                    exp.className = 'fe-tree-expander';
                    exp.setAttribute('role', 'button');
                    exp.setAttribute('tabindex', '0');
                    exp.setAttribute('aria-label', shouldExpand ? 'Contraer carpeta' : 'Expandir carpeta');
                    exp.textContent = shouldExpand ? '−' : '+';
                    exp.setAttribute('aria-disabled', 'true');
                    const icon = document.createElement('i');
                    icon.className = 'fas fa-folder fe-tree-icon';
                    icon.setAttribute('aria-hidden', 'true');
                    const lab = document.createElement('span');
                    lab.className = 'fe-tree-label';
                    lab.textContent = d.name;
                    row.appendChild(exp);
                    row.appendChild(icon);
                    row.appendChild(lab);
                    row.addEventListener('click', (e) => {
                        e.stopPropagation();
                        navigateToTreePath(full, ctx, { fromTree: true });
                    });
                    const treeCacheKey = (ctx.bucket === 'department' ? 'dept:' + (ctx.departmentId || '') + ':' : ctx.bucket + ':') + full;
                    const ensureHasChildren = async () => {
                        if (!feTreeHasChildrenCache) return false;
                        if (feTreeHasChildrenCache.has(treeCacheKey)) return !!feTreeHasChildrenCache.get(treeCacheKey);
                        try {
                            const child = await fetchExplorerList(
                                ctx.bucket === 'cache' ? 'cache' : (ctx.bucket === 'community' ? 'community' : (ctx.bucket === 'department' ? 'department' : 'files')),
                                full,
                                ctx.bucket === 'department' ? (ctx.departmentId || '') : ''
                            );
                            const has = (child.items || []).some((i) => i && i.type === 'dir');
                            feTreeHasChildrenCache.set(treeCacheKey, has);
                            return has;
                        } catch (_) {
                            feTreeHasChildrenCache.set(treeCacheKey, false);
                            return false;
                        }
                    };
                    const updateExpanderVisibility = async () => {
                        const has = await ensureHasChildren();
                        exp.setAttribute('aria-disabled', has ? 'false' : 'true');
                        if (!has) {
                            // Si no tiene hijos, asegúrate de que no quede "abierta".
                            if (feTreeExpanded) feTreeExpanded.delete(full);
                            row.classList.remove('fe-tree-open');
                        }
                    };
                    updateExpanderVisibility();

                    const toggleExpand = async () => {
                        const has = await ensureHasChildren();
                        if (!has) return;
                        const open = !(feTreeExpanded && feTreeExpanded.has(full));
                        if (feTreeExpanded) {
                            if (open) feTreeExpanded.add(full);
                            else feTreeExpanded.delete(full);
                        }
                        refreshSidebarTree();
                    };
                    exp.addEventListener('click', async (e) => {
                        e.preventDefault();
                        e.stopPropagation();
                        await toggleExpand();
                    });
                    exp.addEventListener('keydown', async (e) => {
                        if (e.key !== 'Enter' && e.key !== ' ') return;
                        e.preventDefault();
                        e.stopPropagation();
                        await toggleExpand();
                    });
                    row.addEventListener('dragover', (e) => {
                        if (!explorerDragPayload || !isLocalLibraryBucket()) return;
                        if (!feExplorerLocalDropAllowedOnPath(full)) return;
                        e.preventDefault();
                        e.dataTransfer.dropEffect = explorerDragPayload.mode === 'copy' ? 'copy' : 'move';
                        row.classList.add('explorer-drop-target');
                    });
                    row.addEventListener('dragenter', (e) => {
                        if (!explorerDragPayload || !isLocalLibraryBucket()) return;
                        if (!feExplorerLocalDropAllowedOnPath(full)) return;
                        row.classList.add('explorer-drop-target');
                    });
                    row.addEventListener('dragleave', () => row.classList.remove('explorer-drop-target'));
                    row.addEventListener('drop', async (e) => {
                        e.preventDefault();
                        e.stopPropagation();
                        row.classList.remove('explorer-drop-target');
                        if (!explorerDragPayload || !isLocalLibraryBucket()) return;
                        const names = explorerDragPayload.names && explorerDragPayload.names.length
                            ? explorerDragPayload.names
                            : [explorerDragPayload.name];
                        const isCopy = explorerDragPayload.mode === 'copy';
                        await executeExplorerDropToPath(full, names, isCopy);
                    });
                    row.setAttribute('draggable', 'true');
                    row.addEventListener('dragstart', (e) => {
                        if (e.target && e.target.closest && e.target.closest('.fe-tree-expander')) {
                            e.preventDefault();
                            return;
                        }
                        if (!['files', 'cache', 'community', 'department'].includes(ctx.bucket)) return;
                        e.stopPropagation();
                        const mode = e.ctrlKey ? 'copy' : 'move';
                        explorerDragPayload = {
                            fromPath: parentPath,
                            name: d.name,
                            names: [d.name],
                            mode,
                            fromBucket: ctx.bucket === 'cache' ? 'cache' : (ctx.bucket === 'community' ? 'community' : (ctx.bucket === 'department' ? 'department' : 'files')),
                            fromDepartmentId: ctx.bucket === 'department' ? (ctx.departmentId || '') : '',
                            kind: 'dir'
                        };
                        e.dataTransfer.effectAllowed = mode === 'copy' ? 'copy' : 'move';
                        e.dataTransfer.setData('text/plain', d.name);
                        row.classList.add('fe-tree-dragging');
                    });
                    row.addEventListener('dragend', () => {
                        row.classList.remove('fe-tree-dragging');
                        if (feTree) feTree.querySelectorAll('.explorer-drop-target').forEach((el) => el.classList.remove('explorer-drop-target'));
                        explorerDragPayload = null;
                    });
                    row.addEventListener('contextmenu', (e) => {
                        e.preventDefault();
                        e.stopPropagation();
                        showContextMenu(
                            buildLocalExplorerDirContextMenuOptions(parentPath, d.name, () => navigateToTreePath(full, ctx, { fromTree: true })),
                            e
                        );
                    }, true);
                    container.appendChild(row);

                    if (shouldExpand) {
                        const childWrap = document.createElement('div');
                        childWrap.className = 'fe-tree-children';
                        await buildSidebarTreeLevel(childWrap, full, ctx);
                        container.appendChild(childWrap);
                    }
                }
            }

            if (feBackBtn) feBackBtn.addEventListener('click', () => goHistoryBack());
            if (feFwdBtn) feFwdBtn.addEventListener('click', () => goHistoryForward());
            if (feSearchInput) {
                const persistExplorerSearchForActiveTab = () => {
                    if (feActiveTab === 'local' && (bucket === 'files' || bucket === 'cache' || bucket === 'trash')) {
                        localTabSearchQuery = feSearchInput.value || '';
                    }
                    if (bucket === 'remote' && activeRemoteTabId) {
                        const t = remoteTabList.find((x) => x.tabId === activeRemoteTabId);
                        if (t) t.searchQuery = feSearchInput.value || '';
                    }
                };
                const runExplorerSearch = async () => {
                    persistExplorerSearchForActiveTab();
                    const q = (feSearchInput.value || '').trim();
                    if (!q) {
                        explorerToolbarSearchCommitted = false;
                        localTabSearchCommitted = false;
                        searchRequestSeq++;
                        globalSearchPending = false;
                        globalSearchActive = false;
                        globalSearchRaw = null;
                        if (feRoot) feRoot.classList.remove('fe-searching');
                        if (bucket === 'remote' && activeRemoteTabId) {
                            const t0 = remoteTabList.find((x) => x.tabId === activeRemoteTabId);
                            if (t0) t0.searchCommitted = false;
                        }
                        renderItems();
                        applyFeTreeSearchFilter();
                        updateFeSearchClearVisibility();
                        return;
                    }
                    if (bucket === 'trash') {
                        globalSearchPending = false;
                        globalSearchActive = false;
                        globalSearchRaw = null;
                        explorerToolbarSearchCommitted = true;
                        localTabSearchCommitted = true;
                        renderItems();
                        applyFeTreeSearchFilter();
                        updateFeSearchClearVisibility();
                        return;
                    }
                    if (bucket !== 'files' && bucket !== 'cache' && bucket !== 'remote') {
                        explorerToolbarSearchCommitted = true;
                        renderItems();
                        updateFeSearchClearVisibility();
                        return;
                    }
                    const reqId = ++searchRequestSeq;
                    globalSearchPending = true;
                    globalSearchActive = false;
                    globalSearchRaw = null;
                    if (feRoot) feRoot.classList.add('fe-searching');
                    renderItems();
                    updateFeSearchClearVisibility();
                    try {
                        if (bucket === 'files' || bucket === 'cache') {
                            const data = await explorerSearchGlobal(q, bucket);
                            if (reqId !== searchRequestSeq) return;
                            globalSearchRaw = data && data.success && Array.isArray(data.items) ? data.items : [];
                            globalSearchActive = true;
                        } else if (bucket === 'remote' && remoteCtx?.id) {
                            globalSearchRaw = await explorerConnRecursiveSearch(remoteCtx.id, q, 400);
                            if (reqId !== searchRequestSeq) return;
                            globalSearchActive = true;
                        }
                    } catch (_) {
                        if (reqId !== searchRequestSeq) return;
                        globalSearchRaw = [];
                        globalSearchActive = true;
                    } finally {
                        if (reqId !== searchRequestSeq) return;
                        globalSearchPending = false;
                        if (feRoot) feRoot.classList.remove('fe-searching');
                        explorerToolbarSearchCommitted = true;
                        if (feActiveTab === 'local') localTabSearchCommitted = true;
                        if (bucket === 'remote' && activeRemoteTabId) {
                            const t1 = remoteTabList.find((x) => x.tabId === activeRemoteTabId);
                            if (t1) t1.searchCommitted = true;
                        }
                        renderItems();
                        applyFeTreeSearchFilter();
                        updateFeSearchClearVisibility();
                    }
                };

                feSearchInput.addEventListener('input', () => {
                    explorerToolbarSearchCommitted = false;
                    localTabSearchCommitted = false;
                    if (bucket === 'remote' && activeRemoteTabId) {
                        const tx = remoteTabList.find((x) => x.tabId === activeRemoteTabId);
                        if (tx) tx.searchCommitted = false;
                    }
                    persistExplorerSearchForActiveTab();
                    updateFeSearchClearVisibility();
                    clearTimeout(searchDebounceTimer);
                    searchDebounceTimer = setTimeout(() => { void runExplorerSearch(); }, 220);
                });
                feSearchInput.addEventListener('search', () => {
                    clearTimeout(searchDebounceTimer);
                    void runExplorerSearch();
                });
                feSearchInput.addEventListener('keydown', (e) => {
                    e.stopPropagation();
                    if (e.key === 'Escape') {
                        e.preventDefault();
                        clearTimeout(searchDebounceTimer);
                        clearExplorerGlobalSearch();
                        persistExplorerSearchForActiveTab();
                        updateFeSearchClearVisibility();
                        renderItems();
                        applyFeTreeSearchFilter();
                        return;
                    }
                    if (e.key === 'Enter') {
                        e.preventDefault();
                        clearTimeout(searchDebounceTimer);
                        void runExplorerSearch();
                    }
                });
                if (feSearchClearBtn) {
                    feSearchClearBtn.addEventListener('click', (e) => {
                        e.preventDefault();
                        e.stopPropagation();
                        clearTimeout(searchDebounceTimer);
                        clearExplorerGlobalSearch();
                        persistExplorerSearchForActiveTab();
                        updateFeSearchClearVisibility();
                        renderItems();
                        applyFeTreeSearchFilter();
                        try { feSearchInput.focus(); } catch (_) {}
                    });
                }
            }
            if (feTreeFilterInput) {
                feTreeFilterInput.addEventListener('input', () => {
                    updateFeTreeFilterClearVisibility();
                    clearTimeout(treeFilterDebounceTimer);
                    treeFilterDebounceTimer = setTimeout(() => { void runTreeFolderFilterExpand(); }, 180);
                });
                feTreeFilterInput.addEventListener('search', () => {
                    clearTimeout(treeFilterDebounceTimer);
                    void runTreeFolderFilterExpand();
                    updateFeTreeFilterClearVisibility();
                });
                feTreeFilterInput.addEventListener('keydown', (e) => {
                    e.stopPropagation();
                    if (e.key === 'Escape') {
                        e.preventDefault();
                        clearTimeout(treeFilterDebounceTimer);
                        treeFilterExpandSeq++;
                        feTreeFilterInput.value = '';
                        updateFeTreeFilterClearVisibility();
                        applyFeTreeSearchFilter();
                        try { feTreeFilterInput.blur(); } catch (_) {}
                    }
                });
                if (feTreeFilterClearBtn) {
                    feTreeFilterClearBtn.addEventListener('click', (e) => {
                        e.preventDefault();
                        e.stopPropagation();
                        clearTimeout(treeFilterDebounceTimer);
                        treeFilterExpandSeq++;
                        feTreeFilterInput.value = '';
                        updateFeTreeFilterClearVisibility();
                        applyFeTreeSearchFilter();
                        try { feTreeFilterInput.focus(); } catch (_) {}
                    });
                }
                updateFeTreeFilterClearVisibility();
            }
            if (feItemsWrap) {
                feItemsWrap.addEventListener('contextmenu', (e) => {
                    if (bucket !== 'files' && bucket !== 'remote') return;
                    if (e.target.closest('.explorer-item')) return;
                    e.preventDefault();
                    e.stopPropagation();
                    if (bucket === 'files') {
                        const opts = [
                            { className: 'refresh', icon: 'fas fa-sync', label: tr('explorerRefresh'), action: () => { void renderFilesView({ refreshTree: false }); } },
                            { className: 'explorer-new-folder-ctx', icon: 'fas fa-folder-plus', label: tr('explorerNewFolder'), action: () => { void promptAndCreateLocalFolder(relPath || ''); } }
                        ];
                        if (fileExplorerClipboardHasPasteableItems()) {
                            opts.push({ className: 'clip-paste', icon: 'fas fa-paste', label: tr('explorerPaste'), action: () => { pasteExplorerClipboard(); } });
                        }
                        showContextMenu(opts, e);
                        return;
                    }
                    const opts = [];
                    opts.push({ className: 'refresh', icon: 'fas fa-sync', label: tr('explorerRefresh'), action: () => { void renderRemoteView(); } });
                    if (remoteCtx?.id) {
                        opts.push({ className: 'explorer-new-folder-ctx', icon: 'fas fa-folder-plus', label: tr('explorerNewFolder'), action: () => {
                            void promptAndCreateRemoteFolder(remoteRelPath || '');
                        }});
                    }
                    if (fileExplorerClipboardHasPasteableItems()) {
                        // En remoto, pegar debe respetar la ruta actual de la pestaña (remoteRelPath).
                        opts.push({ className: 'clip-paste', icon: 'fas fa-paste', label: tr('explorerPaste'), action: () => { pasteExplorerClipboard(); } });
                    }
                    showContextMenu(opts, e);
                }, true);
            }

            function showConnectionsPanel(show) {
                if (!connectionsPanel) return;
                connectionsPanel.style.display = show ? 'block' : 'none';
            }

            function renderConnectionsList() {
                if (!connectionsListEl) return;
                connectionsListEl.innerHTML = '';
                const list = Array.isArray(fileExplorerConnections) ? fileExplorerConnections : [];
                if (list.length === 0) {
                    connectionsListEl.innerHTML = `<div style="opacity:0.75;padding:6px 0;">${tr('explorerConnections')} (0)</div>`;
                    return;
                }
                list.forEach((c) => {
                    const row = document.createElement('div');
                    row.className = 'explorer-connection-row';

                    const headRow = document.createElement('div');
                    headRow.className = 'explorer-connection-head';

                    const openBtn = document.createElement('button');
                    openBtn.type = 'button';
                    openBtn.className = 'explorer-connection-open';
                    openBtn.innerHTML = `<i class="fas fa-plug" aria-hidden="true"></i><span class="explorer-connection-open-label"></span>`;
                    const openLbl = openBtn.querySelector('.explorer-connection-open-label');
                    if (openLbl) openLbl.textContent = c.name || c.id || 'Connection';
                    openBtn.title = (c.type || '') + (c.baseUrl ? ' — ' + c.baseUrl : '') + (c.host ? ' — ' + c.host : '');
                    openBtn.addEventListener('click', () => {
                        showConnectionsPanel(false);
                        openRemoteForConnection(c, { newTab: false });
                    });

                    const newTabBtn = document.createElement('button');
                    newTabBtn.type = 'button';
                    newTabBtn.className = 'explorer-connection-newtab';
                    newTabBtn.innerHTML = '<i class="fas fa-external-link-alt"></i>';
                    newTabBtn.title = tr('explorerConnNewTab');
                    newTabBtn.addEventListener('click', (e) => {
                        e.stopPropagation();
                        showConnectionsPanel(false);
                        openRemoteForConnection(c, { newTab: true });
                    });

                    headRow.appendChild(openBtn);
                    headRow.appendChild(newTabBtn);

                    const mkBtn = (label, primary) => {
                        const b = document.createElement('button');
                        b.type = 'button';
                        b.innerHTML = `<span class="explorer-conn-btn-ico" aria-hidden="true"></span><span class="explorer-conn-btn-label"></span>`;
                        const bl = b.querySelector('.explorer-conn-btn-label');
                        if (bl) bl.textContent = label;
                        b.className = primary ? 'explorer-btn-secondary' : 'explorer-btn-danger';
                        return b;
                    };

                    const testBtn = mkBtn(tr('explorerConnTest'), true);
                    testBtn.addEventListener('click', async () => {
                        testBtn.disabled = true;
                        try {
                            // UX: "Conectar" abre la conexión (igual que click en el nombre),
                            // y además valida rápidamente listando la raíz para feedback inmediato.
                            const r = await fetchExplorerConnList(c.id, '');
                            if (r && r.success) {
                                showToast(tr('explorerConnTestOk'));
                                showConnectionsPanel(false);
                                openRemoteForConnection(c, { newTab: false });
                            } else {
                                showToast((r && r.error) ? String(r.error) : tr('explorerConnTestFail'));
                            }
                        } catch (err) {
                            showToast(tr('explorerConnTestFail'));
                        }
                        testBtn.disabled = false;
                    });
                    try {
                        const ico = testBtn.querySelector('.explorer-conn-btn-ico');
                        if (ico) ico.innerHTML = '<i class="fas fa-plug"></i>';
                    } catch (_) {}

                    const editBtn = mkBtn(tr('explorerConnEdit'), false);
                    try {
                        const ico = editBtn.querySelector('.explorer-conn-btn-ico');
                        if (ico) ico.innerHTML = '<i class="fas fa-pen"></i>';
                    } catch (_) {}
                    editBtn.addEventListener('click', async () => {
                        const idx = fileExplorerConnections.findIndex(x => x.id === c.id);
                        if (idx < 0) return;
                        if (c.type === 'webdav') {
                            const name = await appDialog.prompt(tr('explorerPromptConnName'), c.name || '');
                            if (!name) return;
                            const baseUrl = await appDialog.prompt(tr('explorerPromptWebdavBaseUrl'), c.baseUrl || '');
                            if (!baseUrl) return;
                            const username = await appDialog.prompt(tr('explorerPromptWebdavUser'), c.username || '') || '';
                            const password = await appDialog.prompt(tr('explorerPromptWebdavPass'), c.password || '') || '';
                            fileExplorerConnections[idx] = { ...c, name, baseUrl, username, password };
                        } else if (c.type === 'ftp') {
                            const name = await appDialog.prompt(tr('explorerPromptConnName'), c.name || '');
                            if (!name) return;
                            const hostStr = await appDialog.prompt(tr('explorerPromptFtpHost'), c.host || '');
                            if (!hostStr) return;
                            const portStr = await appDialog.prompt(tr('explorerPromptFtpPort'), String(c.port || 21));
                            const port = portStr ? parseInt(portStr, 10) : 21;
                            const username = await appDialog.prompt(tr('explorerPromptFtpUser'), c.username || '') || '';
                            const password = await appDialog.prompt(tr('explorerPromptFtpPass'), c.password || '') || '';
                            const root = await appDialog.prompt(tr('explorerPromptFtpRoot'), c.root || '') || '';
                            fileExplorerConnections[idx] = { ...c, name, host: hostStr, port, username, password, root, pasv: c.pasv !== false };
                        } else {
                            return;
                        }
                        const r = await saveSettings({ fileExplorerConnections });
                        fileExplorerConnections = r?.data?.fileExplorerConnections || fileExplorerConnections;
                        renderConnectionsList();
                        showToast(tr('explorerStatusOk'));
                    });

                    const delBtn = mkBtn(tr('explorerConnDelete'), false);
                    delBtn.style.background = 'rgba(220,53,69,0.35)';
                    try {
                        const ico = delBtn.querySelector('.explorer-conn-btn-ico');
                        if (ico) ico.innerHTML = '<i class="fas fa-trash"></i>';
                    } catch (_) {}
                    delBtn.addEventListener('click', async () => {
                        if (!(await appDialog.confirm(tr('explorerDelete') + '?'))) return;
                        fileExplorerConnections = (fileExplorerConnections || []).filter(x => x.id !== c.id);
                        const r = await saveSettings({ fileExplorerConnections });
                        fileExplorerConnections = r?.data?.fileExplorerConnections || fileExplorerConnections;
                        renderConnectionsList();
                        showToast(tr('explorerStatusOk'));
                    });

                    const actionsRow = document.createElement('div');
                    actionsRow.className = 'explorer-connection-actions-row';
                    actionsRow.appendChild(testBtn);
                    actionsRow.appendChild(editBtn);
                    actionsRow.appendChild(delBtn);

                    row.appendChild(headRow);
                    try {
                        const meta = document.createElement('div');
                        meta.className = 'explorer-connection-meta';
                        const type = (c.type || '').toUpperCase();
                        const host = c.baseUrl || c.host || '';
                        meta.textContent = `${type}${host ? ' · ' + host : ''}`;
                        row.appendChild(meta);
                    } catch (_) {}
                    row.appendChild(actionsRow);
                    connectionsListEl.appendChild(row);
                });
                renderExplorerTabs();
            }

            if (connectionsBtn) {
                connectionsBtn.addEventListener('click', () => {
                    renderConnectionsList();
                    showConnectionsPanel(true);
                });
            }
            if (closeConnectionsBtn) closeConnectionsBtn.addEventListener('click', () => showConnectionsPanel(false));

            if (addWebdavBtn) {
                addWebdavBtn.addEventListener('click', async () => {
                    const name = await appDialog.prompt(tr('explorerPromptConnName'));
                    if (!name) return;
                    const baseUrl = await appDialog.prompt(tr('explorerPromptWebdavBaseUrl'));
                    if (!baseUrl) return;
                    const username = await appDialog.prompt(tr('explorerPromptWebdavUser')) || '';
                    const password = await appDialog.prompt(tr('explorerPromptWebdavPass')) || '';
                    const id = 'conn_' + Date.now() + '_' + Math.random().toString(36).slice(2, 8);
                    fileExplorerConnections = Array.isArray(fileExplorerConnections) ? fileExplorerConnections : [];
                    fileExplorerConnections.push({ id, name, type: 'webdav', baseUrl, username, password });
                    const r = await saveSettings({ fileExplorerConnections });
                    fileExplorerConnections = r?.data?.fileExplorerConnections || fileExplorerConnections;
                    renderConnectionsList();
                });
            }

            if (addFtpBtn) {
                addFtpBtn.addEventListener('click', async () => {
                    const name = await appDialog.prompt(tr('explorerPromptConnName'));
                    if (!name) return;
                    const host = await appDialog.prompt(tr('explorerPromptFtpHost'));
                    if (!host) return;
                    const portStr = await appDialog.prompt(tr('explorerPromptFtpPort'));
                    const port = portStr ? parseInt(portStr, 10) : 21;
                    const username = await appDialog.prompt(tr('explorerPromptFtpUser')) || '';
                    const password = await appDialog.prompt(tr('explorerPromptFtpPass')) || '';
                    const root = await appDialog.prompt(tr('explorerPromptFtpRoot')) || '';
                    const id = 'conn_' + Date.now() + '_' + Math.random().toString(36).slice(2, 8);
                    fileExplorerConnections = Array.isArray(fileExplorerConnections) ? fileExplorerConnections : [];
                    fileExplorerConnections.push({ id, name, type: 'ftp', host, port, username, password, root, pasv: true });
                    const r = await saveSettings({ fileExplorerConnections });
                    fileExplorerConnections = r?.data?.fileExplorerConnections || fileExplorerConnections;
                    renderConnectionsList();
                });
            }

            function normalizeJoin(base, name) {
                if (!base) return name;
                return `${base}/${name}`;
            }

            function feBuildShortcutAppModel(item, parentPathForItem, listBucket) {
                const b = listBucket === 'cache' ? 'cache' : 'files';
                if (item.type === 'dir') {
                    let relPath = '';
                    if (item.__fromGlobal) {
                        relPath = item.__relPath || normalizeJoin(item.__parentPath || '', item.name || '');
                    } else {
                        relPath = normalizeJoin(parentPathForItem || '', item.name || '');
                    }
                    return {
                        title: item.name,
                        icon: FILE_EXPLORER_APP.icon,
                        category: tr('explorerShortcutCategory'),
                        description: '',
                        lastAction: 'openExplorer',
                        link: '#',
                        linkTarget: '_self',
                        showInGrid: false,
                        explorerOpen: { bucket: b, relPath: relPath || '' },
                        libraryExplorerShortcut: true
                    };
                }
                const mime = item.mimeType || '';
                let icon = FILE_EXPLORER_APP.icon;
                if (mime.indexOf('image/') === 0 && item.url) icon = item.url;
                return {
                    title: item.name,
                    icon,
                    category: tr('explorerShortcutCategory'),
                    description: '',
                    lastAction: 'file',
                    link: '#',
                    linkTarget: '_self',
                    showInGrid: false,
                    fileInfo: { url: item.url, type: mime, name: item.name },
                    libraryExplorerShortcut: true
                };
            }

            /** Si se renombra una carpeta, actualiza rutas abiertas/historial y ramas expandidas del árbol. */
            function remapLocalPathsAfterDirRename(parentPath, oldName, newName) {
                const oldFull = normalizeJoin(parentPath || '', oldName);
                const newFull = normalizeJoin(parentPath || '', newName);
                if (oldFull === newFull) return;
                const remap = (p) => {
                    if (p == null || p === '') return p;
                    if (p === oldFull) return newFull;
                    if (p.startsWith(oldFull + '/')) return newFull + p.slice(oldFull.length);
                    return p;
                };
                relPath = remap(relPath) || '';
                if (Array.isArray(pathHistory)) {
                    pathHistory = pathHistory.map(remap);
                }
                if (feTreeExpanded && feTreeExpanded.size) {
                    const pairs = [];
                    for (const k of feTreeExpanded) {
                        const nk = remap(k);
                        pairs.push([k, nk]);
                    }
                    for (const [k, nk] of pairs) {
                        if (k !== nk) {
                            feTreeExpanded.delete(k);
                            feTreeExpanded.add(nk);
                        }
                    }
                }
                try { feTreeHasChildrenCache.clear(); } catch (_) {}
            }

            function remapRemotePathsAfterDirRename(parentPath, oldName, newName) {
                const oldFull = normalizeJoin(parentPath || '', oldName);
                const newFull = normalizeJoin(parentPath || '', newName);
                if (oldFull === newFull) return;
                const remap = (p) => {
                    if (p == null || p === '') return p;
                    if (p === oldFull) return newFull;
                    if (p.startsWith(oldFull + '/')) return newFull + p.slice(oldFull.length);
                    return p;
                };
                remoteRelPath = remap(remoteRelPath || '') || '';
                if (Array.isArray(remotePathHistory)) {
                    remotePathHistory = remotePathHistory.map(remap);
                }
                if (feTreeRemoteExpanded && feTreeRemoteExpanded.size) {
                    const pairs = [];
                    for (const k of feTreeRemoteExpanded) {
                        const nk = remap(k);
                        pairs.push([k, nk]);
                    }
                    for (const [k, nk] of pairs) {
                        if (k !== nk) {
                            feTreeRemoteExpanded.delete(k);
                            feTreeRemoteExpanded.add(nk);
                        }
                    }
                }
                try { feTreeRemoteHasChildrenCache.clear(); } catch (_) {}
            }

            /** Igual que PHP explorerNormalizeRelPath (rutas relativas seguras). */
            function explorerNormalizeRelPath(p) {
                let path = String(p || '').replace(/\\/g, '/').replace(/\0/g, '');
                path = path.replace(/^\/+/, '');
                if (path.indexOf('..') !== -1) return '';
                const parts = path.split('/').filter(Boolean);
                const safe = [];
                for (const seg of parts) {
                    if (seg === '.' || seg === '..' || seg === '') continue;
                    safe.push(seg);
                }
                return safe.join('/');
            }

            function findExplorerConnectionByIdConn(cid) {
                const list = Array.isArray(fileExplorerConnections) ? fileExplorerConnections : [];
                return list.find((x) => x && x.id === cid) || null;
            }

            function persistActiveRemoteTabState() {
                if (bucket !== 'remote' || !activeRemoteTabId) return;
                const t = remoteTabList.find((x) => x.tabId === activeRemoteTabId);
                if (!t) return;
                t.remoteRelPath = remoteRelPath;
                t.remotePathHistory = remotePathHistory.slice();
                t.remoteHistoryCursor = remoteHistoryCursor;
                if (feSearchInput) t.searchQuery = feSearchInput.value || '';
                t.searchCommitted = explorerToolbarSearchCommitted;
                notifyExplorerStateChanged();
            }

            function syncRemoteTabStateFromEntry(tab) {
                const def = findExplorerConnectionByIdConn(tab.connId);
                remoteCtx = {
                    id: tab.connId,
                    name: tab.connName || def?.name || tab.connId,
                    type: tab.connType || def?.type || ''
                };
                remoteRelPath = tab.remoteRelPath || '';
                remotePathHistory = Array.isArray(tab.remotePathHistory) ? tab.remotePathHistory.slice() : [''];
                remoteHistoryCursor = typeof tab.remoteHistoryCursor === 'number' ? tab.remoteHistoryCursor : 0;
            }

            function activateRemoteTab(tabId) {
                if (!tabId) return;
                const tab = remoteTabList.find((x) => x.tabId === tabId);
                if (!tab) return;
                abortExplorerPendingLoads();
                if (feActiveTab === 'local' && feSearchInput) {
                    localTabSearchQuery = feSearchInput.value || '';
                    localTabSearchCommitted = explorerToolbarSearchCommitted;
                }
                persistActiveRemoteTabState();
                activeRemoteTabId = tabId;
                feActiveTab = tabId;
                bucket = 'remote';
                lastNonTrashBucket = 'remote';
                syncRemoteTabStateFromEntry(tab);
                if (feSearchInput) feSearchInput.value = tab.searchQuery || '';
                explorerToolbarSearchCommitted = !!tab.searchCommitted;
                updateFeSearchClearVisibility();
                renderExplorerTabs();
                renderRemoteView();
                maybeCloseExplorerSidebar();
                notifyExplorerStateChanged();
            }

            function closeRemoteTab(tabId) {
                if (!tabId) return;
                const idx = remoteTabList.findIndex((x) => x.tabId === tabId);
                if (idx < 0) return;
                const wasActive = bucket === 'remote' && activeRemoteTabId === tabId && feActiveTab === tabId;
                remoteTabList.splice(idx, 1);
                if (wasActive) {
                    if (remoteTabList.length) {
                        const nextIdx = Math.min(idx, remoteTabList.length - 1);
                        activateRemoteTab(remoteTabList[nextIdx].tabId);
                    } else {
                        switchToLocalTab();
                    }
                } else {
                    renderExplorerTabs();
                    notifyExplorerStateChanged();
                }
            }

            function openRemoteForConnection(c, opts) {
                if (!c || !c.id) return;
                persistActiveRemoteTabState();
                const wantNew = opts && opts.newTab;
                if (!wantNew) {
                    const existing = remoteTabList.find((t) => t.connId === c.id);
                    if (existing) {
                        activateRemoteTab(existing.tabId);
                        return;
                    }
                }
                const tabId = 'rt_' + Date.now() + '_' + Math.random().toString(36).slice(2, 8);
                remoteTabList.push({
                    tabId,
                    connId: c.id,
                    connName: (c.name || c.id || '').trim() || c.id,
                    connType: c.type || '',
                    remoteRelPath: '',
                    remotePathHistory: [''],
                    remoteHistoryCursor: 0,
                    searchQuery: '',
                    searchCommitted: false
                });
                activateRemoteTab(tabId);
                notifyExplorerStateChanged();
            }

            function renderExplorerTabs() {
                if (!feTabsEl) return;
                feTabsEl.innerHTML = '';
                const mkTabRow = (label, tabId, isActive, showClose) => {
                    const row = document.createElement('div');
                    row.className = 'fe-tab' + (isActive ? ' active' : '');
                    row.dataset.tabId = tabId;
                    row.setAttribute('role', 'presentation');
                    const main = document.createElement('button');
                    main.type = 'button';
                    main.className = 'fe-tab-main';
                    main.setAttribute('role', 'tab');
                    main.setAttribute('aria-selected', isActive ? 'true' : 'false');
                    main.dataset.tabId = tabId;
                    main.textContent = label;
                    const closeBtn = document.createElement('button');
                    closeBtn.type = 'button';
                    closeBtn.className = 'fe-tab-close';
                    closeBtn.setAttribute('aria-label', tr('explorerTabClose'));
                    closeBtn.title = tr('explorerTabClose');
                    closeBtn.innerHTML = '&times;';
                    if (!showClose) closeBtn.style.display = 'none';
                    row.appendChild(main);
                    row.appendChild(closeBtn);
                    return { row, main, closeBtn };
                };
                const localActive = isLocalLibraryBucket() && feActiveTab === 'local';
                const localRow = mkTabRow(tr('explorerTabLocal'), 'local', localActive, false);
                localRow.main.addEventListener('click', () => {
                    if (explorerBusy) return;
                    switchToLocalTab();
                });
                feTabsEl.appendChild(localRow.row);
                remoteTabList.forEach((tab) => {
                    const siblings = remoteTabList.filter((t) => t.connId === tab.connId);
                    const ord = siblings.findIndex((t) => t.tabId === tab.tabId) + 1;
                    const baseLabel = (tab.connName || tab.connId || '').trim() || tab.connId;
                    const label = siblings.length > 1 ? `${baseLabel} (${ord})` : baseLabel;
                    const isActive = bucket === 'remote' && feActiveTab === tab.tabId && activeRemoteTabId === tab.tabId;
                    const { row, main, closeBtn } = mkTabRow(label, tab.tabId, isActive, true);
                    const def = findExplorerConnectionByIdConn(tab.connId);
                    main.title = (def?.type || tab.connType || '') + (def?.host ? ' — ' + def.host : '') + (def?.baseUrl ? ' — ' + def.baseUrl : '');
                    main.addEventListener('click', () => {
                        if (explorerBusy) return;
                        activateRemoteTab(tab.tabId);
                    });
                    closeBtn.addEventListener('click', (e) => {
                        e.stopPropagation();
                        if (explorerBusy) return;
                        closeRemoteTab(tab.tabId);
                    });
                    feTabsEl.appendChild(row);
                });
            }

            function switchToLocalTab() {
                abortExplorerPendingLoads();
                persistActiveRemoteTabState();
                feActiveTab = 'local';
                activeRemoteTabId = null;
                bucket = 'files';
                explorerToolbarSearchCommitted = localTabSearchCommitted;
                if (feSearchInput) feSearchInput.value = localTabSearchQuery || '';
                updateFeSearchClearVisibility();
                renderExplorerTabs();
                renderFilesView();
                maybeCloseExplorerSidebar();
                notifyExplorerStateChanged();
            }

            function switchToRemoteTab(c) {
                openRemoteForConnection(c, { newTab: false });
            }

            async function promptAndCreateLocalFolder(parentPath) {
                const raw = await appDialog.prompt(tr('explorerPromptNewDirName'));
                const name = raw != null ? String(raw).trim() : '';
                if (!name) return;
                const p = parentPath !== undefined && parentPath !== null ? parentPath : '';
                const r = await explorerCreateDir(p, name, localListBucketName());
                if (!r || !r.success) {
                    showToast(r?.error || tr('explorerStatusError'));
                    return;
                }
                logActivity('explorer_create_dir', { path: p, name });
                await renderFilesView();
                const choice = await showExplorerNewFolderChoiceModal(name);
                if (choice === 'enter') {
                    const pNorm = explorerNormalizeRelPath(p);
                    const relNorm = explorerNormalizeRelPath(relPath);
                    if (pNorm === relNorm) {
                        enterFolder(name);
                    } else {
                        navigateToPath(explorerNormalizeRelPath(normalizeJoin(p, name)));
                    }
                } else {
                    fePostRenderFocusItemName = name;
                    await renderFilesView();
                }
            }

            async function promptAndCreateRemoteFolder(parentPath) {
                if (!remoteCtx?.id) return;
                const raw = await appDialog.prompt(tr('explorerPromptNewDirName'));
                const name = raw != null ? String(raw).trim() : '';
                if (!name) return;
                const parent = parentPath !== undefined && parentPath !== null ? parentPath : '';
                const r = await explorerConnCreateDir(remoteCtx.id, parent, name);
                if (!r || !r.success) {
                    showToast(r?.error || tr('explorerStatusError'));
                    return;
                }
                logActivity('explorer_remote_create_dir', { connId: remoteCtx.id, path: parent, name });
                await renderRemoteView();
                const choice = await showExplorerNewFolderChoiceModal(name);
                if (choice === 'enter') {
                    remoteNavigateToPath(explorerNormalizeRelPath(normalizeJoin(parent, name)), { pushHistory: true });
                } else {
                    fePostRenderFocusItemName = name;
                    await renderRemoteView();
                }
            }

            async function executeExplorerDropToPath(destPath, names, isCopy, fromPathOverride) {
                if (!isLocalLibraryBucket()) return false;
                const list = Array.isArray(names) ? names.filter(Boolean) : [];
                if (!list.length) return false;
                const fromPath = fromPathOverride !== undefined ? fromPathOverride : (explorerDragPayload ? explorerDragPayload.fromPath : relPath);
                if (fromPath === undefined || fromPath === null) return false;
                const fromB = explorerDragPayload && explorerDragPayload.fromBucket ? explorerDragPayload.fromBucket : localListBucketName();
                const toB = localListBucketName();
                const fromDept = explorerDragPayload && explorerDragPayload.fromDepartmentId ? explorerDragPayload.fromDepartmentId : (fromB === 'department' ? activeDepartmentId() : '');
                const toDept = toB === 'department' ? activeDepartmentId() : '';
                let ok = true;
                const conflictCtx = { applyMode: null, cache: new Map() };
                for (const nm of list) {
                    if (fromPath === destPath && !isCopy && fromB === toB) continue;
                    const row = (items || []).find((x) => x.name === nm);
                    let kind = (row && row.type === 'dir') ? 'dir' : 'file';
                    if (explorerDragPayload && explorerDragPayload.kind === 'dir' && nm === explorerDragPayload.name && list.length === 1) {
                        kind = 'dir';
                    }
                    if (!isCopy && kind === 'dir') {
                        const srcFull = normalizeJoin(fromPath, nm);
                        if (destPath === srcFull || (destPath && String(destPath).startsWith(srcFull + '/'))) {
                            showToast(tr('explorerMoveFailed'));
                            ok = false;
                            continue;
                        }
                    }
                    const resolved = await explorerResolveLocalConflict(toB, destPath, nm, kind, conflictCtx);
                    if (resolved.action === 'cancel') { ok = false; break; }
                    if (resolved.action === 'skip') continue;
                    let r;
                    if (isCopy) {
                        const opts = {
                            fromBucket: fromB,
                            toBucket: toB,
                            autoRename: false,
                            overwrite: resolved.action === 'replace',
                            fromDepartmentId: fromDept,
                            toDepartmentId: toDept,
                        };
                        if (resolved.name !== nm) opts.destName = resolved.name;
                        r = await explorerCopyTo(fromPath, destPath, nm, opts);
                    } else {
                        r = await explorerMove(fromPath, destPath, nm, {
                            fromBucket: fromB,
                            toBucket: toB,
                            overwrite: resolved.action === 'replace',
                            destName: resolved.name !== nm ? resolved.name : undefined,
                            fromDepartmentId: fromDept,
                            toDepartmentId: toDept,
                        });
                    }
                    if (!r || !r.success) {
                        ok = false;
                        showToast(r?.error || tr('explorerMoveFailed'));
                    }
                }
                if (ok) {
                    logActivity(isCopy ? 'explorer_copy_to' : 'explorer_move', { fromPath, toPath: destPath, names: list });
                    await renderFilesView();
                    showToast(tr('explorerStatusOk'));
                }
                return ok;
            }

            /** Evita soltar (mover) una carpeta sobre sí misma o sobre un descendiente. */
            function feExplorerLocalDropAllowedOnPath(destPath) {
                if (!explorerDragPayload || !isLocalLibraryBucket()) return false;
                const names = explorerDragPayload.names && explorerDragPayload.names.length
                    ? explorerDragPayload.names
                    : [explorerDragPayload.name];
                const isCopy = explorerDragPayload.mode === 'copy';
                if (isCopy) return true;
                const fromPath = explorerDragPayload.fromPath;
                for (const nm of names) {
                    if (!nm) continue;
                    const row = (items || []).find((x) => x.name === nm);
                    let kind = (row && row.type === 'dir') ? 'dir' : 'file';
                    if (explorerDragPayload.kind === 'dir' && nm === explorerDragPayload.name && names.length === 1) {
                        kind = 'dir';
                    }
                    if (kind !== 'dir') continue;
                    const srcFull = normalizeJoin(fromPath, nm);
                    const d = destPath || '';
                    if (d === srcFull || (d && String(d).startsWith(srcFull + '/'))) return false;
                }
                return true;
            }

            function updatePathBar() {
                if (isLocalLibraryBucket()) {
                    if (pathEl) {
                        pathEl.style.display = 'none';
                        pathEl.textContent = '/' + (relPath ? relPath : '');
                    }
                    if (feBC) feBC.style.display = 'flex';
                    if (feSidebar) feSidebar.style.display = '';
                    renderBreadcrumb();
                    updateNavButtons();
                    updateSidebarActivePath();
                } else if (bucket === 'trash') {
                    if (pathEl) pathEl.style.display = 'none';
                    if (feBC) feBC.style.display = 'flex';
                    if (feSidebar) feSidebar.style.display = '';
                    renderBreadcrumb();
                    updateNavButtons();
                    updateSidebarActivePath();
                } else {
                    // Remoto: breadcrumb + barra de ruta + árbol lateral (como local).
                    if (feBC) feBC.style.display = 'flex';
                    if (pathEl) pathEl.style.display = 'none';
                    if (feSidebar) feSidebar.style.display = '';
                    renderBreadcrumb();
                    updateNavButtons();
                    updateSidebarActivePath();
                }
                const trashTgl = host.querySelector('.explorer-trash-toggle-btn');
                if (trashTgl) trashTgl.classList.toggle('active', bucket === 'trash');
            }

            function handleLocalNavCancel() {
                localNavSeq++;
                const snap = localNavCancelSnapshot;
                if (!snap) {
                    setExplorerBusy(false);
                    return;
                }
                relPath = snap.relPath;
                pathHistory = snap.pathHistory.slice();
                historyCursor = snap.historyCursor;
                selectedNames.clear();
                updatePathBar();
                maybeCloseExplorerSidebar();
                notifyExplorerStateChanged();
                void renderFilesView({ refreshTree: true });
            }

            async function renderFilesView(options = {}) {
                feResetExplorerTypeAhead();
                syncExplorerActiveContext();
                const refreshTree = options && options.refreshTree !== undefined ? !!options.refreshTree : true;
                const reqId = ++localNavSeq;
                localNavCancelSnapshot = {
                    relPath: lastRenderedLocalState.relPath,
                    pathHistory: [...lastRenderedLocalState.pathHistory],
                    historyCursor: lastRenderedLocalState.historyCursor
                };
                updatePathBar();
                setExplorerBusy(true, tr('explorerLoading'), { onCancel: handleLocalNavCancel });
                try {
                    const data = await fetchExplorerListActive(relPath);
                    if (reqId !== localNavSeq) return;
                    items = (data && data.success && Array.isArray(data.items)) ? data.items : [];
                    items.sort((a, b) => (a.type === b.type ? a.name.localeCompare(b.name) : (a.type === 'dir' ? -1 : 1)));
                    lastRenderedLocalState = { relPath, pathHistory: [...pathHistory], historyCursor };
                    renderItems();
                    if (refreshTree) refreshSidebarTree();
                    renderExplorerTabs();
                } finally {
                    if (reqId === localNavSeq) setExplorerBusy(false);
                }
            }

            async function explorerConnCreateDir(connId, path, name) {
                const res = await fetch('?action=explorerConnCreateDir', {
                    method: 'POST',
                    headers: { 'Content-Type': 'application/json' },
                    body: JSON.stringify({ connId, path: path || '', name })
                });
                return await res.json();
            }

            async function explorerConnRename(connId, path, oldName, newName) {
                const res = await fetch('?action=explorerConnRename', {
                    method: 'POST',
                    headers: { 'Content-Type': 'application/json' },
                    body: JSON.stringify({ connId, path: path || '', oldName, newName })
                });
                return await res.json();
            }

            async function explorerConnDelete(connId, path, name, kind) {
                const res = await fetch('?action=explorerConnDelete', {
                    method: 'POST',
                    headers: { 'Content-Type': 'application/json' },
                    body: JSON.stringify({ connId, path: path || '', name, kind: kind || 'file' })
                });
                return await res.json();
            }

            async function explorerConnDeleteToTrash(connId, path, name) {
                const res = await fetch('?action=explorerConnDeleteToTrash', {
                    method: 'POST',
                    headers: { 'Content-Type': 'application/json' },
                    body: JSON.stringify({ connId, path: path || '', name })
                });
                return await res.json();
            }

            async function explorerConnUploadFiles(connId, path, file, nameOverride) {
                const formData = new FormData();
                formData.append('connId', connId);
                formData.append('path', path || '');
                formData.append('files[]', file, nameOverride || file.name);
                return await new Promise((resolve) => {
                    const xhr = new XMLHttpRequest();
                    xhr.open('POST', '?action=explorerConnUpload', true);
                    xhr.responseType = 'json';
                    xhr.upload.onprogress = (e) => {
                        if (!e || !e.lengthComputable) return;
                        const pct = e.total > 0 ? Math.max(0, Math.min(100, Math.round((e.loaded / e.total) * 100))) : 0;
                        try {
                            if (typeof window.__gridExplorerProgressOnUploadProgress === 'function') {
                                window.__gridExplorerProgressOnUploadProgress({ loaded: e.loaded, total: e.total, percent: pct });
                            }
                        } catch (_) {}
                    };
                    xhr.onload = () => {
                        try {
                            const data = xhr.response && typeof xhr.response === 'object' ? xhr.response : JSON.parse(xhr.responseText || '{}');
                            resolve(data);
                        } catch (_) {
                            resolve({ success: false, error: 'Respuesta inválida' });
                        }
                    };
                    xhr.onerror = () => resolve({ success: false, error: 'Error de red' });
                    xhr.send(formData);
                });
            }

            async function explorerConnMoveRemote(connId, fromPath, toPath, name, newName, opts = {}) {
                const body = { connId, fromPath: fromPath || '', toPath: toPath || '', name };
                if (newName) body.newName = newName;
                if (opts && opts.overwrite) body.overwrite = true;
                const res = await fetch('?action=explorerConnMove', {
                    method: 'POST',
                    headers: { 'Content-Type': 'application/json' },
                    body: JSON.stringify(body)
                });
                return await res.json();
            }

            async function explorerConnCopyRemote(connId, fromPath, toPath, name, newName) {
                const body = { connId, fromPath: fromPath || '', toPath: toPath || '', name };
                if (newName) body.newName = newName;
                const res = await fetch('?action=explorerConnCopy', {
                    method: 'POST',
                    headers: { 'Content-Type': 'application/json' },
                    body: JSON.stringify(body)
                });
                return await res.json();
            }

            async function renderRemoteView() {
                feResetExplorerTypeAhead();
                updatePathBar();
                if (!remoteCtx || !remoteCtx.id) {
                    itemsEl.innerHTML = `<div style="grid-column:1/-1; padding:10px; opacity:0.7;">${tr('explorerNoConnection')}</div>`;
                    return;
                }
                const reqId = ++remoteNavSeq;
                setExplorerBusy(true, tr('explorerLoading'));
                try {
                    const data = await fetchExplorerConnList(remoteCtx.id, remoteRelPath);
                    if (reqId !== remoteNavSeq) return;
                    items = (data && data.success && Array.isArray(data.items)) ? data.items : [];
                    items.sort((a, b) => (a.type === b.type ? (a.name || '').localeCompare(b.name || '') : (a.type === 'dir' ? -1 : 1)));
                    renderItems();
                    await refreshSidebarTree();
                    renderExplorerTabs();
                } finally {
                    if (reqId === remoteNavSeq) setExplorerBusy(false);
                }
            }

            async function openExplorerRemoteFile(item) {
                if (!remoteCtx || !remoteCtx.id) return;
                setExplorerBusy(true, tr('explorerOpeningRemote'));
                try {
                    const parentPath = item && item.__fromGlobal ? (item.__parentPath ?? '') : (remoteRelPath || '');
                    const r = await explorerConnFetch(remoteCtx.id, parentPath, item.name);
                    if (!r || !r.success) {
                        await appDialog.alert(r?.error || tr('explorerRemoteLoadFailed'));
                        return;
                    }
                    const mime = (r.mimeType || item.mimeType || 'application/octet-stream').toLowerCase();
                    const pseudoApp = {
                        title: item.name,
                        icon: FILE_EXPLORER_APP.icon,
                        lastAction: 'file',
                        fileInfo: { url: r.url, type: r.mimeType || 'application/octet-stream', name: item.name }
                    };
                    if (mime.startsWith('image/') || mime.startsWith('video/')) {
                        const gPath = item && item.__fromGlobal ? (item.__parentPath ?? '') : (remoteRelPath || '');
                        pseudoApp.galleryContext = {
                            source: 'remote',
                            connId: remoteCtx.id,
                            path: gPath,
                            name: item.name
                        };
                    }
                    openFileWindowFrame(pseudoApp);
                } finally {
                    setExplorerBusy(false);
                }
            }

            async function renderTrashView() {
                feResetExplorerTypeAhead();
                await fetchExplorerListTrash();
                updatePathBar();
                if (!feTrashCtx) {
                    items = (fileExplorerTrash || []).map(t => ({
                        type: t.kind,
                        id: t.id,
                        name: t.name,
                        mimeType: t.mimeType,
                        size: t.size,
                        url: t.url,
                        deletedAt: t.deletedAt,
                        originalPath: t.originalPath || ''
                    }));
                    items.sort((a, b) => (b.deletedAt || 0) - (a.deletedAt || 0));
                } else {
                    // Listar contenido dentro de una carpeta eliminada (sin restaurar)
                    const data = await fetchExplorerTrashListDir(feTrashCtx.id, feTrashCtx.name, feTrashRelPath);
                    if (!data || data.success === false) {
                        // Si ya no existe (p. ej. se restauró/eliminó), volver a raíz sin mostrar error.
                        feTrashCtx = null;
                        feTrashRelPath = '';
                        await renderTrashView();
                        return;
                    }
                    const childItems = (Array.isArray(data.items)) ? data.items : [];
                    items = childItems.map((it) => ({
                        type: it.type,
                        name: it.name,
                        mimeType: it.mimeType || '',
                        size: it.size || 0,
                        mtime: it.mtime || 0,
                        url: it.url || '',
                        __trash: { id: feTrashCtx.id, rootName: feTrashCtx.name, relPath: feTrashRelPath || '' }
                    }));
                    items.sort((a, b) => (a.type === b.type ? (a.name || '').localeCompare(b.name || '') : (a.type === 'dir' ? -1 : 1)));
                }
                renderItems();
                renderExplorerTabs();
            }

            function explorerSanitizeName(raw) {
                let s = String(raw || '').trim();
                if (!s) return '';
                // Evitar caracteres problemáticos típicos (Windows + rutas).
                s = s.replace(/[\\\/:*?"<>|]/g, '_');
                return s;
            }

            function explorerSplitName(name, kind) {
                const raw = String(name || '');
                if (kind === 'dir') return { base: raw, ext: '' };
                const i = raw.lastIndexOf('.');
                if (i <= 0 || i === raw.length - 1) return { base: raw, ext: '' };
                return { base: raw.slice(0, i), ext: raw.slice(i) };
            }

            async function explorerRemoteNameExists(connId, destPath, name, cache) {
                const key = `${connId}::${destPath || ''}`;
                if (cache && cache.has(key)) {
                    const set = cache.get(key);
                    return set ? set.has(name) : false;
                }
                let data;
                try {
                    data = await fetchExplorerConnList(connId, destPath || '');
                } catch (_) {
                    data = null;
                }
                const items = (data && data.success && Array.isArray(data.items)) ? data.items : [];
                const set = new Set(items.map((it) => String(it?.name || '')).filter(Boolean));
                if (cache) cache.set(key, set);
                return set.has(name);
            }

            async function showExplorerConflictModal(conflictName, kind) {
                const modal = document.getElementById('explorerConflictModal');
                if (!modal) return { action: 'cancel' };

                return new Promise((resolve) => {
                    const getApplyAll = () => {
                        const el = document.getElementById('explorerConflictApplyAll');
                        return !!(el && el.checked);
                    };

                    const mainEl = document.getElementById('explorerConflictModalMain');
                    const renameEl = document.getElementById('explorerConflictModalRename');
                    const inputEl = document.getElementById('explorerConflictNewNameInput');
                    const applyAllEl = document.getElementById('explorerConflictApplyAll');

                    const close = (result) => {
                        modal.removeEventListener('click', onBackdrop);
                        document.removeEventListener('keydown', onKey, true);
                        modal.style.display = 'none';
                        modal.setAttribute('aria-hidden', 'true');
                        document.body.classList.remove('modal-open');
                        resolve(result);
                    };

                    const onBackdrop = (e) => {
                        if (e.target === modal) close({ action: 'cancel' });
                    };

                    const onKey = (e) => {
                        if (e.key !== 'Escape') return;
                        e.preventDefault();
                        if (renameEl && renameEl.style.display !== 'none' && mainEl) {
                            renameEl.style.display = 'none';
                            mainEl.style.display = '';
                            return;
                        }
                        close({ action: 'cancel' });
                    };

                    const setTexts = () => {
                        const el = (id) => document.getElementById(id);
                        if (el('explorerConflictModalTitle')) el('explorerConflictModalTitle').textContent = tr('explorerConflictModalTitle');
                        if (el('explorerConflictModalMessage')) el('explorerConflictModalMessage').textContent = tr('explorerConflictIntro').replace(/%s/g, conflictName);
                        if (el('explorerConflictApplyAllLabel')) el('explorerConflictApplyAllLabel').textContent = tr('explorerConflictApplyAllLabel');
                        if (el('explorerConflictBtnReplace')) el('explorerConflictBtnReplace').textContent = tr('explorerConflictReplaceBtn');
                        if (el('explorerConflictBtnMerge')) el('explorerConflictBtnMerge').textContent = tr('explorerConflictMergeBtn');
                        if (el('explorerConflictBtnRename')) el('explorerConflictBtnRename').textContent = tr('explorerConflictRenameBtn');
                        if (el('explorerConflictBtnSkip')) el('explorerConflictBtnSkip').textContent = tr('explorerConflictSkipBtn');
                        if (el('explorerConflictBtnCancel')) el('explorerConflictBtnCancel').textContent = tr('explorerConflictCancelBtn');
                        if (el('explorerConflictNewNameLabel')) el('explorerConflictNewNameLabel').textContent = tr('explorerConflictNewName');
                        if (el('explorerConflictBtnRenameBack')) el('explorerConflictBtnRenameBack').textContent = tr('explorerConflictRenameBack');
                        if (el('explorerConflictBtnRenameOk')) el('explorerConflictBtnRenameOk').textContent = tr('explorerConflictRenameOk');
                    };

                    setTexts();
                    if (applyAllEl) applyAllEl.checked = false;
                    if (mainEl) mainEl.style.display = '';
                    if (renameEl) renameEl.style.display = 'none';

                    const showRename = () => {
                        const parsed = explorerSplitName(conflictName, kind);
                        const suggested = explorerSanitizeName(`${parsed.base} (copia)${parsed.ext}`) || conflictName;
                        if (inputEl) {
                            inputEl.value = suggested;
                            setTimeout(() => {
                                try {
                                    inputEl.focus();
                                    inputEl.select();
                                } catch (_) {}
                            }, 0);
                        }
                        if (mainEl) mainEl.style.display = 'none';
                        if (renameEl) renameEl.style.display = '';
                    };

                    const onConfirmRename = () => {
                        if (!inputEl) return;
                        const nextName = explorerSanitizeName(inputEl.value);
                        if (!nextName) return;
                        close({ action: 'rename', newName: nextName, applyAll: getApplyAll() });
                    };

                    const btnReplace = document.getElementById('explorerConflictBtnReplace');
                    const btnMerge = document.getElementById('explorerConflictBtnMerge');
                    const btnRename = document.getElementById('explorerConflictBtnRename');
                    const btnSkip = document.getElementById('explorerConflictBtnSkip');
                    const btnCancel = document.getElementById('explorerConflictBtnCancel');
                    const btnCloseX = document.getElementById('explorerConflictModalCloseX');
                    const btnRenameBack = document.getElementById('explorerConflictBtnRenameBack');
                    const btnRenameOk = document.getElementById('explorerConflictBtnRenameOk');

                    if (btnReplace) btnReplace.onclick = () => close({ action: 'replace', applyAll: getApplyAll() });
                    if (btnMerge) {
                        btnMerge.style.display = (kind === 'dir') ? '' : 'none';
                        btnMerge.onclick = () => close({ action: 'merge', applyAll: getApplyAll() });
                    }
                    if (btnRename) btnRename.onclick = showRename;
                    if (btnSkip) btnSkip.onclick = () => close({ action: 'skip', applyAll: getApplyAll() });
                    if (btnCancel) btnCancel.onclick = () => close({ action: 'cancel' });
                    if (btnCloseX) btnCloseX.onclick = () => close({ action: 'cancel' });
                    if (btnRenameBack) {
                        btnRenameBack.onclick = () => {
                            if (renameEl) renameEl.style.display = 'none';
                            if (mainEl) mainEl.style.display = '';
                        };
                    }
                    if (btnRenameOk) btnRenameOk.onclick = onConfirmRename;
                    if (inputEl) {
                        inputEl.onkeydown = (e) => {
                            if (e.key === 'Enter') {
                                e.preventDefault();
                                onConfirmRename();
                            }
                        };
                    }

                    modal.addEventListener('click', onBackdrop);
                    document.addEventListener('keydown', onKey, true);
                    modal.style.display = 'block';
                    modal.setAttribute('aria-hidden', 'false');
                    document.body.classList.add('modal-open');
                });
            }

            /** Escape o cierre = permanecer (foco en la carpeta nueva tras re-render). */
            async function showExplorerNewFolderChoiceModal(folderName) {
                const modal = document.getElementById('explorerNewFolderChoiceModal');
                if (!modal) return 'stay';
                const displayName = String(folderName || '');
                return await new Promise((resolve) => {
                    const titleEl = document.getElementById('explorerNewFolderChoiceModalTitle');
                    const msgEl = document.getElementById('explorerNewFolderChoiceModalMessage');
                    const btnEnter = document.getElementById('explorerNewFolderChoiceBtnEnter');
                    const btnStay = document.getElementById('explorerNewFolderChoiceBtnStay');
                    const btnCloseX = document.getElementById('explorerNewFolderChoiceModalCloseX');

                    const close = (result) => {
                        modal.removeEventListener('click', onBackdrop);
                        document.removeEventListener('keydown', onKey, true);
                        modal.style.display = 'none';
                        modal.setAttribute('aria-hidden', 'true');
                        document.body.classList.remove('modal-open');
                        resolve(result);
                    };

                    const onBackdrop = (ev) => {
                        if (ev.target === modal) close('stay');
                    };

                    const onKey = (ev) => {
                        if (ev.key === 'Escape') {
                            ev.preventDefault();
                            close('stay');
                        }
                    };

                    if (titleEl) titleEl.textContent = tr('explorerNewFolderChoiceTitle');
                    if (msgEl) msgEl.textContent = tr('explorerNewFolderChoiceMessage').replace('%s', displayName);
                    if (btnEnter) btnEnter.textContent = tr('explorerNewFolderChoiceEnter');
                    if (btnStay) btnStay.textContent = tr('explorerNewFolderChoiceStay');

                    if (btnEnter) btnEnter.onclick = () => close('enter');
                    if (btnStay) btnStay.onclick = () => close('stay');
                    if (btnCloseX) btnCloseX.onclick = () => close('stay');

                    modal.addEventListener('click', onBackdrop);
                    document.addEventListener('keydown', onKey, true);
                    modal.style.display = 'block';
                    modal.setAttribute('aria-hidden', 'false');
                    document.body.classList.add('modal-open');
                    setTimeout(() => {
                        try {
                            if (btnStay) btnStay.focus();
                        } catch (_) {}
                    }, 0);
                });
            }

            async function explorerResolveRemoteConflict(connId, destPath, desiredName, kind, ctx) {
                // ctx: { applyMode?: 'replace'|'rename'|'skip', cache?:Map }
                const cache = ctx?.cache;
                let name = explorerSanitizeName(desiredName);
                if (!name) return { action: 'cancel' };

                while (await explorerRemoteNameExists(connId, destPath, name, cache)) {
                    if (ctx?.applyMode === 'replace') return { action: 'replace', name };
                    if (ctx?.applyMode === 'skip') return { action: 'skip', name };
                    if (ctx?.applyMode === 'rename') {
                        const parsed = explorerSplitName(name, kind);
                        name = explorerSanitizeName(`${parsed.base} (copia)${parsed.ext}`);
                        if (!name) return { action: 'cancel' };
                        continue;
                    }

                    const choice = await showExplorerConflictModal(name, kind);
                    if (!choice || choice.action === 'cancel') return { action: 'cancel' };
                    if (choice.action === 'replace') {
                        if (choice.applyAll) ctx.applyMode = 'replace';
                        return { action: 'replace', name };
                    }
                    if (choice.action === 'skip') {
                        if (choice.applyAll) ctx.applyMode = 'skip';
                        return { action: 'skip', name };
                    }
                    if (choice.action === 'rename') {
                        if (choice.applyAll) ctx.applyMode = 'rename';
                        name = choice.newName;
                        continue;
                    }
                }

                return { action: 'use', name };
            }

            async function explorerLocalNameExists(bucket, destPath, name, cache) {
                let data;
                try {
                    data = await fetchExplorerList(bucket, destPath || '');
                } catch (_) {
                    data = null;
                }
                const list = (data && data.success && Array.isArray(data.items)) ? data.items : [];
                const set = new Set(list.map((it) => String(it?.name || '')).filter(Boolean));
                if (cache) {
                    cache.set(`${bucket}::${destPath || ''}`, set);
                }
                return set.has(name);
            }

            async function explorerResolveLocalConflict(bucket, destPath, desiredName, kind, ctx) {
                const cache = ctx?.cache;
                let name = explorerSanitizeName(desiredName);
                if (!name) return { action: 'cancel' };

                while (await explorerLocalNameExists(bucket, destPath, name, cache)) {
                    if (ctx?.applyMode === 'replace') return { action: 'replace', name };
                    if (ctx?.applyMode === 'skip') return { action: 'skip', name };
                    if (ctx?.applyMode === 'rename') {
                        const parsed = explorerSplitName(name, kind);
                        name = explorerSanitizeName(`${parsed.base} (copia)${parsed.ext}`);
                        if (!name) return { action: 'cancel' };
                        continue;
                    }

                    const choice = await showExplorerConflictModal(name, kind);
                    if (!choice || choice.action === 'cancel') return { action: 'cancel' };
                    if (choice.action === 'replace') {
                        if (choice.applyAll) ctx.applyMode = 'replace';
                        return { action: 'replace', name };
                    }
                    if (choice.action === 'skip') {
                        if (choice.applyAll) ctx.applyMode = 'skip';
                        return { action: 'skip', name };
                    }
                    if (choice.action === 'rename') {
                        if (choice.applyAll) ctx.applyMode = 'rename';
                        name = choice.newName;
                        continue;
                    }
                }

                return { action: 'use', name };
            }

            function getPasteDestinationForTarget(targetItem) {
                if (bucket === 'remote') {
                    if (targetItem && targetItem.type === 'dir') {
                        if (targetItem.__fromGlobal) return targetItem.__relPath || '';
                        return normalizeJoin(remoteRelPath || '', targetItem.name);
                    }
                    return remoteRelPath || '';
                }
                if (isLocalLibraryBucket()) {
                    if (targetItem && targetItem.type === 'dir') {
                        if (targetItem.__fromGlobal) return targetItem.__relPath || '';
                        return normalizeJoin(relPath, targetItem.name);
                    }
                    return relPath || '';
                }
                return relPath || '';
            }

            /** Menú contextual de carpeta local: mismo conjunto de acciones en lista principal y en árbol lateral. */
            function buildLocalExplorerDirContextMenuOptions(parentPath, folderName, openAction) {
                const lb = localListBucketName();
                const targetItem = { name: folderName, type: 'dir' };
                const rowPath = () => parentPath;
                const newFolderParent = normalizeJoin(parentPath, folderName);
                async function feEnsureShortcutAppInGrid() {
                    const draft = feBuildShortcutAppModel(targetItem, rowPath(targetItem), lb);
                    const key = getAppLinkKey(draft);
                    const existingIdx = apps.findIndex((a) => getAppLinkKey(a) === key);
                    if (existingIdx >= 0) return { app: apps[existingIdx], index: existingIdx, created: false };
                    apps.push(draft);
                    if (window.GridLauncher && window.GridLauncher.layoutModel && launcherLayout) {
                        launcherLayout = GridLauncher.layoutModel.appendAppToOrder(launcherLayout, getAppStableId(draft), getAppStableId);
                    }
                    await saveSettings({ apps: apps, launcherLayout: launcherLayout });
                    renderApps();
                    if (windowsMenuVisible) renderWindowsMenu(apps);
                    return { app: draft, index: apps.length - 1, created: true };
                }
                const explorerShortcutMenuOpts = [
                    'separator',
                    { className: 'explorer-shortcut-pin', icon: 'fas fa-thumbtack', label: tr('explorerShortcutPinTaskbar'), action: () => {
                        void feEnsureShortcutAppInGrid().then((r) => pinApp(r.app));
                    }},
                    { className: 'explorer-shortcut-desktop', icon: 'fas fa-desktop', label: tr('explorerShortcutAddDesktop'), action: () => {
                        void feEnsureShortcutAppInGrid().then((r) => {
                            addToDesktop(r.app);
                        });
                    }},
                    { className: 'explorer-shortcut-start', icon: 'fas fa-th-large', label: tr('explorerShortcutAddStartMenu'), action: () => {
                        void feEnsureShortcutAppInGrid().then((r) => {
                            showToast(r.created ? tr('explorerShortcutAddedStartMenu') : tr('explorerShortcutAlreadyInApps'));
                        });
                    }}
                ];
                return [
                    { className: 'open', icon: 'fas fa-external-link-alt', label: tr('open'), action: openAction },
                    ...(window.GridExplorerCustomization ? [{
                        className: 'fe-cust-personalize',
                        icon: 'fas fa-paint-brush',
                        label: 'Personalizar carpeta…',
                        action: () => {
                            const b = window.GridExplorerCustomization.normBucket(lb);
                            const relKey = window.GridExplorerCustomization.folderRelKey(b, parentPath, folderName, null);
                            window.GridExplorerCustomization.fetchCustomization(false).then(() => {
                                window.GridExplorerCustomization.openModal({
                                    mode: 'folder',
                                    bucket: b,
                                    relKey,
                                    title: folderName,
                                    onSaved: () => { void renderFilesView({ refreshTree: true }); }
                                });
                            }).catch(() => {});
                        }
                    }] : []),
                    { className: 'fe-folder-info', icon: 'fas fa-info-circle', label: tr('explorerProperties'), action: () => { void openExplorerFolderInfoFromPath(parentPath, folderName); } },
                    { className: 'share-secure', icon: 'fas fa-share-alt', label: tr('explorerShareSecure'), action: async () => {
                        const r = await explorerShareCreate(lb, parentPath, folderName, 'dir');
                        if (r && r.success && r.url) {
                            await shareSecureUrl(r.url, tr('explorerShareSecure'), true);
                        } else {
                            await appDialog.alert(tr('explorerShareFailed'));
                        }
                    }},
                    ...(bucket === 'files' ? [{ className: 'explorer-new-folder-ctx', icon: 'fas fa-folder-plus', label: tr('explorerNewFolder'), action: () => { void promptAndCreateLocalFolder(newFolderParent); } }] : []),
                    { className: 'clip-copy', icon: 'fas fa-copy', label: tr('explorerClipCopy'), action: () => {
                        const c = getFileClipboard();
                        c.items = [{ fromPath: parentPath, name: folderName, type: 'dir', sourceBucket: lb }];
                        c.action = 'copy';
                        renderItems();
                        showToast(tr('explorerClipCopy'));
                    }},
                    { className: 'clip-cut', icon: 'fas fa-cut', label: tr('explorerClipCut'), action: () => {
                        const c = getFileClipboard();
                        c.items = [{ fromPath: parentPath, name: folderName, type: 'dir', sourceBucket: lb }];
                        c.action = 'cut';
                        renderItems();
                        showToast(tr('explorerClipCut'));
                    }},
                    ...(fileExplorerClipboardHasPasteableItems() ? [{ className: 'clip-paste', icon: 'fas fa-paste', label: tr('explorerPaste'), action: () => { pasteExplorerClipboard(newFolderParent); }}] : []),
                    ...explorerShortcutMenuOpts,
                    { className: 'rename', icon: 'fas fa-edit', label: tr('explorerRename'), action: async () => {
                        const newName = await showExplorerRenameModal(folderName, { isDirectory: true });
                        if (!newName || newName === folderName) return;
                        explorerRename(parentPath, folderName, newName, lb).then((r) => {
                            if (!r || !r.success) return;
                            logActivity('explorer_rename', { path: parentPath, from: folderName, to: newName });
                            remapLocalPathsAfterDirRename(parentPath, folderName, newName);
                            feSyncSelectionAfterItemRename(folderName, newName);
                            fePostRenderFocusItemName = newName;
                            renderFilesView();
                            if (windowsMenuVisible) renderWindowsMenu(apps);
                        });
                    }},
                    { className: 'copy', icon: 'fas fa-clone', label: tr('explorerCopy'), action: async () => {
                        const newName = await appDialog.prompt(tr('explorerPromptDestinationName'), folderName);
                        if (!newName || newName === folderName) return;
                        const kind = 'dir';
                        const conflictCtx = { applyMode: null, cache: new Map() };
                        const resolved = await explorerResolveLocalConflict(lb, parentPath, newName, kind, conflictCtx);
                        if (resolved.action === 'cancel' || resolved.action === 'skip') return;
                        const r = await explorerCopy(parentPath, folderName, resolved.name, lb, { overwrite: resolved.action === 'replace' });
                        if (r && r.success) logActivity('explorer_copy', { path: parentPath, from: folderName, to: resolved.name });
                        renderFilesView();
                    }},
                    { className: 'delete', icon: 'fas fa-trash', label: tr('explorerDelete'), action: async () => {
                        const disp = itemsEl && itemsEl._feDisplayItems ? itemsEl._feDisplayItems : items;
                        const rows = feResolveLocalDeleteRows(disp, folderName);
                        await feDeleteLocalRowsToTrash(rows, lb);
                    }}
                ];
            }

            async function pasteExplorerClipboard(destOverride) {
                const c = getFileClipboard();
                if (!c.items.length) return;
                const dest = destOverride !== undefined && destOverride !== null
                    ? destOverride
                    : (bucket === 'remote' ? (remoteRelPath || '') : ((bucket === 'files' || bucket === 'cache') ? (relPath || '') : ''));
                const total = c.items.length;
                showToast(tr('explorerPasting'));

                // Modal de progreso para copias (especialmente hacia FTP/WebDAV)
                const ensureExplorerProgressModalOnce = () => {
                    if (document.getElementById('explorerProgressModal')) return;
                    const el = document.createElement('div');
                    el.id = 'explorerProgressModal';
                    el.className = 'explorer-progress-modal';
                    el.style.display = 'none';
                    el.setAttribute('aria-hidden', 'true');
                    el.innerHTML = `
                        <div class="explorer-progress-card" role="dialog" aria-modal="true" aria-label="Progreso de copia">
                            <div class="explorer-progress-title" id="explorerProgressTitle">Copiando archivos…</div>
                            <div class="explorer-progress-sub" id="explorerProgressSub"></div>
                            <div class="explorer-progress-bar">
                                <div class="explorer-progress-bar-fill" id="explorerProgressBarFill" style="width:0%"></div>
                            </div>
                            <div class="explorer-progress-meta">
                                <div id="explorerProgressPct">0%</div>
                                <div id="explorerProgressCount"></div>
                            </div>
                        </div>
                    `;
                    document.body.appendChild(el);
                };
                const openExplorerProgressModal = (title) => {
                    ensureExplorerProgressModalOnce();
                    const modal = document.getElementById('explorerProgressModal');
                    if (!modal) return;
                    const t = document.getElementById('explorerProgressTitle');
                    if (t && title) t.textContent = title;
                    const sub = document.getElementById('explorerProgressSub');
                    if (sub) sub.textContent = '';
                    const fill = document.getElementById('explorerProgressBarFill');
                    if (fill) fill.style.width = '0%';
                    const pctEl = document.getElementById('explorerProgressPct');
                    if (pctEl) pctEl.textContent = '0%';
                    const countEl = document.getElementById('explorerProgressCount');
                    if (countEl) countEl.textContent = '';
                    modal.style.display = 'flex';
                    modal.setAttribute('aria-hidden', 'false');
                    document.body.classList.add('modal-open');
                };
                const setExplorerProgressTitle = (title) => {
                    const t = document.getElementById('explorerProgressTitle');
                    if (t && title) t.textContent = title;
                };
                const setExplorerProgress = (percent, subText, doneCount, totalCount) => {
                    const fill = document.getElementById('explorerProgressBarFill');
                    const pctEl = document.getElementById('explorerProgressPct');
                    const sub = document.getElementById('explorerProgressSub');
                    const countEl = document.getElementById('explorerProgressCount');
                    const p = Number.isFinite(percent) ? Math.max(0, Math.min(100, Math.round(percent))) : 0;
                    if (fill) fill.style.width = `${p}%`;
                    if (pctEl) pctEl.textContent = `${p}%`;
                    if (sub) sub.textContent = subText ? String(subText) : '';
                    if (countEl && totalCount) countEl.textContent = `${doneCount || 0}/${totalCount}`;
                };
                const closeExplorerProgressModal = () => {
                    const modal = document.getElementById('explorerProgressModal');
                    if (!modal) return;
                    modal.style.display = 'none';
                    modal.setAttribute('aria-hidden', 'true');
                    document.body.classList.remove('modal-open');
                };

                let okAll = true;
                let done = 0;
                const splitNameForPrompt = (name, kind) => explorerSplitName(name, kind);
                const undoSteps = [];

                const isRemoteItem = (it) => it && it.source === 'remote' && it.connId;
                const allRemote = c.items.every(isRemoteItem);
                const allLocal = c.items.every((it) => !isRemoteItem(it));

                async function localToRemoteCopyRecursive(connId, fromBucket, fromPath, name, kind, remoteDestPath, conflictCtx, onStartCopy) {
                    // kind: 'file' | 'dir'
                    if (kind !== 'dir') {
                        const resolved = await explorerResolveRemoteConflict(connId, remoteDestPath, name, 'file', conflictCtx);
                        if (resolved.action === 'cancel') return { ok: false, cancelled: true };
                        if (resolved.action === 'skip') return { ok: true, skipped: true };
                        try { if (typeof onStartCopy === 'function') onStartCopy(resolved.name || name); } catch (_) {}
                        const r = await explorerExportToRemote(connId, remoteDestPath, fromPath, name, { autoRename: false, remoteName: resolved.name });
                        if (!r || !r.success) return { ok: false, error: r?.error };
                        return { ok: true, storedAs: r.storedAs || resolved.name || name };
                    }

                    // Directorio: crear carpeta destino y copiar contenido recursivo.
                    const resolvedDir = await explorerResolveRemoteConflict(connId, remoteDestPath, name, 'dir', conflictCtx);
                    if (resolvedDir.action === 'cancel') return { ok: false, cancelled: true };
                    if (resolvedDir.action === 'skip') return { ok: true, skipped: true };
                    const dirName = resolvedDir.name || name;
                    try { if (typeof onStartCopy === 'function') onStartCopy(dirName); } catch (_) {}

                    const mk = await explorerConnCreateDir(connId, remoteDestPath, dirName);
                    if (!mk || !mk.success) return { ok: false, error: mk?.error || tr('explorerMoveFailed') };

                    const childLocalPath = normalizeJoin(fromPath || '', name);
                    let data = null;
                    try { data = await fetchExplorerList(fromBucket, childLocalPath); } catch (_) { data = null; }
                    const children = (data && data.success && Array.isArray(data.items)) ? data.items : [];
                    for (const ch of children) {
                        if (!ch || !ch.name) continue;
                        const ck = ch.type === 'dir' ? 'dir' : 'file';
                        const rr = await localToRemoteCopyRecursive(connId, fromBucket, childLocalPath, ch.name, ck, normalizeJoin(remoteDestPath || '', dirName), conflictCtx, onStartCopy);
                        if (!rr || !rr.ok) return rr || { ok: false };
                    }
                    return { ok: true, storedAs: dirName };
                }

                // Remoto → biblioteca local (Archivos o caché remota)
                if ((bucket === 'files' || bucket === 'cache') && allRemote) {
                    const lb = bucket === 'cache' ? 'cache' : 'files';
                    const conflictCtx = { applyMode: null, cache: new Map() };
                    for (const it of c.items) {
                        if (it.type === 'dir') {
                            showToast(tr('explorerMoveFailed'));
                            okAll = false;
                            done++;
                            continue;
                        }
                        const resolved = await explorerResolveLocalConflict(lb, dest, it.name, 'file', conflictCtx);
                        if (resolved.action === 'cancel') { okAll = false; break; }
                        if (resolved.action === 'skip') { done++; if (total > 1) showToast(`${done}/${total}…`); continue; }
                        const r = await explorerImportFromRemote(it.connId, it.fromPath, it.name, dest, {
                            autoRename: false,
                            overwrite: resolved.action === 'replace',
                            destName: resolved.name !== it.name ? resolved.name : undefined,
                            toBucket: lb
                        });
                        let dr = null;
                        if (!r || !r.success) {
                            okAll = false;
                            showToast(r?.error || tr('explorerMoveFailed'));
                        } else {
                            const stored = r.storedAs || it.name;
                            undoSteps.push(async () => {
                                await explorerDelete(dest, stored, lb);
                            });
                            if (c.action === 'cut') {
                                dr = await explorerConnDeleteToTrash(it.connId, it.fromPath, it.name);
                                if (!dr || !dr.success) {
                                    okAll = false;
                                    showToast(dr?.error || tr('explorerMoveFailed'));
                                } else if (dr.id) {
                                    const trashId = dr.id;
                                    undoSteps.push(async () => {
                                        await explorerRestoreTrashItem(trashId);
                                    });
                                }
                            }
                        }
                        done++;
                        if (total > 1) showToast(`${done}/${total}…`);
                    }
                    if (c.action === 'cut') {
                        window.__gridFileClipboard = { items: [], action: 'copy' };
                    }
                    await renderFilesView();
                    if (okAll) showToast(tr('explorerStatusOk'));
                    feCommitUndoSequential(undoSteps);
                    return;
                }

                // Local → remoto
                if (bucket === 'remote' && remoteCtx && remoteCtx.id && allLocal) {
                    const conflictCtx = { applyMode: null, cache: new Map() };
                    const connIdLocal = remoteCtx.id;
                    let progressOpened = false;
                    const onStartCopy = (currentName) => {
                        if (!progressOpened) {
                            openExplorerProgressModal('Copiando a FTP…');
                            progressOpened = true;
                        }
                        setExplorerProgress(total ? (done / total) * 100 : 0, currentName, done, total);
                    };
                    for (const it of c.items) {
                        const kind = it.type === 'dir' ? 'dir' : 'file';
                        const r = await localToRemoteCopyRecursive(connIdLocal, it.sourceBucket || 'files', it.fromPath || '', it.name, kind, dest, conflictCtx, onStartCopy);
                        let dr = null;
                        if (!r || !r.ok) {
                            okAll = false;
                            showToast(r?.error || tr('explorerMoveFailed'));
                        } else {
                            const remoteStored = r.storedAs || it.name;
                            const delKind = kind === 'dir' ? 'dir' : 'file';
                            undoSteps.push(async () => {
                                await explorerConnDelete(connIdLocal, dest, remoteStored, delKind);
                            });
                            if (c.action === 'cut') {
                                dr = await explorerDelete(it.fromPath, it.name, it.sourceBucket || 'files');
                                if (!dr || !dr.success) {
                                    okAll = false;
                                    showToast(dr?.error || tr('explorerMoveFailed'));
                                } else if (dr.id) {
                                    const trashId = dr.id;
                                    undoSteps.push(async () => {
                                        await explorerRestoreTrashItem(trashId);
                                    });
                                }
                            }
                        }
                        done++;
                        if (progressOpened) setExplorerProgress(total ? (done / total) * 100 : 100, it.name, done, total);
                        if (total > 1) showToast(`${done}/${total}…`);
                    }
                    if (c.action === 'cut') {
                        window.__gridFileClipboard = { items: [], action: 'copy' };
                    }
                    // Esperar a que el FTP refleje y renderice antes de cerrar el modal.
                    if (!progressOpened) openExplorerProgressModal('Copiando a FTP…');
                    setExplorerProgressTitle('Actualizando carpeta…');
                    setExplorerProgress(100, 'Esperando que el FTP muestre el archivo…', done, total);
                    try {
                        const expected = undoSteps
                            .map((_, idx) => idx) // dummy; se sobreescribe abajo si hay nombres
                            .filter(() => false);
                        // Mejor esfuerzo: al menos esperar por los nombres pegados (si los tenemos).
                        // Como aquí el nombre puede cambiar por conflicto, usamos el storedAs si existió.
                        // Recorremos el clipboard original y asumimos el nombre final como storedAs cuando exista.
                        // (Si no se pudo, igual se renderiza con reintentos por tiempo.)
                        // Nota: este array se rellena durante el loop con setExplorerProgress, así que si no, queda vacío.
                        void expected;
                    } catch (_) {}
                    // Reintentar refrescar un par de veces (FTP puede tardar)
                    await explorerWaitForRemoteItemsVisible(connIdLocal, dest, [], 2000);
                    await renderRemoteView();
                    closeExplorerProgressModal();
                    if (okAll) showToast(tr('explorerStatusOk'));
                    feCommitUndoSequential(undoSteps);
                    return;
                }

                // Solo remoto (misma u otra conexión vía API remota)
                if (bucket === 'remote' && remoteCtx && remoteCtx.id && allRemote) {
                    if (!c.items.every((it) => it.connId === remoteCtx.id)) {
                        showToast(tr('explorerMoveFailed'));
                        return;
                    }
                    const connId = remoteCtx.id;
                    const conflictCtx = { applyMode: null, cache: new Map() };
                    for (const it of c.items) {
                        const isCopy = c.action === 'copy';
                        if (it.type === 'dir' && isCopy) {
                            showToast(tr('explorerMoveFailed'));
                            okAll = false;
                            done++;
                            continue;
                        }
                        let r;
                        if (isCopy) {
                            const kind = it.type === 'dir' ? 'dir' : 'file';
                            const resolved = await explorerResolveRemoteConflict(connId, dest, it.name, kind, conflictCtx);
                            if (resolved.action === 'cancel') { okAll = false; break; }
                            if (resolved.action === 'skip') { done++; if (total > 1) showToast(`${done}/${total}…`); continue; }
                            // Reemplazar o usar: COPY con Overwrite:T (WebDAV) / ftp_put (FTP) ya sobrescribe.
                            r = await explorerConnCopyRemote(connId, it.fromPath, dest, it.name, resolved.name !== it.name ? resolved.name : undefined);
                            if (r && r.success) {
                                const destFinal = resolved.name !== it.name ? resolved.name : it.name;
                                const k = it.type === 'dir' ? 'dir' : 'file';
                                undoSteps.push(async () => {
                                    await explorerConnDelete(connId, dest, destFinal, k);
                                });
                            }
                        } else {
                            const kind = it.type === 'dir' ? 'dir' : 'file';
                            const resolved = await explorerResolveRemoteConflict(connId, dest, it.name, kind, conflictCtx);
                            if (resolved.action === 'cancel') { okAll = false; break; }
                            if (resolved.action === 'skip') { done++; if (total > 1) showToast(`${done}/${total}…`); continue; }
                            const destFinal = resolved.name !== it.name ? resolved.name : it.name;
                            if (resolved.action === 'replace') {
                                r = await explorerConnMoveRemote(connId, it.fromPath, dest, it.name, resolved.name !== it.name ? resolved.name : undefined, { overwrite: true });
                            } else {
                                r = await explorerConnMoveRemote(connId, it.fromPath, dest, it.name, resolved.name !== it.name ? resolved.name : undefined);
                            }
                            if (r && r.success) {
                                undoSteps.push(async () => {
                                    await explorerConnMoveRemote(connId, dest, it.fromPath, destFinal, it.name);
                                });
                            }
                        }
                        if (!r || !r.success) {
                            okAll = false;
                            showToast(r?.error || tr('explorerMoveFailed'));
                        }
                        done++;
                        if (total > 1) showToast(`${done}/${total}…`);
                    }
                    if (c.action === 'cut') {
                        window.__gridFileClipboard = { items: [], action: 'copy' };
                    }
                    await renderRemoteView();
                    if (okAll) showToast(tr('explorerStatusOk'));
                    feCommitUndoSequential(undoSteps);
                    return;
                }

                if (bucket === 'remote' && !allRemote) {
                    showToast(tr('explorerMoveFailed'));
                    return;
                }
                if (bucket !== 'remote' && allRemote) {
                    showToast(tr('explorerMoveFailed'));
                    return;
                }

                const localConflictCtx = { applyMode: null, cache: new Map() };
                for (const it of c.items) {
                    const isCopy = c.action === 'copy';
                    const fromB = it.sourceBucket || 'files';
                    const toB = bucket === 'cache' ? 'cache' : 'files';
                    const kind = it.type === 'dir' ? 'dir' : 'file';
                    if (isCopy) {
                        let r;
                        const samePathPaste = (it.fromPath || '') === (dest || '') && fromB === toB;
                        if (samePathPaste) {
                            const parsed = splitNameForPrompt(it.name, kind);
                            const suggested = parsed.base ? (parsed.base + ' - copia') : 'copia';
                            const nextBase = await appDialog.prompt(tr('explorerPromptDestinationName'), suggested);
                            if (!nextBase) { done++; if (total > 1) showToast(`${done}/${total}…`); continue; }
                            let safeBase = String(nextBase).trim();
                            if (!safeBase) { done++; if (total > 1) showToast(`${done}/${total}…`); continue; }
                            safeBase = safeBase.replace(/[\\\/:*?"<>|]/g, '_');
                            const tentative = safeBase + parsed.ext;
                            const resolved = await explorerResolveLocalConflict(toB, dest, tentative, kind, localConflictCtx);
                            if (resolved.action === 'cancel') { okAll = false; break; }
                            if (resolved.action === 'skip') { done++; if (total > 1) showToast(`${done}/${total}…`); continue; }
                            const finalName = resolved.name;
                            r = await explorerCopy(it.fromPath, it.name, finalName, fromB, { overwrite: resolved.action === 'replace' });
                            if (r && r.success) {
                                undoSteps.push(async () => {
                                    await explorerDelete(dest, finalName, toB);
                                });
                            }
                        } else {
                            const resolved = await explorerResolveLocalConflict(toB, dest, it.name, kind, localConflictCtx);
                            if (resolved.action === 'cancel') { okAll = false; break; }
                            if (resolved.action === 'skip') { done++; if (total > 1) showToast(`${done}/${total}…`); continue; }
                            const opts = {
                                fromBucket: fromB,
                                toBucket: toB,
                                autoRename: false,
                                overwrite: resolved.action === 'replace',
                            };
                            if (resolved.name !== it.name) opts.destName = resolved.name;
                            r = await explorerCopyTo(it.fromPath, dest, it.name, opts);
                            if (r && r.success) {
                                const sn = r.storedAs || resolved.name || it.name;
                                undoSteps.push(async () => {
                                    await explorerDelete(dest, sn, toB);
                                });
                            }
                        }
                        if (!r || !r.success) {
                            okAll = false;
                            showToast(r?.error || tr('explorerMoveFailed'));
                        }
                    } else {
                        const resolved = await explorerResolveLocalConflict(toB, dest, it.name, kind, localConflictCtx);
                        if (resolved.action === 'cancel') { okAll = false; break; }
                        if (resolved.action === 'skip') { done++; if (total > 1) showToast(`${done}/${total}…`); continue; }
                        const finalName = resolved.name;
                        const r = await explorerMove(it.fromPath, dest, it.name, {
                            fromBucket: fromB,
                            toBucket: toB,
                            overwrite: resolved.action === 'replace',
                            destName: finalName !== it.name ? finalName : undefined
                        });
                        if (!r || !r.success) {
                            okAll = false;
                            showToast(r?.error || tr('explorerMoveFailed'));
                        } else {
                            const fp = it.fromPath;
                            const nm = it.name;
                            undoSteps.push(async () => {
                                await explorerMove(dest, fp, finalName, { fromBucket: toB, toBucket: fromB });
                            });
                        }
                    }
                    done++;
                    if (total > 1) showToast(`${done}/${total}…`);
                }
                if (c.action === 'cut') {
                    window.__gridFileClipboard = { items: [], action: 'copy' };
                }
                await renderFilesView();
                if (okAll) showToast(tr('explorerStatusOk'));
                feCommitUndoSequential(undoSteps);
            }

            function renderItems() {
                itemsEl.innerHTML = '';
                syncExplorerItemsViewClasses();
                if (feMultiSelectBtn) {
                    const show = isLocalLibraryBucket();
                    feMultiSelectBtn.style.display = show ? '' : 'none';
                    if (!show) {
                        feToolbarMultiSelectActive = false;
                        feMultiSelectBtn.classList.remove('active');
                        feMultiSelectBtn.setAttribute('aria-pressed', 'false');
                        feMultiSelectBtn.title = tr('explorerMultiSelectMode');
                    }
                }
                const qTrim = feSearchInput ? (feSearchInput.value || '').trim() : '';
                if (globalSearchPending && qTrim && (bucket === 'files' || bucket === 'cache' || bucket === 'remote')) {
                    itemsEl.innerHTML = `<div style="grid-column:1/-1; padding:14px; opacity:0.85;">${tr('explorerSearching')}</div>`;
                    itemsEl._feDisplayItems = [];
                    try { syncExplorerItemsSplitAdaptive(); } catch (_) {}
                    scheduleExplorerKeyboardFocus();
                    return;
                }
                let displayItems = items;
                if (feSearchInput) {
                    const sq = (feSearchInput.value || '').trim().toLowerCase();
                    if ((bucket === 'files' || bucket === 'cache') && globalSearchActive && globalSearchRaw !== null) {
                        displayItems = globalSearchRaw.map((r) => ({
                            name: r.name,
                            type: r.type,
                            mimeType: r.mimeType || 'application/octet-stream',
                            size: r.size || 0,
                            mtime: r.mtime || 0,
                            url: r.url || '',
                            __fromGlobal: true,
                            __parentPath: r.parentPath || '',
                            __relPath: r.relPath || '',
                            __pathLabel: formatExplorerTreeLabel(r.relPath || '', r.parentPath || '', r.type)
                        }));
                    } else if (bucket === 'remote' && globalSearchActive && globalSearchRaw !== null) {
                        displayItems = globalSearchRaw.map((r) => ({
                            name: r.name,
                            type: r.type,
                            mimeType: r.mimeType || 'application/octet-stream',
                            size: r.size || 0,
                            mtime: r.mtime || 0,
                            __fromGlobal: true,
                            __parentPath: r.parentPath || '',
                            __relPath: r.relPath || '',
                            __pathLabel: r.__pathLabel || formatExplorerTreeLabel(r.relPath || '', r.parentPath || '', r.type)
                        }));
                    } else if (sq && (isLocalLibraryBucket() || bucket === 'remote' || bucket === 'trash')) {
                        displayItems = items.filter((it) => (it.name || '').toLowerCase().includes(sq));
                    }
                }
                displayItems = appendSharedSpaceShortcutsIfRoot(displayItems);

                const sortedDisplay = (displayItems && displayItems.length && feDetailsLike(viewMode))
                    ? (() => {
                        const col = feDetailsSort.col;
                        const dir = feDetailsSort.dir === 'desc' ? -1 : 1;
                        const out = displayItems.slice();
                        const typeRank = (t) => (t === 'dir' ? 0 : 1);
                        out.sort((a, b) => {
                            let va;
                            let vb;
                            if (col === 'name') {
                                va = (a.__shortcutLabel || a.name || '').toLowerCase();
                                vb = (b.__shortcutLabel || b.name || '').toLowerCase();
                            } else if (col === 'size') {
                                va = a.type === 'dir' ? -1 : Number(a.size || 0);
                                vb = b.type === 'dir' ? -1 : Number(b.size || 0);
                            } else if (col === 'mtime') {
                                va = Number(a.mtime || 0);
                                vb = Number(b.mtime || 0);
                            } else if (col === 'type') {
                                va = typeRank(a.type) + (a.mimeType || a.name || '').toLowerCase();
                                vb = typeRank(b.type) + (b.mimeType || b.name || '').toLowerCase();
                            } else {
                                va = (a.name || '').toLowerCase();
                                vb = (b.name || '').toLowerCase();
                            }
                            if (va < vb) return -1 * dir;
                            if (va > vb) return 1 * dir;
                            return 0;
                        });
                        return out;
                    })()
                    : displayItems;

                function isPendingCut(name) {
                    const c = getFileClipboard();
                    if (c.action !== 'cut') return false;
                    const row = displayItems.find((x) => x.name === name);
                    const fp = row && row.__fromGlobal ? row.__parentPath : relPath;
                    const nfp = (fp == null || fp === '') ? '' : String(fp);
                    return c.items.some((it) => {
                        const itp = (it.fromPath == null || it.fromPath === '') ? '' : String(it.fromPath);
                        return itp === nfp && it.name === name;
                    });
                }

                function updateSelectionStyles() {
                    itemsEl.querySelectorAll('.explorer-item').forEach((el) => {
                        const n = el.dataset.name;
                        el.classList.toggle('explorer-item-selected', (isLocalLibraryBucket() || bucket === 'remote') && selectedNames.has(n));
                        el.classList.toggle('explorer-item-pending-cut', isLocalLibraryBucket() && isPendingCut(n));
                    });
                    try { refreshMultiSelectUi(); } catch (_) {}
                    try { void refreshExplorerPropertiesPanel(); } catch (_) {}
                    try { void updateExplorerPreviewPane(); } catch (_) {}
                }

                function scheduleExplorerKeyboardFocus() {
                    if (!itemsEl || typeof itemsEl.focus !== 'function') return;
                    const ae0 = document.activeElement;
                    if (feSearchInput && (ae0 === feSearchInput || (feSearchInput.contains && feSearchInput.contains(ae0)))) return;
                    if (ae0 && host.contains(ae0) && ae0.matches && ae0.matches('input, textarea, select')) return;
                    requestAnimationFrame(() => {
                        requestAnimationFrame(() => {
                            const ae = document.activeElement;
                            if (feSearchInput && (ae === feSearchInput || (feSearchInput.contains && feSearchInput.contains(ae)))) return;
                            if (ae && host.contains(ae) && ae.matches && ae.matches('input, textarea, select')) return;
                            const tiles = Array.from(itemsEl.querySelectorAll('.explorer-item'));
                            if (tiles.length === 0) {
                                try {
                                    itemsEl.focus({ preventScroll: true });
                                } catch (_) {
                                    itemsEl.focus();
                                }
                                return;
                            }
                            let focusTile = tiles[0];
                            if (isLocalLibraryBucket() || bucket === 'remote') {
                                const namesInView = new Set((sortedDisplay || []).map((x) => x && x.name).filter(Boolean));
                                Array.from(selectedNames).forEach((n) => {
                                    if (!namesInView.has(n)) selectedNames.delete(n);
                                });
                                const pendingFocus = fePostRenderFocusItemName;
                                if (pendingFocus) fePostRenderFocusItemName = null;
                                if (pendingFocus && namesInView.has(pendingFocus)) {
                                    const m = tiles.find((t) => t.dataset.name === pendingFocus);
                                    if (m) {
                                        focusTile = m;
                                        if (selectedNames.size === 0 && pendingFocus) {
                                            selectedNames.add(pendingFocus);
                                            const fi = parseInt(m.dataset.idx, 10);
                                            lastSelIdx = Number.isFinite(fi) ? fi : 0;
                                            keyboardSelAnchorIdx = lastSelIdx;
                                        }
                                    }
                                } else {
                                    const firstSel = Array.from(selectedNames).find((n) => namesInView.has(n));
                                    if (firstSel) {
                                        const match = tiles.find((t) => t.dataset.name === firstSel);
                                        if (match) focusTile = match;
                                    }
                                    if (selectedNames.size === 0 && focusTile.dataset.name) {
                                        selectedNames.add(focusTile.dataset.name);
                                        const fi = parseInt(focusTile.dataset.idx, 10);
                                        lastSelIdx = Number.isFinite(fi) ? fi : 0;
                                        keyboardSelAnchorIdx = lastSelIdx;
                                    }
                                }
                                updateSelectionStyles();
                            } else if (fePostRenderFocusItemName) {
                                const want = fePostRenderFocusItemName;
                                fePostRenderFocusItemName = null;
                                const m = tiles.find((t) => t.dataset.name === want);
                                if (m) focusTile = m;
                            }
                            try {
                                focusTile.focus({ preventScroll: true });
                            } catch (_) {
                                focusTile.focus();
                            }
                        });
                    });
                }

                if (!sortedDisplay || sortedDisplay.length === 0) {
                    const qStr = feSearchInput ? (feSearchInput.value || '').trim() : '';
                    let emptyMsg = tr('explorerNoItems');
                    if (qStr) {
                        if ((bucket === 'files' || bucket === 'cache' || bucket === 'remote') && globalSearchActive && Array.isArray(globalSearchRaw) && globalSearchRaw.length === 0) {
                            emptyMsg = tr('explorerSearchNoResults');
                        } else if (!globalSearchActive && (isLocalLibraryBucket() || bucket === 'remote' || bucket === 'trash') && items.length > 0) {
                            emptyMsg = tr('explorerSearchNoResults');
                        }
                    }
                    itemsEl.innerHTML = `<div class="fe-items-empty-msg">${emptyMsg}</div>`;
                    itemsEl._feDisplayItems = [];
                    try { syncExplorerItemsSplitAdaptive(); } catch (_) {}
                    scheduleExplorerKeyboardFocus();
                    return;
                }

                if (feDetailsLike(viewMode)) {
                    const sortBtn = (col, labelKey) => {
                        const label = tr(labelKey);
                        const cur = feDetailsSort.col === col;
                        const arrow = cur ? (feDetailsSort.dir === 'asc' ? ' ▲' : ' ▼') : '';
                        return `<span role="button" tabindex="0" class="fe-details-sort-label" data-fe-sort="${escapeHtmlAttr(col)}">${escapeHtml(label + arrow)}</span>`;
                    };
                    const hdr = document.createElement('div');
                    hdr.className = 'fe-details-header';
                    hdr.innerHTML = `
                            <div class="fe-detail-corner" aria-hidden="true"></div>
                            <div class="fe-detail-h fe-d-name">${sortBtn('name', 'explorerColName')}</div>
                            <div class="fe-detail-h fe-d-type">${sortBtn('type', 'explorerColType')}</div>
                            <div class="fe-detail-h fe-d-size">${sortBtn('size', 'explorerColSize')}</div>
                            <div class="fe-detail-h fe-d-mtime">${sortBtn('mtime', 'explorerColModified')}</div>
                            <div class="fe-detail-h fe-d-ext"><span class="fe-detail-h-static">${escapeHtml(tr('explorerColExt'))}</span></div>`;
                    const applyDetailsSortFromEl = (el) => {
                        const c = el.getAttribute('data-fe-sort');
                        if (!c) return;
                        if (feDetailsSort.col === c) feDetailsSort.dir = feDetailsSort.dir === 'asc' ? 'desc' : 'asc';
                        else {
                            feDetailsSort.col = c;
                            feDetailsSort.dir = 'asc';
                        }
                        try { localStorage.setItem(DETAILS_SORT_KEY, JSON.stringify(feDetailsSort)); } catch (_) {}
                        renderItems();
                    };
                    hdr.querySelectorAll('[data-fe-sort]').forEach((b) => {
                        b.addEventListener('click', (ev) => {
                            ev.preventDefault();
                            applyDetailsSortFromEl(b);
                        });
                        b.addEventListener('keydown', (ev) => {
                            if (ev.key !== 'Enter' && ev.key !== ' ') return;
                            ev.preventDefault();
                            applyDetailsSortFromEl(b);
                        });
                    });
                    itemsEl.appendChild(hdr);
                }

                function feExplorerThumbWrapClass() {
                    if (viewMode === 'icons_xl') return 'fe-thumb-xl';
                    if (viewMode === 'icons_lg' || viewMode === 'multimedia') return 'fe-thumb-lg';
                    if (viewMode === 'icons_sm' || viewMode === 'compact' || viewMode === 'split_preview_hscroll') return 'fe-thumb-sm';
                    if (viewMode === 'content') return 'fe-thumb-content';
                    if (viewMode === 'tiles') return 'fe-thumb-tile';
                    return 'fe-thumb-md';
                }

                sortedDisplay.forEach((item, idx) => {
                    const tile = document.createElement('div');
                    tile.className = 'explorer-item' + (item.__sharedShortcut ? ' explorer-item-shared-shortcut' : '') + (feDetailsLike(viewMode) ? ' fe-details-row' : '');
                    tile.tabIndex = 0;
                    tile.dataset.idx = String(idx);
                    tile.dataset.name = item.name || '';
                    tile.dataset.type = item.type || '';
                    tile.dataset.id = item.id || '';
                    const displayName = item.__shortcutLabel || item.name || '';

                    const remoteThumbParent = (bucket === 'remote' && item.__fromGlobal) ? (item.__parentPath || '') : (remoteRelPath || '');
                    const thumbCtxBase = bucket === 'remote' && remoteCtx && remoteCtx.id
                        ? { isRemote: true, connId: remoteCtx.id, remotePath: remoteThumbParent, thumbWrapClass: feExplorerThumbWrapClass() }
                        : {
                            thumbWrapClass: feExplorerThumbWrapClass(),
                            ecBucket: isLocalLibraryBucket() ? localListBucketName() : null,
                            ecParentRel: relPath
                        };
                    let thumb;
                    if (item.__sharedShortcut) {
                        const icon = item.__shortcutIcon || 'fa-folder';
                        thumb = `<div class="explorer-item-thumb ${feExplorerThumbWrapClass()} explorer-item-thumb-shared explorer-item-thumb-shared-${item.__sharedBucket || 'community'}"><i class="fas ${escapeHtmlAttr(icon)} fe-thumb-shared-ico" aria-hidden="true"></i></div>`;
                    } else {
                        thumb = getFileThumbHtml(item, thumbCtxBase);
                        if (item.type !== 'dir' && thumbCtxBase.ecBucket && window.GridExplorerCustomization && typeof window.GridExplorerCustomization.postProcessFileThumbHtml === 'function') {
                            thumb = window.GridExplorerCustomization.postProcessFileThumbHtml(thumb, item, thumbCtxBase);
                        }
                    }
                    const sub = item.__shortcutSub || (item.type === 'dir' ? tr('explorerFolder') : formatBytes(item.size));
                    const subDisplay = (item.__pathLabel !== undefined && item.__pathLabel !== '') ? item.__pathLabel : sub;
                    const extOnly = (displayName || '').includes('.') ? displayName.slice(displayName.lastIndexOf('.') + 1) : '';
                    const typeLabel = item.__shortcutSub || (item.type === 'dir' ? tr('explorerFolder') : (item.mimeType || extOnly || '—'));

                    if (feDetailsLike(viewMode)) {
                        tile.innerHTML = `
                                ${thumb}
                                <div class="fe-detail-cell fe-d-name explorer-item-name">${escapeHtml(displayName)}</div>
                                <div class="fe-detail-cell fe-d-type">${escapeHtml(typeLabel)}</div>
                                <div class="fe-detail-cell fe-d-size">${escapeHtml(item.type === 'dir' ? '—' : formatBytes(item.size))}</div>
                                <div class="fe-detail-cell fe-d-mtime">${escapeHtml(formatModDate(item.mtime))}</div>
                                <div class="fe-detail-cell fe-d-ext">${escapeHtml(item.type === 'dir' ? '—' : extOnly)}</div>`;
                    } else if (feListLike(viewMode)) {
                        tile.innerHTML = `
                            ${thumb}
                            <div class="explorer-item-meta">
                                <div style="flex:1;min-width:0;">
                                    <div class="explorer-item-name">${escapeHtml(displayName)}</div>
                                    <div class="explorer-item-sub explorer-item-pathline">${escapeHtml(subDisplay || '')}</div>
                                </div>
                                <div class="explorer-col-size">${item.type === 'dir' ? '—' : escapeHtml(formatBytes(item.size))}</div>
                                <div class="explorer-col-date">${escapeHtml(formatModDate(item.mtime))}</div>
                            </div>`;
                    } else if (viewMode === 'content') {
                        const snippet = (item.type === 'dir') ? tr('explorerFolder') : '—';
                        tile.innerHTML = `
                            ${thumb}
                            <div class="explorer-item-content-meta">
                                <div class="explorer-item-name">${escapeHtml(displayName)}</div>
                                <div class="explorer-item-sub">${escapeHtml(typeLabel)} · ${escapeHtml(formatModDate(item.mtime))} · ${item.type === 'dir' ? '—' : escapeHtml(formatBytes(item.size))}</div>
                                <div class="explorer-item-snippet">${escapeHtml(tr('explorerContentSnippet'))}: ${escapeHtml(snippet)}</div>
                            </div>`;
                    } else if (viewMode === 'tiles') {
                        tile.innerHTML = `
                            ${thumb}
                            <div class="explorer-item-name">${escapeHtml(displayName)}</div>
                            <div class="explorer-item-sub explorer-tile-meta">${escapeHtml(typeLabel)} · ${item.type === 'dir' ? '—' : escapeHtml(formatBytes(item.size))}</div>`;
                    } else {
                        tile.innerHTML = `
                            ${thumb}
                            <div class="explorer-item-name">${escapeHtml(displayName)}</div>
                            <div class="explorer-item-sub explorer-item-pathline">${escapeHtml(subDisplay || '')}</div>`;
                    }

                    if (isLocalLibraryBucket() && !item.__sharedShortcut) {
                        tile.draggable = true;
                    }

                    const menuBtn = document.createElement('button');
                    menuBtn.type = 'button';
                    menuBtn.className = 'explorer-item-menu-btn';
                    menuBtn.title = 'Menú';
                    menuBtn.setAttribute('aria-label', 'Menú');
                    menuBtn.innerHTML = '<i class="fas fa-chevron-down"></i>';
                    tile.appendChild(menuBtn);

                    if (isLocalLibraryBucket() && item.type === 'dir' && !item.__sharedShortcut) {
                        tile.addEventListener('dragover', (e) => {
                            if (!explorerDragPayload) return;
                            e.preventDefault();
                            const isCopy = explorerDragPayload.mode === 'copy';
                            e.dataTransfer.dropEffect = isCopy ? 'copy' : 'move';
                            tile.classList.add('explorer-drop-target');
                        });
                        tile.addEventListener('dragleave', () => tile.classList.remove('explorer-drop-target'));
                        tile.addEventListener('drop', async (e) => {
                            e.preventDefault();
                            e.stopPropagation();
                            tile.classList.remove('explorer-drop-target');
                            if (!explorerDragPayload || !isLocalLibraryBucket()) return;
                            const destPath = normalizeJoin(relPath, item.name);
                            const names = explorerDragPayload.names && explorerDragPayload.names.length
                                ? explorerDragPayload.names
                                : [explorerDragPayload.name];
                            const isCopy = explorerDragPayload.mode === 'copy';
                            await executeExplorerDropToPath(destPath, names, isCopy);
                        });
                    } else if (item.__sharedShortcut) {
                        tile.addEventListener('dragover', (e) => {
                            if (!explorerDragPayload) return;
                            e.preventDefault();
                            const isCopy = explorerDragPayload.mode === 'copy';
                            e.dataTransfer.dropEffect = isCopy ? 'copy' : 'move';
                            tile.classList.add('explorer-drop-target');
                        });
                        tile.addEventListener('dragleave', () => tile.classList.remove('explorer-drop-target'));
                        tile.addEventListener('drop', async (e) => {
                            e.preventDefault();
                            e.stopPropagation();
                            tile.classList.remove('explorer-drop-target');
                            if (!explorerDragPayload) return;
                            const names = explorerDragPayload.names && explorerDragPayload.names.length
                                ? explorerDragPayload.names
                                : [explorerDragPayload.name];
                            const isCopy = explorerDragPayload.mode === 'copy';
                            await executeExplorerDropOnShortcut(item, names, isCopy);
                        });
                    }

                    if (isLocalLibraryBucket()) {
                        tile.addEventListener('click', (e) => {
                            if (e.detail > 1) return;
                            e.stopPropagation();
                            try {
                                tile.focus({ preventScroll: true });
                            } catch (_) {
                                tile.focus();
                            }
                            const toolbarMulti = feToolbarMultiSelectActive && !(e.ctrlKey || e.metaKey || e.shiftKey);
                            const touchMulti = (feLastPointerType === 'touch' && feTouchMultiSelectActive && !(e.ctrlKey || e.metaKey || e.shiftKey));
                            if (toolbarMulti || touchMulti) {
                                if (selectedNames.has(item.name)) selectedNames.delete(item.name);
                                else selectedNames.add(item.name);
                                lastSelIdx = idx;
                                keyboardSelAnchorIdx = idx;
                            } else if (e.ctrlKey || e.metaKey) {
                                if (selectedNames.has(item.name)) selectedNames.delete(item.name);
                                else selectedNames.add(item.name);
                                lastSelIdx = idx;
                                keyboardSelAnchorIdx = idx;
                            } else if (e.shiftKey) {
                                const a = Math.min(lastSelIdx, idx);
                                const b = Math.max(lastSelIdx, idx);
                                for (let i = a; i <= b; i++) {
                                    if (sortedDisplay[i]) selectedNames.add(sortedDisplay[i].name);
                                }
                            } else {
                                selectedNames.clear();
                                selectedNames.add(item.name);
                                lastSelIdx = idx;
                                keyboardSelAnchorIdx = idx;
                                if (item.__sharedShortcut) {
                                    try {
                                        if (window.matchMedia('(pointer: coarse)').matches) {
                                            e.preventDefault();
                                            openSharedSpaceShortcut(item);
                                            return;
                                        }
                                    } catch (_) {}
                                }
                            }
                            updateSelectionStyles();
                        });
                        // UX táctil: long-press activa multiselección (sin teclas modificadoras)
                        tile.addEventListener('pointerdown', (e) => {
                            if (!e || e.pointerType !== 'touch') return;
                            // evitar interferencia con botón de menú / inputs
                            if (e.target && e.target.closest && e.target.closest('.explorer-item-menu-btn')) return;
                            feTouchLongPressFired = false;
                            feTouchLongPressPointerId = e.pointerId;
                            if (feTouchLongPressTimer) {
                                clearTimeout(feTouchLongPressTimer);
                                feTouchLongPressTimer = null;
                            }
                            feTouchLongPressTimer = setTimeout(() => {
                                feTouchLongPressTimer = null;
                                feTouchLongPressFired = true;
                                feTouchMultiSelectActive = true;
                                if (selectedNames.has(item.name)) selectedNames.delete(item.name);
                                else selectedNames.add(item.name);
                                lastSelIdx = idx;
                                keyboardSelAnchorIdx = idx;
                                updateSelectionStyles();
                                feExplorerSkipNextClick = true; // cancelar el click sintético post long-press
                                try { tile.focus({ preventScroll: true }); } catch (_) { try { tile.focus(); } catch (_) {} }
                            }, 420);
                        }, { passive: true });
                        const cancelLongPress = (e) => {
                            if (feTouchLongPressPointerId !== null && e && e.pointerId !== feTouchLongPressPointerId) return;
                            if (feTouchLongPressTimer) {
                                clearTimeout(feTouchLongPressTimer);
                                feTouchLongPressTimer = null;
                            }
                            feTouchLongPressPointerId = null;
                        };
                        tile.addEventListener('pointerup', cancelLongPress, { passive: true });
                        tile.addEventListener('pointercancel', cancelLongPress, { passive: true });
                        tile.addEventListener('pointerleave', cancelLongPress, { passive: true });
                        tile.addEventListener('dblclick', (e) => {
                            e.preventDefault();
                            if (item.__sharedShortcut) {
                                openSharedSpaceShortcut(item);
                                return;
                            }
                            if (item.__fromGlobal) {
                                if (item.type === 'dir') navigateToPath(item.__relPath || '');
                                else openExplorerFile(item, item.__parentPath);
                                return;
                            }
                            if (item.type === 'dir') enterFolder(item.name);
                            else openExplorerFile(item, relPath);
                        });
                    } else if (bucket === 'trash') {
                        const openTrashItem = () => {
                            if (item.type === 'dir') {
                                if (!feTrashCtx && item.id && item.name) {
                                    feTrashCtx = { id: item.id, name: item.name };
                                    feTrashRelPath = '';
                                    selectedNames.clear();
                                    void renderTrashView();
                                    return;
                                }
                                if (feTrashCtx && item.name) {
                                    feTrashRelPath = normalizeJoin(feTrashRelPath || '', item.name);
                                    selectedNames.clear();
                                    void renderTrashView();
                                }
                                return;
                            }
                            if (item.url) openExplorerFile(item, (feTrashCtx ? ('trash:' + (feTrashCtx.id || '') + ':' + (feTrashRelPath || '')) : 'trash'));
                        };
                        tile.addEventListener('dblclick', (e) => {
                            e.preventDefault();
                            openTrashItem();
                        });
                        tile.addEventListener('click', (e) => {
                            // En papelera, click no restaura. En móvil (puntero grueso), abre/navega.
                            e.stopPropagation();
                            try { tile.focus({ preventScroll: true }); } catch (_) { try { tile.focus(); } catch (_) {} }
                            try {
                                if (window.matchMedia('(pointer: coarse)').matches) {
                                    e.preventDefault();
                                    openTrashItem();
                                }
                            } catch (_) {}
                        });
                    } else if (bucket === 'remote') {
                        const openRemoteItem = () => {
                            if (item.__fromGlobal) {
                                if (item.type === 'dir') remoteNavigateToPath(item.__relPath || '', { pushHistory: true });
                                else openExplorerRemoteFile(item);
                                return;
                            }
                            if (item.type === 'dir') enterRemoteFolder(item.name);
                            else openExplorerRemoteFile(item);
                        };
                        tile.addEventListener('dblclick', (e) => {
                            e.preventDefault();
                            openRemoteItem();
                        });
                        tile.addEventListener('click', (e) => {
                            if (e.detail > 1) return;
                            try {
                                tile.focus({ preventScroll: true });
                            } catch (_) {
                                try { tile.focus(); } catch (_) {}
                            }
                            try {
                                if (window.matchMedia('(pointer: coarse)').matches) {
                                    e.preventDefault();
                                    openRemoteItem();
                                    return;
                                }
                            } catch (_) {}
                            selectedNames.clear();
                            selectedNames.add(item.name);
                            lastSelIdx = idx;
                            keyboardSelAnchorIdx = idx;
                            updateSelectionStyles();
                            try { void updateExplorerPreviewPane(); } catch (_) {}
                        });
                    }

                    const openTileContextMenu = (e) => {
                        e.preventDefault();
                        e.stopPropagation();

                        const targetIdx = parseInt(tile.dataset.idx, 10);
                        const targetItem = sortedDisplay[targetIdx];
                        if (!targetItem) return;
                        if (isLocalLibraryBucket()) {
                            // UX: clic derecho cambia selección al elemento objetivo cuando no está en selección múltiple.
                            const inMultiMode = !!(feToolbarMultiSelectActive || feTouchMultiSelectActive);
                            if (!inMultiMode && (!selectedNames.has(targetItem.name) || selectedNames.size <= 1)) {
                                selectedNames.clear();
                                selectedNames.add(targetItem.name);
                                lastSelIdx = targetIdx;
                                updateSelectionStyles();
                            }
                        } else if (bucket === 'remote') {
                            const inMultiMode = !!(feToolbarMultiSelectActive || feTouchMultiSelectActive);
                            if (!inMultiMode && (!selectedNames.has(targetItem.name) || selectedNames.size <= 1)) {
                                selectedNames.clear();
                                selectedNames.add(targetItem.name);
                                lastSelIdx = targetIdx;
                                updateSelectionStyles();
                            }
                            try { void updateExplorerPreviewPane(); } catch (_) {}
                        }
                        const rowPath = (it) => {
                            if (!it) return bucket === 'remote' ? (remoteRelPath || '') : relPath;
                            if (it.__fromGlobal) return it.__parentPath ?? '';
                            return bucket === 'remote' ? (remoteRelPath || '') : relPath;
                        };
                        const lb = localListBucketName();

                        const commonOpen = { className: 'open', icon: 'fas fa-external-link-alt', label: tr('open'), action: () => {
                            if (bucket === 'trash') {
                                if (targetItem.type === 'dir') {
                                    if (!feTrashCtx && targetItem.id && targetItem.name) {
                                        feTrashCtx = { id: targetItem.id, name: targetItem.name };
                                        feTrashRelPath = '';
                                        selectedNames.clear();
                                        void renderTrashView();
                                    } else if (feTrashCtx && targetItem.name) {
                                        feTrashRelPath = normalizeJoin(feTrashRelPath || '', targetItem.name);
                                        selectedNames.clear();
                                        void renderTrashView();
                                    }
                                } else if (targetItem.url) {
                                    openExplorerFile(targetItem, (feTrashCtx ? ('trash:' + (feTrashCtx.id || '') + ':' + (feTrashRelPath || '')) : 'trash'));
                                }
                            } else if (isLocalLibraryBucket() && targetItem.type === 'dir') {
                                if (targetItem.__sharedShortcut) openSharedSpaceShortcut(targetItem);
                                else if (targetItem.__fromGlobal) navigateToPath(targetItem.__relPath || '');
                                else enterFolder(targetItem.name);
                            } else if (isLocalLibraryBucket()) {
                                openExplorerFile(targetItem, rowPath(targetItem));
                            } else if (bucket === 'remote' && targetItem.type === 'dir') {
                                if (targetItem.__fromGlobal) remoteNavigateToPath(targetItem.__relPath || '', { pushHistory: true });
                                else enterRemoteFolder(targetItem.name);
                            } else if (bucket === 'remote') {
                                openExplorerRemoteFile(targetItem);
                            }
                        }};

                        if (isLocalLibraryBucket()) {
                            if (targetItem.__sharedShortcut) {
                                showContextMenu([
                                    { className: 'open', icon: 'fas fa-external-link-alt', label: tr('open'), action: () => openSharedSpaceShortcut(targetItem) }
                                ], e);
                                return;
                            }
                            if (targetItem.type === 'dir') {
                                const openAct = targetItem.__fromGlobal
                                    ? () => navigateToPath(targetItem.__relPath || '')
                                    : () => enterFolder(targetItem.name);
                                showContextMenu(buildLocalExplorerDirContextMenuOptions(rowPath(targetItem), targetItem.name, openAct), e);
                                return;
                            }
                            const newFolderParent = rowPath(targetItem);
                            async function feEnsureShortcutAppInGrid() {
                                const draft = feBuildShortcutAppModel(targetItem, rowPath(targetItem), lb);
                                const key = getAppLinkKey(draft);
                                const existingIdx = apps.findIndex((a) => getAppLinkKey(a) === key);
                                if (existingIdx >= 0) return { app: apps[existingIdx], index: existingIdx, created: false };
                                apps.push(draft);
                                if (window.GridLauncher && window.GridLauncher.layoutModel && launcherLayout) {
                                    launcherLayout = GridLauncher.layoutModel.appendAppToOrder(launcherLayout, getAppStableId(draft), getAppStableId);
                                }
                                await saveSettings({ apps: apps, launcherLayout: launcherLayout });
                                renderApps();
                                if (windowsMenuVisible) renderWindowsMenu(apps);
                                return { app: draft, index: apps.length - 1, created: true };
                            }
                            const explorerShortcutMenuOpts = [
                                'separator',
                                { className: 'explorer-shortcut-pin', icon: 'fas fa-thumbtack', label: tr('explorerShortcutPinTaskbar'), action: () => {
                                    void feEnsureShortcutAppInGrid().then((r) => pinApp(r.app));
                                }},
                                { className: 'explorer-shortcut-desktop', icon: 'fas fa-desktop', label: tr('explorerShortcutAddDesktop'), action: () => {
                                    void feEnsureShortcutAppInGrid().then((r) => {
                                        addToDesktop(r.app);
                                    });
                                }},
                                { className: 'explorer-shortcut-start', icon: 'fas fa-th-large', label: tr('explorerShortcutAddStartMenu'), action: () => {
                                    void feEnsureShortcutAppInGrid().then((r) => {
                                        showToast(r.created ? tr('explorerShortcutAddedStartMenu') : tr('explorerShortcutAlreadyInApps'));
                                    });
                                }}
                            ];
                            const wallpaperKind = getExplorerWallpaperMediaKind(targetItem);
                            const wallpaperMenuOpts = wallpaperKind ? [
                                'separator',
                                {
                                    className: 'wallpaper-apply',
                                    icon: wallpaperKind === 'video' ? 'fas fa-film' : 'fas fa-image',
                                    label: wallpaperKind === 'video' ? tr('wallpaperApplyDesktop') : tr('wallpaperSetFixed'),
                                    action: () => { void applyExplorerWallpaper(targetItem); }
                                },
                                {
                                    className: 'wallpaper-add-list',
                                    icon: 'fas fa-images',
                                    label: wallpaperKind === 'video' ? tr('wallpaperAddToListVideos') : tr('wallpaperAddToListImages'),
                                    action: () => { void addExplorerWallpaperToList(targetItem); }
                                }
                            ] : [];
                            const options = [
                                commonOpen,
                                ...wallpaperMenuOpts,
                                ...(window.GridExplorerCustomization ? [{
                                    className: 'fe-cust-file-personalize',
                                    icon: 'fas fa-icons',
                                    label: 'Personalizar icono…',
                                    action: () => {
                                        const b = window.GridExplorerCustomization.normBucket(lb);
                                        const relK = window.GridExplorerCustomization.fileRelKey(b, rowPath(targetItem), targetItem.name, targetItem);
                                        window.GridExplorerCustomization.fetchCustomization(false).then(() => {
                                            window.GridExplorerCustomization.openModal({
                                                mode: 'file',
                                                bucket: b,
                                                relKey: relK,
                                                relParent: rowPath(targetItem),
                                                fileName: targetItem.name,
                                                item: targetItem,
                                                title: targetItem.name,
                                                onSaved: () => {
                                                    renderItems();
                                                    try { window.GridExplorerCustomization.applyExplorerChromeToHost(host); } catch (_) {}
                                                }
                                            });
                                        }).catch(() => {});
                                    }
                                }] : []),
                                { className: 'share-secure', icon: 'fas fa-share-alt', label: tr('explorerShareSecure'), action: async () => {
                                    const r = await explorerShareCreate(lb, rowPath(targetItem), targetItem.name, 'file');
                                    if (r && r.success && r.url) {
                                        await shareSecureUrl(r.url, tr('explorerShareSecure'), true);
                                    } else {
                                        await appDialog.alert(tr('explorerShareFailed'));
                                    }
                                }},
                                ...(bucket === 'files' ? [{ className: 'explorer-new-folder-ctx', icon: 'fas fa-folder-plus', label: tr('explorerNewFolder'), action: () => { void promptAndCreateLocalFolder(newFolderParent); } }] : []),
                                { className: 'clip-copy', icon: 'fas fa-copy', label: tr('explorerClipCopy'), action: () => {
                                    const names = selectedNames.size ? Array.from(selectedNames) : [targetItem.name];
                                    const c = getFileClipboard();
                                    c.items = names.map((n) => {
                                        const row = sortedDisplay.find((x) => x.name === n) || targetItem;
                                        return { fromPath: rowPath(row), name: n, type: row && row.type ? row.type : 'file', sourceBucket: lb };
                                    });
                                    c.action = 'copy';
                                    renderItems();
                                    showToast(tr('explorerClipCopy'));
                                }},
                                { className: 'clip-cut', icon: 'fas fa-cut', label: tr('explorerClipCut'), action: () => {
                                    const names = selectedNames.size ? Array.from(selectedNames) : [targetItem.name];
                                    const c = getFileClipboard();
                                    c.items = names.map((n) => {
                                        const row = sortedDisplay.find((x) => x.name === n) || targetItem;
                                        return { fromPath: rowPath(row), name: n, type: row && row.type ? row.type : 'file', sourceBucket: lb };
                                    });
                                    c.action = 'cut';
                                    renderItems();
                                    showToast(tr('explorerClipCut'));
                                }},
                                ...(fileExplorerClipboardHasPasteableItems() ? [{ className: 'clip-paste', icon: 'fas fa-paste', label: tr('explorerPaste'), action: () => { pasteExplorerClipboard(getPasteDestinationForTarget(targetItem)); }}] : []),
                                ...explorerShortcutMenuOpts,
                                { className: 'rename', icon: 'fas fa-edit', label: tr('explorerRename'), action: async () => {
                                    const newName = await showExplorerRenameModal(targetItem.name, { isDirectory: false });
                                    if (!newName || newName === targetItem.name) return;
                                    const rp = rowPath(targetItem);
                                    explorerRename(rp, targetItem.name, newName, lb).then((r) => {
                                        if (!r || !r.success) return;
                                        logActivity('explorer_rename', { path: rp, from: targetItem.name, to: newName });
                                        feSyncSelectionAfterItemRename(targetItem.name, newName);
                                        fePostRenderFocusItemName = newName;
                                        renderFilesView();
                                        if (windowsMenuVisible) renderWindowsMenu(apps);
                                    });
                                }},
                                { className: 'copy', icon: 'fas fa-clone', label: tr('explorerCopy'), action: async () => {
                                    const newName = await appDialog.prompt(tr('explorerPromptDestinationName'), targetItem.name);
                                    if (!newName || newName === targetItem.name) return;
                                    const rp = rowPath(targetItem);
                                    const kind = 'file';
                                    const conflictCtx = { applyMode: null, cache: new Map() };
                                    const resolved = await explorerResolveLocalConflict(lb, rp, newName, kind, conflictCtx);
                                    if (resolved.action === 'cancel' || resolved.action === 'skip') return;
                                    const r = await explorerCopy(rp, targetItem.name, resolved.name, lb, { overwrite: resolved.action === 'replace' });
                                    if (r && r.success) logActivity('explorer_copy', { path: rp, from: targetItem.name, to: resolved.name });
                                    renderFilesView();
                                }},
                                { className: 'delete', icon: 'fas fa-trash', label: tr('explorerDelete'), action: async () => {
                                    const disp = itemsEl && itemsEl._feDisplayItems ? itemsEl._feDisplayItems : items;
                                    const rows = feResolveLocalDeleteRows(disp, targetItem.name);
                                    await feDeleteLocalRowsToTrash(rows, lb);
                                }}
                            ];
                            showContextMenu(options, e);
                        } else if (bucket === 'remote') {
                            const options = [
                                commonOpen,
                                ...(targetItem.type === 'dir' ? [{ className: 'fe-folder-info', icon: 'fas fa-info-circle', label: tr('explorerProperties'), action: () => { void openExplorerRemoteFolderInfoFromPath(rowPath(targetItem), targetItem.name); } }] : []),
                                { className: 'explorer-new-folder-ctx', icon: 'fas fa-folder-plus', label: tr('explorerNewFolder'), action: () => {
                                    const parent = targetItem.type === 'dir'
                                        ? normalizeJoin(remoteRelPath || '', targetItem.name)
                                        : (remoteRelPath || '');
                                    void promptAndCreateRemoteFolder(parent);
                                }},
                                { className: 'clip-copy', icon: 'fas fa-copy', label: tr('explorerClipCopy'), action: () => {
                                    if (!remoteCtx?.id) return;
                                    const c = getFileClipboard();
                                    c.items = [{ source: 'remote', connId: remoteCtx.id, fromPath: remoteRelPath || '', name: targetItem.name, type: targetItem.type || 'file' }];
                                    c.action = 'copy';
                                    showToast(tr('explorerClipCopy'));
                                }},
                                { className: 'clip-cut', icon: 'fas fa-cut', label: tr('explorerClipCut'), action: () => {
                                    if (!remoteCtx?.id) return;
                                    const c = getFileClipboard();
                                    c.items = [{ source: 'remote', connId: remoteCtx.id, fromPath: remoteRelPath || '', name: targetItem.name, type: targetItem.type || 'file' }];
                                    c.action = 'cut';
                                    showToast(tr('explorerClipCut'));
                                }},
                                ...(fileExplorerClipboardHasPasteableItems() ? [{ className: 'clip-paste', icon: 'fas fa-paste', label: tr('explorerPaste'), action: () => { pasteExplorerClipboard(getPasteDestinationForTarget(targetItem)); }}] : []),
                                { className: 'rename', icon: 'fas fa-edit', label: tr('explorerRename'), action: async () => {
                                    const newName = await showExplorerRenameModal(targetItem.name, { isDirectory: targetItem.type === 'dir' });
                                    if (!newName || newName === targetItem.name) return;
                                    if (!remoteCtx?.id) return;
                                    const parentRp = rowPath(targetItem);
                                    explorerConnRename(remoteCtx.id, parentRp, targetItem.name, newName).then((r) => {
                                        if (!r || !r.success) return;
                                        logActivity('explorer_remote_rename', { connId: remoteCtx.id, path: parentRp, from: targetItem.name, to: newName });
                                        if (targetItem.type === 'dir') remapRemotePathsAfterDirRename(parentRp, targetItem.name, newName);
                                        persistActiveRemoteTabState();
                                        feSyncSelectionAfterItemRename(targetItem.name, newName);
                                        fePostRenderFocusItemName = newName;
                                        void renderRemoteView();
                                    });
                                }},
                                { className: 'delete', icon: 'fas fa-trash', label: tr('explorerDelete'), action: async () => {
                                    if (!remoteCtx?.id) return;
                                    const disp = itemsEl && itemsEl._feDisplayItems ? itemsEl._feDisplayItems : items;
                                    const rows = feResolveRemoteDeleteRows(disp, targetItem.name);
                                    await feDeleteRemoteRows(rows, remoteCtx.id, { permanent: false });
                                }}
                            ];
                            showContextMenu(options, e);
                        } else {
                            const options = [
                                commonOpen,
                                { className: 'restore', icon: 'fas fa-undo', label: tr('explorerRestore'), action: () => {
                                    if (targetItem.id) {
                                        doRestoreTrashItem(targetItem.id);
                                    } else if (feTrashCtx && targetItem.name) {
                                        void doRestoreTrashPath(targetItem.name, targetItem.type);
                                    }
                                }},
                                { className: 'delete-perm', icon: 'fas fa-trash-alt', label: tr('explorerDeletePerm'), action: () => {
                                    if (targetItem.id) doDeleteTrashItem(targetItem.id);
                                }}
                            ];
                            showContextMenu(options, e);
                        }
                    };

                    tile.addEventListener('contextmenu', openTileContextMenu, true);
                    const openMenuFromButton = (e) => {
                        e.preventDefault();
                        e.stopPropagation();
                        const rect = menuBtn.getBoundingClientRect();
                        const evt = new MouseEvent('contextmenu', {
                            bubbles: true,
                            cancelable: true,
                            view: window,
                            clientX: Math.round(rect.left + (rect.width / 2)),
                            clientY: Math.round(rect.bottom + 2)
                        });
                        tile.dispatchEvent(evt);
                    };
                    menuBtn.addEventListener('click', openMenuFromButton);
                    menuBtn.addEventListener('touchend', openMenuFromButton, { passive: false });

                    itemsEl.appendChild(tile);
                });
                itemsEl._feDisplayItems = sortedDisplay;
                if (isLocalLibraryBucket()) updateSelectionStyles();
                else {
                    try { void refreshExplorerPropertiesPanel(); } catch (_) {}
                    try { void updateExplorerPreviewPane(); } catch (_) {}
                }
                try { syncExplorerItemsSplitAdaptive(); } catch (_) {}
                scheduleExplorerKeyboardFocus();
            }

            async function doRestoreTrashItem(id) {
                const entry = (fileExplorerTrash || []).find(t => t.id === id);
                const conflictCtx = { applyMode: null };
                while (true) {
                    const r = await explorerRestoreTrashItem(id, conflictCtx.applyMode || {});
                    if (r && r.success) {
                        const restoredName = getExplorerTrashRestoredName(entry, conflictCtx.applyMode || {});
                        logActivity('explorer_restore_trash', { id, name: restoredName || id });
                        await fetchExplorerListTrash();
                        try { feTreeHasChildrenCache.clear(); } catch (_) {}
                        await refreshSidebarTree();
                        feTrashCtx = null;
                        feTrashRelPath = '';
                        renderTrashView();
                        await notifyMediaWidgetTrashRestore(entry, conflictCtx.applyMode || {});
                        renderTrashItems();
                        updateRecycleBinIcon();
                        return;
                    }
                    if (r && r.conflict) {
                        const kind = r.kind || (entry?.kind || 'file');
                        const conflictName = r.name || entry?.name || id;
                        const choice = await showExplorerConflictModal(conflictName, kind);
                        if (!choice || choice.action === 'cancel') return;
                        if (choice.action === 'skip') return;
                        if (choice.action === 'replace') {
                            if (choice.applyAll) conflictCtx.applyMode = { overwrite: true };
                            else conflictCtx.applyMode = { overwrite: true };
                            continue;
                        }
                        if (choice.action === 'merge') {
                            if (choice.applyAll) conflictCtx.applyMode = { merge: true };
                            else conflictCtx.applyMode = { merge: true };
                            continue;
                        }
                        if (choice.action === 'rename') {
                            const nn = choice.newName;
                            if (!nn) return;
                            conflictCtx.applyMode = { newName: nn };
                            continue;
                        }
                        return;
                    }
                    await appDialog.alert(r?.error || tr('explorerRestoreFailed'));
                    return;
                }
            }

            async function doRestoreTrashPath(childName, childKind) {
                if (!feTrashCtx || !feTrashCtx.id || !feTrashCtx.name) return;
                const conflictCtx = { opts: {} };
                while (true) {
                    const r = await explorerRestoreTrashPath(feTrashCtx.id, feTrashCtx.name, feTrashRelPath || '', childName, conflictCtx.opts);
                    if (r && r.success) {
                        logActivity('explorer_restore_trash_path', { id: feTrashCtx.id, root: feTrashCtx.name, path: feTrashRelPath || '', name: childName });
                        await fetchExplorerListTrash().catch(() => {});
                        try { feTreeHasChildrenCache.clear(); } catch (_) {}
                        await refreshSidebarTree();
                        // Mantener navegación actual en papelera; refrescar listado.
                        await renderTrashView();
                        renderTrashItems();
                        updateRecycleBinIcon();
                        return;
                    }
                    if (r && r.conflict) {
                        const kind = r.kind || childKind || 'file';
                        const conflictName = r.name || childName;
                        const choice = await showExplorerConflictModal(conflictName, kind);
                        if (!choice || choice.action === 'cancel') return;
                        if (choice.action === 'skip') return;
                        if (choice.action === 'replace') { conflictCtx.opts = { overwrite: true }; continue; }
                        if (choice.action === 'merge') { conflictCtx.opts = { merge: true }; continue; }
                        if (choice.action === 'rename') { conflictCtx.opts = { newName: choice.newName || '' }; continue; }
                        return;
                    }
                    await appDialog.alert(r?.error || tr('explorerRestoreFailed'));
                    return;
                }
            }

            async function doDeleteTrashItem(id) {
                const r = await explorerDeleteTrashItem(id);
                if (r && r.success) {
                    const deletedName = (fileExplorerTrash || []).find(t => t.id === id)?.name || id;
                    logActivity('explorer_delete_trash_perm', { id, name: deletedName });
                    await fetchExplorerListTrash();
                    renderTrashView();
                    renderTrashItems();
                    updateRecycleBinIcon();
                }
            }

            const explorerUpBtn = host.querySelector('.explorer-up-btn');
            const explorerNewFolderBtn = host.querySelector('.explorer-newfolder-btn');
            const explorerUploadBtn = host.querySelector('.explorer-upload-btn');
            const explorerRefreshBtn = host.querySelector('.explorer-refresh-btn');
            const explorerTrashToggleBtn = host.querySelector('.explorer-trash-toggle-btn');

            itemsEl.addEventListener('dragstart', (e) => {
                const tile = e.target.closest('.explorer-item');
                if (!tile || !isLocalLibraryBucket()) return;
                const nm = tile.dataset.name;
                const selectedFromDom = Array.from(itemsEl.querySelectorAll('.explorer-item.explorer-item-selected'))
                    .map((el) => el.dataset.name)
                    .filter(Boolean);
                const selectedSet = new Set(selectedFromDom.length ? selectedFromDom : Array.from(selectedNames));
                const names = (selectedSet.size && selectedSet.has(nm))
                    ? Array.from(selectedSet)
                    : [nm];
                const mode = e.ctrlKey ? 'copy' : 'move';
                const payload = { fromPath: relPath, name: nm, names, mode, fromBucket: localListBucketName(), fromDepartmentId: bucket === 'department' ? activeDepartmentId() : '' };
                const primaryRow = (items || []).find((x) => x.name === nm);
                if (primaryRow && primaryRow.type === 'dir' && names.length === 1) payload.kind = 'dir';
                explorerDragPayload = payload;
                e.dataTransfer.effectAllowed = mode === 'copy' ? 'copy' : 'move';
                e.dataTransfer.setData('text/plain', names.join(','));
                if (names.length > 1 && e.dataTransfer && typeof e.dataTransfer.setDragImage === 'function') {
                    try {
                        const displayItems = itemsEl._feDisplayItems || items || [];
                        const preview = buildExplorerMultiDragPreview(names, displayItems);
                        document.body.appendChild(preview);
                        e.dataTransfer.setDragImage(preview, 18, 18);
                        setTimeout(() => {
                            try { preview.remove(); } catch (_) {}
                        }, 0);
                    } catch (_) {}
                }
                itemsEl.querySelectorAll('.explorer-item').forEach((el) => {
                    if (names.includes(el.dataset.name)) el.classList.add('explorer-dragging');
                });
            });
            itemsEl.addEventListener('dragend', (e) => {
                const tile = e.target.closest('.explorer-item');
                if (tile) tile.classList.remove('explorer-dragging');
                itemsEl.querySelectorAll('.explorer-item.explorer-dragging').forEach(el => el.classList.remove('explorer-dragging'));
                itemsEl.querySelectorAll('.explorer-drop-target').forEach(el => el.classList.remove('explorer-drop-target'));
                if (feTree) feTree.querySelectorAll('.explorer-drop-target').forEach(el => el.classList.remove('explorer-drop-target'));
                if (pathEl) pathEl.classList.remove('explorer-drop-parent');
                if (feBC) feBC.classList.remove('explorer-drop-parent');
                if (explorerUpBtn) explorerUpBtn.classList.remove('explorer-drop-parent');
                explorerDragPayload = null;
            });

            function bindExplorerDropToParent(el) {
                if (!el) return;
                el.addEventListener('dragover', (e) => {
                    if (!isLocalLibraryBucket() || !explorerDragPayload || !relPath) return;
                    e.preventDefault();
                    e.dataTransfer.dropEffect = 'move';
                    el.classList.add('explorer-drop-parent');
                });
                el.addEventListener('dragleave', () => el.classList.remove('explorer-drop-parent'));
                el.addEventListener('drop', async (e) => {
                    e.preventDefault();
                    el.classList.remove('explorer-drop-parent');
                        if (!isLocalLibraryBucket() || !relPath || !explorerDragPayload) return;
                    const destPath = relPath.split('/').slice(0, -1).join('/');
                    const names = explorerDragPayload.names && explorerDragPayload.names.length
                        ? explorerDragPayload.names
                        : [explorerDragPayload.name];
                    const isCopy = explorerDragPayload.mode === 'copy';
                    await executeExplorerDropToPath(destPath, names, isCopy);
                });
            }
            bindExplorerDropToParent(explorerUpBtn);
            bindExplorerDropToParent(pathEl);
            bindExplorerDropToParent(feBC);
            if (pathEl) pathEl.title = tr('explorerDropToMove');
            if (feBC) feBC.title = tr('explorerDropToMove');
            if (explorerUpBtn) explorerUpBtn.title = (tr('explorerUp') + ' — ' + tr('explorerDropToMove'));

            itemsEl.addEventListener('click', (e) => {
                if (!isLocalLibraryBucket()) return;
                if (feExplorerSkipNextClick) {
                    feExplorerSkipNextClick = false;
                    e.preventDefault();
                    e.stopPropagation();
                    e.stopImmediatePropagation();
                    return;
                }
                if (itemsEl && typeof itemsEl.focus === 'function') itemsEl.focus();
                if (e.target === itemsEl) {
                    selectedNames.clear();
                    itemsEl.querySelectorAll('.explorer-item').forEach(el => el.classList.remove('explorer-item-selected'));
                    feTouchMultiSelectActive = false;
                }
            }, true);

            // Tablet + Pencil/stylus: permitir selección por rectángulo como en PC usando Pointer Events
            itemsEl.addEventListener('pointerdown', (e) => {
                if (!isLocalLibraryBucket()) return;
                if (!e || e.pointerType !== 'pen') return;
                if (e.button !== 0) return;
                if (e.target && e.target.closest && e.target.closest('.explorer-item')) return;
                // iniciar marquee desde fondo
                e.preventDefault();
                e.stopPropagation();
                const startClientX = e.clientX;
                const startClientY = e.clientY;
                const baseSelection = new Set(selectedNames);
                const additive = false;
                const box = document.createElement('div');
                box.className = 'fe-marquee';
                box.style.left = '0px';
                box.style.top = '0px';
                box.style.width = '0px';
                box.style.height = '0px';
                itemsEl.appendChild(box);
                marqueeState = { startClientX, startClientY, box, additive, baseSelection, moved: false, pointerId: e.pointerId };
                try { itemsEl.setPointerCapture(e.pointerId); } catch (_) {}

                const applyMarqueeSelection = (ev) => {
                    const x1 = Math.min(startClientX, ev.clientX);
                    const y1 = Math.min(startClientY, ev.clientY);
                    const x2 = Math.max(startClientX, ev.clientX);
                    const y2 = Math.max(startClientY, ev.clientY);
                    const ir = itemsEl.getBoundingClientRect();
                    box.style.left = (x1 - ir.left + itemsEl.scrollLeft) + 'px';
                    box.style.top = (y1 - ir.top + itemsEl.scrollTop) + 'px';
                    box.style.width = (x2 - x1) + 'px';
                    box.style.height = (y2 - y1) + 'px';
                    const next = additive ? new Set(baseSelection) : new Set();
                    itemsEl.querySelectorAll('.explorer-item').forEach((tile) => {
                        const r = tile.getBoundingClientRect();
                        const hit = !(r.right < x1 || r.left > x2 || r.bottom < y1 || r.top > y2);
                        if (hit) next.add(tile.dataset.name || '');
                    });
                    selectedNames.clear();
                    next.forEach((n) => { if (n) selectedNames.add(n); });
                    itemsEl.querySelectorAll('.explorer-item').forEach((el) => {
                        el.classList.toggle('explorer-item-selected', selectedNames.has(el.dataset.name));
                    });
                };

                const onMove = (ev) => {
                    if (!marqueeState || !marqueeState.box) return;
                    if (ev.pointerId !== marqueeState.pointerId) return;
                    const dragW = Math.abs(ev.clientX - startClientX);
                    const dragH = Math.abs(ev.clientY - startClientY);
                    if (dragW <= 3 && dragH <= 3) return;
                    marqueeState.moved = true;
                    applyMarqueeSelection(ev);
                };

                const onUpOrCancel = (ev) => {
                    if (!marqueeState) return;
                    if (ev.pointerId !== marqueeState.pointerId) return;
                    itemsEl.removeEventListener('pointermove', onMove, true);
                    itemsEl.removeEventListener('pointerup', onUpOrCancel, true);
                    itemsEl.removeEventListener('pointercancel', onUpOrCancel, true);
                    try { itemsEl.releasePointerCapture(ev.pointerId); } catch (_) {}
                    const dragW = Math.abs(ev.clientX - startClientX);
                    const dragH = Math.abs(ev.clientY - startClientY);
                    const didMarquee = dragW > 3 || dragH > 3;
                    const ms = marqueeState;
                    if (ms && ms.box && ms.box.parentNode) ms.box.parentNode.removeChild(ms.box);
                    marqueeState = null;
                    if (didMarquee) {
                        feExplorerSkipNextClick = true;
                        const disp = itemsEl._feDisplayItems || items;
                        if (disp && disp.length && selectedNames.size > 0) {
                            const fi = disp.findIndex((it) => it && selectedNames.has(it.name));
                            if (fi >= 0) {
                                lastSelIdx = fi;
                                keyboardSelAnchorIdx = fi;
                            }
                        }
                    }
                    if (itemsEl && typeof itemsEl.focus === 'function') itemsEl.focus();
                };

                itemsEl.addEventListener('pointermove', onMove, true);
                itemsEl.addEventListener('pointerup', onUpOrCancel, true);
                itemsEl.addEventListener('pointercancel', onUpOrCancel, true);
            }, true);

            itemsEl.addEventListener('mousedown', (e) => {
                if (!isLocalLibraryBucket()) return;
                if (e.button !== 0) return;
                if (e.target.closest('.explorer-item')) return;
                e.preventDefault();
                const startClientX = e.clientX;
                const startClientY = e.clientY;
                const baseSelection = new Set(selectedNames);
                const additive = !!(e.ctrlKey || e.metaKey);
                const box = document.createElement('div');
                box.className = 'fe-marquee';
                box.style.left = '0px';
                box.style.top = '0px';
                box.style.width = '0px';
                box.style.height = '0px';
                itemsEl.appendChild(box);
                marqueeState = { startClientX, startClientY, box, additive, baseSelection, moved: false };
                const applyMarqueeSelection = (ev) => {
                    const x1 = Math.min(startClientX, ev.clientX);
                    const y1 = Math.min(startClientY, ev.clientY);
                    const x2 = Math.max(startClientX, ev.clientX);
                    const y2 = Math.max(startClientY, ev.clientY);
                    const ir = itemsEl.getBoundingClientRect();
                    box.style.left = (x1 - ir.left + itemsEl.scrollLeft) + 'px';
                    box.style.top = (y1 - ir.top + itemsEl.scrollTop) + 'px';
                    box.style.width = (x2 - x1) + 'px';
                    box.style.height = (y2 - y1) + 'px';
                    const next = additive ? new Set(baseSelection) : new Set();
                    itemsEl.querySelectorAll('.explorer-item').forEach((tile) => {
                        const r = tile.getBoundingClientRect();
                        const hit = !(r.right < x1 || r.left > x2 || r.bottom < y1 || r.top > y2);
                        if (hit) next.add(tile.dataset.name || '');
                    });
                    selectedNames.clear();
                    next.forEach((n) => { if (n) selectedNames.add(n); });
                    itemsEl.querySelectorAll('.explorer-item').forEach((el) => {
                        el.classList.toggle('explorer-item-selected', selectedNames.has(el.dataset.name));
                    });
                };
                const onMove = (ev) => {
                    if (!marqueeState || !marqueeState.box) return;
                    const dragW = Math.abs(ev.clientX - startClientX);
                    const dragH = Math.abs(ev.clientY - startClientY);
                    if (dragW <= 3 && dragH <= 3) return;
                    marqueeState.moved = true;
                    applyMarqueeSelection(ev);
                };
                const onUp = (ev) => {
                    document.removeEventListener('mousemove', onMove, true);
                    document.removeEventListener('mouseup', onUp, true);
                    const dragW = Math.abs(ev.clientX - startClientX);
                    const dragH = Math.abs(ev.clientY - startClientY);
                    const didMarquee = dragW > 3 || dragH > 3;
                    const ms = marqueeState;
                    if (ms && ms.box && ms.box.parentNode) ms.box.parentNode.removeChild(ms.box);
                    marqueeState = null;
                    if (didMarquee) {
                        feExplorerSkipNextClick = true;
                        const disp = itemsEl._feDisplayItems || items;
                        if (disp && disp.length && selectedNames.size > 0) {
                            const fi = disp.findIndex((it) => it && selectedNames.has(it.name));
                            if (fi >= 0) {
                                lastSelIdx = fi;
                                keyboardSelAnchorIdx = fi;
                            }
                        }
                    }
                    if (itemsEl && typeof itemsEl.focus === 'function') itemsEl.focus();
                };
                document.addEventListener('mousemove', onMove, true);
                document.addEventListener('mouseup', onUp, true);
            });

            if (feItemsWrap) {
                feItemsWrap.addEventListener('dragover', (e) => {
                    if (bucket !== 'files' && bucket !== 'remote') return;
                    if (bucket === 'remote' && (!remoteCtx || !remoteCtx.id)) return;
                    const ok = e.dataTransfer.types && Array.from(e.dataTransfer.types).includes('Files');
                    if (!ok) return;
                    e.preventDefault();
                    e.dataTransfer.dropEffect = 'copy';
                });
                feItemsWrap.addEventListener('drop', async (e) => {
                    if (bucket !== 'files' && bucket !== 'remote') return;
                    if (!e.dataTransfer.files || e.dataTransfer.files.length === 0) return;
                    if (bucket === 'remote' && (!remoteCtx || !remoteCtx.id)) return;
                    e.preventDefault();
                    setExplorerBusy(true, tr('explorerLoading'));
                    const dropUndoSteps = [];
                    try {
                        const conflictCtx = bucket === 'remote' ? { applyMode: null, cache: new Map() } : null;
                        for (const f of Array.from(e.dataTransfer.files)) {
                            try {
                                if (isLocalLibraryBucket() && bucket !== 'cache') {
                                    const formData = new FormData();
                                    formData.append('path', relPath || '');
                                    formData.append('bucket', localListBucketName());
                                    if (bucket === 'department' && sharedDepartmentId) {
                                        formData.append('departmentId', sharedDepartmentId);
                                    }
                                    formData.append('files[]', f, f.name);
                                    const resp = await fetch('?action=explorerUpload', { method: 'POST', body: formData });
                                    const data = await resp.json();
                                    const r0 = data && data.results && data.results[0];
                                    if (r0 && r0.success && r0.storedAs) {
                                        const sn = r0.storedAs;
                                        const lb = localListBucketName();
                                        dropUndoSteps.push(async () => {
                                            await explorerDelete(relPath || '', sn, lb);
                                        });
                                    }
                                } else if (bucket === 'remote') {
                                    const resolved = await explorerResolveRemoteConflict(remoteCtx.id, remoteRelPath || '', f.name, 'file', conflictCtx);
                                    if (resolved.action === 'cancel') break;
                                    if (resolved.action === 'skip') continue;
                                    const data = await explorerConnUploadFiles(remoteCtx.id, remoteRelPath || '', f, resolved.name);
                                    const r0 = data && data.results && data.results[0];
                                    if (r0 && r0.success && remoteCtx.id) {
                                        const un = r0.name || resolved.name || f.name;
                                        const cid = remoteCtx.id;
                                        const rp = remoteRelPath || '';
                                        dropUndoSteps.push(async () => {
                                            await explorerConnDelete(cid, rp, un, 'file');
                                        });
                                    }
                                }
                            } catch (_) {}
                        }
                    } finally {
                        setExplorerBusy(false);
                    }
                    if (bucket === 'files') await renderFilesView();
                    if (bucket === 'remote') await renderRemoteView();
                    feCommitUndoSequential(dropUndoSteps);
                    showToast(tr('explorerStatusOk'));
                });
            }

            explorerUpBtn.addEventListener('click', () => {
                if (isLocalLibraryBucket()) {
                    goUpFolder();
                } else if (bucket === 'trash') {
                    bucket = lastNonTrashBucket || 'files';
                    if (isLocalLibraryBucket()) renderFilesView();
                    else if (bucket === 'remote') renderRemoteView();
                } else if (bucket === 'remote') {
                    if (!remoteRelPath) return;
                    remoteRelPath = remoteRelPath.split('/').slice(0, -1).join('/');
                    renderRemoteView();
                }
            });

            explorerNewFolderBtn.addEventListener('click', () => {
                if (bucket === 'files') {
                    if (feHomeLanding) feHomeLanding = false;
                    void promptAndCreateLocalFolder(relPath);
                } else if (bucket === 'remote') {
                    void promptAndCreateRemoteFolder(remoteRelPath || '');
                }
            });

            explorerUploadBtn.addEventListener('click', () => {
                if (bucket === 'files') {
                    if (feHomeLanding) feHomeLanding = false;
                    uploadInput.click();
                } else if (bucket === 'remote') {
                    uploadInput.click();
                }
            });

            uploadInput.addEventListener('change', async () => {
                if (bucket !== 'files' && bucket !== 'remote') return;
                const files = Array.from(uploadInput.files || []);
                if (files.length === 0) return;

                uploadListEl.innerHTML = '';
                const conflictCtx = bucket === 'remote' ? { applyMode: null, cache: new Map() } : null;
                const uploadUndoSteps = [];
                let uploadProgressWasOpened = false;
                let uploadProgressCloseFn = null;
                const remoteDestPathSnapshot = bucket === 'remote' ? (remoteRelPath || '') : '';
                const remoteExpectedNames = [];
                for (const f of files) {
                    const row = document.createElement('div');
                    row.className = 'explorer-upload-item';
                    row.innerHTML = `<span>${f.name}</span><span>${tr('explorerLoading')}</span>`;
                    uploadListEl.appendChild(row);

                    try {
                        if (bucket === 'files') {
                            // Modal de progreso con porcentaje real de subida (bytes)
                            const ensureProgressModal = () => {
                                if (document.getElementById('explorerProgressModal')) return;
                                const el = document.createElement('div');
                                el.id = 'explorerProgressModal';
                                el.className = 'explorer-progress-modal';
                                el.style.display = 'none';
                                el.setAttribute('aria-hidden', 'true');
                                el.innerHTML = `
                                    <div class="explorer-progress-card" role="dialog" aria-modal="true" aria-label="Progreso de copia">
                                        <div class="explorer-progress-title" id="explorerProgressTitle">Subiendo…</div>
                                        <div class="explorer-progress-sub" id="explorerProgressSub"></div>
                                        <div class="explorer-progress-bar">
                                            <div class="explorer-progress-bar-fill" id="explorerProgressBarFill" style="width:0%"></div>
                                        </div>
                                        <div class="explorer-progress-meta">
                                            <div id="explorerProgressPct">0%</div>
                                            <div id="explorerProgressCount"></div>
                                        </div>
                                    </div>
                                `;
                                document.body.appendChild(el);
                            };
                            const openP = (title, filename) => {
                                ensureProgressModal();
                                const modal = document.getElementById('explorerProgressModal');
                                if (!modal) return;
                                const t = document.getElementById('explorerProgressTitle');
                                if (t) t.textContent = title || 'Subiendo…';
                                const sub = document.getElementById('explorerProgressSub');
                                if (sub) sub.textContent = filename || '';
                                const fill = document.getElementById('explorerProgressBarFill');
                                if (fill) fill.style.width = '0%';
                                const pctEl = document.getElementById('explorerProgressPct');
                                if (pctEl) pctEl.textContent = '0%';
                                modal.style.display = 'flex';
                                modal.setAttribute('aria-hidden', 'false');
                                document.body.classList.add('modal-open');
                            };
                            const setP = (pct) => {
                                const fill = document.getElementById('explorerProgressBarFill');
                                const pctEl = document.getElementById('explorerProgressPct');
                                const p = Math.max(0, Math.min(100, Math.round(pct || 0)));
                                if (fill) fill.style.width = `${p}%`;
                                if (pctEl) pctEl.textContent = `${p}%`;
                            };
                            const closeP = () => {
                                const modal = document.getElementById('explorerProgressModal');
                                if (!modal) return;
                                modal.style.display = 'none';
                                modal.setAttribute('aria-hidden', 'true');
                                document.body.classList.remove('modal-open');
                            };
                            window.__gridExplorerProgressOnUploadProgress = (st) => {
                                if (st && typeof st.percent === 'number') setP(st.percent);
                            };
                            openP('Subiendo al explorador…', f.name);
                            uploadProgressWasOpened = true;
                            uploadProgressCloseFn = closeP;

                            const data = await new Promise((resolve) => {
                                const formData = new FormData();
                                formData.append('path', relPath || '');
                                formData.append('bucket', localListBucketName());
                                if (bucket === 'department' && sharedDepartmentId) {
                                    formData.append('departmentId', sharedDepartmentId);
                                }
                                formData.append('files[]', f, f.name);
                                const xhr = new XMLHttpRequest();
                                xhr.open('POST', '?action=explorerUpload', true);
                                xhr.responseType = 'json';
                                xhr.upload.onprogress = (e) => {
                                    if (!e || !e.lengthComputable) return;
                                    const pct = e.total > 0 ? Math.max(0, Math.min(100, Math.round((e.loaded / e.total) * 100))) : 0;
                                    try {
                                        if (typeof window.__gridExplorerProgressOnUploadProgress === 'function') {
                                            window.__gridExplorerProgressOnUploadProgress({ loaded: e.loaded, total: e.total, percent: pct });
                                        }
                                    } catch (_) {}
                                };
                                xhr.onload = () => {
                                    try {
                                        const out = xhr.response && typeof xhr.response === 'object' ? xhr.response : JSON.parse(xhr.responseText || '{}');
                                        resolve(out);
                                    } catch (_) {
                                        resolve({ success: false, error: 'Respuesta inválida' });
                                    }
                                };
                                xhr.onerror = () => resolve({ success: false, error: 'Error de red' });
                                xhr.send(formData);
                            });
                            // No cerrar aún: esperar a renderizar la lista local.
                            setP(100);
                            const t = document.getElementById('explorerProgressTitle');
                            if (t) t.textContent = 'Actualizando carpeta…';
                            const sub = document.getElementById('explorerProgressSub');
                            if (sub) sub.textContent = 'Esperando que aparezca el archivo…';

                            const ok = data && data.results && data.results[0] && data.results[0].success;
                            const res0 = data && data.results && data.results[0] ? data.results[0] : null;
                            row.querySelectorAll('span')[1].textContent = ok ? tr('explorerStatusOk') : tr('explorerStatusError');
                            if (ok) logActivity('explorer_upload', { path: relPath, name: f.name, storedAs: res0?.storedAs || f.name });
                            if (ok && res0 && res0.storedAs) {
                                const sn = res0.storedAs;
                                uploadUndoSteps.push(async () => {
                                    await explorerDelete(relPath || '', sn, 'files');
                                });
                            }
                        } else if (bucket === 'remote') {
                            if (!remoteCtx?.id) throw new Error('No connection');
                            // Si hay conflicto de nombre, primero resolverlo (NO mostrar progreso en ese momento).
                            const ensureProgressModal = () => {
                                if (document.getElementById('explorerProgressModal')) return;
                                const el = document.createElement('div');
                                el.id = 'explorerProgressModal';
                                el.className = 'explorer-progress-modal';
                                el.style.display = 'none';
                                el.setAttribute('aria-hidden', 'true');
                                el.innerHTML = `
                                    <div class="explorer-progress-card" role="dialog" aria-modal="true" aria-label="Progreso de copia">
                                        <div class="explorer-progress-title" id="explorerProgressTitle">Subiendo a FTP…</div>
                                        <div class="explorer-progress-sub" id="explorerProgressSub"></div>
                                        <div class="explorer-progress-bar">
                                            <div class="explorer-progress-bar-fill" id="explorerProgressBarFill" style="width:0%"></div>
                                        </div>
                                        <div class="explorer-progress-meta">
                                            <div id="explorerProgressPct">0%</div>
                                            <div id="explorerProgressCount"></div>
                                        </div>
                                    </div>
                                `;
                                document.body.appendChild(el);
                            };
                            const openP = (filename) => {
                                ensureProgressModal();
                                const modal = document.getElementById('explorerProgressModal');
                                if (!modal) return;
                                const t = document.getElementById('explorerProgressTitle');
                                if (t) t.textContent = 'Subiendo a FTP…';
                                const sub = document.getElementById('explorerProgressSub');
                                if (sub) sub.textContent = filename || '';
                                const fill = document.getElementById('explorerProgressBarFill');
                                if (fill) fill.style.width = '0%';
                                const pctEl = document.getElementById('explorerProgressPct');
                                if (pctEl) pctEl.textContent = '0%';
                                modal.style.display = 'flex';
                                modal.setAttribute('aria-hidden', 'false');
                                document.body.classList.add('modal-open');
                            };
                            const setP = (pct) => {
                                const fill = document.getElementById('explorerProgressBarFill');
                                const pctEl = document.getElementById('explorerProgressPct');
                                const p = Math.max(0, Math.min(100, Math.round(pct || 0)));
                                if (fill) fill.style.width = `${p}%`;
                                if (pctEl) pctEl.textContent = `${p}%`;
                            };
                            const closeP = () => {
                                const modal = document.getElementById('explorerProgressModal');
                                if (!modal) return;
                                modal.style.display = 'none';
                                modal.setAttribute('aria-hidden', 'true');
                                document.body.classList.remove('modal-open');
                            };
                            window.__gridExplorerProgressOnUploadProgress = (st) => {
                                if (st && typeof st.percent === 'number') setP(st.percent);
                            };
                            const resolved = await explorerResolveRemoteConflict(remoteCtx.id, remoteRelPath || '', f.name, 'file', conflictCtx);
                            if (resolved.action === 'cancel') { row.querySelectorAll('span')[1].textContent = tr('explorerStatusError'); break; }
                            if (resolved.action === 'skip') { row.querySelectorAll('span')[1].textContent = 'Omitido'; continue; }
                            openP(f.name);
                            uploadProgressWasOpened = true;
                            uploadProgressCloseFn = closeP;
                            const data = await explorerConnUploadFiles(remoteCtx.id, remoteRelPath, f, resolved.name);
                            // No cerrar aún: esperar a refrescar el FTP para ver el archivo.
                            setP(100);
                            const t = document.getElementById('explorerProgressTitle');
                            if (t) t.textContent = 'Actualizando carpeta…';
                            const sub = document.getElementById('explorerProgressSub');
                            if (sub) sub.textContent = 'Esperando que el FTP muestre el archivo…';
                            const ok = data && data.results && data.results[0] && data.results[0].success;
                            row.querySelectorAll('span')[1].textContent = ok ? tr('explorerStatusOk') : tr('explorerStatusError');
                            if (ok) logActivity('explorer_remote_upload', { connId: remoteCtx.id, path: remoteRelPath, name: f.name });
                            if (ok && remoteCtx.id) {
                                const r0 = data.results[0];
                                const un = (r0 && r0.name) ? r0.name : (resolved.name || f.name);
                                remoteExpectedNames.push(un);
                                const cid = remoteCtx.id;
                                const rp = remoteRelPath || '';
                                uploadUndoSteps.push(async () => {
                                    await explorerConnDelete(cid, rp, un, 'file');
                                });
                            }
                        }
                    } catch (_) {
                        row.querySelectorAll('span')[1].textContent = tr('explorerStatusError');
                    }
                }
                if (bucket === 'files') await renderFilesView();
                if (bucket === 'remote') {
                    // En algunos FTP la lista tarda en reflejar. Esperar/pollear hasta ver el archivo, luego render.
                    if (remoteCtx?.id && remoteExpectedNames.length > 0) {
                        await explorerWaitForRemoteItemsVisible(remoteCtx.id, remoteDestPathSnapshot, remoteExpectedNames, 12000);
                    }
                    await renderRemoteView(); // refrescar FTP para visualizar lo subido
                }
                // Cerrar modal SOLO después de renderizar
                if (typeof uploadProgressCloseFn === 'function') uploadProgressCloseFn();
                if (uploadProgressWasOpened) window.__gridExplorerProgressOnUploadProgress = null;
                feCommitUndoSequential(uploadUndoSteps);
                uploadInput.value = '';
            });

            explorerRefreshBtn.addEventListener('click', () => {
                if (isLocalLibraryBucket()) renderFilesView();
                else if (bucket === 'trash') renderTrashView();
                else if (bucket === 'remote') renderRemoteView();
            });

            explorerTrashToggleBtn.addEventListener('click', async () => {
                if (bucket === 'trash') {
                    bucket = lastNonTrashBucket || 'files';
                    if (isLocalLibraryBucket()) renderFilesView();
                    else if (bucket === 'remote') renderRemoteView();
                } else {
                    if (bucket === 'remote') persistActiveRemoteTabState();
                    lastNonTrashBucket = bucket;
                    bucket = 'trash';
                    await renderTrashView();
                }
            });

            function openExplorerItemAtKeyboardIndex(idx) {
                const disp = itemsEl._feDisplayItems || items;
                if (!disp || idx < 0 || idx >= disp.length) return;
                const item = disp[idx];
                if (!item) return;
                if (isLocalLibraryBucket()) {
                    if (item.__fromGlobal) {
                        if (item.type === 'dir') navigateToPath(item.__relPath || '');
                        else openExplorerFile(item, item.__parentPath);
                    } else if (item.type === 'dir') {
                        if (item.__sharedShortcut) openSharedSpaceShortcut(item);
                        else enterFolder(item.name);
                    } else {
                        openExplorerFile(item, relPath);
                    }
                } else if (bucket === 'trash') {
                    if (item.type === 'dir') {
                        // Navegación dentro de papelera (sin restaurar)
                        if (!feTrashCtx && item.id && item.name) {
                            feTrashCtx = { id: item.id, name: item.name };
                            feTrashRelPath = '';
                            selectedNames.clear();
                            void renderTrashView();
                        } else if (feTrashCtx && item.name) {
                            feTrashRelPath = normalizeJoin(feTrashRelPath || '', item.name);
                            selectedNames.clear();
                            void renderTrashView();
                        }
                    } else if (item.url) {
                        // Abrir archivo eliminado (sin restaurar)
                        openExplorerFile(item, (feTrashCtx ? ('trash:' + (feTrashCtx.id || '') + ':' + (feTrashRelPath || '')) : 'trash'));
                    }
                } else if (bucket === 'remote') {
                    if (item.type === 'dir') {
                        remoteRelPath = normalizeJoin(remoteRelPath, item.name);
                        renderRemoteView();
                    } else {
                        openExplorerRemoteFile(item);
                    }
                }
            }

            function explorerHandleBackspaceOrDeleteTreeUp(e) {
                if (e.key !== 'Backspace') return;
                if (e.repeat) return;
                const t = e.target;
                if (feSearchInput && (t === feSearchInput || (feSearchInput.contains && feSearchInput.contains(t)))) return;
                let handled = false;
                if (isLocalLibraryBucket() && relPath) {
                    goUpFolder();
                    handled = true;
                } else if (!feHomeLanding && !relPath && (bucket === 'files' || bucket === 'community' || bucket === 'department')) {
                    navigateToExplorerHome();
                    handled = true;
                } else if (bucket === 'remote' && remoteRelPath) {
                    remoteRelPath = remoteRelPath.split('/').filter(Boolean);
                    remoteRelPath.pop();
                    remoteRelPath = remoteRelPath.join('/');
                    renderRemoteView();
                    handled = true;
                } else if (bucket === 'trash') {
                    if (feTrashCtx && feTrashRelPath) {
                        feTrashRelPath = feTrashRelPath.split('/').filter(Boolean).slice(0, -1).join('/');
                        selectedNames.clear();
                        void renderTrashView();
                        handled = true;
                    } else if (feTrashCtx) {
                        feTrashCtx = null;
                        feTrashRelPath = '';
                        selectedNames.clear();
                        void renderTrashView();
                        handled = true;
                    } else {
                        bucket = lastNonTrashBucket || 'files';
                        if (isLocalLibraryBucket()) renderFilesView();
                        else if (bucket === 'remote') renderRemoteView();
                        handled = true;
                    }
                }
                if (handled) {
                    e.preventDefault();
                    e.stopPropagation();
                }
            }
            host.addEventListener('keydown', explorerHandleBackspaceOrDeleteTreeUp, true);

            host.addEventListener('keydown', async (e) => {
                if (!(e.ctrlKey || e.metaKey) || (e.key !== 'z' && e.key !== 'Z')) return;
                if (e.shiftKey) return;
                if (feExplorerIsTextInputEl(e.target)) return;
                if (feUndoStack.length === 0) return;
                e.preventDefault();
                e.stopPropagation();
                if (!(await appDialog.confirm(tr('explorerUndoConfirm')))) return;
                const entry = feUndoStack.pop();
                setExplorerBusy(true, tr('explorerLoading'));
                try {
                    await entry.run();
                    showToast(tr('explorerUndoDone'));
                    if (isLocalLibraryBucket()) await renderFilesView();
                    else if (bucket === 'remote') await renderRemoteView();
                } catch (_) {
                    showToast(tr('explorerStatusError'));
                } finally {
                    setExplorerBusy(false);
                }
            }, true);

            function explorerHandleClipboardShortcuts(e) {
                if (!(e.ctrlKey || e.metaKey)) return;
                if (feExplorerIsTextInputEl(e.target)) return;
                const t = e.target;
                const inHost = (t && host.contains(t)) || (document.activeElement && host.contains(document.activeElement));
                if (!inHost) return;
                const k = e.key || '';
                const code = e.code || '';
                const isSelectAll = (k === 'a' || k === 'A') || (code === 'KeyA');
                const isCopy = (k === 'c' || k === 'C') || (code === 'KeyC');
                const isCut = (k === 'x' || k === 'X') || (code === 'KeyX');
                const isPaste = (k === 'v' || k === 'V') || (code === 'KeyV');
                if (!isSelectAll && !isCopy && !isCut && !isPaste) return;
                if (bucket === 'trash') return;
                const tiles = Array.from(itemsEl.querySelectorAll('.explorer-item'));
                const disp = itemsEl._feDisplayItems || items;
                const active = document.activeElement;
                let idx = tiles.indexOf(active);
                if (idx < 0) {
                    const firstSelName = selectedNames.size > 0 ? Array.from(selectedNames)[0] : null;
                    const selIdx = firstSelName ? disp.findIndex((x) => x && x.name === firstSelName) : -1;
                    idx = selIdx >= 0 ? selIdx : 0;
                }
                if (isSelectAll) {
                    if (!isLocalLibraryBucket()) return;
                    e.preventDefault();
                    e.stopPropagation();
                    disp.forEach((it) => { if (it && it.name) selectedNames.add(it.name); });
                    itemsEl.querySelectorAll('.explorer-item').forEach((el) => {
                        el.classList.toggle('explorer-item-selected', selectedNames.has(el.dataset.name));
                    });
                    return;
                }
                if (isPaste) {
                    e.preventDefault();
                    e.stopPropagation();
                    pasteExplorerClipboard();
                    return;
                }
                if (isCopy || isCut) {
                    const isCutOp = isCut;
                    if (isLocalLibraryBucket()) {
                        const names = selectedNames.size > 0
                            ? Array.from(selectedNames)
                            : (disp[idx] && disp[idx].name ? [disp[idx].name] : []);
                        if (names.length === 0) return;
                        e.preventDefault();
                        e.stopPropagation();
                        const c = getFileClipboard();
                        const lb = localListBucketName();
                        c.items = names.map((n) => {
                            const row = disp.find((x) => x.name === n);
                            const fp = row && row.__fromGlobal ? row.__parentPath : relPath;
                            return { fromPath: fp, name: n, type: row && row.type ? row.type : 'file', sourceBucket: lb };
                        });
                        c.action = isCutOp ? 'cut' : 'copy';
                        renderItems();
                        showToast(isCutOp ? tr('explorerClipCut') : tr('explorerClipCopy'));
                        return;
                    }
                    if (bucket === 'remote' && remoteCtx && remoteCtx.id) {
                        if (tiles.length === 0) return;
                        const item = disp[idx];
                        if (!item) return;
                        e.preventDefault();
                        e.stopPropagation();
                        const c = getFileClipboard();
                        const fp = item.__fromGlobal ? (item.__parentPath ?? '') : (remoteRelPath || '');
                        c.items = [{ source: 'remote', connId: remoteCtx.id, fromPath: fp, name: item.name, type: item.type || 'file' }];
                        c.action = isCutOp ? 'cut' : 'copy';
                        showToast(isCutOp ? tr('explorerClipCut') : tr('explorerClipCopy'));
                    }
                }
            }
            host.addEventListener('keydown', explorerHandleClipboardShortcuts, true);

            async function explorerHandleDeleteShortcut(e) {
                if (!e || e.key !== 'Delete') return;
                if (e.shiftKey) return;
                if (!isLocalLibraryBucket()) return;
                if (bucket === 'trash') return;
                if (feExplorerIsTextInputEl(e.target)) return;
                const t = e.target;
                const inHost = (t && host.contains(t)) || (document.activeElement && host.contains(document.activeElement));
                if (!inHost) return;

                const disp = (itemsEl && itemsEl._feDisplayItems) ? itemsEl._feDisplayItems : items;
                const tiles = itemsEl ? Array.from(itemsEl.querySelectorAll('.explorer-item')) : [];
                const active = document.activeElement;
                let idx = tiles.indexOf(active);
                if (idx < 0) {
                    const firstSelName = selectedNames.size > 0 ? Array.from(selectedNames)[0] : null;
                    const selIdx = firstSelName ? (disp || []).findIndex((x) => x && x.name === firstSelName) : -1;
                    idx = selIdx >= 0 ? selIdx : 0;
                }
                const fallbackName = (disp && disp[idx] && disp[idx].name) ? disp[idx].name : null;
                const rows = feResolveLocalDeleteRows(disp, fallbackName);
                if (rows.length === 0) return;
                e.preventDefault();
                e.stopPropagation();
                await feDeleteLocalRowsToTrash(rows, localListBucketName());
            }
            host.addEventListener('keydown', explorerHandleDeleteShortcut, true);

            // Navegación teclado tipo galería
            itemsEl.addEventListener('keydown', async (e) => {
                if (feExplorerIsTextInputEl(e.target)) return;
                const tiles = Array.from(itemsEl.querySelectorAll('.explorer-item'));
                if (tiles.length === 0) return;
                const disp = itemsEl._feDisplayItems || items;
                const active = document.activeElement;
                let idx = tiles.indexOf(active);
                if (idx < 0) {
                    const firstSelName = selectedNames.size > 0 ? Array.from(selectedNames)[0] : null;
                    const selIdx = firstSelName ? disp.findIndex((x) => x && x.name === firstSelName) : -1;
                    idx = selIdx >= 0 ? selIdx : 0;
                }
                const idxBeforeMove = idx;

                let cols = 1;
                if (viewMode === 'split_preview_hscroll') {
                    cols = 1;
                } else if (!feListLike(viewMode) && !feDetailsLike(viewMode)) {
                    const first = tiles[0].getBoundingClientRect();
                    cols = Math.max(1, Math.round(itemsEl.clientWidth / Math.max(1, first.width)));
                }
                let movedByArrow = false;

                if (e.key === 'ArrowRight') { idx = Math.min(tiles.length - 1, idx + 1); movedByArrow = true; }
                else if (e.key === 'ArrowLeft') { idx = Math.max(0, idx - 1); movedByArrow = true; }
                else if (e.key === 'ArrowDown') { idx = Math.min(tiles.length - 1, idx + cols); movedByArrow = true; }
                else if (e.key === 'ArrowUp') { idx = Math.max(0, idx - cols); movedByArrow = true; }
                else if (e.key === 'Enter') {
                    e.preventDefault();
                    e.stopPropagation();
                    feResetExplorerTypeAhead();
                    openExplorerItemAtKeyboardIndex(idx);
                    return;
                }
                else if (e.key === 'Escape' && isLocalLibraryBucket() && (feToolbarMultiSelectActive || feTouchMultiSelectActive || selectedNames.size > 1)) {
                    e.preventDefault();
                    e.stopPropagation();
                    feExitMultiSelect({ clearSelection: true });
                    return;
                }
                else if (e.key === 'F2' && (isLocalLibraryBucket() || bucket === 'remote')) {
                    e.preventDefault();
                    e.stopPropagation();
                    const item = disp[idx];
                    if (!item || feIsSharedShortcutItem(item)) return;
                    (async () => {
                        const newName = await showExplorerRenameModal(item.name, { isDirectory: item.type === 'dir' });
                        if (!newName || newName === item.name) return;
                        if (isLocalLibraryBucket()) {
                            const rp = item && item.__fromGlobal ? item.__parentPath : relPath;
                            const lb = localListBucketName();
                            explorerRename(rp, item.name, newName, lb).then((r) => {
                                if (!r || !r.success) return;
                                logActivity('explorer_rename', { path: rp, from: item.name, to: newName });
                                if (item.type === 'dir') remapLocalPathsAfterDirRename(rp, item.name, newName);
                                feSyncSelectionAfterItemRename(item.name, newName);
                                fePostRenderFocusItemName = newName;
                                renderFilesView();
                            });
                        } else if (bucket === 'remote' && remoteCtx?.id) {
                            const parentRp = item && item.__fromGlobal ? (item.__parentPath ?? '') : (remoteRelPath || '');
                            explorerConnRename(remoteCtx.id, parentRp, item.name, newName).then((r) => {
                                if (!r || !r.success) return;
                                logActivity('explorer_remote_rename', { connId: remoteCtx.id, path: parentRp, from: item.name, to: newName });
                                if (item.type === 'dir') remapRemotePathsAfterDirRename(parentRp, item.name, newName);
                                persistActiveRemoteTabState();
                                feSyncSelectionAfterItemRename(item.name, newName);
                                fePostRenderFocusItemName = newName;
                                void renderRemoteView();
                            });
                        }
                    })();
                    return;
                }
                else if (e.key === 'Delete' && isLocalLibraryBucket() && !e.shiftKey) {
                    e.preventDefault();
                    e.stopPropagation();
                    const fallbackName = (disp[idx] && disp[idx].name) ? disp[idx].name : null;
                    const rows = feResolveLocalDeleteRows(disp, fallbackName);
                    if (rows.length === 0) return;
                    await feDeleteLocalRowsToTrash(rows, localListBucketName());
                    return;
                }
                else if (e.key === 'Delete' && isLocalLibraryBucket() && e.shiftKey) {
                    e.preventDefault();
                    e.stopPropagation();
                    const fallbackName = (disp[idx] && disp[idx].name) ? disp[idx].name : null;
                    const rows = feResolveLocalDeleteRows(disp, fallbackName);
                    if (rows.length === 0) return;
                    await feDeleteLocalRowsPermanent(rows, localListBucketName());
                    return;
                }
                else if (e.key === 'Delete' && bucket === 'remote' && remoteCtx?.id) {
                    e.preventDefault();
                    e.stopPropagation();
                    const fallbackName = (disp[idx] && disp[idx].name) ? disp[idx].name : null;
                    const rows = feResolveRemoteDeleteRows(disp, fallbackName);
                    if (rows.length === 0) return;
                    await feDeleteRemoteRows(rows, remoteCtx.id, { permanent: !!e.shiftKey });
                    return;
                }
                else if (feExplorerTypeAheadPrintableKey(e) && (isLocalLibraryBucket() || bucket === 'remote' || bucket === 'trash')) {
                    const ch = e.key;
                    feExplorerTypeAheadBuffer = (feExplorerTypeAheadBuffer + ch).slice(0, 64);
                    if (feExplorerTypeAheadTimer != null) clearTimeout(feExplorerTypeAheadTimer);
                    feExplorerTypeAheadTimer = setTimeout(() => {
                        feExplorerTypeAheadBuffer = '';
                        feExplorerTypeAheadTimer = null;
                    }, FE_EXPLORER_TYPEAHEAD_MS);
                    const j = feFindExplorerTypeAheadIndex(disp, feExplorerTypeAheadBuffer, idx);
                    if (j < 0) {
                        e.preventDefault();
                        e.stopPropagation();
                        return;
                    }
                    e.preventDefault();
                    e.stopPropagation();
                    idx = j;
                    if (isLocalLibraryBucket()) {
                        keyboardSelAnchorIdx = idx;
                        selectedNames.clear();
                        if (disp[idx] && disp[idx].name) selectedNames.add(disp[idx].name);
                        lastSelIdx = idx;
                        itemsEl.querySelectorAll('.explorer-item').forEach((el) => {
                            el.classList.toggle('explorer-item-selected', selectedNames.has(el.dataset.name));
                        });
                    } else if (bucket === 'remote') {
                        selectedNames.clear();
                        if (disp[idx] && disp[idx].name) selectedNames.add(disp[idx].name);
                        lastSelIdx = idx;
                        itemsEl.querySelectorAll('.explorer-item').forEach((el) => {
                            el.classList.toggle('explorer-item-selected', selectedNames.has(el.dataset.name));
                        });
                    }
                    if (isLocalLibraryBucket() || bucket === 'remote') {
                        try { void updateExplorerPreviewPane(); } catch (_) {}
                        try { void refreshExplorerPropertiesPanel(); } catch (_) {}
                    }
                    const nextTa = tiles[idx];
                    if (nextTa) nextTa.focus();
                    return;
                }
                else {
                    return;
                }

                if (movedByArrow) {
                    feResetExplorerTypeAhead();
                    if (isLocalLibraryBucket()) {
                        if (e.shiftKey) {
                            if (keyboardSelAnchorIdx === null) keyboardSelAnchorIdx = idxBeforeMove;
                            const a = Math.min(keyboardSelAnchorIdx, idx);
                            const b = Math.max(keyboardSelAnchorIdx, idx);
                            selectedNames.clear();
                            for (let i = a; i <= b; i++) {
                                if (disp[i] && disp[i].name) selectedNames.add(disp[i].name);
                            }
                        } else {
                            keyboardSelAnchorIdx = idx;
                            selectedNames.clear();
                            if (disp[idx] && disp[idx].name) selectedNames.add(disp[idx].name);
                        }
                        lastSelIdx = idx;
                        itemsEl.querySelectorAll('.explorer-item').forEach((el) => {
                            el.classList.toggle('explorer-item-selected', selectedNames.has(el.dataset.name));
                        });
                    } else if (bucket === 'remote' && !e.shiftKey) {
                        selectedNames.clear();
                        if (disp[idx] && disp[idx].name) selectedNames.add(disp[idx].name);
                        lastSelIdx = idx;
                        itemsEl.querySelectorAll('.explorer-item').forEach((el) => {
                            el.classList.toggle('explorer-item-selected', selectedNames.has(el.dataset.name));
                        });
                    }
                    if (isLocalLibraryBucket() || bucket === 'remote') {
                        try { void updateExplorerPreviewPane(); } catch (_) {}
                        try { void refreshExplorerPropertiesPanel(); } catch (_) {}
                    }
                }

                e.preventDefault();
                const next = tiles[idx];
                if (next) next.focus();
            });

            host.__gridRefreshExplorerTree = function () {
                try { feTreeHasChildrenCache.clear(); } catch (_) {}
                void refreshSidebarTree();
            };

            host.__gridNavigateTo = function (target) {
                if (!target || typeof target !== 'object') return;
                if (explorerBusy && isLocalLibraryBucket()) return;
                clearExplorerGlobalSearch();
                selectedNames.clear();
                if (target.bucket === 'trash') {
                    if (bucket === 'remote' && activeRemoteTabId) persistActiveRemoteTabState();
                    lastNonTrashBucket = (bucket !== 'trash' && bucket !== 'remote') ? bucket : (lastNonTrashBucket || 'files');
                    bucket = 'trash';
                    void renderTrashView();
                    return;
                }
                if (target.bucket === 'cache') {
                    bucket = 'cache';
                    sharedDepartmentId = '';
                    relPath = target.relPath || '';
                    pathHistory = [''];
                    let acc = '';
                    if (relPath) {
                        relPath.split('/').filter(Boolean).forEach((p) => {
                            acc = acc ? acc + '/' + p : p;
                            pathHistory.push(acc);
                        });
                    }
                    historyCursor = pathHistory.length - 1;
                    feActiveTab = 'local';
                    syncExplorerActiveContext();
                    void renderFilesView();
                    return;
                }
                if (target.bucket === 'community') {
                    navigateToSharedRoot('community', '', {});
                    if (target.relPath) navigateToPath(target.relPath);
                    return;
                }
                if (target.bucket === 'department' && target.departmentId) {
                    navigateToSharedRoot('department', String(target.departmentId), {});
                    if (target.relPath) navigateToPath(target.relPath);
                    return;
                }
                if (target.bucket === 'files') {
                    navigateToSharedRoot('files', '', {});
                    if (target.relPath) navigateToPath(target.relPath);
                }
            };

            if (window.GridExplorerCustomization) {
                window.GridExplorerCustomization.fetchCustomization(false).then(() => {
                    window.GridExplorerCustomization.applyExplorerChromeToHost(host);
                }).catch(() => {});
            }

            // Inicial
            void loadExplorerSharedSpaces(true).then(() => {
                syncExplorerActiveContext();
                if (bucket === 'trash') {
                    void renderTrashView();
                } else {
                    void renderFilesView();
                }
            });
        }

        window.openExplorerStorageShortcut = function (kind) {
            const map = {
                files: { bucket: 'files', relPath: '' },
                cache: { bucket: 'cache', relPath: '' },
                trash: { bucket: 'trash' }
            };
            const target = map[kind];
            if (!target) return;
            openFileExplorerWindow(FILE_EXPLORER_APP, null, target);
        };

        function showTrashInfoPreview(title, innerHtml) {
            const modalTitle = document.getElementById('previewModalTitle');
            const previewContent = document.getElementById('previewContent');
            const shareBtn = document.getElementById('shareBtn');
            const downloadBtn = document.getElementById('downloadBtn');
            const previewActions = document.getElementById('previewActions');
            const previewPrintBtn = document.getElementById('previewPrintBtn');
            if (modalTitle) modalTitle.textContent = title || tr('explorerPreview');
            if (previewContent) {
                previewContent.classList.remove('preview-text-plain-host');
                previewContent.innerHTML = innerHtml || '';
            }
            if (shareBtn) shareBtn.style.display = 'none';
            if (downloadBtn) downloadBtn.style.display = 'none';
            if (previewActions) previewActions.style.display = 'none';
            if (previewPrintBtn) previewPrintBtn.style.display = 'none';
            if (previewZoomInBtn) previewZoomInBtn.style.display = 'none';
            if (previewZoomOutBtn) previewZoomOutBtn.style.display = 'none';
            if (previewZoomResetBtn) previewZoomResetBtn.style.display = 'none';
            if (previewExternalBtn) previewExternalBtn.dataset.externalUrl = '';
            layoutPreviewWindowCenteredProportional();
            document.getElementById('previewModal').style.display = 'block';
            document.body.classList.add('modal-open');
            previewActive = true;
            schedulePreviewVisualViewportLayout();
            setTimeout(schedulePreviewVisualViewportLayout, 120);
            setTimeout(schedulePreviewVisualViewportLayout, 450);
        }

        function openRecycleBin() {
            recycleBinModal.style.display = 'block';
            document.body.classList.add('modal-open');
            // Sincronizar también la papelera del explorador.
            fetchExplorerListTrash().then(() => renderTrashItems()).catch(() => renderTrashItems());
        }

        function updateRecycleBinEmptyButtons() {
            const has =
                (Array.isArray(trash) && trash.length > 0) ||
                (Array.isArray(fileExplorerTrash) && fileExplorerTrash.length > 0);
            document.querySelectorAll('#emptyTrashBtnTop, #emptyTrashBtn').forEach((btn) => {
                if (!btn) return;
                btn.disabled = !has;
                btn.setAttribute('aria-disabled', has ? 'false' : 'true');
            });
        }

        function renderTrashItems() {
            try {
                trashItems.innerHTML = '';
                const hasAppTrash = Array.isArray(trash) && trash.length > 0;
                const hasFileTrash = Array.isArray(fileExplorerTrash) && fileExplorerTrash.length > 0;
                if (!hasAppTrash && !hasFileTrash) {
                    trashItems.innerHTML = `<p class="trash-empty-msg">${tr('recycleTrashEmpty')}</p>`;
                    return;
                }

                if (hasAppTrash) {
                    trash.forEach((item, index) => {
                        const div = document.createElement('div');
                        div.className = 'trash-item';
                        const iconSrc = item.icon || (gridTrashItemIsQuickNote(item) ? QUICK_NOTE_TRASH_ICON : '');
                        const safeTitle = String(item.title || '').replace(/</g, '&lt;');
                        div.innerHTML = `
                        <img src="${iconSrc}" alt="${safeTitle}">
                        <span class="trash-item-title">${safeTitle}</span>
                        <div class="trash-item-actions">
                            <button type="button" class="trash-btn trash-btn-view" onclick="previewTrashApp(${index})">${tr('recycleView')}</button>
                            <button type="button" class="trash-btn trash-btn-restore" onclick="restoreFromTrash(${index})">${tr('recycleRestore')}</button>
                            <button type="button" class="trash-btn trash-btn-delete" onclick="deletePermanently(${index})">${tr('recycleDelete')}</button>
                        </div>
                    `;
                        trashItems.appendChild(div);
                    });
                }

                if (hasFileTrash) {
                    fileExplorerTrash.forEach((t) => {
                        const div = document.createElement('div');
                        div.className = 'trash-item';
                        const isImage = (t.mimeType || '').startsWith('image/');
                        const secureUrl = isImage && t.url ? buildSecureFileUrl(t.url) : FILE_EXPLORER_APP.icon;
                        div.innerHTML = `
                        <img src="${secureUrl}" alt="${t.name}">
                        <span class="trash-item-title">${t.name}</span>
                        <div class="trash-item-actions">
                            <button type="button" class="trash-btn trash-btn-view" onclick="previewFileExplorerTrashFromId('${t.id}')">${tr('recycleView')}</button>
                            <button type="button" class="trash-btn trash-btn-restore" onclick="restoreFileExplorerTrashFromId('${t.id}')">${tr('recycleRestore')}</button>
                            <button type="button" class="trash-btn trash-btn-delete" onclick="deleteFileExplorerTrashFromId('${t.id}')">${tr('recycleDelete')}</button>
                        </div>
                    `;
                        trashItems.appendChild(div);
                    });
                }
            } finally {
                updateRecycleBinEmptyButtons();
            }
        }

        window.previewTrashApp = function(index) {
            const item = trash[index];
            if (!item) return;
            if (gridTrashItemIsQuickNote(item)) {
                const note = item.quickNote || {};
                const title = String(note.title || '').trim() || tr('recyclePreviewQuickNote');
                const text = String(note.text || '').trim();
                showTrashInfoPreview(
                    title,
                    `<pre class="preview-text-plain-pre">${escapeHtml(text || '—')}</pre>`
                );
                return;
            }
            if (item.lastAction === 'file' && item.fileInfo) {
                showFilePreview(item.fileInfo);
                return;
            }
            if (item.lastAction === 'link' && item.link && item.link !== '#') {
                showLinkPreview(item.link);
                return;
            }
            if (item.explorerOpen && item.explorerOpen.bucket) {
                const rel = item.explorerOpen.relPath || '';
                const pathLabel = tr('explorerTrash') + (rel ? ' / ' + rel : '');
                showTrashInfoPreview(
                    item.title || tr('explorerTrash'),
                    `<p>${escapeHtml(pathLabel)}</p><p class="trash-preview-meta">${escapeHtml(tr('recyclePreviewExplorerShortcut'))}</p>`
                );
                return;
            }
            const icon = item.icon || '';
            const iconLooksLikeImage =
                icon.startsWith('data:image') ||
                /\.(jpe?g|png|gif|webp|svg)(\?|$)/i.test(icon) ||
                (icon.includes('media/') && !icon.includes('placeholder'));
            if (iconLooksLikeImage) {
                showTrashInfoPreview(
                    item.title || tr('explorerPreview'),
                    `<div class="image-preview-wrapper" style="display:flex;justify-content:center;padding:12px;"><img src="${escapeHtmlAttr(icon)}" alt="" style="max-width:100%;max-height:70vh;object-fit:contain;border-radius:8px;"></div>`
                );
                return;
            }
            const desc = item.description ? `<p>${escapeHtml(String(item.description))}</p>` : '';
            showTrashInfoPreview(
                item.title || tr('explorerPreview'),
                desc || `<p>${escapeHtml(tr('recyclePreviewNoPreview'))}</p>`
            );
        };

        window.previewFileExplorerTrashFromId = async function(id) {
            if (!id) return;
            const entry = (fileExplorerTrash || []).find((t) => t.id === id);
            if (!entry) return;
            if (entry.kind === 'dir') {
                const data = await fetchExplorerTrashListDir(entry.id, entry.name, '').catch(() => null);
                const items = data && data.success && Array.isArray(data.items) ? data.items : [];
                let html = `<p><i class="fas fa-folder" aria-hidden="true"></i> ${escapeHtml(entry.name)}</p>`;
                if (!items.length) {
                    html += `<p>${escapeHtml(tr('recyclePreviewFolderEmpty'))}</p>`;
                } else {
                    html += '<ul class="trash-folder-preview-list">' + items.map((it) => {
                        const kindIcon = it.kind === 'dir' ? '<i class="fas fa-folder" aria-hidden="true"></i>' : '<i class="fas fa-file" aria-hidden="true"></i>';
                        return `<li>${kindIcon} ${escapeHtml(it.name || '')}</li>`;
                    }).join('') + '</ul>';
                }
                showTrashInfoPreview(entry.name, html);
                return;
            }
            showFilePreview({
                name: entry.name,
                url: entry.url,
                type: entry.mimeType || ''
            });
        };

        window.restoreFromTrash = function(index) {
            const item = trash[index];
            if (gridTrashItemIsQuickNote(item)) {
                restoreQuickNoteFromTrash(index);
                return;
            }
            apps.push(item);
            trash.splice(index, 1);
            saveSettings({ apps: apps, trash: trash }).then(() => {
                renderApps();
                if (windowsMenuVisible) renderWindowsMenu(apps);
                renderTrashItems();
                updateRecycleBinIcon();
            });
        };

        function getExplorerTrashRestoredName(entry, restoreOpts) {
            if (restoreOpts && restoreOpts.newName) return String(restoreOpts.newName).trim();
            return entry && entry.name ? String(entry.name) : '';
        }

        async function notifyMediaWidgetTrashRestore(entry, restoreOpts) {
            if (!entry) return;
            try {
                if (window.OsShell && typeof window.OsShell.restoreMediaWidgetFromTrash === 'function') {
                    await window.OsShell.restoreMediaWidgetFromTrash(entry, getExplorerTrashRestoredName(entry, restoreOpts));
                }
            } catch (_) {}
        }

        // Papelera del explorador (archivos/carpetas)
        window.restoreFileExplorerTrashFromId = async function(id) {
            if (!id) return;
            const entry = (fileExplorerTrash || []).find(t => t.id === id);
            let opts = {};
            while (true) {
                const r = await explorerRestoreTrashItem(id, opts).catch(() => ({ success: false }));
                if (r && r.success) {
                    await fetchExplorerListTrash().catch(() => {});
                    document.querySelectorAll('[id^="explorerHost_"]').forEach((h) => {
                        if (typeof h.__gridRefreshExplorerTree === 'function') h.__gridRefreshExplorerTree();
                    });
                    await notifyMediaWidgetTrashRestore(entry, opts);
                    renderTrashItems();
                    updateRecycleBinIcon();
                    logActivity('explorer_restore_trash', { id, name: entry?.name || id });
                    return;
                }
                if (r && r.conflict) {
                    const kind = r.kind || (entry?.kind || 'file');
                    const conflictName = r.name || entry?.name || id;
                    const choice = await showExplorerConflictModal(conflictName, kind);
                    if (!choice || choice.action === 'cancel') return;
                    if (choice.action === 'skip') return;
                    if (choice.action === 'replace') { opts = { overwrite: true }; continue; }
                    if (choice.action === 'merge') { opts = { merge: true }; continue; }
                    if (choice.action === 'rename') { opts = { newName: choice.newName || '' }; continue; }
                    return;
                }
                await appDialog.alert(r?.error || tr('explorerRestoreFailed'));
                return;
            }
        };

        window.deletePermanently = async function(index) {
            const item = trash[index];
            if (gridTrashItemIsQuickNote(item)) {
                trash.splice(index, 1);
                saveSettings({ trash: trash }).then(() => {
                    renderTrashItems();
                    updateRecycleBinIcon();
                });
                return;
            }
            const skipMedia = gridTrashItemSkipsMediaDelete(item);
            if (!skipMedia && item.fileInfo && item.fileInfo.url) {
                await deleteMediaFile({ url: item.fileInfo.url });
            }
            if (!skipMedia && item.icon && !item.icon.includes('placeholder') && !item.icon.includes('data:image')) {
                await deleteMediaFile({ url: item.icon });
            }
            trash.splice(index, 1);
            saveSettings({ trash: trash }).then(() => {
                renderTrashItems();
                updateRecycleBinIcon();
            });
        };

        window.deleteFileExplorerTrashFromId = async function(id) {
            if (!id) return;
            const entry = (fileExplorerTrash || []).find(t => t.id === id);
            const r = await explorerDeleteTrashItem(id).catch(() => ({ success: false }));
            if (r && r.success) {
                await fetchExplorerListTrash().catch(() => {});
                renderTrashItems();
                updateRecycleBinIcon();
                logActivity('explorer_delete_trash_perm', { id, name: entry?.name || id });
            } else {
                await appDialog.alert(r?.error || tr('recycleDeleteFailed'));
            }
        };

        window.emptyTrash = async function() {
            if ((!trash || trash.length === 0) && (!fileExplorerTrash || fileExplorerTrash.length === 0)) return;
            if (await appDialog.confirm(tr('recycleEmptyConfirm'))) {
                for (let item of trash) {
                    if (gridTrashItemIsQuickNote(item)) continue;
                    const skipMedia = gridTrashItemSkipsMediaDelete(item);
                    if (!skipMedia && item.fileInfo && item.fileInfo.url) {
                        await deleteMediaFile({ url: item.fileInfo.url });
                    }
                    if (!skipMedia && item.icon && !item.icon.includes('placeholder') && !item.icon.includes('data:image')) {
                        await deleteMediaFile({ url: item.icon });
                    }
                }
                trash = [];
                const ex = await explorerEmptyTrash().catch(() => null);
                if (ex && ex.success) logActivity('explorer_empty_trash', {});
                fileExplorerTrash = [];
                saveSettings({ trash: trash }).then(() => {
                    renderTrashItems();
                    updateRecycleBinIcon();
                });
            }
        };

        function updateRecycleBinIcon() {
            const icon = recycleBin.querySelector('i');
            const hasTrash = (trash && trash.length > 0) || (fileExplorerTrash && fileExplorerTrash.length > 0);
            if (hasTrash) {
                icon.className = 'fas fa-trash-alt';
            } else {
                icon.className = 'fas fa-trash';
            }
            updateRecycleBinEmptyButtons();
        }

        function updateClock() {
            if (!clock) return;
            const now = new Date();
            if (typeof window.AntaraClockFormat !== 'undefined' && window.AntaraClockFormat.taskbarClockPayload) {
                const pay = window.AntaraClockFormat.taskbarClockPayload(now, {
                    mobilePcMode: document.body.classList.contains('mobile-pc-mode'),
                    verticalTaskbar: document.body.classList.contains('taskbar-left') || document.body.classList.contains('taskbar-right'),
                });
                clock.textContent = pay.text;
                clock.title = pay.title || '';
                return;
            }
            const longDateStr = now.toLocaleDateString('es', { weekday: 'long', day: 'numeric', month: 'long', year: 'numeric' });
            const time12 = now.toLocaleTimeString('es', { hour: 'numeric', minute: '2-digit', second: '2-digit', hour12: true });
            const time12Short = now.toLocaleTimeString('es', { hour: 'numeric', minute: '2-digit', hour12: true });
            const mobileDateLine = now.toLocaleDateString('es', { weekday: 'short', day: 'numeric', month: 'short', year: 'numeric' });

            if (document.body.classList.contains('mobile-pc-mode')) {
                clock.textContent = `${time12Short}\n${mobileDateLine}`;
                clock.title = longDateStr;
                return;
            }

            const verticalBar = document.body.classList.contains('taskbar-left') || document.body.classList.contains('taskbar-right');
            clock.textContent = verticalBar ? `${time12Short}\n${longDateStr}` : `${time12}\n${longDateStr}`;
            clock.title = '';
        }
        setInterval(updateClock, 1000);
        updateClock();

        let touchTimer;
        let currentTouchAppIndex = -1;

        function handleTouchStart(index) {
            return (e) => {
                if (previewActive) return;
                if (index === -1) return;
                touchStartTime = Date.now();
                touchStartY = e.touches[0].clientY;
                currentTouchAppIndex = index;
                isScrolling = false;
                e.target.closest('.app-item').classList.add('touch-active');
                touchTimer = setTimeout(() => {
                    if (!isScrolling && currentTouchAppIndex === index && index >= 0 && index < apps.length && !apps[index]?.isSystemApp) {
                        e.preventDefault();
                        editingIndex = index;
                        selectedAppIndex = index;
                        const visibleItems = Array.from(document.querySelectorAll('#appGrid .app-item'));
                        selectedGridVisibleIndex = visibleItems.findIndex(item => parseInt(item.dataset.index) === index);
                        e.target.closest('.app-item').classList.remove('touch-active');
                        const app = apps[index];
                        const isMobileDevice = isMobile();
                        const mobileDesktopMode = getSetting('mobileDesktopMode', false);
                        const desktopMode = getSetting('desktopMode', 'grid');
                        const showWindowsMode = (!isMobileDevice && desktopMode === 'windows') || (isMobileDevice && mobileDesktopMode && desktopMode === 'windows');
                        if (isMobileDevice || !showWindowsMode) {
                            toggleGridBtn.style.display = 'none';
                        } else {
                            toggleGridBtn.style.display = 'block';
                            const onGridTouch = appIsVisibleOnWindowsLauncherGrid(app);
                            toggleGridBtn.innerHTML = `<i class="fas ${onGridTouch ? 'fa-eye-slash' : 'fa-eye'}"></i> ${onGridTouch ? tr('removeDesktop') : tr('addDesktop')}`;
                        }
                        pinToTaskbarBtn.style.display = (showWindowsMode && ((app.lastAction === 'link' && app.link && app.link !== '#') || (app.lastAction === 'file' && app.fileInfo) || (app.explorerOpen && app.explorerOpen.bucket))) ? 'block' : 'none';
                        showStripMenu(e, stripMenu);
                    }
                }, 500);
            };
        }

        function handleTouchMove(index) {
            return (e) => {
                if (previewActive) return;
                const touchY = e.touches[0].clientY;
                const diffY = Math.abs(touchY - touchStartY);
                if (diffY > 10) {
                    isScrolling = true;
                    clearTimeout(touchTimer);
                    e.target.closest('.app-item').classList.remove('touch-active');
                }
            };
        }

        function handleTouchEnd(index) {
            return (e) => {
                if (previewActive) return;
                clearTimeout(touchTimer);
                e.target.closest('.app-item').classList.remove('touch-active');
                const touchEndTime = Date.now();
                const touchDuration = touchEndTime - touchStartTime;
                if (!isScrolling && touchDuration < 300 && currentTouchAppIndex === index && index >= 0 && index < apps.length) {
                    e.preventDefault();
                    const app = apps[index];
                    if (app.isSystemApp && app.action === 'openRecycleBin') {
                        openRecycleBin();
                    } else {
                        launchDesktopApp(app);
                    }
                }
            };
        }

        function clearTouchTimer() {
            clearTimeout(touchTimer);
            document.querySelectorAll('.app-item').forEach(item => item.classList.remove('touch-active'));
        }

        function showStripMenu(event, menuElement) {
            if (currentContextMenu) {
                currentContextMenu.remove();
                currentContextMenu = null;
            }
            stripMenu.style.display = 'none';
            desktopStripMenu.style.display = 'none';
            if (menuElement === desktopStripMenu && desktopNextWallpaperBtn) {
                const nwLabel = document.getElementById('desktopNextWallpaperLabel');
                if (nwLabel) nwLabel.textContent = tr('nextWallpaper');
                desktopNextWallpaperBtn.setAttribute('title', tr('nextWallpaper'));
                desktopNextWallpaperBtn.style.display = canCycleWallpaper() ? 'flex' : 'none';
            }

            if (window.innerWidth <= 768) {
                menuElement.style.left = '50%';
                menuElement.style.top = '50%';
                menuElement.style.transform = 'translate(-50%, -50%)';
            } else {
                let clientX, clientY;
                if (event.type === 'touchstart' && event.touches && event.touches[0]) {
                    clientX = event.touches[0].clientX;
                    clientY = event.touches[0].clientY;
                } else if (typeof event.clientX === 'number' && typeof event.clientY === 'number') {
                    clientX = event.clientX;
                    clientY = event.clientY;
                } else {
                    clientX = 0;
                    clientY = 0;
                }
                
                const menuWidth = menuElement.offsetWidth;
                const menuHeight = menuElement.offsetHeight;
                const windowWidth = window.innerWidth;
                const windowHeight = window.innerHeight;
                
                let left = clientX;
                let top = clientY;
                
                if (left + menuWidth > windowWidth) left = windowWidth - menuWidth - 10;
                if (top + menuHeight > windowHeight) top = windowHeight - menuHeight - 10;
                
                menuElement.style.left = left + 'px';
                menuElement.style.top = top + 'px';
                menuElement.style.transform = 'none';
            }
            menuElement.style.display = 'block';
            stripMenuBackdrop.style.display = 'block';
        }

        function hideStripMenu() {
            stripMenu.style.display = 'none';
            desktopStripMenu.style.display = 'none';
            stripMenuBackdrop.style.display = 'none';
        }
        stripMenuBackdrop.addEventListener('click', hideStripMenu);

        desktopAddAppBtn.addEventListener('click', () => {
            hideStripMenu();
            openModal();
        });
        desktopConfigBtn.addEventListener('click', () => {
            hideStripMenu();
            configBtn.click();
        });
        if (desktopNextWallpaperBtn) {
            desktopNextWallpaperBtn.addEventListener('click', () => {
                hideStripMenu();
                cycleNextWallpaper();
            });
        }

        editAppBtn.addEventListener('click', () => {
            hideStripMenu();
            if (launcherContextTarget && launcherContextTarget.type === 'folder') return;
            if (editingIndex !== -1 && apps[editingIndex] && !apps[editingIndex].isSystemApp) {
                openModal(apps[editingIndex], editingIndex);
            }
        });

        toggleGridBtn.addEventListener('click', () => {
            hideStripMenu();
            if (launcherContextTarget && launcherContextTarget.type === 'folder') return;
            if (editingIndex !== -1 && apps[editingIndex] && !apps[editingIndex].isSystemApp) {
                const app = apps[editingIndex];
                const onGrid = appIsVisibleOnWindowsLauncherGrid(app);
                if (onGrid) {
                    app.showInGrid = false;
                    saveSettings({ apps: apps }).then(() => {
                        renderApps();
                        if (windowsMenuVisible) renderWindowsMenu(apps);
                        showToast(tr('desktopToastRemoved'));
                    });
                } else {
                    app.showInGrid = true;
                    const savePayload = { apps: apps };
                    if (window.GridLauncher && window.GridLauncher.layoutModel && launcherLayout) {
                        launcherLayout = GridLauncher.layoutModel.appendAppToOrder(launcherLayout, getAppStableId(app), getAppStableId);
                        savePayload.launcherLayout = launcherLayout;
                    }
                    saveSettings(savePayload).then(() => {
                        renderApps();
                        if (windowsMenuVisible) renderWindowsMenu(apps);
                        showToast(tr('desktopToastAdded'));
                    });
                }
            }
        });

        pinToTaskbarBtn.addEventListener('click', () => {
            hideStripMenu();
            if (launcherContextTarget && launcherContextTarget.type === 'folder') return;
            if (editingIndex !== -1 && apps[editingIndex] && !apps[editingIndex].isSystemApp) {
                const app = apps[editingIndex];
                pinApp(app);
            }
        });

        deleteAppBtn.addEventListener('click', () => {
            hideStripMenu();
            if (launcherContextTarget && launcherContextTarget.type === 'folder') return;
            if (editingIndex !== -1 && apps[editingIndex] && !apps[editingIndex].isSystemApp) {
                moveToTrash(editingIndex);
            }
        });

        if (folderOpenBtn) {
            folderOpenBtn.addEventListener('click', () => {
                hideStripMenu();
                if (launcherContextTarget && launcherContextTarget.type === 'folder' && launcherContextTarget.folderId) {
                    openLauncherFolderModal(launcherContextTarget.folderId);
                }
            });
        }
        if (folderInfoBtn) {
            folderInfoBtn.addEventListener('click', async () => {
                hideStripMenu();
                if (!launcherLayout || !launcherContextTarget || launcherContextTarget.type !== 'folder') return;
                const fid = launcherContextTarget.folderId;
                const f = launcherLayout.folders[fid];
                if (!f) return;
                const n = (f.appStableIds || []).length;
                const col = f.color || '—';
                await appDialog.alert(`${f.name || ''}\n\n${tr('explorerFilesLabel')}: ${n}\n${tr('launcherFolderColor')}: ${col}`);
            });
        }
        if (folderRenameBtn) {
            folderRenameBtn.addEventListener('click', async () => {
                hideStripMenu();
                if (!launcherLayout || !launcherContextTarget || launcherContextTarget.type !== 'folder') return;
                const fid = launcherContextTarget.folderId;
                const f = launcherLayout.folders[fid];
                if (!f) return;
                const name = await appDialog.prompt('Nombre de la carpeta', f.name || 'Carpeta');
                if (name === null) return;
                const r = GridLauncher.folderService.updateFolderMeta(launcherLayout, fid, { name: name });
                if (r.error) return;
                launcherLayout = r.layout;
                saveSettings({ launcherLayout: launcherLayout }).then(() => renderApps());
            });
        }
        if (folderColorBtn) {
            folderColorBtn.addEventListener('click', async () => {
                hideStripMenu();
                if (!launcherLayout || !launcherContextTarget || launcherContextTarget.type !== 'folder') return;
                const fid = launcherContextTarget.folderId;
                const f = launcherLayout.folders[fid];
                if (!f) return;
                const col = await appDialog.prompt('Color (hex, ej. #448aff)', f.color || '#5c6bc0');
                if (col === null) return;
                if (!GridLauncher.layoutModel.isValidHexColor(col)) {
                    showToast('Color no válido');
                    return;
                }
                const r = GridLauncher.folderService.updateFolderMeta(launcherLayout, fid, { color: col });
                if (r.error) return;
                launcherLayout = r.layout;
                saveSettings({ launcherLayout: launcherLayout }).then(() => renderApps());
            });
        }
        if (folderDeleteBtn) {
            folderDeleteBtn.addEventListener('click', async () => {
                hideStripMenu();
                if (!launcherLayout || !launcherContextTarget || launcherContextTarget.type !== 'folder') return;
                const fid = launcherContextTarget.folderId;
                const f = launcherLayout.folders[fid];
                if (!f) return;
                const ids = f.appStableIds || [];
                if (ids.length === 0) {
                    const r = GridLauncher.folderService.deleteFolderIfEmpty(launcherLayout, fid);
                    if (!r.error) {
                        launcherLayout = r.layout;
                        saveSettings({ launcherLayout: launcherLayout }).then(() => renderApps());
                    }
                    return;
                }
                if (await appDialog.confirm('La carpeta contiene aplicaciones. ¿Descomponer y colocar las apps en el escritorio?')) {
                    const r2 = GridLauncher.folderService.removeFolderAndUnpack(launcherLayout, fid);
                    if (!r2.error) {
                        launcherLayout = r2.layout;
                        saveSettings({ launcherLayout: launcherLayout }).then(() => renderApps());
                    }
                }
            });
        }
        if (launcherFolderModalClose && launcherFolderModal) {
            launcherFolderModalClose.addEventListener('click', () => {
                launcherFolderModal.style.display = 'none';
                document.body.classList.remove('modal-open');
            });
        }

        async function moveToTrash(index) {
            if (index > -1 && index < apps.length && (await appDialog.confirm('¿Mover a la papelera?'))) {
                const appToTrash = apps[index];
                if (appToTrash.isSystemApp) {
                    showToast('No se puede desinstalar una aplicación de sistema');
                    return;
                }
                updatePinnedAppsOnDelete(appToTrash);
                removeDesktopShortcut(appToTrash);
                const trashSid = getAppStableId(appToTrash);
                const windowsToClose = openWindows.filter(w => getAppStableId(w.app) === trashSid);
                windowsToClose.forEach(w => w.frame.remove());
                openWindows = openWindows.filter(w => getAppStableId(w.app) !== trashSid);
                activeTaskbarApps = activeTaskbarApps.filter(a => a.appId !== trashSid);
                renderTaskbarApps();
                trash.push(appToTrash);
                apps.splice(index, 1);
                if (window.GridLauncher && window.GridLauncher.layoutModel && launcherLayout) {
                    launcherLayout = GridLauncher.layoutModel.removeAppEverywhere(launcherLayout, getAppStableId(appToTrash));
                }
                saveSettings({ apps: apps, trash: trash, launcherLayout: launcherLayout }).then(() => {
                    renderApps();
                    if (windowsMenuVisible) renderWindowsMenu(apps);
                    updateRecycleBinIcon();
                    if (selectedAppIndex === index) selectedAppIndex = -1;
                });
            }
        }

        /**
         * Vecinos espaciales { left, right, up, down } para navegación con flechas alineada al grid real.
         * @param {HTMLElement[]} elements Orden DOM (row-major como pinta el layout).
         * @returns {WeakMap<HTMLElement, { left: HTMLElement|null, right: HTMLElement|null, up: HTMLElement|null, down: HTMLElement|null }>}
         */
        function buildGridKeyboardNeighbors(elements) {
            const map = new WeakMap();
            if (!elements || elements.length === 0) return map;
            const EPS = 3;
            const rects = elements.map(function (el) {
                return { el: el, r: el.getBoundingClientRect() };
            });

            function verticalOverlap(fr, br) {
                const top = Math.max(fr.top, br.top);
                const bot = Math.min(fr.bottom, br.bottom);
                const inter = Math.max(0, bot - top);
                const minH = Math.min(fr.height, br.height) || 1;
                return inter / minH;
            }

            for (let i = 0; i < rects.length; i++) {
                const from = rects[i].el;
                const fr = rects[i].r;
                const fcx = (fr.left + fr.right) / 2;
                const fcy = (fr.top + fr.bottom) / 2;
                let left = null;
                let right = null;
                let up = null;
                let down = null;
                let bestL = Infinity;
                let bestR = Infinity;
                let bestU = Infinity;
                let bestD = Infinity;

                for (let j = 0; j < rects.length; j++) {
                    if (i === j) continue;
                    const o = rects[j].el;
                    const br = rects[j].r;
                    const bcx = (br.left + br.right) / 2;
                    const bcy = (br.top + br.bottom) / 2;
                    const ov = verticalOverlap(fr, br);

                    if (ov >= 0.22 && br.left >= fr.right - EPS) {
                        const gap = Math.max(0, br.left - fr.right);
                        const align = Math.abs(bcy - fcy);
                        const score = gap * 80 + align;
                        if (score < bestR) {
                            bestR = score;
                            right = o;
                        }
                    }
                    if (ov >= 0.22 && br.right <= fr.left + EPS) {
                        const gap = Math.max(0, fr.left - br.right);
                        const align = Math.abs(bcy - fcy);
                        const score = gap * 80 + align;
                        if (score < bestL) {
                            bestL = score;
                            left = o;
                        }
                    }
                    if (br.top >= fr.bottom - EPS) {
                        const horiz = Math.abs(bcx - fcx);
                        const vert = br.top - fr.top;
                        const score = horiz * 12 + vert;
                        if (score < bestD) {
                            bestD = score;
                            down = o;
                        }
                    }
                    if (br.bottom <= fr.top + EPS) {
                        const horiz = Math.abs(bcx - fcx);
                        const vert = fr.bottom - br.bottom;
                        const score = horiz * 12 + vert;
                        if (score < bestU) {
                            bestU = score;
                            up = o;
                        }
                    }
                }
                map.set(from, { left: left, right: right, up: up, down: down });
            }
            return map;
        }

        const CLOSE_WIN_ESC_ICON_FALLBACK = "data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath fill='%2394a3b8' d='M4 6h16v2H4V6zm0 4h16v2H4v-2zm0 4h16v2H4v-2z'/%3E%3C/svg%3E";

        function isCloseWindowEscConfirmOpen() {
            const el = document.getElementById('closeWindowEscConfirmModal');
            return !!(el && el.style.display === 'block');
        }

        function dismissCloseWindowEscConfirm() {
            const el = document.getElementById('closeWindowEscConfirmModal');
            if (el) el.style.display = 'none';
            delete document.body.dataset.closeWindowEscPendingId;
            document.body.classList.remove('modal-open');
        }

        function ensureCloseWindowEscConfirmModal() {
            let modal = document.getElementById('closeWindowEscConfirmModal');
            if (modal) return modal;
            modal = document.createElement('div');
            modal.id = 'closeWindowEscConfirmModal';
            modal.className = 'modal';
            modal.style.display = 'none';
            modal.setAttribute('role', 'dialog');
            modal.setAttribute('aria-modal', 'true');
            modal.setAttribute('aria-labelledby', 'closeWindowEscConfirmHeading');
            modal.innerHTML = `
                <div class="modal-content" style="max-width:440px;text-align:center;">
                    <h2 id="closeWindowEscConfirmHeading" style="margin-top:0;margin-bottom:0.35em;font-size:1.15rem;"></h2>
                    <div id="closeWindowEscConfirmRow" style="display:flex;align-items:center;gap:16px;margin:18px 0 22px;text-align:left;">
                        <img id="closeWindowEscConfirmIcon" src="" alt="" width="56" height="56" style="border-radius:12px;object-fit:cover;flex-shrink:0;box-shadow:0 2px 10px rgba(0,0,0,.12);background:#f1f5f9;">
                        <p id="closeWindowEscConfirmMessage" style="margin:0;font-size:1rem;line-height:1.45;opacity:0.95;"></p>
                    </div>
                    <div style="display:flex;flex-wrap:wrap;gap:10px;justify-content:flex-end;">
                        <button type="button" id="closeWindowEscConfirmCancelBtn"></button>
                        <button type="button" id="closeWindowEscConfirmYesBtn" style="font-weight:600;"></button>
                    </div>
                </div>`;
            document.body.appendChild(modal);
            const onNo = () => dismissCloseWindowEscConfirm();
            const onYes = () => {
                const id = document.body.dataset.closeWindowEscPendingId;
                dismissCloseWindowEscConfirm();
                if (!id) return;
                const w = openWindows.find(x => x.id === id);
                if (w && w.frame && document.body.contains(w.frame)) {
                    const btn = w.frame.querySelector('.close-btn');
                    if (btn) btn.click();
                }
            };
            modal.querySelector('#closeWindowEscConfirmCancelBtn').addEventListener('click', onNo);
            modal.querySelector('#closeWindowEscConfirmYesBtn').addEventListener('click', onYes);
            modal.addEventListener('click', (ev) => {
                if (ev.target === modal) onNo();
            });
            return modal;
        }

        function showCloseWindowEscConfirm(winEntry) {
            if (!winEntry || !winEntry.frame || !document.body.contains(winEntry.frame)) return;
            const modal = ensureCloseWindowEscConfirmModal();
            const app = winEntry.app;
            const title = (app && app.title) ? String(app.title) : tr('windowDefaultTitle');
            let iconSrc = (app && app.icon) ? String(app.icon) : '';
            if (!iconSrc) {
                const titleImg = winEntry.frame.querySelector('.window-title img');
                if (titleImg && titleImg.getAttribute('src')) iconSrc = titleImg.getAttribute('src');
            }
            const iconEl = modal.querySelector('#closeWindowEscConfirmIcon');
            const msgEl = modal.querySelector('#closeWindowEscConfirmMessage');
            const h = modal.querySelector('#closeWindowEscConfirmHeading');
            const yesBtn = modal.querySelector('#closeWindowEscConfirmYesBtn');
            const noBtn = modal.querySelector('#closeWindowEscConfirmCancelBtn');
            h.textContent = tr('closeWindowEscConfirmTitle');
            msgEl.textContent = tr('closeWindowEscConfirmMessage').replace('%title%', title);
            yesBtn.textContent = tr('closeWindowEscConfirmYes');
            noBtn.textContent = tr('closeWindowEscConfirmCancel');
            iconEl.onerror = function oe() {
                iconEl.removeEventListener('error', oe);
                iconEl.src = CLOSE_WIN_ESC_ICON_FALLBACK;
            };
            iconEl.src = iconSrc || CLOSE_WIN_ESC_ICON_FALLBACK;
            document.body.dataset.closeWindowEscPendingId = winEntry.id;
            bumpShellModalAboveWindows(modal);
            modal.style.display = 'block';
            document.body.classList.add('modal-open');
            setTimeout(() => {
                try { noBtn.focus(); } catch (_) {}
            }, 0);
        }

        // Navegación por teclado mejorada
        document.addEventListener('keydown', (e) => {
            if (e.key === 'Escape' && isCloseWindowEscConfirmOpen()) {
                e.preventDefault();
                e.stopPropagation();
                dismissCloseWindowEscConfirm();
                return;
            }
            if (e.key === 'Escape' && typeof window.__gridCloseExplorerRenameModal === 'function') {
                e.preventDefault();
                e.stopPropagation();
                window.__gridCloseExplorerRenameModal();
                return;
            }
            if (isExplorerFocusLockActive() && !isTargetInsideExplorerScope(e.target) && !isExplorerKeyboardLockExemptTarget(e.target)) {
                e.preventDefault();
                e.stopPropagation();
                focusExplorerPrimaryElement();
                return;
            }
                // Cuando el foco está dentro del explorador, dejamos que su propio
                // manejador procese navegación, selección múltiple y atajos.
                if (isTargetInsideExplorerScope(e.target)) {
                    const k = e.key;
                    if ((k === 'Control' || k === 'Meta') && !isTextEditingTarget(e.target)) {
                        e.preventDefault();
                        e.stopPropagation();
                        return;
                    }
                    const isExplorerNavKey =
                        k === 'ArrowRight' || k === 'ArrowLeft' || k === 'ArrowDown' || k === 'ArrowUp' ||
                        k === 'Enter' || k === 'Backspace' || k === 'Delete' || k === 'F2' ||
                        ((e.ctrlKey || e.metaKey) && (k === 'a' || k === 'A' || k === 'c' || k === 'C' || k === 'x' || k === 'X' || k === 'v' || k === 'V'));
                    if (isExplorerNavKey) return;
                }
            const appItems = Array.from(document.querySelectorAll('#appGrid .app-item'));
            const windowsAppItems = Array.from(document.querySelectorAll('.windows-menu-apps .app-item'));
            const categoryItems = Array.from(document.querySelectorAll('#windowsCategoryList li'));
            const visibleWindows = openWindows
                .filter(w => w.frame && w.frame.style.display !== 'none' && !w.frame.classList.contains('minimized'))
                .sort((a, b) => (parseInt(b.frame.style.zIndex || '0', 10) - parseInt(a.frame.style.zIndex || '0', 10)));
            const topVisibleWindow = visibleWindows.length ? visibleWindows[0] : null;
            const topVisibleIsFile = !!(topVisibleWindow && topVisibleWindow.app && topVisibleWindow.app.lastAction === 'file');

            if (topVisibleIsFile) {
                const frame = topVisibleWindow.frame;
                const zoomIn = frame.querySelector('.zoom-in-btn');
                const zoomOut = frame.querySelector('.zoom-out-btn');
                const zoomReset = frame.querySelector('.zoom-reset-btn');
                if (e.key === '+' || e.key === '=') {
                    e.preventDefault();
                    if (zoomIn) zoomIn.click();
                    return;
                }
                if (e.key === '-' || e.key === '_') {
                    e.preventDefault();
                    if (zoomOut) zoomOut.click();
                    return;
                }
                if (e.key === '0') {
                    e.preventDefault();
                    if (zoomReset) zoomReset.click();
                    return;
                }
                if (activePreviewImg && frame.contains(activePreviewImg) && isImageZoomed) {
                    if (e.key === 'ArrowUp') { e.preventDefault(); translateY += 20; updateImageTransform(); return; }
                    if (e.key === 'ArrowDown') { e.preventDefault(); translateY -= 20; updateImageTransform(); return; }
                    if (e.key === 'ArrowLeft') { e.preventDefault(); translateX += 20; updateImageTransform(); return; }
                    if (e.key === 'ArrowRight') { e.preventDefault(); translateX -= 20; updateImageTransform(); return; }
                }
            }
            
            if (previewActive && activePreviewImg) {
                if (e.key === '+' || e.key === '=') {
                    e.preventDefault();
                    zoomImageIn();
                } else if (e.key === '-' || e.key === '_') {
                    e.preventDefault();
                    zoomImageOut();
                } else if (isImageZoomed && e.key === 'ArrowUp') {
                    e.preventDefault();
                    translateY += 20;
                    updateImageTransform();
                } else if (isImageZoomed && e.key === 'ArrowDown') {
                    e.preventDefault();
                    translateY -= 20;
                    updateImageTransform();
                } else if (isImageZoomed && e.key === 'ArrowLeft') {
                    e.preventDefault();
                    translateX += 20;
                    updateImageTransform();
                } else if (isImageZoomed && e.key === 'ArrowRight') {
                    e.preventDefault();
                    translateX -= 20;
                    updateImageTransform();
                } else if (e.key === '0') {
                    e.preventDefault();
                    resetImageZoom();
                }
                return;
            }
            
            if (e.key === 'Escape') {
                if (mobileAppHubBackdrop && mobileAppHubBackdrop.classList.contains('visible')) {
                    closeMobileAppHub();
                    return;
                }
                if (stripMenu.style.display === 'block') {
                    hideStripMenu();
                    return;
                }
                if (addAppModal.style.display === 'block') {
                    addAppModal.style.display = 'none';
                    document.body.classList.remove('modal-open');
                    return;
                }
                if (configModal.style.display === 'block') {
                    if (previewThemeActive) {
                        applyTheme(currentTheme);
                        themeSelect.value = currentTheme;
                    }
                    configModal.style.display = 'none';
                    document.body.classList.remove('modal-open');
                    return;
                }
                if (addCategoryModal.style.display === 'block') {
                    addCategoryModal.style.display = 'none';
                    document.body.classList.remove('modal-open');
                    return;
                }
                if (editCategoryIconModal && editCategoryIconModal.style.display === 'block') {
                    editCategoryIconModal.style.display = 'none';
                    return;
                }
                if (document.getElementById('previewModal').style.display === 'block') {
                    closePreviewModal();
                    return;
                }
                if (windowsMenuVisible) {
                    toggleWindowsMenu();
                    return;
                }
                if (recycleBinModal.style.display === 'block') {
                    recycleBinModal.style.display = 'none';
                    document.body.classList.remove('modal-open');
                    return;
                }
                if (currentContextMenu) {
                    currentContextMenu.remove();
                    currentContextMenu = null;
                    return;
                }
                if (currentHoverPreview) {
                    currentHoverPreview.remove();
                    currentHoverPreview = null;
                    return;
                }
                if (windowsMosaicModal && windowsMosaicModal.style.display === 'block') {
                    windowsMosaicModal.style.display = 'none';
                    document.body.classList.remove('modal-open');
                    return;
                }
                if (topVisibleWindow) {
                    e.preventDefault();
                    showCloseWindowEscConfirm(topVisibleWindow);
                    return;
                }
                return;
            }
            
            if (e.key === 'Delete' && selectedAppIndex !== -1 && selectedAppIndex < apps.length && !apps[selectedAppIndex]?.isSystemApp) {
                e.preventDefault();
                moveToTrash(selectedAppIndex);
                return;
            }
            
            // Navegación en grid
            if (!windowsMenuVisible && addAppModal.style.display !== 'block' && configModal.style.display !== 'block' && 
                addCategoryModal.style.display !== 'block' && (!editCategoryIconModal || editCategoryIconModal.style.display !== 'block') && document.getElementById('previewModal').style.display !== 'block' && recycleBinModal.style.display !== 'block' &&
                (!document.getElementById('explorerRenameModal') || document.getElementById('explorerRenameModal').style.display !== 'block')) {
                
                if (e.key === 'F2') {
                    e.preventDefault();
                    if (selectedAppIndex !== -1 && selectedAppIndex < apps.length && apps[selectedAppIndex] && !apps[selectedAppIndex].isSystemApp) {
                        openModal(apps[selectedAppIndex], selectedAppIndex);
                    }
                    return;
                }
                if (e.key === 'F3') {
                    e.preventDefault();
                    openModal();
                    return;
                }
                if (e.key === 'F4') {
                    e.preventDefault();
                    configModal.style.display = 'block';
                    document.body.classList.add('modal-open');
                    return;
                }
                if (e.key === 'Enter' && selectedAppIndex !== -1 && selectedAppIndex < apps.length && apps[selectedAppIndex]) {
                    e.preventDefault();
                    launchDesktopApp(apps[selectedAppIndex]);
                    return;
                }
                
                // Navegación con flechas en grid (vecinos espaciales = layout real)
                if (!isMobile() && appItems.length > 0 && (e.key === 'ArrowRight' || e.key === 'ArrowLeft' || e.key === 'ArrowDown' || e.key === 'ArrowUp')) {
                    e.preventDefault();
                    let cur = null;
                    if (appItems.indexOf(document.activeElement) >= 0) {
                        cur = document.activeElement;
                    } else if (selectedGridVisibleIndex >= 0 && selectedGridVisibleIndex < appItems.length) {
                        cur = appItems[selectedGridVisibleIndex];
                    } else {
                        cur = appItems[0];
                    }
                    const navMap = buildGridKeyboardNeighbors(appItems);
                    const n = navMap.get(cur);
                    if (!n) return;
                    let next = null;
                    if (e.key === 'ArrowRight') next = n.right;
                    else if (e.key === 'ArrowLeft') next = n.left;
                    else if (e.key === 'ArrowDown') next = n.down;
                    else if (e.key === 'ArrowUp') next = n.up;
                    if (next) {
                        next.focus();
                        selectedGridVisibleIndex = appItems.indexOf(next);
                        const di = parseInt(next.dataset.index, 10);
                        if (!isNaN(di)) selectedAppIndex = di;
                    }
                }
            }
            
            // Navegación en menú Windows
            if (windowsMenuVisible && !isMobile()) {
                const activeElement = document.activeElement;
                const isInCategories = categoryItems.includes(activeElement);
                const isInApps = windowsAppItems.includes(activeElement);

                if (isInCategories || (!isInApps && categoryItems.length > 0)) {
                    if (e.key === 'ArrowUp' || e.key === 'ArrowDown') {
                        e.preventDefault();
                        let currentIndex = categoryItems.indexOf(activeElement);
                        if (currentIndex === -1) currentIndex = selectedCategoryIndex;
                        if (e.key === 'ArrowDown') {
                            currentIndex = (currentIndex + 1) % categoryItems.length;
                        } else if (e.key === 'ArrowUp') {
                            currentIndex = (currentIndex - 1 + categoryItems.length) % categoryItems.length;
                        }
                        selectedCategoryIndex = currentIndex;
                        categoryItems[currentIndex].focus();
                        categoryItems[currentIndex].click();
                    } else if (e.key === 'ArrowRight') {
                        e.preventDefault();
                        if (windowsAppItems.length > 0) {
                            selectedWindowsAppIndex = 0;
                            windowsAppItems[0].focus();
                        }
                    }
                } else if (isInApps && windowsAppItems.length > 0) {
                    const winNavMap = buildGridKeyboardNeighbors(windowsAppItems);
                    const curWin = activeElement;
                    const wn = winNavMap.get(curWin);

                    if (e.key === 'ArrowLeft') {
                        e.preventDefault();
                        if (wn && wn.left) {
                            selectedWindowsAppIndex = windowsAppItems.indexOf(wn.left);
                            wn.left.focus();
                        } else if (categoryItems.length > 0) {
                            selectedCategoryIndex = Math.min(selectedCategoryIndex, categoryItems.length - 1);
                            categoryItems[selectedCategoryIndex].focus();
                        }
                    } else if (e.key === 'ArrowRight') {
                        e.preventDefault();
                        if (wn && wn.right) {
                            selectedWindowsAppIndex = windowsAppItems.indexOf(wn.right);
                            wn.right.focus();
                        }
                    } else if (e.key === 'ArrowUp') {
                        e.preventDefault();
                        if (wn && wn.up) {
                            selectedWindowsAppIndex = windowsAppItems.indexOf(wn.up);
                            wn.up.focus();
                        }
                    } else if (e.key === 'ArrowDown') {
                        e.preventDefault();
                        if (wn && wn.down) {
                            selectedWindowsAppIndex = windowsAppItems.indexOf(wn.down);
                            wn.down.focus();
                        }
                    } else if (e.key === 'Enter') {
                        e.preventDefault();
                        const focusedApp = activeElement;
                        if (focusedApp && focusedApp.classList && focusedApp.classList.contains('app-item')) {
                            const index = parseInt(focusedApp.dataset.index, 10);
                            if (!isNaN(index) && apps[index]) {
                                launchDesktopApp(apps[index]);
                            }
                        }
                    }
                }
            }
        });

        function previewIcon(input) {
            if (input.files && input.files[0]) {
                const reader = new FileReader();
                reader.onload = e => {
                    document.getElementById('iconPreviewImg').src = e.target.result;
                    document.getElementById('iconGridPreview').style.display = 'flex';
                };
                reader.readAsDataURL(input.files[0]);
            }
        }

        async function uploadIcon(file) {
            const formData = new FormData();
            formData.append('file', file);
            const response = await fetch('?action=uploadMedia', { method: 'POST', body: formData });
            const result = await response.json();
            if (result.success) return result.url;
            else throw new Error('Error al subir el icono');
        }

        async function uploadDocument(file) {
            const formData = new FormData();
            formData.append('file', file);
            const response = await fetch('?action=uploadMedia', { method: 'POST', body: formData });
            const result = await response.json();
            if (result.success) {
                return { url: result.url, filename: result.filename, name: file.name, type: file.type };
            } else throw new Error('Error al subir el documento');
        }

        function openModal(app = null, appIndex = -1) {
            const currentCategory = activeWindowsCategory;
            const currentSelectedIndex = selectedAppIndex;
            
            modalTitle.textContent = app ? 'Editar Aplicación' : 'Agregar Aplicación';
            deleteBtn.style.display = app ? 'inline-flex' : 'none';
            appForm.reset();
            document.getElementById('appIcon').required = !app;
            document.getElementById('iconGridPreview').style.display = 'none';
            document.getElementById('iconPreviewImg').src = '';
            currentFile = null;
            editingIndex = app ? appIndex : -1;
            
            if (app && !app.isSystemApp) {
                document.getElementById('appTitle').value = app.title || '';
                document.getElementById('appCategory').value = app.category || 'General';
                document.getElementById('appDescription').value = app.description || '';
                if (app.icon) {
                    document.getElementById('iconPreviewImg').src = app.icon;
                    document.getElementById('iconGridPreview').style.display = 'flex';
                }
                document.getElementById('appLink').value = app.link || '';
                if (app.linkTarget) {
                    const targetRadio = document.querySelector(`input[name="linkTarget"][value="${app.linkTarget}"]`);
                    if (targetRadio) targetRadio.checked = true;
                } else {
                    document.querySelector('input[name="linkTarget"][value="_blank"]').checked = true;
                }
                
                if (app.explorerOpen && app.explorerOpen.bucket) {
                    document.querySelector('input[name="actionType"][value="link"]').checked = true;
                    linkInput.style.display = 'none';
                    fileInput.style.display = 'none';
                    document.getElementById('linkTargetSelector').style.display = 'none';
                    appLinkInput.value = '';
                    currentFile = null;
                    currentFileName.textContent = `${tr('explorerShortcutFolderHint')}: ${app.explorerOpen.bucket}${app.explorerOpen.relPath ? ' / ' + app.explorerOpen.relPath : ''}`;
                    currentFileInfo.style.display = 'flex';
                    document.getElementById('appIcon').required = false;
                } else if (app.lastAction === 'file') {
                    document.querySelector('input[name="actionType"][value="file"]').checked = true;
                    linkInput.style.display = 'none';
                    fileInput.style.display = 'block';
                    document.getElementById('linkTargetSelector').style.display = 'none';
                    appFileInput.required = false;
                    if (app.fileInfo) {
                        currentFile = app.fileInfo;
                        currentFileName.textContent = `Archivo: ${app.fileInfo.name}`;
                        currentFileInfo.style.display = 'flex';
                    }
                } else {
                    document.querySelector('input[name="actionType"][value="link"]').checked = true;
                    linkInput.style.display = 'block';
                    fileInput.style.display = 'none';
                    document.getElementById('linkTargetSelector').style.display = 'block';
                    appLinkInput.value = app.link || '';
                    updateLinkPreviewButton();
                }
                showInGridCheckbox.checked = app.showInGrid !== false;
            } else {
                document.querySelector('input[name="actionType"][value="link"]').checked = true;
                linkInput.style.display = 'block';
                fileInput.style.display = 'none';
                document.getElementById('linkTargetSelector').style.display = 'block';
                document.querySelector('input[name="linkTarget"][value="_blank"]').checked = true;
                showInGridCheckbox.checked = true;
            }
            
            addAppModal.style.display = 'block';
            document.body.classList.add('modal-open');
            setTimeout(() => document.getElementById('appTitle').focus(), 100);
            
            window.__pendingRestore = { category: currentCategory, selectedIndex: currentSelectedIndex };
        }

        appForm.addEventListener('submit', async (e) => {
            e.preventDefault();
            const iconInput = document.getElementById('appIcon');
            const title = document.getElementById('appTitle').value.trim();
            const actionType = document.querySelector('input[name="actionType"]:checked').value;
            const link = document.getElementById('appLink').value.trim();
            const category = document.getElementById('appCategory').value;
            const description = document.getElementById('appDescription').value.trim();
            const linkTarget = document.querySelector('input[name="linkTarget"]:checked')?.value || '_blank';
            const showInGrid = showInGridCheckbox.checked;

            if (!title) { await appDialog.alert('Por favor, ingrese un título.'); return; }

            try {
                let iconUrl = editingIndex > -1 && apps[editingIndex] && apps[editingIndex].icon ? apps[editingIndex].icon : 'data:image/svg+xml,%3Csvg xmlns=\'http://www.w3.org/2000/svg\' viewBox=\'0 0 24 24\'%3E%3Cpath fill=\'%23999\' d=\'M4 6h16v2H4V6zm0 4h16v2H4v-2zm0 4h16v2H4v-2z\'/%3E%3C/svg%3E';
                if (iconInput.files.length > 0) {
                    iconUrl = await uploadIcon(iconInput.files[0]);
                }

                const app = {
                    icon: iconUrl,
                    title,
                    category,
                    description,
                    link: link,
                    linkTarget: linkTarget,
                    lastAction: actionType,
                    showInGrid: showInGrid
                };

                if (actionType === 'file') {
                    if (appFileInput.files.length > 0) {
                        const newFile = appFileInput.files[0];
                        const uploadedFile = await uploadDocument(newFile);
                        app.fileInfo = {
                            name: newFile.name,
                            type: newFile.type,
                            url: uploadedFile.url,
                            filename: uploadedFile.filename
                        };
                    } else if (currentFile) {
                        app.fileInfo = currentFile;
                    }
                } else {
                    if (editingIndex > -1 && apps[editingIndex] && apps[editingIndex].fileInfo) {
                        app.fileInfo = apps[editingIndex].fileInfo;
                    }
                }

                const prevExplorerOpen = editingIndex > -1 && apps[editingIndex] && apps[editingIndex].explorerOpen && apps[editingIndex].explorerOpen.bucket
                    ? apps[editingIndex].explorerOpen
                    : null;
                if (prevExplorerOpen) {
                    app.explorerOpen = { bucket: prevExplorerOpen.bucket, relPath: prevExplorerOpen.relPath || '' };
                    app.lastAction = 'openExplorer';
                    app.link = '#';
                    app.linkTarget = '_self';
                    delete app.fileInfo;
                    app.libraryExplorerShortcut = true;
                } else if (editingIndex > -1 && apps[editingIndex] && apps[editingIndex].libraryExplorerShortcut) {
                    app.libraryExplorerShortcut = true;
                }

                let sidMigrated = false;
                if (editingIndex > -1 && apps[editingIndex] && !apps[editingIndex].isSystemApp) {
                    const oldApp = apps[editingIndex];
                    const oldSid = getAppStableId(oldApp);
                    const newSid = getAppStableId(app);
                    if (oldSid !== newSid) {
                        sidMigrated = true;
                        if (launcherLayout && window.GridLauncher && GridLauncher.layoutModel && typeof GridLauncher.layoutModel.remapAppStableId === 'function') {
                            launcherLayout = GridLauncher.layoutModel.remapAppStableId(launcherLayout, oldSid, newSid);
                        }
                        pinnedApps.forEach(function (p) {
                            if (pinMatchesApp(p, oldApp)) {
                                p.title = app.title;
                                p.link = getAppLinkKey(app);
                                if (app.icon) p.icon = app.icon;
                            }
                        });
                        activeTaskbarApps.forEach(function (g) {
                            if (g.appId === oldSid) {
                                g.appId = newSid;
                                g.title = app.title;
                                if (app.icon) g.icon = app.icon;
                            }
                        });
                        taskbarOpenOrder = taskbarOpenOrder.map(function (id) {
                            return id === oldSid ? newSid : id;
                        });
                        taskbarOpenOrder = taskbarOpenOrder.filter(function (id, i, arr) {
                            return arr.indexOf(id) === i;
                        });
                        if (Array.isArray(desktopShortcuts)) {
                            const oldLk = getAppLinkKey(oldApp);
                            desktopShortcuts.forEach(function (s) {
                                if ((s.title || '') === (oldApp.title || '') && (s.link || '') === (oldLk || '')) {
                                    s.title = app.title;
                                    s.link = getAppLinkKey(app);
                                    if (s.appId) s.appId = newSid;
                                }
                            });
                        }
                    }
                    apps[editingIndex] = app;
                } else {
                    apps.push(app);
                    if (window.GridLauncher && window.GridLauncher.layoutModel && launcherLayout) {
                        launcherLayout = GridLauncher.layoutModel.appendAppToOrder(launcherLayout, getAppStableId(app), getAppStableId);
                    }
                }

                const savePatch = { apps: apps, launcherLayout: launcherLayout };
                if (sidMigrated) {
                    savePatch.pinnedApps = pinnedApps;
                    savePatch.taskbarOpenOrder = taskbarOpenOrder;
                    savePatch.desktopShortcuts = desktopShortcuts;
                }
                await saveSettings(savePatch);
                renderApps();
                if (windowsMenuVisible) renderWindowsMenu(apps);
                addAppModal.style.display = 'none';
                document.body.classList.remove('modal-open');
                if (window.__pendingRestore && window.__pendingRestore.category) {
                    activeWindowsCategory = window.__pendingRestore.category;
                    if (windowsMenuVisible) renderWindowsMenu(apps);
                }
                await appDialog.alert(editingIndex > -1 ? 'Aplicación actualizada.' : 'Aplicación añadida.');
            } catch (error) {
                console.error('Error:', error);
                await appDialog.alert(`Error: ${error.message}`);
            }
        });

        document.getElementById('cancelAddAppBtn').addEventListener('click', () => {
            addAppModal.style.display = 'none';
            document.body.classList.remove('modal-open');
            if (window.__pendingRestore && window.__pendingRestore.category) {
                activeWindowsCategory = window.__pendingRestore.category;
                if (windowsMenuVisible) renderWindowsMenu(apps);
            }
        });

        configBtn.addEventListener('click', () => {
            currentTheme = getSetting('selectedTheme', 'neumorphism');
            
            document.getElementById('mosaicColor').value = getSetting('mosaicColor', '#000000');
            fontSizeInput.value = getSetting('selectedFontSize', '16').toString().replace('px', '');
            textColorPicker.value = getSetting('textColor', '#000000');
            gridTextColorPicker.value = getSetting('gridTextColor', '#000000');
            themeSelect.value = currentTheme;
            fontSelect.value = getSetting('selectedFont', 'Arial, sans-serif');
            
            initThemePalettePanel();
            fillThemePaletteInputs();
            updateThemePaletteSectionVisibility();
            
            const bgType = getSetting('backgroundType', 'file');
            useYoutubeRadio.checked = (bgType === 'youtube');
            useFileRadio.checked = (bgType === 'file');
            useImagesRadio.checked = (bgType === 'images');
            useVideosRadio.checked = (bgType === 'videos');
            
            youtubeLinkInput.value = getSetting('youtubeBackground', '');
            youtubeLinkMobileInput.value = getSetting('youtubeBackgroundMobile', '');
            
            renderYoutubePlaylist('pc', getSetting('youtubePlaylistPC', []));
            renderYoutubePlaylist('mobile', getSetting('youtubePlaylistMobile', []));
            document.getElementById('usePlaylist').checked = getSetting('usePlaylist', false);
            document.querySelectorAll('input[name="youtubePlayMode"]').forEach(r => {
                r.checked = (r.value === getSetting('youtubePlayMode', 'random'));
            });
            
            renderVideoList();
            renderImageList();
            
            slideshowEnabledCheck.checked = getSetting('slideshowEnabled', false);
            slideshowIntervalInput.value = getSetting('slideshowInterval', 5);
            slideshowIntervalGroup.style.display = slideshowEnabledCheck.checked ? 'block' : 'none';
            
            document.querySelectorAll('input[name="videoPlayMode"]').forEach(r => {
                r.checked = (r.value === getSetting('videoPlayMode', 'random'));
            });
            
            const desktopMode = getSetting('desktopMode', 'grid');
            document.querySelectorAll('input[name="desktopMode"]').forEach(r => {
                r.checked = (r.value === desktopMode);
            });
            
            const taskbarPosition = getSetting('taskbarPosition', 'bottom');
            document.querySelectorAll('input[name="taskbarPosition"]').forEach(r => {
                r.checked = (r.value === taskbarPosition);
            });
            
            if (mobileDesktopModeCheck) mobileDesktopModeCheck.checked = getSetting('mobileDesktopMode', false);
            
            if (showTypeBadgeCheck) showTypeBadgeCheck.checked = getSetting('showTypeBadge', true);
            const badgeMode = getSetting('typeBadgeMode', 'icon');
            typeBadgeModeRadios.forEach(r => {
                r.checked = (r.value === badgeMode);
            });
            
            if (inspectBlockCheck) inspectBlockCheck.checked = getSetting('inspectBlock', true);
            
            toggleBackgroundInputs();
            updatePreviewFromSettings();
            updateActionButtonsVisibility();
            
            previewThemeActive = false;
            document.getElementById('themePreviewNotice').style.display = 'none';
            document.getElementById('restoreThemeBtn').style.display = 'none';

            try { refreshIntegrationsTab(); } catch (_) {}

            configModal.style.display = 'block';
            document.body.classList.add('modal-open');
        });

        function flexiaSpotifyRedirectUri() {
            try {
                return new URL('spotify-callback.php', document.baseURI).toString();
            } catch (_) {
                try { return window.location.origin + '/spotify-callback.php'; } catch (__) { return ''; }
            }
        }

        function refreshIntegrationsTab() {
            const integrations = getSetting('integrations', {}) || {};
            const sp = (integrations && integrations.spotify) ? integrations.spotify : {};
            const idInp = document.getElementById('spotifyClientId');
            const redirInp = document.getElementById('spotifyRedirectUri');
            const statusEl = document.getElementById('spotifyIntegrationStatus');
            const testBtn = document.getElementById('spotifyIntegrationTestBtn');
            const msg = document.getElementById('spotifyIntegrationMsg');
            const setup = document.getElementById('spotifyIntegrationSetup');
            const redirComputed = flexiaSpotifyRedirectUri();
            if (idInp) idInp.value = sp.clientId || '';
            if (redirInp) redirInp.value = redirComputed;
            const hasClientId = !!(sp.clientId && String(sp.clientId).trim());
            const isVerified = !!(sp.connectionVerified === true);
            if (statusEl) {
                statusEl.classList.remove('integration-card__status--ok', 'integration-card__status--idle', 'integration-card__status--err');
                if (hasClientId && isVerified) {
                    statusEl.textContent = 'Conectada';
                    statusEl.classList.add('integration-card__status--ok');
                } else if (hasClientId) {
                    statusEl.textContent = 'Pendiente de verificación';
                    statusEl.classList.add('integration-card__status--idle');
                } else {
                    statusEl.textContent = 'No configurada';
                    statusEl.classList.add('integration-card__status--idle');
                }
            }
            if (testBtn) testBtn.disabled = !hasClientId;
            if (msg) msg.textContent = '';
            if (setup) {
                const setupWasHidden = setup.hidden;
                setup.hidden = hasClientId;
                if (!hasClientId && setupWasHidden && typeof window.__flexiaResetSpotifySetupFlow === 'function') {
                    window.__flexiaResetSpotifySetupFlow(true);
                }
            }
        }
        // Exponer al closure exterior para el listener genérico de tabs.
        window.__flexiaRefreshIntegrationsTab = refreshIntegrationsTab;

        window.__flexiaOpenConfigIntegrationsTab = function () {
            const configModal = document.getElementById('configModal');
            const configBtn = document.getElementById('configBtn');
            const integrationsTabBtn = document.getElementById('integrationsTabBtn');
            const isOpen = !!(configModal && (configModal.style.display === 'block'));
            if (!isOpen && configBtn) configBtn.click();
            window.setTimeout(function () {
                if (integrationsTabBtn) integrationsTabBtn.click();
            }, isOpen ? 0 : 60);
        };

        const spotifyClientIdInput = document.getElementById('spotifyClientId');
        const spotifyRedirectInput = document.getElementById('spotifyRedirectUri');
        const spotifySaveBtn = document.getElementById('spotifyIntegrationSaveBtn');
        const spotifyTestBtn = document.getElementById('spotifyIntegrationTestBtn');
        const spotifyCopyBtn = document.getElementById('spotifyRedirectUriCopyBtn');
        const spotifyMsg = document.getElementById('spotifyIntegrationMsg');
        const spotifySetupQuestion = document.getElementById('spotifyIntegrationSetupQuestion');
        const spotifySetupExistingPath = document.getElementById('spotifySetupExistingPath');
        const spotifySetupNewPath = document.getElementById('spotifySetupNewPath');
        const spotifyHasClientIdYes = document.getElementById('spotifyHasClientIdYes');
        const spotifyHasClientIdNo = document.getElementById('spotifyHasClientIdNo');
        const spotifySetupBackFromExisting = document.getElementById('spotifySetupBackFromExisting');
        const spotifySetupBackFromNew = document.getElementById('spotifySetupBackFromNew');
        const SPOTIFY_DASHBOARD_URL = 'https://developer.spotify.com/dashboard';

        function resetSpotifySetupFlow(forceQuestion) {
            if (!spotifySetupQuestion || !spotifySetupExistingPath || !spotifySetupNewPath) return;
            if (forceQuestion) {
                spotifySetupQuestion.hidden = false;
                spotifySetupExistingPath.hidden = true;
                spotifySetupNewPath.hidden = true;
            }
        }
        window.__flexiaResetSpotifySetupFlow = resetSpotifySetupFlow;

        function showSpotifySetupPath(path) {
            if (!spotifySetupQuestion || !spotifySetupExistingPath || !spotifySetupNewPath) return;
            spotifySetupQuestion.hidden = true;
            spotifySetupExistingPath.hidden = path !== 'existing';
            spotifySetupNewPath.hidden = path !== 'new';
        }

        if (spotifyHasClientIdYes) {
            spotifyHasClientIdYes.addEventListener('click', () => {
                showSpotifySetupPath('existing');
                try {
                    window.open(SPOTIFY_DASHBOARD_URL, '_blank', 'noopener,noreferrer');
                } catch (_) {}
            });
        }
        if (spotifyHasClientIdNo) {
            spotifyHasClientIdNo.addEventListener('click', () => {
                showSpotifySetupPath('new');
            });
        }
        if (spotifySetupBackFromExisting) {
            spotifySetupBackFromExisting.addEventListener('click', () => resetSpotifySetupFlow(true));
        }
        if (spotifySetupBackFromNew) {
            spotifySetupBackFromNew.addEventListener('click', () => resetSpotifySetupFlow(true));
        }

        if (spotifyCopyBtn && spotifyRedirectInput) {
            spotifyCopyBtn.addEventListener('click', async () => {
                const txt = spotifyRedirectInput.value || '';
                try {
                    if (navigator.clipboard && navigator.clipboard.writeText) {
                        await navigator.clipboard.writeText(txt);
                    } else {
                        spotifyRedirectInput.select();
                        document.execCommand('copy');
                    }
                    if (spotifyMsg) {
                        spotifyMsg.textContent = 'Redirect URI copiada al portapapeles.';
                        spotifyMsg.className = 'integration-card__msg integration-card__msg--ok';
                    }
                } catch (e) {
                    if (spotifyMsg) {
                        spotifyMsg.textContent = 'No se pudo copiar: ' + (e && e.message ? e.message : e);
                        spotifyMsg.className = 'integration-card__msg integration-card__msg--err';
                    }
                }
            });
        }

        function runSpotifyConnectionTest() {
            const integrations = getSetting('integrations', {}) || {};
            const sp = (integrations && integrations.spotify) ? integrations.spotify : {};
            if (!sp.clientId) {
                if (spotifyMsg) {
                    spotifyMsg.textContent = 'Guarda primero un Client ID válido.';
                    spotifyMsg.className = 'integration-card__msg integration-card__msg--err';
                }
                return false;
            }
            if (spotifyMsg) {
                spotifyMsg.textContent = 'Abriendo Spotify para autorizar… completa el flujo en la ventana emergente.';
                spotifyMsg.className = 'integration-card__msg';
            }
            if (window.OsShell && typeof window.OsShell.startSpotifyAuth === 'function') {
                window.OsShell.startSpotifyAuth();
            }
            return true;
        }

        if (spotifySaveBtn) {
            spotifySaveBtn.addEventListener('click', async () => {
                const newId = (spotifyClientIdInput && spotifyClientIdInput.value || '').trim();
                const integrations = Object.assign({}, getSetting('integrations', {}) || {});
                const prevSp = (integrations && integrations.spotify) ? integrations.spotify : {};
                const prevId = String(prevSp.clientId || '').trim();
                const keepVerified = newId !== '' && newId === prevId && prevSp.connectionVerified === true;
                integrations.spotify = Object.assign({}, prevSp, {
                    clientId: newId,
                    redirectUri: flexiaSpotifyRedirectUri(),
                    connectionVerified: keepVerified
                });
                if (spotifyMsg) {
                    spotifyMsg.textContent = 'Guardando…';
                    spotifyMsg.className = 'integration-card__msg';
                }
                try {
                    const res = await saveSettings({ integrations: integrations });
                    if (res && res.success) {
                        refreshIntegrationsTab();
                        try { if (window.OsShell && window.OsShell.refreshSpotify) window.OsShell.refreshSpotify(true); } catch (_) {}
                        if (!newId) {
                            if (spotifyMsg) {
                                spotifyMsg.textContent = 'Configuración borrada.';
                                spotifyMsg.className = 'integration-card__msg integration-card__msg--ok';
                            }
                        } else {
                            if (spotifyMsg) {
                                spotifyMsg.textContent = 'Client ID guardado. Abriendo Spotify para autorizar…';
                                spotifyMsg.className = 'integration-card__msg integration-card__msg--ok';
                            }
                            runSpotifyConnectionTest();
                        }
                    } else {
                        if (spotifyMsg) {
                            spotifyMsg.textContent = 'No se pudo guardar la configuración.';
                            spotifyMsg.className = 'integration-card__msg integration-card__msg--err';
                        }
                    }
                } catch (e) {
                    if (spotifyMsg) {
                        spotifyMsg.textContent = 'Error: ' + (e && e.message ? e.message : e);
                        spotifyMsg.className = 'integration-card__msg integration-card__msg--err';
                    }
                }
            });
        }

        if (spotifyTestBtn) {
            spotifyTestBtn.addEventListener('click', () => {
                runSpotifyConnectionTest();
            });
        }

        fontSelect.addEventListener('change', function() {
            document.body.style.fontFamily = this.value;
        });
        fontSizeInput.addEventListener('input', function() {
            document.body.style.fontSize = this.value + 'px';
        });

        function toggleBackgroundInputs() {
            youtubeInputGroup.style.display = useYoutubeRadio.checked ? 'block' : 'none';
            fileInputGroup.style.display = useFileRadio.checked ? 'flex' : 'none';
            imagesInputGroup.style.display = useImagesRadio.checked ? 'block' : 'none';
            videosInputGroup.style.display = useVideosRadio.checked ? 'block' : 'none';
            
            if (!useYoutubeRadio.checked) {
                previewContainer.innerHTML = '';
                youtubeFeedback.textContent = '';
            }
            
            if (useYoutubeRadio.checked) updateYoutubePreview();
            else if (useFileRadio.checked && tempSelectedFile) previewBackground(bgFileInput);
            else if (useFileRadio.checked) updateFilePreviewFromSettings();
            else if (useImagesRadio.checked) updateImagesPreview();
            else if (useVideosRadio.checked) updateVideosPreview();
        }

        useYoutubeRadio.addEventListener('change', toggleBackgroundInputs);
        useFileRadio.addEventListener('change', toggleBackgroundInputs);
        useImagesRadio.addEventListener('change', toggleBackgroundInputs);
        useVideosRadio.addEventListener('change', toggleBackgroundInputs);

        function updateYoutubePreview() {
            const url = isMobile() ? youtubeLinkMobileInput.value : youtubeLinkInput.value;
            const embedSrc = buildYoutubeWallpaperEmbedUrl(url);
            if (embedSrc) {
                const yt = window.GridYoutubeUrl;
                const html = yt && yt.youtubeWallpaperIframeHtml
                    ? yt.youtubeWallpaperIframeHtml(embedSrc, 'id="youtubePreview" style="width:100%; height:100%;"')
                    : `<iframe id="youtubePreview" src="${embedSrc}" referrerpolicy="strict-origin-when-cross-origin" frameborder="0" allow="autoplay; fullscreen; encrypted-media" allowfullscreen style="width:100%; height:100%;"></iframe>`;
                previewContainer.innerHTML = html;
                youtubeFeedback.textContent = 'Vista previa cargada.';
            } else {
                youtubeFeedback.textContent = 'Enlace inválido (use watch, Shorts o youtu.be).';
                previewContainer.innerHTML = '';
            }
        }

        function updateImagesPreview() {
            const images = getSetting('backgroundImages', []);
            previewContainer.innerHTML = images.length
                ? `<img src="${escapeHtmlAttr(resolveWallpaperMediaUrl(images[0]))}" style="width:100%; height:100%; object-fit:cover;">`
                : '';
        }

        function updateVideosPreview() {
            const videos = getSetting('backgroundVideos', []);
            previewContainer.innerHTML = videos.length
                ? `<video src="${escapeHtmlAttr(resolveWallpaperMediaUrl(videos[0]))}" muted autoplay loop playsinline style="width:100%; height:100%; object-fit:cover;"></video>`
                : '';
        }

        function updateFilePreviewFromSettings() {
            const bgUrl = getSetting('backgroundUrl', '');
            if (!bgUrl) {
                if (!tempSelectedFile) previewContainer.innerHTML = '';
                return;
            }
            const displayUrl = resolveWallpaperMediaUrl(bgUrl);
            if (/\.(jpe?g|png|gif|webp)(\?.*)?$/i.test(bgUrl)) {
                previewContainer.innerHTML = `<img src="${escapeHtmlAttr(displayUrl)}" style="width:100%; height:100%; object-fit:cover;">`;
            } else if (/\.(mp4|webm|ogg)(\?.*)?$/i.test(bgUrl)) {
                previewContainer.innerHTML = `<video src="${escapeHtmlAttr(displayUrl)}" muted autoplay loop playsinline style="width:100%; height:100%; object-fit:cover;"></video>`;
            } else {
                previewContainer.innerHTML = '';
            }
        }

        slideshowEnabledCheck.addEventListener('change', () => {
            slideshowIntervalGroup.style.display = slideshowEnabledCheck.checked ? 'block' : 'none';
        });

        function renderYoutubePlaylist(type, urls) {
            const container = document.getElementById(`youtubePlaylist${type === 'pc' ? 'PC' : 'Mobile'}Container`);
            if (!container) return;
            container.innerHTML = '';
            const uniqueUrls = [...new Set(urls)];
            uniqueUrls.forEach(url => {
                const videoId = extractYoutubeId(url);
                const thumbnail = videoId ? `https://img.youtube.com/vi/${videoId}/0.jpg` : '';
                const card = document.createElement('div');
                card.className = 'media-item';
                card.dataset.url = url;
                card.onclick = () => window.open(url, '_blank');
                card.innerHTML = `
                    <div class="media-thumb" style="background-image: url('${thumbnail}');">
                        ${!thumbnail ? '<i class="fab fa-youtube"></i>' : ''}
                    </div>
                    <span>${url.length > 30 ? url.substr(0,30)+'...' : url}</span>
                    <button type="button" onclick="event.stopPropagation(); removeYoutubeUrl('${type}', '${url.replace(/'/g, "\\'")}')"><i class="fas fa-trash"></i></button>
                `;
                container.appendChild(card);
            });
        }

        window.addYoutubeUrl = async function(type) {
            const input = document.getElementById(`newYoutube${type === 'pc' ? 'PC' : 'Mobile'}`);
            const url = input.value.trim();
            if (!url) return;
            if (!isValidYoutubeUrl(url)) {
                await appDialog.alert('URL de YouTube no válida');
                return;
            }
            const key = type === 'pc' ? 'youtubePlaylistPC' : 'youtubePlaylistMobile';
            let list = getSetting(key, []);
            if (list.includes(url)) {
                await appDialog.alert('Esta URL ya está en la lista');
                return;
            }
            list.push(url);
            settings[key] = list;
            renderYoutubePlaylist(type, list);
            input.value = '';
        };

        window.removeYoutubeUrl = function(type, url) {
            const key = type === 'pc' ? 'youtubePlaylistPC' : 'youtubePlaylistMobile';
            let list = getSetting(key, []);
            list = list.filter(u => u !== url);
            settings[key] = list;
            renderYoutubePlaylist(type, list);
        };

        function renderImageList() {
            imageListContainer.innerHTML = '';
            (getSetting('backgroundImages', [])).forEach(url => {
                const item = document.createElement('div');
                item.className = 'media-item';
                item.dataset.url = url;
                const safeUrl = String(url).replace(/'/g, "\\'");
                item.innerHTML = `
                    <img src="${escapeHtmlAttr(resolveWallpaperMediaUrl(url))}" alt="preview">
                    <span>${escapeHtml(basename(url))}</span>
                    <button type="button" onclick="removeMedia('${safeUrl}', 'images')"><i class="fas fa-trash"></i></button>
                `;
                imageListContainer.appendChild(item);
            });
        }

        function renderVideoList() {
            videoListContainer.innerHTML = '';
            (getSetting('backgroundVideos', [])).forEach(url => {
                const item = document.createElement('div');
                item.className = 'media-item';
                item.dataset.url = url;
                const safeUrl = String(url).replace(/'/g, "\\'");
                item.innerHTML = `
                    <video src="${escapeHtmlAttr(resolveWallpaperMediaUrl(url))}" muted></video>
                    <span>${escapeHtml(basename(url))}</span>
                    <button type="button" onclick="removeMedia('${safeUrl}', 'videos')"><i class="fas fa-trash"></i></button>
                `;
                videoListContainer.appendChild(item);
            });
        }

        function basename(str) {
            return str.split('/').pop();
        }

        window.removeMedia = async function(url, type) {
            const explorerManaged = isExplorerManagedWallpaperPath(url);
            const confirmMsg = explorerManaged
                ? tr('wallpaperRemoveFromListConfirm')
                : '¿Eliminar este archivo de la lista y del servidor?';
            if (await appDialog.confirm(confirmMsg)) {
                if (!explorerManaged) {
                    const result = await deleteMediaFile({ url: url });
                    if (!result.success) {
                        await appDialog.alert('Error al eliminar el archivo.');
                        return;
                    }
                }
                document.querySelectorAll(`.media-item[data-url="${url}"]`).forEach(el => el.remove());
                if (type === 'images') {
                    let images = getSetting('backgroundImages', []);
                    images = images.filter(u => u !== url);
                    settings.backgroundImages = images;
                    renderImageList();
                } else if (type === 'videos') {
                    let videos = getSetting('backgroundVideos', []);
                    videos = videos.filter(u => u !== url);
                    settings.backgroundVideos = videos;
                    renderVideoList();
                }
                await saveSettings({ backgroundImages: settings.backgroundImages, backgroundVideos: settings.backgroundVideos });
            }
        };

        imageFilesInput.addEventListener('change', async function() {
            const files = Array.from(this.files);
            for (const file of files) {
                const formData = new FormData();
                formData.append('file', file);
                try {
                    const response = await fetch('?action=uploadMedia', { method: 'POST', body: formData });
                    const result = await response.json();
                    if (result.success) {
                        let images = getSetting('backgroundImages', []);
                        images.push(result.url);
                        settings.backgroundImages = images;
                        renderImageList();
                    }
                } catch (error) {
                    console.error('Error subiendo imagen:', error);
                }
            }
            this.value = '';
        });

        videoFilesInput.addEventListener('change', async function() {
            const files = Array.from(this.files);
            for (const file of files) {
                const formData = new FormData();
                formData.append('file', file);
                try {
                    const response = await fetch('?action=uploadMedia', { method: 'POST', body: formData });
                    const result = await response.json();
                    if (result.success) {
                        let videos = getSetting('backgroundVideos', []);
                        videos.push(result.url);
                        settings.backgroundVideos = videos;
                        renderVideoList();
                    }
                } catch (error) {
                    console.error('Error subiendo video:', error);
                }
            }
            this.value = '';
        });

        bgFileInput.addEventListener('change', function() {
            if (this.files.length > 0) {
                useFileRadio.checked = true;
                toggleBackgroundInputs();
                const file = this.files[0];
                tempSelectedFile = file;
                const progressDiv = document.getElementById('bgUploadProgress');
                const progressBar = document.getElementById('bgProgressBar');
                const progressLabel = document.getElementById('bgProgressLabel');
                progressDiv.style.display = 'block';
                progressBar.style.width = '0%';
                progressLabel.textContent = '0%';
                const reader = new FileReader();
                reader.onprogress = (ev) => {
                    if (ev.lengthComputable) {
                        const percent = Math.round((ev.loaded / ev.total) * 100);
                        progressBar.style.width = percent + '%';
                        progressLabel.textContent = percent + '%';
                    }
                };
                reader.onload = async (e) => {
                    progressBar.style.width = '100%';
                    progressLabel.textContent = '100% - Procesando...';
                    previewContainer.innerHTML = '';
                    if (file.type.startsWith('image/') || file.name.endsWith('.webp')) {
                        const img = document.createElement('img');
                        img.src = e.target.result;
                        img.style.width = '100%';
                        img.style.height = '100%';
                        img.style.objectFit = 'cover';
                        previewContainer.appendChild(img);
                    } else if (file.type.startsWith('video/')) {
                        const video = document.createElement('video');
                        video.src = e.target.result;
                        video.style.width = '100%';
                        video.style.height = '100%';
                        video.style.objectFit = 'cover';
                        video.controls = true;
                        video.autoplay = true;
                        video.muted = true;
                        video.loop = true;
                        previewContainer.appendChild(video);
                    }
                    progressLabel.textContent = '✔ Listo';
                    setTimeout(() => progressDiv.style.display = 'none', 2000);
                };
                reader.readAsDataURL(file);
            }
        });

        async function uploadAndApplyFileBackground(file) {
            const formData = new FormData();
            formData.append('file', file);
            const response = await fetch('?action=uploadMedia', { method: 'POST', body: formData });
            const result = await response.json();
            if (result.success) {
                applyBackgroundFromUrl(result.url);
                saveSettings({ backgroundUrl: result.url });
            }
        }

        const ytUrl = () => window.GridYoutubeUrl || {};

        function isValidYoutubeUrl(url) {
            return ytUrl().isValidYoutubeUrl ? ytUrl().isValidYoutubeUrl(url) : false;
        }

        function extractYoutubeId(url) {
            return ytUrl().extractYoutubeId ? ytUrl().extractYoutubeId(url) : null;
        }

        function buildYoutubeWallpaperEmbedUrl(urlOrId) {
            return ytUrl().buildYoutubeWallpaperEmbedUrl
                ? ytUrl().buildYoutubeWallpaperEmbedUrl(urlOrId)
                : null;
        }

        function applyYoutubeBackground(url) {
            if (!url) return;
            const embedSrc = buildYoutubeWallpaperEmbedUrl(url);
            if (embedSrc) {
                removeExistingBackground();
                const iframe = document.createElement('iframe');
                iframe.id = 'youtubeBackground';
                iframe.className = 'video-background';
                iframe.src = embedSrc;
                if (ytUrl().applyYoutubeWallpaperIframeAttrs) {
                    ytUrl().applyYoutubeWallpaperIframeAttrs(iframe);
                } else {
                    iframe.setAttribute('referrerpolicy', 'strict-origin-when-cross-origin');
                    iframe.setAttribute('frameborder', '0');
                    iframe.allow = 'autoplay; fullscreen; encrypted-media';
                }
                document.body.insertBefore(iframe, document.body.firstChild);
                document.body.style.backgroundImage = 'none';
                updateActionButtons('youtube', url);
                settings.currentYoutubeUrl = url;
                updateActionButtonsVisibility();
                try { syncAuroraCrystal3dBackdrop(); } catch (_) {}
            }
        }

        function applyBackgroundFromUrl(url) {
            const displayUrl = resolveWallpaperMediaUrl(url);
            removeExistingBackground();
            if (url.match(/\.(jpg|jpeg|png|gif|webp)(\?.*)?$/i)) {
                document.body.style.backgroundImage = `url(${displayUrl})`;
                document.body.style.backgroundSize = 'cover';
                document.body.style.backgroundPosition = 'center';
                document.body.style.backgroundAttachment = 'fixed';
                updateActionButtons('image');
            } else if (url.match(/\.(mp4|webm|ogg)(\?.*)?$/i)) {
                document.body.style.backgroundImage = 'none';
                const video = document.createElement('video');
                video.className = 'video-background';
                video.src = displayUrl;
                video.autoplay = true;
                video.loop = true;
                video.muted = true;
                video.playsInline = true;
                document.body.insertBefore(video, document.body.firstChild);
                updateActionButtons('video');
            }
            updateActionButtonsVisibility();
            try { syncAuroraCrystal3dBackdrop(); } catch (_) {}
        }

        function removeExistingBackground() {
            const oldYoutube = document.getElementById('youtubeBackground');
            if (oldYoutube) oldYoutube.remove();
            const oldVideo = document.querySelector('.video-background');
            if (oldVideo) oldVideo.remove();
            document.body.style.backgroundImage = '';
            if (slideshowTimer) {
                clearInterval(slideshowTimer);
                slideshowTimer = null;
            }
            try { updateAuroraCrystal3dLayer(); } catch (_) {}
        }

        function applyStoredConfigurations() {
            const bgType = getSetting('backgroundType', 'file');
            const bgUrl = getSetting('backgroundUrl', '');
            const images = getSetting('backgroundImages', []);
            const videos = getSetting('backgroundVideos', []);
            const slideshow = getSetting('slideshowEnabled', false);
            
            if (bgType === 'youtube') {
                applyVideoBackground();
            } else if (bgType === 'file' && bgUrl) {
                applyBackgroundFromUrl(bgUrl);
            } else if (bgType === 'images' && images.length > 0) {
                applyImagesBackground(images, slideshow);
            } else if (bgType === 'videos' && videos.length > 0) {
                applyVideosBackground(videos);
            }
            
            const mosaicColor = getSetting('mosaicColor', '#000000');
            document.documentElement.style.setProperty('--mosaic-bg-color', 
                `rgba(${parseInt(mosaicColor.slice(1,3),16)}, ${parseInt(mosaicColor.slice(3,5),16)}, ${parseInt(mosaicColor.slice(5,7),16)}, 0.3)`);
            
            const savedTheme = getSetting('selectedTheme', 'neumorphism');
            const savedFont = getSetting('selectedFont', 'Arial, sans-serif');
            const savedFontSize = getSetting('selectedFontSize', '16');
            const savedTextColor = getSetting('textColor', '#000000');
            const savedGridTextColor = getSetting('gridTextColor', '#000000');
            
            // Preservar clases de layout (mobile/taskbar) y solo cambiar la clase de tema.
            const themeClasses = ['neumorphism', 'glassmorphism', 'skeuomorphism', 'flat', 'material', 'brutalism', 'retro', 'dark', 'minimalism', 'morphing', 'isometric', 'cyberpunk', 'gradient', 'glassgradient', 'auroracrystal3d', 'card', 'typography'];
            document.body.classList.remove(...themeClasses);
            document.body.classList.add(savedTheme);
            document.body.style.fontFamily = savedFont;
            document.body.style.fontSize = savedFontSize + 'px';
            document.documentElement.style.setProperty('--text-color', savedTextColor);
            document.documentElement.style.setProperty('--grid-text-color', savedGridTextColor);
            
            currentTheme = savedTheme;
            applyTaskbarPosition(getSetting('taskbarPosition', 'bottom'));
            updateStartButtonIcon(getSetting('startButtonIcon', ''));
            updateMobileMode(getSetting('mobileDesktopMode', false));
            updateActionButtonsVisibility();
            // Zoom persistente por dispositivo (grid + iframe).
            const storedGridZoom = localStorage.getItem(gridZoomStorageKey);
            const storedIframeZoom = localStorage.getItem(iframeZoomStorageKey);
            applyPageZoom(storedGridZoom !== null ? parseFloat(storedGridZoom) : getSetting('pageZoom', 1));
            globalIframeZoom = storedIframeZoom !== null ? parseFloat(storedIframeZoom) : getSetting('iframeZoom', 1);
            applyThemeToTaskbar();
            applyAppLocale(getSetting('appLocale', 'es'));
            renderDesktopShortcuts();
        }

        function getYoutubeBackgroundPool() {
            const usePlaylist = getSetting('usePlaylist', false);
            const singlePC = getSetting('youtubeBackground', '');
            const singleMobile = getSetting('youtubeBackgroundMobile', '');
            const playlistPC = getSetting('youtubePlaylistPC', []);
            const playlistMobile = getSetting('youtubePlaylistMobile', []);
            const isMobileDevice = isMobile();
            let pool = [];
            if (usePlaylist) {
                pool = Array.isArray(isMobileDevice ? playlistMobile : playlistPC)
                    ? [...(isMobileDevice ? playlistMobile : playlistPC)]
                    : [];
                const single = isMobileDevice ? singleMobile : singlePC;
                if (single && isValidYoutubeUrl(single) && !pool.includes(single)) {
                    pool.push(single);
                }
            } else {
                const single = isMobileDevice ? singleMobile : singlePC;
                if (single && isValidYoutubeUrl(single)) {
                    pool = [single];
                }
            }
            return pool.filter(Boolean);
        }

        function youtubePoolIndexOf(pool, currentUrl) {
            const curId = extractYoutubeId(currentUrl || '');
            if (!curId) return 0;
            const i = pool.findIndex((u) => extractYoutubeId(u) === curId);
            return i >= 0 ? i : 0;
        }

        function applyVideoBackground() {
            const pool = getYoutubeBackgroundPool();
            const playMode = getSetting('youtubePlayMode', 'random');
            if (pool.length > 0) {
                if (playMode === 'random') {
                    const idx = Math.floor(Math.random() * pool.length);
                    applyYoutubeBackground(pool[idx]);
                } else if (playMode === 'sequential') {
                    applyYoutubeBackground(pool[0]);
                }
            }
            updateActionButtonsVisibility();
        }

        function restartImageSlideshowTimer() {
            const images = getSetting('backgroundImages', []).filter(Boolean);
            const slideshow = getSetting('slideshowEnabled', false);
            if (slideshowTimer) {
                clearInterval(slideshowTimer);
                slideshowTimer = null;
            }
            if (slideshow && images.length > 1) {
                document.body.classList.add('slideshow');
                slideshowTimer = setInterval(() => {
                    currentImageIndex = (currentImageIndex + 1) % images.length;
                    document.body.style.backgroundImage = `url(${resolveWallpaperMediaUrl(images[currentImageIndex])})`;
                    try { syncAuroraCrystal3dBackdrop(); } catch (_) {}
                }, (getSetting('slideshowInterval', 5) * 1000));
            } else {
                document.body.classList.remove('slideshow');
            }
        }

        function canCycleWallpaper() {
            const bgType = getSetting('backgroundType', 'file');
            if (bgType === 'images') {
                return getSetting('backgroundImages', []).filter(Boolean).length > 1;
            }
            if (bgType === 'videos') {
                return getSetting('backgroundVideos', []).filter(Boolean).length > 1;
            }
            if (bgType === 'youtube') {
                return getYoutubeBackgroundPool().length > 1;
            }
            return false;
        }

        function cycleNextWallpaper() {
            if (!canCycleWallpaper()) return;
            const bgType = getSetting('backgroundType', 'file');
            if (bgType === 'images') {
                const images = getSetting('backgroundImages', []).filter(Boolean);
                currentImageIndex = (currentImageIndex + 1) % images.length;
                document.body.style.backgroundImage = `url(${resolveWallpaperMediaUrl(images[currentImageIndex])})`;
                document.body.style.backgroundSize = 'cover';
                document.body.style.backgroundPosition = 'center';
                document.body.style.backgroundAttachment = 'fixed';
                restartImageSlideshowTimer();
                try { syncAuroraCrystal3dBackdrop(); } catch (_) {}
                return;
            }
            if (bgType === 'videos') {
                const videos = getSetting('backgroundVideos', []).filter(Boolean);
                const mode = getSetting('videoPlayMode', 'random');
                const next = (videoPlayIndex + 1) % videos.length;
                playVideo(videos, next, mode);
                return;
            }
            if (bgType === 'youtube') {
                const pool = getYoutubeBackgroundPool();
                const cur = settings.currentYoutubeUrl || '';
                const idx = youtubePoolIndexOf(pool, cur);
                applyYoutubeBackground(pool[(idx + 1) % pool.length]);
            }
        }

        function applyImagesBackground(images, slideshow) {
            if (images.length === 0) return;
            removeExistingBackground();
            currentImageIndex = 0;
            document.body.style.backgroundImage = `url(${resolveWallpaperMediaUrl(images[0])})`;
            document.body.style.backgroundSize = 'cover';
            document.body.style.backgroundPosition = 'center';
            document.body.style.backgroundAttachment = 'fixed';
            updateActionButtons('image');
            updateActionButtonsVisibility();
            if (slideshow && images.length > 1) {
                restartImageSlideshowTimer();
            } else {
                document.body.classList.remove('slideshow');
            }
            try { syncAuroraCrystal3dBackdrop(); } catch (_) {}
        }

        function applyVideosBackground(videos) {
            if (videos.length === 0) return;
            removeExistingBackground();
            const playMode = getSetting('videoPlayMode', 'random');
            let index = 0;
            if (playMode === 'random') {
                index = Math.floor(Math.random() * videos.length);
            } else if (playMode === 'sequential') {
                index = 0;
            } else if (playMode === 'loop') {
                index = 0;
            }
            playVideo(videos, index, playMode);
            updateActionButtonsVisibility();
        }

        function playVideo(videos, index, mode) {
            if (!videos.length) return;
            if (currentVideoElement) {
                currentVideoElement.onended = null;
                currentVideoElement.remove();
                currentVideoElement = null;
            }
            const videoUrl = videos[index];
            const videoElement = document.createElement('video');
            videoElement.className = 'video-background';
            videoElement.src = resolveWallpaperMediaUrl(videoUrl);
            videoElement.autoplay = true;
            videoElement.muted = true;
            videoElement.loop = (mode === 'loop');
            videoElement.playsInline = true;
            videoElement.onended = function() {
                if (mode === 'sequential') {
                    const nextIndex = (index + 1) % videos.length;
                    playVideo(videos, nextIndex, mode);
                } else if (mode === 'random') {
                    const nextIndex = Math.floor(Math.random() * videos.length);
                    playVideo(videos, nextIndex, mode);
                }
            };
            document.body.insertBefore(videoElement, document.body.firstChild);
            currentVideoElement = videoElement;
            videoPlayIndex = index;
            updateActionButtons('video');
            try { updateAuroraCrystal3dLayer(); } catch (_) {}
        }

        function updateFileInputPreview(dataUrl) {
            previewContainer.innerHTML = '';
            if (dataUrl.match(/\.(jpg|jpeg|png|gif|webp)(\?.*)?$/i)) {
                const img = document.createElement('img');
                img.src = dataUrl;
                img.style.width = '100%';
                img.style.height = '100%';
                img.style.objectFit = 'cover';
                previewContainer.appendChild(img);
            } else if (dataUrl.match(/\.(mp4|webm|ogg)(\?.*)?$/i)) {
                const video = document.createElement('video');
                video.src = dataUrl;
                video.style.width = '100%';
                video.style.height = '100%';
                video.style.objectFit = 'cover';
                video.controls = true;
                video.autoplay = true;
                video.muted = true;
                video.loop = true;
                previewContainer.appendChild(video);
            }
        }

        // ========== FUNCIONES DE CATEGORÍAS ==========
        function renderCategories() {
            const currentAppCategory = appCategorySelect.value;
            const currentFilterCategory = categoryFilterSelect.value;
            
            appCategorySelect.innerHTML = '<option value="General">General</option>';
            categoryFilterSelect.innerHTML = '<option value="">Todas las categorías</option>';
            
            const categoryList = document.getElementById('categoryList');
            categoryList.innerHTML = '';
            
            if (!categories || categories.length === 0) {
                categories = [gridNormalizeCategoryEntry('General')];
            }
            
            categories.forEach(categoryRaw => {
                const cNorm = gridNormalizeCategoryEntry(categoryRaw);
                const category = cNorm.name;
                if (category !== 'General') {
                    const option = document.createElement('option');
                    option.value = category;
                    option.textContent = category;
                    appCategorySelect.appendChild(option);
                    
                    const filterOption = document.createElement('option');
                    filterOption.value = category;
                    filterOption.textContent = category;
                    categoryFilterSelect.appendChild(filterOption);

                    const li = document.createElement('li');
                    li.style.display = 'flex';
                    li.style.justifyContent = 'space-between';
                    li.style.alignItems = 'center';
                    li.style.marginBottom = '10px';
                    const span = document.createElement('span');
                    if (cNorm.icon) {
                        const ic = document.createElement('i');
                        ic.className = gridSanitizeFaClass(cNorm.icon);
                        ic.style.marginRight = '8px';
                        ic.style.color = cNorm.color || '#64748b';
                        span.appendChild(ic);
                    }
                    span.appendChild(document.createTextNode(category));
                    const div = document.createElement('div');
                    div.style.display = 'flex';
                    div.style.gap = '8px';
                    div.style.flexWrap = 'wrap';
                    div.style.alignItems = 'center';
                    const iconBtn = document.createElement('button');
                    iconBtn.type = 'button';
                    iconBtn.className = 'editCategoryIconBtn';
                    iconBtn.textContent = 'Icono';
                    iconBtn.dataset.category = category;
                    const editBtn = document.createElement('button');
                    editBtn.type = 'button';
                    editBtn.className = 'editCategoryBtn';
                    editBtn.textContent = 'Editar';
                    editBtn.dataset.category = category;
                    const delBtn = document.createElement('button');
                    delBtn.type = 'button';
                    delBtn.className = 'deleteCategoryBtn';
                    delBtn.textContent = 'Eliminar';
                    delBtn.dataset.category = category;
                    div.appendChild(iconBtn);
                    div.appendChild(editBtn);
                    div.appendChild(delBtn);
                    li.appendChild(span);
                    li.appendChild(div);
                    categoryList.appendChild(li);
                }
            });
            
            if (currentAppCategory && Array.from(appCategorySelect.options).some(opt => opt.value === currentAppCategory)) {
                appCategorySelect.value = currentAppCategory;
            }
            if (currentFilterCategory && Array.from(categoryFilterSelect.options).some(opt => opt.value === currentFilterCategory)) {
                categoryFilterSelect.value = currentFilterCategory;
            }
            
            addCategoryEventListeners();
            
            const catInput = document.getElementById('appCategoryFilter');
            if (catInput) {
                const newInput = catInput.cloneNode(true);
                catInput.parentNode.replaceChild(newInput, catInput);
                document.getElementById('appCategoryFilter').addEventListener('input', function() {
                    const q = this.value.toLowerCase();
                    Array.from(appCategorySelect.options).forEach(opt => {
                        opt.style.display = opt.text.toLowerCase().includes(q) ? '' : 'none';
                    });
                    const first = Array.from(appCategorySelect.options).find(o => o.style.display !== 'none');
                });
            }
        }

        function addCategoryEventListeners() {
            document.querySelectorAll('.editCategoryIconBtn').forEach(btn => {
                btn.addEventListener('click', (e) => {
                    const el = e.target && e.target.closest ? e.target.closest('button') : e.target;
                    const category = el && el.dataset ? el.dataset.category : '';
                    if (category) openEditCategoryIconModal(category);
                });
            });
            document.querySelectorAll('.editCategoryBtn').forEach(btn => {
                btn.addEventListener('click', async (e) => {
                    const el = e.target && e.target.closest ? e.target.closest('button') : e.target;
                    const category = el && el.dataset ? el.dataset.category : '';
                    const newName = await appDialog.prompt(`Editar categoría "${category}":`, category);
                    if (newName && newName !== category) editCategory(category, newName.trim());
                });
            });
            document.querySelectorAll('.deleteCategoryBtn').forEach(btn => {
                btn.addEventListener('click', (e) => {
                    const el = e.target && e.target.closest ? e.target.closest('button') : e.target;
                    const category = el && el.dataset ? el.dataset.category : '';
                    deleteCategory(category);
                });
            });
        }

        async function addNewCategory() {
            const newCategory = document.getElementById('newCategoryName').value.trim();
            const descEl = document.getElementById('newCategoryDescription');
            const colorEl = document.getElementById('newCategoryColor');
            const iconEl = document.getElementById('newCategoryIconClass');
            const desc = descEl ? descEl.value.trim() : '';
            const color = colorEl ? colorEl.value : '#38bdf8';
            const icon = iconEl && iconEl.value ? gridSanitizeFaClass(iconEl.value) : 'fas fa-folder';
            if (newCategory && !gridCategoryListIncludesName(categories, newCategory)) {
                categories.push(gridNormalizeCategoryEntry({
                    name: newCategory,
                    description: desc,
                    color: color,
                    icon: icon,
                }));
                saveSettings({ categories: categories }).then(renderCategories);
                document.getElementById('newCategoryName').value = '';
                if (descEl) descEl.value = '';
                if (iconEl) iconEl.value = '';
                if (colorEl) colorEl.value = '#38bdf8';
            } else if (newCategory && gridCategoryListIncludesName(categories, newCategory)) {
                await appDialog.alert('Esta categoría ya existe');
            }
        }

        function editCategory(oldName, newName) {
            const index = categories.findIndex(c => gridCategoryNameFromEntry(c) === oldName);
            if (index > -1) {
                const prev = gridNormalizeCategoryEntry(categories[index]);
                categories[index] = gridNormalizeCategoryEntry({
                    name: newName.trim(),
                    icon: prev.icon,
                    color: prev.color,
                    description: prev.description,
                });
                let appsUpdated = false;
                apps.forEach(app => {
                    if (app.category === oldName) {
                        app.category = newName.trim();
                        appsUpdated = true;
                    }
                });
                const savePromise = appsUpdated ? saveSettings({ categories: categories, apps: apps }) : saveSettings({ categories: categories });
                savePromise.then(() => { renderCategories(); renderApps(); });
            }
        }

        function closeEditCategoryIconModal() {
            if (!editCategoryIconModal) return;
            editCategoryIconModal.style.display = 'none';
            try { delete editCategoryIconModal.dataset.editingCategory; } catch (_) {}
        }

        function openEditCategoryIconModal(categoryName) {
            if (!categoryName || categoryName === 'General' || !editCategoryIconModal) return;
            if (window.OsShell && typeof window.OsShell.ensureCategoryIconSelect === 'function') {
                window.OsShell.ensureCategoryIconSelect('editCategoryIconSelect');
            }
            const sel = document.getElementById('editCategoryIconSelect');
            const col = document.getElementById('editCategoryIconColor');
            const lab = document.getElementById('editCategoryIconModalCategoryLabel');
            if (!sel || !col) return;
            const idx = categories.findIndex(c => gridCategoryNameFromEntry(c) === categoryName);
            const prev = idx >= 0 ? gridNormalizeCategoryEntry(categories[idx]) : gridNormalizeCategoryEntry(categoryName);
            const rawIcon = prev.icon ? String(prev.icon).trim() : '';
            const curIcon = rawIcon ? gridSanitizeFaClass(rawIcon) : '';
            editCategoryIconModal.dataset.editingCategory = categoryName;
            if (lab) lab.textContent = `Categoría: ${categoryName}`;
            if (!curIcon) {
                sel.value = '';
            } else if (Array.from(sel.options).some(o => o.value === curIcon)) {
                sel.value = curIcon;
            } else {
                const o = document.createElement('option');
                o.value = curIcon;
                const short = curIcon.split(/\s+/).filter(Boolean).pop() || curIcon;
                o.textContent = short.indexOf('fa-') === 0 ? short.replace(/^fa-/, '').replace(/-/g, ' ') : curIcon;
                sel.appendChild(o);
                sel.value = curIcon;
            }
            col.value = (prev.color && /^#([0-9a-f]{3}|[0-9a-f]{6})$/i.test(prev.color)) ? prev.color : '#38bdf8';
            editCategoryIconModal.style.display = 'block';
        }

        function saveEditCategoryIconModal() {
            if (!editCategoryIconModal) return;
            const name = editCategoryIconModal.dataset.editingCategory || '';
            if (!name) return closeEditCategoryIconModal();
            const sel = document.getElementById('editCategoryIconSelect');
            const colInp = document.getElementById('editCategoryIconColor');
            const icon = sel && sel.value ? gridSanitizeFaClass(sel.value) : 'fas fa-folder';
            const color = colInp && colInp.value ? colInp.value : '#38bdf8';
            const index = categories.findIndex(c => gridCategoryNameFromEntry(c) === name);
            if (index < 0) return closeEditCategoryIconModal();
            const prev = gridNormalizeCategoryEntry(categories[index]);
            categories[index] = gridNormalizeCategoryEntry({
                name: prev.name,
                icon: icon,
                color: color,
                description: prev.description,
            });
            saveSettings({ categories: categories }).then(() => {
                renderCategories();
                renderApps();
                if (windowsMenuVisible) renderWindowsMenu(apps);
                closeEditCategoryIconModal();
            });
        }

        async function deleteCategory(category) {
            if (await appDialog.confirm(`¿Eliminar categoría "${category}"? Las apps pasarán a General.`)) {
                const index = categories.findIndex(c => gridCategoryNameFromEntry(c) === category);
                if (index > -1) {
                    categories.splice(index, 1);
                    apps.forEach(app => { if (app.category === category) app.category = 'General'; });
                    saveSettings({ categories: categories, apps: apps }).then(() => { renderCategories(); renderApps(); });
                }
            }
        }

        categoryForm.addEventListener('submit', async (e) => {
            e.preventDefault();
            const categoryName = document.getElementById('categoryName').value.trim();
            const descEl = document.getElementById('categoryDescription');
            const colorEl = document.getElementById('categoryColor');
            const iconEl = document.getElementById('categoryIconClass');
            const desc = descEl ? descEl.value.trim() : '';
            const color = colorEl ? colorEl.value : '#38bdf8';
            const icon = iconEl && iconEl.value ? gridSanitizeFaClass(iconEl.value) : 'fas fa-folder';
            if (categoryName && !gridCategoryListIncludesName(categories, categoryName)) {
                categories.push(gridNormalizeCategoryEntry({
                    name: categoryName,
                    description: desc,
                    color: color,
                    icon: icon,
                }));
                saveSettings({ categories: categories }).then(() => {
                    renderCategories();
                    addCategoryModal.style.display = 'none';
                    categoryForm.reset();
                    if (iconEl) iconEl.value = '';
                    if (colorEl) colorEl.value = '#38bdf8';
                    appCategorySelect.value = categoryName;
                    document.body.classList.remove('modal-open');
                    if (window.OsShell && typeof window.OsShell.resetCategoryModal === 'function') window.OsShell.resetCategoryModal();
                });
            } else if (categoryName && gridCategoryListIncludesName(categories, categoryName)) {
                await appDialog.alert('Esta categoría ya existe');
            } else {
                await appDialog.alert('Nombre inválido');
            }
        });

        function filterAppsByCategory(category) {
            if (!category) renderApps(getDisplayApps());
            else renderApps(getDisplayApps().filter(app => app.category === category));
        }

        function searchApps(query) {
            if (!query) renderApps(getDisplayApps());
            else renderApps(getDisplayApps().filter(app =>
                app.title.toLowerCase().includes(query.toLowerCase()) ||
                (app.description && app.description.toLowerCase().includes(query.toLowerCase()))
            ));
        }

        categoryFilterSelect.addEventListener('change', (e) => filterAppsByCategory(e.target.value));

        // ========== FUNCIONES DE TEMAS ==========
        const THEME_IDS_ORDER = ['neumorphism', 'glassmorphism', 'skeuomorphism', 'flat', 'material', 'brutalism', 'retro', 'dark', 'minimalism', 'morphing', 'isometric', 'cyberpunk', 'gradient', 'glassgradient', 'auroracrystal3d', 'card', 'typography'];
        /** Opacidades 0–100 para Aurora cristal (paleta + CSS --gg-*). */
        const GLASSGRADIENT_OPACITY_KEYS = ['layer1Opacity', 'layer2Opacity', 'tintOpacity', 'borderOpacity', 'inputOpacity', 'inputBorderOpacity'];
        /** Aurora cristal 3D: blur y deformación de lente (0–100). */
        const AURORACRYSTAL3D_EFFECT_KEYS = ['blurAmount', 'distortLevel'];
        const AURORACRYSTAL3D_RANGE_KEYS = [...GLASSGRADIENT_OPACITY_KEYS, ...AURORACRYSTAL3D_EFFECT_KEYS];
        const THEME_PALETTE_DEFAULTS = {
            neumorphism: { bg: '#e0e5ec', shadowLight: '#ffffff', shadowDark: '#a3b1c6' },
            glassmorphism: { tint: '#ffffff', borderTint: '#ffffff' },
            skeuomorphism: { top: '#f0f0f0', bottom: '#d0d0d0', border: '#999999' },
            flat: { surface: '#f0f0f0', sidebar: '#e8e8e8', sidebarBorder: '#dddddd' },
            material: { surface: '#ffffff', sidebar: '#f7f7f7', shadow: '#000000' },
            brutalism: { bg: '#000000', accent: '#ff0000', text: '#ffffff' },
            retro: { bg: '#008080', sidebar: '#006666', text: '#ffffff', border: '#ffffff' },
            dark: { surface: '#121212', sidebar: '#1a1a1a', text: '#ffffff' },
            minimalism: { surface: '#ffffff', sidebar: '#fafafa', text: '#000000' },
            morphing: { panel: '#e0e5ec' },
            isometric: { panel: '#e0e5ec' },
            cyberpunk: { bg: '#000000', neon: '#00ffff', accent: '#ff00ff' },
            gradient: { color1: '#ff9a9e', color2: '#fad0c4' },
            glassgradient: {
                color1: '#667eea',
                color2: '#764ba2',
                tint: '#ffffff',
                borderTint: '#ffffff',
                layer1Opacity: 48,
                layer2Opacity: 42,
                tintOpacity: 32,
                borderOpacity: 38,
                inputOpacity: 22,
                inputBorderOpacity: 24
            },
            auroracrystal3d: {
                color1: '#667eea',
                color2: '#764ba2',
                tint: '#ffffff',
                borderTint: '#ffffff',
                layer1Opacity: 48,
                layer2Opacity: 42,
                tintOpacity: 32,
                borderOpacity: 38,
                inputOpacity: 22,
                inputBorderOpacity: 24,
                blurAmount: 50,
                distortLevel: 35
            },
            card: { surface: '#ffffff', sidebar: '#f6f7f9' },
            typography: { panel: '#ffffff' }
        };
        const THEME_DISPLAY_NAMES = {
            neumorphism: 'Neumorfismo',
            glassmorphism: 'Glassmorfismo',
            skeuomorphism: 'Skeuomorfismo',
            flat: 'Diseño plano',
            material: 'Material',
            brutalism: 'Brutalismo',
            retro: 'Retro',
            dark: 'Modo oscuro',
            minimalism: 'Minimalismo',
            morphing: 'Morphing',
            isometric: 'Isométrico',
            cyberpunk: 'Retro futurista',
            gradient: 'Gradientes',
            glassgradient: 'Aurora cristal',
            auroracrystal3d: 'Aurora cristal 3D',
            card: 'Cartas',
            typography: 'Tipografía audaz'
        };
        const THEME_PALETTE_LABELS = {
            neumorphism: { bg: 'Fondo base', shadowLight: 'Sombra clara', shadowDark: 'Sombra oscura' },
            glassmorphism: { tint: 'Tinte cristal', borderTint: 'Tinte borde' },
            skeuomorphism: { top: 'Degradado superior', bottom: 'Degradado inferior', border: 'Borde' },
            flat: { surface: 'Superficie', sidebar: 'Panel lateral', sidebarBorder: 'Borde lateral' },
            material: { surface: 'Superficie', sidebar: 'Panel lateral', shadow: 'Color sombra' },
            brutalism: { bg: 'Fondo', accent: 'Acento / borde', text: 'Texto' },
            retro: { bg: 'Fondo principal', sidebar: 'Panel lateral', text: 'Texto', border: 'Borde' },
            dark: { surface: 'Superficie', sidebar: 'Panel lateral', text: 'Texto' },
            minimalism: { surface: 'Superficie', sidebar: 'Panel lateral', text: 'Texto' },
            morphing: { panel: 'Paneles (barra, menú…)' },
            isometric: { panel: 'Paneles (barra, menú…)' },
            cyberpunk: { bg: 'Fondo', neon: 'Neón (texto)', accent: 'Acento (borde)' },
            gradient: { color1: 'Color gradiente 1', color2: 'Color gradiente 2' },
            glassgradient: {
                color1: 'Gradiente 1',
                color2: 'Gradiente 2',
                tint: 'Tinte cristal',
                borderTint: 'Borde luminoso',
                layer1Opacity: 'Opacidad capa gradiente 1',
                layer2Opacity: 'Opacidad capa gradiente 2',
                tintOpacity: 'Opacidad velo cristal (tinte)',
                borderOpacity: 'Opacidad borde paneles',
                inputOpacity: 'Opacidad fondo campos',
                inputBorderOpacity: 'Opacidad borde campos'
            },
            auroracrystal3d: {
                color1: 'Gradiente 1',
                color2: 'Gradiente 2',
                tint: 'Tinte cristal',
                borderTint: 'Borde luminoso',
                layer1Opacity: 'Opacidad capa gradiente 1',
                layer2Opacity: 'Opacidad capa gradiente 2',
                tintOpacity: 'Opacidad velo cristal (tinte)',
                borderOpacity: 'Opacidad borde paneles',
                inputOpacity: 'Opacidad fondo campos',
                inputBorderOpacity: 'Opacidad borde campos',
                blurAmount: 'Intensidad blur (cristal)',
                distortLevel: 'Deformación 3D cristal (lente al frente)'
            },
            card: { surface: 'Superficie', sidebar: 'Panel lateral' },
            typography: { panel: 'Paneles' }
        };

        /* Colores del explorador de archivos por tema (paleta en Configuración → Temas) */
        const EXPLORER_PALETTE_BY_THEME = {
            neumorphism: { feMainBg: '#e0e5ec', feSidebarBg: '#dde3ea', feText: '#0f172a', feMuted: '#64748b', feAccent: '#0a84ff', feBorder: '#a8b8d0', feInputBg: '#e8edf2', feItemBg: '#d3dae6' },
            glassmorphism: { feMainBg: '#e8eef6', feSidebarBg: '#e4eaf4', feText: '#0f172a', feMuted: '#64748b', feAccent: '#0a84ff', feBorder: '#c5d0e0', feInputBg: '#ffffff', feItemBg: '#ecf2f9' },
            skeuomorphism: { feMainBg: '#e8e8e8', feSidebarBg: '#d8d8d8', feText: '#1a1a1a', feMuted: '#5c5c5c', feAccent: '#0a84ff', feBorder: '#999999', feInputBg: '#f5f5f5', feItemBg: '#e0e0e0' },
            flat: { feMainBg: '#f0f0f0', feSidebarBg: '#e8e8e8', feText: '#0f172a', feMuted: '#64748b', feAccent: '#0a84ff', feBorder: '#dddddd', feInputBg: '#ffffff', feItemBg: '#e8e8e8' },
            material: { feMainBg: '#ffffff', feSidebarBg: '#f7f7f7', feText: '#0f172a', feMuted: '#64748b', feAccent: '#0a84ff', feBorder: '#e0e0e0', feInputBg: '#fafafa', feItemBg: '#f0f0f0' },
            brutalism: { feMainBg: '#000000', feSidebarBg: '#000000', feText: '#ffffff', feMuted: '#d4d4d4', feAccent: '#ff0000', feBorder: '#ff0000', feInputBg: '#111111', feItemBg: '#0a0a0a' },
            retro: { feMainBg: '#008080', feSidebarBg: '#006666', feText: '#ffffff', feMuted: '#b0e0e0', feAccent: '#ffff99', feBorder: '#ffffff', feInputBg: '#005050', feItemBg: '#007070' },
            dark: { feMainBg: '#121212', feSidebarBg: '#1a1a1a', feText: '#f8fafc', feMuted: '#94a3b8', feAccent: '#5ac8fa', feBorder: '#334155', feInputBg: '#1e293b', feItemBg: '#1e293b' },
            minimalism: { feMainBg: '#ffffff', feSidebarBg: '#fafafa', feText: '#000000', feMuted: '#666666', feAccent: '#0a84ff', feBorder: '#e5e5e5', feInputBg: '#ffffff', feItemBg: '#f5f5f5' },
            morphing: { feMainBg: '#e0e5ec', feSidebarBg: '#dde3e9', feText: '#0f172a', feMuted: '#64748b', feAccent: '#0a84ff', feBorder: '#b8c4d4', feInputBg: '#e8ecf0', feItemBg: '#d3dae6' },
            isometric: { feMainBg: '#e0e5ec', feSidebarBg: '#dde3e9', feText: '#0f172a', feMuted: '#64748b', feAccent: '#0a84ff', feBorder: '#b8c4d4', feInputBg: '#e8ecf0', feItemBg: '#d3dae6' },
            cyberpunk: { feMainBg: '#000000', feSidebarBg: '#000000', feText: '#00ffff', feMuted: '#ff00ff', feAccent: '#ff00ff', feBorder: '#ff00ff', feInputBg: '#0a0a0a', feItemBg: '#050505' },
            gradient: { feMainBg: '#fff5f7', feSidebarBg: '#ffffff', feText: '#1e293b', feMuted: '#64748b', feAccent: '#0a84ff', feBorder: '#f9a8c4', feInputBg: '#ffffff', feItemBg: '#fce7f0' },
            glassgradient: { feMainBg: '#4f46e5', feSidebarBg: '#4338ca', feText: '#f8fafc', feMuted: '#c7d2fe', feAccent: '#c4b5fd', feBorder: '#818cf8', feInputBg: '#312e81', feItemBg: '#3730a3' },
            auroracrystal3d: { feMainBg: '#4f46e5', feSidebarBg: '#4338ca', feText: '#f8fafc', feMuted: '#c7d2fe', feAccent: '#c4b5fd', feBorder: '#818cf8', feInputBg: '#312e81', feItemBg: '#3730a3' },
            card: { feMainBg: '#ffffff', feSidebarBg: '#f6f7f9', feText: '#0f172a', feMuted: '#64748b', feAccent: '#0a84ff', feBorder: '#e5e7eb', feInputBg: '#ffffff', feItemBg: '#f3f4f6' },
            typography: { feMainBg: '#ffffff', feSidebarBg: '#f6f6f6', feText: '#000000', feMuted: '#333333', feAccent: '#000000', feBorder: '#000000', feInputBg: '#ffffff', feItemBg: '#eeeeee' }
        };
        const FE_EXPLORER_LABELS = {
            feMainBg: 'Explorador: fondo principal',
            feSidebarBg: 'Explorador: panel lateral',
            feText: 'Explorador: texto principal',
            feMuted: 'Explorador: texto secundario',
            feAccent: 'Explorador: acento (selección, árbol activo)',
            feBorder: 'Explorador: bordes',
            feInputBg: 'Explorador: fondo búsqueda',
            feItemBg: 'Explorador: tarjetas de ítems'
        };

        function applyExplorerPaletteVars(c) {
            const r = document.documentElement;
            const map = [
                ['feMainBg', '--fe-palette-main-bg'],
                ['feSidebarBg', '--fe-palette-sidebar-bg'],
                ['feTreeBg', '--fe-palette-tree-bg'],
                ['feToolbarBg', '--fe-palette-toolbar-bg'],
                ['feText', '--fe-palette-text'],
                ['feMuted', '--fe-palette-muted'],
                ['feAccent', '--fe-palette-accent'],
                ['feBorder', '--fe-palette-border'],
                ['feInputBg', '--fe-palette-input-bg'],
                ['feItemBg', '--fe-palette-item-bg']
            ];
            map.forEach(([k, cssVar]) => {
                const v = c[k];
                if (v && typeof v === 'string') r.style.setProperty(cssVar, v);
            });
        }

        function hexToRgb(hex) {
            const s = String(hex || '').trim();
            const m = /^#?([a-f0-9]{2})([a-f0-9]{2})([a-f0-9]{2})$/i.exec(s);
            if (!m) return null;
            return { r: parseInt(m[1], 16), g: parseInt(m[2], 16), b: parseInt(m[3], 16) };
        }

        function rgbaFromHex(hex, alpha01) {
            const rgb = hexToRgb(hex);
            if (!rgb) return null;
            const a = Math.max(0, Math.min(1, Number(alpha01)));
            return `rgba(${rgb.r}, ${rgb.g}, ${rgb.b}, ${a})`;
        }

        function getExplorerUiSettings() {
            const s = getSetting('explorerUi', {});
            if (!s || typeof s !== 'object' || Array.isArray(s)) return {};
            return s;
        }

        function applyExplorerUiOverridesFromSettings() {
            const ui = getExplorerUiSettings();
            const hasAny = Object.keys(ui).length > 0;
            if (!hasAny) return;
            const r = document.documentElement;

            const mainA = (Number(ui.mainAlpha) / 100);
            const sideA = (Number(ui.sidebarAlpha) / 100);
            const treeA = (Number(ui.treeAlpha) / 100);
            const toolbarA = (Number(ui.toolbarAlpha) / 100);
            const inputA = (Number(ui.inputAlpha) / 100);
            const itemA = (Number(ui.itemAlpha) / 100);
            const mainBg = ui.mainBg ? rgbaFromHex(ui.mainBg, isFinite(mainA) ? mainA : 1) : null;
            const sideBg = ui.sidebarBg ? rgbaFromHex(ui.sidebarBg, isFinite(sideA) ? sideA : 1) : null;
            const treeBg = ui.treeBg ? rgbaFromHex(ui.treeBg, isFinite(treeA) ? treeA : 1) : null;
            const toolbarBg = ui.toolbarBg ? rgbaFromHex(ui.toolbarBg, isFinite(toolbarA) ? toolbarA : 1) : null;
            const inputBg = ui.inputBg ? rgbaFromHex(ui.inputBg, isFinite(inputA) ? inputA : 1) : null;
            const itemBg = ui.itemBg ? rgbaFromHex(ui.itemBg, isFinite(itemA) ? itemA : 1) : null;

            if (mainBg) r.style.setProperty('--fe-palette-main-bg', mainBg);
            if (sideBg) r.style.setProperty('--fe-palette-sidebar-bg', sideBg);
            if (treeBg) r.style.setProperty('--fe-palette-tree-bg', treeBg);
            if (toolbarBg) r.style.setProperty('--fe-palette-toolbar-bg', toolbarBg);
            if (ui.text) r.style.setProperty('--fe-palette-text', ui.text);
            if (ui.muted) r.style.setProperty('--fe-palette-muted', ui.muted);
            if (ui.accent) r.style.setProperty('--fe-palette-accent', ui.accent);
            if (ui.border) r.style.setProperty('--fe-palette-border', ui.border);
            if (inputBg) r.style.setProperty('--fe-palette-input-bg', inputBg);
            if (itemBg) r.style.setProperty('--fe-palette-item-bg', itemBg);
        }

        function initExplorerThemeModalOnce() {
            const modal = document.getElementById('explorerThemeModal');
            if (!modal || modal.dataset.inited === '1') return;
            modal.dataset.inited = '1';

            const closeX = document.getElementById('explorerThemeModalCloseX');
            const resetBtn = document.getElementById('explorerThemeResetBtn');
            const saveBtn = document.getElementById('explorerThemeSaveBtn');

            const mainBg = document.getElementById('explorerThemeMainBg');
            const mainAlpha = document.getElementById('explorerThemeMainAlpha');
            const mainAlphaLabel = document.getElementById('explorerThemeMainAlphaLabel');
            const sidebarBg = document.getElementById('explorerThemeSidebarBg');
            const sidebarAlpha = document.getElementById('explorerThemeSidebarAlpha');
            const sidebarAlphaLabel = document.getElementById('explorerThemeSidebarAlphaLabel');

            const treeBg = document.getElementById('explorerThemeTreeBg');
            const treeAlpha = document.getElementById('explorerThemeTreeAlpha');
            const treeAlphaLabel = document.getElementById('explorerThemeTreeAlphaLabel');
            const toolbarBg = document.getElementById('explorerThemeToolbarBg');
            const toolbarAlpha = document.getElementById('explorerThemeToolbarAlpha');
            const toolbarAlphaLabel = document.getElementById('explorerThemeToolbarAlphaLabel');
            const text = document.getElementById('explorerThemeText');
            const muted = document.getElementById('explorerThemeMuted');
            const accent = document.getElementById('explorerThemeAccent');
            const border = document.getElementById('explorerThemeBorder');
            const inputBg = document.getElementById('explorerThemeInputBg');
            const inputAlpha = document.getElementById('explorerThemeInputAlpha');
            const inputAlphaLabel = document.getElementById('explorerThemeInputAlphaLabel');
            const itemBg = document.getElementById('explorerThemeItemBg');
            const itemAlpha = document.getElementById('explorerThemeItemAlpha');
            const itemAlphaLabel = document.getElementById('explorerThemeItemAlphaLabel');

            function setAlphaLabel(inp, lab) {
                if (!inp || !lab) return;
                const v = Math.max(0, Math.min(100, Number(inp.value)));
                lab.textContent = `${v}%`;
            }

            function fillFromSettings() {
                const ui = getExplorerUiSettings();
                mainBg.value = ui.mainBg || '#e0e5ec';
                sidebarBg.value = ui.sidebarBg || '#dde3ea';
                treeBg.value = ui.treeBg || (ui.sidebarBg || '#dde3ea');
                toolbarBg.value = ui.toolbarBg || (ui.mainBg || '#e0e5ec');
                text.value = ui.text || '#0f172a';
                muted.value = ui.muted || '#64748b';
                accent.value = ui.accent || '#0a84ff';
                border.value = ui.border || '#a8b8d0';
                inputBg.value = ui.inputBg || '#e8edf2';
                itemBg.value = ui.itemBg || '#d3dae6';
                mainAlpha.value = (ui.mainAlpha !== undefined ? ui.mainAlpha : 100);
                sidebarAlpha.value = (ui.sidebarAlpha !== undefined ? ui.sidebarAlpha : 100);
                treeAlpha.value = (ui.treeAlpha !== undefined ? ui.treeAlpha : 100);
                toolbarAlpha.value = (ui.toolbarAlpha !== undefined ? ui.toolbarAlpha : 100);
                inputAlpha.value = (ui.inputAlpha !== undefined ? ui.inputAlpha : 100);
                itemAlpha.value = (ui.itemAlpha !== undefined ? ui.itemAlpha : 100);
                setAlphaLabel(mainAlpha, mainAlphaLabel);
                setAlphaLabel(sidebarAlpha, sidebarAlphaLabel);
                setAlphaLabel(treeAlpha, treeAlphaLabel);
                setAlphaLabel(toolbarAlpha, toolbarAlphaLabel);
                setAlphaLabel(inputAlpha, inputAlphaLabel);
                setAlphaLabel(itemAlpha, itemAlphaLabel);
            }

            function applyPreview() {
                try {
                    const r = document.documentElement;
                    const main = rgbaFromHex(mainBg.value, Number(mainAlpha.value) / 100);
                    const side = rgbaFromHex(sidebarBg.value, Number(sidebarAlpha.value) / 100);
                    const tree = rgbaFromHex(treeBg.value, Number(treeAlpha.value) / 100);
                    const toolbar = rgbaFromHex(toolbarBg.value, Number(toolbarAlpha.value) / 100);
                    const input = rgbaFromHex(inputBg.value, Number(inputAlpha.value) / 100);
                    const item = rgbaFromHex(itemBg.value, Number(itemAlpha.value) / 100);
                    if (main) r.style.setProperty('--fe-palette-main-bg', main);
                    if (side) r.style.setProperty('--fe-palette-sidebar-bg', side);
                    if (tree) r.style.setProperty('--fe-palette-tree-bg', tree);
                    if (toolbar) r.style.setProperty('--fe-palette-toolbar-bg', toolbar);
                    r.style.setProperty('--fe-palette-text', text.value);
                    r.style.setProperty('--fe-palette-muted', muted.value);
                    r.style.setProperty('--fe-palette-accent', accent.value);
                    r.style.setProperty('--fe-palette-border', border.value);
                    if (input) r.style.setProperty('--fe-palette-input-bg', input);
                    if (item) r.style.setProperty('--fe-palette-item-bg', item);
                } catch (_) {}
                setAlphaLabel(mainAlpha, mainAlphaLabel);
                setAlphaLabel(sidebarAlpha, sidebarAlphaLabel);
                setAlphaLabel(treeAlpha, treeAlphaLabel);
                setAlphaLabel(toolbarAlpha, toolbarAlphaLabel);
                setAlphaLabel(inputAlpha, inputAlphaLabel);
                setAlphaLabel(itemAlpha, itemAlphaLabel);
                try { refreshThemeContrast(); } catch (_) {}
            }

            function closeModal() {
                modal.style.display = 'none';
                modal.setAttribute('aria-hidden', 'true');
                document.body.classList.remove('modal-open');
            }

            function openModal() {
                fillFromSettings();
                // Aplicar un preview inicial para que el usuario vea el estado actual.
                applyThemeToTaskbar();
                applyPreview();
                bumpShellModalAboveWindows(modal);
                modal.style.display = 'block';
                modal.setAttribute('aria-hidden', 'false');
                document.body.classList.add('modal-open');
            }

            window.openExplorerThemeModal = openModal;

            if (closeX) closeX.addEventListener('click', closeModal);
            modal.addEventListener('click', (e) => { if (e.target === modal) closeModal(); });

            [
                mainBg, mainAlpha, sidebarBg, sidebarAlpha,
                treeBg, treeAlpha, toolbarBg, toolbarAlpha,
                text, muted, accent, border,
                inputBg, inputAlpha, itemBg, itemAlpha
            ].forEach((el) => {
                if (!el) return;
                el.addEventListener('input', applyPreview);
                el.addEventListener('change', applyPreview);
            });

            if (resetBtn) {
                resetBtn.addEventListener('click', () => {
                    saveSettings({ explorerUi: {} }).then(() => {
                        fillFromSettings();
                        applyThemeToTaskbar();
                    });
                });
            }
            if (saveBtn) {
                saveBtn.addEventListener('click', () => {
                    const ui = {
                        mainBg: mainBg.value,
                        mainAlpha: Number(mainAlpha.value),
                        sidebarBg: sidebarBg.value,
                        sidebarAlpha: Number(sidebarAlpha.value),
                        treeBg: treeBg.value,
                        toolbarBg: toolbarBg.value,
                        text: text.value,
                        muted: muted.value,
                        accent: accent.value,
                        border: border.value,
                        inputBg: inputBg.value,
                        itemBg: itemBg.value
                    };
                    ui.treeAlpha = Number(treeAlpha.value);
                    ui.toolbarAlpha = Number(toolbarAlpha.value);
                    ui.inputAlpha = Number(inputAlpha.value);
                    ui.itemAlpha = Number(itemAlpha.value);
                    saveSettings({ explorerUi: ui }).then(() => {
                        applyThemeToTaskbar();
                        closeModal();
                    });
                });
            }
        }

        window.initExplorerThemeModalOnce = initExplorerThemeModalOnce;

        function mergeThemeColors() {
            let stored = getSetting('themeColors', {});
            if (!stored || typeof stored !== 'object' || Array.isArray(stored)) stored = {};
            const out = {};
            THEME_IDS_ORDER.forEach((id) => {
                out[id] = {
                    ...THEME_PALETTE_DEFAULTS[id],
                    ...(EXPLORER_PALETTE_BY_THEME[id] || {}),
                    ...(stored[id] || {})
                };
            });
            const g = out.gradient;
            if (g) {
                g.color1 = g.color1 || getSetting('gradientColor1', '#ff9a9e');
                g.color2 = g.color2 || getSetting('gradientColor2', '#fad0c4');
            }
            const gg = out.glassgradient;
            if (gg) {
                gg.color1 = gg.color1 || getSetting('gradientColor1', '#667eea');
                gg.color2 = gg.color2 || getSetting('gradientColor2', '#764ba2');
            }
            const ac3d = out.auroracrystal3d;
            if (ac3d) {
                ac3d.color1 = ac3d.color1 || getSetting('gradientColor1', '#667eea');
                ac3d.color2 = ac3d.color2 || getSetting('gradientColor2', '#764ba2');
            }
            return out;
        }

        function ac3dPctToBlur(pxPct) {
            const n = Number(pxPct);
            const x = Number.isFinite(n) ? Math.max(0, Math.min(100, Math.round(n))) : 50;
            return `${(x * 0.4).toFixed(1)}px`;
        }

        function ac3dPctToDistortScale(distPct) {
            const n = Number(distPct);
            const x = Number.isFinite(n) ? Math.max(0, Math.min(100, Math.round(n))) : 35;
            return Math.round(x * 0.55);
        }

        function ac3dPctToLensScale(distPct) {
            const n = Number(distPct);
            const x = Number.isFinite(n) ? Math.max(0, Math.min(100, n)) : 35;
            return (1 + x * 0.00085).toFixed(4);
        }

        function applyAuroraCrystal3dDisplacementScale(scale) {
            const el = document.getElementById('ac3d-displace-map');
            if (el) el.setAttribute('scale', String(Math.max(0, Math.min(80, Math.round(Number(scale) || 0)))));
        }

        function hasLiveVideoWallpaper() {
            const yt = document.getElementById('youtubeBackground');
            if (yt) return true;
            const vid = document.querySelector('video.video-background');
            return !!(vid && vid.src);
        }

        function buildAuroraCrystal3dBackdropImage() {
            if (hasLiveVideoWallpaper()) return 'none';
            const r = document.documentElement;
            const cs = getComputedStyle(document.body);
            const bgImg = cs.backgroundImage;
            if (bgImg && bgImg !== 'none') return bgImg;
            const c1 = getComputedStyle(r).getPropertyValue('--gradient-color-1').trim() || '#667eea';
            const c2 = getComputedStyle(r).getPropertyValue('--gradient-color-2').trim() || '#764ba2';
            return `linear-gradient(135deg, ${c1} 0%, ${c2} 48%, ${c1} 100%)`;
        }

        function updateAuroraCrystal3dLayer() {
            const layer = document.getElementById('ac3dDistortLayer');
            const on = document.body.classList.contains('auroracrystal3d');
            const videoWallpaper = hasLiveVideoWallpaper();
            document.body.classList.toggle('ac3d-video-wallpaper', on && videoWallpaper);
            if (!on) {
                document.body.classList.remove('ac3d-warp-off', 'ac3d-video-wallpaper');
                if (layer) layer.hidden = true;
                return;
            }
            const c = getEffectiveThemeColors('auroracrystal3d') || THEME_PALETTE_DEFAULTS.auroracrystal3d;
            const distortScale = ac3dPctToDistortScale(c.distortLevel);
            document.body.classList.toggle('ac3d-warp-off', distortScale < 2);
            applyAuroraCrystal3dDisplacementScale(distortScale);
            document.documentElement.style.setProperty('--ac3d-backdrop-image', buildAuroraCrystal3dBackdropImage());
            /* Con YouTube/MP4 el iframe/video queda visible; el cristal usa backdrop-filter sobre él. */
            if (layer) layer.hidden = videoWallpaper;
            if (!videoWallpaper && layer) layer.hidden = false;
        }

        /** @deprecated use updateAuroraCrystal3dLayer */
        function syncAuroraCrystal3dBackdrop() {
            updateAuroraCrystal3dLayer();
        }

        function getEffectiveThemeColors(theme) {
            const merged = mergeThemeColors()[theme];
            if (!merged) return null;
            const copy = { ...merged };
            const panel = document.getElementById('themePalettePanel');
            if (!panel) return copy;
            panel.querySelectorAll(`input[type="color"][data-tc-theme="${theme}"]`).forEach((inp) => {
                const key = inp.dataset.tcKey;
                if (key) copy[key] = inp.value;
            });
            panel.querySelectorAll(`input[type="range"][data-tc-theme="${theme}"]`).forEach((inp) => {
                const key = inp.dataset.tcKey;
                if (key) {
                    const n = parseFloat(inp.value);
                    copy[key] = Number.isFinite(n) ? Math.max(0, Math.min(100, n)) : inp.value;
                }
            });
            return copy;
        }

        function applyThemePaletteToDocument(theme) {
            const c = getEffectiveThemeColors(theme);
            if (!c) return;
            const r = document.documentElement;
            const runners = {
                neumorphism: () => {
                    r.style.setProperty('--neumorphic-bg', c.bg);
                    r.style.setProperty('--neumorphic-shadow-light', c.shadowLight);
                    r.style.setProperty('--neumorphic-shadow-dark', c.shadowDark);
                },
                glassmorphism: () => {
                    r.style.setProperty('--glass-tint', c.tint);
                    r.style.setProperty('--glass-border-tint', c.borderTint);
                },
                skeuomorphism: () => {
                    r.style.setProperty('--skeuo-gradient-top', c.top);
                    r.style.setProperty('--skeuo-gradient-bottom', c.bottom);
                    r.style.setProperty('--skeuo-border', c.border);
                },
                flat: () => {
                    r.style.setProperty('--flat-surface', c.surface);
                    r.style.setProperty('--flat-sidebar', c.sidebar);
                    r.style.setProperty('--flat-sidebar-border', c.sidebarBorder);
                },
                material: () => {
                    r.style.setProperty('--material-surface', c.surface);
                    r.style.setProperty('--material-sidebar', c.sidebar);
                    r.style.setProperty('--material-shadow-color', c.shadow);
                },
                brutalism: () => {
                    r.style.setProperty('--brutal-bg', c.bg);
                    r.style.setProperty('--brutal-accent', c.accent);
                    r.style.setProperty('--brutal-text', c.text);
                },
                retro: () => {
                    r.style.setProperty('--retro-bg', c.bg);
                    r.style.setProperty('--retro-sidebar', c.sidebar);
                    r.style.setProperty('--retro-text', c.text);
                    r.style.setProperty('--retro-border', c.border);
                },
                dark: () => {
                    r.style.setProperty('--dark-surface', c.surface);
                    r.style.setProperty('--dark-sidebar', c.sidebar);
                    r.style.setProperty('--dark-text', c.text);
                },
                minimalism: () => {
                    r.style.setProperty('--minimal-surface', c.surface);
                    r.style.setProperty('--minimal-sidebar', c.sidebar);
                    r.style.setProperty('--minimal-text', c.text);
                },
                morphing: () => {
                    r.style.setProperty('--morph-panel', c.panel);
                },
                isometric: () => {
                    r.style.setProperty('--iso-panel', c.panel);
                },
                cyberpunk: () => {
                    r.style.setProperty('--cyber-bg', c.bg);
                    r.style.setProperty('--cyber-neon', c.neon);
                    r.style.setProperty('--cyber-accent', c.accent);
                },
                gradient: () => {
                    r.style.setProperty('--gradient-color-1', c.color1);
                    r.style.setProperty('--gradient-color-2', c.color2);
                },
                glassgradient: () => {
                    r.style.setProperty('--gradient-color-1', c.color1);
                    r.style.setProperty('--gradient-color-2', c.color2);
                    r.style.setProperty('--glass-tint', c.tint);
                    r.style.setProperty('--glass-border-tint', c.borderTint);
                    const ggPct = (v, def) => {
                        const n = Number(v);
                        const x = Number.isFinite(n) ? Math.max(0, Math.min(100, Math.round(n))) : def;
                        return `${x}%`;
                    };
                    r.style.setProperty('--gg-layer1', ggPct(c.layer1Opacity, 48));
                    r.style.setProperty('--gg-layer2', ggPct(c.layer2Opacity, 42));
                    r.style.setProperty('--gg-tint-mix', ggPct(c.tintOpacity, 32));
                    r.style.setProperty('--gg-border-mix', ggPct(c.borderOpacity, 38));
                    r.style.setProperty('--gg-input-mix', ggPct(c.inputOpacity, 22));
                    r.style.setProperty('--gg-input-border-mix', ggPct(c.inputBorderOpacity, 24));
                },
                auroracrystal3d: () => {
                    runners.glassgradient();
                    r.style.setProperty('--ac3d-blur', ac3dPctToBlur(c.blurAmount));
                    r.style.setProperty('--ac3d-lens-scale', ac3dPctToLensScale(c.distortLevel));
                    updateAuroraCrystal3dLayer();
                },
                card: () => {
                    r.style.setProperty('--card-surface', c.surface);
                    r.style.setProperty('--card-sidebar', c.sidebar);
                },
                typography: () => {
                    r.style.setProperty('--typography-panel', c.panel);
                }
            };
            const run = runners[theme];
            if (run) run();
            applyExplorerPaletteVars(c);
        }

        function collectThemeColorsForSave() {
            const merged = mergeThemeColors();
            const out = {};
            THEME_IDS_ORDER.forEach((id) => {
                out[id] = { ...merged[id] };
            });
            const panel = document.getElementById('themePalettePanel');
            if (!panel) return out;
            panel.querySelectorAll('input[type="color"][data-tc-key]').forEach((inp) => {
                const tid = inp.dataset.tcTheme;
                const key = inp.dataset.tcKey;
                if (tid && key && out[tid]) out[tid][key] = inp.value;
            });
            panel.querySelectorAll('input[type="range"][data-tc-key]').forEach((inp) => {
                const tid = inp.dataset.tcTheme;
                const key = inp.dataset.tcKey;
                if (tid && key && out[tid]) {
                    const n = parseFloat(inp.value);
                    out[tid][key] = Number.isFinite(n) ? Math.max(0, Math.min(100, n)) : inp.value;
                }
            });
            return out;
        }

        function fillThemePaletteInputs() {
            const merged = mergeThemeColors();
            const panel = document.getElementById('themePalettePanel');
            if (!panel) return;
            panel.querySelectorAll('input[type="color"][data-tc-key]').forEach((inp) => {
                const tid = inp.dataset.tcTheme;
                const key = inp.dataset.tcKey;
                if (tid && key && merged[tid] && merged[tid][key] !== undefined) {
                    inp.value = merged[tid][key];
                }
            });
            panel.querySelectorAll('input[type="range"][data-tc-key]').forEach((inp) => {
                const tid = inp.dataset.tcTheme;
                const key = inp.dataset.tcKey;
                if (tid && key && merged[tid] && merged[tid][key] !== undefined) {
                    const n = Number(merged[tid][key]);
                    inp.value = String(Number.isFinite(n) ? Math.max(0, Math.min(100, Math.round(n))) : merged[tid][key]);
                }
                const meta = inp.closest('label') && inp.closest('label').querySelector('.theme-palette-range-meta');
                if (meta) meta.textContent = `${inp.value}%`;
            });
        }

        function updateThemePaletteSectionVisibility() {
            const sel = document.getElementById('themeSelect');
            if (!sel) return;
            const t = sel.value;
            document.querySelectorAll('[data-theme-palette]').forEach((el) => {
                el.style.display = el.getAttribute('data-theme-palette') === t ? 'block' : 'none';
            });
        }

        function initThemePalettePanel() {
            const panel = document.getElementById('themePalettePanel');
            if (!panel) return;
            if (panel.dataset.paletteV === '4') return;
            panel.dataset.paletteV = '4';
            panel.innerHTML = '';
            const frag = document.createDocumentFragment();
            const mergedAll = mergeThemeColors();
            THEME_IDS_ORDER.forEach((tid) => {
                const wrap = document.createElement('div');
                wrap.setAttribute('data-theme-palette', tid);
                wrap.style.display = 'none';
                wrap.className = 'theme-palette-section';
                const title = document.createElement('h4');
                title.className = 'theme-palette-heading';
                title.textContent = THEME_DISPLAY_NAMES[tid] || tid;
                wrap.appendChild(title);
                const keys = { ...THEME_PALETTE_DEFAULTS[tid], ...(EXPLORER_PALETTE_BY_THEME[tid] || {}) };
                const labels = { ...(THEME_PALETTE_LABELS[tid] || {}), ...FE_EXPLORER_LABELS };
                Object.keys(keys).forEach((key) => {
                    const isGgOpacity = tid === 'glassgradient' && GLASSGRADIENT_OPACITY_KEYS.includes(key);
                    const isAc3dRange = tid === 'auroracrystal3d' && AURORACRYSTAL3D_RANGE_KEYS.includes(key);
                    if (isGgOpacity || isAc3dRange) {
                        const row = document.createElement('div');
                        row.className = 'theme-palette-row';
                        const lab = document.createElement('label');
                        lab.className = 'theme-palette-range-label';
                        const titleSpan = document.createElement('span');
                        titleSpan.textContent = labels[key] || key;
                        const meta = document.createElement('span');
                        meta.className = 'theme-palette-range-meta';
                        const inp = document.createElement('input');
                        inp.type = 'range';
                        inp.min = '0';
                        inp.max = '100';
                        inp.step = '1';
                        inp.dataset.tcTheme = tid;
                        inp.dataset.tcKey = key;
                        const mv = mergedAll[tid] || {};
                        const defN = Number((THEME_PALETTE_DEFAULTS[tid] || {})[key]);
                        const cur = mv[key];
                        const n0 = Number(cur);
                        const v0 = Number.isFinite(n0) ? Math.max(0, Math.min(100, Math.round(n0))) : (Number.isFinite(defN) ? defN : 40);
                        inp.value = String(v0);
                        meta.textContent = `${inp.value}%`;
                        inp.addEventListener('input', () => {
                            meta.textContent = `${inp.value}%`;
                            const ts = document.getElementById('themeSelect');
                            if (ts && ts.value === tid) {
                                applyThemePaletteToDocument(tid);
                            }
                            refreshThemeContrast();
                        });
                        lab.appendChild(titleSpan);
                        lab.appendChild(meta);
                        lab.appendChild(inp);
                        row.appendChild(lab);
                        wrap.appendChild(row);
                        return;
                    }
                    const row = document.createElement('div');
                    row.className = 'theme-palette-row';
                    const lab = document.createElement('label');
                    lab.appendChild(document.createTextNode(labels[key] || key));
                    const inp = document.createElement('input');
                    inp.type = 'color';
                    inp.dataset.tcTheme = tid;
                    inp.dataset.tcKey = key;
                    inp.addEventListener('input', () => {
                        const ts = document.getElementById('themeSelect');
                        if (ts && ts.value === tid) {
                            applyThemePaletteToDocument(tid);
                        }
                        refreshThemeContrast();
                    });
                    lab.appendChild(inp);
                    row.appendChild(lab);
                    wrap.appendChild(row);
                });
                frag.appendChild(wrap);
            });
            panel.appendChild(frag);
            fillThemePaletteInputs();
            updateThemePaletteSectionVisibility();
        }

        function applyTheme(theme) {
            const themeClasses = ['neumorphism', 'glassmorphism', 'skeuomorphism', 'flat', 'material', 'brutalism', 'retro', 'dark', 'minimalism', 'morphing', 'isometric', 'cyberpunk', 'gradient', 'glassgradient', 'auroracrystal3d', 'card', 'typography'];
            document.body.classList.remove(...themeClasses);
            document.body.classList.add(theme);
            document.body.style.fontFamily = fontSelect.value;
            document.body.style.fontSize = fontSizeInput.value + 'px';
            document.documentElement.style.setProperty('--text-color', textColorPicker.value);
            document.documentElement.style.setProperty('--grid-text-color', gridTextColorPicker.value);
            currentTheme = theme;
            previewThemeActive = false;
            document.getElementById('themePreviewNotice').style.display = 'none';
            document.getElementById('restoreThemeBtn').style.display = 'none';
            const taskbarPos = getSetting('taskbarPosition', 'bottom');
            applyTaskbarPosition(taskbarPos);
            updateMobileMode(getSetting('mobileDesktopMode', false));
            updateActionButtonsVisibility();
            applyThemeToTaskbar();
            // Si el usuario está en Configuración -> Caché y papelera, los canvas del panel
            // dependen del color calculado en el momento de dibujar (p. ej. --text-color).
            // Redibujarlos evita que el explorador muestre colores "viejos" tras Aplicar.
            try {
                const explorerStorageTab = document.getElementById('explorerStorageTab');
                if (configModal && configModal.style.display === 'block' && explorerStorageTab && explorerStorageTab.classList.contains('active')) {
                    refreshExplorerStoragePanel();
                }
            } catch (_) {}
        }

        function previewTheme(theme) {
            const themeClasses = ['neumorphism', 'glassmorphism', 'skeuomorphism', 'flat', 'material', 'brutalism', 'retro', 'dark', 'minimalism', 'morphing', 'isometric', 'cyberpunk', 'gradient', 'glassgradient', 'auroracrystal3d', 'card', 'typography'];
            document.body.classList.remove(...themeClasses);
            document.body.classList.add(theme);
            document.body.style.fontFamily = fontSelect.value;
            document.body.style.fontSize = fontSizeInput.value + 'px';
            document.documentElement.style.setProperty('--text-color', textColorPicker.value);
            document.documentElement.style.setProperty('--grid-text-color', gridTextColorPicker.value);
            applyThemePaletteToDocument(theme);
            updateAuroraCrystal3dLayer();
            previewThemeActive = true;
            document.getElementById('themePreviewNotice').style.display = 'block';
            document.getElementById('restoreThemeBtn').style.display = 'block';
            refreshThemeContrast();
        }

        function restorePreviousTheme() {
            if (currentTheme) {
                themeSelect.value = currentTheme;
                previewThemeActive = false;
                applyTheme(currentTheme);
                document.getElementById('themePreviewNotice').style.display = 'none';
                document.getElementById('restoreThemeBtn').style.display = 'none';
            }
        }

        function toggleGradientInputs() {
            updateThemePaletteSectionVisibility();
        }

        const themeSelect = document.getElementById('themeSelect');
        const appLocaleSelect = document.getElementById('appLocaleSelect');
        const applyThemeBtn = document.getElementById('applyThemeBtn');
        const restoreThemeBtn = document.getElementById('restoreThemeBtn');

        if (appLocaleSelect) {
            appLocaleSelect.addEventListener('change', () => {
                const lang = appLocaleSelect.value;
                applyAppLocale(lang);
                // Guardar y refrescar etiquetas de menús en tiempo real.
                saveSettings({ appLocale: lang }).then(() => {
                    renderApps();
                    if (windowsMenuVisible) renderWindowsMenu(apps);
                    renderPinnedApps();
                    renderTaskbarApps();
                });
            });
        }

        themeSelect.addEventListener('change', () => {
            toggleGradientInputs();
            previewTheme(themeSelect.value);
        });

        restoreThemeBtn.addEventListener('click', restorePreviousTheme);

        applyThemeBtn.addEventListener('click', () => {
            const selectedTheme = themeSelect.value;
            const themeColors = collectThemeColorsForSave();
            const settingsPatch = {
                selectedTheme: selectedTheme,
                selectedFont: fontSelect.value,
                selectedFontSize: fontSizeInput.value + 'px',
                textColor: textColorPicker.value,
                gridTextColor: gridTextColorPicker.value,
                appLocale: appLocaleSelect ? appLocaleSelect.value : getSetting('appLocale', 'es'),
                themeColors: themeColors
            };
            if (selectedTheme === 'glassgradient' && themeColors.glassgradient) {
                settingsPatch.gradientColor1 = themeColors.glassgradient.color1;
                settingsPatch.gradientColor2 = themeColors.glassgradient.color2;
            } else if (selectedTheme === 'auroracrystal3d' && themeColors.auroracrystal3d) {
                settingsPatch.gradientColor1 = themeColors.auroracrystal3d.color1;
                settingsPatch.gradientColor2 = themeColors.auroracrystal3d.color2;
            } else if (themeColors.gradient) {
                settingsPatch.gradientColor1 = themeColors.gradient.color1;
                settingsPatch.gradientColor2 = themeColors.gradient.color2;
            }
            saveSettings(settingsPatch).then(() => {
                applyTheme(selectedTheme);
                applyAppLocale(settingsPatch.appLocale);
            });
        });

        gridTextColorPicker.addEventListener('input', function(e) {
            document.documentElement.style.setProperty('--grid-text-color', e.target.value);
            document.querySelectorAll('.app-item p').forEach(p => {
                p.style.color = e.target.value;
            });
        });

        // ========== FUNCIÓN PARA APLICAR MODO ESCRITORIO ==========
        window.applyDesktopMode = function() {
            const mode = document.querySelector('input[name="desktopMode"]:checked').value;
            const taskbarPos = document.querySelector('input[name="taskbarPosition"]:checked').value;
            const showBadge = showTypeBadgeCheck.checked;
            const badgeMode = document.querySelector('input[name="typeBadgeMode"]:checked').value;
            const mobileDesktopMode = mobileDesktopModeCheck ? mobileDesktopModeCheck.checked : false;
            saveSettings({ desktopMode: mode, taskbarPosition: taskbarPos, showTypeBadge: showBadge, typeBadgeMode: badgeMode, mobileDesktopMode: mobileDesktopMode }).then(async () => {
                renderApps();
                applyTaskbarPosition(taskbarPos);
                updateMobileMode(mobileDesktopMode);
                updateActionButtonsVisibility();
                await appDialog.alert('Configuración guardada.');
            });
        };

        // ========== FUNCIONES DE PINNED APPS ==========
        function renderPinnedApps() {
            if (!pinnedAppsContainer) return;
            pinnedAppsContainer.innerHTML = '';
            
            // Normalización: evitar duplicados persistidos en pinnedApps.
            const seen = new Set();
            pinnedApps = pinnedApps.filter(p => {
                const key = `${p.title}::${p.link || ''}`;
                if (seen.has(key)) return false;
                seen.add(key);
                return true;
            });

            pinnedApps.forEach((pinned, idx) => {
                const app = apps.find(a => a.title === pinned.title && getAppLinkKey(a) === (pinned.link || ''));
                if (!app) return;
                const pinDiv = document.createElement('div');
                pinDiv.className = 'pinned-app';
                pinDiv.title = app.title;
                pinDiv.setAttribute('title', app.title);
                
                const appGroup = activeTaskbarApps.find(a => a.appId === getAppStableId(app));
                const openCount = appGroup ? appGroup.windows.length : 0;
                const hasMinimized = appGroup ? appGroup.windows.some(w => w.isMinimized) : false;
                
                pinDiv.innerHTML = `
                    <img src="${app.icon}" alt="${app.title}">
                    <span class="window-count" style="display: ${openCount > 0 ? 'flex' : 'none'}">${openCount}</span>
                    ${hasMinimized ? '<span class="minimized-indicator" style="position:absolute; bottom:-2px; left:50%; transform:translateX(-50%); width:6px; height:6px; background:#ffbd2e; border-radius:50%;"></span>' : ''}
                `;
                pinDiv.draggable = true;
                pinDiv.dataset.pinTitle = pinned.title;
                pinDiv.dataset.pinLink = pinned.link || '';
                const pinImg = pinDiv.querySelector('img');
                if (pinImg) pinImg.draggable = false;
                pinDiv.addEventListener('dragstart', (e) => {
                    e.dataTransfer.effectAllowed = 'move';
                    e.dataTransfer.setData('text/plain', 'pinned');
                    pinDiv.classList.add('taskbar-dragging');
                });
                pinDiv.addEventListener('dragend', () => {
                    pinDiv.classList.remove('taskbar-dragging');
                });

                pinDiv.addEventListener('mouseenter', (e) => {
                    if (!appGroup || appGroup.windows.length === 0) return;
                    if (hoverPreviewTimeout) clearTimeout(hoverPreviewTimeout);
                    hoverPreviewTimeout = setTimeout(() => {
                        showWindowPreview(appGroup, pinDiv);
                    }, 400);
                });

                pinDiv.addEventListener('mouseleave', () => {
                    if (hoverPreviewTimeout) clearTimeout(hoverPreviewTimeout);
                    setTimeout(() => {
                        if (currentHoverPreview && !currentHoverPreview.matches(':hover')) {
                            currentHoverPreview.remove();
                            currentHoverPreview = null;
                        }
                    }, 300);
                });

                pinDiv.addEventListener('click', (e) => {
                    e.stopPropagation();
                    if (Date.now() < taskbarDragSuppressClickUntil) return;
                    
                    // UX: si ya hay ventanas abiertas para esta app, restaurar en lugar de duplicar.
                    if (appGroup && appGroup.windows.length > 0) {
                        if (appGroup.windows.length === 1) {
                            const activeWindow = appGroup.windows[0];
                            if (activeWindow.isMinimized) {
                                activeWindow.frame.classList.remove('minimized');
                                activeWindow.frame.style.display = 'flex';
                                activeWindow.frame.style.zIndex = ++windowZIndex;
                                activeWindow.isMinimized = false;
                                appGroup.activeWindowId = activeWindow.id;
                            } else if (appGroup.activeWindowId === activeWindow.id) {
                                activeWindow.frame.classList.add('minimized');
                                activeWindow.frame.style.display = 'none';
                                activeWindow.isMinimized = true;
                                appGroup.activeWindowId = null;
                            } else {
                                activeWindow.frame.style.zIndex = ++windowZIndex;
                                appGroup.activeWindowId = activeWindow.id;
                            }
                        } else if (appGroup.windows.length > 1) {
                            showWindowPreview(appGroup, pinDiv);
                        }
                        renderPinnedApps();
                        renderTaskbarApps();
                        return;
                    }

                    // Si no hay ventana abierta, abrir la app.
                    launchDesktopApp(app);
                });
                pinDiv.addEventListener('contextmenu', (e) => {
                    e.preventDefault();
                    e.stopPropagation();
                    if (appGroup && appGroup.windows.length > 0) {
                        showWindowContextMenu(appGroup, pinDiv, e);
                    } else {
                        // El explorador no se puede desanclar.
                        if (app && app.id === 'file-explorer-system') {
                            const options = [
                                { className: 'open-btn', icon: 'fas fa-folder-open', label: tr('open'), action: () => openFileExplorerWindow(app) }
                            ];
                            showContextMenu(options, e);
                            return;
                        }
                        const options = [
                            { className: 'pin-btn', icon: 'far fa-star', label: tr('unpinTaskbar'), action: () => unpinApp(app) }
                        ];
                        showContextMenu(options, e);
                    }
                });
                pinnedAppsContainer.appendChild(pinDiv);
            });
            renderMobileAppHub();
            updateMobileHubBadge();
        }

        function pinApp(app) {
            if (!pinnedApps.some(p => pinMatchesApp(p, app))) {
                pinnedApps.push({ title: app.title, link: getAppLinkKey(app), icon: app.icon });
                saveSettings({ pinnedApps: pinnedApps }).then(() => renderPinnedApps());
                showToast(`${app.title} anclado a la barra de tareas`);
                return true;
            } else {
                showToast(`${app.title} ya está anclado a la barra de tareas`);
                return false;
            }
        }

        function unpinApp(app) {
            if (app && app.id === 'file-explorer-system') return;
            pinnedApps = pinnedApps.filter(p => !pinMatchesApp(p, app));
            saveSettings({ pinnedApps: pinnedApps }).then(() => renderPinnedApps());
            showToast(`${app.title} desanclado de la barra de tareas`);
        }

        function updatePinnedAppsOnDelete(app) {
            pinnedApps = pinnedApps.filter(p => !pinMatchesApp(p, app));
            saveSettings({ pinnedApps: pinnedApps }).then(() => renderPinnedApps());
        }

        // ========== FUNCIONES DE SEGURIDAD ==========
        window.saveSecuritySettings = function() {
            const block = inspectBlockCheck.checked;
            saveSettings({ inspectBlock: block }).then(async () => {
                if (block) {
                    updateInspectBlock(true);
                } else {
                    updateInspectBlock(false);
                }
                await appDialog.alert('Configuración guardada.');
            });
        };

        // ========== BACKUP ZIP y LOGS ==========
        async function refreshActivityLog() {
            if (!activityLogBox) return;
            try {
                const res = await fetch('?action=getActivityLog', {
                    method: 'POST',
                    headers: { 'Content-Type': 'application/json' },
                    body: JSON.stringify({ limit: 250 })
                });
                const data = await res.json();
                if (data && data.success) {
                    activityLogBox.value = (data.lines || []).join('\n');
                } else {
                    activityLogBox.value = 'No se pudo cargar el log.';
                }
            } catch (_) {
                activityLogBox.value = 'Error al cargar el log.';
            }
        }

        async function clearActivityLog() {
            try {
                await fetch('?action=clearActivityLog', { method: 'POST' });
                await refreshActivityLog();
                showToast('Historial eliminado');
            } catch (_) {}
        }

        async function rotateActivityLog() {
            try {
                await fetch('?action=rotateActivityLog', { method: 'POST' });
                await refreshActivityLog();
                showToast('Nuevo log creado');
            } catch (_) {}
        }

        if (exportBackupBtn) {
            exportBackupBtn.addEventListener('click', () => {
                window.location.href = '?action=exportAllZip';
            });
        }

        if (importBackupBtn && importBackupZipInput) {
            importBackupBtn.addEventListener('click', async () => {
                const file = importBackupZipInput.files && importBackupZipInput.files[0];
                if (!file) {
                    await appDialog.alert('Selecciona un ZIP para importar.');
                    return;
                }
                const fd = new FormData();
                fd.append('zipFile', file, file.name);
                try {
                    const res = await fetch('?action=importAllZip', { method: 'POST', body: fd });
                    const data = await res.json();
                    if (data && data.success) {
                        showToast('Backup importado. Recargando...');
                        setTimeout(() => location.reload(), 800);
                    } else {
                        await appDialog.alert(data?.error || 'No se pudo importar el ZIP');
                    }
                } catch (_) {
                    await appDialog.alert('Error importando el ZIP');
                }
            });
        }

        function setFormatReinstallModeEnabled(on) {
            if (formatModeCheckbox) formatModeCheckbox.checked = on;
            if (diskRecoveryFormatModeCheckbox) diskRecoveryFormatModeCheckbox.checked = on;
            if (formatLaunchInstallBtn) formatLaunchInstallBtn.disabled = !on;
            if (diskRecoveryFormatLaunchBtn) diskRecoveryFormatLaunchBtn.disabled = !on;
        }

        /** Modo formateo: siempre apagado al cargar (no persiste entre sesiones). */
        function initFormatReinstallModeOff() {
            try { localStorage.removeItem('flexia_format_mode_enabled'); } catch (_) {}
            setFormatReinstallModeEnabled(false);
        }

        function onFormatModeCheckboxChange(ev) {
            const on = !!(ev.target && ev.target.checked);
            setFormatReinstallModeEnabled(on);
        }

        if (formatModeCheckbox) {
            formatModeCheckbox.addEventListener('change', onFormatModeCheckboxChange);
        }
        if (diskRecoveryFormatModeCheckbox) {
            diskRecoveryFormatModeCheckbox.addEventListener('change', onFormatModeCheckboxChange);
        }
        initFormatReinstallModeOff();

        async function runPrepareInstallWizardFormat() {
            const armed = Boolean(
                (formatModeCheckbox && formatModeCheckbox.checked)
                || (diskRecoveryFormatModeCheckbox && diskRecoveryFormatModeCheckbox.checked)
            );
            if (!armed) return;
            const confirmToggle = appDialog && typeof appDialog.confirmToggle === 'function'
                ? appDialog.confirmToggle.bind(appDialog)
                : null;
            let keep = false;
            if (confirmToggle) {
                const result = await confirmToggle(tr('formatReinstallConfirm1'), {
                    label: tr('formatKeepUserDocsLabel'),
                    checked: false,
                    hintOff: tr('formatKeepUserDocsHintOff'),
                    hintOn: tr('formatKeepUserDocsHintOn'),
                }, {
                    type: 'danger',
                    title: tr('formatReinstallConfirmTitle'),
                    okLabel: tr('formatLaunchInstallBtn'),
                });
                if (!result || !result.confirmed) return;
                keep = !!result.checked;
            } else {
                if (!(await appDialog.confirm(tr('formatReinstallConfirm1')))) return;
            }
            const base = (typeof BASE_URL !== 'undefined' && BASE_URL) ? BASE_URL : '';
            const lang = encodeURIComponent(__currentLocale || 'es');
            window.location.href = base + 'install/formatting.php?keep=' + (keep ? '1' : '0') + '&lang=' + lang;
        }
        if (formatLaunchInstallBtn) {
            formatLaunchInstallBtn.addEventListener('click', runPrepareInstallWizardFormat);
        }
        if (diskRecoveryFormatLaunchBtn) {
            diskRecoveryFormatLaunchBtn.addEventListener('click', runPrepareInstallWizardFormat);
        }

        if (refreshLogBtn) refreshLogBtn.addEventListener('click', refreshActivityLog);
        if (clearLogBtn) clearLogBtn.addEventListener('click', clearActivityLog);
        if (rotateLogBtn) rotateLogBtn.addEventListener('click', rotateActivityLog);

        // Cargar log al iniciar (si el elemento existe).
        if (activityLogBox) refreshActivityLog();

        // ========== FUNCIONES DE ICONO DE INICIO ==========
        function previewStartIcon(file) {
            const reader = new FileReader();
            reader.onload = (e) => {
                const previewDiv = document.getElementById('startButtonPreview');
                previewDiv.innerHTML = `<img src="${e.target.result}" alt="" class="config-start-icon-preview">`;
            };
            reader.readAsDataURL(file);
        }

        startButtonIconInput.addEventListener('change', (e) => {
            if (e.target.files && e.target.files[0]) {
                previewStartIcon(e.target.files[0]);
            }
        });

        window.uploadStartIcon = async function() {
            const file = startButtonIconInput.files[0];
            if (!file) return;
            const formData = new FormData();
            formData.append('file', file);
            try {
                const response = await fetch('?action=uploadMedia', { method: 'POST', body: formData });
                const result = await response.json();
                if (result.success) {
                    saveSettings({ startButtonIcon: result.url }).then(() => {
                        updateStartButtonIcon(result.url);
                        const previewDiv = document.getElementById('startButtonPreview');
                        previewDiv.innerHTML = `<img src="${result.url}" alt="" class="config-start-icon-preview">`;
                        showToast('Icono de inicio actualizado');
                    });
                }
            } catch (error) {
                console.error('Error subiendo icono:', error);
            }
        };

        window.removeStartIcon = function() {
            saveSettings({ startButtonIcon: '' }).then(() => {
                updateStartButtonIcon('');
                const previewDiv = document.getElementById('startButtonPreview');
                previewDiv.innerHTML = '';
                showToast('Icono de inicio eliminado');
            });
        };

        window.addEventListener('click', (e) => {
            if (e.target === addAppModal) {
                addAppModal.style.display = 'none';
                document.body.classList.remove('modal-open');
                if (window.__pendingRestore && window.__pendingRestore.category) {
                    activeWindowsCategory = window.__pendingRestore.category;
                    if (windowsMenuVisible) renderWindowsMenu(apps);
                }
            }
            if (e.target === configModal) {
                if (previewThemeActive) {
                    applyTheme(currentTheme);
                    themeSelect.value = currentTheme;
                }
                configModal.style.display = 'none';
                document.body.classList.remove('modal-open');
            }
            if (e.target === addCategoryModal) {
                addCategoryModal.style.display = 'none';
                document.body.classList.remove('modal-open');
            }
            if (editCategoryIconModal && e.target === editCategoryIconModal) {
                closeEditCategoryIconModal();
            }
            if (e.target === document.getElementById('previewModal')) {
                closePreviewModal();
            }
            if (windowsMosaicModal && e.target === windowsMosaicModal) {
                windowsMosaicModal.style.display = 'none';
                document.body.classList.remove('modal-open');
            }
            if (e.target === recycleBinModal) {
                recycleBinModal.style.display = 'none';
                document.body.classList.remove('modal-open');
            }
            if (currentContextMenu && !currentContextMenu.contains(e.target)) {
                currentContextMenu.remove();
                currentContextMenu = null;
            }
            if (currentHoverPreview && !currentHoverPreview.contains(e.target)) {
                currentHoverPreview.remove();
                currentHoverPreview = null;
            }
        });

        document.addEventListener('click', (e) => {
            if (!stripMenu.contains(e.target) && !desktopStripMenu.contains(e.target) && !e.target.closest('.app-item') && e.target !== stripMenuBackdrop) {
                hideStripMenu();
            }
        });

        document.addEventListener('DOMContentLoaded', () => {
            applyStoredConfigurations();
            initThemePalettePanel();
            applyAppLocale(getSetting('appLocale', 'es'));
            renderCategories();
            renderApps();
            toggleGradientInputs();
            updateRecycleBinIcon();
            bindTaskbarIconReorder();
            renderPinnedApps();
            renderTaskbarApps();
            renderImageList();
            renderVideoList();
            restoreOpenWindowsSession();

            // Vincular personalización del explorador también desde Configuración.
            try {
                const btn = document.getElementById('openExplorerThemeModalBtn');
                if (btn) {
                    btn.addEventListener('click', () => {
                        initExplorerThemeModalOnce();
                        if (typeof window.openExplorerThemeModal === 'function') window.openExplorerThemeModal();
                    });
                }
            } catch (_) {}
            scheduleGridDesktopHealthCheck();

            try {
                if (localStorage.getItem('gridShowRecoverBtn') === '1') {
                    const fab = document.createElement('button');
                    fab.type = 'button';
                    fab.id = 'gridDesktopRecoverFab';
                    fab.textContent = 'Restaurar escritorio';
                    fab.setAttribute('aria-label', 'Restaurar escritorio');
                    fab.style.cssText = 'position:fixed;bottom:14px;left:14px;z-index:999998;padding:10px 14px;font-size:13px;cursor:pointer;border-radius:10px;border:1px solid rgba(0,0,0,.12);background:var(--neumorphic-bg);color:var(--text-color);box-shadow:0 4px 14px rgba(0,0,0,.15);';
                    fab.addEventListener('click', () => {
                        try { sessionStorage.removeItem(GRID_DESKTOP_RECOVER_COUNT_KEY); } catch (_) {}
                        gridRecoverDesktopUi();
                    });
                    document.body.appendChild(fab);
                }
            } catch (_) {}
        });

        async function refreshSystemCreditsPanel() {
            const statusEl = document.getElementById('systemCreditsHostingStatus');
            const dlEl = document.getElementById('systemCreditsHostingDl');
            if (!statusEl || !dlEl) return;
            statusEl.textContent = tr('systemCreditsHostingLoading');
            dlEl.innerHTML = '';
            const trafficNoteElLoad = document.getElementById('systemCreditsTrafficNote');
            if (trafficNoteElLoad) trafficNoteElLoad.textContent = '';
            try {
                const res = await fetch('?action=osEnvironment');
                const data = await res.json();
                if (!data || data.success !== true) throw new Error('bad');
                statusEl.textContent = '';
                const esc = (s) => escapeHtml(String(s));
                const rows = [];
                const pushRow = (labelKey, val) => {
                    if (val === null || val === undefined || val === '') return;
                    rows.push(`<dt>${esc(tr(labelKey))}</dt><dd>${esc(String(val))}</dd>`);
                };
                const srv = data.server || {};
                const php = data.php || {};
                const envKey = data.environment;
                const envHuman = (data.labels && envKey && data.labels[envKey]) ? data.labels[envKey] : (envKey || '');
                pushRow('systemCreditsHostHttp', srv.host);
                pushRow('systemCreditsServerSoftware', srv.software);
                pushRow('systemCreditsPhpVersion', php.version);
                pushRow('systemCreditsPhpSapi', php.sapi);
                pushRow('systemCreditsOsFamily', php.osFamily);
                pushRow('systemCreditsEnvironment', envHuman);
                const disk = data.disk;
                if (disk && typeof disk.usedBytes === 'number' && typeof disk.totalBytes === 'number') {
                    const line = `${formatBytes(disk.usedBytes)} / ${formatBytes(disk.totalBytes)}` + (disk.usedPercent != null ? ` (${disk.usedPercent}%)` : '');
                    pushRow('systemCreditsDisk', line);
                }
                const pm = data.phpMemory;
                if (pm && typeof pm.peakBytes === 'number') {
                    const lim = pm.limitLabel || '—';
                    pushRow('systemCreditsPhpMemory', `${formatBytes(pm.peakBytes)} / ${lim}`);
                }
                const la = data.loadAvg;
                if (la && typeof la['1m'] === 'number') {
                    pushRow('systemCreditsLoadAvg', String(la['1m']));
                }
                if (srv.httpsActive === true || srv.httpsActive === false) {
                    const httpsLbl = (__currentLocale && String(__currentLocale).indexOf('en') === 0)
                        ? (srv.httpsActive ? 'Yes' : 'No')
                        : (srv.httpsActive ? 'Sí' : 'No');
                    pushRow('systemCreditsHttps', httpsLbl);
                }
                const traffic = data.traffic;
                if (traffic && traffic.global) {
                    const g = traffic.global;
                    pushRow('systemCreditsTrafficGlobalToday',
                        '↓' + formatBytes(g.todayInBytes) + ' · ↑' + formatBytes(g.todayOutBytes));
                    pushRow('systemCreditsTrafficGlobalTotal',
                        '↓' + formatBytes(g.totalInBytes) + ' · ↑' + formatBytes(g.totalOutBytes));
                }
                if (traffic && traffic.user) {
                    const u = traffic.user;
                    pushRow('systemCreditsTrafficUserToday',
                        '↓' + formatBytes(u.todayInBytes) + ' · ↑' + formatBytes(u.todayOutBytes));
                    pushRow('systemCreditsTrafficUserTotal',
                        '↓' + formatBytes(u.totalInBytes) + ' · ↑' + formatBytes(u.totalOutBytes));
                }
                const trafficNoteEl = document.getElementById('systemCreditsTrafficNote');
                if (trafficNoteEl) {
                    trafficNoteEl.textContent = (traffic && traffic.global)
                        ? tr('systemCreditsTrafficScopeNote')
                        : '';
                }
                const panelUrl = String(getSetting('hostingPanelUrl', '') || '').trim();
                if (panelUrl) {
                    const safeUrl = panelUrl.replace(/"/g, '&quot;');
                    rows.push('<dt>' + esc(tr('systemCreditsHostingPanel')) + '</dt><dd><a href="' + safeUrl + '" target="_blank" rel="noopener noreferrer">' + esc(panelUrl) + '</a></dd>');
                }
                if (data.alerts && data.alerts.length) {
                    const alertText = data.alerts.map((a) => (a && a.message) ? String(a.message) : '').filter(Boolean).join(' · ');
                    if (alertText) pushRow('systemCreditsAlerts', alertText);
                }
                const ts = data.timestamp;
                if (typeof ts === 'number') {
                    try {
                        pushRow('systemCreditsServerTime', new Date(ts * 1000).toLocaleString(__currentLocale || undefined));
                    } catch (_) {
                        pushRow('systemCreditsServerTime', String(ts));
                    }
                }
                dlEl.innerHTML = rows.join('');
            } catch (_) {
                statusEl.textContent = tr('systemCreditsHostingError');
            }
        }

        const tabs = document.querySelectorAll('.tab');
        tabs.forEach(tab => {
            tab.addEventListener('click', () => {
                tabs.forEach(t => t.classList.remove('active'));
                tab.classList.add('active');
                document.querySelectorAll('.tab-content').forEach(c => c.classList.remove('active'));
                document.getElementById(`${tab.dataset.tab}Tab`).classList.add('active');
                if (tab.dataset.tab === 'explorerStorage') {
                    refreshExplorerStoragePanel();
                }
                if (tab.dataset.tab === 'systemCredits') {
                    refreshSystemCreditsPanel();
                }
                if (tab.dataset.tab === 'explorerCustomization' && window.GridExplorerCustomization) {
                    window.GridExplorerCustomization.fetchCustomization(true).then(() => {
                        window.GridExplorerCustomization.refreshConfigPanel();
                    }).catch(() => {});
                }
                if (tab.dataset.tab === 'integrations') {
                    try { if (typeof window.__flexiaRefreshIntegrationsTab === 'function') window.__flexiaRefreshIntegrationsTab(); } catch (_) {}
                }
            });
        });

        const explorerEmptyRemoteCacheBtn = document.getElementById('explorerEmptyRemoteCacheBtn');
        if (explorerEmptyRemoteCacheBtn) {
            explorerEmptyRemoteCacheBtn.addEventListener('click', async () => {
                if (!(await appDialog.confirm(tr('explorerConfirmEmptyRemoteCache')))) return;
                const r = await explorerEmptyRemoteCache().catch(() => null);
                if (r && r.success) {
                    logActivity('explorer_empty_remote_cache', {});
                    await refreshExplorerStoragePanel();
                } else {
                    await appDialog.alert(tr('explorerOperationFailed'));
                }
            });
        }
        const explorerEmptyTrashFromConfigBtn = document.getElementById('explorerEmptyTrashFromConfigBtn');
        if (explorerEmptyTrashFromConfigBtn) {
            explorerEmptyTrashFromConfigBtn.addEventListener('click', async () => {
                if (!(await appDialog.confirm(tr('explorerConfirmEmptyTrashExplorer')))) return;
                const r = await explorerEmptyTrash().catch(() => null);
                if (r && r.success) {
                    fileExplorerTrash = [];
                    if (typeof settings === 'object' && settings) settings.fileExplorerTrash = [];
                    updateRecycleBinIcon();
                    await fetchExplorerListTrash().catch(() => {});
                    logActivity('explorer_empty_trash', {});
                    await refreshExplorerStoragePanel();
                } else {
                    await appDialog.alert(tr('explorerOperationFailed'));
                }
            });
        }
        const explorerStorageRefreshBtn = document.getElementById('explorerStorageRefreshBtn');
        if (explorerStorageRefreshBtn) {
            explorerStorageRefreshBtn.addEventListener('click', () => refreshExplorerStoragePanel({ forceRefresh: true }));
        }
        const gridClearAssetCacheBtn = document.getElementById('gridClearAssetCacheBtn');
        if (gridClearAssetCacheBtn) {
            gridClearAssetCacheBtn.addEventListener('click', async () => {
                if (!(await appDialog.confirm(tr('explorerConfirmClearAssetCache')))) return;
                await gridClearAssetCacheAndReload();
            });
        }
        const explorerStorageShortcutMap = [
            ['explorerStorageOpenFilesBtn', 'files'],
            ['explorerStorageOpenCacheBtn', 'cache'],
            ['explorerStorageOpenTrashBtn', 'trash']
        ];
        explorerStorageShortcutMap.forEach(([id, kind]) => {
            const el = document.getElementById(id);
            if (el) el.addEventListener('click', () => openExplorerStorageShortcut(kind));
        });

        const subTabs = document.querySelectorAll('.sub-tab');
        subTabs.forEach(tab => {
            tab.addEventListener('click', () => {
                subTabs.forEach(t => t.classList.remove('active'));
                tab.classList.add('active');
                document.querySelectorAll('.sub-tab-content').forEach(c => c.classList.remove('active'));
                if (tab.dataset.subtab === 'single') {
                    document.getElementById('youtubeSingle').classList.add('active');
                } else {
                    document.getElementById('youtubePlaylist').classList.add('active');
                }
            });
        });

        addCategoryBtn.addEventListener('click', (e) => {
            e.preventDefault();
            e.stopPropagation();
            if (window.OsShell && typeof window.OsShell.resetCategoryModal === 'function') window.OsShell.resetCategoryModal();
            addCategoryModal.style.display = 'block';
            document.body.classList.add('modal-open');
        });

        document.getElementById('cancelCategoryBtn').addEventListener('click', () => {
            addCategoryModal.style.display = 'none';
            document.body.classList.remove('modal-open');
            if (categoryForm) categoryForm.reset();
            if (window.OsShell && typeof window.OsShell.resetCategoryModal === 'function') window.OsShell.resetCategoryModal();
        });

        const editCategoryIconSaveBtn = document.getElementById('editCategoryIconSaveBtn');
        const editCategoryIconCancelBtn = document.getElementById('editCategoryIconCancelBtn');
        const editCategoryIconModalCloseX = document.getElementById('editCategoryIconModalCloseX');
        if (editCategoryIconSaveBtn) editCategoryIconSaveBtn.addEventListener('click', () => saveEditCategoryIconModal());
        if (editCategoryIconCancelBtn) editCategoryIconCancelBtn.addEventListener('click', () => closeEditCategoryIconModal());
        if (editCategoryIconModalCloseX) editCategoryIconModalCloseX.addEventListener('click', () => closeEditCategoryIconModal());

        appFileInput.addEventListener('change', async (e) => {
            const file = e.target.files[0];
            if (file) {
                const allowedTypes = [
                    'application/pdf', 'application/msword', 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
                    'application/vnd.ms-excel', 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
                    'application/vnd.ms-powerpoint', 'application/vnd.openxmlformats-officedocument.presentationml.presentation',
                    'text/plain', 'image/jpeg', 'image/png', 'image/gif', 'video/mp4', 'video/webm', 'video/ogg',
                    'audio/mpeg', 'audio/wav', 'audio/ogg'
                ];
                if (allowedTypes.includes(file.type)) {
                    currentFile = file;
                    currentFileName.textContent = `Archivo: ${file.name}`;
                    currentFileInfo.style.display = 'flex';
                } else {
                    await appDialog.alert('Tipo de archivo no permitido.');
                    appFileInput.value = '';
                    currentFile = null;
                    currentFileInfo.style.display = 'none';
                }
            }
        });

        function showFilePreview(fileInfo) {
            const modalTitle = document.getElementById('previewModalTitle');
            const previewContent = document.getElementById('previewContent');
            const shareBtn = document.getElementById('shareBtn');
            const downloadBtn = document.getElementById('downloadBtn');
            const previewActions = document.getElementById('previewActions');
            const previewPrintBtn = document.getElementById('previewPrintBtn');

            previewZoomMode = 'none';
            previewPdfZoom = 1;
            if (previewPrintBtn) previewPrintBtn.style.display = 'none';

            modalTitle.textContent = `Previsualización: ${fileInfo.name}`;
            
            const fileUrl = fileInfo.url.startsWith('http') ? fileInfo.url : BASE_URL + fileInfo.url;

            // Ajustar ventana de previsualización (ventana tipo iframe).
            if (previewWindowFrame) {
                previewIsMaximized = false;
                previewSavedRect = null;
                previewWindowFrame.classList.remove('maximized');
                const vr = getLayoutViewportRect();
                const w = Math.min(920, Math.max(320, vr.width - 60));
                const h = Math.min(680, Math.max(220, vr.height - 140));
                previewWindowFrame.style.top = (vr.top + 60) + 'px';
                previewWindowFrame.style.left = (vr.left + 50) + 'px';
                previewWindowFrame.style.width = w + 'px';
                previewWindowFrame.style.height = h + 'px';
            }

            // Defaults: ocultar zoom y dejar solo “Abrir”.
            if (previewZoomInBtn) previewZoomInBtn.style.display = 'none';
            if (previewZoomOutBtn) previewZoomOutBtn.style.display = 'none';
            if (previewZoomResetBtn) previewZoomResetBtn.style.display = 'none';

            if (previewExternalBtn) {
                previewExternalBtn.dataset.externalUrl = fileUrl;
            }

            // Reiniciar estado de zoom/pan para cada apertura.
            detachPreviewFeTabsObserver();
            previewViewportWidthSnapshot = 0;
            activePreviewImg = null;
            isImageZoomed = false;
            scale = 1;
            translateX = 0;
            translateY = 0;
            previewContent.classList.remove('preview-text-plain-host');

            if (fileInfo.type.startsWith('image/')) {
                if (previewActions) previewActions.style.display = 'none';
                previewContent.innerHTML = `
                    <div class="image-preview-wrapper">
                        <div class="image-preview-container" id="imagePreviewContainer">
                            <img src="${fileUrl}" id="previewImage" alt="">
                        </div>
                        <div class="image-preview-controls-bottom" id="imagePreviewControlsBottom">
                            <button id="previewZoomInBottomBtn" class="bottom-zoom-btn" title="+"><i class="fas fa-search-plus"></i></button>
                            <button id="previewZoomOutBottomBtn" class="bottom-zoom-btn" title="-"><i class="fas fa-search-minus"></i></button>
                            <button id="previewZoomResetBottomBtn" class="bottom-zoom-btn" title="0"><i class="fas fa-undo"></i></button>
                            <button id="previewShareBottomBtn" class="bottom-action-btn" title="${tr('share')}"><i class="fas fa-share-alt"></i><span>${tr('share')}</span></button>
                            <button id="previewDownloadBottomBtn" class="bottom-action-btn" title="${tr('download')}"><i class="fas fa-download"></i><span>${tr('download')}</span></button>
                        </div>
                    </div>
                `;
                activePreviewImg = document.getElementById('previewImage');
                isImageZoomed = false;
                scale = 1;
                translateX = 0;
                translateY = 0;

                // Mostrar controles de zoom para imagen.
                if (previewZoomInBtn) previewZoomInBtn.style.display = '';
                if (previewZoomOutBtn) previewZoomOutBtn.style.display = '';
                if (previewZoomResetBtn) previewZoomResetBtn.style.display = '';

                if (previewExternalBtn) {
                    previewExternalBtn.dataset.externalUrl = fileUrl;
                }
                
                // Visor FTP (modal): pan SOLO con 1 dedo / botón izq, sin enganchar drag global en document.
                // Esto evita interacciones raras (scroll/viewport) que terminaban en "zoom out" automático.
                (function setupFtpModalImagePan() {
                    const box = document.getElementById('imagePreviewContainer');
                    if (!box || !activePreviewImg) return;
                    let pid = null;
                    let dragging = false;
                    let ox = 0, oy = 0;
                    box.addEventListener('pointerdown', (e) => {
                        if (!isImageZoomed) return;
                        if (e.pointerType === 'mouse' && e.button !== 0) return;
                        if (e.pointerType === 'touch' && e.isPrimary === false) return;
                        if (e.pointerType === 'touch') {
                            // Si el usuario apoya 2 dedos, el pinch lo maneja setupImagePinchZoom.
                            // No iniciar pan aquí.
                            if (typeof e.width === 'number' && typeof e.height === 'number') {
                                // no-op: solo para mantener compatibilidad
                            }
                        }
                        e.preventDefault();
                        e.stopPropagation();
                        pid = e.pointerId;
                        dragging = true;
                        try { box.setPointerCapture(pid); } catch (_) {}
                        ox = e.clientX - translateX;
                        oy = e.clientY - translateY;
                        markFtpImageInteractionActive(650);
                    }, { passive: false });
                    box.addEventListener('pointermove', (e) => {
                        if (!dragging || pid === null || e.pointerId !== pid) return;
                        if (!isImageZoomed) return;
                        // Si aparece un segundo dedo (pinch), dejamos de panear.
                        if (e.pointerType === 'touch' && e.isPrimary === false) return;
                        e.preventDefault();
                        e.stopPropagation();
                        translateX = e.clientX - ox;
                        translateY = e.clientY - oy;
                        updateImageTransform();
                        markFtpImageInteractionActive(650);
                    }, { passive: false });
                    const end = (e) => {
                        if (pid === null || e.pointerId !== pid) return;
                        dragging = false;
                        try { box.releasePointerCapture(pid); } catch (_) {}
                        pid = null;
                        markFtpImageInteractionActive(250);
                    };
                    box.addEventListener('pointerup', end);
                    box.addEventListener('pointercancel', end);
                })();
                activePreviewImg.addEventListener('wheel', (e) => {
                    const box = document.getElementById('imagePreviewContainer') || activePreviewImg.parentElement;
                    handleImageViewerWheel(e, box);
                }, { passive: false });

                const imgPreviewBox = document.getElementById('imagePreviewContainer');
                if (imgPreviewBox && activePreviewImg) setupImagePinchZoom(imgPreviewBox, activePreviewImg);

                previewZoomMode = 'image';

                const previewZoomInBottomBtn = document.getElementById('previewZoomInBottomBtn');
                const previewZoomOutBottomBtn = document.getElementById('previewZoomOutBottomBtn');
                const previewZoomResetBottomBtn = document.getElementById('previewZoomResetBottomBtn');
                const previewShareBottomBtn = document.getElementById('previewShareBottomBtn');
                const previewDownloadBottomBtn = document.getElementById('previewDownloadBottomBtn');

                if (previewZoomInBottomBtn) previewZoomInBottomBtn.addEventListener('click', zoomImageIn);
                if (previewZoomOutBottomBtn) previewZoomOutBottomBtn.addEventListener('click', zoomImageOut);
                if (previewZoomResetBottomBtn) previewZoomResetBottomBtn.addEventListener('click', resetImageZoom);

                if (previewShareBottomBtn) {
                    previewShareBottomBtn.addEventListener('click', () => {
                        shareSecureUrl(fileUrl, fileInfo.name || tr('share')).catch(() => { void appDialog.alert('Compartir no disponible'); });
                    });
                }
                if (previewDownloadBottomBtn) {
                    previewDownloadBottomBtn.addEventListener('click', () => {
                        const a = document.createElement('a');
                        a.href = buildSecureFileUrl(fileUrl);
                        a.download = fileInfo.name || 'archivo';
                        document.body.appendChild(a);
                        a.click();
                        document.body.removeChild(a);
                    });
                }
            } else if (fileInfo.type.startsWith('video/')) {
                if (previewActions) previewActions.style.display = '';
                previewExternalBtn && (previewExternalBtn.dataset.externalUrl = fileUrl);
                previewContent.innerHTML = `<video src="${fileUrl}" controls autoplay muted style="width:100%; height:100%; object-fit: contain;"></video>`;
                shareBtn.style.display = 'inline-block';
                downloadBtn.style.display = 'inline-block';
            } else if (fileInfo.type.startsWith('audio/')) {
                if (previewActions) previewActions.style.display = '';
                previewExternalBtn && (previewExternalBtn.dataset.externalUrl = fileUrl);
                previewContent.innerHTML = `
                    <div style="display: flex; flex-direction: column; align-items: center; justify-content: center; height: 100%;">
                        <div class="audio-animation">
                            <span></span><span></span><span></span>
                        </div>
                        <audio src="${fileUrl}" controls autoplay style="width: 80%; margin-top: 20px;"></audio>
                    </div>
                `;
                shareBtn.style.display = 'inline-block';
                downloadBtn.style.display = 'inline-block';
            } else if (fileInfo.type === 'application/pdf' || /\.pdf$/i.test(fileInfo.name)) {
                if (previewActions) previewActions.style.display = '';
                previewExternalBtn && (previewExternalBtn.dataset.externalUrl = fileUrl);
                previewZoomMode = 'pdf';
                previewPdfZoom = 1;
                if (previewZoomInBtn) previewZoomInBtn.style.display = '';
                if (previewZoomOutBtn) previewZoomOutBtn.style.display = '';
                if (previewZoomResetBtn) previewZoomResetBtn.style.display = '';
                if (previewPrintBtn) previewPrintBtn.style.display = 'inline-block';
                setPdfViewerContent(previewContent, fileUrl, {
                    getZoom: () => previewPdfZoom,
                    setZoom: (z) => { previewPdfZoom = z; },
                    applyZoom: applyPreviewPdfZoom,
                    hud: {
                        enabled: true,
                        minZoom: 0.5,
                        maxZoom: 2.5,
                        stepPercent: 1,
                        onSetZoom: (z) => {
                            previewPdfZoom = z;
                            applyPreviewPdfZoom();
                        },
                        onReset: () => {
                            previewPdfZoom = 1;
                            applyPreviewPdfZoom();
                        }
                    }
                });
                setTimeout(() => applyPreviewPdfZoom(), 0);
                setTimeout(() => applyPreviewPdfZoom(), 600);
                shareBtn.style.display = 'inline-block';
                downloadBtn.style.display = 'inline-block';
            } else if (/\.(ppt|pptx)$/i.test(fileInfo.name) || (fileInfo.type && (fileInfo.type.includes('presentation') || fileInfo.type.includes('powerpoint')))) {
                if (previewActions) previewActions.style.display = '';
                const secureSrc = buildSecureFileUrl(fileUrl);
                const officeViewerUrl = `https://view.officeapps.live.com/op/embed.aspx?src=${encodeURIComponent(secureSrc)}`;
                previewExternalBtn && (previewExternalBtn.dataset.externalUrl = officeViewerUrl);
                previewContent.innerHTML = `<iframe src="${officeViewerUrl}" width="100%" height="100%" frameborder="0" allowfullscreen></iframe>`;
                shareBtn.style.display = 'inline-block';
                downloadBtn.style.display = 'inline-block';
            } else if (fileInfo.type.includes('spreadsheet') || fileInfo.type.includes('excel') || 
                      fileInfo.name.match(/\.(xls|xlsx|csv)$/i)) {
                if (previewActions) previewActions.style.display = '';
                const ext = (fileInfo.name.split('.').pop() || '').toLowerCase();
                const isExcel = ['xls','xlsx'].includes(ext) || (fileInfo.type && fileInfo.type.includes('excel'));
                if (isExcel) {
                    const secureSrc = buildSecureFileUrl(fileUrl);
                    const officeViewerUrl = `https://view.officeapps.live.com/op/embed.aspx?src=${encodeURIComponent(secureSrc)}`;
                    previewExternalBtn && (previewExternalBtn.dataset.externalUrl = officeViewerUrl);
                    previewContent.innerHTML = `<iframe src="${officeViewerUrl}" width="100%" height="100%" frameborder="0" allowfullscreen></iframe>`;
                } else {
                    const googleViewerUrl = `https://docs.google.com/viewer?url=${encodeURIComponent(fileUrl)}&embedded=true`;
                    previewExternalBtn && (previewExternalBtn.dataset.externalUrl = googleViewerUrl);
                    previewContent.innerHTML = `<iframe src="${googleViewerUrl}" width="100%" height="100%" frameborder="0" sandbox="allow-scripts allow-same-origin allow-popups"></iframe>`;
                }
                shareBtn.style.display = 'inline-block';
                downloadBtn.style.display = 'inline-block';
            } else if (fileInfo.type.includes('word') || fileInfo.type.includes('document') || fileInfo.name.match(/\.(doc|docx)$/i)) {
                if (previewActions) previewActions.style.display = '';
                const secureSrc = buildSecureFileUrl(fileUrl);
                const officeViewerUrl = `https://view.officeapps.live.com/op/embed.aspx?src=${encodeURIComponent(secureSrc)}`;
                previewExternalBtn && (previewExternalBtn.dataset.externalUrl = officeViewerUrl);
                previewContent.innerHTML = `<iframe src="${officeViewerUrl}" width="100%" height="100%" frameborder="0"></iframe>`;
                shareBtn.style.display = 'inline-block';
                downloadBtn.style.display = 'inline-block';
            } else if (isTextLikeMimeOrName(fileInfo.type, fileInfo.name)) {
                previewExternalBtn && (previewExternalBtn.dataset.externalUrl = fileUrl);
                previewContent.classList.add('preview-text-plain-host');
                fetch(fileUrl, { credentials: 'same-origin' })
                    .then(response => response.text())
                    .then(text => {
                        previewContent.innerHTML = `<pre class="preview-text-plain-pre">${escapeHtml(text)}</pre>`;
                    });
                shareBtn.style.display = 'inline-block';
                downloadBtn.style.display = 'inline-block';
            } else {
                previewContent.innerHTML = `<p>No se puede previsualizar este tipo de archivo.</p>`;
                shareBtn.style.display = 'inline-block';
                downloadBtn.style.display = 'inline-block';
            }

            currentFile = fileInfo;
            document.getElementById('previewModal').style.display = 'block';
            document.body.classList.add('modal-open');
            previewActive = true;
            if (previewZoomMode === 'image') {
                applyPreviewWindowMaximized();
                initPreviewViewportWidthSnapshot();
                setTimeout(() => attachPreviewFeTabsObserver(), 0);
            }
            schedulePreviewVisualViewportLayout();
            setTimeout(schedulePreviewVisualViewportLayout, 120);
            setTimeout(schedulePreviewVisualViewportLayout, 450);
        }

        function startDrag(e) {
            if (!isImageZoomed) return;
            // Regla universal (local + FTP + galería): si hay multitouch, es pinch.
            // Evitar que el drag compita con el pellizco (provoca saltos/zoom-out percibido).
            if (e && e.type === 'touchstart' && e.touches && e.touches.length > 1) return;
            markImageInteractionActive(650);
            e.preventDefault();
            isDragging = true;
            if (e.type === 'touchstart') {
                startX = e.touches[0].clientX - translateX;
                startY = e.touches[0].clientY - translateY;
                document.addEventListener('touchmove', drag, { passive: false });
                document.addEventListener('touchend', stopDrag);
            } else {
                startX = e.clientX - translateX;
                startY = e.clientY - translateY;
                document.addEventListener('mousemove', drag);
                document.addEventListener('mouseup', stopDrag);
            }
            activePreviewImg.style.cursor = 'grabbing';
        }

        function drag(e) {
            if (!isDragging || !activePreviewImg) return;
            e.preventDefault();
            let clientX, clientY;
            if (e.type === 'touchmove') {
                if (e.touches && e.touches.length > 1) return;
                clientX = e.touches[0].clientX;
                clientY = e.touches[0].clientY;
            } else {
                clientX = e.clientX;
                clientY = e.clientY;
            }
            translateX = clientX - startX;
            translateY = clientY - startY;
            updateImageTransform();
            markImageInteractionActive(650);
        }

        function stopDrag() {
            isDragging = false;
            if (activePreviewImg) activePreviewImg.style.cursor = isImageZoomed ? 'grab' : 'grab';
            document.removeEventListener('mousemove', drag);
            document.removeEventListener('mouseup', stopDrag);
            document.removeEventListener('touchmove', drag);
            document.removeEventListener('touchend', stopDrag);
            markImageInteractionActive(250);
        }

        function updateImageTransform() {
            if (activePreviewImg) {
                activePreviewImg.style.transform = `translate(calc(-50% + ${translateX}px), calc(-50% + ${translateY}px)) scale(${scale})`;
            }
        }

        function closePreviewModal() {
            detachPreviewFeTabsObserver();
            previewViewportWidthSnapshot = 0;
            resetPreviewModalViewportStyles();
            document.getElementById('previewModal').style.display = 'none';
            document.getElementById('previewContent').innerHTML = '';
            document.body.classList.remove('modal-open');
            activePreviewImg = null;
            isDragging = false;
            previewActive = false;
            previewZoomMode = 'none';
            stopDrag();
        }

        const shareBtn = document.getElementById('shareBtn');
        const downloadBtn = document.getElementById('downloadBtn');

        shareBtn.addEventListener('click', () => {
            if (!currentFile) return;
            const fileUrl = currentFile.url.startsWith('http') ? currentFile.url : BASE_URL + currentFile.url;
            shareSecureUrl(fileUrl, 'Compartir archivo').catch(() => { void appDialog.alert('Compartir no disponible'); });
        });

        downloadBtn.addEventListener('click', () => {
            if (currentFile) {
                const fileUrl = currentFile.url.startsWith('http') ? currentFile.url : BASE_URL + currentFile.url;
                const secure = buildSecureFileUrl(fileUrl);
                const isPdf = currentFile.type === 'application/pdf' || /\.pdf$/i.test(currentFile.name || '');
                if (isPdf) {
                    downloadFileViaBlob(secure, currentFile.name || 'documento.pdf');
                } else {
                    const a = document.createElement('a');
                    a.href = secure;
                    a.download = currentFile.name;
                    document.body.appendChild(a);
                    a.click();
                    document.body.removeChild(a);
                }
            }
        });

        const previewPrintBtnEl = document.getElementById('previewPrintBtn');
        if (previewPrintBtnEl) {
            previewPrintBtnEl.addEventListener('click', () => {
                if (!currentFile) return;
                const fileUrl = currentFile.url.startsWith('http') ? currentFile.url : BASE_URL + currentFile.url;
                const previewContentEl = document.getElementById('previewContent');
                printPdfFromUrl(buildSecureFileUrl(fileUrl), previewContentEl);
            });
        }

        document.getElementById('resetBackgroundBtn').addEventListener('click', async () => {
            removeExistingBackground();
            document.body.style.backgroundImage = '';
            document.body.style.backgroundColor = '';
            await saveSettings({ backgroundUrl: '', backgroundType: 'file' });
            updateActionButtonsVisibility();
            await appDialog.alert('Fondo restablecido');
        });

        function applyBackgroundSettings() {
            const mosaicColor = document.getElementById('mosaicColor').value;
            document.documentElement.style.setProperty('--mosaic-bg-color', `rgba(${parseInt(mosaicColor.slice(1,3),16)}, ${parseInt(mosaicColor.slice(3,5),16)}, ${parseInt(mosaicColor.slice(5,7),16)}, 0.3)`);

            if (useYoutubeRadio.checked) {
                const youtubeLink = youtubeLinkInput.value;
                const youtubeLinkMobile = youtubeLinkMobileInput.value;
                const usePlaylist = document.getElementById('usePlaylist').checked;
                const pcList = getSetting('youtubePlaylistPC', []);
                const mobileList = getSetting('youtubePlaylistMobile', []);
                const youtubePlayMode = document.querySelector('input[name="youtubePlayMode"]:checked')?.value || 'random';
                saveSettings({
                    backgroundType: 'youtube',
                    youtubeBackground: youtubeLink,
                    youtubeBackgroundMobile: youtubeLinkMobile,
                    youtubePlaylistPC: pcList,
                    youtubePlaylistMobile: mobileList,
                    usePlaylist,
                    youtubePlayMode,
                    mosaicColor
                }).then(applyVideoBackground);
            } else if (useFileRadio.checked) {
                if (tempSelectedFile) {
                    uploadAndApplyFileBackground(tempSelectedFile);
                } else if (getSetting('backgroundUrl')) {
                    applyBackgroundFromUrl(getSetting('backgroundUrl'));
                }
                saveSettings({ backgroundType: 'file', mosaicColor });
            } else if (useImagesRadio.checked) {
                const images = getSetting('backgroundImages', []);
                const slideshow = slideshowEnabledCheck.checked;
                const interval = parseInt(slideshowIntervalInput.value) || 5;
                saveSettings({
                    backgroundType: 'images',
                    backgroundImages: images,
                    slideshowEnabled: slideshow,
                    slideshowInterval: interval,
                    mosaicColor
                }).then(() => applyImagesBackground(images, slideshow));
            } else if (useVideosRadio.checked) {
                const videos = getSetting('backgroundVideos', []);
                const videoPlayMode = document.querySelector('input[name="videoPlayMode"]:checked')?.value || 'random';
                saveSettings({
                    backgroundType: 'videos',
                    backgroundVideos: videos,
                    videoPlayMode,
                    mosaicColor
                }).then(() => applyVideosBackground(videos));
            }
            updateActionButtonsVisibility();
            configModal.style.display = 'none';
            document.body.classList.remove('modal-open');
        }

        window.applyBackgroundSettings = applyBackgroundSettings;

        function updateActionButtons(type, url) {
            updateActionButtonsVisibility();
            if (type === 'video') {
                const video = document.querySelector('.video-background');
                if (video) muteToggle.innerHTML = video.muted ? '<i class="fas fa-volume-mute"></i>' : '<i class="fas fa-volume-up"></i>';
            } else if (type === 'youtube') {
                youtubeLinkBtn.dataset.url = url;
            }
        }

        muteToggle.addEventListener('click', () => {
            const video = document.querySelector('.video-background');
            if (video && video.tagName === 'VIDEO') {
                video.muted = !video.muted;
                muteToggle.innerHTML = video.muted ? '<i class="fas fa-volume-mute"></i>' : '<i class="fas fa-volume-up"></i>';
            }
        });

        youtubeLinkBtn.addEventListener('click', () => {
            const url = youtubeLinkBtn.dataset.url;
            if (url) window.open(url, '_blank');
        });

        function updatePreviewFromSettings() {
            if (useYoutubeRadio.checked) updateYoutubePreview();
            else if (useImagesRadio.checked) updateImagesPreview();
            else if (useVideosRadio.checked) updateVideosPreview();
            else if (useFileRadio.checked) updateFilePreviewFromSettings();
        }
