<?php
$connector_url = $connector ?? '';
?>
<style>
    #fm_app {
        position: relative;
        isolation: isolate;
        --fm-border: rgba(203, 213, 225, 0.9);
        --fm-border-strong: rgba(148, 163, 184, 0.42);
        --fm-divider: rgba(226, 232, 240, 0.88);
        --fm-accent-border: rgba(191, 219, 254, 0.95);
        --fm-surface: rgba(255, 255, 255, 0.94);
        --fm-surface-soft: rgba(248, 250, 252, 0.96);
        --fm-surface-elevated: rgba(255, 255, 255, 0.98);
        --fm-surface-muted: rgba(248, 250, 252, 0.88);
        --fm-text: #0f172a;
        --fm-muted: #64748b;
        --fm-primary: #0f172a;
        --fm-accent: #7c3aed;
        --fm-chip-bg: #e2e8f0;
        --fm-chip-text: #475569;
        --fm-status-bg: #dcfce7;
        --fm-status-text: #0f766e;
        --fm-thumb-border: rgba(226, 232, 240, 0.88);
    }
    #fm_app::before {
        content: "";
        position: absolute;
        inset: -36px -24px auto;
        height: 360px;
        background:
            radial-gradient(540px 220px at 0% 0%, rgba(99, 102, 241, 0.12), transparent 70%),
            radial-gradient(520px 240px at 100% 0%, rgba(14, 165, 233, 0.1), transparent 72%);
        pointer-events: none;
        z-index: -1;
    }
    #fm_app .fm-shell {
        display: grid;
        gap: 1rem;
        align-items: start;
    }
    @media (min-width: 1280px) {
        #fm_app .fm-shell {
            grid-template-columns: 248px minmax(0, 1fr);
        }
    }
    #fm_app .fm-panel {
        border: 1px solid var(--fm-border);
        background: var(--fm-surface);
        border-radius: 1.5rem;
        box-shadow: 0 24px 54px -44px rgba(15, 23, 42, 0.28);
        backdrop-filter: blur(12px);
    }
    #fm_app .fm-sidebar,
    #fm_app .fm-browser {
        overflow: hidden;
    }
    #fm_app .fm-sidebar {
        padding: 0.85rem;
    }
    #fm_app .fm-browser {
        padding: 0.78rem;
    }
    #fm_app .fm-eyebrow {
        color: #64748b;
        font-size: 0.75rem;
        font-weight: 700;
        letter-spacing: 0.28em;
        text-transform: uppercase;
    }
    #fm_app .fm-icon-btn,
    #fm_app .fm-pill-btn,
    #fm_app .fm-view-btn {
        border: 1px solid var(--fm-border);
        background: var(--fm-surface-elevated);
        color: var(--fm-text);
        box-shadow: 0 14px 24px -22px rgba(15, 23, 42, 0.34);
        transition: all 0.18s ease;
        overflow: hidden;
        line-height: 1;
    }
    #fm_app .fm-icon-btn:hover,
    #fm_app .fm-pill-btn:hover,
    #fm_app .fm-view-btn:hover {
        border-color: var(--fm-border-strong);
        background: var(--fm-surface-elevated);
        transform: translateY(-1px);
    }
    #fm_app .fm-icon-btn {
        display: inline-flex;
        height: 2.45rem;
        width: 2.45rem;
        align-items: center;
        justify-content: center;
        border-radius: 999px;
        flex-shrink: 0;
    }
    #fm_app .fm-pill-btn {
        display: inline-flex;
        align-items: center;
        gap: 0.55rem;
        border-radius: 1rem;
        padding: 0.68rem 0.88rem;
        font-size: 0.78rem;
        font-weight: 600;
    }
    #fm_app .fm-input-shell,
    #fm_app .fm-select-shell {
        display: flex;
        align-items: center;
        gap: 0.65rem;
        border: 1px solid var(--fm-border);
        background: var(--fm-surface-elevated);
        border-radius: 1rem;
        min-height: 2.6rem;
        box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.7);
        min-width: 0;
    }
    #fm_app .fm-input-shell {
        padding: 0 0.8rem;
    }
    #fm_app .fm-input-shell input,
    #fm_app .fm-select-shell select {
        width: 100%;
        min-width: 0;
        border: 0;
        background: transparent;
        color: var(--fm-text);
        box-shadow: none;
        font-size: 0.84rem;
        font-weight: 600;
    }
    #fm_app .fm-input-shell input:focus,
    #fm_app .fm-select-shell select:focus {
        outline: none;
    }
    #fm_app .fm-select-shell {
        padding: 0 0.72rem;
    }
    #fm_app .fm-quick-drop {
        margin-top: 1.1rem;
        border: 1px dashed var(--fm-accent-border);
        background:
            radial-gradient(180px 120px at 50% 0%, rgba(129, 140, 248, 0.14), transparent 70%),
            linear-gradient(180deg, rgba(255, 255, 255, 0.96), rgba(248, 250, 252, 0.94));
        border-radius: 1.3rem;
        padding: 0.85rem 0.8rem 0.9rem;
        text-align: center;
    }
    #fm_app .fm-drop-icon {
        margin: 0 auto 0.8rem;
        display: inline-flex;
        height: 3.35rem;
        width: 3.35rem;
        align-items: center;
        justify-content: center;
        border-radius: 1.35rem;
        background: linear-gradient(180deg, #8b5cf6, #6d28d9);
        color: #fff;
        box-shadow: 0 16px 30px -18px rgba(109, 40, 217, 0.65);
    }
    #fm_app .fm-folder-list {
        margin-top: 1rem;
        display: flex;
        flex-direction: column;
        gap: 0.55rem;
    }
    #fm_app .fm-sidebar-path {
        margin-top: 0.65rem;
        padding-top: 0.65rem;
        border-top: 1px solid var(--fm-divider);
    }
    #fm_app .fm-sidebar-path #fm_breadcrumb {
        min-height: 0;
    }
    #fm_app .fm-folder-row {
        display: flex;
        align-items: center;
        justify-content: space-between;
        gap: 0.75rem;
        border-radius: 1rem;
        padding: 0.35rem 0.2rem 0.35rem 0.35rem;
        transition: background 0.18s ease;
    }
    #fm_app .fm-folder-row:hover {
        background: rgba(248, 250, 252, 0.9);
    }
    #fm_app .fm-folder-row.is-active {
        border: 1px solid var(--fm-accent-border);
        background: linear-gradient(180deg, rgba(255, 255, 255, 0.96), rgba(239, 246, 255, 0.94));
        box-shadow: 0 10px 24px -24px rgba(37, 99, 235, 0.55);
    }
    #fm_app .fm-folder-main {
        display: flex;
        min-width: 0;
        flex: 1;
        align-items: center;
        gap: 0.6rem;
        color: var(--fm-text);
        text-align: left;
    }
    #fm_app .fm-folder-glyph {
        display: inline-flex;
        height: 2.1rem;
        width: 2.1rem;
        align-items: center;
        justify-content: center;
        border-radius: 0.9rem;
        border: 1px solid var(--fm-border);
        background: var(--fm-surface-elevated);
        color: var(--fm-chip-text);
        box-shadow: 0 8px 20px -18px rgba(15, 23, 42, 0.28);
        flex-shrink: 0;
    }
    #fm_app .fm-folder-glyph-folder {
        background: linear-gradient(180deg, rgba(255, 247, 237, 0.98), rgba(254, 243, 199, 0.9));
        border-color: rgba(251, 191, 36, 0.18);
        color: #b45309;
    }
    #fm_app .fm-folder-main span {
        overflow: hidden;
        text-overflow: ellipsis;
        white-space: nowrap;
        font-size: 0.83rem;
        font-weight: 600;
    }
    #fm_app .fm-folder-main small {
        display: block;
        margin-top: 0.1rem;
        color: var(--fm-muted);
        font-size: 0.72rem;
        font-weight: 500;
    }
    #fm_app .fm-folder-actions {
        display: flex;
        align-items: center;
        gap: 0.45rem;
    }
    #fm_app .fm-folder-action {
        display: inline-flex;
        height: 1.95rem;
        width: 1.95rem;
        align-items: center;
        justify-content: center;
        border-radius: 999px;
        border: 1px solid var(--fm-border);
        background: var(--fm-surface-elevated);
        color: var(--fm-muted);
        transition: all 0.18s ease;
    }
    #fm_app .fm-folder-action:hover {
        border-color: var(--fm-border-strong);
        color: var(--fm-text);
    }
    #fm_app .fm-folder-action.is-danger:hover {
        color: #dc2626;
        background: rgba(254, 242, 242, 0.95);
    }
    #fm_app .fm-browser-head {
        display: flex;
        flex-direction: column;
        gap: 0.55rem;
        padding: 0 0 0.62rem;
        border-bottom: 1px solid var(--fm-divider);
    }
    @media (min-width: 1200px) {
        #fm_app .fm-browser-head {
            flex-direction: row;
            align-items: center;
            justify-content: space-between;
        }
    }
    #fm_app .fm-browser-toolbar {
        display: grid;
        gap: 0.45rem;
        align-items: center;
        min-width: 0;
    }
    @media (min-width: 980px) {
        #fm_app .fm-browser-toolbar {
            grid-template-columns: minmax(170px, 1fr) minmax(124px, 0.34fr) auto auto auto auto;
        }
    }
    #fm_app .fm-view-switch {
        display: inline-flex;
        align-items: center;
        border: 1px solid var(--fm-border);
        border-radius: 999px;
        padding: 0.14rem;
        background: var(--fm-surface-elevated);
        box-shadow: 0 14px 24px -22px rgba(15, 23, 42, 0.34);
        overflow: hidden;
        flex-shrink: 0;
    }
    #fm_app .fm-view-btn {
        display: inline-flex;
        align-items: center;
        justify-content: center;
        height: 2.3rem;
        width: 2.3rem;
        border-radius: 999px;
        border: 0;
        box-shadow: none;
        background: transparent;
        color: var(--fm-muted);
    }
    #fm_app .fm-view-btn.is-active {
        background: #0f172a;
        color: #fff;
    }
    #fm_app .fm-item-count {
        color: var(--fm-muted);
        font-size: 0.72rem;
        font-weight: 700;
        letter-spacing: 0.14em;
        text-transform: uppercase;
        white-space: nowrap;
    }
    #fm_app .fm-path {
        display: inline-flex;
        flex-wrap: wrap;
        align-items: center;
        gap: 0.3rem;
        border: 1px solid var(--fm-border);
        background: var(--fm-surface-elevated);
        border-radius: 999px;
        padding: 0.34rem 0.58rem;
        box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.72);
    }
    #fm_app .fm-path button {
        color: var(--fm-text);
    }
    #fm_app .fm-path-sep,
    #fm_app .fm-section-note {
        color: var(--fm-muted);
    }
    #fm_app .fm-bulkbar {
        margin-top: 0.75rem;
        border: 1px solid var(--fm-accent-border);
        background: linear-gradient(180deg, rgba(239, 246, 255, 0.92), rgba(255, 255, 255, 0.96));
        border-radius: 1rem;
        padding: 0.62rem 0.72rem;
    }
    #fm_app .fm-stage {
        position: relative;
        margin-top: 0.75rem;
        border: 1px solid var(--fm-border);
        background:
            linear-gradient(180deg, rgba(248, 250, 252, 0.98), rgba(241, 245, 249, 0.9));
        border-radius: 1.45rem;
        padding: 0.55rem;
        min-height: 48vh;
        overflow: hidden;
    }
    #fm_app .fm-grid {
        display: grid;
        gap: 0.65rem;
        grid-template-columns: repeat(auto-fill, minmax(162px, 1fr));
    }
    #fm_app .fm-list-grid {
        display: grid;
        gap: 0.65rem;
    }
    @media (min-width: 1100px) {
        #fm_app .fm-list-grid {
            grid-template-columns: repeat(3, minmax(0, 1fr));
        }
    }
    @media (min-width: 760px) and (max-width: 1099px) {
        #fm_app .fm-list-grid {
            grid-template-columns: repeat(2, minmax(0, 1fr));
        }
    }
    #fm_app .fm-card,
    #fm_app .fm-list-card {
        position: relative;
        overflow: hidden;
        border: 1px solid var(--fm-border);
        background: var(--fm-surface-elevated);
        box-shadow: 0 24px 46px -40px rgba(15, 23, 42, 0.28);
        transition: all 0.18s ease;
    }
    #fm_app .fm-card {
        border-radius: 1.2rem;
        padding: 0.5rem;
    }
    #fm_app .fm-list-card {
        border-radius: 1.1rem;
        padding: 0.62rem;
    }
    #fm_app .fm-card:hover,
    #fm_app .fm-list-card:hover {
        transform: translateY(-2px);
        border-color: rgba(147, 197, 253, 0.9);
        box-shadow: 0 30px 50px -36px rgba(37, 99, 235, 0.2);
    }
    #fm_app .fm-card-check {
        background: var(--fm-surface-elevated);
        border: 1px solid var(--fm-border);
        box-shadow: 0 12px 18px -18px rgba(15, 23, 42, 0.28);
    }
    #fm_app .fm-thumb {
        display: flex;
        align-items: center;
        justify-content: center;
        overflow: hidden;
        border-radius: 1.2rem;
        border: 1px solid var(--fm-thumb-border);
        background: linear-gradient(180deg, rgba(255, 255, 255, 0.98), rgba(248, 250, 252, 0.94));
    }
    #fm_app .fm-thumb-grid {
        height: 94px;
    }
    #fm_app .fm-thumb-list {
        height: 2.7rem;
        width: 2.7rem;
        flex: 0 0 2.7rem;
        border-radius: 0.75rem;
    }
    #fm_app .fm-thumb img {
        height: 100%;
        width: 100%;
        object-fit: cover;
    }
    #fm_app .fm-thumb-placeholder {
        display: inline-flex;
        flex-direction: column;
        align-items: center;
        justify-content: center;
        gap: 0.45rem;
        height: 100%;
        width: 100%;
    }
    #fm_app .fm-thumb-doc {
        background: linear-gradient(180deg, #1e293b, #0f172a);
        color: #fff;
    }
    #fm_app .fm-thumb-folder {
        background: linear-gradient(180deg, rgba(255, 247, 237, 0.98), rgba(254, 243, 199, 0.9));
        color: #b45309;
    }
    #fm_app .fm-thumb-media {
        background: linear-gradient(180deg, rgba(236, 253, 245, 0.98), rgba(220, 252, 231, 0.92));
        color: #047857;
    }
    #fm_app .fm-badge,
    #fm_app .fm-status-badge {
        display: inline-flex;
        align-items: center;
        justify-content: center;
        min-height: 1.85rem;
        padding: 0.14rem 0.58rem;
        border-radius: 999px;
        font-size: 0.72rem;
        font-weight: 700;
        white-space: nowrap;
    }
    #fm_app .fm-badge {
        background: var(--fm-chip-bg);
        color: var(--fm-chip-text);
    }
    #fm_app .fm-status-badge {
        background: var(--fm-status-bg);
        color: var(--fm-status-text);
    }
    #fm_app .fm-card-menu,
    #fm_app .fm-card-star {
        display: inline-flex;
        height: 1.95rem;
        width: 1.95rem;
        align-items: center;
        justify-content: center;
        border-radius: 999px;
        border: 1px solid var(--fm-thumb-border);
        background: var(--fm-surface-elevated);
        color: #64748b;
        transition: all 0.18s ease;
    }
    #fm_app .fm-card-menu:hover,
    #fm_app .fm-card-star:hover {
        color: var(--fm-text);
        border-color: var(--fm-border-strong);
    }
    #fm_app .fm-browser-footer {
        display: flex;
        flex-direction: column;
        gap: 0.55rem;
        padding: 0.7rem 0.15rem 0;
    }
    @media (min-width: 1100px) {
        #fm_app .fm-browser-footer {
            flex-direction: row;
            align-items: center;
            justify-content: space-between;
        }
    }
    #fm_app .fm-footer-meta {
        display: flex;
        flex-wrap: wrap;
        align-items: center;
        gap: 1rem;
        color: var(--fm-muted);
        font-size: 0.8rem;
        font-weight: 600;
    }
    #fm_app .fm-page-select {
        border: 1px solid var(--fm-border);
        background: var(--fm-surface-elevated);
        border-radius: 1rem;
        min-height: 2.45rem;
        min-width: 5rem;
        padding: 0 0.7rem;
        color: var(--fm-text);
        font-weight: 600;
    }
    #fm_app .fm-pagination {
        display: inline-flex;
        align-items: center;
        gap: 0.5rem;
    }
    #fm_app .fm-pagination-btn {
        display: inline-flex;
        min-height: 2.45rem;
        align-items: center;
        justify-content: center;
        gap: 0.4rem;
        border: 1px solid var(--fm-border);
        background: var(--fm-surface-elevated);
        border-radius: 1rem;
        padding: 0.52rem 0.78rem;
        color: var(--fm-muted);
        font-size: 0.8rem;
        font-weight: 700;
    }
    #fm_app .fm-pagination-btn.is-active {
        background: #0f172a;
        border-color: #0f172a;
        color: #fff;
    }
    #fm_app .fm-pagination-btn:disabled {
        opacity: 0.6;
    }
    #fm_app .fm-empty {
        border: 1px dashed var(--fm-accent-border);
        background: rgba(255, 255, 255, 0.78);
        border-radius: 1.1rem;
        padding: 1.8rem 1rem;
        text-align: center;
    }
    #fm_app .fm-grid.hidden,
    #fm_app #fm_list.hidden,
    #fm_app .fm-empty.hidden {
        display: none !important;
    }
    .dark #fm_app {
        --fm-border: rgba(51, 65, 85, 0.96);
        --fm-border-strong: rgba(100, 116, 139, 0.8);
        --fm-divider: rgba(51, 65, 85, 0.96);
        --fm-accent-border: rgba(59, 130, 246, 0.34);
        --fm-surface: rgba(15, 23, 42, 0.92);
        --fm-surface-soft: rgba(15, 23, 42, 0.94);
        --fm-surface-elevated: rgba(15, 23, 42, 0.96);
        --fm-surface-muted: rgba(15, 23, 42, 0.82);
        --fm-text: #e2e8f0;
        --fm-muted: #94a3b8;
        --fm-chip-bg: rgba(30, 41, 59, 0.96);
        --fm-chip-text: #cbd5e1;
        --fm-status-bg: rgba(6, 78, 59, 0.42);
        --fm-status-text: #99f6e4;
        --fm-thumb-border: rgba(51, 65, 85, 0.96);
    }
    .dark #fm_app .fm-input-shell,
    .dark #fm_app .fm-select-shell,
    .dark #fm_app .fm-path,
    .dark #fm_app .fm-card,
    .dark #fm_app .fm-list-card,
    .dark #fm_app .fm-folder-action,
    .dark #fm_app .fm-icon-btn,
    .dark #fm_app .fm-pill-btn,
    .dark #fm_app .fm-pagination-btn {
        background: rgba(15, 23, 42, 0.96);
        color: #e2e8f0;
    }
    .dark #fm_app .fm-stage {
        background: linear-gradient(180deg, rgba(8, 15, 30, 0.98), rgba(15, 23, 42, 0.92));
    }
    .dark #fm_app .fm-quick-drop,
    .dark #fm_app .fm-bulkbar,
    .dark #fm_app .fm-empty {
        background: linear-gradient(180deg, rgba(15, 23, 42, 0.96), rgba(8, 15, 30, 0.94));
    }
    .dark #fm_app .fm-folder-row:hover {
        background: rgba(30, 41, 59, 0.72);
    }
    .dark #fm_app .fm-folder-row.is-active {
        background: linear-gradient(180deg, rgba(15, 23, 42, 0.98), rgba(30, 41, 59, 0.96));
    }
    .dark #fm_app .fm-thumb {
        background: linear-gradient(180deg, rgba(30, 41, 59, 0.96), rgba(15, 23, 42, 0.94));
    }
    .dark #fm_app .fm-folder-glyph {
        background: rgba(15, 23, 42, 0.96);
        border-color: var(--fm-border);
        color: #cbd5e1;
    }
    .dark #fm_app .fm-folder-glyph-folder {
        background: rgba(69, 26, 3, 0.5);
        border-color: rgba(120, 53, 15, 0.8);
        color: #fdba74;
    }
    .dark #fm_app .fm-card-menu,
    .dark #fm_app .fm-card-star,
    .dark #fm_app .fm-view-switch,
    .dark #fm_app .fm-page-select {
        background: rgba(15, 23, 42, 0.96);
        border-color: var(--fm-border);
        color: #cbd5e1;
    }
    #fm_app .fm-replaced {
        animation: fmReplaceFlash 1.2s ease-in-out;
        border-color: #38bdf8;
        box-shadow: 0 0 0 2px rgba(56, 189, 248, 0.3);
    }
    @keyframes fmReplaceFlash {
        0% { transform: scale(1); background-color: rgba(56, 189, 248, 0.12); }
        40% { transform: scale(1.01); background-color: rgba(56, 189, 248, 0.2); }
        100% { transform: scale(1); background-color: transparent; }
    }
