OpenNIT

Quelltext des aktuellen Standes

Fassung 1.1.1

index.php

120 KB
<?php
$this->layout('layouts/main', [
    'page_title'   => $page_title,
    'current_route'=> $current_route,
    'full_width'   => true,
]);
$teams          = $teams           ?? [];
$folders        = $folders         ?? [];
$teamFolders    = $team_folders    ?? [];
$allTeamFolders = $all_team_folders ?? [];
$entries        = $entries         ?? [];
$folderId       = $active_folder_id ?? null;
$teamId         = $active_team_id   ?? null;
$activeTeam     = $active_team      ?? null;
$searchQuery    = $search_query     ?? '';
$activeScope    = $active_scope     ?? null;
$recentIds      = $recent_ids       ?? [];
$folderCounts   = $folder_counts    ?? [];
$teamFolderCnt  = $team_folder_counts ?? [];
$canWriteTeam   = $can_write_active_team ?? false;
$showAll        = $show_all         ?? false;
$canCreate      = $can_create       ?? false;
?>
<style>
/* ---- Vault Layout ---- */
a, a:hover { text-decoration: none; }
.v-wrap { display:flex; flex-direction:column; height:calc(100vh - 60px); overflow:hidden; }

/* Body */
.v-body { flex:1; display:flex; overflow:hidden; }

/* Sidebar */
.v-sidebar { width:220px; flex-shrink:0; background:var(--slate-50); border-right:1px solid var(--gray-200);
    overflow-y:auto; display:flex; flex-direction:column; }
.v-sidebar-sec { padding:0.7rem 0.875rem 0.2rem; font-size:0.67rem; font-weight:700;
    text-transform:uppercase; letter-spacing:0.07em; color:var(--gray-400); }
.v-sidebar-item { display:flex; align-items:center; gap:0.4rem; padding:0.38rem 0.875rem;
    font-size:0.83rem; color:var(--gray-700); text-decoration:none; cursor:pointer; transition:background 0.1s;
    white-space:nowrap; overflow:hidden; text-overflow:ellipsis; border:none; background:none; width:100%; text-align:left; }
.v-sidebar-item:hover { background:var(--color-gray-200); }
.v-sidebar-item.active { background:var(--blue-100); color:var(--blue-700); font-weight:600; }
.v-sidebar-icon { width:14px; height:14px; flex-shrink:0; color:var(--gray-400); }
.v-sidebar-item.active .v-sidebar-icon { color:var(--blue-700); }
.v-sidebar-add { font-size:0.76rem; color:var(--gray-500); padding:0.15rem 0.875rem 0.55rem;
    display:flex; align-items:center; gap:0.3rem; cursor:pointer; text-decoration:none; }
.v-sidebar-add:hover { color:var(--blue-600); }
.v-sidebar-divider { height:1px; background:var(--gray-200); margin:0.4rem 0; }
.v-sidebar-folder { display:flex; align-items:center; }
.v-sidebar-folder .v-sidebar-item { flex:1; min-width:0; }
.v-folder-del { display:none; border:none; background:none; cursor:pointer; color:var(--gray-400); font-size:0.75rem; padding:0.2rem 0.4rem; border-radius:0.25rem; flex-shrink:0; line-height:1; }
.v-folder-del:hover { color:var(--red-600); background:var(--red-100); }
.v-sidebar-folder:hover .v-folder-del { display:block; }
.v-folder-manage { text-decoration:none; }
.v-folder-manage:hover { color:var(--blue-600); background:var(--blue-100); }
.v-sidebar-folder:hover .v-folder-manage { display:block; }

/* Main */
.v-main { flex:1; display:flex; flex-direction:column; overflow:hidden; }
.v-toolbar { display:flex; align-items:center; gap:0.75rem; padding:0.6rem 1.25rem;
    border-bottom:1px solid var(--gray-200); background:var(--color-bg); flex-shrink:0; }
.v-toolbar-title { font-size:0.82rem; font-weight:600; color:var(--gray-700); flex:1; }
.v-toolbar-tools { display:flex; align-items:center; gap:0.55rem; flex-shrink:0; }
/* Inline-Suche im Header (im Emoji-Modus wird <i> durch span.emoji-icon ersetzt –
   Selektor muss beide Varianten treffen, sonst rutscht das Icon über das Feld) */
.v-head-search { position:relative; display:inline-block; }
.v-head-search > i.fa-search,
.v-head-search > .emoji-icon:first-child { position:absolute; left:9px; top:50%; transform:translateY(-50%); color:var(--gray-400); font-size:0.75rem; line-height:1; pointer-events:none; }
.v-head-search input { width:200px; padding-left:1.7rem; }
/* Natives Lösch-X des Suchfelds ausblenden (sonst doppeltes X neben dem eigenen) */
.v-head-search input::-webkit-search-cancel-button { -webkit-appearance:none; appearance:none; }
.v-head-search-clear { position:absolute; right:7px; top:50%; transform:translateY(-50%); color:var(--gray-400); text-decoration:none; font-size:1.05rem; line-height:1; }
.v-head-search-clear:hover { color:var(--gray-500); }

/* Entries */
.v-entries { flex:1; overflow-y:auto; padding:0.875rem 1.25rem; }
.v-entry { display:flex; align-items:center; gap:0.75rem; padding:0.6rem 0.875rem;
    background:var(--color-bg); border:1px solid var(--gray-200); border-radius:0.6rem; margin-bottom:0.4rem;
    color:inherit; transition:box-shadow 0.1s, border-color 0.1s; }
.v-entry { cursor:pointer; }
.v-entry:hover { box-shadow:0 2px 8px rgba(0,0,0,0.06); border-color:var(--gray-300); }
.v-entry-actions a, .v-entry-actions button, .v-entry-actions form { position:relative; z-index:1; }
.v-favicon { width:26px; height:26px; border-radius:5px; background:var(--gray-100);
    display:flex; align-items:center; justify-content:center; flex-shrink:0; overflow:hidden; }
.v-favicon img { width:18px; height:18px; object-fit:contain; }
.v-favicon-ph { font-size:11px; color:var(--gray-400); font-weight:700; }
.v-entry-info { flex:1; min-width:0; }
.v-entry-title { font-size:0.875rem; font-weight:600; color:var(--gray-900);
    white-space:nowrap; overflow:hidden; text-overflow:ellipsis; }
.v-entry-sub { font-size:0.75rem; color:var(--gray-400);
    white-space:nowrap; overflow:hidden; text-overflow:ellipsis; }
.v-entry-badge { display:inline-block; font-size:0.6rem; font-weight:700;
    padding:0.1rem 0.3rem; border-radius:3px; margin-left:0.3rem; vertical-align:middle; }
.v-badge-totp { background:var(--amber-100); color:var(--amber-800); }
.v-badge-team { background:var(--violet-100); color:var(--violet-700); }
.v-entry-actions { display:flex; gap:0.3rem; flex-shrink:0; }
.v-empty { text-align:center; padding:2.5rem 1rem; color:var(--gray-400); }

/* Kleine Ordner-/Export-Modals: Basis aus app.css (.modal-overlay/.modal-card),
   lokal nur Zustands-Toggle und kompakte Breite */
.v-modal-overlay { display:none; }
.v-modal-overlay.open { display:flex; }
.v-modal-overlay .modal-card { max-width:400px; }

/* Mobile folder nav */
.v-mobile-nav { display:none; }

@media(max-width:768px){
    .v-sidebar { display:none; }
    .v-body { display:block; }
    /* Natürliches Seitenscrollen statt eingesperrter 100vh-Box */
    .v-wrap { height:auto; overflow:visible; }
    .v-body, .v-main, .v-table-scroll { overflow:visible; }
    .v-table-scroll .data-table thead th { position:static; box-shadow:none; }

    .v-mobile-nav {
        display:flex; align-items:center; gap:0.4rem; overflow-x:auto; padding:0.5rem 0.875rem;
        background:var(--color-bg); border-bottom:1px solid var(--gray-200); flex-shrink:0;
        scrollbar-width:none; -webkit-overflow-scrolling:touch;
    }
    .v-mobile-nav::-webkit-scrollbar { display:none; }
    .v-mobile-nav-item {
        display:inline-flex; align-items:center; gap:0.3rem; white-space:nowrap;
        padding:0.3rem 0.7rem; border-radius:9999px; font-size:0.78rem; font-weight:600;
        border:1px solid var(--gray-200); background:var(--color-bg); color:var(--gray-700); text-decoration:none;
        flex-shrink:0;
    }
    .v-mobile-nav-item.active { background:var(--color-primary-deep); color:var(--color-on-accent); border-color:var(--color-primary-deep); }
    .v-mobile-nav-sep { color:var(--gray-300); font-size:0.7rem; flex-shrink:0; }
}

/* ---- Redesign: KPI, Filter, Tabelle, Sidebar-Suche/Collapse ---- */
.v-head { flex-shrink:0; }
/* Sidebar-Suche + kollabierbare Sektionen */
.v-sidebar-search { padding:0.55rem 0.7rem 0.35rem; }
.v-sidebar-search input { width:100%; box-sizing:border-box; padding:0.32rem 0.55rem; font-size:0.78rem;
    border:1px solid var(--gray-300); border-radius:0.4rem; }
.v-sec-toggle { display:flex; align-items:center; gap:0.3rem; cursor:pointer; user-select:none; }
.v-sec-toggle .v-chev { margin-left:auto; transition:transform 0.15s; font-size:0.62rem; color:var(--gray-400); }
.v-sec.is-collapsed .v-chev { transform:rotate(-90deg); }
.v-sec.is-collapsed .v-sec-body { display:none; }
.v-side-hidden { display:none !important; }
/* Tresor-Liste als Tabelle */
.v-table-scroll { flex:1; overflow-y:auto; padding:0; }
.v-table-scroll .table-wrapper { border:0; border-radius:0; margin:0; box-shadow:none; }
.v-table-scroll .data-table { margin:0; }
.v-load-more { padding:0.7rem; }
/* Import/Export-Aufklappmenü (JS-frei, robust) */
.v-io-menu { position:relative; display:inline-block; }
.v-io-menu > summary { list-style:none; cursor:pointer; }
.v-io-menu > summary::-webkit-details-marker { display:none; }
.v-io-list { position:absolute; right:0; top:calc(100% + 4px); z-index:1100; min-width:200px;
    background:var(--color-bg); border:1px solid var(--gray-200); border-radius:0.45rem; box-shadow:0 10px 30px rgba(0,0,0,0.14); padding:0.25rem 0; }
.v-io-list a { display:flex; align-items:center; gap:0.55rem; padding:0.45rem 0.9rem; color:var(--gray-700); font-size:0.85rem; white-space:nowrap; text-decoration:none; }
.v-io-list a:hover { background:var(--gray-100); color:var(--gray-900); }
.content-header, .content-header-actions { overflow:visible; }
/* Sticky Tabellenkopf */
.v-table-scroll .data-table thead th { position:sticky; top:0; z-index:3; background:var(--slate-50); box-shadow:inset 0 -1px 0 var(--gray-200); }
/* Inline-Kopieren in der Zeile */
.v-cell { display:inline-flex; align-items:center; gap:0.35rem; max-width:100%; }
.v-cell-val { overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
.v-inline-copy { border:none; background:none; color:var(--color-gray-400); cursor:pointer; padding:0 0.15rem; font-size:0.78rem; opacity:0; transition:opacity 0.1s, color 0.1s; }
tr:hover .v-inline-copy { opacity:1; }
.v-inline-copy:hover { color:var(--blue-600); }
.v-inline-copy.copied { color:var(--green-600); opacity:1; }
/* Sidebar ein-/ausblenden */
.v-body.v-sidebar-collapsed .v-sidebar { display:none; }
.v-sidebar-toggle { border:1px solid var(--gray-200); background:var(--gray-50); color:var(--gray-500); border-radius:0.35rem; width:30px; height:26px; cursor:pointer; flex-shrink:0; }
.v-sidebar-toggle:hover { background:var(--slate-100); color:var(--gray-900); }
/* Mobile: Karten-Liste statt Tabelle (Tabelle wird per CSS in Karten transformiert) */
@media(max-width:640px){
    .v-head-search input { width:130px; }
    .v-table-scroll { padding:0.65rem 0.75rem; }
    .v-table-scroll .table-wrapper { border:none; background:none; box-shadow:none; overflow:visible; }
    /* app.css erzwingt mobil min-width:640px für .data-table – Karten brauchen volle Bildschirmbreite */
    #vTable, #vTable tbody { display:block; width:100%; min-width:0 !important; }
    #vTable thead { display:none; }
    #vTable tr.v-row { display:flex; flex-wrap:wrap; align-items:center; gap:0.35rem 0.6rem;
        background:var(--color-bg); border:1px solid var(--gray-200); border-radius:0.65rem; margin-bottom:0.5rem;
        padding:0.65rem 0.75rem; box-shadow:0 1px 2px rgba(0,0,0,0.04); }
    #vTable td { display:block; border:none; padding:0; background:none; }
    /* Zeile 1: Favicon + Titel + Badges (volle Breite) */
    #vTable td:first-child { flex:1 1 100%; min-width:0; }
    /* Adresse/Bereich/Geändert entfallen auf Mobile */
    #vTable td:nth-child(3), #vTable td:nth-child(4), #vTable td:nth-child(5) { display:none; }
    /* Zeile 2: Benutzername links, Aktionen rechts */
    #vTable td:nth-child(2) { flex:1 1 auto; min-width:0; font-size:0.8rem; }
    #vTable td.v-actions-cell { margin-left:auto; display:flex; align-items:center; gap:0.4rem; }
    /* Größere Tap-Targets */
    #vTable td.v-actions-cell .btn { width:36px; height:34px; padding:0; display:inline-flex;
        align-items:center; justify-content:center; font-size:0.85rem; }
    .v-inline-copy { opacity:1; padding:0.35rem 0.45rem; color:var(--gray-400); }
    /* Toolbar entschlacken: Sidebar-/Dichte-Umschalter entfallen, Rest darf umbrechen */
    .v-toolbar { flex-wrap:wrap; row-gap:0.4rem; padding:0.5rem 0.75rem; }
    .v-sidebar-toggle, #vDensity { display:none; }
}
.v-ttitle { display:flex; align-items:center; gap:0.55rem; min-width:0; }
.v-sel { width:15px; height:15px; cursor:pointer; flex-shrink:0; accent-color:var(--blue-600); }
.v-badge-expired { background:var(--red-100); color:var(--red-700); }
.v-badge-expiring { background:var(--orange-100); color:var(--orange-700); }
tr.v-row.v-kbd-active > td { background:var(--blue-50) !important; }
tr.v-row.v-dragging { opacity:0.5; }
.v-sidebar-item.v-drop-hover { background:var(--blue-100); outline:2px dashed var(--blue-600); outline-offset:-2px; }
.v-recent { display:flex; align-items:center; gap:0.4rem; padding:0.45rem 1.25rem 0; flex-wrap:wrap; }
.v-recent-label { font-size:0.7rem; font-weight:700; text-transform:uppercase; letter-spacing:0.05em; color:var(--gray-400); }
.v-recent-chip { display:inline-flex; align-items:center; gap:0.3rem; padding:0.2rem 0.6rem; border-radius:9999px;
    border:1px solid var(--gray-200); background:var(--color-bg); color:var(--gray-700); font-size:0.76rem; cursor:pointer; }
