OpenNIT

Quelltext des aktuellen Standes

Fassung 1.1.1

index.php

101 KB
<?php
$this->layout('layouts/main', [
    'page_title'    => $page_title,
    'current_route' => $current_route,
    'full_width'    => true,
]);
?>
<style>
a, a:hover { text-decoration: none; }

/* ── Overall layout ── */
.fe-page { display:flex; flex-direction:column; height:calc(100vh - 57px); }
.fe-wrap { flex:1; min-height:0; overflow:hidden; display:flex; flex-direction:column; }
.fe-body { flex:1; display:flex; overflow:hidden; position:relative; }

/* ── Sidebar ── */
.fe-sidebar {
    width:230px; flex-shrink:0;
    background:var(--slate-50); border-right:1px solid var(--slate-200);
    overflow-y:auto; display:flex; flex-direction:column;
}
.fe-sidebar-sec {
    padding:0.65rem 0.875rem 0.2rem;
    font-size:0.67rem; font-weight:700; text-transform:uppercase;
    letter-spacing:0.07em; color:var(--slate-400);
}
.fe-tree-root {
    display:flex; align-items:center; gap:0.45rem;
    padding:0.42rem 0.875rem; font-size:0.83rem; color:var(--gray-700);
    text-decoration:none; border-left:3px solid transparent;
    transition:background 0.1s;
}
.fe-tree-root:hover { background:var(--color-gray-200); }
.fe-tree-root.active { background:var(--blue-100); color:var(--blue-700); font-weight:600; border-left-color:var(--blue-500); }

.fe-tree-node {}
.fe-tree-item {
    display:flex; align-items:center;
    padding:0.38rem 0.875rem 0.38rem 0;
    font-size:0.82rem; color:var(--gray-700);
    border-left:3px solid transparent;
    text-decoration:none; transition:background 0.1s;
    white-space:nowrap; overflow:hidden;
}
.fe-tree-item:hover { background:var(--color-gray-200); }
.fe-tree-item.active { background:var(--blue-100); color:var(--blue-700); font-weight:600; border-left-color:var(--blue-500); }
.fe-tree-item span.label { overflow:hidden; text-overflow:ellipsis; flex:1; min-width:0; }
.fe-tree-toggle {
    flex-shrink:0; width:18px; height:18px;
    display:flex; align-items:center; justify-content:center;
    color:var(--slate-400); cursor:pointer; background:none; border:none; padding:0;
    transition:transform 0.15s;
}
.fe-tree-toggle.open { transform:rotate(90deg); }
.fe-tree-children { display:none; }
.fe-tree-children.open { display:block; }
.fe-sidebar-divider { height:1px; background:var(--slate-200); margin:0.4rem 0; }
.fe-sidebar-foot { margin-top:auto; padding:0.6rem 0.875rem; border-top:1px solid var(--slate-200); }
.fe-storage-bar-wrap { height:6px; background:var(--slate-200); border-radius:99px; overflow:hidden; margin-bottom:0.3rem; }
.fe-storage-bar { height:6px; background:var(--blue-500); border-radius:99px; }
.fe-storage-text { font-size:0.7rem; color:var(--slate-400); }

/* ── Main ── */
.fe-main { flex:1; display:flex; flex-direction:column; overflow:hidden; background:var(--color-bg-alt); }

/* ── Toolbar ── */
.fe-toolbar {
    display:flex; align-items:center; gap:0.6rem; flex-wrap:wrap;
    padding:0.65rem 1.25rem; background:var(--color-bg);
    border-bottom:1px solid var(--slate-200); flex-shrink:0;
}
.fe-toolbar-breadcrumb { flex:1; min-width:0; display:flex; align-items:center; gap:0.3rem;
    flex-wrap:wrap; font-size:0.82rem; }
.fe-toolbar-breadcrumb a { color:var(--color-primary); }
.fe-toolbar-breadcrumb a:hover { text-decoration:underline; }
.fe-toolbar-breadcrumb .sep { color:var(--slate-300); margin:0 0.1rem; }
.fe-toolbar-breadcrumb .cur { color:var(--slate-800); font-weight:600; }
.fe-burger {
    display:none; background:none; border:1px solid var(--slate-200); border-radius:5px;
    padding:0.3rem 0.45rem; cursor:pointer; color:var(--slate-600);
}

/* ── Search ── */
.fe-search { position:relative; }
.fe-search input {
    width:230px; max-width:48vw; padding:0.4rem 0.7rem 0.4rem 1.9rem;
    border:1px solid var(--gray-300); border-radius:5px; font-size:0.82rem; outline:none;
    transition:border-color .15s, box-shadow .15s;
}
.fe-search input:focus { border-color:var(--blue-500); box-shadow:0 0 0 3px rgba(59,130,246,.1); }
.fe-search svg { position:absolute; left:0.55rem; top:50%; transform:translateY(-50%); color:var(--slate-400); pointer-events:none; }
.fe-search-results {
    position:absolute; top:calc(100% + 4px); right:0; width:340px; max-width:80vw;
    max-height:60vh; overflow-y:auto; background:var(--color-bg); border:1px solid var(--slate-200);
    border-radius:6px; box-shadow:0 12px 36px rgba(0,0,0,.14); z-index:1056; display:none;
}
.fe-search-results.open { display:block; }
.fe-sr-group { font-size:0.66rem; font-weight:700; text-transform:uppercase; letter-spacing:.05em;
    color:var(--slate-400); padding:0.5rem 0.85rem 0.25rem; }