</style>
<div id="fm_app" class="min-h-[720px]" data-connector="<?= $connector_url ?>">
    <div class="fm-shell">
        <aside class="fm-panel fm-sidebar">
            <div class="relative flex items-center justify-between">
                <div>
                    <div class="fm-eyebrow">Folders</div>
                </div>
                <div class="relative z-30">
                    <button type="button" class="fm-icon-btn" data-fm-new>
                        <i data-lucide="plus" class="h-5 w-5"></i>
                    </button>
                    <div class="absolute right-0 top-full z-50 mt-2 hidden w-44 rounded-2xl border border-border bg-white p-2 text-xs shadow-lg dark:bg-gray-800 dark:border-gray-700" id="fm_new_menu">
                        <button type="button" class="flex w-full items-center gap-2 rounded-xl px-3 py-2 text-left text-foreground hover:bg-muted" data-fm-new-folder>
                            <i data-lucide="folder-plus" class="h-4 w-4"></i><?= lang('new_folder') ?>
                        </button>
                        <button type="button" class="flex w-full items-center gap-2 rounded-xl px-3 py-2 text-left text-foreground hover:bg-muted" data-fm-new-file>
                            <i data-lucide="file-plus" class="h-4 w-4"></i><?= lang('new_file') ?>
                        </button>
                    </div>
                </div>
            </div>

            <div class="fm-quick-drop">
                <div class="fm-eyebrow !tracking-[0.22em]">Quick Drop</div>
                <div class="fm-drop-icon">
                    <i data-lucide="package-plus" class="h-7 w-7"></i>
                </div>
                <div class="text-[15px] font-semibold text-slate-900 dark:text-slate-100">Drop images here</div>
                <p class="mt-1.5 text-[11px] leading-4 text-slate-500 dark:text-slate-400">Drag files into this panel or browse directly from your computer.</p>
                <button type="button" class="fm-pill-btn mt-3" data-fm-upload>
                    <i data-lucide="upload" class="h-4 w-4"></i><?= lang('upload') ?>
                </button>
                <input id="fm_upload_input" type="file" class="hidden" multiple>
            </div>

            <div class="fm-sidebar-path">
                <div id="fm_breadcrumb" class="flex flex-wrap items-center gap-2"></div>
            </div>

            <div id="fm_sidebar_folders" class="fm-folder-list"></div>
        </aside>

        <section class="fm-panel fm-browser">
            <div class="fm-browser-head">
                <div>
                    <div class="fm-eyebrow">Browser</div>
                    <h2 class="mt-1 text-[1.2rem] font-semibold tracking-tight text-slate-900 dark:text-slate-100">Media Files</h2>
                </div>
                <div class="fm-browser-toolbar">
                    <div class="fm-input-shell">
                        <i data-lucide="search" class="h-5 w-5 text-slate-400"></i>
                        <input id="fm_search" type="text" placeholder="<?= lang('search_media') ?>">
                    </div>
                    <label class="fm-select-shell">
                        <i data-lucide="filter" class="h-4 w-4 text-slate-400"></i>
                        <select id="fm_filter">
                            <option value="all">All file types</option>
                            <option value="folder"><?= lang('folder') ?></option>
                            <option value="image"><?= lang('images') ?></option>
                            <option value="video"><?= lang('video') ?></option>
                            <option value="audio"><?= lang('music') ?></option>
                            <option value="doc"><?= lang('document') ?></option>
                            <option value="other"><?= lang('others') ?></option>
                            <option value="favorite"><?= lang('favourite') ?></option>
                        </select>
                    </label>
                    <div class="relative" id="fm_sort_group">
                        <button id="fm_sort_btn" type="button" class="fm-icon-btn">
                            <i data-lucide="arrow-up-down" class="h-4 w-4"></i>
                        </button>
                        <div id="fm_sort_menu" class="absolute right-0 top-full z-50 mt-2 hidden w-44 rounded-2xl border border-border bg-white p-2 text-xs shadow-lg dark:bg-gray-800 dark:border-gray-700">
                            <button class="flex w-full items-center gap-2 rounded-xl px-3 py-2 text-left hover:bg-muted" data-sort="folderFirst"><?= lang('folder') ?> <?= lang('first') ?></button>
                            <button class="flex w-full items-center gap-2 rounded-xl px-3 py-2 text-left hover:bg-muted" data-sort="name"><?= lang('name') ?></button>
                            <button class="flex w-full items-center gap-2 rounded-xl px-3 py-2 text-left hover:bg-muted" data-sort="size"><?= lang('size') ?></button>
                            <button class="flex w-full items-center gap-2 rounded-xl px-3 py-2 text-left hover:bg-muted" data-sort="type"><?= lang('type') ?></button>
                            <button class="flex w-full items-center gap-2 rounded-xl px-3 py-2 text-left hover:bg-muted" data-sort="date"><?= lang('date') ?></button>
                        </div>
                    </div>
                    <button type="button" class="fm-icon-btn" data-fm-back title="<?= lang('back') ?>">
                        <i data-lucide="arrow-left" class="h-4 w-4"></i>
                    </button>
                    <div class="fm-view-switch">
                        <button type="button" class="fm-view-btn" data-fm-view="grid" title="<?= lang('grid') ?>">
                            <i data-lucide="layout-grid" class="h-4 w-4"></i>
                        </button>
                        <button type="button" class="fm-view-btn" data-fm-view="list" title="<?= lang('list') ?>">
                            <i data-lucide="list" class="h-4 w-4"></i>
                        </button>
                    </div>
                    <div id="fm_item_count" class="fm-item-count">0 Items</div>
                </div>
            </div>

            <div id="fm_bulkbar" class="fm-bulkbar hidden items-center justify-between gap-3 text-xs shadow-sm">
                <div><span id="fm_bulk_count">0</span> <?= lang('files') ?> <?= lang('selected') ?></div>
                <div class="flex flex-wrap items-center gap-2">
                    <button type="button" class="fm-pill-btn !min-h-0 !px-3 !py-2 !text-xs" data-fm-bulk-clear">
                        <?= lang('clear') ?>
                    </button>
                    <button type="button" class="fm-pill-btn !min-h-0 !px-3 !py-2 !text-xs" data-fm-bulk-zip>
                        <i data-lucide="archive" class="h-4 w-4"></i><?= lang('zip') ?>
                    </button>
                    <button type="button" class="fm-pill-btn !min-h-0 !px-3 !py-2 !text-xs" data-fm-bulk-unzip>
                        <i data-lucide="package-open" class="h-4 w-4"></i><?= lang('unzip') ?>
                    </button>
                    <button type="button" class="inline-flex items-center gap-2 rounded-full bg-rose-600 px-3 py-2 text-xs font-semibold text-white transition hover:bg-rose-500" data-fm-bulk-delete>
                        <i data-lucide="trash-2" class="h-4 w-4"></i><?= lang('delete') ?>
                    </button>
                </div>
            </div>

            <div class="relative">
                <div id="fm_drop" class="pointer-events-none absolute inset-0 z-10 hidden items-center justify-center rounded-[28px] border-2 border-dashed border-sky-300 bg-sky-50/85 text-sm font-semibold text-sky-600 shadow-inner">
                    <?= lang('drop') ?> <?= lang('files') ?> <?= lang('here') ?>
                </div>
                <div id="fm_select_box" class="pointer-events-none absolute z-20 hidden rounded-md border border-indigo-400 bg-indigo-200/30"></div>
                <div id="fm_grid_wrap" class="fm-stage">
                    <div id="fm_grid" class="fm-grid"></div>
                    <div id="fm_list" class="hidden">
                        <div id="fm_list_body" class="fm-list-grid"></div>
                    </div>
                    <div class="fm-empty hidden" id="fm_empty">
                        <div class="mx-auto inline-flex h-14 w-14 items-center justify-center rounded-2xl border border-border bg-background text-foreground shadow-sm">
                            <i data-lucide="folder-open-dot" class="h-6 w-6"></i>
                        </div>
                        <div class="mt-4 text-base font-semibold text-foreground"><?= lang('no_data') ?></div>
                        <p class="mt-1 text-sm text-muted-foreground">This folder is empty or nothing matches your current filters.</p>
                    </div>
                </div>
            </div>

            <div class="fm-browser-footer">
                <div class="fm-footer-meta">
                    <span id="fm_footer_summary">Showing 0 - 0 of 0</span>
                    <span class="inline-flex items-center gap-3">
                        <span>Per page</span>
                        <select id="fm_page_size" class="fm-page-select">
                            <option value="12">12</option>
                            <option value="24" selected>24</option>
                            <option value="48">48</option>
                        </select>
                    </span>
                </div>
                <div class="fm-pagination">
                    <button type="button" id="fm_prev_page" class="fm-pagination-btn" disabled>
                        <i data-lucide="chevron-left" class="h-4 w-4"></i>Previous
                    </button>
                    <div id="fm_page_numbers" class="fm-pagination"></div>
                    <button type="button" id="fm_next_page" class="fm-pagination-btn" disabled>
                        Next<i data-lucide="chevron-right" class="h-4 w-4"></i>
                    </button>
                </div>
            </div>
        </section>
    </div>