.v-recent-chip:hover { border-color:var(--blue-300); background:var(--blue-50); }
.v-chev-btn { border:none; background:none; cursor:pointer; color:var(--gray-400); font-size:0.6rem; padding:0 0.25rem; flex-shrink:0; }
.v-fcount { margin-left:auto; font-size:0.68rem; color:var(--gray-400); flex-shrink:0; }
.v-ttitle .v-entry-title { font-weight:600; font-size:0.875rem; }
.v-tsub { color:var(--gray-500); }
.data-table.vlt-dense td, .data-table.vlt-dense th { padding-top:0.28rem; padding-bottom:0.28rem; }
.data-table td.v-actions-cell { white-space:nowrap; text-align:right; }
.vlt-hidden-row { display:none !important; }
.v-load-more { text-align:center; padding:0.6rem 0 0.2rem; }
</style>

<div class="v-wrap">
    <!-- Content Header -->
    <div class="content-header" style="flex-shrink:0;">
        <div>
            <h1><?= $this->te('Passwörter') ?></h1>
            <ol class="breadcrumb">
                <li class="breadcrumb-item"><a href="/"><?= $this->te('Home') ?></a></li>
                <li class="breadcrumb-item active"><?= $this->te('Vault') ?></li>
            </ol>
        </div>
        <div class="content-header-actions">
            <form method="GET" action="/vault" class="v-head-search">
                <i class="fas fa-search"></i>
                <input type="search" name="q" id="vSearchInput" class="form-control form-control-sm" placeholder="<?= $this->te('Durchsuchen…') ?>"
                       value="<?= $this->e($searchQuery) ?>" autocomplete="off" data-act-input="vaultSearchAusFeld">
                <a href="#" class="v-head-search-clear" id="vSearchClear" title="<?= $this->te('Zurücksetzen') ?>"
                   style="<?= $searchQuery === '' ? 'display:none;' : '' ?>" data-act="vaultSearchClear">&times;</a>
            </form>
            <button type="button" class="btn btn-primary btn-sm" data-act="openCreateModal"><i class="fas fa-plus"></i> <?= $this->te('Neuer Eintrag') ?></button>
            <details class="v-io-menu">
                <summary class="btn btn-default btn-sm"><i class="fas fa-exchange-alt"></i> <?= $this->te('Import/Export') ?> <i class="fas fa-caret-down"></i></summary>
                <div class="v-io-list">
                    <a href="/vault/import<?= $teamId !== null ? '?team_id=' . (int)$teamId : '' ?>"><i class="fas fa-file-import"></i> <?= $this->te('Importieren (CSV/KDBX)') ?></a>
                    <a href="/vault/export"><i class="fas fa-file-export"></i> <?= $this->te('Exportieren (CSV)') ?></a>
                    <a href="#" data-open="kdbxExportModal"><i class="fas fa-file-archive"></i> <?= $this->te('Exportieren (KDBX, verschlüsselt)') ?></a>
                </div>
            </details>
            <a href="/vault/pin" class="btn btn-default btn-sm" title="<?= $this->te('Tresor-PIN / Master-Passwort') ?>"><i class="fas fa-lock"></i> PIN</a>
        </div>
    </div>

    <!-- Mobile folder navigation (only visible on small screens) -->
    <div class="v-mobile-nav">
        <a href="/vault" class="v-mobile-nav-item <?= $teamId === null && $folderId === null && $searchQuery === '' && $activeScope === null ? 'active' : '' ?>"><?= $this->te('Alle') ?></a>
        <a href="/vault?scope=personal" class="v-mobile-nav-item <?= $activeScope === 'personal' ? 'active' : '' ?>"><?= $this->te('Persönlich') ?></a>
        <?php foreach ($folders as $folder): ?>
            <a href="/vault?folder=<?= $folder['id'] ?>"
               class="v-mobile-nav-item <?= ($teamId === null && $folderId === (int)$folder['id']) ? 'active' : '' ?>">
                <?= $this->e($folder['name']) ?>
            </a>
        <?php endforeach; ?>
        <?php if (!empty($teams)): ?>
            <span class="v-mobile-nav-sep">|</span>
            <?php foreach ($teams as $team): ?>
                <a href="/vault?team_id=<?= $team['id'] ?>"
                   class="v-mobile-nav-item <?= ($teamId === (int)$team['id'] && $folderId === null) ? 'active' : '' ?>">
                    <?= $this->e($team['name']) ?>
                </a>
                <?php if ($teamId === (int)$team['id']): ?>
                    <?php foreach ($teamFolders as $tf): ?>
                        <a href="/vault?team_id=<?= $team['id'] ?>&folder=<?= $tf['id'] ?>"
                           class="v-mobile-nav-item <?= ($folderId === (int)$tf['id']) ? 'active' : '' ?>">
                            › <?= $this->e($tf['name']) ?>
                        </a>
                    <?php endforeach; ?>
                <?php endif; ?>
            <?php endforeach; ?>
        <?php endif; ?>
        <?php if ($teamId !== null): ?>
        <a href="#" class="v-mobile-nav-item"
           data-open="teamFolderModal"><?= $this->te('+ Ordner') ?></a>
        <?php else: ?>
        <a href="#" class="v-mobile-nav-item"
           data-open="folderModal"><?= $this->te('+ Ordner') ?></a>
        <?php endif; ?>
    </div>

    <!-- Body -->
    <div class="v-body">
        <!-- Sidebar -->
        <aside class="v-sidebar">
            <?php if (!empty($folders) || !empty($teams)): ?>
            <div class="v-sidebar-search">
                <input type="search" id="vSideSearch" placeholder="<?= $this->te('Ordner/Team filtern…') ?>" autocomplete="off" data-act-input="vaultSeitenfilterAusFeld">
            </div>
            <?php endif; ?>

            <div class="v-sec" id="vSecPersonal">
                <div class="v-sidebar-sec v-sec-toggle" data-toggle-parent="is-collapsed">
                    <?= $this->te('Mein Tresor') ?> <span class="v-chev">▾</span>
                </div>
                <div class="v-sec-body">
                    <a href="/vault"
                       class="v-sidebar-item <?= $teamId === null && $folderId === null && $searchQuery === '' && $activeScope === null ? 'active' : '' ?>">
                        <svg class="v-sidebar-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
                            <rect x="3" y="3" width="18" height="18" rx="2"/><path d="M3 9h18"/>
                        </svg>
                        <?= $this->te('Alle Einträge') ?>
                    </a>

                    <a href="/vault?scope=personal"
                       class="v-sidebar-item <?= $activeScope === 'personal' ? 'active' : '' ?>"
                       title="<?= $this->te('Nur persönliche Einträge (ohne Team-Tresore)') ?>">
                        <svg class="v-sidebar-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
                            <circle cx="12" cy="8" r="4"/><path d="M4 20c0-4 3.6-7 8-7s8 3 8 7"/>
                        </svg>
                        <?= $this->te('Meine Einträge') ?>
                    </a>

                    <?php foreach ($folders as $folder): ?>
                        <div class="v-sidebar-folder js-side-item">
                            <a href="/vault?folder=<?= $folder['id'] ?>"
                               class="v-sidebar-item <?= ($teamId === null && $folderId === (int)$folder['id']) ? 'active' : '' ?>"
                               style="padding-left:<?= 0.875 + 0.9 * (int)($folder['depth'] ?? 0) ?>rem;"
                               title="<?= $this->e($folder['path_label'] ?? $folder['name']) ?>">
                                <svg class="v-sidebar-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
                                    <path d="M3 7a2 2 0 0 1 2-2h4l2 2h8a2 2 0 0 1 2 2v9a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"/>
                                </svg>
                                <?= $this->e($folder['name']) ?>
                                <?php if (!empty($folderCounts[(int)$folder['id']])): ?><span class="v-fcount"><?= (int)$folderCounts[(int)$folder['id']] ?></span><?php endif; ?>
                            </a>
                            <form method="POST" action="/vault/folders/<?= $folder['id'] ?>/delete" style="margin:0;"
                                  data-confirm="Ordner löschen? Einträge bleiben erhalten.">
                                <input type="hidden" name="_csrf" value="<?= $this->csrf() ?>">
                                <button type="submit" class="v-folder-del" title="<?= $this->te('Ordner löschen') ?>">✕</button>
                            </form>
                        </div>
                    <?php endforeach; ?>

                    <a href="#" class="v-sidebar-add"
                       data-open="folderModal">
                        <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5"><path d="M12 5v14M5 12h14"/></svg>
                        <?= $this->te('Ordner anlegen') ?>
                    </a>
                </div>
            </div>

            <?php if (!empty($teams)): ?>
                <div class="v-sidebar-divider"></div>
                <div class="v-sec" id="vSecTeams">
                    <div class="v-sidebar-sec v-sec-toggle" data-toggle-parent="is-collapsed">
                        <?= $this->te('Teams') ?> <span style="font-weight:600;color:var(--gray-400);">(<?= count($teams) ?>)</span> <span class="v-chev">▾</span>
                    </div>
                    <div class="v-sec-body">
                    <?php foreach ($teams as $team): ?>
                        <div class="v-sidebar-folder js-side-item">
                        <a href="/vault?team_id=<?= $team['id'] ?>"
                           class="v-sidebar-item <?= ($teamId === (int)$team['id'] && $folderId === null) ? 'active' : '' ?>">
                            <svg class="v-sidebar-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
                                <path d="M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"/><circle cx="9" cy="7" r="4"/>
                                <path d="M23 21v-2a4 4 0 0 0-3-3.87M16 3.13a4 4 0 0 1 0 7.75"/>
                            </svg>
                            <?= $this->e($team['name']) ?>
                            <?php if (!(bool)($team['tvk_ready'] ?? false)): ?>
                                <span class="status-pill status-pill--warning" style="margin-left:auto;font-size:0.6rem;padding:0.05rem 0.35rem;"><?= $this->te('Pending') ?></span>
                            <?php endif; ?>
                        </a>
                        <a href="/vault/teams/<?= (int)$team['id'] ?>" class="v-folder-del v-folder-manage"
                           title="<?= $this->te('Team-Tresor verwalten (Ordner, Mitglieder)') ?>"><i class="fas fa-cog"></i></a>
                        </div>

                        <?php if ($teamId === (int)$team['id']): ?>
                            <?php foreach ($teamFolders as $tf): ?>
                                <div class="v-sidebar-folder">
                                <a href="/vault?team_id=<?= $team['id'] ?>&folder=<?= $tf['id'] ?>"
                                   class="v-sidebar-item <?= ($folderId === (int)$tf['id']) ? 'active' : '' ?>"
                                   style="padding-left:<?= 1.75 + 0.9 * (int)($tf['depth'] ?? 0) ?>rem;"
                                   title="<?= $this->e($tf['path_label'] ?? $tf['name']) ?>">
                                    <svg class="v-sidebar-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
                                        <path d="M3 7a2 2 0 0 1 2-2h4l2 2h8a2 2 0 0 1 2 2v9a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"/>
                                    </svg>
                                    <?= $this->e($tf['name']) ?>
                                    <?php if (!empty($teamFolderCnt[(int)$tf['id']])): ?><span class="v-fcount"><?= (int)$teamFolderCnt[(int)$tf['id']] ?></span><?php endif; ?>
                                </a>
                                <?php if ($canWriteTeam): ?>
                                <button type="button" class="v-folder-del v-folder-manage" title="<?= $this->te('Ordner verschieben') ?>"
                                        data-act="openTeamFolderMove" data-args='[<?= (int)$tf['id'] ?>]'><i class="fas fa-exchange-alt"></i></button>
                                <form method="POST" action="/vault/teams/<?= (int)$team['id'] ?>/folders/<?= (int)$tf['id'] ?>/delete" style="margin:0;"
                                      data-confirm="Team-Ordner löschen? Einträge bleiben erhalten.">
                                    <input type="hidden" name="_csrf" value="<?= $this->csrf() ?>">
                                    <input type="hidden" name="_return" value="vault">
                                    <button type="submit" class="v-folder-del" title="<?= $this->te('Team-Ordner löschen') ?>">✕</button>
                                </form>
                                <?php endif; ?>
                                </div>
                            <?php endforeach; ?>
                            <a href="#" class="v-sidebar-add" style="padding-left:1.75rem;"
                               data-open="teamFolderModal">
                                <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5"><path d="M12 5v14M5 12h14"/></svg>
                                <?= $this->te('Ordner anlegen') ?>
                            </a>
                        <?php endif; ?>
                    <?php endforeach; ?>
                    </div>
                </div>
            <?php endif; ?>

            <div class="v-sidebar-divider"></div>
            <div class="v-sec">
                <div class="v-sidebar-sec"><?= $this->te('Werkzeuge') ?></div>
                <a href="/vault/health" class="v-sidebar-item">
                    <svg class="v-sidebar-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M22 12h-4l-3 9L9 3l-3 9H2"/></svg>
                    <?= $this->te('Passwort-Gesundheit') ?>
                </a>
                <a href="/vault/trash" class="v-sidebar-item">
                    <svg class="v-sidebar-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="3 6 5 6 21 6"/><path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"/></svg>
                    <?= $this->te('Papierkorb') ?>
                </a>
            </div>
        </aside>

        <!-- Main content -->
        <div class="v-main">
            <div class="v-toolbar">
                <button type="button" class="v-sidebar-toggle" title="<?= $this->te('Seitenleiste ein-/ausblenden') ?>" data-act="vaultToggleSidebar"><i class="fas fa-bars"></i></button>
                <span class="v-toolbar-title">
                    <?php if ($searchQuery !== ''): ?>
                        <i class="fas fa-search" style="color:var(--gray-400);"></i> <?= $this->te('Suchergebnisse') ?>
                    <?php elseif ($activeTeam): ?>
                        <i class="fas fa-users" style="color:var(--gray-400);"></i> <?= $this->e($activeTeam['name']) ?>
                    <?php elseif ($folderId): ?>
                        <i class="fas fa-folder" style="color:var(--gray-400);"></i> <?= $this->te('Ordner') ?>
                    <?php elseif ($activeScope === 'personal'): ?>
                        <i class="fas fa-user" style="color:var(--gray-400);"></i> <?= $this->te('Meine Einträge') ?>
                    <?php else: ?>
                        <i class="fas fa-key" style="color:var(--gray-400);"></i> <?= $this->te('Alle Einträge') ?>
                    <?php endif; ?>
                    <span id="vCount" style="font-weight:400;color:var(--gray-500);margin-left:0.3rem;">(<?= count($entries) ?>)</span>
                </span>
                <div id="vBulkBar" style="display:none;align-items:center;gap:0.45rem;flex-shrink:0;">
                    <span id="vBulkCount" style="font-size:0.78rem;font-weight:700;color:var(--blue-700);white-space:nowrap;"></span>
                    <button type="button" class="btn btn-default btn-xs" data-act="vaultBulkMove"><i class="fas fa-exchange-alt"></i> <?= $this->te('Verschieben') ?></button>
                    <button type="button" class="btn btn-danger btn-xs" data-act="vaultBulkDelete"><i class="fas fa-trash"></i> <?= $this->te('Löschen') ?></button>
                    <button type="button" class="btn btn-default btn-xs" title="<?= $this->te('Auswahl aufheben') ?>" data-act="vaultSelAll" data-args='[false]'>&times;</button>
                </div>
                <div class="v-toolbar-tools">
                    <label class="mb-0" style="display:inline-flex;align-items:center;gap:0.3rem;font-size:0.78rem;white-space:nowrap;color:var(--gray-500);">
                        <input type="checkbox" id="vTotpOnly" data-act-change="vaultApplyTable"> <?= $this->te('nur 2FA') ?>
                    </label>
                    <select id="vSort" class="form-control form-control-sm" style="width:auto;" title="<?= $this->te('Sortierung') ?>" data-act-change="vaultApplyTable">
                        <option value="title-asc"><?= $this->te('Titel A–Z') ?></option>
                        <option value="title-desc"><?= $this->te('Titel Z–A') ?></option>
                        <option value="user-asc"><?= $this->te('Benutzer A–Z') ?></option>
                    </select>
                    <button type="button" id="vDensity" class="btn btn-default btn-xs" title="<?= $this->te('Kompakte Ansicht umschalten') ?>" data-act="vaultToggleDensity"><i class="fas fa-compress"></i></button>
                </div>
            </div>

            <?php
            // „Zuletzt verwendet"-Chips (nur in der Alle-Einträge-Ansicht)
            $recentChips = [];
            if ($showAll && !empty($recentIds)) {
                $byId = [];
                foreach ($entries as $e) { $byId[(int)$e['id']] = $e; }
                foreach ($recentIds as $rid => $usedAt) {
                    if (isset($byId[$rid])) { $recentChips[] = $byId[$rid]; }
                    if (count($recentChips) >= 8) { break; }
                }
            }
            ?>
            <?php if (!empty($recentChips)): ?>
            <div class="v-recent">
                <span class="v-recent-label"><i class="fas fa-history" style="margin-right:0.2rem;"></i><?= $this->te('Zuletzt:') ?></span>
                <?php foreach ($recentChips as $rc):
                    $rcTeam = $rc['team_id'] !== null ? (int)$rc['team_id'] : null;
                    $rcBase = $rcTeam !== null ? '/vault/teams/' . $rcTeam . '/entries/' . (int)$rc['id'] : '/vault/entries/' . (int)$rc['id'];
                ?>
                <button type="button" class="v-recent-chip"
                        data-act="openEntryModal" data-args='["<?= $this->e($rcBase) ?>?json=1","<?= $this->e($rcBase) ?>/edit","<?= $this->e($rcBase) ?>/delete"]'>
                    <?= $this->e(mb_strimwidth((string)$rc['title'], 0, 28, '…')) ?>
                </button>
                <?php endforeach; ?>
            </div>
            <?php endif; ?>

            <div class="v-table-scroll">
                <?php if (empty($entries)): ?>
                    <div class="empty-state">
                        <div class="empty-icon"><i class="fas fa-lock"></i></div>
                        <?php if ($searchQuery): ?>
                            <?= $this->t('Keine Treffer für „{suche}"', ['suche' => $this->e($searchQuery)]) ?>
                        <?php else: ?>
                            <div><?= $this->te('Keine Einträge') ?></div>
                            <a href="/vault/entries/create" class="btn btn-primary btn-sm" style="margin-top:0.6rem;"><i class="fas fa-plus"></i> <?= $this->te('Ersten Eintrag anlegen') ?></a>
                        <?php endif; ?>
                    </div>
                <?php else: ?>
                    <div class="table-wrapper">
                        <table class="data-table" id="vTable">
                            <thead>
                                <tr>
                                    <th scope="col"><span style="display:inline-flex;align-items:center;gap:0.55rem;"><input type="checkbox" id="vSelAll" class="v-sel" title="<?= $this->te('Alle (gefilterten) auswählen') ?>" data-act="vaultAlleAusKaestchen"> <?= $this->te('Titel') ?></span></th>
                                    <th scope="col"><?= $this->te('Benutzername') ?></th>
                                    <th scope="col"><?= $this->te('Adresse') ?></th>
                                    <th scope="col"><?= $this->te('Bereich') ?></th>
                                    <th scope="col"><?= $this->te('Geändert') ?></th>
                                    <th scope="col" class="text-right"><?= $this->te('Aktionen') ?></th>
                                </tr>
                            </thead>
                            <tbody id="vTbody">
                            <?php foreach ($entries as $entry):
                                $isTeamEntry = !empty($entry['team_id']) || ($entry['_source'] ?? '') === 'team';
                                $entryTeamId = $entry['_team_id'] ?? ($entry['team_id'] ?? null);

                                if ($isTeamEntry && $entryTeamId) {
                                    $showUrl   = '/vault/teams/' . $entryTeamId . '/entries/' . $entry['id'];
                                    $editUrl   = '/vault/teams/' . $entryTeamId . '/entries/' . $entry['id'] . '/edit';
                                    $deleteUrl = '/vault/teams/' . $entryTeamId . '/entries/' . $entry['id'] . '/delete';
                                } else {
                                    $showUrl   = '/vault/entries/' . $entry['id'];
                                    $editUrl   = '/vault/entries/' . $entry['id'] . '/edit';
                                    $deleteUrl = '/vault/entries/' . $entry['id'] . '/delete';
                                }
                                $jsonUrl = $showUrl . '?json=1';
                                $_avTitle = $entry['title'] ?? '?';
                                $_avHue   = crc32((string)$_avTitle) % 360;
                                $_user    = (string)($entry['username'] ?? '');
                                $_url     = (string)($entry['url'] ?? '');
                                $_favDom  = strtolower(trim((string)($entry['favicon_domain'] ?? '')));
                                $_favUrl  = ($favicon_urls ?? [])[$_favDom] ?? null;
                            ?>
                                <tr class="clickable v-row" draggable="true"
                                    data-id="<?= (int)$entry['id'] ?>"
                                    data-title="<?= $this->e(mb_strtolower((string)$_avTitle)) ?>"
                                    data-user="<?= $this->e(mb_strtolower($_user)) ?>"
                                    data-url="<?= $this->e(mb_strtolower($_url)) ?>"
                                    data-totp="<?= !empty($entry['has_totp']) ? '1' : '0' ?>"
                                    data-folder="<?= $entry['folder_id'] !== null ? (int)$entry['folder_id'] : '' ?>"
                                    data-teamid="<?= $entry['team_id'] !== null ? (int)$entry['team_id'] : '' ?>"
                                    data-expired="<?= (!empty($entry['expires_at']) && strtotime((string)$entry['expires_at']) < time()) ? '1' : '0' ?>"
                                    data-act="openEntryModal" data-args='["<?= $this->e($jsonUrl) ?>","<?= $this->e($editUrl) ?>","<?= $this->e($deleteUrl) ?>"]'>
                                    <td>
                                        <div class="v-ttitle">
                                            <input type="checkbox" class="v-sel v-sel-row" value="<?= (int)$entry['id'] ?>"
                                                   data-act="vaultSelChanged">
                                            <?php if ($_favDom !== ''): ?>
                                            <span class="v-favicon">
                                                <img src="/vault/entries/<?= (int)$entry['id'] ?>/favicon" alt="" loading="lazy"
                                                     data-favicon-fallback>
                                                <span class="v-favicon-ph" hidden data-bg="hsl(<?= $_avHue ?>,52%,90%)"
                                                      style="color:hsl(<?= $_avHue ?>,55%,38%);"><?= $this->e(mb_strtoupper(mb_substr((string)$_avTitle, 0, 1))) ?></span>
                                            </span>
                                            <?php else: ?>
                                            <span class="v-favicon" style="background:hsl(<?= $_avHue ?>,52%,90%);">
                                                <span class="v-favicon-ph" style="color:hsl(<?= $_avHue ?>,55%,38%);"><?= $this->e(mb_strtoupper(mb_substr((string)$_avTitle, 0, 1))) ?></span>
                                            </span>
                                            <?php endif; ?>
                                            <span class="v-entry-title"><?= $this->e($entry['title']) ?></span>
                                            <?php if (!empty($entry['has_totp'])): ?>
                                                <span class="v-entry-badge v-badge-totp">2FA</span>
                                            <?php endif; ?>
                                            <?php if (!empty($entry['expires_at'])):
                                                $_expTs = strtotime((string)$entry['expires_at']); ?>
                                                <?php if ($_expTs !== false && $_expTs < time()): ?>
                                                    <span class="v-entry-badge v-badge-expired" title="<?= $this->te('Abgelaufen am {datum}', ['datum' => date('d.m.Y', $_expTs)]) ?>"><?= $this->te('Abgelaufen') ?></span>
                                                <?php elseif ($_expTs !== false && $_expTs < time() + 14 * 86400): ?>
                                                    <span class="v-entry-badge v-badge-expiring" title="<?= $this->te('Läuft ab am {datum}', ['datum' => date('d.m.Y', $_expTs)]) ?>"><?= $this->te('Läuft ab') ?></span>
                                                <?php endif; ?>
                                            <?php endif; ?>
                                        </div>
                                    </td>
                                    <td class="v-tsub"><?php if ($_user !== ''): ?><span class="v-cell"><span class="v-cell-val"><?= $this->e($_user) ?></span><button type="button" class="v-inline-copy" data-copy="<?= $this->e($_user) ?>" title="<?= $this->te('Benutzername kopieren') ?>" ><i class="fas fa-copy"></i></button></span><?php else: ?>—<?php endif; ?></td>
                                    <td class="v-tsub" style="max-width:240px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;" title="<?= $this->e($_url) ?>"><?= $_url !== '' ? $this->e($_url) : '—' ?></td>
                                    <td>
                                        <?php if ($isTeamEntry && !empty($entry['_team_name'])): ?>
                                            <span class="v-entry-badge v-badge-team"><?= $this->e($entry['_team_name']) ?></span>
                                        <?php elseif ($isTeamEntry): ?>
                                            <span class="v-entry-badge v-badge-team"><?= $this->te('Team') ?></span>
                                        <?php else: ?>
                                            <span class="v-tsub"><?= $this->te('Persönlich') ?></span>
                                        <?php endif; ?>
                                    </td>
                                    <td class="v-tsub" style="white-space:nowrap;"><?= !empty($entry['updated_at']) ? date('d.m.Y', strtotime((string)$entry['updated_at'])) : '—' ?></td>
                                    <td class="v-actions-cell" data-stop>
                                        <button type="button" class="btn btn-xs btn-default" title="<?= $this->te('Passwort kopieren') ?>"
                                                data-act="vaultCopyPassword" data-args='[<?= (int)$entry['id'] ?>]'><i class="fas fa-key"></i></button>
                                        <button type="button" class="btn btn-xs btn-default" title="<?= $this->te('Aktionen') ?>"
                                                data-json="<?= $this->e($jsonUrl) ?>" data-edit="<?= $this->e($editUrl) ?>"
                                                data-del="<?= $this->e($deleteUrl) ?>" data-id="<?= (int)$entry['id'] ?>"
                                                data-team="<?= $entry['team_id'] !== null ? (int)$entry['team_id'] : '' ?>"
                                                data-folder="<?= $entry['folder_id'] !== null ? (int)$entry['folder_id'] : '' ?>"
                                                data-act="vaultRowMenu"><i class="fas fa-ellipsis-v"></i></button>
                                    </td>
                                </tr>
                            <?php endforeach; ?>
                            </tbody>
                        </table>
                    </div>
                    <div class="v-load-more" id="vLoadMore" style="display:none;">
                        <button type="button" class="btn btn-default btn-sm" data-act="vaultShowMore"><?= $this->te('Mehr anzeigen') ?></button>
                    </div>
                <?php endif; ?>
            </div>
        </div>
    </div>