.fe-sr-item { display:flex; align-items:center; gap:0.5rem; padding:0.45rem 0.85rem; color:var(--gray-700); }
.fe-sr-item:hover { background:var(--slate-100); }
.fe-sr-name { flex:1; min-width:0; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; font-size:0.82rem; }
.fe-sr-path { font-size:0.7rem; color:var(--slate-400); overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
.fe-sr-empty { padding:0.85rem; font-size:0.8rem; color:var(--slate-400); text-align:center; }

/* ── Scroll area ── */
.fe-scroll { flex:1; overflow-y:auto; padding:1.25rem; }

/* ── Drop zone ── */
.fe-dropzone {
    border:2px dashed var(--slate-300); border-radius:6px;
    padding:2rem 1rem; text-align:center; color:var(--slate-400);
    background:var(--color-bg); cursor:pointer; margin-bottom:1rem;
    transition:border-color 0.15s, background 0.15s;
}
.fe-dropzone:hover, .fe-dropzone.dragover {
    border-color:var(--blue-500); background:var(--blue-50); color:var(--blue-700);
}
.fe-dropzone-icon { font-size:1.75rem; margin-bottom:0.35rem; }
.fe-dropzone-text { font-size:0.85rem; font-weight:500; }
.fe-dropzone-hint { font-size:0.75rem; margin-top:0.2rem; opacity:.7; }

/* ── Upload list ── */
.fe-upload-list { margin-bottom:0.75rem; }
.fe-upload-item {
    background:var(--color-bg); border:1px solid var(--slate-200); border-radius:4px;
    padding:0.45rem 0.75rem; margin-bottom:0.35rem;
    display:flex; align-items:center; gap:0.5rem; font-size:0.81rem;
}
.fe-upload-name { flex:1; min-width:0; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; color:var(--gray-700); }
.fe-upload-bar-wrap { width:100px; flex-shrink:0; background:var(--slate-100); border-radius:99px; height:5px; }
.fe-upload-bar { height:5px; border-radius:99px; background:var(--blue-500); width:0; transition:width 0.15s; }
.fe-upload-status { font-size:0.74rem; white-space:nowrap; }
.fe-upload-status.ok  { color:var(--green-600); }
.fe-upload-status.err { color:var(--red-600); }

/* ── Bulk action bar ── */
.fe-bulkbar {
    display:none; align-items:center; gap:0.75rem;
    background:var(--slate-800); color:var(--color-on-accent); border-radius:6px;
    padding:0.5rem 0.9rem; margin-bottom:1rem; font-size:0.83rem;
}
.fe-bulkbar.open { display:flex; }
.fe-bulkbar-count { font-weight:600; }
.fe-bulkbar-spacer { flex:1; }
.fe-bulkbar button { font-size:0.8rem; }
.fe-check { width:16px; height:16px; cursor:pointer; accent-color:var(--blue-500); flex-shrink:0; }

/* ── Section headers ── */
.fe-sec {
    font-size:0.68rem; font-weight:700; text-transform:uppercase;
    letter-spacing:0.06em; color:var(--slate-400); margin:0 0 0.6rem;
}

/* ── Folder cards ── */
.fe-folder-grid {
    display:grid; grid-template-columns:repeat(auto-fill, minmax(190px,1fr));
    gap:0.65rem; margin-bottom:1.25rem;
}
.fe-folder-card {
    background:var(--color-bg); border:1px solid var(--slate-200); border-radius:6px;
    display:flex; align-items:flex-start; gap:0.65rem;
    padding:0.75rem 0.875rem; cursor:pointer; position:relative;
    transition:box-shadow 0.12s, border-color 0.12s;
    text-decoration:none; color:inherit;
}
.fe-folder-card:hover { box-shadow:0 2px 10px rgba(0,0,0,.08); border-color:var(--blue-200); }
.fe-folder-card.selected { border-color:var(--blue-500); background:var(--blue-50); }
.fe-folder-card-check { position:absolute; top:6px; left:6px; opacity:0; transition:opacity .1s; z-index:2; }
.fe-folder-card:hover .fe-folder-card-check, .fe-folder-card.selected .fe-folder-card-check { opacity:1; }
.fe-folder-card-icon { font-size:1.5rem; line-height:1; flex-shrink:0; }
.fe-folder-card-body { min-width:0; flex:1; }
.fe-folder-card-name { font-size:0.84rem; font-weight:600; color:var(--slate-800);
    word-break:break-word; }
.fe-folder-card-meta { font-size:0.71rem; color:var(--slate-400); margin-top:0.1rem; }
/* Ordner-Aktionen: ein Kebab-Icon klappt die Optionen aus, damit beim Öffnen
   des Ordners nicht versehentlich eine Aktion getroffen wird. */
.fe-folder-card.menu-open { z-index:30; }
.fe-folder-menu-wrap { position:absolute; top:4px; right:4px; z-index:3; }
.fe-folder-menu-btn {
    display:none; background:rgba(255,255,255,.95); border:1px solid var(--slate-200);
    border-radius:4px; width:1.5rem; height:1.5rem; cursor:pointer; color:var(--gray-500);
    line-height:1; font-size:1rem; align-items:center; justify-content:center; padding:0;
}
.fe-folder-card:hover .fe-folder-menu-btn,
.fe-folder-menu-wrap.open .fe-folder-menu-btn { display:flex; }
.fe-folder-menu-btn:hover { background:var(--color-bg); color:var(--gray-700); }
.fe-folder-menu {
    position:absolute; top:100%; right:0; margin-top:3px; min-width:180px;
    background:var(--color-bg); border:1px solid var(--slate-200); border-radius:6px;
    box-shadow:0 6px 20px rgba(0,0,0,.14); padding:0.25rem; display:none; z-index:40;
}
.fe-folder-menu-wrap.open .fe-folder-menu { display:block; }
.fe-folder-menu button, .fe-folder-menu a {
    display:flex; align-items:center; gap:0.55rem; width:100%;
    background:none; border:none; text-align:left; cursor:pointer;
    padding:0.4rem 0.55rem; border-radius:4px; font-size:0.8rem; color:var(--gray-700);
    text-decoration:none; line-height:1.2;
}
.fe-folder-menu button:hover, .fe-folder-menu a:hover { background:var(--slate-100); }
.fe-folder-menu button.danger { color:var(--red-600); }
.fe-folder-menu button.danger:hover { background:var(--red-50); }
.fe-card-btn {
    background:rgba(255,255,255,.95); border:1px solid var(--slate-200);
    border-radius:4px; padding:0.2rem 0.35rem; cursor:pointer;
    color:var(--gray-500); line-height:1; display:flex; align-items:center;
    font-size:0.72rem; transition:color 0.1s;
}
.fe-card-btn:hover { background:var(--color-bg); color:var(--gray-700); }
.fe-card-btn.danger:hover { color:var(--red-600); border-color:var(--red-300); }

/* ── File table ── */
.fe-file-box {
    background:var(--color-bg); border:1px solid var(--slate-200); border-radius:6px; overflow:hidden;
}
.fe-file-box table { width:100%; border-collapse:collapse; }
.fe-file-box th {
    padding:0.5rem 1rem; text-align:left; font-size:0.69rem; font-weight:700;
    text-transform:uppercase; letter-spacing:0.05em; color:var(--slate-400);
    background:var(--slate-50); border-bottom:1px solid var(--slate-100); white-space:nowrap;
}
.fe-file-box th.sortable { cursor:pointer; user-select:none; }
.fe-file-box th.sortable:hover { color:var(--slate-600); }
.fe-sort-ind { opacity:.5; font-size:0.6rem; }
.fe-file-box td {
    padding:0.6rem 1rem; border-bottom:1px solid var(--slate-50);
    font-size:0.84rem; vertical-align:middle;
}
.fe-file-box tbody tr:last-child td { border-bottom:none; }
.fe-file-box tbody tr:hover td { background:var(--slate-50); }
.fe-file-box tbody tr.selected td { background:var(--blue-50); }
.fe-col-check { width:34px; text-align:center; }
.fe-file-name-cell { display:flex; align-items:center; gap:0.5rem; font-weight:500; color:var(--slate-800); }
.fe-file-name-text { overflow:hidden; text-overflow:ellipsis; white-space:nowrap; max-width:260px; }
.fe-ext-badge {
    font-size:0.68rem; font-weight:700; padding:0.1rem 0.35rem;
    border-radius:3px; background:var(--slate-100); color:var(--slate-500); white-space:nowrap; flex-shrink:0;
}
.fe-file-size { color:var(--slate-500); white-space:nowrap; }
.fe-file-date { color:var(--slate-400); font-size:0.79rem; white-space:nowrap; }
.fe-file-by   { color:var(--slate-500); font-size:0.79rem; }
.fe-file-actions { display:flex; gap:0.35rem; justify-content:flex-end; }

/* ── Thumbnails ── */
.fe-thumb { width:26px; height:26px; border-radius:4px; object-fit:cover; flex-shrink:0; background:var(--slate-100); }

/* ── View toggle ── */
.fe-viewtoggle { display:inline-flex; border:1px solid var(--gray-300); border-radius:5px; overflow:hidden; }
.fe-viewtoggle button {
    background:var(--color-bg); border:none; padding:0.35rem 0.5rem; cursor:pointer; color:var(--slate-500);
    display:flex; align-items:center; line-height:1;
}
.fe-viewtoggle button + button { border-left:1px solid var(--slate-200); }
.fe-viewtoggle button.active { background:var(--blue-500); color:var(--color-on-accent); }

/* ── List vs. grid switching ── */
.fe-view-list .fe-files-grid { display:none; }
.fe-view-grid .fe-files-list { display:none; }

/* ── File grid cards ── */
.fe-files-grid { display:grid; grid-template-columns:repeat(auto-fill, minmax(150px,1fr)); gap:0.65rem; }
.fe-file-card {
    background:var(--color-bg); border:1px solid var(--slate-200); border-radius:6px; position:relative;
    display:flex; flex-direction:column; overflow:hidden; transition:box-shadow .12s, border-color .12s;
}
.fe-file-card:hover { box-shadow:0 2px 10px rgba(0,0,0,.08); border-color:var(--blue-200); }
.fe-file-card.selected { border-color:var(--blue-500); background:var(--blue-50); }
.fe-file-card-preview {
    height:104px; display:flex; align-items:center; justify-content:center;
    background:var(--slate-50); border-bottom:1px solid var(--slate-100); cursor:pointer; overflow:hidden;
}
.fe-file-card-preview img { width:100%; height:100%; object-fit:cover; }
.fe-file-card-body { padding:0.5rem 0.6rem; min-width:0; }
.fe-file-card-name { font-size:0.8rem; font-weight:500; color:var(--slate-800); overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
.fe-file-card-meta { font-size:0.7rem; color:var(--slate-400); margin-top:0.1rem; }
.fe-file-card-check { position:absolute; top:6px; left:6px; opacity:0; z-index:2; }
.fe-file-card:hover .fe-file-card-check, .fe-file-card.selected .fe-file-card-check { opacity:1; }
.fe-file-card-actions { position:absolute; top:4px; right:4px; display:none; gap:2px; z-index:2; }
.fe-file-card:hover .fe-file-card-actions { display:flex; }

/* ── Drag & Drop verschieben ── */
[draggable="true"] { cursor:grab; }
.fe-drag-active { opacity:0.5; }
.fe-drop-target { outline:2px dashed var(--blue-500); outline-offset:-2px; background:var(--blue-50) !important; }
.fe-tree-item.fe-drop-target, .fe-tree-root.fe-drop-target { background:var(--blue-100) !important; }

/* ── File type icon colors ── */
.fi { display:inline-flex; align-items:center; justify-content:center; width:22px; height:22px; flex-shrink:0; }
.fi svg { width:18px; height:18px; }
.fi-folder     { color:var(--amber-500); }
.fi-file       { color:var(--slate-400); }
.fi-image      { color:var(--violet-500); }
.fi-pdf        { color:var(--red-500); }
.fi-text       { color:var(--blue-500); }
.fi-sheet      { color:var(--green-600); }
.fi-code       { color:var(--cyan-500); }
.fi-archive    { color:var(--orange-500); }
.fi-exe        { color:var(--red-600); }
.fi-video      { color:var(--violet-600); }
.fi-audio      { color:var(--pink-600); }

/* ── Empty state ── */
.fe-empty-text { font-size:0.875rem; }
.fe-empty-hint { font-size:0.78rem; margin-top:0.25rem; }

/* ── Modals: Basis kommt aus app.css (.modal-overlay/.modal-card, .form-group/.form-control),
   hier nur Zustands-Toggle und Explorer-spezifische Maße ── */
.fe-modal { display:none; z-index:1055; }
.fe-modal.open { display:flex; }
.fe-modal .modal-card { max-width:400px; }
.fe-modal .form-group:last-child { margin-bottom:0; }
.fe-modal .fe-label { display:block; font-size:0.8rem; font-weight:600; color:var(--gray-700); margin-bottom:0.35rem; }

/* ── Vorschau-Modal (Bilder & PDFs inline, wie im Ticket-Modul) ── */
.fe-preview-modal { position:fixed; inset:0; z-index:1080; display:flex; align-items:center; justify-content:center; padding:1.25rem 1.5rem; }
.fe-preview-modal[hidden] { display:none; }
.fe-preview-backdrop { position:absolute; inset:0; background:rgba(0,0,0,0.82); cursor:zoom-out; }
.fe-preview-figure { position:relative; z-index:1; margin:0; display:flex; flex-direction:column; align-items:center; gap:0.6rem; width:100%; max-width:100%; max-height:100%; }
.fe-preview-img { max-width:96vw; max-height:calc(100vh - 5rem); border-radius:0.4rem; box-shadow:0 12px 40px rgba(0,0,0,0.5); background:var(--color-bg); }
.fe-preview-img[hidden], .fe-preview-frame[hidden] { display:none; }
.fe-preview-frame { width:min(1600px, 96vw); height:calc(100vh - 5rem); border:none; border-radius:0.4rem; box-shadow:0 12px 40px rgba(0,0,0,0.5); background:var(--color-bg); }
.fe-preview-caption { display:flex; align-items:center; gap:1rem; max-width:100%; color:var(--gray-200); font-size:0.85rem; }
.fe-preview-name { overflow:hidden; text-overflow:ellipsis; white-space:nowrap; font-weight:500; }
.fe-preview-link { color:var(--blue-300); text-decoration:none; white-space:nowrap; flex-shrink:0; }
.fe-preview-link:hover { color:var(--blue-200); text-decoration:underline; }
.fe-preview-btn { position:absolute; z-index:2; background:rgba(255,255,255,0.12); border:none; color:var(--color-on-accent); cursor:pointer; border-radius:999px; display:flex; align-items:center; justify-content:center; transition:background-color 0.15s; }
.fe-preview-btn:hover { background:rgba(255,255,255,0.25); }
.fe-preview-close { top:1.1rem; right:1.1rem; width:2.4rem; height:2.4rem; font-size:1.6rem; line-height:1; }
.fe-preview-nav { top:50%; transform:translateY(-50%); width:2.8rem; height:2.8rem; font-size:1.3rem; }
.fe-preview-prev { left:1rem; }
.fe-preview-next { right:1rem; }
@media(max-width:768px) {
    .fe-preview-modal { padding:1rem; }
    .fe-preview-nav { width:2.2rem; height:2.2rem; }
    .fe-preview-prev { left:0.25rem; }
    .fe-preview-next { right:0.25rem; }
}

/* ── Mobile ── */
.fe-sidebar-backdrop { display:none; }
@media(max-width:768px) {
    .fe-burger { display:inline-flex; }
    .fe-sidebar {
        position:absolute; top:0; left:0; bottom:0; z-index:1057;
        transform:translateX(-100%); transition:transform .2s ease;
        box-shadow:0 0 30px rgba(0,0,0,.2);
    }
    .fe-sidebar.open { transform:translateX(0); }
    .fe-sidebar-backdrop.open {
        display:block; position:absolute; inset:0; background:rgba(0,0,0,.35); z-index:1056;
    }
    .fe-folder-grid { grid-template-columns:repeat(auto-fill, minmax(140px,1fr)); }
    .fe-hide-sm { display:none !important; }
    .fe-search input { width:150px; }
}
</style>

<?php
/* ── Icon helpers ────────────────────────────────────────── */
function feIcon(string $type, int $size = 16, string $extraClass = ''): string
{
    $icons = [
        'folder'   => '<path d="M22 19a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h5l2 3h9a2 2 0 0 1 2 2z"/>',
        'home'     => '<path d="M3 9l9-7 9 7v11a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"/><polyline points="9 22 9 12 15 12 15 22"/>',
        'chevron'  => '<polyline points="9 18 15 12 9 6"/>',
        'plus'     => '<line x1="12" y1="5" x2="12" y2="19"/><line x1="5" y1="12" x2="19" y2="12"/>',
        'upload'   => '<polyline points="16 16 12 12 8 16"/><line x1="12" y1="12" x2="12" y2="21"/><path d="M20.39 18.39A5 5 0 0 0 18 9h-1.26A8 8 0 1 0 3 16.3"/>',
        'download' => '<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><polyline points="7 10 12 15 17 10"/><line x1="12" y1="15" x2="12" y2="3"/>',
        'trash'    => '<polyline points="3 6 5 6 21 6"/><path d="M19 6l-1 14H6L5 6"/><path d="M10 11v6"/><path d="M14 11v6"/><path d="M9 6V4h6v2"/>',
        'edit'     => '<path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7"/><path d="M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z"/>',
        'move'     => '<polyline points="5 9 2 12 5 15"/><polyline points="9 5 12 2 15 5"/><polyline points="15 19 12 22 9 19"/><polyline points="19 9 22 12 19 15"/><line x1="2" y1="12" x2="22" y2="12"/><line x1="12" y1="2" x2="12" y2="22"/>',
        'x'        => '<line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/>',
        'file'     => '<path d="M13 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V9z"/><polyline points="13 2 13 9 20 9"/>',
        'image'    => '<rect x="3" y="3" width="18" height="18" rx="2"/><circle cx="8.5" cy="8.5" r="1.5"/><polyline points="21 15 16 10 5 21"/>',
        'pdf'      => '<path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/><polyline points="14 2 14 8 20 8"/><line x1="16" y1="13" x2="8" y2="13"/><line x1="16" y1="17" x2="8" y2="17"/>',
        'code'     => '<path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/><polyline points="14 2 14 8 20 8"/><polyline points="10 13 8 15 10 17"/><polyline points="14 13 16 15 14 17"/>',
        'archive'  => '<path d="M21 8v13H3V8"/><rect x="1" y="3" width="22" height="5"/><line x1="10" y1="12" x2="14" y2="12"/>',
        'video'    => '<polygon points="23 7 16 12 23 17 23 7"/><rect x="1" y="5" width="15" height="14" rx="2"/>',
        'music'    => '<path d="M9 18V5l12-2v13"/><circle cx="6" cy="18" r="3"/><circle cx="18" cy="16" r="3"/>',
        'eye'      => '<path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z"/><circle cx="12" cy="12" r="3"/>',
        'search'   => '<circle cx="11" cy="11" r="8"/><line x1="21" y1="21" x2="16.65" y2="16.65"/>',
        'menu'     => '<line x1="3" y1="12" x2="21" y2="12"/><line x1="3" y1="6" x2="21" y2="6"/><line x1="3" y1="18" x2="21" y2="18"/>',
        'grid'     => '<rect x="3" y="3" width="7" height="7"/><rect x="14" y="3" width="7" height="7"/><rect x="14" y="14" width="7" height="7"/><rect x="3" y="14" width="7" height="7"/>',
        'list'     => '<line x1="8" y1="6" x2="21" y2="6"/><line x1="8" y1="12" x2="21" y2="12"/><line x1="8" y1="18" x2="21" y2="18"/><line x1="3" y1="6" x2="3.01" y2="6"/><line x1="3" y1="12" x2="3.01" y2="12"/><line x1="3" y1="18" x2="3.01" y2="18"/>',
        'zip'      => '<path d="M22 19a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h5l2 3h9a2 2 0 0 1 2 2z"/><line x1="12" y1="11" x2="12" y2="17"/><line x1="9" y1="14" x2="15" y2="14"/>',
        'history'  => '<path d="M3 3v5h5"/><path d="M3.05 13A9 9 0 1 0 6 5.3L3 8"/><polyline points="12 7 12 12 15 14"/>',
        'lock'     => '<rect x="3" y="11" width="18" height="11" rx="2" ry="2"/><path d="M7 11V7a5 5 0 0 1 10 0v4"/>',
        'activity' => '<polyline points="22 12 18 12 15 21 9 3 6 12 2 12"/>',
        'share'    => '<circle cx="18" cy="5" r="3"/><circle cx="6" cy="12" r="3"/><circle cx="18" cy="19" r="3"/><line x1="8.59" y1="13.51" x2="15.42" y2="17.49"/><line x1="15.41" y1="6.51" x2="8.59" y2="10.49"/>',
        'cog'      => '<circle cx="12" cy="12" r="3"/><path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 1 1-2.83 2.83l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-4 0v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 1 1-2.83-2.83l.06-.06a1.65 1.65 0 0 0 .33-1.82 1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1 0-4h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 1 1 2.83-2.83l.06.06a1.65 1.65 0 0 0 1.82.33H9a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 4 0v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 1 1 2.83 2.83l-.06.06a1.65 1.65 0 0 0-.33 1.82V9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 0 4h-.09a1.65 1.65 0 0 0-1.51 1z"/>',
    ];
    $d   = $icons[$type] ?? $icons['file'];
    $cls = match($type) {
        'folder'  => 'fi fi-folder',
        'image'   => 'fi fi-image',
        'pdf'     => 'fi fi-pdf',
        'code'    => 'fi fi-code',
        'archive' => 'fi fi-archive',
        'video'   => 'fi fi-video',
        'music'   => 'fi fi-audio',
        default   => 'fi fi-file',
    };
    return '<span class="' . $cls . ' ' . htmlspecialchars($extraClass) . '"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" width="' . $size . '" height="' . $size . '">' . $d . '</svg></span>';
}

function feFileIcon(string $name): string
{
    $ext  = strtolower(pathinfo($name, PATHINFO_EXTENSION));
    if (in_array($ext, ['jpg','jpeg','png','gif','bmp','svg','webp','ico','tiff'])) return 'image';
    if ($ext === 'pdf')  return 'pdf';
    if (in_array($ext, ['doc','docx','odt','rtf','txt','md','log']))  return 'file';
    if (in_array($ext, ['php','js','ts','py','rb','java','cs','cpp','c','h','sh','bash','zsh','ps1','psm1','cmd','bat','vbs','sql','html','htm','css','xml','json','yaml','yml','toml','ini','conf','cfg','env'])) return 'code';
    if (in_array($ext, ['zip','tar','gz','bz2','xz','rar','7z','tgz'])) return 'archive';
    if (in_array($ext, ['mp4','mkv','avi','mov','wmv','flv','webm'])) return 'video';
    if (in_array($ext, ['mp3','wav','flac','aac','ogg','m4a','wma'])) return 'music';
    return 'file';
}

/** Hat die Datei ein Bild-Thumbnail? */
function feHasThumb(string $name): bool
{
    $ext = strtolower(pathinfo($name, PATHINFO_EXTENSION));
    return in_array($ext, ['jpg','jpeg','png','gif','webp','bmp']);
}

/**
 * Vorschau-Typ: image | pdf | text | video | audio | '' (nicht previewbar).
 * Muss mit FileExplorerController::previewPlan() übereinstimmen. SVG/HTML/JS
 * sind bewusst NICHT previewbar (werden serverseitig als Download erzwungen).
 */
function fePreviewType(string $name): string
{
    $ext = strtolower(pathinfo($name, PATHINFO_EXTENSION));
    if (in_array($ext, ['jpg','jpeg','png','gif','webp'])) return 'image';
    if ($ext === 'pdf') return 'pdf';
    if (in_array($ext, ['txt','md','log','csv','tsv','json','xml','yaml','yml','ini','conf','cfg','sql','py','sh','bash','ps1','ts','css','c','cpp','h','hpp','java','rb','go','rs','toml','env','properties','tex','diff','patch'])) return 'text';
    if (in_array($ext, ['mp4','m4v','webm','ogv'])) return 'video';
    if (in_array($ext, ['mp3','wav','ogg','m4a','aac','flac'])) return 'audio';
    return '';
}

function feRenderTree(array $nodes, ?int $currentId, int $depth = 0): void
{
    foreach ($nodes as $node) {
        $id       = (int)$node['id'];
        $isActive = $currentId === $id;
        $hasKids  = !empty($node['children']);
        $isOpen   = $isActive || feIsAncestor($node, $currentId);
        $pad      = ($depth * 16) + 12;
        ?>
        <div class="fe-tree-node">
            <div class="fe-tree-item <?= $isActive ? 'active' : '' ?>"
                 style="padding-left:<?= $pad ?>px;"
                 data-drop-folder="<?= $id ?>"
                 onclick="location.href='/files/folder/<?= $id ?>'">
                <?php if ($hasKids): ?>
                <button class="fe-tree-toggle <?= $isOpen ? 'open' : '' ?>"
                        onclick="event.stopPropagation();feTreeToggle(this)" type="button">
                    <?= feIcon('chevron', 12) ?>
                </button>
                <?php else: ?>
                <span style="width:18px;flex-shrink:0;display:inline-block;"></span>
                <?php endif; ?>
                <?= feIcon('folder', 14) ?>
                <span class="label" style="padding-left:0.25rem;"><?= htmlspecialchars($node['name']) ?></span>
            </div>
            <?php if ($hasKids): ?>
            <div class="fe-tree-children <?= $isOpen ? 'open' : '' ?>">
                <?php feRenderTree($node['children'], $currentId, $depth + 1); ?>
            </div>
            <?php endif; ?>
        </div>
        <?php
    }
}

function feIsAncestor(array $node, ?int $targetId): bool
{
    if ($targetId === null) return false;
    foreach ($node['children'] ?? [] as $child) {
        if ((int)$child['id'] === $targetId) return true;
        if (feIsAncestor($child, $targetId)) return true;
    }
    return false;
}

/** Flache, eingerückte <option>-Liste für den Ziel-Ordner-Picker (Verschieben). */
function feFolderOptions(array $flat): void
{
    // nach Pfad sortieren für hierarchische Reihenfolge
    usort($flat, fn($a, $b) => strcmp($a['path'], $b['path']));
    foreach ($flat as $f) {
        $depth  = max(0, substr_count(trim((string)$f['path'], '/'), '/'));
        $indent = str_repeat("\u{00A0}\u{00A0}", $depth);
        echo '<option value="' . (int)$f['id'] . '" data-path="' . htmlspecialchars($f['path']) . '">'
           . $indent . htmlspecialchars($f['name']) . '</option>';
    }
}

/** Aktions-Buttons einer Datei – wird in Listen- und Kachelansicht wiederverwendet. */
function feFileActions(array $file, bool $canUpload, bool $canDelete): void
{
    $id   = (int)$file['id'];
    $name = (string)$file['original_name'];
    $type = fePreviewType($name);
    $jn   = htmlspecialchars(json_encode($name));
    $esc  = htmlspecialchars($name);

    if ($type !== '') {
        echo '<button type="button" class="btn btn-default btn-xs" title="Vorschau"'
            . ' data-fe-view="/files/preview/' . $id . '" data-fe-view-name="' . $esc . '"'
            . ' data-fe-view-type="' . $type . '" data-fe-view-download="/files/download/' . $id . '">'
            . feIcon('eye', 13) . '</button>';
    }
    echo '<a href="/files/download/' . $id . '" class="btn btn-default btn-xs" title="Herunterladen">'
        . feIcon('download', 13) . '</a>';
    $vcount = (int)($file['version_count'] ?? 0);
    if ($vcount > 0) {
        echo '<button class="btn btn-default btn-xs" type="button" title="' . $vcount . ' frühere Version(en)"'
            . ' onclick="feOpenVersions(' . $id . ',' . $jn . ')">' . feIcon('history', 13)
            . '<span style="font-size:.68rem;margin-left:2px;">' . $vcount . '</span></button>';
    }
    if ($canUpload) {
        echo '<button class="btn btn-default btn-xs" type="button" title="Teilen (Link)"'
            . ' onclick="feOpenShare(\'file\',' . $id . ',' . $jn . ')">' . feIcon('share', 13) . '</button>';
        echo '<button class="btn btn-default btn-xs" type="button" title="Zugriffe"'
            . ' onclick="feOpenAccess(' . $id . ',' . $jn . ')">' . feIcon('activity', 13) . '</button>';
        echo '<button class="btn btn-default btn-xs" type="button" title="Umbenennen"'
            . ' onclick="feOpenRenameFile(' . $id . ',' . $jn . ')">' . feIcon('edit', 13) . '</button>';
        echo '<button class="btn btn-default btn-xs" type="button" title="Verschieben"'
            . ' onclick="feOpenMoveFile(' . $id . ',' . $jn . ')">' . feIcon('move', 13) . '</button>';
    }
    if ($canDelete) {
        echo '<button class="btn btn-default btn-xs" type="button" title="Löschen" style="color:var(--red-600);"'
            . ' onclick="feOpenDeleteFile(' . $id . ',' . $jn . ')">' . feIcon('trash', 13) . '</button>';
    }
}
?>

<div class="fe-page">

<div class="content-header">
    <div>
        <h1><?= $this->te('Datei-Explorer') ?></h1>
        <ol class="breadcrumb">
            <li class="breadcrumb-item"><a href="/"><?= $this->te('Home') ?></a></li>
            <li class="breadcrumb-item active"><?= $this->te('Datei-Explorer') ?></li>
        </ol>
    </div>
    <div class="content-header-actions">
        <a class="btn btn-default btn-sm" href="/files/trash" title="<?= $this->te('Papierkorb') ?>">
            <?= feIcon('trash', 13) ?>&nbsp;<?= $this->te('Papierkorb') ?><?php if (!empty($trash_count)): ?> <span style="background:var(--gray-200);border-radius:99px;padding:0 .4rem;font-size:.72rem;"><?= (int)$trash_count ?></span><?php endif; ?>
        </a>
        <a class="btn btn-default btn-sm" title="<?= $this->te('Diesen Ordner als ZIP herunterladen') ?>"
           href="<?= $current_folder_id !== null ? '/files/folder/' . $current_folder_id . '/zip' : '/files/zip' ?>">
            <?= feIcon('download', 13) ?>&nbsp;ZIP
        </a>
        <?php if ($can_manage_folders): ?>
        <button class="btn btn-default btn-sm" onclick="feOpenNewFolder()" type="button">
            <?= feIcon('plus', 13) ?>&nbsp;<?= $this->te('Neuer Ordner') ?>
        </button>
        <?php endif; ?>
        <?php if ($can_upload): ?>
        <button class="btn btn-primary btn-sm" onclick="document.getElementById('feFileInput').click()" type="button">
            <?= feIcon('upload', 13) ?>&nbsp;<?= $this->te('Hochladen') ?>
        </button>
        <?php endif; ?>
    </div>
</div>

<div class="fe-wrap">

  <!-- ── Toolbar / breadcrumb ── -->
  <div class="fe-toolbar">
    <button class="fe-burger" type="button" onclick="feToggleSidebar()" title="<?= $this->te('Ordner') ?>"><?= feIcon('menu', 16) ?></button>
    <div class="fe-toolbar-breadcrumb">
      <a href="/files" data-drop-folder=""><?= feIcon('home', 14) ?> <?= $this->te('Dateien') ?></a>
      <?php foreach ($breadcrumb as $crumb): ?>
        <span class="sep">/</span>
        <?php if ((int)$crumb['id'] === $current_folder_id): ?>
          <span class="cur"><?= htmlspecialchars($crumb['name']) ?></span>
        <?php else: ?>
          <a href="/files/folder/<?= $crumb['id'] ?>" data-drop-folder="<?= $crumb['id'] ?>"><?= htmlspecialchars($crumb['name']) ?></a>
        <?php endif; ?>
      <?php endforeach; ?>
    </div>
    <div class="fe-search">
      <?= feIcon('search', 15) ?>
      <input type="text" id="feSearchInput" placeholder="<?= $this->te('Suchen…') ?>" autocomplete="off"
             oninput="feSearch(this.value)" onfocus="feSearch(this.value)">
      <div class="fe-search-results" id="feSearchResults"></div>
    </div>
    <div class="fe-viewtoggle" title="<?= $this->te('Ansicht') ?>">
      <button type="button" id="feViewList" class="<?= $pref_view !== 'grid' ? 'active' : '' ?>"
              onclick="feSetView('list')" title="<?= $this->te('Liste') ?>"><?= feIcon('list', 15) ?></button>
      <button type="button" id="feViewGrid" class="<?= $pref_view === 'grid' ? 'active' : '' ?>"
              onclick="feSetView('grid')" title="<?= $this->te('Kacheln') ?>"><?= feIcon('grid', 15) ?></button>
    </div>
  </div>

  <div class="fe-body">

    <div class="fe-sidebar-backdrop" id="feSidebarBackdrop" onclick="feToggleSidebar()"></div>

    <!-- ── Sidebar tree ── -->
    <div class="fe-sidebar" id="feSidebar">
      <div class="fe-sidebar-sec"><?= $this->te('Ordner') ?></div>
      <a class="fe-tree-root <?= $current_folder_id === null ? 'active' : '' ?>" href="/files" data-drop-folder="">
        <?= feIcon('home', 14) ?>
        <span><?= $this->te('Alle Dateien') ?></span>
      </a>
      <div class="fe-sidebar-divider"></div>
      <?php feRenderTree($tree, $current_folder_id); ?>
      <?php
        $pct = $storage_max > 0 ? min(100, round($storage_used / $storage_max * 100)) : 0;
      ?>
      <div class="fe-sidebar-foot">
        <div class="fe-storage-bar-wrap"><div class="fe-storage-bar" style="width:<?= $pct ?>%;"></div></div>
        <div class="fe-storage-text"><?= $this->te('{genutzt} von {gesamt} belegt', ['genutzt' => $mgr->formatSize((int)$storage_used), 'gesamt' => $mgr->formatSize((int)$storage_max)]) ?></div>
      </div>
    </div>

    <!-- ── Main scroll area ── -->
    <div class="fe-main">
      <div class="fe-scroll" id="feDropTarget">

        <!-- Upload zone -->
        <?php if ($can_upload): ?>
        <div class="fe-dropzone" id="feDropzone" onclick="document.getElementById('feFileInput').click()">
          <div class="fe-dropzone-icon">&#x1F4E4;</div>
          <div class="fe-dropzone-text"><?= $this->t('Dateien hierher ziehen oder <strong>klicken</strong> zum Auswählen') ?></div>
          <div class="fe-dropzone-hint"><?= $this->t('Alle Formate erlaubt &middot; max. <span id="feMaxLabel"></span> pro Datei') ?></div>
        </div>
        <input type="file" id="feFileInput" style="display:none" multiple onchange="feUploadFiles(this.files)">
        <div class="fe-upload-list" id="feUploadList"></div>
        <?php endif; ?>

        <!-- Bulk action bar -->
        <div class="fe-bulkbar" id="feBulkbar">
          <span class="fe-bulkbar-count" id="feBulkCount"><?= $this->te('0 ausgewählt') ?></span>
          <span class="fe-bulkbar-spacer"></span>
          <button class="btn btn-default btn-sm" type="button" onclick="feBulkZip()"><?= feIcon('zip', 13) ?>&nbsp;<?= $this->te('Als ZIP') ?></button>
          <?php if ($can_delete || $can_manage_folders): ?>
          <button class="btn btn-danger btn-sm" type="button" onclick="feBulkDelete()"><?= feIcon('trash', 13) ?>&nbsp;<?= $this->te('Löschen') ?></button>
          <?php endif; ?>
          <button class="btn btn-default btn-sm" type="button" onclick="feClearSelection()"><?= $this->te('Abbrechen') ?></button>
        </div>

        <!-- Sub-folders -->
        <?php if (!empty($subfolders)): ?>
        <p class="fe-sec"><?= $this->te('Ordner') ?></p>
        <div class="fe-folder-grid">
          <?php foreach ($subfolders as $f):
            $meta = [];
            if ((int)$f['subfolder_count'] > 0) $meta[] = $f['subfolder_count'] . ' Unterordner';
            if ((int)$f['file_count'] > 0)      $meta[] = $f['file_count'] . ' Datei' . ((int)$f['file_count'] !== 1 ? 'en' : '');
            $metaStr = $meta ? implode(' &middot; ', $meta) : 'Leer';
          ?>
          <div class="fe-folder-card" data-folder-id="<?= $f['id'] ?>"
               <?php if ($can_manage_folders): ?>draggable="true" data-drag-folder="<?= $f['id'] ?>"<?php endif; ?>
               data-drop-folder="<?= $f['id'] ?>"
               onclick="location.href='/files/folder/<?= $f['id'] ?>'">
            <input type="checkbox" class="fe-check fe-folder-card-check" title="<?= $this->te('Auswählen') ?>"
                   onclick="event.stopPropagation();feToggleFolder(<?= $f['id'] ?>,this)">
            <div class="fe-folder-card-icon">&#x1F4C1;</div>
            <div class="fe-folder-card-body">
              <div class="fe-folder-card-name"><?= htmlspecialchars($f['name']) ?></div>
              <div class="fe-folder-card-meta"><?= $metaStr ?></div>
            </div>
            <div class="fe-folder-menu-wrap">
              <button class="fe-folder-menu-btn" type="button" title="<?= $this->te('Optionen') ?>" aria-label="<?= $this->te('Optionen') ?>"
                      onclick="feToggleFolderMenu(this, event)">&#8942;</button>
              <div class="fe-folder-menu" role="menu">
                <a href="/files/folder/<?= $f['id'] ?>/zip" role="menuitem"
                   onclick="event.stopPropagation();feCloseFolderMenus()"><?= feIcon('download', 13) ?> <?= $this->te('Als ZIP herunterladen') ?></a>
                <?php if (!empty($f['acl_can_manage'])): ?>
                <button type="button" role="menuitem"
                        onclick="event.stopPropagation();feCloseFolderMenus();feOpenShare('folder',<?= $f['id'] ?>,<?= htmlspecialchars(json_encode($f['name'])) ?>)"><?= feIcon('share', 13) ?> <?= $this->te('Teilen (Link)') ?></button>
                <button type="button" role="menuitem"
                        onclick="event.stopPropagation();feCloseFolderMenus();feOpenAcl(<?= $f['id'] ?>,<?= htmlspecialchars(json_encode($f['name'])) ?>)"><?= feIcon('lock', 13) ?> <?= $this->te('Berechtigungen') ?></button>
                <button type="button" role="menuitem"
                        onclick="event.stopPropagation();feCloseFolderMenus();feOpenMoveFolder(<?= $f['id'] ?>,<?= htmlspecialchars(json_encode($f['name'])) ?>)"><?= feIcon('move', 13) ?> <?= $this->te('Verschieben') ?></button>
                <button type="button" role="menuitem"
                        onclick="event.stopPropagation();feCloseFolderMenus();feOpenRename(<?= $f['id'] ?>,<?= htmlspecialchars(json_encode($f['name'])) ?>)"><?= feIcon('edit', 13) ?> <?= $this->te('Umbenennen') ?></button>
                <button type="button" role="menuitem" class="danger"
                        onclick="event.stopPropagation();feCloseFolderMenus();feOpenDeleteFolder(<?= $f['id'] ?>,<?= htmlspecialchars(json_encode($f['name'])) ?>)"><?= feIcon('trash', 13) ?> <?= $this->te('Löschen') ?></button>
                <?php endif; ?>
              </div>
            </div>
          </div>
          <?php endforeach; ?>
        </div>
        <?php endif; ?>

        <!-- Files -->
        <?php if (!empty($files)): ?>
        <p class="fe-sec" style="margin-top:<?= !empty($subfolders) ? '.75rem' : '0' ?>">
          <?= $this->te('Dateien ({anzahl})', ['anzahl' => count($files)]) ?>
        </p>
        <div id="feFiles" class="fe-view-<?= $pref_view === 'grid' ? 'grid' : 'list' ?>">

          <!-- Listenansicht -->
          <div class="fe-files-list">
          <div class="fe-file-box">
          <table id="feFileTable">
            <thead>
              <tr>
                <th scope="col" class="fe-col-check"><input type="checkbox" class="fe-check" title="<?= $this->te('Alle auswählen') ?>" onclick="feToggleAllFiles(this)"></th>
                <th scope="col" class="sortable" data-sort-col="name" onclick="feSort(1,'name')">Name <span class="fe-sort-ind">↕</span></th>
                <th scope="col" class="fe-hide-sm sortable" data-sort-col="size" onclick="feSort(2,'size')"><?= $this->te('Größe') ?> <span class="fe-sort-ind">↕</span></th>
                <th scope="col" class="fe-hide-sm sortable" data-sort-col="by" onclick="feSort(3,'by')"><?= $this->te('Hochgeladen von') ?> <span class="fe-sort-ind">↕</span></th>
                <th scope="col" class="fe-hide-sm sortable" data-sort-col="date" onclick="feSort(4,'date')"><?= $this->te('Datum') ?> <span class="fe-sort-ind">↕</span></th>
                <th scope="col"></th>
              </tr>
            </thead>
            <tbody>
            <?php foreach ($files as $file):
              $iconType = feFileIcon($file['original_name']);
              $ext      = strtoupper(pathinfo($file['original_name'], PATHINFO_EXTENSION)) ?: '–';
              $ts       = strtotime($file['created_at']);
            ?>
            <tr data-file-id="<?= $file['id'] ?>" <?php if ($can_upload): ?>draggable="true" data-drag-file="<?= $file['id'] ?>"<?php endif; ?>>
              <td class="fe-col-check">
                <input type="checkbox" class="fe-check" onclick="feToggleFile(<?= $file['id'] ?>,this)">
              </td>
              <td data-sort="<?= htmlspecialchars(mb_strtolower($file['original_name'])) ?>">
                <div class="fe-file-name-cell">
                  <?php if (feHasThumb($file['original_name'])): ?>
                  <img class="fe-thumb" draggable="false" loading="lazy" src="/files/thumb/<?= $file['id'] ?>" alt="" onerror="feThumbFail(this)">
                  <?php else: ?>
                  <?= feIcon($iconType, 18) ?>
                  <?php endif; ?>
                  <span class="fe-file-name-text" title="<?= htmlspecialchars($file['original_name']) ?>">
                    <?= htmlspecialchars($file['original_name']) ?>
                  </span>
                  <span class="fe-ext-badge"><?= htmlspecialchars($ext) ?></span>
                </div>
              </td>
              <td class="fe-file-size fe-hide-sm" data-sort="<?= (int)$file['file_size'] ?>"><?= $mgr->formatSize((int)$file['file_size']) ?></td>
              <td class="fe-file-by fe-hide-sm" data-sort="<?= htmlspecialchars(mb_strtolower($file['uploader_name'] ?? '')) ?>"><?= htmlspecialchars($file['uploader_name'] ?? '–') ?></td>
              <td class="fe-file-date fe-hide-sm" data-sort="<?= (int)$ts ?>"><?= date('d.m.Y H:i', $ts) ?></td>
              <td>
                <div class="fe-file-actions"><?php feFileActions($file, $can_upload, $can_delete); ?></div>
              </td>
            </tr>
            <?php endforeach; ?>
            </tbody>
          </table>
          </div>
          </div><!-- /fe-files-list -->

          <!-- Kachelansicht -->
          <div class="fe-files-grid">
            <?php foreach ($files as $file):
              $iconType = feFileIcon($file['original_name']);
              $ext      = strtoupper(pathinfo($file['original_name'], PATHINFO_EXTENSION)) ?: '–';
              $ptype    = fePreviewType($file['original_name']);
            ?>
            <div class="fe-file-card" data-file-id="<?= $file['id'] ?>" <?php if ($can_upload): ?>draggable="true" data-drag-file="<?= $file['id'] ?>"<?php endif; ?>>
              <input type="checkbox" class="fe-check fe-file-card-check" title="<?= $this->te('Auswählen') ?>" onclick="feToggleFile(<?= $file['id'] ?>,this)">
              <div class="fe-file-card-actions"><?php feFileActions($file, $can_upload, $can_delete); ?></div>
              <div class="fe-file-card-preview"
                   <?php if ($ptype !== ''): ?>data-fe-view="/files/preview/<?= $file['id'] ?>"
                   data-fe-view-name="<?= htmlspecialchars($file['original_name']) ?>"
                   data-fe-view-type="<?= $ptype ?>" data-fe-view-download="/files/download/<?= $file['id'] ?>"<?php endif; ?>>
                <?php if (feHasThumb($file['original_name'])): ?>
                <img draggable="false" loading="lazy" src="/files/thumb/<?= $file['id'] ?>" alt="" onerror="feThumbFail(this)">
                <?php else: ?>
                <?= feIcon($iconType, 40) ?>
                <?php endif; ?>
              </div>
              <div class="fe-file-card-body">
                <div class="fe-file-card-name" title="<?= htmlspecialchars($file['original_name']) ?>"><?= htmlspecialchars($file['original_name']) ?></div>
                <div class="fe-file-card-meta"><?= htmlspecialchars($ext) ?> &middot; <?= $mgr->formatSize((int)$file['file_size']) ?></div>
              </div>
            </div>
            <?php endforeach; ?>
          </div><!-- /fe-files-grid -->

        </div><!-- /feFiles -->

        <?php elseif (empty($subfolders)): ?>
        <div class="empty-state" style="border:none;background:transparent;">
          <div class="empty-icon">&#x1F4C2;</div>
          <div class="fe-empty-text"><?= $this->te('Dieser Ordner ist leer.') ?></div>
          <?php if ($can_upload): ?>
          <div class="fe-empty-hint"><?= $this->te('Dateien per Drag &amp; Drop ablegen oder auf &bdquo;Hochladen&ldquo; klicken.') ?></div>
          <?php endif; ?>
        </div>
        <?php endif; ?>

      </div><!-- /fe-scroll -->
    </div><!-- /fe-main -->
  </div><!-- /fe-body -->
</div><!-- /fe-wrap -->
</div><!-- /fe-page -->


<!-- ══ Modals ══════════════════════════════════════════════════ -->

<!-- Neuer Ordner -->
<div class="modal-overlay fe-modal" id="modalNewFolder">
  <div class="modal-card">
    <div class="modal-header">
      <h2><?= $this->te('&#x1F4C1;&nbsp; Neuer Ordner') ?></h2>
      <button class="modal-close" type="button" onclick="feClose('modalNewFolder')"><?= feIcon('x', 15) ?></button>
    </div>
    <form method="POST" action="/files/folders">
      <input type="hidden" name="_csrf" value="<?= $this->csrf() ?>">
      <input type="hidden" name="parent_id" id="newFolderParentId" value="<?= $current_folder_id ?? '' ?>">
      <div class="modal-body">
        <label class="fe-label" for="newFolderName"><?= $this->te('Ordnername') ?></label>
        <input type="text" id="newFolderName" name="name" class="form-control"
               placeholder="<?= $this->te('z.B. Skripte') ?>" maxlength="255" autocomplete="off" required>
      </div>
      <div class="modal-footer">
        <button type="button" class="btn btn-default btn-sm" onclick="feClose('modalNewFolder')"><?= $this->te('Abbrechen') ?></button>
        <button type="submit" class="btn btn-primary btn-sm"><?= $this->te('Erstellen') ?></button>
      </div>
    </form>
  </div>
</div>

<!-- Ordner umbenennen -->
<div class="modal-overlay fe-modal" id="modalRenameFolder">
  <div class="modal-card">
    <div class="modal-header">
      <h2><?= $this->te('Ordner umbenennen') ?></h2>
      <button class="modal-close" type="button" onclick="feClose('modalRenameFolder')"><?= feIcon('x', 15) ?></button>
    </div>
    <form method="POST" id="renameFolderForm" action="">
      <input type="hidden" name="_csrf" value="<?= $this->csrf() ?>">
      <div class="modal-body">
        <label class="fe-label" for="renameFolderName"><?= $this->te('Neuer Name') ?></label>
        <input type="text" id="renameFolderName" name="name" class="form-control"
               placeholder="<?= $this->te('Ordnername') ?>" maxlength="255" autocomplete="off" required>
      </div>
      <div class="modal-footer">
        <button type="button" class="btn btn-default btn-sm" onclick="feClose('modalRenameFolder')"><?= $this->te('Abbrechen') ?></button>
        <button type="submit" class="btn btn-primary btn-sm"><?= $this->te('Umbenennen') ?></button>
      </div>
    </form>
  </div>
</div>

<!-- Datei umbenennen -->
<div class="modal-overlay fe-modal" id="modalRenameFile">
  <div class="modal-card">
    <div class="modal-header">
      <h2><?= $this->te('Datei umbenennen') ?></h2>
      <button class="modal-close" type="button" onclick="feClose('modalRenameFile')"><?= feIcon('x', 15) ?></button>
    </div>
    <form method="POST" id="renameFileForm" action="">
      <input type="hidden" name="_csrf" value="<?= $this->csrf() ?>">
      <div class="modal-body">
        <label class="fe-label" for="renameFileName"><?= $this->te('Neuer Dateiname') ?></label>
        <input type="text" id="renameFileName" name="name" class="form-control"
               placeholder="datei.pdf" maxlength="255" autocomplete="off" required>
      </div>
      <div class="modal-footer">
        <button type="button" class="btn btn-default btn-sm" onclick="feClose('modalRenameFile')"><?= $this->te('Abbrechen') ?></button>
        <button type="submit" class="btn btn-primary btn-sm"><?= $this->te('Umbenennen') ?></button>
      </div>
    </form>
  </div>
</div>

<!-- Verschieben (Ordner & Datei) -->
<div class="modal-overlay fe-modal" id="modalMove">
  <div class="modal-card">
    <div class="modal-header">
      <h2 id="moveTitle"><?= $this->te('Verschieben') ?></h2>
      <button class="modal-close" type="button" onclick="feClose('modalMove')"><?= feIcon('x', 15) ?></button>
    </div>
    <form method="POST" id="moveForm" action="">
      <input type="hidden" name="_csrf" value="<?= $this->csrf() ?>">
      <div class="modal-body">
        <p style="font-size:.82rem;color:var(--gray-500);margin:0 0 .6rem;"><?= $this->t('<strong id="moveItemName"></strong> verschieben nach') ?>:</p>
        <label class="fe-label" for="moveTarget"><?= $this->te('Zielordner') ?></label>
        <select id="moveTarget" name="target_id" class="form-control">
          <option value=""><?= $this->te('Alle Dateien (Hauptebene)') ?></option>
          <?php feFolderOptions($flat_folders); ?>
        </select>
      </div>
      <div class="modal-footer">
        <button type="button" class="btn btn-default btn-sm" onclick="feClose('modalMove')"><?= $this->te('Abbrechen') ?></button>
        <button type="submit" class="btn btn-primary btn-sm"><?= $this->te('Verschieben') ?></button>
      </div>
    </form>
  </div>
</div>

<!-- Versionen -->
<div class="modal-overlay fe-modal" id="modalVersions">
  <div class="modal-card" style="max-width:560px;">
    <div class="modal-header">
      <h2><?= feIcon('history', 15) ?>&nbsp; <?= $this->te('Versionen:') ?> <span id="versionsFileName"></span></h2>
      <button class="modal-close" type="button" onclick="feClose('modalVersions')"><?= feIcon('x', 15) ?></button>
    </div>
    <div class="modal-body" style="max-height:60vh;overflow-y:auto;">
      <div id="versionsList" style="font-size:0.85rem;color:var(--gray-500);"><?= $this->te('Lädt…') ?></div>
    </div>
    <div class="modal-footer">
      <button type="button" class="btn btn-default btn-sm" onclick="feClose('modalVersions')"><?= $this->te('Schließen') ?></button>
    </div>
  </div>
</div>

<!-- Version wiederherstellen (dynamisch befüllt) -->
<form method="POST" id="versionRestoreForm" action="" style="display:none;">
  <input type="hidden" name="_csrf" value="<?= $this->csrf() ?>">
</form>

<!-- Zugriffe (Audit-Log) -->
<div class="modal-overlay fe-modal" id="modalAccess">
  <div class="modal-card" style="max-width:640px;">
    <div class="modal-header">
      <h2><?= feIcon('activity', 15) ?>&nbsp; <?= $this->te('Zugriffe:') ?> <span id="accessFileName"></span></h2>
      <button class="modal-close" type="button" onclick="feClose('modalAccess')"><?= feIcon('x', 15) ?></button>
    </div>
    <div class="modal-body" style="max-height:62vh;overflow-y:auto;">
      <div id="accessList" style="font-size:0.83rem;color:var(--gray-500);"><?= $this->te('Lädt…') ?></div>
    </div>
    <div class="modal-footer">
      <button type="button" class="btn btn-default btn-sm" onclick="feClose('modalAccess')"><?= $this->te('Schließen') ?></button>
    </div>
  </div>
</div>

<!-- Teilen (geteilte Links) -->
<div class="modal-overlay fe-modal" id="modalShare">
  <div class="modal-card" style="max-width:600px;">
    <div class="modal-header">
      <h2><?= feIcon('share', 15) ?>&nbsp; <?= $this->te('Teilen:') ?> <span id="shareItemName"></span></h2>
      <button class="modal-close" type="button" onclick="feClose('modalShare')"><?= feIcon('x', 15) ?></button>
    </div>
    <div class="modal-body" style="max-height:62vh;overflow-y:auto;">
      <div style="display:flex;gap:.4rem;align-items:flex-end;flex-wrap:wrap;margin-bottom:.85rem;">
        <div style="width:150px;">
          <label for="shareExpiry" class="fe-label"><?= $this->te('Gültig für') ?></label>
          <select id="shareExpiry" class="form-control">
            <option value="1"><?= $this->te('1 Tag') ?></option>
            <option value="7" selected><?= $this->te('7 Tage') ?></option>
            <option value="30"><?= $this->te('30 Tage') ?></option>
            <option value="0"><?= $this->te('Unbegrenzt') ?></option>
          </select>
        </div>
        <div style="flex:1;min-width:140px;">
          <label class="fe-label"><?= $this->te('Passwort (optional)') ?></label>
          <input id="sharePassword" class="form-control" type="text" autocomplete="off" placeholder="<?= $this->te('leer = ohne') ?>">
        </div>
        <button class="btn btn-primary btn-sm" type="button" onclick="feShareCreate()"><?= $this->te('Link erstellen') ?></button>
      </div>
      <div id="shareList" style="font-size:0.83rem;color:var(--gray-500);"><?= $this->te('Lädt…') ?></div>
    </div>
    <div class="modal-footer">
      <button type="button" class="btn btn-default btn-sm" onclick="feClose('modalShare')"><?= $this->te('Schließen') ?></button>
    </div>
  </div>
</div>

<!-- Ordner-Berechtigungen (ACL) -->
<div class="modal-overlay fe-modal" id="modalAcl">
  <div class="modal-card" style="max-width:600px;">
    <div class="modal-header">
      <h2><?= feIcon('lock', 15) ?>&nbsp; <?= $this->te('Berechtigungen:') ?> <span id="aclFolderName"></span></h2>
      <button class="modal-close" type="button" onclick="feClose('modalAcl')"><?= feIcon('x', 15) ?></button>
    </div>
    <div class="modal-body" style="max-height:62vh;overflow-y:auto;">
      <p id="aclHint" style="font-size:0.78rem;color:var(--gray-500);margin:0 0 .75rem;"></p>
      <div id="aclList" style="font-size:0.85rem;"><?= $this->te('Lädt…') ?></div>

      <div style="border-top:1px solid var(--gray-100);margin-top:.85rem;padding-top:.85rem;display:flex;flex-direction:column;gap:.6rem;">
        <div style="display:flex;gap:.4rem;align-items:flex-end;flex-wrap:wrap;">
          <div style="flex:1;min-width:140px;">
            <label for="aclRoleSelect" class="fe-label"><?= $this->te('Rolle') ?></label>
            <select id="aclRoleSelect" class="form-control"></select>
          </div>
          <div style="width:130px;">
            <label for="aclRolePerm" class="fe-label"><?= $this->te('Recht') ?></label>
            <select id="aclRolePerm" class="form-control">
              <option value="view"><?= $this->te('Ansehen') ?></option>
              <option value="upload"><?= $this->te('Hochladen') ?></option>
              <option value="manage"><?= $this->te('Verwalten') ?></option>
            </select>
          </div>
          <button class="btn btn-default btn-sm" type="button" onclick="feAclAddRole()"><?= $this->te('Hinzufügen') ?></button>
        </div>
        <div style="display:flex;gap:.4rem;align-items:flex-end;flex-wrap:wrap;">
          <div style="flex:1;min-width:140px;">
            <label class="fe-label"><?= $this->te('Benutzer (E-Mail)') ?></label>
            <input id="aclUserEmail" class="form-control" type="email" placeholder="<?= $this->te('name@firma.de') ?>" autocomplete="off">
          </div>
          <div style="width:130px;">
            <label for="aclUserPerm" class="fe-label"><?= $this->te('Recht') ?></label>
            <select id="aclUserPerm" class="form-control">
              <option value="view"><?= $this->te('Ansehen') ?></option>
              <option value="upload"><?= $this->te('Hochladen') ?></option>
              <option value="manage"><?= $this->te('Verwalten') ?></option>
            </select>
          </div>
          <button class="btn btn-default btn-sm" type="button" onclick="feAclAddUser()"><?= $this->te('Hinzufügen') ?></button>
        </div>
      </div>
    </div>
    <div class="modal-footer">
      <button type="button" class="btn btn-default btn-sm" onclick="feClose('modalAcl')"><?= $this->te('Schließen') ?></button>
    </div>
  </div>
</div>

<!-- Ordner löschen -->
<div class="modal-overlay fe-modal" id="modalDeleteFolder">
  <div class="modal-card">
    <div class="modal-header">
      <h2 style="color:var(--red-600);"><?= $this->te('&#x26A0;&#xFE0F;&nbsp; Ordner löschen') ?></h2>
      <button class="modal-close" type="button" onclick="feClose('modalDeleteFolder')"><?= feIcon('x', 15) ?></button>
    </div>
    <form method="POST" id="deleteFolderForm" action="">
      <input type="hidden" name="_csrf" value="<?= $this->csrf() ?>">
      <div class="modal-body">
        <p style="font-size:.875rem;color:var(--gray-700);margin:0;">
          <?= $this->t('Ordner <strong id="deleteFolderName"></strong> und alle enthaltenen Unterordner und Dateien löschen?') ?>
        </p>
        <p style="font-size:.8rem;color:var(--red-600);margin:.6rem 0 0;"><?= $this->te('Diese Aktion kann nicht rückgängig gemacht werden.') ?></p>
      </div>
      <div class="modal-footer">
        <button type="button" class="btn btn-default btn-sm" onclick="feClose('modalDeleteFolder')"><?= $this->te('Abbrechen') ?></button>
        <button type="submit" class="btn btn-danger btn-sm"><?= $this->te('Endgültig löschen') ?></button>
      </div>
    </form>
  </div>
</div>

<!-- Datei löschen -->
<div class="modal-overlay fe-modal" id="modalDeleteFile">
  <div class="modal-card">
    <div class="modal-header">
      <h2 style="color:var(--red-600);"><?= $this->te('&#x26A0;&#xFE0F;&nbsp; Datei löschen') ?></h2>
      <button class="modal-close" type="button" onclick="feClose('modalDeleteFile')"><?= feIcon('x', 15) ?></button>
    </div>
    <form method="POST" id="deleteFileForm" action="">
      <input type="hidden" name="_csrf" value="<?= $this->csrf() ?>">
      <div class="modal-body">
        <p style="font-size:.875rem;color:var(--gray-700);margin:0;">
          <?= $this->t('Datei <strong id="deleteFileName"></strong> wirklich löschen?') ?>
        </p>
      </div>
      <div class="modal-footer">
        <button type="button" class="btn btn-default btn-sm" onclick="feClose('modalDeleteFile')"><?= $this->te('Abbrechen') ?></button>
        <button type="submit" class="btn btn-danger btn-sm"><?= $this->te('Löschen') ?></button>
      </div>
    </form>
  </div>
</div>

<!-- Bulk löschen -->
<form method="POST" id="bulkDeleteForm" action="/files/bulk-delete" style="display:none;">
  <input type="hidden" name="_csrf" value="<?= $this->csrf() ?>">
  <input type="hidden" name="folder_id" value="<?= $current_folder_id ?? '' ?>">
  <div id="bulkDeleteInputs"></div>
</form>

<!-- Bulk ZIP -->
<form method="POST" id="bulkZipForm" action="/files/bulk-zip" style="display:none;">
  <input type="hidden" name="_csrf" value="<?= $this->csrf() ?>">
  <input type="hidden" name="folder_id" value="<?= $current_folder_id ?? '' ?>">
  <div id="bulkZipInputs"></div>
</form>

<!-- Vorschau-Modal (Bilder, PDFs, Text, Video & Audio inline) -->
<div id="fePreviewModal" class="fe-preview-modal" hidden>
  <div class="fe-preview-backdrop" data-fe-preview-close></div>
  <button type="button" class="fe-preview-btn fe-preview-close" data-fe-preview-close
          aria-label="<?= $this->te('Schließen') ?>" title="<?= $this->te('Schließen (Esc)') ?>">&times;</button>
  <button type="button" class="fe-preview-btn fe-preview-nav fe-preview-prev"
          aria-label="<?= $this->te('Vorheriges') ?>" title="<?= $this->te('Vorheriges (←)') ?>">&#10094;</button>
  <figure class="fe-preview-figure" role="dialog" aria-modal="true" aria-label="<?= $this->te('Datei-Vorschau') ?>">
    <img id="fePreviewImg" class="fe-preview-img" src="" alt="" hidden>
    <iframe id="fePreviewFrame" class="fe-preview-frame" src="" title="<?= $this->te('Vorschau') ?>" hidden></iframe>
    <video id="fePreviewVideo" class="fe-preview-frame" controls hidden></video>
    <audio id="fePreviewAudio" controls hidden style="width:min(640px,80vw);"></audio>
    <figcaption class="fe-preview-caption">
      <span id="fePreviewName" class="fe-preview-name"></span>
      <a id="fePreviewOpen" class="fe-preview-link" href="#" target="_blank" rel="noopener"><?= $this->te('In neuem Tab öffnen &#8599;') ?></a>
      <a id="fePreviewDownload" class="fe-preview-link" href="#"><?= $this->te('Herunterladen &#8615;') ?></a>
    </figcaption>
  </figure>
  <button type="button" class="fe-preview-btn fe-preview-nav fe-preview-next"
          aria-label="<?= $this->te('Nächstes') ?>" title="<?= $this->te('Nächstes (→)') ?>">&#10095;</button>
</div>

<script>
// ── Config ─────────────────────────────────────────────────
var feMaxUpload   = <?= (int)$max_upload_bytes ?>;   // max. pro Einzel-Request (post_max_size)
var feHardMax     = <?= (int)$hard_max_bytes ?>;     // absolutes Datei-Limit des Moduls
var feCurrentFolder = '<?= $current_folder_id ?? '' ?>';
var feFolderPaths = <?= json_encode(array_column($flat_folders, 'path', 'id')) ?>;

// Chunk-Größe: kleiner als das Einzel-Request-Limit (Shared Hosting!), gedeckelt auf 8 MB.
var feChunkSize = Math.max(1048576, Math.min(8388608, feMaxUpload > 0 ? Math.floor(feMaxUpload * 0.8) : 4194304));

document.getElementById('feMaxLabel') && (document.getElementById('feMaxLabel').textContent = feFmtBytes(feHardMax));

function feRandHex(n) {
    var a = new Uint8Array(n);
    (window.crypto || window.msCrypto).getRandomValues(a);
    return Array.prototype.map.call(a, function(b){ return ('0' + b.toString(16)).slice(-2); }).join('');
}

function feFmtBytes(b) {
    if (b < 1024) return b + ' B';
    if (b < 1048576) return (b/1024).toFixed(1) + ' KB';
    if (b < 1073741824) return (b/1048576).toFixed(1) + ' MB';
    return (b/1073741824).toFixed(2) + ' GB';
}

// ── Modals ─────────────────────────────────────────────────
function feOpen(id)  { document.getElementById(id).classList.add('open'); }
function feClose(id) { document.getElementById(id).classList.remove('open'); }

// Ordner-Optionsmenü (Kebab) auf-/zuklappen.
function feCloseFolderMenus() {
    document.querySelectorAll('.fe-folder-menu-wrap.open').forEach(function(w) {
        w.classList.remove('open');
        var c = w.closest('.fe-folder-card');
        if (c) c.classList.remove('menu-open');
    });
}
function feToggleFolderMenu(btn, ev) {
    if (ev) ev.stopPropagation();
    var wrap = btn.closest('.fe-folder-menu-wrap');
    if (!wrap) return;
    var wasOpen = wrap.classList.contains('open');
    feCloseFolderMenus();
    if (!wasOpen) {
        wrap.classList.add('open');
        var card = btn.closest('.fe-folder-card');
        if (card) card.classList.add('menu-open');
    }
}
document.addEventListener('click', feCloseFolderMenus);
document.addEventListener('keydown', function(e) { if (e.key === 'Escape') feCloseFolderMenus(); });
document.querySelectorAll('.fe-modal').forEach(function(el) {
    el.addEventListener('click', function(e) { if (e.target === el) feClose(el.id); });
});
document.addEventListener('keydown', function(e) {
    if (e.key === 'Escape') {
        document.querySelectorAll('.fe-modal.open').forEach(function(el){ el.classList.remove('open'); });
        feCloseSearch();
    }
});

// ── New Folder ─────────────────────────────────────────────
function feOpenNewFolder() {
    document.getElementById('newFolderName').value = '';
    feOpen('modalNewFolder');
    setTimeout(function() { document.getElementById('newFolderName').focus(); }, 80);
}

// ── Rename Folder ──────────────────────────────────────────
function feOpenRename(id, name) {
    document.getElementById('renameFolderForm').action = '/files/folders/' + id + '/rename';
    document.getElementById('renameFolderName').value  = name;
    feOpen('modalRenameFolder');
    setTimeout(function() {
        var el = document.getElementById('renameFolderName'); el.focus(); el.select();
    }, 80);
}

// ── Rename File ────────────────────────────────────────────
function feOpenRenameFile(id, name) {
    document.getElementById('renameFileForm').action = '/files/files/' + id + '/rename';
    document.getElementById('renameFileName').value  = name;
    feOpen('modalRenameFile');
    setTimeout(function() {
        var el = document.getElementById('renameFileName'); el.focus();
        var dot = name.lastIndexOf('.');
        if (dot > 0) el.setSelectionRange(0, dot); else el.select();
    }, 80);
}

// ── Move ───────────────────────────────────────────────────
function feOpenMoveFolder(id, name) {
    var form = document.getElementById('moveForm');
    form.action = '/files/folders/' + id + '/move';
    document.getElementById('moveTitle').textContent  = 'Ordner verschieben';
    document.getElementById('moveItemName').textContent = name;
    var movedPath = feFolderPaths[id] || '';
    // Eigenen Ordner und alle Nachfahren als Ziel sperren
    document.querySelectorAll('#moveTarget option').forEach(function(opt) {
        var p = opt.getAttribute('data-path') || '';
        opt.disabled = (opt.value == id) || (movedPath && p.indexOf(movedPath) === 0);
    });
    document.getElementById('moveTarget').value = '';
    feOpen('modalMove');
}
function feOpenMoveFile(id, name) {
    var form = document.getElementById('moveForm');
    form.action = '/files/files/' + id + '/move';
    document.getElementById('moveTitle').textContent  = 'Datei verschieben';
    document.getElementById('moveItemName').textContent = name;
    document.querySelectorAll('#moveTarget option').forEach(function(opt){ opt.disabled = false; });
    document.getElementById('moveTarget').value = feCurrentFolder;
    feOpen('modalMove');
}

// ── Delete Folder / File ───────────────────────────────────
function feOpenDeleteFolder(id, name) {
    document.getElementById('deleteFolderForm').action = '/files/folders/' + id + '/delete';
    document.getElementById('deleteFolderName').textContent = name;
    feOpen('modalDeleteFolder');
}
function feOpenDeleteFile(id, name) {
    document.getElementById('deleteFileForm').action = '/files/' + id + '/delete';
    document.getElementById('deleteFileName').textContent = name;
    feOpen('modalDeleteFile');
}

// ── Versionen ──────────────────────────────────────────────
function feOpenVersions(id, name) {
    document.getElementById('versionsFileName').textContent = name;
    document.getElementById('versionsList').textContent = 'Lädt…';
    feOpen('modalVersions');
    fetch('/files/files/' + id + '/versions', { headers: { 'X-Requested-With': 'XMLHttpRequest' } })
        .then(function(r){ return r.json(); })
        .then(function(data){ feRenderVersions(id, data); })
        .catch(function(){ document.getElementById('versionsList').textContent = 'Fehler beim Laden.'; });
}
function feRenderVersions(fileId, data) {
    var html = '';
    html += '<div style="display:flex;align-items:center;gap:.5rem;padding:.5rem .6rem;background:#eff6ff;border:1px solid #bfdbfe;border-radius:5px;margin-bottom:.6rem;">'
          + '<strong style="color:#1e40af;">Aktuell · v' + data.file.version + '</strong>'
          + '<span style="flex:1;"></span>'
          + '<a class="btn btn-default btn-xs" href="/files/download/' + fileId + '">Herunterladen</a></div>';
    if (!data.versions || !data.versions.length) {
        html += '<div style="color:#94a3b8;">Keine früheren Versionen.</div>';
    } else {
        data.versions.forEach(function(v) {
            html += '<div style="display:flex;align-items:center;gap:.5rem;padding:.5rem .6rem;border-bottom:1px solid #f1f5f9;">'
                 + '<span style="font-weight:600;color:#475569;">v' + v.version + '</span>'
                 + '<span style="flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;color:#64748b;">'
                 + feEsc(v.date) + ' · ' + feEsc(v.by) + ' · ' + feEsc(v.size) + '</span>'
                 + '<a class="btn btn-default btn-xs" href="/files/versions/' + v.id + '/download">Herunterladen</a>';
            if (data.can_restore) {
                html += '<button class="btn btn-default btn-xs" type="button" onclick="feRestoreVersion(' + fileId + ',' + v.id + ')">Wiederherstellen</button>';
            }
            html += '</div>';
        });
    }
    document.getElementById('versionsList').innerHTML = html;
}
function feRestoreVersion(fileId, versionId) {
    if (!confirm('Diese Version wiederherstellen? Die aktuelle Fassung wird dabei als neue Version archiviert.')) return;
    var form = document.getElementById('versionRestoreForm');
    form.action = '/files/files/' + fileId + '/versions/' + versionId + '/restore';
    form.submit();
}

// ── Zugriffe (Audit-Log einer Datei) ───────────────────────
function feOpenAccess(id, name) {
    document.getElementById('accessFileName').textContent = name;
    document.getElementById('accessList').textContent = 'Lädt…';
    feOpen('modalAccess');
    fetch('/files/files/' + id + '/access', { headers: { 'X-Requested-With': 'XMLHttpRequest' } })
        .then(function(r){ return r.json(); })
        .then(function(data){
            if (data.error) { document.getElementById('accessList').textContent = data.error; return; }
            if (!data.entries || !data.entries.length) {
                document.getElementById('accessList').innerHTML = '<div style="color:#94a3b8;">Noch keine Zugriffe protokolliert.</div>';
                return;
            }
            var html = '<table style="width:100%;border-collapse:collapse;">'
                     + '<thead><tr style="text-align:left;color:#94a3b8;font-size:.7rem;text-transform:uppercase;">'
                     + '<th style="padding:.3rem .4rem;">Zeit</th><th>Benutzer</th><th>Aktion</th><th>IP</th></tr></thead><tbody>';
            data.entries.forEach(function(e){
                var col = e.result === 'denied' ? '#dc2626' : '#374151';
                html += '<tr style="border-top:1px solid #f1f5f9;color:' + col + ';">'
                     + '<td style="padding:.35rem .4rem;white-space:nowrap;">' + feEsc(e.time) + '</td>'
                     + '<td>' + feEsc(e.user) + '</td>'
                     + '<td>' + feEsc(e.action) + (e.result === 'denied' ? ' (abgelehnt)' : '') + '</td>'
                     + '<td style="color:#94a3b8;">' + feEsc(e.ip) + '</td></tr>';
            });
            html += '</tbody></table>';
            document.getElementById('accessList').innerHTML = html;
        })
        .catch(function(){ document.getElementById('accessList').textContent = 'Fehler beim Laden.'; });
}

// ── Geteilte Links ─────────────────────────────────────────
var feShareType = null, feShareId = null;
function feOpenShare(type, id, name) {
    feShareType = type; feShareId = id;
    document.getElementById('shareItemName').textContent = name;
    document.getElementById('sharePassword').value = '';
    document.getElementById('shareList').textContent = 'Lädt…';
    feOpen('modalShare');
    feShareLoad();
}
function feShareBase() {
    return feShareType === 'folder' ? '/files/folders/' + feShareId + '/shares' : '/files/files/' + feShareId + '/shares';
}
function feShareLoad() {
    fetch(feShareBase(), { headers: { 'X-Requested-With': 'XMLHttpRequest' } })
        .then(function(r){ return r.json(); })
        .then(feRenderShares)
        .catch(function(){ document.getElementById('shareList').textContent = 'Fehler beim Laden.'; });
}
function feRenderShares(data) {
    if (data.error) { document.getElementById('shareList').textContent = data.error; return; }
    if (!data.shares || !data.shares.length) {
        document.getElementById('shareList').innerHTML = '<div style="color:#94a3b8;">Noch keine Links.</div>';
        return;
    }
    var html = '';
    data.shares.forEach(function(s){
        var meta = [];
        meta.push(s.expires ? 'bis ' + s.expires : 'unbegrenzt');
        if (s.has_password) meta.push('🔒 Passwort');
        meta.push(s.count + 'x abgerufen');
        html += '<div style="padding:.5rem 0;border-bottom:1px solid #f1f5f9;">'
             + '<div style="display:flex;gap:.4rem;align-items:center;">'
             + '<input class="form-control" style="flex:1;font-size:.78rem;" readonly value="' + feEsc(s.url) + '" onclick="this.select()">'
             + '<button class="btn btn-default btn-xs" type="button" onclick="feShareCopy(this,\'' + feEsc(s.url) + '\')">Kopieren</button>'
             + '<button class="btn btn-outline-danger btn-xs" type="button" onclick="feShareDelete(' + s.id + ')">Widerrufen</button>'
             + '</div>'
             + '<div style="font-size:.72rem;color:#94a3b8;margin-top:.2rem;">' + feEsc(meta.join(' · ')) + '</div>'
             + '</div>';
    });
    document.getElementById('shareList').innerHTML = html;
}
function feShareCreate() {
    var fd = new FormData();
    fd.append('_csrf', feCsrf());
    fd.append('expires_days', document.getElementById('shareExpiry').value);
    fd.append('password', document.getElementById('sharePassword').value);
    fetch(feShareBase(), { method: 'POST', body: fd, headers: { 'X-Requested-With': 'XMLHttpRequest' } })
        .then(function(r){ return r.json(); })
        .then(function(d){ if (d.error) { alert(d.error); } else { document.getElementById('sharePassword').value=''; feShareLoad(); } })
        .catch(function(){ alert('Fehler beim Erstellen.'); });
}
function feShareDelete(id) {
    if (!confirm('Diesen Link widerrufen? Er funktioniert danach nicht mehr.')) return;
    var fd = new FormData();
    fd.append('_csrf', feCsrf());
    fetch('/files/shares/' + id + '/delete', { method: 'POST', body: fd, headers: { 'X-Requested-With': 'XMLHttpRequest' } })
        .then(function(r){ return r.json(); })
        .then(function(d){ if (d.error) { alert(d.error); } else { feShareLoad(); } });
}
function feShareCopy(btn, url) {
    if (navigator.clipboard) { navigator.clipboard.writeText(url); btn.textContent = 'Kopiert'; setTimeout(function(){ btn.textContent='Kopieren'; }, 1200); }
}

// ── Berechtigungen (ACL) ───────────────────────────────────
var feAclFolderId = null;
var fePermLabel = { view: 'Ansehen', upload: 'Hochladen', manage: 'Verwalten' };
function feOpenAcl(id, name) {
    feAclFolderId = id;
    document.getElementById('aclFolderName').textContent = name;
    document.getElementById('aclList').textContent = 'Lädt…';
    document.getElementById('aclUserEmail').value = '';
    feOpen('modalAcl');
    feAclLoad();
}
function feAclLoad() {
    fetch('/files/folders/' + feAclFolderId + '/acl', { headers: { 'X-Requested-With': 'XMLHttpRequest' } })
        .then(function(r){ return r.json(); })
        .then(feRenderAcl)
        .catch(function(){ document.getElementById('aclList').textContent = 'Fehler beim Laden.'; });
}
function feRenderAcl(data) {
    if (data.error) { document.getElementById('aclList').textContent = data.error; return; }
    // Rollen-Dropdown füllen
    var sel = document.getElementById('aclRoleSelect');
    sel.innerHTML = '';
    (data.roles || []).forEach(function(r){
        var o = document.createElement('option'); o.value = r.id; o.textContent = r.name; sel.appendChild(o);
    });
    // Hinweis zur Vererbung
    var hint = document.getElementById('aclHint');
    if (!data.has_own) {
        hint.innerHTML = 'Dieser Ordner hat <strong>keine eigenen Berechtigungen</strong> und erbt sie vom übergeordneten Ordner. '
                       + 'Ohne Einträge ist er für alle mit globaler Datei-Berechtigung sichtbar. Der erste Eintrag schränkt den Zugriff ein.';
    } else {
        hint.innerHTML = 'Eigene Berechtigungen gesetzt – nur die unten gelisteten Rollen/Benutzer (plus Admins und der Ordner-Eigentümer) haben Zugriff. Unterordner erben diese Regeln.';
    }
    // Einträge
    var list = document.getElementById('aclList');
    if (!data.entries || !data.entries.length) {
        list.innerHTML = '<div style="color:#94a3b8;padding:.4rem 0;">Keine eigenen Einträge.</div>';
        return;
    }
    var html = '';
    data.entries.forEach(function(e){
        var icon = e.type === 'role' ? '👥' : '👤';
        var sub  = e.type === 'user' && e.email ? ' <span style="color:#94a3b8;">' + feEsc(e.email) + '</span>' : '';
        html += '<div style="display:flex;align-items:center;gap:.5rem;padding:.45rem .1rem;border-bottom:1px solid #f1f5f9;">'
             + '<span>' + icon + '</span>'
             + '<span style="flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;">' + feEsc(e.name) + sub + '</span>'
             + '<span class="tr-badge" style="background:#eff6ff;color:#1d4ed8;border-radius:99px;padding:.05rem .5rem;font-size:.72rem;">' + (fePermLabel[e.permission] || e.permission) + '</span>'
             + '<button class="btn btn-outline-danger btn-xs" type="button" onclick="feAclRemove(' + e.id + ')">Entfernen</button>'
             + '</div>';
    });
    list.innerHTML = html;
}
function feAclPost(url, fd) {
    var csrf = document.querySelector('meta[name="csrf-token"]');
    if (csrf) fd.append('_csrf', csrf.content);
    return fetch(url, { method: 'POST', body: fd, headers: { 'X-Requested-With': 'XMLHttpRequest' } })
        .then(function(r){ return r.json(); })
        .then(function(d){ if (d.error) { alert(d.error); } else { feAclLoad(); } });
}
function feAclAddRole() {
    var fd = new FormData();
    fd.append('principal_type', 'role');
    fd.append('role_id', document.getElementById('aclRoleSelect').value);
    fd.append('permission', document.getElementById('aclRolePerm').value);
    feAclPost('/files/folders/' + feAclFolderId + '/acl', fd);
}
function feAclAddUser() {
    var email = document.getElementById('aclUserEmail').value.trim();
    if (!email) { alert('Bitte eine E-Mail-Adresse angeben.'); return; }
    var fd = new FormData();
    fd.append('principal_type', 'user');
    fd.append('user_email', email);
    fd.append('permission', document.getElementById('aclUserPerm').value);
    feAclPost('/files/folders/' + feAclFolderId + '/acl', fd).then(function(){
        document.getElementById('aclUserEmail').value = '';
    });
}
function feAclRemove(entryId) {
    var fd = new FormData();
    fd.append('entry_id', entryId);
    feAclPost('/files/folders/' + feAclFolderId + '/acl/delete', fd);
}

// ── Sidebar (mobile) ───────────────────────────────────────
function feToggleSidebar() {
    document.getElementById('feSidebar').classList.toggle('open');
    document.getElementById('feSidebarBackdrop').classList.toggle('open');
}

// ── Tree toggle ────────────────────────────────────────────
function feTreeToggle(btn) {
    btn.classList.toggle('open');
    var children = btn.closest('.fe-tree-node').querySelector('.fe-tree-children');
    if (children) children.classList.toggle('open');
}

// ── Selection / Bulk ───────────────────────────────────────
var feSelFiles = new Set();
var feSelFolders = new Set();

function feToggleFile(id, cb) {
    if (cb.checked) feSelFiles.add(id); else feSelFiles.delete(id);
    cb.closest('tr').classList.toggle('selected', cb.checked);
    feUpdateBulk();
}
function feToggleFolder(id, cb) {
    if (cb.checked) feSelFolders.add(id); else feSelFolders.delete(id);
    cb.closest('.fe-folder-card').classList.toggle('selected', cb.checked);
    feUpdateBulk();
}
function feToggleAllFiles(cb) {
    document.querySelectorAll('#feFileTable tbody tr').forEach(function(tr) {
        var box = tr.querySelector('.fe-check');
        box.checked = cb.checked;
        feToggleFile(parseInt(tr.getAttribute('data-file-id'), 10), box);
    });
}
function feUpdateBulk() {
    var n = feSelFiles.size + feSelFolders.size;
    var bar = document.getElementById('feBulkbar');
    bar.classList.toggle('open', n > 0);
    document.getElementById('feBulkCount').textContent = n + ' ausgewählt';
}
function feClearSelection() {
    feSelFiles.clear(); feSelFolders.clear();
    document.querySelectorAll('.fe-check').forEach(function(c){ c.checked = false; });
    document.querySelectorAll('.fe-folder-card.selected, tr.selected').forEach(function(e){ e.classList.remove('selected'); });
    feUpdateBulk();
}
function feBulkDelete() {
    var n = feSelFiles.size + feSelFolders.size;
    if (!n) return;
    if (!confirm(n + ' Element(e) endgültig löschen? Ordner werden inkl. Inhalt entfernt.')) return;
    var box = document.getElementById('bulkDeleteInputs');
    box.innerHTML = '';
    feSelFiles.forEach(function(id){
        box.insertAdjacentHTML('beforeend', '<input type="hidden" name="file_ids[]" value="' + id + '">');
    });
    feSelFolders.forEach(function(id){
        box.insertAdjacentHTML('beforeend', '<input type="hidden" name="folder_ids[]" value="' + id + '">');
    });
    document.getElementById('bulkDeleteForm').submit();
}

// ── Sorting (mit gemerkter Präferenz) ──────────────────────
var feSortCol = null, feSortAsc = true;
function feSort(col, key, persist) {
    var tbody = document.querySelector('#feFileTable tbody');
    if (!tbody) return;
    var rows = Array.prototype.slice.call(tbody.querySelectorAll('tr'));
    feSortAsc = (feSortCol === col) ? !feSortAsc : true;
    feSortCol = col;
    rows.sort(function(a, b) {
        var av = a.children[col].getAttribute('data-sort') || a.children[col].textContent.trim();
        var bv = b.children[col].getAttribute('data-sort') || b.children[col].textContent.trim();
        var an = parseFloat(av), bn = parseFloat(bv);
        var cmp;
        if (!isNaN(an) && !isNaN(bn)) cmp = an - bn;
        else cmp = String(av).localeCompare(String(bv), 'de');
        return feSortAsc ? cmp : -cmp;
    });
    rows.forEach(function(r){ tbody.appendChild(r); });
    if (persist !== false && key) feSavePref(null, key + ':' + (feSortAsc ? 'asc' : 'desc'));
}

// Gemerkte Sortierung beim Laden anwenden
(function () {
    var sort = '<?= htmlspecialchars($pref_sort) ?>';
    if (!sort) return;
    var parts = sort.split(':');
    var colMap = { name:1, size:2, by:3, date:4 };
    var col = colMap[parts[0]];
    if (!col) return;
    // gewünschte Richtung herstellen (feSort toggelt, daher ggf. zweimal)
    feSortCol = col; feSortAsc = (parts[1] === 'asc');
    feSort(col, parts[0], false);
    if ((parts[1] === 'asc') !== feSortAsc) feSort(col, parts[0], false);
})();

// ── Ansicht (Liste/Kacheln) + Präferenz speichern ──────────
function feSetView(view) {
    var box = document.getElementById('feFiles');
    if (box) box.className = 'fe-view-' + view;
    var lb = document.getElementById('feViewList'), gb = document.getElementById('feViewGrid');
    if (lb) lb.classList.toggle('active', view === 'list');
    if (gb) gb.classList.toggle('active', view === 'grid');
    feSavePref(view, null);
}
function feSavePref(view, sort) {
    var fd = new FormData();
    var csrf = document.querySelector('meta[name="csrf-token"]');
    if (csrf) fd.append('_csrf', csrf.content);
    if (view) fd.append('view', view);
    if (sort) fd.append('sort', sort);
    fetch('/files/pref', { method: 'POST', body: fd, headers: { 'X-Requested-With': 'XMLHttpRequest' } })
        .catch(function(){});
}

// ── Thumbnail-Fallback (wenn keine Vorschau generierbar) ───
function feThumbFail(img) {
    var span = document.createElement('span');
    span.className = 'fi fi-file';
    span.innerHTML = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" width="18" height="18"><path d="M13 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V9z"/><polyline points="13 2 13 9 20 9"/></svg>';
    if (img.parentNode) img.parentNode.replaceChild(span, img);
}

// ── Bulk: Als ZIP herunterladen ────────────────────────────
function feBulkZip() {
    var n = feSelFiles.size + feSelFolders.size;
    if (!n) return;
    var box = document.getElementById('bulkZipInputs');
    box.innerHTML = '';
    feSelFiles.forEach(function(id){
        box.insertAdjacentHTML('beforeend', '<input type="hidden" name="file_ids[]" value="' + id + '">');
    });
    feSelFolders.forEach(function(id){
        box.insertAdjacentHTML('beforeend', '<input type="hidden" name="folder_ids[]" value="' + id + '">');
    });
    document.getElementById('bulkZipForm').submit();
}

// ── Drag & Drop: Dateien/Ordner verschieben ────────────────
(function () {
    var dragType = null, dragId = null;

    document.addEventListener('dragstart', function (e) {
        var el = e.target.closest('[data-drag-file], [data-drag-folder]');
        if (!el) return;
        if (el.hasAttribute('data-drag-file')) { dragType = 'file';   dragId = el.getAttribute('data-drag-file'); }
        else                                   { dragType = 'folder'; dragId = el.getAttribute('data-drag-folder'); }
        el.classList.add('fe-drag-active');
        e.dataTransfer.effectAllowed = 'move';
        try { e.dataTransfer.setData('text/plain', dragType + ':' + dragId); } catch (err) {}
    });
    document.addEventListener('dragend', function (e) {
        var el = e.target.closest('[data-drag-file], [data-drag-folder]');
        if (el) el.classList.remove('fe-drag-active');
        clearDropHints();
        dragType = null; dragId = null;
    });

    function targetOf(e) { return e.target.closest('[data-drop-folder]'); }
    function clearDropHints() {
        document.querySelectorAll('.fe-drop-target').forEach(function(el){ el.classList.remove('fe-drop-target'); });
    }
    function validTarget(t) {
        if (!t || dragId === null) return false;
        var targetFolder = t.getAttribute('data-drop-folder'); // '' = Wurzel
        if (dragType === 'folder') {
            if (targetFolder === dragId) return false;                 // in sich selbst
            var movedPath = feFolderPaths[dragId] || '';
            var tPath = targetFolder === '' ? '' : (feFolderPaths[targetFolder] || '');
            if (movedPath && tPath && tPath.indexOf(movedPath) === 0) return false; // in Nachfahren
        }
        return true;
    }

    document.addEventListener('dragover', function (e) {
        if (dragId === null) return;
        var t = targetOf(e);
        if (!validTarget(t)) return;
        e.preventDefault();
        e.dataTransfer.dropEffect = 'move';
        if (!t.classList.contains('fe-drop-target')) { clearDropHints(); t.classList.add('fe-drop-target'); }
    });
    document.addEventListener('drop', function (e) {
        if (dragId === null) return;
        var t = targetOf(e);
        if (!validTarget(t)) return;
        e.preventDefault();
        var targetFolder = t.getAttribute('data-drop-folder');
        feMove(dragType, dragId, targetFolder);
    });

    function feMove(type, id, targetFolder) {
        var url = type === 'file' ? '/files/files/' + id + '/move' : '/files/folders/' + id + '/move';
        var fd = new FormData();
        var csrf = document.querySelector('meta[name="csrf-token"]');
        if (csrf) fd.append('_csrf', csrf.content);
        fd.append('target_id', targetFolder);
        fetch(url, { method: 'POST', body: fd, headers: { 'X-Requested-With': 'XMLHttpRequest' }, redirect: 'manual' })
            .then(function(){ location.reload(); })
            .catch(function(){ location.reload(); });
    }
})();

// ── Vorschau-Modal (Bilder & PDFs inline, mit Vor/Zurück) ──
(function () {
    var modal = document.getElementById('fePreviewModal');
    if (!modal) return;
    var imgEl   = document.getElementById('fePreviewImg');
    var frameEl = document.getElementById('fePreviewFrame');
    var videoEl = document.getElementById('fePreviewVideo');
    var audioEl = document.getElementById('fePreviewAudio');
    var nameEl  = document.getElementById('fePreviewName');
    var openEl  = document.getElementById('fePreviewOpen');
    var dlEl    = document.getElementById('fePreviewDownload');
    var prevEl  = modal.querySelector('.fe-preview-prev');
    var nextEl  = modal.querySelector('.fe-preview-next');
    var mediaEls = [imgEl, frameEl, videoEl, audioEl];

    var nodes = [];
    var index = 0;

    function dataFor(node) {
        return {
            url:  node.getAttribute('data-fe-view'),
            name: node.getAttribute('data-fe-view-name') || '',
            type: node.getAttribute('data-fe-view-type') || 'image',
            dl:   node.getAttribute('data-fe-view-download') || node.getAttribute('data-fe-view')
        };
    }
    function resetMedia() {
        mediaEls.forEach(function(el){ el.hidden = true; });
        imgEl.src = ''; frameEl.src = '';
        videoEl.pause && videoEl.pause(); videoEl.removeAttribute('src'); videoEl.load && videoEl.load();
        audioEl.pause && audioEl.pause(); audioEl.removeAttribute('src'); audioEl.load && audioEl.load();
    }
    function render() {
        var d = dataFor(nodes[index]);
        resetMedia();
        if (d.type === 'image') {
            imgEl.src = d.url; imgEl.alt = d.name; imgEl.hidden = false;
        } else if (d.type === 'video') {
            videoEl.src = d.url; videoEl.hidden = false;
        } else if (d.type === 'audio') {
            audioEl.src = d.url; audioEl.hidden = false;
        } else { // pdf, text
            frameEl.src = d.url; frameEl.hidden = false;
        }
        nameEl.textContent = d.name;
        openEl.href = d.url;
        dlEl.href   = d.dl;
        var multiple = nodes.length > 1;
        prevEl.style.display = multiple ? '' : 'none';
        nextEl.style.display = multiple ? '' : 'none';
    }
    function open(startNode) {
        // Nur sichtbare Trigger der aktiven Ansicht, dedupliziert nach URL
        // (Kachelansicht hat pro Datei zwei Trigger: Vorschaufläche + Button).
        var all = Array.prototype.slice.call(document.querySelectorAll('[data-fe-view]'))
            .filter(function(n){ return n.offsetParent !== null; });
        if (!all.length) all = Array.prototype.slice.call(document.querySelectorAll('[data-fe-view]'));
        var seen = {};
        nodes = [];
        all.forEach(function(n){
            var u = n.getAttribute('data-fe-view');
            if (!seen[u]) { seen[u] = 1; nodes.push(n); }
        });
        var startUrl = startNode.getAttribute('data-fe-view');
        index = nodes.findIndex(function(n){ return n.getAttribute('data-fe-view') === startUrl; });
        if (index < 0) index = 0;
        render();
        modal.removeAttribute('hidden');
        document.body.style.overflow = 'hidden';
    }
    function close() {
        modal.setAttribute('hidden', '');
        resetMedia();
        document.body.style.overflow = '';
    }
    function step(delta) {
        if (nodes.length < 2) return;
        index = (index + delta + nodes.length) % nodes.length;
        render();
    }

    document.addEventListener('click', function (e) {
        if (e.metaKey || e.ctrlKey || e.shiftKey || e.button !== 0) return;
        var node = e.target.closest('[data-fe-view]');
        if (!node) return;
        e.preventDefault();
        open(node);
    });
    prevEl.addEventListener('click', function () { step(-1); });
    nextEl.addEventListener('click', function () { step(1); });
    modal.querySelectorAll('[data-fe-preview-close]').forEach(function (el) {
        el.addEventListener('click', close);
    });
    document.addEventListener('keydown', function (e) {
        if (modal.hasAttribute('hidden')) return;
        if (e.key === 'Escape')          { close(); }
        else if (e.key === 'ArrowLeft')  { step(-1); }
        else if (e.key === 'ArrowRight') { step(1); }
    });
    window.feClosePreview = close;
})();

// ── Search ─────────────────────────────────────────────────
var feSearchTimer = null;
function feSearch(q) {
    clearTimeout(feSearchTimer);
    q = (q || '').trim();
    if (q.length < 2) { feCloseSearch(); return; }
    feSearchTimer = setTimeout(function() {
        fetch('/files/search?q=' + encodeURIComponent(q), { headers: { 'X-Requested-With': 'XMLHttpRequest' } })
            .then(function(r){ return r.json(); })
            .then(feRenderSearch)
            .catch(function(){ feCloseSearch(); });
    }, 220);
}
function feRenderSearch(data) {
    var box = document.getElementById('feSearchResults');
    var html = '';
    if (data.folders && data.folders.length) {
        html += '<div class="fe-sr-group">Ordner</div>';
        data.folders.forEach(function(f) {
            html += '<a class="fe-sr-item" href="/files/folder/' + f.id + '">'
                 + feSvgFolder() + '<span class="fe-sr-name">' + feEsc(f.name) + '</span>'
                 + '<span class="fe-sr-path">' + feEsc(f.path) + '</span></a>';
        });
    }
    if (data.files && data.files.length) {
        html += '<div class="fe-sr-group">Dateien</div>';
        data.files.forEach(function(f) {
            var href = f.folder_id ? '/files/folder/' + f.folder_id : '/files';
            html += '<a class="fe-sr-item" href="' + href + '">'
                 + '<span class="fe-sr-name">' + feEsc(f.name) + '</span>'
                 + '<span class="fe-sr-path">' + feEsc(f.path) + '</span></a>';
        });
    }
    if (!html) html = '<div class="fe-sr-empty">Keine Treffer.</div>';
    box.innerHTML = html;
    box.classList.add('open');
}
function feCloseSearch() { document.getElementById('feSearchResults').classList.remove('open'); }
function feSvgFolder() {
    return '<span class="fi fi-folder"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" width="16" height="16"><path d="M22 19a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h5l2 3h9a2 2 0 0 1 2 2z"/></svg></span>';
}
document.addEventListener('click', function(e) {
    if (!e.target.closest('.fe-search')) feCloseSearch();
});

// ── Drag & Drop ────────────────────────────────────────────
var feTarget = document.getElementById('feDropTarget');
if (feTarget) {
    feTarget.addEventListener('dragover', function(e) {
        e.preventDefault();
        document.getElementById('feDropzone')?.classList.add('dragover');
    });
    feTarget.addEventListener('dragleave', function(e) {
        if (!feTarget.contains(e.relatedTarget))
            document.getElementById('feDropzone')?.classList.remove('dragover');
    });
    feTarget.addEventListener('drop', function(e) {
        e.preventDefault();
        document.getElementById('feDropzone')?.classList.remove('dragover');
        if (e.dataTransfer.files.length) feUploadFiles(e.dataTransfer.files);
    });
}

// ── Upload ─────────────────────────────────────────────────
var feUploadPending = 0;
var feUploadOk = 0;
function feUploadFiles(fileList) {
    Array.from(fileList).forEach(function(file) { feUploadOne(file); });
    var inp = document.getElementById('feFileInput');
    if (inp) inp.value = '';
}

var feUploadSeq = 0;
function feUploadOne(file) {
    var seq     = ++feUploadSeq;
    var listEl  = document.getElementById('feUploadList');
    var barId   = 'fe-bar-' + seq;
    var stId    = 'fe-st-' + seq;
    var item    = document.createElement('div');
    item.className = 'fe-upload-item';
    item.innerHTML =
        '<span class="fe-upload-name">' + feEsc(file.name) + '</span>' +
        '<div class="fe-upload-bar-wrap"><div class="fe-upload-bar" id="' + barId + '"></div></div>' +
        '<span class="fe-upload-status" id="' + stId + '">0&thinsp;%</span>';
    listEl.prepend(item);

    // Absolutes Modul-Limit – größere Dateien werden gar nicht erst versucht
    if (feHardMax > 0 && file.size > feHardMax) {
        var stEl = document.getElementById(stId);
        document.getElementById(barId).style.background = '#dc2626';
        stEl.textContent = 'Zu groß (max. ' + feFmtBytes(feHardMax) + ')';
        stEl.className = 'fe-upload-status err';
        return;
    }

    feUploadPending++;
    var bar = document.getElementById(barId), st = document.getElementById(stId);
    function setPct(p) { bar.style.width = p + '%'; st.innerHTML = p + '&thinsp;%'; }
    function fail(msg) { bar.style.background = '#dc2626'; st.textContent = msg; st.className = 'fe-upload-status err'; feUploadDone(false); }
    function done()   { bar.style.background = '#16a34a'; bar.style.width = '100%'; st.textContent = 'OK'; st.className = 'fe-upload-status ok'; feUploadDone(true); }

    // Große Dateien stückweise hochladen (umgeht kleines post_max_size)
    if (file.size > feChunkSize) {
        feUploadChunked(file, setPct, done, fail);
    } else {
        feUploadSingle(file, setPct, done, fail);
    }
}

function feCsrf() { var m = document.querySelector('meta[name="csrf-token"]'); return m ? m.content : ''; }

function feUploadSingle(file, setPct, done, fail) {
    var fd = new FormData();
    fd.append('files[]', file);
    if (feCurrentFolder !== '') fd.append('folder_id', feCurrentFolder);
    fd.append('_csrf', feCsrf());

    var xhr = new XMLHttpRequest();
    xhr.open('POST', '/files/upload', true);
    xhr.setRequestHeader('X-Requested-With', 'XMLHttpRequest');
    xhr.upload.onprogress = function(e) { if (e.lengthComputable) setPct(Math.round(e.loaded / e.total * 100)); };
    xhr.onload = function() {
        if (xhr.status === 200) {
            try {
                var data = JSON.parse(xhr.responseText);
                var res  = data.results && data.results[0];
                if (res && res.ok) { done(); } else { fail((res && res.error) ? res.error : 'Fehler'); }
            } catch(e) { fail('Fehler'); }
        } else if (xhr.status === 413) { fail('Zu groß für Server'); }
        else { fail('HTTP ' + xhr.status); }
    };
    xhr.onerror = function() { fail('Netzwerkfehler'); };
    xhr.send(fd);
}

function feUploadChunked(file, setPct, done, fail) {
    var uploadId = feRandHex(24);
    var total = Math.ceil(file.size / feChunkSize);
    var idx = 0;
    function sendNext() {
        var start = idx * feChunkSize;
        var end   = Math.min(file.size, start + feChunkSize);
        var blob  = file.slice(start, end);
        var fd = new FormData();
        fd.append('_csrf', feCsrf());
        fd.append('upload_id', uploadId);
        fd.append('chunk_index', idx);
        fd.append('total_chunks', total);
        fd.append('filename', file.name);
        if (feCurrentFolder !== '') fd.append('folder_id', feCurrentFolder);
        fd.append('chunk', blob, file.name);

        var xhr = new XMLHttpRequest();
        xhr.open('POST', '/files/upload/chunk', true);
        xhr.setRequestHeader('X-Requested-With', 'XMLHttpRequest');
        xhr.onload = function() {
            if (xhr.status !== 200) { fail('HTTP ' + xhr.status); return; }
            var d;
            try { d = JSON.parse(xhr.responseText); } catch(e) { fail('Fehler'); return; }
            if (d.error) { fail(d.error); return; }
            idx++;
            setPct(Math.round(idx / total * 100));
            if (idx >= total) { done(); } else { sendNext(); }
        };
        xhr.onerror = function() { fail('Netzwerkfehler'); };
        xhr.send(fd);
    }
    sendNext();
}
function feUploadDone(ok) {
    if (ok) feUploadOk++;
    feUploadPending--;
    // Erst neu laden, wenn ALLE Uploads abgeschlossen sind (kein Race mehr)
    if (feUploadPending === 0 && feUploadOk > 0) {
        setTimeout(function() { location.reload(); }, 700);
    }
}

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