</div>

<div id="fm_preview_backdrop" class="fixed inset-0 z-40 hidden bg-slate-900/30"></div>
<div id="fm_preview_drawer" class="fm-panel fixed right-0 top-0 z-50 hidden h-full w-full max-w-md translate-x-full flex-col overflow-x-hidden border-l border-slate-200 bg-white shadow-2xl transition-transform duration-300 dark:border-slate-700 dark:bg-slate-900 sm:max-w-lg">
    <div class="flex items-center justify-between border-b border-slate-200 px-5 py-4 dark:border-slate-700">
        <div class="text-base font-semibold text-slate-900 dark:text-slate-100"><?= lang('preview') ?></div>
        <button type="button" class="rounded-full p-1 text-slate-500 transition hover:bg-slate-100 hover:text-slate-700 dark:text-slate-300 dark:hover:bg-slate-800 dark:hover:text-slate-100" data-fm-preview-close>
            <i data-lucide="x" class="h-4 w-4"></i>
        </button>
    </div>
    <div class="flex-1 overflow-auto p-5">
        <div class="space-y-0 text-sm text-slate-600 dark:text-slate-300">
            <div class="rounded-xl border border-dashed border-slate-300 bg-slate-50 p-4 text-center text-xs dark:border-slate-700 dark:bg-slate-800/40" id="fm_preview_empty">
                <?= lang('select') ?> <?= lang('files') ?>
            </div>
            <div class="hidden space-y-4" id="fm_preview_body">
                <div class="flex min-w-0 items-center gap-3">
                    <div class="inline-flex h-12 w-12 items-center justify-center rounded-2xl bg-slate-100 dark:bg-slate-800" id="fm_preview_icon"></div>
                    <div class="min-w-0 flex-1">
                        <div class="truncate text-base font-semibold text-slate-900 dark:text-slate-100" id="fm_preview_name"></div>
                        <div class="truncate text-xs text-slate-500 dark:text-slate-400" id="fm_preview_meta"></div>
                    </div>
                </div>
                <div id="fm_preview_media" class="overflow-hidden rounded-2xl border border-slate-200 bg-slate-50/60 dark:border-slate-700 dark:bg-slate-800/40"></div>
                <div class="grid grid-cols-1 gap-2 sm:grid-cols-3">
                    <button type="button" class="inline-flex w-full items-center justify-center gap-2 rounded-lg border border-slate-200 bg-white px-3 py-2 text-xs font-semibold text-slate-700 transition hover:bg-slate-50 dark:border-slate-700 dark:bg-slate-800 dark:text-slate-200 dark:hover:bg-slate-700" id="fm_copy_link">
                        <i data-lucide="link" class="h-4 w-4"></i><?= lang('copy') ?> <?= lang('link') ?>
                    </button>
                    <button type="button" class="inline-flex w-full items-center justify-center gap-2 rounded-lg border border-slate-200 bg-white px-3 py-2 text-xs font-semibold text-slate-700 transition hover:bg-slate-50 dark:border-slate-700 dark:bg-slate-800 dark:text-slate-200 dark:hover:bg-slate-700" id="fm_open_file">
                        <i data-lucide="external-link" class="h-4 w-4"></i><?= lang('open') ?>
                    </button>
                    <button type="button" class="inline-flex w-full items-center justify-center gap-2 rounded-lg bg-rose-600 px-3 py-2 text-xs font-semibold text-white transition hover:bg-rose-500" id="fm_delete_single">
                        <i data-lucide="trash-2" class="h-4 w-4"></i><?= lang('delete') ?>
                    </button>
                </div>
            </div>
        </div>
    </div>
</div>

<div id="fm_modal" class="fixed inset-0 z-50 hidden items-center justify-center bg-slate-950/70 p-4 backdrop-blur-[1px]">
    <div class="w-full max-w-md overflow-hidden rounded-2xl border border-border bg-card shadow-2xl">
        <div class="flex items-center justify-between border-b border-border px-5 py-4">
            <h3 class="text-base font-semibold text-foreground" id="fm_modal_title"></h3>
            <button type="button" class="inline-flex h-8 w-8 items-center justify-center rounded-full text-muted-foreground transition hover:bg-muted hover:text-foreground" data-fm-modal-close>
                <i data-lucide="x" class="h-4 w-4"></i>
            </button>
        </div>
        <div class="space-y-3 px-5 py-4">
            <label class="text-sm font-medium text-foreground" id="fm_modal_label"></label>
            <input id="fm_modal_input" type="text" class="w-full rounded-lg border border-border bg-input px-3 py-2 text-sm text-foreground placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-secondary/30 dark:focus-visible:ring-secondary/40">
        </div>
        <div class="flex items-center justify-end gap-2 border-t border-border px-5 py-4">
            <button type="button" class="inline-flex items-center rounded-lg border border-border bg-card px-3 py-2 text-xs font-semibold text-foreground transition hover:bg-muted" data-fm-modal-close>
                <?= lang('close') ?>
            </button>
            <button type="button" class="inline-flex items-center rounded-lg bg-slate-900 px-3 py-2 text-xs font-semibold text-white transition hover:bg-slate-800 dark:bg-slate-100 dark:text-slate-900 dark:hover:bg-white" id="fm_modal_submit">
                <?= lang('save') ?>
            </button>
        </div>
    </div>
    <div id="fm_inline_editor" class="hidden"></div>
</div>

<div id="fm_confirm" class="fixed inset-0 z-50 hidden items-center justify-center bg-slate-950/70 p-4 backdrop-blur-[1px]">
    <div class="w-full max-w-sm overflow-hidden rounded-2xl border border-border bg-card shadow-2xl">
        <div class="border-b border-border px-5 py-4">
            <div class="text-base font-semibold text-foreground"><?= lang('delete') ?>?</div>
            <p class="mt-1 text-sm text-muted-foreground"><?= lang('are_you_sure') ?></p>
        </div>
        <div class="flex items-center justify-end gap-2 px-5 py-4">
            <button type="button" class="inline-flex items-center rounded-lg border border-border bg-card px-3 py-2 text-xs font-semibold text-foreground transition hover:bg-muted" data-fm-confirm-close>
                <?= lang('close') ?>
            </button>
            <button type="button" class="inline-flex items-center rounded-lg bg-rose-600 px-3 py-2 text-xs font-semibold text-white transition hover:bg-rose-500" id="fm_confirm_submit">
                <?= lang('delete') ?>
            </button>
        </div>
    </div>
</div>

<div id="fm_dup_modal" class="fixed inset-0 z-50 hidden items-center justify-center bg-slate-950/60 p-4">
    <div class="w-full max-w-md rounded-2xl bg-white p-5 shadow-xl">
        <div class="mb-3 text-base font-semibold text-foreground"><?= lang('file_exists') ?></div>
        <p class="text-sm text-muted-foreground"><?= lang('file_exists') ?>. <?= lang('choose_action') ?></p>
        <div id="fm_dup_list" class="mt-3 max-h-40 overflow-auto rounded-lg border border-border bg-muted/20 p-3 text-xs text-muted-foreground"></div>
        <div class="mt-5 flex flex-wrap items-center justify-end gap-2">
            <button type="button" class="inline-flex items-center rounded-lg border border-border bg-card px-3 py-2 text-xs font-semibold text-foreground hover:bg-muted" data-fm-dup-cancel>
                <?= lang('not_upload') ?>
            </button>
            <button type="button" class="inline-flex items-center rounded-lg border border-border bg-card px-3 py-2 text-xs font-semibold text-foreground hover:bg-muted" data-fm-dup-keep>
                <?= lang('keep_both') ?>
            </button>
            <button type="button" class="inline-flex items-center rounded-lg bg-rose-600 px-3 py-2 text-xs font-semibold text-white hover:bg-rose-500" data-fm-dup-replace>
                <?= lang('replace') ?>
            </button>
        </div>
    </div>