</div>

<script<?= $this->scriptNonce() ?>>
/* Tresor-Liste: Sortierung, 2FA-Filter, Dichte, Paginierung, Sidebar-Filter (clientseitig) */
(function () {
    var PAGE = 50;
    var vPageLimit = PAGE;
    var vQuery = '';
    var vQueryTimer = null;

    // Passt eine Zeile zu 2FA-Filter + Livesuche (Titel/Benutzer/URL)?
    function vRowMatches(r) {
        var totpOnly = document.getElementById('vTotpOnly') && document.getElementById('vTotpOnly').checked;
        if (totpOnly && r.getAttribute('data-totp') !== '1') return false;
        if (vQuery === '') return true;
        return (r.getAttribute('data-title') || '').indexOf(vQuery) !== -1
            || (r.getAttribute('data-user')  || '').indexOf(vQuery) !== -1
            || (r.getAttribute('data-url')   || '').indexOf(vQuery) !== -1;
    }

    window.vaultApplyTable = function () {
        var tbody = document.getElementById('vTbody');
        if (!tbody) return;
        var rows = Array.prototype.slice.call(tbody.querySelectorAll('tr.v-row'));
        var sortEl = document.getElementById('vSort');
        var sort = sortEl ? sortEl.value : 'title-asc';
        var key = sort.indexOf('user') === 0 ? 'user' : 'title';
        var desc = sort.indexOf('desc') !== -1;
        rows.sort(function (a, b) {
            var av = a.getAttribute('data-' + key) || '', bv = b.getAttribute('data-' + key) || '';
            var cmp = av.localeCompare(bv, 'de');
            return desc ? -cmp : cmp;
        });
        rows.forEach(function (r) { tbody.appendChild(r); });
        var shown = 0, matched = 0;
        rows.forEach(function (r) {
            if (!vRowMatches(r)) { r.classList.add('vlt-hidden-row'); return; }
            matched++;
            if (shown < vPageLimit) { r.classList.remove('vlt-hidden-row'); shown++; }
            else { r.classList.add('vlt-hidden-row'); }
        });
        var cnt = document.getElementById('vCount'); if (cnt) cnt.textContent = '(' + matched + ')';
        var lm = document.getElementById('vLoadMore'); if (lm) lm.style.display = (matched > shown) ? '' : 'none';
    };
    window.vaultShowMore = function () { vPageLimit += PAGE; vaultApplyTable(); };
    window.vaultRowMatchesFilter = vRowMatches;

    // Livesuche: filtert die geladene Liste beim Tippen (Titel/Benutzer/URL).
    // Enter sendet weiterhin die serverseitige Suche (findet auch Notizen
    // und durchsucht immer persönlich + alle Teams).
    window.vaultSearchInput = function (q) {
        var clear = document.getElementById('vSearchClear');
        if (clear) clear.style.display = (q || '') !== '' ? '' : 'none';
        clearTimeout(vQueryTimer);
        vQueryTimer = setTimeout(function () {
            vQuery = (q || '').toLowerCase().trim();
            vPageLimit = PAGE;
            vaultApplyTable();
        }, 150);
    };
    // Der Verteiler übergibt das Element; diese Felder reichten früher ihren Wert.
    window.vaultSearchAusFeld = function (el) { vaultSearchInput(el.value); };
    window.vaultSeitenfilterAusFeld = function (el) { vaultFilterSidebar(el.value); };
    window.vaultAlleAusKaestchen = function (el) { vaultSelAll(el.checked); };

    window.vaultSearchClear = function () {
        // Aktive Server-Suche (?q=…) → komplett zurück zur Übersicht
        try {
            if (new URLSearchParams(window.location.search).get('q')) {
                window.location.href = '/vault';
                return false;
            }
        } catch (e) {}
        var inp = document.getElementById('vSearchInput');
        if (inp) { inp.value = ''; inp.focus(); }
        vaultSearchInput('');
        return false;
    };
    window.vaultToggleDensity = function () {
        var t = document.getElementById('vTable'); if (!t) return;
        t.classList.toggle('vlt-dense');
        try { localStorage.setItem('vaultDense', t.classList.contains('vlt-dense') ? '1' : '0'); } catch (e) {}
    };
    window.vaultFilterSidebar = function (q) {
        q = (q || '').toLowerCase().trim();
        document.querySelectorAll('.js-side-item').forEach(function (it) {
            var t = it.textContent.toLowerCase();
            it.classList.toggle('v-side-hidden', q !== '' && t.indexOf(q) === -1);
        });
    };
    window.vaultToggleSidebar = function () {
        var body = document.querySelector('.v-body');
        if (!body) return;
        body.classList.toggle('v-sidebar-collapsed');
        try { localStorage.setItem('vaultSidebar', body.classList.contains('v-sidebar-collapsed') ? '0' : '1'); } catch (e) {}
    };
    // ── Massen-Auswahl ────────────────────────────────────────────────
    window.vaultSelIds = function () {
        return Array.prototype.slice.call(document.querySelectorAll('.v-sel-row:checked'))
            .map(function (c) { return c.value; });
    };
    window.vaultSelChanged = function () {
        var n   = vaultSelIds().length;
        var bar = document.getElementById('vBulkBar');
        if (bar) bar.style.display = n ? 'flex' : 'none';
        var c = document.getElementById('vBulkCount');
        if (c) c.textContent = n + ' ausgewählt';
    };
    // Wählt alle Einträge, die dem aktuellen Filter (2FA + Livesuche) entsprechen –
    // auch die per Paginierung ausgeblendeten (wichtig, um z. B. 200 Stück zu verschieben).
    window.vaultSelAll = function (on) {
        document.querySelectorAll('tr.v-row').forEach(function (r) {
            var cb = r.querySelector('.v-sel-row');
            if (!cb) return;
            cb.checked = on && window.vaultRowMatchesFilter(r);
        });
        var all = document.getElementById('vSelAll');
        if (all && !on) all.checked = false;
        vaultSelChanged();
    };
    window.vaultBulkDelete = function () {
        var ids = vaultSelIds();
        if (!ids.length) return;
        if (!confirm(ids.length + ' Einträge in den Papierkorb verschieben?')) return;
        fetch('/vault/entries/bulk-delete', {
            method: 'POST',
            headers: { 'Content-Type': 'application/x-www-form-urlencoded', 'X-Requested-With': 'XMLHttpRequest', 'Accept': 'application/json' },
            body: '_csrf=' + encodeURIComponent(vaultCsrf) + '&ids=' + encodeURIComponent(ids.join(','))
        })
        .then(function (r) { return r.json(); })
        .then(function (data) {
            if (data.message) alert(data.message);
            location.reload();
        })
        .catch(function (err) { alert('Verbindungsfehler: ' + err.message); });
    };
    // Aufklappmenüs (details.v-io-menu) bei Klick außerhalb schließen
    document.addEventListener('click', function (ev) {
        document.querySelectorAll('details.v-io-menu[open]').forEach(function (d) {
            if (!d.contains(ev.target)) d.removeAttribute('open');
        });
    });
    document.addEventListener('DOMContentLoaded', function () {
        try { if (localStorage.getItem('vaultDense') !== '0') { var t = document.getElementById('vTable'); if (t) t.classList.add('vlt-dense'); } } catch (e) {}
        try { if (localStorage.getItem('vaultSidebar') === '0') { var b = document.querySelector('.v-body'); if (b) b.classList.add('v-sidebar-collapsed'); } } catch (e) {}
        if (document.getElementById('vTbody')) vaultApplyTable();

        // Endlos-Laden: nächste Einträge automatisch nachladen, sobald das
        // Listenende in Sichtweite kommt (der Button bleibt als Fallback).
        var lm = document.getElementById('vLoadMore');
        if (lm && 'IntersectionObserver' in window) {
            new IntersectionObserver(function (entries) {
                entries.forEach(function (e) {
                    if (e.isIntersecting && lm.style.display !== 'none') vaultShowMore();
                });
            }, { rootMargin: '300px' }).observe(lm);
        }
    });
})();
</script>

<!-- Modal: KDBX-Export -->
<div class="modal-overlay v-modal-overlay" id="kdbxExportModal"
     data-close-self>
    <div class="modal-card">
        <div class="modal-header">
            <h2><?= $this->te('Verschlüsselter Export (KDBX)') ?></h2>
            <button aria-label="<?= $this->te('Schließen') ?>" type="button" class="modal-close"
                    data-close="kdbxExportModal">&times;</button>
        </div>
        <form method="POST" action="/vault/export-kdbx">
            <input type="hidden" name="_csrf" value="<?= $this->csrf() ?>">
            <input type="hidden" name="target_team_id" value="<?= $teamId !== null ? (int)$teamId : '' ?>">
            <div class="modal-body">
                <p class="form-hint" style="margin:0 0 0.8rem;">
                    <?= $teamId !== null && $activeTeam
                        ? $this->t('Exportiert den Team-Tresor „{name}" als KeePass-Datei (KDBX 4). Die Datei wird mit dem hier gewählten Master-Passwort verschlüsselt.', ['name' => $this->e($activeTeam['name'])])
                        : $this->te('Exportiert deinen persönlichen Tresor als KeePass-Datei (KDBX 4). Die Datei wird mit dem hier gewählten Master-Passwort verschlüsselt.') ?>
                </p>
                <div class="form-group">
                    <label for="kdbxExportPassword"><?= $this->te('Master-Passwort') ?> <span class="text-danger">*</span></label>
                    <input type="password" id="kdbxExportPassword" name="kdbx_password" class="form-control"
                           placeholder="<?= $this->te('Master-Passwort (min. 8 Zeichen)') ?>" minlength="8" required autocomplete="new-password">
                </div>
            </div>
            <div class="modal-footer">
                <button type="button" class="btn btn-default"
                        data-close="kdbxExportModal"><?= $this->te('Abbrechen') ?></button>
                <button type="submit" class="btn btn-primary"><?= $this->te('Exportieren') ?></button>
            </div>
        </form>
    </div>
</div>

<!-- Modal: persönlicher Ordner -->
<div class="modal-overlay v-modal-overlay" id="folderModal"
     data-close-self>
    <div class="modal-card">
        <div class="modal-header">
            <h2><?= $this->te('Ordner anlegen') ?></h2>
            <button aria-label="<?= $this->te('Schließen') ?>" type="button" class="modal-close"
                    data-close="folderModal">&times;</button>
        </div>
        <form method="POST" action="/vault/folders">
            <input type="hidden" name="_csrf" value="<?= $this->csrf() ?>">
            <div class="modal-body">
                <div class="form-group">
                    <label for="folderModalName"><?= $this->te('Ordner-Name') ?> <span class="text-danger">*</span></label>
                    <input type="text" id="folderModalName" name="name" class="form-control" placeholder="<?= $this->te('Ordner-Name') ?>" required autofocus>
                </div>
                <?php if (!empty($folders)): ?>
                <div class="form-group">
                    <label for="folderModalParent"><?= $this->te('Übergeordneter Ordner') ?></label>
                    <select id="folderModalParent" name="parent_id" class="form-control">
                        <option value=""><?= $this->te('— Kein übergeordneter Ordner —') ?></option>
                        <?php foreach ($folders as $f): ?>
                            <option value="<?= (int)$f['id'] ?>" <?= ($teamId === null && $folderId === (int)$f['id']) ? 'selected' : '' ?>>
                                <?= $this->e($f['path_label'] ?? $f['name']) ?>
                            </option>
                        <?php endforeach; ?>
                    </select>
                </div>
                <?php endif; ?>
            </div>
            <div class="modal-footer">
                <button type="button" class="btn btn-default"
                        data-close="folderModal"><?= $this->te('Abbrechen') ?></button>
                <button type="submit" class="btn btn-primary"><?= $this->te('Anlegen') ?></button>
            </div>
        </form>
    </div>
</div>

<?php if ($teamId !== null): ?>
<div class="modal-overlay v-modal-overlay" id="teamFolderModal"
     data-close-self>
    <div class="modal-card">
        <div class="modal-header">
            <h2><?= $this->te('Team-Ordner anlegen') ?></h2>
            <button aria-label="<?= $this->te('Schließen') ?>" type="button" class="modal-close"
                    data-close="teamFolderModal">&times;</button>
        </div>
        <form method="POST" action="/vault/teams/<?= (int)$teamId ?>/folders">
            <input type="hidden" name="_csrf" value="<?= $this->csrf() ?>">
            <div class="modal-body">
                <div class="form-group">
                    <label for="teamFolderModalName"><?= $this->te('Ordner-Name') ?> <span class="text-danger">*</span></label>
                    <input type="text" id="teamFolderModalName" name="name" class="form-control" placeholder="<?= $this->te('Ordner-Name') ?>" required autofocus>
                </div>
                <?php if (!empty($teamFolders)): ?>
                <div class="form-group">
                    <label for="teamFolderModalParent"><?= $this->te('Übergeordneter Ordner') ?></label>
                    <select id="teamFolderModalParent" name="parent_id" class="form-control">
                        <option value=""><?= $this->te('— Kein übergeordneter Ordner —') ?></option>
                        <?php foreach ($teamFolders as $tf): ?>
                            <option value="<?= (int)$tf['id'] ?>" <?= ($folderId === (int)$tf['id']) ? 'selected' : '' ?>>
                                <?= $this->e($tf['path_label'] ?? $tf['name']) ?>
                            </option>
                        <?php endforeach; ?>
                    </select>
                </div>
                <?php endif; ?>
            </div>
            <div class="modal-footer">
                <button type="button" class="btn btn-default"
                        data-close="teamFolderModal"><?= $this->te('Abbrechen') ?></button>
                <button type="submit" class="btn btn-primary"><?= $this->te('Anlegen') ?></button>
            </div>
        </form>
    </div>
</div>

<?php if ($canWriteTeam): ?>
<!-- Modal: Team-Ordner an anderen Eltern-Ordner hängen -->
<div class="modal-overlay v-modal-overlay" id="teamFolderMoveModal"
     data-close-self>
    <div class="modal-card">
        <div class="modal-header">
            <h2><?= $this->te('Ordner verschieben') ?></h2>
            <button aria-label="<?= $this->te('Schließen') ?>" type="button" class="modal-close"
                    data-close="teamFolderMoveModal">&times;</button>
        </div>
        <form method="POST" id="tfMoveForm" action="">
            <input type="hidden" name="_csrf" value="<?= $this->csrf() ?>">
            <input type="hidden" name="_return" value="vault">
            <div class="modal-body">
                <div class="form-group">
                    <label for="tfMoveParent"><?= $this->te('Neuer übergeordneter Ordner') ?></label>
                    <select name="parent_id" id="tfMoveParent" class="form-control"></select>
                </div>
            </div>
            <div class="modal-footer">
                <button type="button" class="btn btn-default"
                        data-close="teamFolderMoveModal"><?= $this->te('Abbrechen') ?></button>
                <button type="submit" class="btn btn-primary"><?= $this->te('Verschieben') ?></button>
            </div>
        </form>
    </div>
</div>
<script<?= $this->scriptNonce() ?>>
var _tfFolders = <?= json_encode(array_values(array_map(fn($f) => [
    'id'     => (int)$f['id'],
    'parent' => $f['parent_id'] !== null ? (int)$f['parent_id'] : null,
    'label'  => $f['path_label'] ?? $f['name'],
], $teamFolders)), JSON_HEX_TAG | JSON_HEX_AMP | JSON_HEX_APOS | JSON_HEX_QUOT) ?>;

function openTeamFolderMove(folderId) {
    document.getElementById('tfMoveForm').action =
        '/vault/teams/<?= (int)$teamId ?>/folders/' + folderId + '/move';

    // Den Ordner selbst und alle Nachfahren als Ziel ausschließen (Zyklus)
    var children = {};
    _tfFolders.forEach(function (f) {
        var p = f.parent === null ? 0 : f.parent;
        (children[p] = children[p] || []).push(f.id);
    });
    var excluded = {};
    var stack = [folderId];
    while (stack.length) {
        var c = stack.pop();
        excluded[c] = true;
        (children[c] || []).forEach(function (k) { stack.push(k); });
    }

    var curParent = null;
    _tfFolders.forEach(function (f) { if (f.id === folderId) curParent = f.parent; });

    var sel = document.getElementById('tfMoveParent');
    sel.innerHTML = '<option value="">— Oberste Ebene —</option>';
    _tfFolders.forEach(function (f) {
        if (excluded[f.id]) return;
        var o = document.createElement('option');
        o.value = String(f.id);
        o.textContent = f.label;
        if (curParent !== null && f.id === curParent) o.selected = true;
        sel.appendChild(o);
    });

    document.getElementById('teamFolderMoveModal').classList.add('open');
}
</script>
<?php endif; ?>
<?php endif; ?>

<!-- ================================================================ -->
<!-- Modal: Eintrag verschieben                                        -->
<!-- ================================================================ -->
<div class="modal-overlay vm-overlay" id="moveModal" data-close-self>
    <div class="modal-card">
        <div class="modal-header">
            <h2><?= $this->te('Eintrag verschieben') ?></h2>
            <button aria-label="<?= $this->te('Schließen') ?>" type="button" class="modal-close" data-act="closeMoveModal">&times;</button>
        </div>
        <div class="modal-body">
            <p class="form-hint" id="moveModalHint" style="margin:0 0 0.9rem;"><?= $this->te('Ziel auswählen') ?></p>
            <div class="form-group">
                <label for="moveTargetContext"><?= $this->te('Zielbereich') ?></label>
                <select id="moveTargetContext" data-act-change="moveFillFolders" class="form-control">
                    <option value=""><?= $this->te('— Mein persönlicher Tresor —') ?></option>
                    <?php foreach ($teams as $team): ?>
                        <option value="<?= (int)$team['id'] ?>">👥 <?= $this->e($team['name']) ?></option>
                    <?php endforeach; ?>
                </select>
            </div>
            <div class="form-group">
                <label for="moveTargetFolder"><?= $this->te('Zielordner') ?> <span class="vm-opt"><?= $this->te('(optional)') ?></span></label>
                <select id="moveTargetFolder" class="form-control">
                    <option value=""><?= $this->te('— Kein Ordner (Stammverzeichnis) —') ?></option>
                </select>
            </div>
        </div>
        <div class="modal-footer">
            <button type="button" class="btn btn-default" data-act="closeMoveModal"><?= $this->te('Abbrechen') ?></button>
            <button type="button" class="btn btn-primary" id="moveSubmitBtn" data-act="submitMove">
                <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5"><path d="M5 9l7-7 7 7M5 15l7 7 7-7"/></svg>
                <?= $this->te('Verschieben') ?>
            </button>
        </div>
    </div>
</div>

<style>
/* ── Vault-Modals (Verschieben/Anlegen): Basis aus app.css (.modal-overlay/.modal-card),
   hier nur Zustands-Toggle, Vault-Stapelreihenfolge und Maße ── */