</div>
<div id="fm_paste_modal" class="fixed inset-0 z-50 hidden items-center justify-center bg-slate-950/60 p-4">
    <div class="w-full max-w-md rounded-2xl border border-border bg-card p-5 shadow-xl">
        <div class="text-base font-semibold text-foreground"><?= lang('duplicate') ?> <?= lang('files') ?></div>
        <p class="mt-1 text-xs text-muted-foreground"><?= lang('file') ?> <?= lang('already_exist') ?>. <?= lang('choose') ?> <?= lang('action') ?>.</p>
        <div id="fm_paste_list" class="mt-3 max-h-40 overflow-auto rounded-lg border border-border bg-muted/20 p-3 text-xs text-muted-foreground"></div>
        <div class="mt-4 flex flex-wrap items-center justify-end gap-2">
            <button type="button" class="inline-flex items-center rounded-lg border border-border bg-card px-3 py-2 text-xs font-semibold text-foreground hover:bg-muted" data-fm-paste-cancel>
                <?= lang('cancel') ?>
            </button>
            <button type="button" class="inline-flex items-center rounded-lg border border-border bg-card px-3 py-2 text-xs font-semibold text-foreground hover:bg-muted" data-fm-paste-keep>
                <?= lang('keep') ?> <?= lang('both') ?>
            </button>
            <button type="button" class="inline-flex items-center rounded-lg bg-rose-600 px-3 py-2 text-xs font-semibold text-white hover:bg-rose-500" data-fm-paste-replace>
                <?= lang('replace') ?>
            </button>
        </div>
    </div>
</div>