.vm-overlay { display:none; z-index:2500; padding:1rem; }
.vm-overlay.open { display:flex; }
#moveModal .modal-card { max-width:440px; }
#createModal .modal-card { max-width:580px; }

/* Formular-Spezifika der Vault-Modals */
.vm-opt { font-weight:400; color:var(--gray-400); }

.vm-row-2 { display:grid; grid-template-columns:1fr 1fr; gap:.75rem; }
@media(max-width:480px) { .vm-row-2 { grid-template-columns:1fr; } }

.vm-pw-row { display:flex; gap:.4rem; }
.vm-pw-row .form-control { flex:1; }
.vm-pw-btn { padding:.5rem .7rem; border:1px solid var(--gray-300); border-radius:.45rem;
    background:var(--gray-50); color:var(--gray-700); cursor:pointer; font-size:.82rem; white-space:nowrap;
    display:flex; align-items:center; gap:.3rem; }
.vm-pw-btn:hover { background:var(--gray-200); }

.vm-gen-bar { display:flex; align-items:center; gap:.6rem; flex-wrap:wrap; font-size:.75rem;
    color:var(--gray-500); background:var(--slate-50); border:1px solid var(--gray-200); border-radius:.4rem; padding:.4rem .65rem; margin-top:.35rem; }
.vm-gen-bar input[type=number] { width:52px; padding:.2rem .4rem; border:1px solid var(--gray-300);
    border-radius:.3rem; font-size:.78rem; }
.vm-gen-bar label { display:flex; align-items:center; gap:.25rem; cursor:pointer; }

.vm-url-list { display:flex; flex-direction:column; gap:.35rem; }
.vm-url-row  { display:flex; gap:.4rem; }
.vm-url-row .form-control { flex:1; }
.vm-url-del { padding:.5rem .55rem; border:1px solid var(--red-300); background:var(--red-100); color:var(--red-600);
    border-radius:.45rem; cursor:pointer; font-size:.8rem; }
.vm-url-del:hover { background:var(--red-200); }
.vm-url-add { font-size:.78rem; color:var(--blue-600); cursor:pointer; background:none; border:none;
    padding:.2rem 0; text-decoration:none; display:inline-flex; align-items:center; gap:.25rem; }
.vm-url-add:hover { text-decoration:underline; }
</style>

<!-- ================================================================ -->
<!-- Modal: Neuen Eintrag erstellen                                    -->
<!-- ================================================================ -->
<div class="modal-overlay vm-overlay" id="createModal" data-close-self>
    <div class="modal-card">
        <div class="modal-header">
            <h2 id="cmTitleHead"><?= $this->te('Neuen Eintrag erstellen') ?></h2>
            <button aria-label="<?= $this->te('Schließen') ?>" type="button" class="modal-close" data-act="closeCreateModal">&times;</button>
        </div>
        <div class="modal-body">
            <form id="createEntryForm" method="POST" action="" data-act-submit="submitCreateForm">
                <input type="hidden" name="_csrf" id="createCsrf" value="<?= $this->csrf() ?>">
                <input type="hidden" name="_method" id="cmMethod" value="">
                <input type="hidden" name="create_team_id" id="createTeamId" value="<?= $teamId !== null ? (int)$teamId : '' ?>">
                <input type="hidden" name="create_folder_id" id="createFolderId" value="<?= $folderId !== null ? (int)$folderId : '' ?>">

                <?php $writableTeams = $writable_teams ?? []; ?>
                <?php if (!empty($writableTeams)): ?>
                <!-- Ziel-Tresor frei wählbar; vorbelegt ist der gerade geöffnete. -->
                <div class="form-group" id="cmVaultGroup">
                    <label for="cm_vault"><?= $this->te('Tresor') ?></label>
                    <select id="cm_vault" class="form-control" data-act-change="vaultZielAusFeld">
                        <option value=""><?= $this->te('Persönlicher Tresor') ?></option>
                        <?php foreach ($writableTeams as $wt): ?>
                        <option value="<?= (int)$wt['id'] ?>"<?= $teamId === (int)$wt['id'] ? ' selected' : '' ?>>
                            <?= $this->e($wt['name']) ?>
                        </option>
                        <?php endforeach; ?>
                    </select>
                </div>
                <?php endif; ?>

                <div class="form-group">
                    <label for="cm_title"><?= $this->te('Titel') ?> <span class="text-danger">*</span></label>
                    <input type="text" id="cm_title" name="title" class="form-control" required autofocus placeholder="<?= $this->te('z.B. GitHub, Office 365, Router…') ?>">
                </div>

                <div class="form-group">
                    <label><?= $this->te('URL(s)') ?></label>
                    <div class="vm-url-list" id="cmUrlList">
                        <div class="vm-url-row">
                            <input type="url" name="urls[]" class="form-control" placeholder="https://example.com">
                        </div>
                    </div>
                    <button type="button" class="vm-url-add" data-act="cmAddUrl">
                        <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5"><path d="M12 5v14M5 12h14"/></svg>
                        <?= $this->te('Weitere URL') ?>
                    </button>
                </div>

                <div class="vm-row-2">
                    <div class="form-group">
                        <label for="cm_username"><?= $this->te('Benutzername / E-Mail') ?></label>
                        <input type="text" id="cm_username" name="username" class="form-control" placeholder="user@example.com" autocomplete="off">
                    </div>
                    <div class="form-group">
                        <label for="cm_folder"><?= $this->te('Ordner') ?></label>
                        <select id="cm_folder" name="folder_id" class="form-control">
                            <option value=""><?= $this->te('— Kein Ordner —') ?></option>
                            <?php foreach ($folders as $folder): ?>
                                <option value="<?= $folder['id'] ?>" <?= $folderId === (int)$folder['id'] ? 'selected' : '' ?>>
                                    <?= $this->e($folder['path_label'] ?? $folder['name']) ?>
                                </option>
                            <?php endforeach; ?>
                        </select>
                    </div>
                </div>

                <div class="form-group">
                    <label for="cm_password"><?= $this->te('Passwort') ?> <span class="text-danger">*</span></label>
                    <div class="vm-pw-row">
                        <input type="password" id="cm_password" name="password" class="form-control" required autocomplete="new-password" placeholder="<?= $this->te('Sicheres Passwort') ?>">
                        <button aria-label="<?= $this->te('Passwort anzeigen') ?>" type="button" class="vm-pw-btn" data-act="cmTogglePw">
                            <svg id="cmPwEye" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><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"/></svg>
                        </button>
                        <button type="button" class="vm-pw-btn" data-act="cmGeneratePw" title="<?= $this->te('Generieren') ?>">
                            <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M21.5 2v6h-6M21.34 15.57a10 10 0 1 1-.57-8.38"/></svg>
                        </button>
                    </div>
                    <div class="vm-gen-bar">
                        <span><?= $this->te('Länge:') ?></span>
                        <input type="number" id="cmGenLen" value="20" min="8" max="128">
                        <label><input type="checkbox" id="cmGenUpper" checked> A–Z</label>
                        <label><input type="checkbox" id="cmGenNum" checked> 0–9</label>
                        <label><input type="checkbox" id="cmGenSym" checked> !@#$</label>
                    </div>
                </div>

                <div class="form-group">
                    <label for="cm_totp"><?= $this->te('TOTP-Secret') ?> <span class="vm-opt"><?= $this->te('(optional)') ?></span></label>
                    <input type="text" id="cm_totp" name="totp" class="form-control" placeholder="<?= $this->te('Base32-Secret, z.B. JBSWY3DPEHPK3PXP') ?>" autocomplete="off" spellcheck="false">
                    <span class="form-hint"><?= $this->te('Einrichtungsschlüssel aus dem Authenticator-QR-Code (nicht der 6-stellige Code).') ?></span>
                </div>

                <div class="form-group">
                    <label for="cm_expires"><?= $this->te('Ablaufdatum') ?> <span class="vm-opt"><?= $this->te('(optional – Erinnerung zur Passwort-Rotation)') ?></span></label>
                    <input type="date" id="cm_expires" name="expires_at" class="form-control" style="max-width:200px;">
                </div>

                <div class="form-group">
                    <label><?= $this->te('Zusatzfelder') ?> <span class="vm-opt"><?= $this->te('(optional, verschlüsselt)') ?></span></label>
                    <div class="vm-url-list" id="cmFieldList"></div>
                    <button type="button" class="vm-url-add" data-act="cmAddField">
                        <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5"><path d="M12 5v14M5 12h14"/></svg>
                        <?= $this->te('Feld hinzufügen') ?>
                    </button>
                </div>

                <div class="form-group">
                    <label for="cm_notes"><?= $this->te('Notizen') ?> <span class="vm-opt"><?= $this->te('(optional)') ?></span></label>
                    <textarea id="cm_notes" name="notes" class="form-control" style="min-height:72px;" placeholder="<?= $this->te('Optionale Anmerkungen…') ?>"></textarea>
                </div>
            </form>
        </div>
        <div class="modal-footer">
            <button type="button" class="btn btn-default" data-act="closeCreateModal"><?= $this->te('Abbrechen') ?></button>
            <button type="button" class="btn btn-primary" id="createSubmitBtn" data-act="cmAbsenden">
                <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5"><path d="M19 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h11l5 5v14a2 2 0 0 1-2 2z"/><polyline points="17 21 17 13 7 13 7 21"/><polyline points="7 3 7 8 15 8"/></svg>
                <?= $this->te('Speichern') ?>
            </button>
        </div>
    </div>
</div>

<script<?= $this->scriptNonce() ?>>
var vaultPersonalFolders = <?php echo json_encode(array_values(array_map(fn($f) => ['id' => (int)$f['id'], 'name' => $f['path_label'] ?? $f['name']], $folders)), JSON_HEX_TAG | JSON_HEX_AMP | JSON_HEX_APOS | JSON_HEX_QUOT); ?>;
var vaultTeamFolders     = <?php echo json_encode(array_map(fn($tf) => array_values(array_map(fn($f) => ['id' => (int)$f['id'], 'name' => $f['path_label'] ?? $f['name']], $tf)), $allTeamFolders), JSON_HEX_TAG | JSON_HEX_AMP | JSON_HEX_APOS | JSON_HEX_QUOT); ?>;
var vaultCsrf            = <?php echo json_encode($this->csrf(), JSON_HEX_TAG | JSON_HEX_AMP | JSON_HEX_APOS | JSON_HEX_QUOT); ?>;

var _moveEntryId   = null;
var _bulkMoveIds   = null;

function openMoveModal(entryId, srcTeamId, srcFolderId) {
    _moveEntryId = entryId;
    _bulkMoveIds = null;

    var ctx = document.getElementById('moveTargetContext');
    ctx.value = srcTeamId !== null ? String(srcTeamId) : '';
    moveFillFolders();
    if (srcFolderId !== null) {
        document.getElementById('moveTargetFolder').value = String(srcFolderId);
    }

    document.getElementById('moveModalHint').textContent = 'Ziel auswählen';
    document.getElementById('moveModal').classList.add('open');
}

// Massen-Verschieben: nutzt dasselbe Modal, sendet an /vault/entries/bulk-move
function vaultBulkMove() {
    var ids = vaultSelIds();
    if (!ids.length) return;
    _moveEntryId = null;
    _bulkMoveIds = ids;

    document.getElementById('moveTargetContext').value = '';
    moveFillFolders();
    document.getElementById('moveModalHint').textContent = ids.length + ' Einträge verschieben – Ziel auswählen';
    document.getElementById('moveModal').classList.add('open');
}

function closeMoveModal() {
    document.getElementById('moveModal').classList.remove('open');
    _moveEntryId = null;
    _bulkMoveIds = null;
}

function moveFillFolders() {
    var ctx    = document.getElementById('moveTargetContext').value;
    var sel    = document.getElementById('moveTargetFolder');
    var teamId = ctx !== '' ? parseInt(ctx, 10) : null;

    sel.innerHTML = '<option value="">— Kein Ordner —</option>';

    var list = teamId !== null ? (vaultTeamFolders[teamId] || []) : vaultPersonalFolders;
    list.forEach(function(f) {
        var opt = document.createElement('option');
        opt.value = String(f.id);
        opt.textContent = f.name;
        sel.appendChild(opt);
    });
}

function submitMove() {
    if (_moveEntryId === null && (_bulkMoveIds === null || !_bulkMoveIds.length)) return;

    var targetTeamId   = document.getElementById('moveTargetContext').value;
    var targetFolderId = document.getElementById('moveTargetFolder').value;
    var btn = document.getElementById('moveSubmitBtn');
    btn.disabled = true;
    btn.innerHTML = '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5"><path d="M5 9l7-7 7 7M5 15l7 7 7-7"/></svg> Verschiebt\u2026';

    var body = '_csrf=' + encodeURIComponent(vaultCsrf)
             + '&target_team_id='   + encodeURIComponent(targetTeamId)
             + '&target_folder_id=' + encodeURIComponent(targetFolderId);
    var url = '/vault/entries/' + _moveEntryId + '/move';
    if (_bulkMoveIds !== null) {
        url  = '/vault/entries/bulk-move';
        body += '&ids=' + encodeURIComponent(_bulkMoveIds.join(','));
    }

    fetch(url, {
        method:  'POST',
        headers: { 'Content-Type': 'application/x-www-form-urlencoded',
                   'X-Requested-With': 'XMLHttpRequest',
                   'Accept': 'application/json' },
        body: body
    })
    .then(function(r) { return r.json(); })
    .then(function(data) {
        if (data.success) {
            if (_bulkMoveIds !== null && data.message) alert(data.message);
            closeMoveModal();
            location.reload();
        } else {
            alert('Fehler: ' + (data.message || 'Unbekannter Fehler'));
            btn.disabled = false;
            btn.innerHTML = '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5"><path d="M5 9l7-7 7 7M5 15l7 7 7-7"/></svg> Verschieben';
        }
    })
    .catch(function(err) {
        alert('Verbindungsfehler: ' + err.message);
        btn.disabled = false;
        btn.innerHTML = '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5"><path d="M5 9l7-7 7 7M5 15l7 7 7-7"/></svg> Verschieben';
    });
}

// ── Create Entry Modal ──────────────────────────────────────────────────────
var _createTeamId  = <?= $teamId !== null ? (int)$teamId : 'null' ?>;
var _createFolderId = <?= $folderId !== null ? (int)$folderId : 'null' ?>;

/**
 * Richtet den Anlege-Dialog auf einen Ziel-Tresor aus: Formular-Ziel, verstecktes
 * Feld und Ordner-Auswahl. teamId === null bedeutet persönlicher Tresor.
 */
function vaultApplyCreateTarget(teamId, preselectFolderId) {
    var form = document.getElementById('createEntryForm');
    var sel  = document.getElementById('cm_folder');
    var folders;

    if (teamId !== null) {
        form.action = '/vault/teams/' + teamId + '/entries';
        document.getElementById('createTeamId').value = teamId;
        folders = vaultTeamFolders[teamId] || [];
    } else {
        form.action = '/vault/entries';
        document.getElementById('createTeamId').value = '';
        folders = vaultPersonalFolders || [];
    }

    // Ordner gehören immer genau einem Tresor – beim Wechsel neu aufbauen.
    if (sel) {
        sel.innerHTML = '<option value="">— Kein Ordner —</option>';
        var ziel = preselectFolderId !== null && preselectFolderId !== '' ? String(preselectFolderId) : null;
        folders.forEach(function (f) {
            var opt = document.createElement('option');
            opt.value = String(f.id);
            opt.textContent = f.name;
            // Vergleich über Zeichenketten: Die Kennungen kommen je nach
            // Datenbanktreiber als Zahl oder als Text aus dem Server.
            if (ziel !== null && String(f.id) === ziel) { opt.selected = true; }
            sel.appendChild(opt);
        });
    }
}

/** Auswahl im Dialog: Ziel-Tresor umstellen (Ordner-Vorauswahl entfällt dabei). */
// Zielwahl im Anlegen-Dialog: der Verteiler übergibt das Auswahlfeld.
function vaultZielAusFeld(el) { vaultSwitchCreateTarget(el.value); }

// Speichern-Knopf des Anlegen-Dialogs steht außerhalb des Formulars.
function cmAbsenden() {
    var form = document.getElementById('createEntryForm');
    if (form) { form.requestSubmit(); }
}

function vaultSwitchCreateTarget(value) {
    vaultApplyCreateTarget(value === '' ? null : parseInt(value, 10), null);
}

function openCreateModal() {
    vaultApplyCreateTarget(_createTeamId, _createFolderId);

    // Auswahlfeld auf den gerade geöffneten Tresor stellen und wieder einblenden
    // (das Bearbeiten eines Eintrags blendet es aus).
    var vaultSel = document.getElementById('cm_vault');
    if (vaultSel) { vaultSel.value = _createTeamId === null ? '' : String(_createTeamId); }
    var vaultGroup = document.getElementById('cmVaultGroup');
    if (vaultGroup) { vaultGroup.style.display = ''; }

    // Zurück in den Anlege-Modus (vedEditInModal überschreibt danach bei Bedarf)
    document.getElementById('cmTitleHead').textContent = 'Neuen Eintrag erstellen';
    document.getElementById('cmMethod').value = '';
    document.getElementById('cm_title').value    = '';
    document.getElementById('cm_username').value = '';
    document.getElementById('cm_password').value = '';
    document.getElementById('cm_totp').value     = '';
    document.getElementById('cm_notes').value    = '';
    var cmExp = document.getElementById('cm_expires'); if (cmExp) cmExp.value = '';
    var cmFields = document.getElementById('cmFieldList'); if (cmFields) cmFields.innerHTML = '';
    document.getElementById('cmUrlList').innerHTML = '<div class="vm-url-row"><input type="url" name="urls[]" class="form-control" placeholder="https://example.com"></div>';
    document.getElementById('createModal').classList.add('open');
    setTimeout(function() { document.getElementById('cm_title').focus(); }, 100);
}

function closeCreateModal() {
    document.getElementById('createModal').classList.remove('open');
}

function cmAddUrl() {
    var list = document.getElementById('cmUrlList');
    var row = document.createElement('div');
    row.className = 'vm-url-row';
    row.innerHTML = '<input type="url" name="urls[]" class="form-control" placeholder="https://example.com">'
        + '<button type="button" class="vm-url-del" data-remove-closest=".vm-url-row">&#215;</button>';
    list.appendChild(row);
    row.querySelector('input').focus();
}

function cmTogglePw() {
    var inp = document.getElementById('cm_password');
    inp.type = inp.type === 'password' ? 'text' : 'password';
}

function cmAddField(name, value, isSecret) {
    var list = document.getElementById('cmFieldList');
    var row = document.createElement('div');
    row.className = 'vm-url-row';
    row.style.flexWrap = 'wrap';
    row.innerHTML =
        '<input type="text" name="cf_name[]" class="form-control" placeholder="Feldname" style="flex:1 1 35%;">' +
        '<input type="text" name="cf_value[]" class="form-control" placeholder="Wert" style="flex:1 1 45%;">' +
        '<label style="display:inline-flex;align-items:center;gap:0.25rem;font-size:0.75rem;color:#6b7280;">' +
        '<input type="checkbox" name="cf_secret[]" value="1"> geheim</label>' +
        '<button type="button" class="vm-url-del" data-remove-closest=".vm-url-row">&#215;</button>';
    list.appendChild(row);
    var inputs = row.querySelectorAll('input');
    if (name)  inputs[0].value = name;
    if (value) inputs[1].value = value;
    if (isSecret) inputs[2].checked = true;
    return row;
}

function cmGeneratePw() {
    var len     = parseInt(document.getElementById('cmGenLen').value, 10)  || 20;
    var upper   = document.getElementById('cmGenUpper').checked;
    var numbers = document.getElementById('cmGenNum').checked;
    var symbols = document.getElementById('cmGenSym').checked;
    var chars = 'abcdefghijklmnopqrstuvwxyz';
    if (upper)   chars += 'ABCDEFGHIJKLMNOPQRSTUVWXYZ';
    if (numbers) chars += '0123456789';
    if (symbols) chars += '!@#$%^&*()-_=+[]{}|;:,.<>?';
    var pw = '';
    var arr = new Uint32Array(len);
    window.crypto.getRandomValues(arr);
    for (var i = 0; i < len; i++) pw += chars[arr[i] % chars.length];
    var inp = document.getElementById('cm_password');
    inp.value = pw;
    inp.type  = 'text';
}

function submitCreateForm(formular, e) {
    e.preventDefault();
    var btn  = document.getElementById('createSubmitBtn');
    var form = document.getElementById('createEntryForm');
    btn.disabled = true;
    btn.textContent = 'Speichert…';
    form.submit();
    return false;
}

// Escape key closes modals
document.addEventListener('keydown', function(e) {
    if (e.key !== 'Escape') return;
    if (document.getElementById('createModal').classList.contains('open')) { closeCreateModal(); return; }
    if (document.getElementById('moveModal').classList.contains('open'))   { closeMoveModal();   return; }
});
</script>


<!-- ================================================================ -->
<!-- Modal: Eintrag Details                                            -->
<!-- ================================================================ -->
<style>
/* Basis aus app.css (.modal-overlay/.modal-card); lokal Zustands-Toggle,
   Vault-Stapelreihenfolge und die randlosen Feldzeilen des Detail-Modals */
.ved-overlay { display:none; z-index:3000; padding:1rem; }
.ved-overlay.open { display:flex; }
.ved-overlay .modal-header { gap:0.75rem; padding:1rem 1.25rem; }
.ved-overlay .modal-body { padding:0; }
.ved-overlay .modal-footer { flex-wrap:wrap; gap:0.5rem; padding:0.875rem 1.25rem; }
.ved-favicon { width:32px; height:32px; border-radius:6px; background:var(--gray-100);
    display:flex; align-items:center; justify-content:center; flex-shrink:0; overflow:hidden; }
.ved-favicon img { width:22px; height:22px; object-fit:contain; }
.ved-title { flex:1; min-width:0; white-space:nowrap; overflow:hidden; text-overflow:ellipsis; }
.ved-field { display:flex; align-items:center; padding:0.8rem 1.25rem;
    border-bottom:1px solid var(--gray-100); gap:0.75rem; }
.ved-field:last-child { border-bottom:none; }
.ved-field-label { font-size:0.7rem; font-weight:700; text-transform:uppercase;
    letter-spacing:0.06em; color:var(--gray-400); min-width:105px; flex-shrink:0; }
.ved-field-value { flex:1; font-size:0.875rem; color:var(--gray-900); word-break:break-all; }
.ved-field-value.mono { font-family:monospace; }
.ved-pw-blur { filter:blur(5px); user-select:none; transition:filter 0.15s; cursor:pointer; }
.ved-copy-btn { display:inline-flex; align-items:center; justify-content:center;
    width:28px; height:28px; border:1px solid var(--gray-200); border-radius:0.35rem;
    background:var(--gray-50); color:var(--gray-500); cursor:pointer; font-size:12px; flex-shrink:0; }
.ved-copy-btn:hover { background:var(--gray-200); }
.ved-copy-btn.copied { background:var(--green-100); color:var(--green-700); border-color:var(--green-200); }
.ved-show-btn { display:inline-flex; align-items:center; justify-content:center;
    width:28px; height:28px; border:1px solid var(--gray-200); border-radius:0.35rem;
    background:var(--gray-50); color:var(--gray-500); cursor:pointer; font-size:12px; flex-shrink:0; }
.ved-show-btn:hover { background:var(--gray-200); }
.ved-totp-btn { display:inline-flex; align-items:center; gap:0.3rem;
    padding:0.3rem 0.7rem; background:var(--blue-50); color:var(--blue-600);
    border:1px solid var(--blue-200); border-radius:0.45rem;
    font-size:0.8rem; font-weight:600; cursor:pointer; }
.ved-totp-code { font-size:1.3rem; font-weight:800; letter-spacing:0.12em;
    color:var(--blue-600); font-family:monospace; }
.ved-meta { flex:1; font-size:0.72rem; color:var(--gray-400); }
.ved-loading { padding:2.5rem; text-align:center; color:var(--gray-400); }
</style>

<div class="modal-overlay ved-overlay" id="entryDetailModal"
     data-close-self>
    <div class="modal-card">
        <div class="modal-header">
            <div class="ved-favicon" id="vedFavicon">
                <span id="vedFaviconPh" style="font-size:13px;color:var(--gray-400);font-weight:700;"></span>
            </div>
            <h2 class="ved-title" id="vedTitle"></h2>
            <button aria-label="<?= $this->te('Schließen') ?>" type="button" class="modal-close" data-act="closeEntryModal">&times;</button>
        </div>
        <div class="modal-body" id="vedBody">
            <div class="ved-loading"><?= $this->te('Lade…') ?></div>
        </div>
        <div class="modal-footer">
            <div class="ved-meta" id="vedMeta"></div>
            <button type="button" class="btn btn-default btn-sm" data-act="vedShare" title="<?= $this->te('Einmal-Freigabe erstellen') ?>"><?= $this->te('&#128279; Teilen') ?></button>
            <button type="button" id="vedEditBtn" class="btn btn-default btn-sm" data-act="vedEditInModal"><?= $this->te('&#9998; Bearbeiten') ?></button>
            <form id="vedDeleteForm" method="POST" action="#" style="margin:0;"
                  data-confirm="Eintrag in den Papierkorb verschieben?">
                <input type="hidden" name="_csrf" value="<?= $this->csrf() ?>">
                <button type="submit" class="btn btn-outline-danger btn-sm">
                    <?= $this->te('&#128465; Löschen') ?>
                </button>
            </form>
        </div>
    </div>
</div>

<script<?= $this->scriptNonce() ?>>
var _vedTotpUrl     = null;
var _vedTotpInterval = null;
var _vedCurrent     = null;