<script>
(() => {
    const app = document.getElementById("fm_app");
    if (!app) return;
    const connector = app.dataset.connector;
    const grid = document.getElementById("fm_grid");
    const gridWrap = document.getElementById("fm_grid_wrap");
    const list = document.getElementById("fm_list");
    const listBody = document.getElementById("fm_list_body");
    const empty = document.getElementById("fm_empty");
    const breadcrumb = document.getElementById("fm_breadcrumb");
    const sidebarFolders = document.getElementById("fm_sidebar_folders");
    const itemCount = document.getElementById("fm_item_count");
    const footerSummary = document.getElementById("fm_footer_summary");
    const pageSizeSelect = document.getElementById("fm_page_size");
    const prevPageBtn = document.getElementById("fm_prev_page");
    const nextPageBtn = document.getElementById("fm_next_page");
    const pageNumbers = document.getElementById("fm_page_numbers");
    const uploadInput = document.getElementById("fm_upload_input");
    const dropZone = document.getElementById("fm_drop");
    const searchInput = document.getElementById("fm_search");
    const filterSelect = document.getElementById("fm_filter");
    const sortBtn = document.getElementById("fm_sort_btn");
    const sortMenu = document.getElementById("fm_sort_menu");
    const bulkBar = document.getElementById("fm_bulkbar");
    const bulkCount = document.getElementById("fm_bulk_count");
    const selectAll = document.getElementById("fm_select_all");
    const selectBox = document.getElementById("fm_select_box");
    const previewDrawer = document.getElementById("fm_preview_drawer");
    const previewBackdrop = document.getElementById("fm_preview_backdrop");
    const previewEmpty = document.getElementById("fm_preview_empty");
    const previewBody = document.getElementById("fm_preview_body");
    const previewIcon = document.getElementById("fm_preview_icon");
    const previewName = document.getElementById("fm_preview_name");
    const previewMeta = document.getElementById("fm_preview_meta");
    const previewMedia = document.getElementById("fm_preview_media");
    const copyLinkBtn = document.getElementById("fm_copy_link");
    const deleteSingleBtn = document.getElementById("fm_delete_single");
    const openFileBtn = document.getElementById("fm_open_file");

    const modal = document.getElementById("fm_modal");
    const modalTitle = document.getElementById("fm_modal_title");
    const modalLabel = document.getElementById("fm_modal_label");
    const modalInput = document.getElementById("fm_modal_input");
    const modalSubmit = document.getElementById("fm_modal_submit");

    const confirmModal = document.getElementById("fm_confirm");
    const confirmSubmit = document.getElementById("fm_confirm_submit");
    const dupModal = document.getElementById("fm_dup_modal");
    const dupList = document.getElementById("fm_dup_list");
    const dupCancel = document.querySelector("[data-fm-dup-cancel]");
    const dupKeep = document.querySelector("[data-fm-dup-keep]");
    const dupReplace = document.querySelector("[data-fm-dup-replace]");
    const pasteModal = document.getElementById("fm_paste_modal");
    const pasteList = document.getElementById("fm_paste_list");
    const pasteCancel = document.querySelector("[data-fm-paste-cancel]");
    const pasteKeep = document.querySelector("[data-fm-paste-keep]");
    const pasteReplace = document.querySelector("[data-fm-paste-replace]");

    const state = {
        cwd: null,
        files: [],
        history: [],
        modalAction: null,
        target: null,
        view: localStorage.getItem("fm_view") || "grid",
        selected: new Set(),
        previewTarget: null,
        search: "",
        filter: localStorage.getItem("fm_filter") || "all",
        sort: localStorage.getItem("fm_sort") || "folderFirst",
        favorites: new Set(JSON.parse(localStorage.getItem("fm_favorites") || "[]")),
        page: 1,
        pageSize: Number(localStorage.getItem("fm_page_size") || 24),
        pathMap: {},
        pathParts: [],
        rootName: "filemanager",
        contextMenu: null,
        clipboard: null,
        replacedNames: new Set(),
        previewBlobUrl: null
    };

    let previewRequestToken = 0;

    const request = async (data, files) => {
        if (!connector) return null;
        const form = new FormData();
        Object.keys(data).forEach((key) => {
            const value = data[key];
            if (Array.isArray(value)) {
                value.forEach((val) => form.append(key + "[]", val));
            } else if (value !== undefined && value !== null) {
                form.append(key, value);
            }
        });
        if (files && files.length) {
            Array.from(files).forEach((file) => form.append("upload[]", file));
        }
        const res = await fetch(connector, { method: "POST", body: form });
        return res.json();
    };

    let pathUpdateToken = 0;
    const updatePathMap = async (cwd) => {
        state.pathMap = {};
        state.pathParts = [];
        if (!cwd) return;
        const token = ++pathUpdateToken;
        const nodes = [];
        const seen = new Set();
        let node = cwd;
        while (node) {
            if (!node.hash || seen.has(node.hash)) break;
            seen.add(node.hash);
            nodes.push({ name: node.name || state.rootName, hash: node.hash, phash: node.phash });
            if (!node.phash) break;
            const res = await request({ cmd: "open", target: node.phash });
            if (!res || !res.cwd) break;
            node = res.cwd;
        }
        if (token !== pathUpdateToken) return;
        nodes.reverse();
        if (!nodes.length) {
            state.pathMap["/"] = cwd.hash;
            state.pathParts = [state.rootName];
            return;
        }
        state.rootName = nodes[0].name || state.rootName;
        let cur = "";
        nodes.forEach((n, idx) => {
            if (idx === 0) {
                state.pathMap["/"] = n.hash;
                state.pathParts.push(n.name || state.rootName);
            } else {
                cur += "/" + n.name;
                state.pathMap[cur] = n.hash;
                state.pathParts.push(n.name);
            }
        });
        const rootName = state.pathParts[0];
        if (rootName) {
            let lastRoot = -1;
            state.pathParts.forEach((part, idx) => {
                if (part === rootName) lastRoot = idx;
            });
            if (lastRoot > 0) {
                state.pathParts = state.pathParts.slice(lastRoot);
            }
        }
    };

    const loadRoot = async () => {
        const data = await request({ cmd: "open", init: 1 });
        if (data && data.cwd) {
            state.cwd = data.cwd;
            state.files = data.files || [];
            state.history = [];
            state.page = 1;
            state.rootName = data.cwd.name || "filemanager";
            await updatePathMap(data.cwd);
            render();
        }
    };

    const openDir = async (hash) => {
        const data = await request({ cmd: "open", target: hash });
        if (data && data.cwd) {
            state.history.push(state.cwd.hash);
            state.cwd = data.cwd;
            state.files = data.files || [];
            state.page = 1;
            await updatePathMap(state.cwd);
            render();
        }
    };

    const goBack = async () => {
        if (!state.history.length) return;
        const prev = state.history.pop();
        const data = await request({ cmd: "open", target: prev });
        if (data && data.cwd) {
            state.cwd = data.cwd;
            state.files = data.files || [];
            state.page = 1;
            await updatePathMap(state.cwd);
            render();
        }
    };

    const openModal = (title, label, action, target, value = "") => {
        state.modalAction = action;
        state.target = target || null;
        modalTitle.textContent = title;
        modalLabel.textContent = label;
        if (!value && action === "rename" && target) {
            const match = (state.files || []).find((f) => f.hash === target);
            value = match?.name || "";
        }
        modalInput.value = value || "";
        modal.classList.remove("hidden");
        modal.classList.add("flex");
        modalInput.focus();
    };

    const closeModal = () => {
        modal.classList.add("hidden");
        modal.classList.remove("flex");
    };

    const openConfirm = (target) => {
        state.target = target;
        confirmModal.classList.remove("hidden");
        confirmModal.classList.add("flex");
    };

    const closeConfirm = () => {
        confirmModal.classList.add("hidden");
        confirmModal.classList.remove("flex");
    };

    const iconFor = (file) => {
        if (file.mime === "directory" || file.mime === "application/x-directory") {
            return "folder";
        }
        if ((file.mime || "").startsWith("image")) return "image";
        if ((file.mime || "").startsWith("video")) return "video";
        if ((file.mime || "").startsWith("audio")) return "music";
        return "file-text";
    };

    const typeFor = (file) => {
        if (file.mime === "directory" || file.mime === "application/x-directory") return "folder";
        if ((file.mime || "").startsWith("image")) return "image";
        if ((file.mime || "").startsWith("video")) return "video";
        if ((file.mime || "").startsWith("audio")) return "audio";
        if ((file.mime || "").startsWith("text") || (file.mime || "").includes("pdf") || (file.mime || "").includes("word") || (file.mime || "").includes("excel")) return "doc";
        return "other";
    };

    const colorFor = (file) => {
        const type = typeFor(file);
        if (type === "folder") return "bg-orange-100 text-orange-600";
        if (type === "image") return "bg-emerald-100 text-emerald-600";
        if (type === "video") return "bg-sky-100 text-sky-600";
        if (type === "audio") return "bg-amber-100 text-amber-600";
        if (type === "doc") return "bg-indigo-100 text-indigo-600";
        return "bg-slate-100 text-slate-600";
    };

    const formatBytes = (value) => {
        const bytes = Number(value || 0);
        if (!bytes) {
            return "0 KB";
        }
        if (bytes < 1024) {
            return `${bytes} B`;
        }
        const units = ["KB", "MB", "GB", "TB"];
        let amount = bytes / 1024;
        let unitIndex = 0;
        while (amount >= 1024 && unitIndex < units.length - 1) {
            amount /= 1024;
            unitIndex += 1;
        }
        const precision = amount >= 100 ? 0 : amount >= 10 ? 1 : 2;
        return `${amount.toFixed(precision)} ${units[unitIndex]}`;
    };

    const escapeHtml = (value) => String(value || "")
        .replace(/&/g, "&amp;")
        .replace(/</g, "&lt;")
        .replace(/>/g, "&gt;")
        .replace(/"/g, "&quot;")
        .replace(/'/g, "&#39;");

    const isDirectory = (file) => file?.mime === "directory" || file?.mime === "application/x-directory";

    const getExtensionLabel = (file) => {
        if (isDirectory(file)) return "DIR";
        const name = file?.name || "";
        const ext = name.includes(".") ? name.split(".").pop() : "";
        if (ext) return ext.toUpperCase().slice(0, 6);
        const mime = typeFor(file);
        return mime ? mime.toUpperCase() : "FILE";
    };

    const getDateLabel = (file) => {
        if (!file?.ts) return "";
        return new Date(file.ts * 1000).toLocaleDateString();
    };

    const getFolderContextLabel = (file) => {
        if (isDirectory(file)) {
            return "Folder";
        }
        const current = state.pathParts[state.pathParts.length - 1];
        return current || "Unsorted";
    };

    const thumbMarkup = (file, mode = "grid") => {
        const thumbClass = mode === "list" ? "fm-thumb-list" : "fm-thumb-grid";
        const type = typeFor(file);
        const icon = iconFor(file);
        const url = file.url || `${connector}?cmd=file&target=${encodeURIComponent(file.hash)}`;
        const safeUrl = escapeHtml(url);

        if ((file.mime || "").startsWith("image")) {
            return `<div class="fm-thumb ${thumbClass}"><img src="${safeUrl}" alt="${escapeHtml(file.name)}"></div>`;
        }

        let skinClass = "fm-thumb-doc";
        let label = getExtensionLabel(file);
        if (isDirectory(file)) {
            skinClass = "fm-thumb-folder";
            label = "Folder";
        } else if (type === "video" || type === "audio") {
            skinClass = "fm-thumb-media";
        }

        return `
            <div class="fm-thumb ${thumbClass} ${skinClass}">
                <div class="fm-thumb-placeholder">
                    <i data-lucide="${icon}" class="${mode === "list" ? "h-5 w-5" : "h-8 w-8"}"></i>
                    <span class="${mode === "list" ? "text-[10px]" : "text-xs"} font-bold uppercase tracking-[0.18em]">${escapeHtml(label)}</span>
                </div>
            </div>
        `;
    };

    const revokePreviewBlobUrl = () => {
        if (state.previewBlobUrl) {
            URL.revokeObjectURL(state.previewBlobUrl);
            state.previewBlobUrl = null;
        }
    };

    const loadPreviewBlobUrl = async (file) => {
        const url = file.url || `${connector}?cmd=file&target=${encodeURIComponent(file.hash)}`;
        const response = await fetch(url, { credentials: "same-origin" });
        if (!response.ok) {
            throw new Error(`Preview request failed with status ${response.status}`);
        }
        const blob = await response.blob();
        revokePreviewBlobUrl();
        state.previewBlobUrl = URL.createObjectURL(blob);
        return state.previewBlobUrl;
    };

    const applyFilters = (files) => {
        const q = state.search.toLowerCase();
        return files.filter((f) => {
            const matchesSearch = !q || (f.name || "").toLowerCase().includes(q);
            const matchesFilter = state.filter === "all" || (state.filter === "favorite" ? state.favorites.has(f.hash) : typeFor(f) === state.filter);
            return matchesSearch && matchesFilter;
        });
    };

    const applySort = (files) => {
        const sorted = [...files];
        sorted.sort((a, b) => {
            if (state.sort === "folderFirst") {
                const aDir = a.mime === "directory" || a.mime === "application/x-directory";
                const bDir = b.mime === "directory" || b.mime === "application/x-directory";
                if (aDir !== bDir) return aDir ? -1 : 1;
                return (a.name || "").localeCompare(b.name || "");
            }
            if (state.sort === "date") return (b.ts || 0) - (a.ts || 0);
            if (state.sort === "size") return (b.size || 0) - (a.size || 0);
            if (state.sort === "type") return typeFor(a).localeCompare(typeFor(b));
            return (a.name || "").localeCompare(b.name || "");
        });
        return sorted;
    };

    const renderBulk = () => {
        const count = state.selected.size;
        bulkCount.textContent = count;
        bulkBar.classList.toggle("hidden", count === 0);
        bulkBar.classList.toggle("flex", count > 0);
    };

    const renderStats = () => {
        const all = (state.files || []).filter((f) => f.name && f.hash !== state.cwd?.hash);
        const folders = all.filter((f) => f.mime === "directory" || f.mime === "application/x-directory");
        const label = `${all.length} ${all.length === 1 ? "Item" : "Items"}`;
        if (itemCount) itemCount.textContent = label;
        return { all, folders };
    };

    const toggleSelected = (hash, checked) => {
        if (checked) state.selected.add(hash);
        else state.selected.delete(hash);
        renderBulk();
        render();
    };

    const renderBreadcrumb = () => {
        breadcrumb.innerHTML = "";
        const parts = state.pathParts.length ? state.pathParts : [state.rootName || "filemanager"];
        const entries = [];
        let cur = "";
        parts.forEach((p, idx) => {
            if (idx === 0) {
                entries.push({ name: p, path: "/" });
            } else {
                cur += "/" + p;
                entries.push({ name: p, path: cur });
            }
        });
        const wrap = document.createElement("div");
        wrap.className = "fm-path inline-flex items-center gap-1 rounded-full px-3 py-1.5 text-xs font-semibold text-foreground";
        entries.forEach((entry, idx) => {
            const hash = state.pathMap[entry.path];
            const btn = document.createElement("button");
            btn.type = "button";
            btn.className = "rounded-full px-1.5 py-0.5 text-xs font-semibold hover:underline";
            btn.textContent = entry.name;
            if (hash) {
                btn.addEventListener("click", () => openDir(hash));
            } else {
                btn.disabled = true;
                btn.classList.add("opacity-60");
            }
            wrap.appendChild(btn);
            if (idx < entries.length - 1) {
                const sep = document.createElement("span");
                sep.className = "fm-path-sep text-xs";
                sep.textContent = "/";
                wrap.appendChild(sep);
            }
        });
        breadcrumb.appendChild(wrap);
    };

    const renderSidebarFolders = (allFiles) => {
        if (!sidebarFolders) return;
        const directories = (allFiles || []).filter((file) => isDirectory(file));
        const rootHash = state.pathMap["/"] || state.cwd?.hash;
        const atRoot = rootHash && state.cwd?.hash === rootHash;

        sidebarFolders.innerHTML = "";

        const rootRow = document.createElement("div");
        rootRow.className = `fm-folder-row${atRoot ? " is-active" : ""}`;
        rootRow.innerHTML = `
            <button type="button" class="fm-folder-main">
                <span class="fm-folder-glyph">
                    <i data-lucide="images" class="h-4 w-4"></i>
                </span>
                <span>
                    All Media
                    <small>root</small>
                </span>
            </button>
        `;
        rootRow.querySelector(".fm-folder-main").addEventListener("click", () => {
            if (rootHash) {
                openDir(rootHash);
            }
        });
        sidebarFolders.appendChild(rootRow);

        directories.forEach((file) => {
            const row = document.createElement("div");
            row.className = "fm-folder-row";
            row.innerHTML = `
                <button type="button" class="fm-folder-main">
                    <span class="fm-folder-glyph fm-folder-glyph-folder">
                        <i data-lucide="folder" class="h-4 w-4"></i>
                    </span>
                    <span>
                        ${escapeHtml(file.name)}
                        <small>${getDateLabel(file) || "Folder"}</small>
                    </span>
                </button>
                <div class="fm-folder-actions">
                    <button type="button" class="fm-folder-action" data-action="rename" title="<?= lang('rename') ?>">
                        <i data-lucide="square-pen" class="h-4 w-4"></i>
                    </button>
                    <button type="button" class="fm-folder-action is-danger" data-action="delete" title="<?= lang('delete') ?>">
                        <i data-lucide="trash-2" class="h-4 w-4"></i>
                    </button>
                </div>
            `;
            row.querySelector(".fm-folder-main").addEventListener("click", () => openDir(file.hash));
            row.querySelector('[data-action="rename"]').addEventListener("click", (e) => {
                e.stopPropagation();
                openModal("<?= lang('rename') ?>", "<?= lang('name') ?>", "rename", file.hash, file.name || "");
            });
            row.querySelector('[data-action="delete"]').addEventListener("click", (e) => {
                e.stopPropagation();
                openConfirm(file.hash);
            });
            sidebarFolders.appendChild(row);
        });

        if (!directories.length) {
            const emptyRow = document.createElement("div");
            emptyRow.className = "rounded-2xl border border-dashed border-border bg-muted/40 px-3 py-4 text-xs text-muted-foreground";
            emptyRow.textContent = "No folders in this directory yet.";
            sidebarFolders.appendChild(emptyRow);
        }
    };

    const paginateFiles = (files) => {
        const pageSize = Math.max(1, Number(state.pageSize || 24));
        const totalPages = Math.max(1, Math.ceil(files.length / pageSize));
        state.page = Math.min(Math.max(1, state.page || 1), totalPages);
        const startIndex = (state.page - 1) * pageSize;
        const endIndex = startIndex + pageSize;
        return {
            pageSize,
            totalPages,
            startIndex,
            endIndex,
            pageFiles: files.slice(startIndex, endIndex),
        };
    };

    const renderPagination = (totalItems, paging) => {
        if (!pageNumbers || !prevPageBtn || !nextPageBtn) return;

        pageNumbers.innerHTML = "";
        prevPageBtn.disabled = state.page <= 1;
        nextPageBtn.disabled = state.page >= paging.totalPages;

        const pages = [];
        if (paging.totalPages <= 5) {
            for (let i = 1; i <= paging.totalPages; i += 1) pages.push(i);
        } else {
            pages.push(1);
            if (state.page > 3) pages.push("ellipsis-left");
            for (let i = Math.max(2, state.page - 1); i <= Math.min(paging.totalPages - 1, state.page + 1); i += 1) {
                pages.push(i);
            }
            if (state.page < paging.totalPages - 2) pages.push("ellipsis-right");
            pages.push(paging.totalPages);
        }

        pages.forEach((page) => {
            if (String(page).includes("ellipsis")) {
                const ellipsis = document.createElement("span");
                ellipsis.className = "px-1 text-sm text-slate-400";
                ellipsis.textContent = "...";
                pageNumbers.appendChild(ellipsis);
                return;
            }
            const btn = document.createElement("button");
            btn.type = "button";
            btn.className = `fm-pagination-btn${page === state.page ? " is-active" : ""}`;
            btn.textContent = String(page);
            btn.addEventListener("click", () => {
                state.page = page;
                render();
            });
            pageNumbers.appendChild(btn);
        });

        if (footerSummary) {
            const showingFrom = totalItems ? paging.startIndex + 1 : 0;
            const showingTo = Math.min(totalItems, paging.startIndex + paging.pageFiles.length);
            footerSummary.textContent = `Showing ${showingFrom} - ${showingTo} of ${totalItems}`;
        }
    };

    const renderGrid = (files) => {
        grid.innerHTML = "";
        files.forEach((file) => {
            const isDir = isDirectory(file);
            const size = file.size ? formatBytes(file.size) : "";
            const date = getDateLabel(file);
            const checked = state.selected.has(file.hash);
            const isFav = state.favorites.has(file.hash);
            const card = document.createElement("div");
            card.dataset.hash = file.hash;
            card.className = `fm-card group relative overflow-hidden rounded-[24px] p-4 transition${state.replacedNames.has(file.name) ? " fm-replaced" : ""}`;
            card.innerHTML = `
                <div class="absolute left-4 top-4 z-10">
                    <input type="checkbox" class="fm-card-check fm-check h-5 w-5 rounded-md transition" ${checked ? "checked" : ""}>
                </div>
                <div class="absolute right-4 top-4 z-10 flex items-center gap-2">
                    <button class="fm-card-star" data-action="favorite" title="<?= lang('favorite') ?>">
                            <i data-lucide="${isFav ? "star" : "star-off"}" class="h-4 w-4 ${isFav ? "text-amber-400" : ""}"></i>
                        </button>
                    <button class="fm-card-menu" data-action="menu" title="<?= lang('more') ?>">
                            <i data-lucide="more-horizontal" class="h-4 w-4"></i>
                        </button>
                </div>
                ${thumbMarkup(file, "grid")}
                <div class="px-1.5 pb-1 pt-2.5">
                    <div class="min-h-[40px] text-[0.88rem] font-semibold leading-5 text-slate-900 dark:text-slate-100 fm-name">${escapeHtml(file.name)}</div>
                    <div class="mt-1.5 flex flex-wrap items-center gap-1.5">
                        <span class="fm-badge">${escapeHtml(getExtensionLabel(file))}</span>
                        <span class="fm-status-badge">${isFav ? "Favourite" : (isDir ? "Folder" : "Stored")}</span>
                    </div>
                    <div class="mt-2 flex items-center justify-between gap-2 text-[0.72rem] text-slate-500 dark:text-slate-400">
                        <span class="truncate">${escapeHtml(getFolderContextLabel(file))}</span>
                        <span class="shrink-0 font-semibold text-sky-700 dark:text-sky-300">${size || date || "0 KB"}</span>
                    </div>
                </div>
            `;
            card.addEventListener("click", () => {
                if (isDir) {
                    openDir(file.hash);
                    return;
                }
                openPreviewFor(file);
            });
            card.addEventListener("dblclick", () => {
                if (isDir) {
                    openDir(file.hash);
                } else {
                    window.open(file.url || `${connector}?cmd=file&target=${encodeURIComponent(file.hash)}`, "_blank");
                }
            });
            card.querySelector(".fm-check").addEventListener("click", (e) => {
                e.stopPropagation();
            });
            card.querySelector(".fm-check").addEventListener("change", (e) => {
                toggleSelected(file.hash, e.target.checked);
            });
            card.querySelector('[data-action="favorite"]').addEventListener("click", (e) => {
                e.stopPropagation();
                toggleFavorite(file.hash);
                render();
            });
            card.querySelector('[data-action="menu"]').addEventListener("click", (e) => {
                e.stopPropagation();
                showContextMenu(e.pageX, e.pageY, file);
            });
            card.addEventListener("contextmenu", (e) => {
                e.preventDefault();
                showContextMenu(e.pageX, e.pageY, file);
            });
            grid.appendChild(card);
        });
    };

    const renderList = (files) => {
        listBody.innerHTML = "";
        files.forEach((file) => {
            const isDir = isDirectory(file);
            const size = file.size ? formatBytes(file.size) : "";
            const date = getDateLabel(file);
            const checked = state.selected.has(file.hash);
            const isFav = state.favorites.has(file.hash);
            const row = document.createElement("div");
            row.dataset.hash = file.hash;
            row.className = `fm-list-card group${state.replacedNames.has(file.name) ? " fm-replaced" : ""}`;
            row.innerHTML = `
                <div class="flex items-start gap-2">
                    <div class="pt-1">
                        <input type="checkbox" class="fm-card-check fm-check h-5 w-5 rounded-md" ${checked ? "checked" : ""}>
                    </div>
                    ${thumbMarkup(file, "list")}
                    <div class="min-w-0 flex-1">
                        <div class="flex items-start justify-between gap-3">
                            <div class="min-w-0">
                                <div class="truncate text-[0.88rem] font-semibold text-slate-900 dark:text-slate-100 fm-name">${escapeHtml(file.name)}</div>
                                <div class="mt-0.5 flex flex-wrap items-center gap-1.5 text-[0.76rem] text-slate-500 dark:text-slate-400">
                                    <span class="font-semibold uppercase tracking-[0.12em]">${escapeHtml(getExtensionLabel(file))}</span>
                                    <span>${size || date || "0 KB"}</span>
                                </div>
                            </div>
                            <div class="flex items-center gap-2">
                                <button class="fm-card-star" data-action="favorite" title="<?= lang('favorite') ?>">
                                    <i data-lucide="${isFav ? "star" : "star-off"}" class="h-4 w-4 ${isFav ? "text-amber-400" : ""}"></i>
                                </button>
                                <button class="fm-card-menu" data-action="menu" title="<?= lang('more') ?>">
                                    <i data-lucide="more-horizontal" class="h-4 w-4"></i>
                                </button>
                            </div>
                        </div>
                        <div class="mt-1.5 flex flex-wrap items-center gap-1.5">
                            <span class="fm-badge">${escapeHtml(getExtensionLabel(file))}</span>
                            <span class="fm-status-badge">${isFav ? "Favourite" : (isDir ? "Folder" : "Stored")}</span>
                        </div>
                    </div>
                </div>
            `;
            row.addEventListener("click", () => {
                if (isDir) {
                    openDir(file.hash);
                    return;
                }
                openPreviewFor(file);
            });
            row.addEventListener("dblclick", () => {
                if (isDir) openDir(file.hash);
                else window.open(file.url || `${connector}?cmd=file&target=${encodeURIComponent(file.hash)}`, "_blank");
            });
            row.addEventListener("contextmenu", (e) => {
                e.preventDefault();
                showContextMenu(e.pageX, e.pageY, file);
            });
            row.querySelector(".fm-check").addEventListener("click", (e) => {
                e.stopPropagation();
            });
            row.querySelector(".fm-check").addEventListener("change", (e) => {
                toggleSelected(file.hash, e.target.checked);
            });
            row.querySelector('[data-action="favorite"]').addEventListener("click", (e) => {
                e.stopPropagation();
                toggleFavorite(file.hash);
                render();
            });
            row.querySelector('[data-action="menu"]').addEventListener("click", (e) => {
                e.stopPropagation();
                showContextMenu(e.pageX, e.pageY, file);
            });
            listBody.appendChild(row);
        });
    };

    const openPreview = () => {
        previewDrawer.classList.remove("hidden");
        previewBackdrop.classList.remove("hidden");
        requestAnimationFrame(() => {
            previewDrawer.classList.remove("translate-x-full");
        });
    };
    const closePreview = () => {
        previewDrawer.classList.add("translate-x-full");
        previewBackdrop.classList.add("hidden");
        setTimeout(() => previewDrawer.classList.add("hidden"), 200);
        revokePreviewBlobUrl();
        state.previewTarget = null;
    };

    const renderPreview = async () => {
        const token = ++previewRequestToken;
        const target = state.previewTarget || Array.from(state.selected).pop();
        if (!target) {
            previewEmpty.classList.remove("hidden");
            previewBody.classList.add("hidden");
            revokePreviewBlobUrl();
            closePreview();
            return;
        }
        const file = state.files.find((f) => f.hash === target);
        if (!file) return;
        previewEmpty.classList.add("hidden");
        previewBody.classList.remove("hidden");
        previewIcon.innerHTML = `<i data-lucide="${iconFor(file)}" class="h-5 w-5"></i>`;
        previewName.textContent = file.name;
        previewMeta.textContent = `${file.mime || ""} • ${file.size ? Math.round(file.size / 1024) + " KB" : ""}`;
        const url = file.url || `${connector}?cmd=file&target=${encodeURIComponent(file.hash)}`;
        previewMedia.innerHTML = "";
        revokePreviewBlobUrl();
        if ((file.mime || "").startsWith("image")) {
            const img = document.createElement("img");
            img.src = url;
            img.className = "w-full";
            previewMedia.appendChild(img);
        } else if ((file.mime || "").startsWith("video")) {
            const vid = document.createElement("video");
            vid.src = url;
            vid.controls = true;
            vid.className = "w-full";
            previewMedia.appendChild(vid);
        } else if ((file.mime || "").includes("pdf")) {
            try {
                const blobUrl = await loadPreviewBlobUrl(file);
                if (token !== previewRequestToken || state.previewTarget !== file.hash) {
                    if (blobUrl === state.previewBlobUrl) {
                        revokePreviewBlobUrl();
                    } else {
                        URL.revokeObjectURL(blobUrl);
                    }
                    return;
                }
                const iframe = document.createElement("iframe");
                iframe.src = blobUrl;
                iframe.className = "h-[28rem] w-full";
                previewMedia.appendChild(iframe);
            } catch (error) {
                const iframe = document.createElement("iframe");
                iframe.src = url;
                iframe.className = "h-[28rem] w-full";
                previewMedia.appendChild(iframe);
            }
        } else {
            previewMedia.innerHTML = `<div class="p-6 text-center text-xs text-muted-foreground"><?= lang('no_preview') ?></div>`;
        }
        openFileBtn.onclick = () => window.open(url, "_blank");
        if (window.lucide) window.lucide.createIcons();
    };

    const openPreviewFor = (file) => {
        state.previewTarget = file.hash;
        renderPreview();
        openPreview();
    };

    const syncControls = () => {
        if (filterSelect) filterSelect.value = state.filter;
        document.querySelectorAll("[data-fm-view]").forEach((btn) => {
            const active = btn.dataset.fmView === state.view;
            btn.classList.toggle("is-active", active);
        });
    };

    const render = () => {
        const all = (state.files || []).filter((f) => f.name && f.hash !== state.cwd?.hash);
        const files = applySort(applyFilters(all));
        const paging = paginateFiles(files);
        renderStats();
        if (itemCount) {
            itemCount.textContent = `${files.length} ${files.length === 1 ? "Item" : "Items"}`;
        }
        renderBreadcrumb();
        renderSidebarFolders(all);
        syncControls();
        renderPagination(files.length, paging);
        if (!files.length) {
            empty.classList.remove("hidden");
        } else {
            empty.classList.add("hidden");
        }
        grid.classList.toggle("hidden", state.view !== "grid");
        list.classList.toggle("hidden", state.view !== "list");
        grid.style.display = state.view === "grid" ? "grid" : "none";
        list.style.display = state.view === "list" ? "block" : "none";
        if (state.view === "grid") {
            renderGrid(paging.pageFiles);
        } else {
            renderList(paging.pageFiles);
        }
        renderBulk();
        renderPreview();
        if (state.replacedNames.size) {
            const next = new Set(state.replacedNames);
            setTimeout(() => {
                state.replacedNames.clear();
                render();
            }, 1200);
            state.replacedNames = next;
        }
        if (window.lucide) {
            window.lucide.createIcons();
        }
    };

    const toggleFavorite = (hash) => {
        if (state.favorites.has(hash)) state.favorites.delete(hash);
        else state.favorites.add(hash);
        localStorage.setItem("fm_favorites", JSON.stringify(Array.from(state.favorites)));
    };

    const inlineRename = (node, file) => {
        const nameEl = node.querySelector(".fm-name");
        if (!nameEl) return;
        const input = document.createElement("input");
        input.value = file.name;
        input.className = "w-full rounded-lg border border-border bg-input px-3 py-1.5 text-sm text-foreground placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-secondary/30 dark:focus-visible:ring-secondary/40";
        nameEl.replaceWith(input);
        input.focus();
        const finish = async (save) => {
            if (save && input.value.trim() && input.value.trim() !== file.name) {
                await request({ cmd: "rename", name: input.value.trim(), target: file.hash });
            }
            const data = await request({ cmd: "open", target: state.cwd?.hash });
            if (data && data.cwd) {
                state.cwd = data.cwd;
                state.files = data.files || [];
                render();
            }
        };
        input.addEventListener("keydown", (e) => {
            if (e.key === "Enter") finish(true);
            if (e.key === "Escape") finish(false);
        });
        input.addEventListener("blur", () => finish(true));
    };

    if (modalSubmit) {
        modalSubmit.addEventListener("click", async () => {
        const name = modalInput.value.trim();
        if (!name) return;
        if (state.modalAction === "mkdir") {
            await request({ cmd: "mkdir", name, target: state.cwd?.hash });
        } else if (state.modalAction === "mkfile") {
            await request({ cmd: "mkfile", name, target: state.cwd?.hash });
        } else if (state.modalAction === "rename") {
            await request({ cmd: "rename", name, target: state.target });
        }
        closeModal();
        const data = await request({ cmd: "open", target: state.cwd?.hash });
        if (data && data.cwd) {
            state.cwd = data.cwd;
            state.files = data.files || [];
            render();
        }
        });
    }

    if (confirmSubmit) {
        confirmSubmit.addEventListener("click", async () => {
        if (!state.target) return;
        await request({ cmd: "rm", targets: [state.target] });
        closeConfirm();
        const data = await request({ cmd: "open", target: state.cwd?.hash });
        if (data && data.cwd) {
            state.cwd = data.cwd;
            state.files = data.files || [];
            render();
        }
        });
    }

    document.querySelectorAll("[data-fm-modal-close]").forEach((btn) => {
        btn.addEventListener("click", closeModal);
    });
    document.querySelectorAll("[data-fm-confirm-close]").forEach((btn) => {
        btn.addEventListener("click", closeConfirm);
    });

    const newBtn = app.querySelector("[data-fm-new]");
    const newMenu = document.getElementById("fm_new_menu");
    if (newBtn && newMenu) {
        newBtn.addEventListener("click", () => {
            newMenu.classList.toggle("hidden");
        });
        document.addEventListener("click", (e) => {
            if (!newBtn.contains(e.target) && !newMenu.contains(e.target)) {
                newMenu.classList.add("hidden");
            }
        });
        const newFolderBtn = document.querySelector("[data-fm-new-folder]");
        const newFileBtn = document.querySelector("[data-fm-new-file]");
        if (newFolderBtn) {
            newFolderBtn.addEventListener("click", () => {
                newMenu.classList.add("hidden");
                openModal("<?= lang('new_folder') ?>", "<?= lang('name') ?>", "mkdir");
            });
        }
        if (newFileBtn) {
            newFileBtn.addEventListener("click", () => {
                newMenu.classList.add("hidden");
                openModal("<?= lang('new_file') ?>", "<?= lang('name') ?>", "mkfile");
            });
        }
    }

    const uploadBtn = app.querySelector("[data-fm-upload]");
    if (uploadBtn && uploadInput) {
        uploadBtn.addEventListener("click", () => {
            uploadInput.click();
        });
    }

    const uploadFiles = async (files, opts = {}) => {
        if (!files || !files.length) return;
        if (!state.cwd?.hash) {
            await loadRoot();
        }
        const resp = await request({ cmd: "upload", target: state.cwd?.hash, ...opts }, files);
        if (resp?.error?.length) {
            alert(resp.error.join("\n"));
        }
        const data = await request({ cmd: "open", target: state.cwd?.hash });
        if (data && data.cwd) {
            state.cwd = data.cwd;
            state.files = data.files || [];
            render();
        }
    };

    const showDupModal = (names) => {
        dupList.innerHTML = names.map((n) => `<div class="py-1">• ${n}</div>`).join("");
        dupModal.classList.remove("hidden");
        dupModal.classList.add("flex");
    };
    const closeDupModal = () => {
        dupModal.classList.add("hidden");
        dupModal.classList.remove("flex");
    };

    let pendingPasteTargets = null;
    let pendingPasteConflicts = [];
    const showPasteModal = (names, targets) => {
        pendingPasteTargets = targets;
        pendingPasteConflicts = names;
        pasteList.innerHTML = names.map((n) => `<div class="py-1">• ${n}</div>`).join("");
        pasteModal.classList.remove("hidden");
        pasteModal.classList.add("flex");
    };
    const closePasteModal = () => {
        pasteModal.classList.add("hidden");
        pasteModal.classList.remove("flex");
        pendingPasteTargets = null;
        pendingPasteConflicts = [];
    };

    const getClipboardNames = async (targets) => {
        const info = await request({ cmd: "info", targets });
        if (info && Array.isArray(info.files)) {
            return info.files.map((f) => f.name).filter(Boolean);
        }
        return [];
    };

    const performPaste = async (opts = {}, replaced = []) => {
        if (!state.clipboard?.targets?.length) return;
        await request({
            cmd: "paste",
            dst: state.cwd?.hash,
            targets: state.clipboard.targets,
            cut: state.clipboard.mode === "cut" ? 1 : 0,
            ...opts
        });
        state.clipboard = null;
        const data = await request({ cmd: "open", target: state.cwd?.hash });
        if (data && data.cwd) {
            state.cwd = data.cwd;
            state.files = data.files || [];
            if (replaced.length) {
                state.replacedNames = new Set(replaced);
            }
            render();
        }
    };

    let pendingUpload = null;
    const handleUpload = async (files) => {
        const fileArr = Array.from(files || []);
        if (!fileArr.length) return;
        const existingNames = new Set((state.files || []).map((f) => f.name));
        const dupes = fileArr.filter((f) => existingNames.has(f.name));
        const newFiles = fileArr.filter((f) => !existingNames.has(f.name));
        if (newFiles.length) {
            await uploadFiles(newFiles);
        }
        if (dupes.length) {
            pendingUpload = dupes;
            showDupModal(dupes.map((f) => f.name));
        }
    };

    uploadInput.addEventListener("change", async (e) => {
        const files = e.target.files;
        if (!files || !files.length) return;
        try {
            await handleUpload(files);
        } catch (err) {
            console.error("Upload failed", err);
        } finally {
            uploadInput.value = "";
        }
    });

    if (searchInput) {
        searchInput.addEventListener("input", () => {
            state.search = searchInput.value || "";
            state.page = 1;
            render();
        });
    }
    if (filterSelect) {
        filterSelect.addEventListener("change", () => {
            state.filter = filterSelect.value;
            state.page = 1;
            localStorage.setItem("fm_filter", state.filter);
            render();
        });
    }
    if (pageSizeSelect) {
        pageSizeSelect.value = String(state.pageSize);
        pageSizeSelect.addEventListener("change", () => {
            state.pageSize = Number(pageSizeSelect.value || 24);
            state.page = 1;
            localStorage.setItem("fm_page_size", String(state.pageSize));
            render();
        });
    }
    if (prevPageBtn) {
        prevPageBtn.addEventListener("click", () => {
            if (state.page <= 1) return;
            state.page -= 1;
            render();
        });
    }
    if (nextPageBtn) {
        nextPageBtn.addEventListener("click", () => {
            state.page += 1;
            render();
        });
    }
    const sortGroup = document.getElementById("fm_sort_group");
    if (sortBtn && sortMenu && sortGroup) {
        sortBtn.addEventListener("click", (e) => {
            e.stopPropagation();
            sortMenu.classList.toggle("hidden");
        });
        sortMenu.querySelectorAll("[data-sort]").forEach((btn) => {
            btn.addEventListener("click", (e) => {
                e.stopPropagation();
                state.sort = btn.dataset.sort;
                state.page = 1;
                localStorage.setItem("fm_sort", state.sort);
                sortMenu.classList.add("hidden");
                render();
            });
        });
        document.addEventListener("click", (e) => {
            if (!sortGroup.contains(e.target)) {
                sortMenu.classList.add("hidden");
            }
        });
    }
    document.querySelectorAll("[data-fm-view]").forEach((btn) => {
        btn.addEventListener("click", () => {
            state.view = btn.dataset.fmView;
            localStorage.setItem("fm_view", state.view);
            render();
        });
    });

    const bulkClear = document.querySelector("[data-fm-bulk-clear]");
    const bulkZip = document.querySelector("[data-fm-bulk-zip]");
    const bulkUnzip = document.querySelector("[data-fm-bulk-unzip]");
    const bulkDelete = document.querySelector("[data-fm-bulk-delete]");
    if (bulkClear) {
        bulkClear.addEventListener("click", () => {
            state.selected.clear();
            render();
        });
    }
    if (bulkZip) {
        bulkZip.addEventListener("click", async () => {
            if (!state.selected.size) return;
            const name = `archive-${Date.now()}.zip`;
            await request({ cmd: "archive", targets: Array.from(state.selected), type: "zip", name });
            const data = await request({ cmd: "open", target: state.cwd?.hash });
            if (data && data.cwd) {
                state.cwd = data.cwd;
                state.files = data.files || [];
                render();
            }
        });
    }
    if (bulkUnzip) {
        bulkUnzip.addEventListener("click", async () => {
            if (!state.selected.size) return;
            for (const hash of Array.from(state.selected)) {
                await request({ cmd: "extract", target: hash });
            }
            const data = await request({ cmd: "open", target: state.cwd?.hash });
            if (data && data.cwd) {
                state.cwd = data.cwd;
                state.files = data.files || [];
                render();
            }
        });
    }
    if (bulkDelete) {
        bulkDelete.addEventListener("click", async () => {
            if (!state.selected.size) return;
            await request({ cmd: "rm", targets: Array.from(state.selected) });
            state.selected.clear();
            const data = await request({ cmd: "open", target: state.cwd?.hash });
            if (data && data.cwd) {
                state.cwd = data.cwd;
                state.files = data.files || [];
                render();
            }
        });
    }

    if (selectAll) {
        selectAll.addEventListener("change", (e) => {
            const all = applySort(applyFilters((state.files || []).filter((f) => f.name && f.hash !== state.cwd?.hash)));
            if (e.target.checked) {
                all.forEach((f) => state.selected.add(f.hash));
            } else {
                state.selected.clear();
            }
            render();
        });
    }

    const positionMenu = (menu, x, y) => {
        const pad = 8;
        const { innerWidth, innerHeight } = window;
        const rect = menu.getBoundingClientRect();
        let left = x;
        let top = y - 12;
        if (left + rect.width + pad > innerWidth) left = innerWidth - rect.width - pad;
        if (top + rect.height + pad > innerHeight) top = y - rect.height - pad;
        if (left < pad) left = pad;
        if (top < pad) top = pad;
        menu.style.left = `${left}px`;
        menu.style.top = `${top}px`;
    };

    const showContextMenu = (x, y, file) => {
        if (state.contextMenu) {
            state.contextMenu.remove();
            state.contextMenu = null;
        }
        const menu = document.createElement("div");
        const removeMenu = () => menu.remove();
        menu.className = "fixed z-50 min-w-[190px] rounded-xl border border-border bg-white p-2 text-xs shadow-lg dark:bg-gray-800 dark:border-gray-700";
        menu.innerHTML = `
            <button class="flex w-full items-center gap-2 rounded-lg px-3 py-2 text-left hover:bg-muted" data-action="open">
                <i data-lucide="folder-open" class="h-4 w-4"></i><?= lang('open') ?>
            </button>
            <button class="flex w-full items-center gap-2 rounded-lg px-3 py-2 text-left hover:bg-muted" data-action="preview">
                <i data-lucide="eye" class="h-4 w-4"></i><?= lang('preview') ?>
            </button>
            <button class="flex w-full items-center gap-2 rounded-lg px-3 py-2 text-left hover:bg-muted" data-action="rename">
                <i data-lucide="edit-3" class="h-4 w-4"></i><?= lang('rename') ?>
            </button>
            <button class="flex w-full items-center gap-2 rounded-lg px-3 py-2 text-left hover:bg-muted" data-action="cut">
                <i data-lucide="scissors" class="h-4 w-4"></i><?= lang('cut') ?>
            </button>
            <button class="flex w-full items-center gap-2 rounded-lg px-3 py-2 text-left hover:bg-muted" data-action="copy">
                <i data-lucide="copy" class="h-4 w-4"></i><?= lang('copy') ?>
            </button>
            <button class="flex w-full items-center gap-2 rounded-lg px-3 py-2 text-left hover:bg-muted" data-action="duplicate">
                <i data-lucide="copy-check" class="h-4 w-4"></i><?= lang('duplicate') ?>
            </button>
            <button class="flex w-full items-center gap-2 rounded-lg px-3 py-2 text-left hover:bg-muted" data-action="download">
                <i data-lucide="download" class="h-4 w-4"></i><?= lang('download') ?>
            </button>
            <button class="flex w-full items-center gap-2 rounded-lg px-3 py-2 text-left hover:bg-muted" data-action="zip">
                <i data-lucide="archive" class="h-4 w-4"></i><?= lang('zip') ?>
            </button>
            <button class="flex w-full items-center gap-2 rounded-lg px-3 py-2 text-left hover:bg-muted" data-action="unzip">
                <i data-lucide="package-open" class="h-4 w-4"></i><?= lang('unzip') ?>
            </button>
            <button class="flex w-full items-center gap-2 rounded-lg px-3 py-2 text-left hover:bg-muted" data-action="copylink">
                <i data-lucide="link" class="h-4 w-4"></i><?= lang('copy') ?> <?= lang('link') ?>
            </button>
            <button class="flex w-full items-center gap-2 rounded-lg px-3 py-2 text-left text-rose-600 hover:bg-rose-50" data-action="delete">
                <i data-lucide="trash-2" class="h-4 w-4"></i><?= lang('delete') ?>
            </button>
        `;
        menu.addEventListener("click", async (e) => {
            const action = e.target.closest("button")?.dataset.action;
            removeMenu();
            const url = file.url || `${connector}?cmd=file&target=${encodeURIComponent(file.hash)}`;
            if (action === "open") {
                if (file.mime === "directory" || file.mime === "application/x-directory") openDir(file.hash);
                else window.open(url, "_blank");
            } else if (action === "preview") {
                openPreviewFor(file);
            } else if (action === "rename") {
                openModal("<?= lang('rename') ?>", "<?= lang('name') ?>", "rename", file.hash, file.name || "");
            } else if (action === "cut") {
                await request({ cmd: "cut", targets: [file.hash] });
                state.clipboard = { mode: "cut", targets: [file.hash] };
                render();
            } else if (action === "copy") {
                await request({ cmd: "copy", targets: [file.hash] });
                state.clipboard = { mode: "copy", targets: [file.hash] };
            } else if (action === "duplicate") {
                await request({ cmd: "duplicate", targets: [file.hash] });
                const data = await request({ cmd: "open", target: state.cwd?.hash });
                if (data && data.cwd) {
                    state.cwd = data.cwd;
                    state.files = data.files || [];
                    render();
                }
            } else if (action === "download") {
                window.open(url, "_blank");
            } else if (action === "zip") {
                const name = (file.name || "archive") + ".zip";
                request({ cmd: "archive", targets: [file.hash], type: "zip", name }).then(loadRoot);
            } else if (action === "unzip") {
                request({ cmd: "extract", target: file.hash }).then(loadRoot);
            } else if (action === "copylink") {
                navigator.clipboard?.writeText(url);
            } else if (action === "delete") {
                openConfirm(file.hash);
            }
        });
        document.body.appendChild(menu);
        positionMenu(menu, x, y);
        state.contextMenu = menu;
        document.addEventListener("click", () => {
            removeMenu();
            state.contextMenu = null;
        }, { once: true });
        if (window.lucide) window.lucide.createIcons();
    };

    const clearSelection = () => {
        state.selected.clear();
        render();
    };

    const showBackgroundMenu = (x, y) => {
        if (state.contextMenu) {
            state.contextMenu.remove();
            state.contextMenu = null;
        }
        const menu = document.createElement("div");
        const removeMenu = () => menu.remove();
        menu.className = "fixed z-50 min-w-[220px] rounded-xl border border-border bg-white p-0 text-xs shadow-lg dark:bg-gray-800 dark:border-gray-700";
        const viewLabel = state.view === "grid" ? "<?= lang('list') ?>" : "<?= lang('grid') ?>";
        menu.innerHTML = `<div class="p-2">
            <button class="flex w-full items-center gap-2 rounded-lg px-3 py-2 text-left hover:bg-muted" data-action="new_folder">
                <i data-lucide="folder-plus" class="h-4 w-4"></i><?= lang('new_folder') ?>
            </button>
            <button class="flex w-full items-center gap-2 rounded-lg px-3 py-2 text-left hover:bg-muted" data-action="new_file">
                <i data-lucide="file-plus" class="h-4 w-4"></i><?= lang('new_file') ?>
            </button>
            <button class="flex w-full items-center gap-2 rounded-lg px-3 py-2 text-left hover:bg-muted" data-action="paste">
                <i data-lucide="clipboard-paste" class="h-4 w-4"></i><?= lang('paste') ?>
            </button>
            <button class="flex w-full items-center gap-2 rounded-lg px-3 py-2 text-left hover:bg-muted" data-action="upload">
                <i data-lucide="upload" class="h-4 w-4"></i><?= lang('upload') ?>
            </button>
            <button class="flex w-full items-center gap-2 rounded-lg px-3 py-2 text-left hover:bg-muted" data-action="reload">
                <i data-lucide="refresh-cw" class="h-4 w-4"></i><?= lang('reload') ?>
            </button>
            <div class="my-1 border-t border-border"></div>
            <div class="relative" data-submenu>
                <button class="flex w-full items-center justify-between gap-2 rounded-lg px-3 py-2 text-left hover:bg-muted" data-action="sort_menu">
                    <span class="flex items-center gap-2"><i data-lucide="arrow-up-down" class="h-4 w-4"></i><?= lang('sort') ?></span>
                    <i data-lucide="chevron-right" class="h-3.5 w-3.5"></i>
                </button>
                <div class="absolute left-full top-0 hidden w-40 rounded-xl border border-border bg-white dark:bg-gray-800 dark:border-gray-700 p-2 text-xs shadow-lg" data-submenu-panel>
                    <button class="flex w-full items-center gap-2 rounded-lg px-3 py-2 text-left hover:bg-muted" data-action="sort_name"><?= lang('name') ?></button>
                    <button class="flex w-full items-center gap-2 rounded-lg px-3 py-2 text-left hover:bg-muted" data-action="sort_size"><?= lang('size') ?></button>
                    <button class="flex w-full items-center gap-2 rounded-lg px-3 py-2 text-left hover:bg-muted" data-action="sort_type"><?= lang('type') ?></button>
                    <button class="flex w-full items-center gap-2 rounded-lg px-3 py-2 text-left hover:bg-muted" data-action="sort_date"><?= lang('date') ?></button>
                    <button class="flex w-full items-center gap-2 rounded-lg px-3 py-2 text-left hover:bg-muted" data-action="sort_folder"><?= lang('folder') ?> <?= lang('first') ?></button>
                </div>
            </div>
            <div class="my-1 border-t border-border"></div>
            <button class="flex w-full items-center gap-2 rounded-lg px-3 py-2 text-left hover:bg-muted" data-action="select_all">
                <i data-lucide="check-square" class="h-4 w-4"></i><?= lang('select_all') ?>
            </button>
            <button class="flex w-full items-center gap-2 rounded-lg px-3 py-2 text-left hover:bg-muted" data-action="empty">
                <i data-lucide="trash-2" class="h-4 w-4"></i><?= lang('empty') ?> <?= lang('folder') ?>
            </button>
            <button class="flex w-full items-center gap-2 rounded-lg px-3 py-2 text-left hover:bg-muted" data-action="toggle_view">
                <i data-lucide="layout-grid" class="h-4 w-4"></i>${viewLabel}
            </button>
         </div>`;
        menu.addEventListener("mouseenter", (e) => {
            const sub = menu.querySelector("[data-submenu]");
            const panel = menu.querySelector("[data-submenu-panel]");
            if (sub && panel) {
                sub.addEventListener("mouseenter", () => panel.classList.remove("hidden"));
                sub.addEventListener("mouseleave", () => panel.classList.add("hidden"));
            }
        }, { once: true });
        menu.addEventListener("click", async (e) => {
            const action = e.target.closest("button")?.dataset.action;
            removeMenu();
            if (action === "new_folder") {
                openModal("<?= lang('new_folder') ?>", "<?= lang('name') ?>", "mkdir");
            } else if (action === "new_file") {
                openModal("<?= lang('new_file') ?>", "<?= lang('name') ?>", "mkfile");
            } else if (action === "paste") {
                if (state.clipboard?.targets?.length) {
                    const targetNames = await getClipboardNames(state.clipboard.targets);
                    const existing = new Set((state.files || []).map((f) => f.name));
                    const conflicts = targetNames.filter((n) => existing.has(n));
                    if (conflicts.length) {
                        showPasteModal(conflicts, state.clipboard.targets);
                    } else {
                        await performPaste();
                    }
                }
            } else if (action === "upload") {
                uploadInput?.click();
            } else if (action === "reload") {
                loadRoot();
            } else if (action === "sort_name") {
                state.sort = "name";
            } else if (action === "sort_size") {
                state.sort = "size";
            } else if (action === "sort_type") {
                state.sort = "type";
            } else if (action === "sort_date") {
                state.sort = "date";
            } else if (action === "sort_folder") {
                state.sort = "folderFirst";
            } else if (action === "select_all") {
                const all = applySort(applyFilters((state.files || []).filter((f) => f.name && f.hash !== state.cwd?.hash)));
                all.forEach((f) => state.selected.add(f.hash));
            } else if (action === "empty") {
                if (state.cwd?.hash) {
                    await request({ cmd: "rm", targets: (state.files || []).filter((f) => f.hash !== state.cwd.hash).map((f) => f.hash) });
                }
            } else if (action === "toggle_view") {
                state.view = state.view === "grid" ? "list" : "grid";
            }
            localStorage.setItem("fm_view", state.view);
            localStorage.setItem("fm_sort", state.sort);
            render();
        });
        document.body.appendChild(menu);
        positionMenu(menu, x, y);
        state.contextMenu = menu;
        document.addEventListener("click", () => {
            removeMenu();
            state.contextMenu = null;
        }, { once: true });
        if (window.lucide) window.lucide.createIcons();
    };

    grid.addEventListener("click", (e) => {
        if (e.target === grid || e.target === gridWrap) {
            clearSelection();
        }
    });
    gridWrap.addEventListener("contextmenu", (e) => {
        if (!e.target.closest("#fm_grid > div")) {
            e.preventDefault();
            showBackgroundMenu(e.pageX, e.pageY);
        }
    });
    list.addEventListener("contextmenu", (e) => {
        if (!e.target.closest(".fm-list-card")) {
            e.preventDefault();
            showBackgroundMenu(e.pageX, e.pageY);
        }
    });
    list.addEventListener("click", (e) => {
        if (e.target === list || e.target === listBody) {
            clearSelection();
        }
    });

    const backBtn = app.querySelector("[data-fm-back]");
    if (backBtn) {
        backBtn.addEventListener("click", () => {
            closePreview();
            goBack();
        });
    }

    const dragState = { counter: 0 };
    app.addEventListener("dragenter", (e) => {
        e.preventDefault();
        dragState.counter += 1;
        dropZone.classList.remove("hidden");
        dropZone.classList.add("flex");
    });
    app.addEventListener("dragleave", (e) => {
        e.preventDefault();
        dragState.counter -= 1;
        if (dragState.counter <= 0) {
            dropZone.classList.add("hidden");
            dropZone.classList.remove("flex");
        }
    });
    app.addEventListener("dragover", (e) => {
        e.preventDefault();
    });
    app.addEventListener("drop", async (e) => {
        e.preventDefault();
        dragState.counter = 0;
        dropZone.classList.add("hidden");
        dropZone.classList.remove("flex");
        if (e.dataTransfer?.files?.length) {
            await handleUpload(e.dataTransfer.files);
        }
    });

    if (dupCancel) {
        dupCancel.addEventListener("click", () => {
            pendingUpload = null;
            closeDupModal();
        });
    }
    if (dupKeep) {
        dupKeep.addEventListener("click", async () => {
            if (!pendingUpload) return;
            await uploadFiles(pendingUpload, { rename: 1, overwrite: 0 });
            pendingUpload = null;
            closeDupModal();
        });
    }
    if (dupReplace) {
        dupReplace.addEventListener("click", async () => {
            if (!pendingUpload) return;
            await uploadFiles(pendingUpload, { overwrite: 1 });
            pendingUpload = null;
            closeDupModal();
        });
    }
    if (pasteCancel) {
        pasteCancel.addEventListener("click", () => {
            closePasteModal();
        });
    }
    if (pasteKeep) {
        pasteKeep.addEventListener("click", async () => {
            if (!pendingPasteTargets?.length) return closePasteModal();
            await performPaste({ renames: 1, rename: 1, overwrite: 0 });
            closePasteModal();
        });
    }
    if (pasteReplace) {
        pasteReplace.addEventListener("click", async () => {
            if (!pendingPasteTargets?.length) return closePasteModal();
            await performPaste({ overwrite: 1 }, pendingPasteConflicts);
            closePasteModal();
        });
    }

    const dragSelect = { active: false, startX: 0, startY: 0 };
    grid.addEventListener("mousedown", (e) => {
        if (e.button !== 0 || e.target.closest(".fm-check") || e.target.closest("button") || e.target.closest("#fm_grid > div")) return;
        dragSelect.active = true;
        dragSelect.startX = e.pageX;
        dragSelect.startY = e.pageY;
        selectBox.classList.remove("hidden");
        selectBox.style.left = `${dragSelect.startX}px`;
        selectBox.style.top = `${dragSelect.startY}px`;
        selectBox.style.width = "0px";
        selectBox.style.height = "0px";
    });
    document.addEventListener("mousemove", (e) => {
        if (!dragSelect.active) return;
        const x = Math.min(e.pageX, dragSelect.startX);
        const y = Math.min(e.pageY, dragSelect.startY);
        const w = Math.abs(e.pageX - dragSelect.startX);
        const h = Math.abs(e.pageY - dragSelect.startY);
        selectBox.style.left = `${x}px`;
        selectBox.style.top = `${y}px`;
        selectBox.style.width = `${w}px`;
        selectBox.style.height = `${h}px`;
        document.querySelectorAll("#fm_grid > div").forEach((card) => {
            const rect = card.getBoundingClientRect();
            const boxRect = selectBox.getBoundingClientRect();
            const intersect = rect.left < boxRect.right && rect.right > boxRect.left && rect.top < boxRect.bottom && rect.bottom > boxRect.top;
            if (intersect) {
                state.selected.add(card.dataset.hash);
            }
        });
        renderBulk();
        renderPreview();
    });
    document.addEventListener("mouseup", () => {
        if (!dragSelect.active) return;
        dragSelect.active = false;
        selectBox.classList.add("hidden");
        render();
    });

    if (copyLinkBtn) {
        copyLinkBtn.addEventListener("click", () => {
            const last = Array.from(state.selected).pop();
            if (!last) return;
            const file = state.files.find((f) => f.hash === last);
            if (!file) return;
            const url = file.url || `${connector}?cmd=file&target=${encodeURIComponent(file.hash)}`;
            navigator.clipboard?.writeText(url);
        });
    }
    if (deleteSingleBtn) {
        deleteSingleBtn.addEventListener("click", () => {
            const last = Array.from(state.selected).pop();
            if (last) openConfirm(last);
        });
    }
    document.querySelectorAll("[data-fm-preview-close]").forEach((btn) => {
        btn.addEventListener("click", closePreview);
    });
    if (previewBackdrop) {
        previewBackdrop.addEventListener("click", closePreview);
    }

    document.addEventListener("keydown", (e) => {
        if (e.key === "Delete") {
            if (state.selected.size) {
                request({ cmd: "rm", targets: Array.from(state.selected) }).then(loadRoot);
            }
        }
        if (e.key === "F2") {
            const last = Array.from(state.selected).pop();
            const file = state.files.find((f) => f.hash === last);
            if (file) openModal("<?= lang('rename') ?>", "<?= lang('name') ?>", "rename", file.hash, file.name || "");
        }
        if (e.key === "Enter") {
            const last = Array.from(state.selected).pop();
            const file = state.files.find((f) => f.hash === last);
            if (file) {
                if (file.mime === "directory" || file.mime === "application/x-directory") openDir(file.hash);
                else window.open(file.url || `${connector}?cmd=file&target=${encodeURIComponent(file.hash)}`, "_blank");
            }
        }
        if (e.key === "Escape") {
            state.selected.clear();
            closePreview();
            if (state.contextMenu) {
                state.contextMenu.remove();
                state.contextMenu = null;
            }
            render();
        }
    });

    loadRoot();
})();
</script>