function openEntryModal(jsonUrl, editUrl, deleteUrl) {
    var overlay = document.getElementById('entryDetailModal');
    var body    = document.getElementById('vedBody');

    // Reset
    document.getElementById('vedTitle').textContent   = 'Lade…';
    document.getElementById('vedFavicon').innerHTML    = '<span id="vedFaviconPh" style="font-size:13px;color:#9ca3af;font-weight:700;"></span>';
    document.getElementById('vedMeta').textContent    = '';
    document.getElementById('vedDeleteForm').action   = deleteUrl || '#';
    body.innerHTML = '<div class="ved-loading">&#128274; Lade…</div>';
    if (_vedTotpInterval) { clearInterval(_vedTotpInterval); _vedTotpInterval = null; }
    _vedTotpUrl = null;

    overlay.classList.add('open');

    fetch(jsonUrl, { headers: { 'X-Requested-With': 'XMLHttpRequest' } })
        .then(function(r) { return r.json(); })
        .then(function(data) {
            if (!data.ok) {
                body.innerHTML = '<div class="ved-loading">Fehler beim Laden.</div>';
                return;
            }
            var e       = data.entry;
            var editUrl = data.edit_url;
            var delUrl  = data.delete_url;
            _vedTotpUrl = data.totp_url || null;
            _vedCurrent = { entry: e, editUrl: editUrl, delUrl: delUrl, jsonUrl: jsonUrl,
                            shareUrl: data.share_url || null, filesUrl: data.files_url || null,
                            passkeys: data.passkeys || [], passkeysUrl: data.passkeys_url || null,
                            fields: data.fields || [], files: data.files || [], history: data.history || [] };

            document.getElementById('vedTitle').textContent = e.title || '';
            document.getElementById('vedDeleteForm').action = delUrl  || '#';

            // Avatar: serverseitig gecachtes Favicon (kein externer Call) oder Monogramm
            var favWrap = document.getElementById('vedFavicon');
            var avHue = vaultHue(e.title || '?');
            var avCh  = (e.title || '?').charAt(0).toUpperCase();
            function vedSetMonogram() {
                favWrap.style.background = 'hsl(' + avHue + ',52%,90%)';
                favWrap.innerHTML = '<span style="font-size:13px;font-weight:700;color:hsl(' + avHue + ',55%,38%);">' + vedEsc(avCh) + '</span>';
            }
            if (data.favicon_url) {
                favWrap.style.background = '#f3f4f6';
                favWrap.innerHTML = '';
                var vedImg = document.createElement('img');
                vedImg.src = data.favicon_url;
                vedImg.alt = '';
                vedImg.style.cssText = 'width:22px;height:22px;object-fit:contain;';
                vedImg.onerror = vedSetMonogram;
                favWrap.appendChild(vedImg);
            } else {
                vedSetMonogram();
            }

            // Meta
            var meta = [];
            if (e.created_at)  meta.push('Erstellt: ' + e.created_at.substring(0,16).replace('T',' '));
            if (e.updated_at && e.updated_at !== e.created_at) meta.push('Geändert: ' + e.updated_at.substring(0,16).replace('T',' '));
            document.getElementById('vedMeta').textContent = meta.join(' · ');

            // Build body
            var html = '';

            // Username
            if (e.username) {
                html += '<div class="ved-field">'
                    + '<div class="ved-field-label">Benutzername</div>'
                    + '<div class="ved-field-value" id="vedUsername">' + vedEsc(e.username) + '</div>'
                    + '<button class="ved-copy-btn" data-act="vedCopy" data-args="[&quot;vedUsername&quot;]" title="Kopieren">&#128203;</button>'
                    + '</div>';
            }

            // Password (farbige Darstellung: Ziffern/Sonderzeichen abgesetzt)
            html += '<div class="ved-field">'
                + '<div class="ved-field-label">Passwort</div>'
                + '<div class="ved-field-value mono ved-pw-blur" id="vedPassword" data-pw="' + vedEsc(e.password) + '" data-act="vedTogglePw">' + vedColorPw(e.password) + '</div>'
                + '<button class="ved-show-btn" id="vedPwToggle" data-act="vedTogglePw" title="Anzeigen">&#128065;</button>'
                + '<button class="ved-copy-btn" data-copy="' + vedEsc(e.password) + '" title="Kopieren">&#128203;</button>'
                + '</div>';

            // URLs
            if (e.url) {
                var urls = e.url.split('\n').map(function(u){return u.trim();}).filter(Boolean);
                if (urls.length) {
                    html += '<div class="ved-field" style="align-items:flex-start;">'
                        + '<div class="ved-field-label" style="padding-top:0.1rem;">URL' + (urls.length > 1 ? 's' : '') + '</div>'
                        + '<div class="ved-field-value" style="flex:1;">';
                    urls.forEach(function(u) {
                        var safe = vedSafeUrl(u);
                        html += '<div style="margin-bottom:0.2rem;">'
                            + (safe
                                ? '<a href="' + vedEsc(safe) + '" target="_blank" rel="noopener noreferrer">' + vedEsc(u) + '</a>'
                                : '<span>' + vedEsc(u) + '</span>')
                            + '</div>';
                    });
                    html += '</div>'
                        + '<button class="ved-copy-btn" data-copy="' + vedEsc(urls[0]) + '" title="Kopieren">&#128203;</button>'
                        + '</div>';
                }
            }

            // TOTP
            if (e.has_totp) {
                html += '<div class="ved-field">'
                    + '<div class="ved-field-label">TOTP</div>'
                    + '<div class="ved-field-value" id="vedTotpArea">'
                    + '<button type="button" class="ved-totp-btn" id="vedTotpBtn" data-act="vedLoadTotp">&#128273; Code anzeigen</button>'
                    + ' <button type="button" class="ved-totp-btn" data-act="vedShowTotpQr" title="QR-Code fürs Handy">&#9636; QR</button>'
                    + '<div id="vedTotpQr" style="margin-top:0.5rem;"></div>'
                    + '</div></div>';
            }

            // Ablaufdatum
            if (e.expires_at) {
                var expTs = new Date(e.expires_at + 'T00:00:00').getTime();
                var warn  = expTs < Date.now();
                html += '<div class="ved-field">'
                    + '<div class="ved-field-label">Ablauf</div>'
                    + '<div class="ved-field-value" style="color:' + (warn ? '#b91c1c' : '#374151') + ';">'
                    + vedEsc(e.expires_at.split('-').reverse().join('.')) + (warn ? ' (abgelaufen)' : '') + '</div></div>';
            }

            // Benutzerdefinierte Felder
            (_vedCurrent.fields || []).forEach(function (f, idx) {
                var vid = 'vedField' + idx;
                var valHtml = f.is_secret
                    ? '<span class="ved-pw-blur" id="' + vid + '" data-plain="' + vedEsc(f.value) + '" data-act="vedRevealField">' + vedEsc(f.value) + '</span>'
                    : '<span id="' + vid + '">' + vedEsc(f.value) + '</span>';
                html += '<div class="ved-field">'
                    + '<div class="ved-field-label">' + vedEsc(f.name) + '</div>'
                    + '<div class="ved-field-value mono">' + valHtml + '</div>'
                    + '<button class="ved-copy-btn" data-copy="' + vedEsc(f.value) + '" title="Kopieren">&#128203;</button>'
                    + '</div>';
            });

            // Notes
            if (e.notes) {
                html += '<div class="ved-field" style="align-items:flex-start;">'
                    + '<div class="ved-field-label" style="padding-top:0.1rem;">Notizen</div>'
                    + '<div class="ved-field-value" style="white-space:pre-wrap;font-size:0.875rem;line-height:1.6;">' + vedEsc(e.notes) + '</div>'
                    + '</div>';
            }

            // Anhänge
            html += '<div class="ved-field" style="align-items:flex-start;">'
                + '<div class="ved-field-label" style="padding-top:0.3rem;">Anhänge</div>'
                + '<div class="ved-field-value" id="vedFiles" style="flex:1;">' + vedRenderFiles(_vedCurrent.files) + '</div></div>';

            // Passkeys (nur über die Browser-Erweiterung angelegt; hier einsehen und entfernen)
            if ((_vedCurrent.passkeys || []).length) {
                html += '<div class="ved-field" style="align-items:flex-start;">'
                    + '<div class="ved-field-label" style="padding-top:0.3rem;">Passkeys</div>'
                    + '<div class="ved-field-value" id="vedPasskeys" style="flex:1;">' + vedRenderPasskeys(_vedCurrent.passkeys) + '</div></div>';
            }

            // Passwort-Historie (einklappbar)
            if ((_vedCurrent.history || []).length) {
                var hh = '<details style="width:100%;"><summary style="cursor:pointer;font-size:0.8rem;color:#6b7280;">'
                    + _vedCurrent.history.length + ' frühere Passwörter</summary>';
                _vedCurrent.history.forEach(function (h) {
                    hh += '<div style="display:flex;align-items:center;gap:0.5rem;margin-top:0.35rem;">'
                        + '<span class="ved-pw-blur mono" data-plain="' + vedEsc(h.password) + '" data-act="vedRevealField" style="flex:1;">' + vedEsc(h.password) + '</span>'
                        + '<span style="font-size:0.72rem;color:#9ca3af;">' + vedEsc((h.changed_at || '').substring(0,16).replace('T',' ')) + '</span>'
                        + '<button class="ved-copy-btn" data-copy="' + vedEsc(h.password) + '" title="Kopieren">&#128203;</button></div>';
                });
                hh += '</details>';
                html += '<div class="ved-field" style="align-items:flex-start;"><div class="ved-field-label" style="padding-top:0.1rem;">Historie</div><div class="ved-field-value">' + hh + '</div></div>';
            }

            body.innerHTML = html;
        })
        .catch(function() {
            body.innerHTML = '<div class="ved-loading">Verbindungsfehler.</div>';
        });
}

function closeEntryModal() {
    document.getElementById('entryDetailModal').classList.remove('open');
    if (_vedTotpInterval) { clearInterval(_vedTotpInterval); _vedTotpInterval = null; }
}

function vedTogglePw() {
    var el  = document.getElementById('vedPassword');
    var btn = document.getElementById('vedPwToggle');
    if (el.classList.contains('ved-pw-blur')) {
        el.classList.remove('ved-pw-blur');
        btn.innerHTML = '&#128584;';
        btn.title = 'Verbergen';
    } else {
        el.classList.add('ved-pw-blur');
        btn.innerHTML = '&#128065;';
        btn.title = 'Anzeigen';
    }
}

function vedCopy(fieldId, btn) {
    var text = document.getElementById(fieldId).textContent.trim();
    vedCopyText(text, btn);
}

function vedCopyText(text, btn) {
    navigator.clipboard.writeText(text).then(function() {
        if (btn) {
            var orig = btn.innerHTML;
            btn.classList.add('copied');
            btn.textContent = '✓';
            setTimeout(function() { btn.classList.remove('copied'); btn.innerHTML = orig; }, 1500);
        }
        vaultScheduleClipboardClear(text);
    });
}

function vedLoadTotp() {
    if (!_vedTotpUrl) return;
    var area = document.getElementById('vedTotpArea');
    var btn  = document.getElementById('vedTotpBtn');
    if (btn) { btn.disabled = true; btn.textContent = '⏳ Laden…'; }

    fetch(_vedTotpUrl)
        .then(function(r) { return r.json(); })
        .then(function(data) {
            if (data.error) { area.textContent = 'Fehler: ' + data.error; return; }
            area.innerHTML = '<span class="ved-totp-code" id="vedTotpCode">' + vedEsc(data.code) + '</span>'
                + '<span style="font-size:0.75rem;color:#9ca3af;margin-left:0.5rem;">noch <span id="vedTotpTimer">' + data.remaining + '</span>s</span>'
                + '<button class="ved-copy-btn" style="margin-left:0.5rem;" data-copy="' + vedEsc(data.code) + '" title="Kopieren">&#128203;</button>';
            vedStartTotpTimer();
        })
        .catch(function() { area.textContent = 'Fehler beim Laden.'; });
}

function vedStartTotpTimer() {
    if (_vedTotpInterval) clearInterval(_vedTotpInterval);
    _vedTotpInterval = setInterval(function() {
        var timerEl = document.getElementById('vedTotpTimer');
        if (!timerEl) { clearInterval(_vedTotpInterval); return; }
        var remaining = parseInt(timerEl.textContent) - 1;
        if (remaining <= 0) {
            clearInterval(_vedTotpInterval);
            vedLoadTotp();
        } else {
            timerEl.textContent = remaining;
        }
    }, 1000);
}

// Maskiert alle Zeichen, die in HTML-Text und in einfach wie doppelt
// gequoteten Attributen Bedeutung haben – damit bleibt jeder Einsatzort sicher,
// unabhängig davon, welche Anführungszeichen die aufbauende Stelle verwendet.
function vedEsc(s) {
    return String(s || '').replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;')
        .replace(/"/g,'&quot;').replace(/'/g,'&#39;').replace(/`/g,'&#96;');
}
// Deterministischer Farbton aus String (für Avatare)
function vaultHue(s) {
    s = String(s || '?');
    var h = 0;
    for (var i = 0; i < s.length; i++) { h = (h * 31 + s.charCodeAt(i)) % 360; }
    return h;
}
// Liefert eine als href sichere URL oder '' (blockiert z. B. javascript:).
function vedSafeUrl(u) {
    u = String(u || '').trim();
    if (/^https?:\/\//i.test(u)) return u;
    if (!/^[a-z][a-z0-9+.\-]*:/i.test(u)) return 'https://' + u; // schemenlos
    return '';
}

// Best-effort: Zwischenablage nach Ablauf leeren, sofern unverändert
var VAULT_CLIP_CLEAR_MS = 45000;
function vaultScheduleClipboardClear(value) {
    setTimeout(function() {
        if (!navigator.clipboard.readText) { navigator.clipboard.writeText('').catch(function(){}); return; }
        navigator.clipboard.readText()
            .then(function(cur) { if (cur === value) navigator.clipboard.writeText('').catch(function(){}); })
            .catch(function(){});
    }, VAULT_CLIP_CLEAR_MS);
}

// Der Ereignisverteiler kopiert über diesen Namen.
window.vaultCopyText = function (text, btn) { vedCopyText(text, btn); };

document.addEventListener('click', function (e) {
    var d = e.target.closest('[data-delfile]');
    if (d) {
        var fileId = parseInt(d.getAttribute('data-delfile'), 10);
        if (fileId > 0) { vedDeleteFile(fileId); }
    }
    var p = e.target.closest('[data-delpasskey]');
    if (p) {
        var passkeyId = parseInt(p.getAttribute('data-delpasskey'), 10);
        if (passkeyId > 0) { vedDeletePasskey(passkeyId); }
    }
});

// Keyboard: Escape closes modal
document.addEventListener('keydown', function(e) {
    if (e.key === 'Escape' && document.getElementById('entryDetailModal').classList.contains('open')) {
        closeEntryModal();
    }
});

// Direktlink ?entry=… (z. B. von der früheren Detailseite): Modal automatisch öffnen
document.addEventListener('DOMContentLoaded', function () {
    var params  = new URLSearchParams(window.location.search);
    var entryId = parseInt(params.get('entry') || '', 10);
    if (!entryId) return;
    var teamId = parseInt(params.get('team_id') || '', 10);
    var base = teamId ? '/vault/teams/' + teamId + '/entries/' + entryId : '/vault/entries/' + entryId;
    openEntryModal(base + '?json=1', base + '/edit', base + '/delete');
    // Parameter aus der URL nehmen, damit Reload/Schließen nicht erneut öffnet
    params.delete('entry');
    var qs = params.toString();
    try { history.replaceState(null, '', window.location.pathname + (qs ? '?' + qs : '')); } catch (e) {}
});

// ── Farbige Passwortdarstellung: Ziffern blau, Sonderzeichen rot ──
function vedColorPw(pw) {
    return String(pw || '').split('').map(function (c) {
        var e = vedEsc(c);
        if (/[0-9]/.test(c)) return '<span style="color:#2563eb;">' + e + '</span>';
        if (/[^a-zA-Z0-9]/.test(c)) return '<span style="color:#dc2626;">' + e + '</span>';
        return e;
    }).join('');
}
// Passwort-Anzeige (ent-blurren zeigt gefärbte Variante)
function vedRevealField(el) {
    if (el.classList.contains('ved-pw-blur')) el.classList.remove('ved-pw-blur');
    else el.classList.add('ved-pw-blur');
}

// ── Anhänge im Modal ──
function vedRenderFiles(files) {
    if (!_vedCurrent) return '';
    var html = '';
    (files || []).forEach(function (f) {
        var kb = Math.max(1, Math.round(f.size_bytes / 1024));
        html += '<div style="display:flex;align-items:center;gap:0.5rem;margin-bottom:0.3rem;">'
            + '<a href="' + vedEsc(_vedCurrent.filesUrl + '/' + f.id) + '" style="flex:1;font-size:0.82rem;">&#128206; ' + vedEsc(f.filename) + '</a>'
            + '<span style="font-size:0.72rem;color:#9ca3af;">' + kb + ' KB</span>'
            + '<button class="ved-copy-btn" title="Löschen" data-delfile="' + vedEsc(f.id) + '">&#128465;</button></div>';
    });
    html += '<label class="btn btn-default btn-xs">'
        + '&#10133; Datei anhängen<input type="file" style="display:none;" data-act-change="vedUploadFile"></label>';
    return html;
}
function vedRenderPasskeys(list) {
    var html = '';
    (list || []).forEach(function (p) {
        var who = p.user_display || p.user_name || '';
        var used = p.last_used_at ? 'zuletzt ' + vedEsc(String(p.last_used_at).substring(0, 16)) : 'noch nicht verwendet';
        html += '<div style="display:flex;align-items:center;gap:0.5rem;margin-bottom:0.3rem;">'
            + '<span style="flex:1;font-size:0.82rem;">&#128273; ' + vedEsc(p.rp_id) + (who ? ' <span class="text-muted">(' + vedEsc(who) + ')</span>' : '') + '</span>'
            + '<span class="text-muted" style="font-size:0.72rem;">' + used + '</span>'
            + '<button class="ved-copy-btn" title="Passkey löschen" data-delpasskey="' + vedEsc(p.id) + '">&#128465;</button></div>';
    });
    return html || '<span class="text-muted" style="font-size:0.82rem;">Keine Passkeys.</span>';
}
function vedDeletePasskey(passkeyId) {
    if (!_vedCurrent || !_vedCurrent.passkeysUrl || !confirm('Passkey löschen? Die Anmeldung damit ist danach nicht mehr möglich.')) return;
    fetch(_vedCurrent.passkeysUrl + '/' + passkeyId + '/delete', {
        method: 'POST',
        headers: { 'Content-Type': 'application/x-www-form-urlencoded', 'X-Requested-With': 'XMLHttpRequest', 'Accept': 'application/json' },
        body: '_csrf=' + encodeURIComponent(vaultCsrf)
    })
    .then(function (r) { return r.json(); })
    .then(function (d) {
        if (!d.success) { alert(d.message || 'Löschen fehlgeschlagen.'); return; }
        _vedCurrent.passkeys = d.passkeys || [];
        var host = document.getElementById('vedPasskeys');
        if (host) { host.innerHTML = vedRenderPasskeys(_vedCurrent.passkeys); }
    })
    .catch(function (e) { alert('Verbindungsfehler: ' + e.message); });
}
function vedUploadFile(input) {
    if (!input.files.length || !_vedCurrent) return;
    var fd = new FormData();
    fd.append('_csrf', vaultCsrf);
    fd.append('file', input.files[0]);
    fetch(_vedCurrent.filesUrl, { method: 'POST', headers: { 'X-Requested-With': 'XMLHttpRequest', 'Accept': 'application/json' }, body: fd })
        .then(function (r) { return r.json(); })
        .then(function (d) {
            if (!d.success) { alert(d.message || 'Upload fehlgeschlagen.'); return; }
            _vedCurrent.files = d.files;
            document.getElementById('vedFiles').innerHTML = vedRenderFiles(d.files);
        })
        .catch(function (e) { alert('Verbindungsfehler: ' + e.message); });
}
function vedDeleteFile(fileId) {
    if (!_vedCurrent || !confirm('Anhang löschen?')) return;
    fetch(_vedCurrent.filesUrl + '/' + fileId + '/delete', {
        method: 'POST',
        headers: { 'Content-Type': 'application/x-www-form-urlencoded', 'X-Requested-With': 'XMLHttpRequest', 'Accept': 'application/json' },
        body: '_csrf=' + encodeURIComponent(vaultCsrf)
    })
    .then(function (r) { return r.json(); })
    .then(function (d) { if (d.success) { _vedCurrent.files = d.files; document.getElementById('vedFiles').innerHTML = vedRenderFiles(d.files); } });
}

// ── TOTP-QR fürs Handy (otpauth-URL über den TOTP-Endpunkt mit ?secret=1) ──
function vedShowTotpQr() {
    if (!_vedCurrent || !_vedTotpUrl) return;
    var host = document.getElementById('vedTotpQr');
    if (host.dataset.shown === '1') { host.innerHTML = ''; host.dataset.shown = '0'; return; }
    fetch(_vedTotpUrl + '?secret=1', { headers: { 'X-Requested-With': 'XMLHttpRequest' } })
        .then(function (r) { return r.json(); })
        .then(function (d) {
            if (!d.otpauth) { host.textContent = 'QR nicht verfügbar.'; return; }
            var sc = document.createElement('script');
            sc.src = '/assets/js/qrcode.min.js';
            sc.onload = function () {
                host.innerHTML = '<canvas id="vedQrCanvas"></canvas>';
                window.QRCode.toCanvas(document.getElementById('vedQrCanvas'), d.otpauth, { width: 150, margin: 1 }, function () {});
                host.dataset.shown = '1';
            };
            sc.onerror = function () { host.textContent = 'QR-Bibliothek nicht ladbar.'; };
            host.appendChild(sc);
        })
        .catch(function () { host.textContent = 'QR nicht verfügbar.'; });
}

// ── Bearbeiten im Modal (füllt das Anlege-Modal als Edit) ──
function vedEditInModal() {
    if (!_vedCurrent) return;
    var e = _vedCurrent.entry;
    closeEntryModal();
    openCreateModal();
    // Tresor und Ordner des Eintrags übernehmen: Das Formular schickt die
    // Ordnerwahl mit, eine leere Wahl bedeutet „kein Ordner".
    var eTeam = (e.team_id === null || e.team_id === undefined || e.team_id === '')
        ? null : parseInt(e.team_id, 10);
    vaultApplyCreateTarget(eTeam, (e.folder_id === null || e.folder_id === undefined) ? null : e.folder_id);
    document.getElementById('cmTitleHead').textContent = 'Eintrag bearbeiten';
    document.getElementById('cmMethod').value = 'PUT';
    document.getElementById('createEntryForm').action = _vedCurrent.editUrl.replace(/\/edit$/, '');
    // Beim Bearbeiten bleibt der Tresor, in dem der Eintrag liegt – ein Wechsel
    // ist ein Verschieben und läuft über die eigene Aktion.
    var vaultGroup = document.getElementById('cmVaultGroup');
    if (vaultGroup) { vaultGroup.style.display = 'none'; }
    document.getElementById('cm_title').value    = e.title || '';
    document.getElementById('cm_username').value = e.username || '';
    document.getElementById('cm_password').value = e.password || '';
    document.getElementById('cm_totp').value     = e.has_totp ? (e.totp || '') : '';
    document.getElementById('cm_notes').value    = e.notes || '';
    document.getElementById('cm_expires').value  = e.expires_at || '';
    // URLs
    var urlList = document.getElementById('cmUrlList');
    var urls = (e.url || '').split('\n').map(function (u) { return u.trim(); }).filter(Boolean);
    urlList.innerHTML = '';
    (urls.length ? urls : ['']).forEach(function (u, i) {
        if (i === 0) {
            urlList.innerHTML = '<div class="vm-url-row"><input type="url" name="urls[]" class="form-control" placeholder="https://example.com"></div>';
            urlList.querySelector('input').value = u;
        } else { cmAddUrl(); urlList.querySelectorAll('input')[i].value = u; }
    });
    // Zusatzfelder
    document.getElementById('cmFieldList').innerHTML = '';
    (_vedCurrent.fields || []).forEach(function (f) { cmAddField(f.name, f.value, f.is_secret); });
}

// ── Einmal-Freigabe ──
function vedShare() {
    if (!_vedCurrent || !_vedCurrent.shareUrl) return;
    var hours = prompt('Freigabe-Link gültig für wie viele Stunden? (1–168)', '24');
    if (hours === null) return;
    fetch(_vedCurrent.shareUrl, {
        method: 'POST',
        headers: { 'Content-Type': 'application/x-www-form-urlencoded', 'X-Requested-With': 'XMLHttpRequest', 'Accept': 'application/json' },
        body: '_csrf=' + encodeURIComponent(vaultCsrf) + '&ttl_hours=' + encodeURIComponent(parseInt(hours, 10) || 24)
    })
    .then(function (r) { return r.json(); })
    .then(function (d) {
        if (!d.success) { alert(d.message || 'Freigabe fehlgeschlagen.'); return; }
        navigator.clipboard.writeText(d.url).catch(function () {});
        window.prompt('Einmal-Link (in Zwischenablage kopiert, gültig ' + d.ttl_hours + 'h, nur EINMAL abrufbar):', d.url);
    })
    .catch(function (e) { alert('Verbindungsfehler: ' + e.message); });
}

// ── Inline-Passwort aus der Zeile kopieren (On-Demand vom Server) ──
function vaultCopyPassword(entryId, btn) {
    var orig = btn.innerHTML;
    fetch('/vault/entries/' + entryId + '/password', { headers: { 'X-Requested-With': 'XMLHttpRequest', 'Accept': 'application/json' } })
        .then(function (r) { return r.json(); })
        .then(function (d) {
            if (d.error || d.password === undefined) { alert(d.error || 'Fehler.'); return; }
            navigator.clipboard.writeText(d.password).then(function () {
                btn.innerHTML = '<i class="fas fa-check" style="color:#16a34a;"></i>';
                setTimeout(function () { btn.innerHTML = orig; }, 1400);
                vaultScheduleClipboardClear(d.password);
            });
        })
        .catch(function (e) { alert('Verbindungsfehler: ' + e.message); });
}

// ── Tastatur: / fokussiert Suche, Pfeile navigieren, Enter öffnet, c kopiert ──
(function () {
    var idx = -1;
    function visibleRows() {
        return Array.prototype.slice.call(document.querySelectorAll('#vTbody tr.v-row:not(.vlt-hidden-row)'));
    }
    function highlight(rows) {
        rows.forEach(function (r, i) { r.classList.toggle('v-kbd-active', i === idx); });
        if (idx >= 0 && rows[idx]) rows[idx].scrollIntoView({ block: 'nearest' });
    }
    document.addEventListener('keydown', function (ev) {
        var tag = (ev.target.tagName || '').toLowerCase();
        var typing = tag === 'input' || tag === 'textarea' || tag === 'select' || ev.target.isContentEditable;
        // Modal offen? Tastatur-Navigation aussetzen
        var modalOpen = document.querySelector('.ved-overlay.open, .vm-overlay.open, .v-modal-overlay.open');
        if (ev.key === '/' && !typing && !modalOpen) {
            ev.preventDefault();
            var si = document.getElementById('vSearchInput');
            if (si) si.focus();
            return;
        }
        if (typing || modalOpen) return;
        var rows = visibleRows();
        if (!rows.length) return;
        if (ev.key === 'ArrowDown') { ev.preventDefault(); idx = Math.min(rows.length - 1, idx + 1); highlight(rows); }
        else if (ev.key === 'ArrowUp') { ev.preventDefault(); idx = Math.max(0, idx - 1); highlight(rows); }
        else if (ev.key === 'Enter' && idx >= 0 && rows[idx]) { ev.preventDefault(); rows[idx].click(); }
        else if ((ev.key === 'c' || ev.key === 'C') && idx >= 0 && rows[idx]) {
            ev.preventDefault();
            var cp = rows[idx].querySelector('.v-actions-cell .btn[title="Passwort kopieren"]');
            if (cp) cp.click();
        }
    });
})();

// ── Drag & Drop: Eintrag auf einen Ordner in der Seitenleiste ziehen ──
(function () {
    var dragId = null, dragTeam = null;
    document.addEventListener('dragstart', function (ev) {
        var row = ev.target.closest && ev.target.closest('tr.v-row');
        if (!row) return;
        dragId   = row.getAttribute('data-id');
        dragTeam = row.getAttribute('data-teamid') || '';
        row.classList.add('v-dragging');
        ev.dataTransfer.effectAllowed = 'move';
    });
    document.addEventListener('dragend', function (ev) {
        var row = ev.target.closest && ev.target.closest('tr.v-row');
        if (row) row.classList.remove('v-dragging');
    });
    // Drop-Ziele: persönliche Ordner-Links (?folder=) und Team-Ordner-Links
    document.querySelectorAll('.v-sidebar-item[href*="folder="], .v-sidebar-item[href="/vault"], .v-sidebar-item[href="/vault?scope=personal"]').forEach(function (el) {
        el.addEventListener('dragover', function (ev) { if (dragId) { ev.preventDefault(); el.classList.add('v-drop-hover'); } });
        el.addEventListener('dragleave', function () { el.classList.remove('v-drop-hover'); });
        el.addEventListener('drop', function (ev) {
            ev.preventDefault();
            el.classList.remove('v-drop-hover');
            if (!dragId) return;
            var href = el.getAttribute('href') || '';
            var fm = href.match(/folder=(\d+)/);
            var tm = href.match(/team_id=(\d+)/);
            var targetFolder = fm ? fm[1] : '';
            var targetTeam   = tm ? tm[1] : '';
            // Nur innerhalb desselben Kontexts per DnD (Team↔Team / persönlich↔persönlich)
            if ((dragTeam || '') !== (targetTeam || '')) {
                alert('Verschieben zwischen persönlichem und Team-Tresor bitte über „Verschieben" im Menü.');
                dragId = null; return;
            }
            var body = '_csrf=' + encodeURIComponent(vaultCsrf)
                + '&target_team_id=' + encodeURIComponent(targetTeam)
                + '&target_folder_id=' + encodeURIComponent(targetFolder);
            fetch('/vault/entries/' + dragId + '/move', {
                method: 'POST',
                headers: { 'Content-Type': 'application/x-www-form-urlencoded', 'X-Requested-With': 'XMLHttpRequest', 'Accept': 'application/json' },
                body: body
            }).then(function (r) { return r.json(); }).then(function (d) { if (d.success) location.reload(); else alert(d.message || 'Verschieben fehlgeschlagen.'); });
            dragId = null;
        });
    });
})();

</script>

<!-- Aktionen-Menü (3 Punkte) – ein gemeinsames, fix positioniertes Menü für alle Zeilen -->
<div id="vRowMenu" class="v-row-menu" style="display:none;">
    <a href="#" data-act="vRmAction" data-args='["show"]'><i class="fas fa-eye"></i> <?= $this->te('Anzeigen') ?></a>
    <a href="#" data-act="vRmAction" data-args='["edit"]'><i class="fas fa-pen"></i> <?= $this->te('Bearbeiten') ?></a>
    <a href="#" data-act="vRmAction" data-args='["move"]'><i class="fas fa-exchange-alt"></i> <?= $this->te('Verschieben') ?></a>
    <a href="#" class="v-rm-danger" data-act="vRmAction" data-args='["delete"]'><i class="fas fa-trash"></i> <?= $this->te('Löschen') ?></a>
</div>
<style>
.v-row-menu { position:fixed; z-index:4000; min-width:180px; background:var(--color-bg); border:1px solid var(--gray-200);
    border-radius:0.45rem; box-shadow:0 10px 30px rgba(0,0,0,0.16); padding:0.25rem 0; }
.v-row-menu a { display:flex; align-items:center; gap:0.55rem; padding:0.5rem 0.9rem; color:var(--gray-700);
    font-size:0.85rem; white-space:nowrap; text-decoration:none; }
.v-row-menu a:hover { background:var(--gray-100); color:var(--gray-900); }
.v-row-menu a.v-rm-danger { color:var(--red-600); }
.v-row-menu a.v-rm-danger:hover { background:var(--red-100); }
.v-row-menu a i { width:15px; text-align:center; color:var(--gray-400); }
.v-row-menu a.v-rm-danger i { color:var(--red-600); }
</style>
<script<?= $this->scriptNonce() ?>>
var _rmData = null;

window.vaultRowMenu = function (btn) {
    var m = document.getElementById('vRowMenu');
    _rmData = {
        json:   btn.getAttribute('data-json'),
        edit:   btn.getAttribute('data-edit'),
        del:    btn.getAttribute('data-del'),
        id:     parseInt(btn.getAttribute('data-id'), 10),
        team:   btn.getAttribute('data-team')   !== '' ? parseInt(btn.getAttribute('data-team'), 10)   : null,
        folder: btn.getAttribute('data-folder') !== '' ? parseInt(btn.getAttribute('data-folder'), 10) : null
    };
    m.style.display = 'block';
    var r  = btn.getBoundingClientRect();
    var mw = m.offsetWidth, mh = m.offsetHeight;
    var left = Math.max(8, Math.min(r.right - mw, window.innerWidth - mw - 8));
    var top  = r.bottom + 4;
    if (top + mh > window.innerHeight - 8) top = Math.max(8, r.top - mh - 4);
    m.style.left = left + 'px';
    m.style.top  = top + 'px';
};

function vRmClose() {
    document.getElementById('vRowMenu').style.display = 'none';
}

window.vRmAction = function (action) {
    if (!_rmData) { vRmClose(); return false; }
    var d = _rmData;
    vRmClose();
    if (action === 'show') {
        openEntryModal(d.json, d.edit, d.del);
    } else if (action === 'edit') {
        window.location.href = d.edit;
    } else if (action === 'move') {
        openMoveModal(d.id, d.team, d.folder);
    } else if (action === 'delete') {
        if (confirm('Eintrag in den Papierkorb verschieben?')) {
            var f = document.createElement('form');
            f.method = 'POST';
            f.action = d.del;
            var i = document.createElement('input');
            i.type = 'hidden'; i.name = '_csrf'; i.value = vaultCsrf;
            f.appendChild(i);
            document.body.appendChild(f);
            f.submit();
        }
    }
    return false;
};

document.addEventListener('click', function (ev) {
    var m = document.getElementById('vRowMenu');
    if (m.style.display !== 'none' && !m.contains(ev.target) && !ev.target.closest('[data-json]')) vRmClose();
});
document.addEventListener('keydown', function (ev) { if (ev.key === 'Escape') vRmClose(); });
window.addEventListener('scroll', vRmClose, true);
</script>
<?= $this->render('vault/_autolock', ['vault_idle_ttl' => $vault_idle_ttl ?? null]) ?>