<?php
/**
 * ⌘K Command Palette for the PPphp console.
 *
 * Self-contained (trigger button + overlay + styles + script). Drop it into any
 * signed-in navbar's right cluster; the trigger sits inline, the overlay is fixed.
 * Reads $baseUrl from the surrounding page, and $project['slug'] when inside a
 * project workspace (so it can offer that project's pages). Destinations are
 * fetched once from GET /command-palette (role-filtered server-side).
 *
 * Keyboard: ⌘K / Ctrl-K open · type to fuzzy-search · ↑/↓ move · Enter open ·
 * Esc close. Theme-aware (--pp-* tokens), responsive, respects reduced motion.
 */
$__cpBase = rtrim((string) ($baseUrl ?? ''), '/');
$__cpSlug = (string) ($project['slug'] ?? '');
?>
<button type="button" class="pp-cmdk-trigger" data-pp-palette-open aria-label="Search (Command K)" title="Search &amp; jump — ⌘K">
    <i class="bi bi-search"></i>
    <span class="pp-cmdk-trigger-txt">Search</span>
    <kbd class="pp-cmdk-kbd"><span class="pp-cmdk-mod">⌘</span>K</kbd>
</button>

<div class="pp-cmdk" id="ppCmdk" data-base="<?= htmlspecialchars($__cpBase) ?>" data-slug="<?= htmlspecialchars($__cpSlug) ?>" hidden>
    <div class="pp-cmdk-backdrop" data-pp-palette-close></div>
    <div class="pp-cmdk-panel" role="dialog" aria-modal="true" aria-label="Command palette">
        <div class="pp-cmdk-search">
            <i class="bi bi-search pp-cmdk-search-ic"></i>
            <input type="text" id="ppCmdkInput" class="pp-cmdk-input" placeholder="Search pages, projects, actions…"
                   autocomplete="off" autocorrect="off" spellcheck="false" role="combobox"
                   aria-expanded="true" aria-controls="ppCmdkList" aria-autocomplete="list">
            <kbd class="pp-cmdk-esc">Esc</kbd>
        </div>
        <div class="pp-cmdk-list" id="ppCmdkList" role="listbox" aria-label="Results"></div>
        <div class="pp-cmdk-foot">
            <span><kbd>↑</kbd><kbd>↓</kbd> navigate</span>
            <span><kbd>↵</kbd> open</span>
            <span><kbd>esc</kbd> close</span>
            <span><kbd>?</kbd> shortcuts</span>
            <span class="pp-cmdk-foot-brand">PPPHP</span>
        </div>
    </div>
</div>

<style>
    .pp-cmdk-trigger { display: inline-flex; align-items: center; gap: 7px; height: 36px; padding: 0 10px;
        border: 1px solid rgba(255,255,255,.14); border-radius: 9px; cursor: pointer;
        background: rgba(255,255,255,.06); color: rgba(255,255,255,.82); font-size: 13px; transition: background .15s, border-color .15s; }
    .pp-cmdk-trigger:hover { background: rgba(255,255,255,.14); border-color: rgba(255,255,255,.24); color: #fff; }
    .pp-cmdk-trigger .bi { font-size: 14px; }
    .pp-cmdk-trigger-txt { line-height: 1; }
    .pp-cmdk-kbd { display: inline-flex; align-items: center; gap: 1px; padding: 1px 5px; border-radius: 6px;
        background: rgba(255,255,255,.13); font: 600 11px/1.4 ui-monospace, SFMono-Regular, Menlo, monospace; color: rgba(255,255,255,.9); }
    .pp-cmdk-mod { font-size: 12px; }
    @media (max-width: 767.98px) { .pp-cmdk-trigger-txt, .pp-cmdk-kbd { display: none; } .pp-cmdk-trigger { padding: 0; width: 36px; justify-content: center; } }

    .pp-cmdk { position: fixed; inset: 0; z-index: 3000; display: flex; align-items: flex-start; justify-content: center; }
    .pp-cmdk[hidden] { display: none; }
    .pp-cmdk-backdrop { position: absolute; inset: 0; background: rgba(2, 6, 23, .55); backdrop-filter: blur(2px); }
    .pp-cmdk-panel { position: relative; margin-top: 12vh; width: min(640px, calc(100vw - 24px)); max-height: 70vh;
        display: flex; flex-direction: column; overflow: hidden; border-radius: 16px;
        background: var(--pp-surface, #fff); color: var(--pp-text, #1e293b);
        border: 1px solid var(--pp-border, #e2e8f0); box-shadow: 0 32px 90px rgba(2, 6, 23, .42);
        animation: pp-cmdk-in .16s ease-out; }
    @keyframes pp-cmdk-in { from { opacity: 0; transform: translateY(-8px) scale(.985); } to { opacity: 1; transform: none; } }
    @media (prefers-reduced-motion: reduce) { .pp-cmdk-panel { animation: none; } }

    .pp-cmdk-search { display: flex; align-items: center; gap: 10px; padding: 13px 15px; border-bottom: 1px solid var(--pp-border, #eef1f6); }
    .pp-cmdk-search-ic { font-size: 17px; color: var(--pp-text-muted, #94a3b8); }
    .pp-cmdk-input { flex: 1; border: 0; outline: 0; background: transparent; color: var(--pp-text, #1e293b);
        font-size: 16px; padding: 2px 0; min-width: 0; }
    .pp-cmdk-input::placeholder { color: var(--pp-text-muted, #94a3b8); }
    .pp-cmdk-esc { padding: 2px 7px; border-radius: 6px; background: var(--pp-surface-muted, #f1f5f9);
        color: var(--pp-text-muted, #64748b); font: 600 11px/1.4 ui-monospace, monospace; }

    .pp-cmdk-list { overflow-y: auto; padding: 6px; }
    .pp-cmdk-grouphd { padding: 9px 12px 4px; font-size: 10.5px; font-weight: 800; letter-spacing: .06em; text-transform: uppercase;
        color: var(--pp-text-muted, #94a3b8); }
    .pp-cmdk-item { display: flex; align-items: center; gap: 11px; padding: 9px 12px; border-radius: 10px; cursor: pointer;
        text-decoration: none; color: inherit; scroll-margin: 8px; }
    .pp-cmdk-item.sel { background: color-mix(in srgb, var(--pp-accent, #4f46e5) 13%, var(--pp-surface, #fff)); }
    .pp-cmdk-ic { flex: 0 0 30px; width: 30px; height: 30px; border-radius: 8px; display: flex; align-items: center; justify-content: center;
        font-size: 14px; color: var(--pp-accent, #4f46e5); background: color-mix(in srgb, var(--pp-accent, #4f46e5) 12%, var(--pp-surface, #fff)); }
    .pp-cmdk-item.sel .pp-cmdk-ic { color: #fff; background: var(--pp-accent, #4f46e5); }
    .pp-cmdk-lbl { flex: 1; min-width: 0; font-size: 14px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
    .pp-cmdk-lbl b { font-weight: 800; color: var(--pp-accent, #4f46e5); }
    .pp-cmdk-item.sel .pp-cmdk-lbl b { color: inherit; text-decoration: underline; }
    .pp-cmdk-hint { flex: 0 0 auto; font-size: 11.5px; color: var(--pp-text-muted, #94a3b8); font-family: ui-monospace, monospace; }
    .pp-cmdk-go { flex: 0 0 auto; font-size: 12px; color: var(--pp-text-muted, #cbd5e1); opacity: 0; }
    .pp-cmdk-item.sel .pp-cmdk-go { opacity: 1; }
    .pp-cmdk-empty { padding: 34px 18px; text-align: center; color: var(--pp-text-muted, #64748b); font-size: 13px; }
    .pp-cmdk-empty i { font-size: 26px; display: block; margin-bottom: 8px; opacity: .5; }

    .pp-cmdk-foot { display: flex; align-items: center; gap: 14px; padding: 9px 14px; border-top: 1px solid var(--pp-border, #eef1f6);
        background: var(--pp-surface-muted, #f8fafc); font-size: 11.5px; color: var(--pp-text-muted, #64748b); }
    .pp-cmdk-foot kbd { padding: 1px 5px; border-radius: 5px; background: var(--pp-surface, #fff); border: 1px solid var(--pp-border, #e2e8f0);
        font: 600 11px/1.4 ui-monospace, monospace; color: var(--pp-text-muted, #64748b); margin-right: 1px; }
    .pp-cmdk-foot-brand { margin-left: auto; font-weight: 800; letter-spacing: .04em; opacity: .6; }
    @media (max-width: 767.98px) {
        .pp-cmdk-panel { margin-top: 0; width: 100vw; max-height: 100dvh; height: 100dvh; border-radius: 0; border: 0; }
        .pp-cmdk-foot { display: none; }
    }
    /* Kind badge (Action vs page) on the right of a row. */
    .pp-cmdk-kind { flex: 0 0 auto; font-size: 10px; font-weight: 800; letter-spacing: .04em; text-transform: uppercase;
        padding: 2px 7px; border-radius: 999px; color: var(--pp-text-muted, #94a3b8);
        background: var(--pp-surface-muted, #f1f5f9); }
    .pp-cmdk-item.sel .pp-cmdk-kind { color: var(--pp-text, #1e293b); }

    /* Transient confirmation toast for actions (copy link, etc.). */
    .pp-cmdk-toast { position: fixed; left: 50%; bottom: 26px; transform: translateX(-50%) translateY(10px);
        z-index: 3200; padding: 10px 16px; border-radius: 11px; font-size: 13px; font-weight: 600;
        color: var(--pp-text, #1e293b); background: var(--pp-surface, #fff); border: 1px solid var(--pp-border, #e2e8f0);
        box-shadow: 0 16px 40px rgba(2, 6, 23, .3); opacity: 0; transition: opacity .2s, transform .2s; display: flex; align-items: center; gap: 8px; }
    .pp-cmdk-toast.in { opacity: 1; transform: translateX(-50%) translateY(0); }
    .pp-cmdk-toast .bi { color: var(--pp-accent, #4f46e5); }
</style>

<script>
(function () {
    if (window.__ppCmdkInit) return; // include once per page
    window.__ppCmdkInit = true;

    var root = document.getElementById('ppCmdk');
    if (!root) return;
    var base = root.dataset.base || '';
    var slug = root.dataset.slug || '';
    var input = document.getElementById('ppCmdkInput');
    var listEl = document.getElementById('ppCmdkList');
    var RECENT_KEY = 'pp-cmdk-recent';

    var all = [];       // full destination pool
    var view = [];      // current filtered+sorted rows
    var sel = 0;        // selected index within view
    var loaded = false, loading = false;

    function esc(s) {
        return String(s == null ? '' : s).replace(/[&<>"]/g, function (c) {
            return ({ '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;' })[c];
        });
    }
    function recents() { try { return JSON.parse(localStorage.getItem(RECENT_KEY) || '[]'); } catch (e) { return []; } }
    function pushRecent(key) {
        try {
            var r = recents().filter(function (p) { return p !== key; });
            r.unshift(key); localStorage.setItem(RECENT_KEY, JSON.stringify(r.slice(0, 6)));
        } catch (e) {}
    }

    // A stable key for recents (nav items key by path, actions by id).
    function keyOf(d) { return d.path ? d.path : ('@' + (d.action || d.label)); }

    // Client-side actions — things that DO something rather than navigate.
    function buildActions() {
        var a = [
            { label: 'Theme: System', action: 'theme:system', icon: 'circle-half', group: 'Theme' },
            { label: 'Theme: Light', action: 'theme:light', icon: 'sun', group: 'Theme' },
            { label: 'Theme: Midnight', action: 'theme:midnight', icon: 'moon-stars', group: 'Theme' },
            { label: 'Theme: Dracula', action: 'theme:dracula', icon: 'moon', group: 'Theme' },
            { label: 'Theme: Gruvbox', action: 'theme:gruvbox', icon: 'brightness-high', group: 'Theme' },
            { label: 'Theme: Rosé Pine', action: 'theme:rosepine', icon: 'flower1', group: 'Theme' },
            { label: 'New project', action: 'new-project', icon: 'plus-lg', group: 'Actions' },
            { label: 'Copy link to this page', action: 'copy-link', icon: 'link-45deg', group: 'Actions' },
            { label: 'Keyboard shortcuts', action: 'shortcuts', icon: 'keyboard', group: 'Actions' },
            { label: 'Sign out', action: 'signout', icon: 'box-arrow-right', group: 'Actions' }
        ];
        if (slug) {
            a.push(
                { label: 'New table', action: 'new-table', icon: 'plus-square', group: 'Project actions' },
                { label: 'Build & deploy', action: 'build', icon: 'box-arrow-up', group: 'Project actions' },
                { label: 'Duplicate this project', action: 'dup-project', icon: 'files', group: 'Project actions' },
                { label: 'Pin to top', action: 'pin-project', icon: 'pin-angle', group: 'Project actions' },
                { label: 'Archive this project', action: 'archive-project', icon: 'archive', group: 'Project actions' },
                { label: 'Toggle sidebar', action: 'toggle-sidebar', icon: 'layout-sidebar', group: 'Project actions' }
            );
        }
        return a;
    }

    var toastEl = null, toastTimer = null;
    function toast(msg, icon) {
        if (!toastEl) { toastEl = document.createElement('div'); toastEl.className = 'pp-cmdk-toast'; document.body.appendChild(toastEl); }
        toastEl.innerHTML = '<i class="bi bi-' + (icon || 'check2-circle') + '"></i>' + esc(msg);
        requestAnimationFrame(function () { toastEl.classList.add('in'); });
        clearTimeout(toastTimer);
        toastTimer = setTimeout(function () { toastEl.classList.remove('in'); }, 2200);
    }

    // Apply a theme by reusing the existing theme switcher (keeps its UI in sync);
    // fall back to setting the same attributes app-theme.js uses.
    function setThemeChoice(choice) {
        var btn = document.querySelector('[data-pp-theme-option="' + choice + '"]');
        if (btn) { btn.click(); }
        else {
            try { localStorage.setItem('ppphp-color-theme', choice); } catch (e) {}
            var sysDark = window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches;
            var theme = choice === 'system' ? (sysDark ? 'dark' : 'light') : (choice === 'light' ? 'light' : 'dark');
            var el = document.documentElement;
            el.setAttribute('data-pp-theme-choice', choice);
            el.setAttribute('data-pp-theme', theme);
            el.setAttribute('data-bs-theme', theme);
            el.style.colorScheme = theme;
            window.dispatchEvent(new CustomEvent('ppphp:themechange', { detail: { theme: theme, choice: choice } }));
        }
        toast('Theme: ' + choice.charAt(0).toUpperCase() + choice.slice(1), 'palette');
    }

    function openModalOrNav(sel, url) {
        var el = document.querySelector(sel);
        if (el && window.bootstrap && window.bootstrap.Modal) { window.bootstrap.Modal.getOrCreateInstance(el).show(); }
        else { window.location.href = url; }
    }

    function metaToken() { var m = document.querySelector('meta[name="csrf-token"]'); return m ? m.getAttribute('content') : ''; }
    function postJson(path, body) {
        body = body || {};
        // Include the token in the BODY (older controllers read $input['_token'])
        // AND via the header (the shared injector adds X-CSRF-Token) — covers both.
        if (!body._token) body._token = metaToken();
        return fetch(base + path, {
            method: 'POST', credentials: 'same-origin',
            headers: { 'Content-Type': 'application/json' },
            body: JSON.stringify(body)
        }).then(function (r) { return r.json().catch(function () { return { success: false }; }); }, function () { return { success: false }; });
    }
    function slugify(s) { return String(s).toLowerCase().trim().replace(/[^a-z0-9]+/g, '-').replace(/^-+|-+$/g, '').slice(0, 60); }
    // Prefer SweetAlert (loaded on project SPAs) for confirm/prompt; fall back to native.
    function confirmDialog(title, text, confirmText) {
        if (window.Swal) {
            return window.Swal.fire({ title: title, text: text, icon: 'question', showCancelButton: true,
                confirmButtonText: confirmText || 'Confirm', reverseButtons: true }).then(function (r) { return !!r.isConfirmed; });
        }
        return Promise.resolve(window.confirm(title + (text ? '\n\n' + text : '')));
    }
    function promptDialog(title, def) {
        if (window.Swal) {
            return window.Swal.fire({ title: title, input: 'text', inputValue: def || '', showCancelButton: true, confirmButtonText: 'Continue' })
                .then(function (r) { return r.isConfirmed ? (r.value || '') : null; });
        }
        return Promise.resolve(window.prompt(title, def || ''));
    }

    function runAction(id) {
        if (id.indexOf('theme:') === 0) { setThemeChoice(id.slice(6)); return; }
        var p = slug ? (base + '/project/' + encodeURIComponent(slug)) : '';
        switch (id) {
            case 'new-project': openModalOrNav('#newProjectModal', base + '/projects?create=1'); break;
            case 'new-table': if (slug) openModalOrNav('#createTableModal', p + '/tables?new=1'); break;
            case 'build': if (slug) window.location.href = p + '/finished'; break;
            case 'toggle-sidebar':
                var t = document.getElementById('sidebarToggle');
                if (t) { t.click(); } else { toast('No sidebar on this page', 'info-circle'); }
                break;
            case 'dup-project':
                if (!slug) break;
                promptDialog('Duplicate project — new name', slug + ' copy').then(function (name) {
                    if (!name) return;
                    toast('Duplicating…', 'hourglass-split');
                    postJson('/project/' + encodeURIComponent(slug) + '/duplicate', { name: name, slug: slugify(name), description: '' }).then(function (j) {
                        if (j && j.success && j.project) { window.location.href = base + '/project/' + encodeURIComponent(j.project.slug) + '/workspace'; }
                        else { toast((j && j.message) || 'Could not duplicate', 'x-circle'); }
                    });
                });
                break;
            case 'pin-project':
                if (!slug) break;
                postJson('/project/' + encodeURIComponent(slug) + '/pin', { pinned: true }).then(function (j) {
                    toast(j && j.success ? 'Pinned to top' : ((j && j.message) || 'Could not pin'), j && j.success ? 'pin-angle-fill' : 'x-circle');
                });
                break;
            case 'archive-project':
                if (!slug) break;
                confirmDialog('Archive this project?', 'It moves to Archived — you can restore it anytime.', 'Archive').then(function (ok) {
                    if (!ok) return;
                    postJson('/project/' + encodeURIComponent(slug) + '/archive', { archived: true }).then(function (j) {
                        if (j && j.success) { toast('Project archived'); setTimeout(function () { window.location.href = base + '/projects'; }, 700); }
                        else { toast((j && j.message) || 'Could not archive', 'x-circle'); }
                    });
                });
                break;
            case 'copy-link':
                var url = window.location.href;
                if (navigator.clipboard && navigator.clipboard.writeText) {
                    navigator.clipboard.writeText(url).then(function () { toast('Link copied'); }, function () { toast('Copy failed', 'x-circle'); });
                } else { toast('Copy not supported', 'x-circle'); }
                break;
            case 'shortcuts': if (window.ppShowShortcuts) { setTimeout(window.ppShowShortcuts, 60); } break;
            case 'signout': window.location.href = base + '/logout'; break;
        }
    }

    function load() {
        if (loaded || loading) return;
        loading = true;
        var url = base + '/command-palette' + (slug ? '?slug=' + encodeURIComponent(slug) : '');
        fetch(url, { credentials: 'same-origin', headers: { 'Accept': 'application/json' } })
            .then(function (r) { return r.json(); })
            .then(function (j) {
                if (!j || j.success === false) { loading = false; return; }
                all = []
                    .concat(j.projectPages || [])
                    .concat(j.console || [])
                    .concat(j.projects || [])
                    .concat(buildActions());
                loaded = true; loading = false;
                render();
            })
            .catch(function () { loading = false; });
    }

    // Fuzzy subsequence scorer — returns {score, marks} or null.
    function fuzzy(q, text) {
        if (!q) return { score: 0, marks: null };
        var t = text.toLowerCase(), needle = q.toLowerCase();
        var ti = 0, score = 0, streak = 0, marks = [];
        for (var ni = 0; ni < needle.length; ni++) {
            var ch = needle[ni], found = -1;
            for (var k = ti; k < t.length; k++) { if (t[k] === ch) { found = k; break; } }
            if (found === -1) return null;
            if (found === ti) { streak++; score += 3 + streak; } else { streak = 0; score += 1; }
            if (found === 0 || /[\s\/\-_.]/.test(t[found - 1])) score += 6; // word-start bonus
            marks.push(found); ti = found + 1;
        }
        if (t.indexOf(needle) === 0) score += 20;      // prefix
        else if (t.indexOf(needle) > -1) score += 8;   // substring
        return { score: score, marks: marks };
    }
    function mark(text, marks) {
        if (!marks) return esc(text);
        var set = {}; marks.forEach(function (i) { set[i] = 1; });
        var out = '';
        for (var i = 0; i < text.length; i++) out += set[i] ? '<b>' + esc(text[i]) + '</b>' : esc(text[i]);
        return out;
    }

    function compute(q) {
        q = q.trim();
        if (!q) {
            // Empty state: what you chose recently, then your recent projects, then the rest.
            var rows = [], seen = {};
            recents().forEach(function (k) {
                var d = all.find(function (x) { return keyOf(x) === k; });
                if (d && !seen[k]) { rows.push({ d: Object.assign({}, d, { group: 'Recent' }), marks: null }); seen[k] = 1; }
            });
            all.filter(function (d) { return d.group === 'Open project'; }).slice(0, 5).forEach(function (d) {
                var k = keyOf(d); if (seen[k]) return;
                rows.push({ d: Object.assign({}, d, { group: 'Recent projects' }), marks: null }); seen[k] = 1;
            });
            all.forEach(function (d) { var k = keyOf(d); if (!seen[k]) { rows.push({ d: d, marks: null }); seen[k] = 1; } });
            return rows;
        }
        var scored = [];
        all.forEach(function (d) {
            var m = fuzzy(q, d.label);
            var hm = d.hint ? fuzzy(q, d.hint) : null;
            var best = m || hm;
            if (!best) return;
            scored.push({ d: d, marks: m ? m.marks : null, score: (m ? m.score : hm.score) });
        });
        scored.sort(function (a, b) { return b.score - a.score; });
        return scored.slice(0, 40);
    }

    function render() {
        var q = input.value;
        view = compute(q);
        if (!view.length) {
            listEl.innerHTML = '<div class="pp-cmdk-empty"><i class="bi bi-compass"></i>' +
                (loaded ? 'No matches for “' + esc(q) + '”' : 'Loading…') + '</div>';
            return;
        }
        sel = 0;
        var html = '', lastGroup = null;
        view.forEach(function (row, i) {
            var d = row.d;
            if (d.group !== lastGroup) { html += '<div class="pp-cmdk-grouphd">' + esc(d.group || '') + '</div>'; lastGroup = d.group; }
            var isAction = !!d.action;
            var href = isAction ? '#' : esc(base + d.path);
            html += '<a class="pp-cmdk-item' + (i === 0 ? ' sel' : '') + '" role="option" data-i="' + i + '" href="' + href + '"' +
                (isAction ? ' data-action="' + esc(d.action) + '"' : '') + '>' +
                '<span class="pp-cmdk-ic"><i class="bi bi-' + esc(d.icon || 'arrow-right-short') + '"></i></span>' +
                '<span class="pp-cmdk-lbl">' + mark(d.label, row.marks) + '</span>' +
                (d.hint ? '<span class="pp-cmdk-hint">' + esc(d.hint) + '</span>' : '') +
                (isAction ? '<span class="pp-cmdk-kind">Action</span>' : '') +
                '<span class="pp-cmdk-go"><i class="bi bi-arrow-return-left"></i></span></a>';
        });
        listEl.innerHTML = html;
    }

    function moveSel(delta) {
        var items = listEl.querySelectorAll('.pp-cmdk-item');
        if (!items.length) return;
        items[sel] && items[sel].classList.remove('sel');
        sel = (sel + delta + items.length) % items.length;
        var el = items[sel];
        el.classList.add('sel');
        el.scrollIntoView({ block: 'nearest' });
    }
    function choose(i) {
        var items = listEl.querySelectorAll('.pp-cmdk-item');
        var el = items[i == null ? sel : i];
        if (!el) return;
        var row = view[parseInt(el.dataset.i, 10)];
        if (!row) return;
        pushRecent(keyOf(row.d));
        if (row.d.action) { close(); runAction(row.d.action); return; }
        window.location.href = el.getAttribute('href');
    }

    function open() {
        root.hidden = false;
        document.body.style.overflow = 'hidden';
        input.value = '';
        if (!all.length) all = buildActions(); // actions are searchable instantly, before the fetch lands
        load();
        render();
        setTimeout(function () { input.focus(); }, 20);
    }
    function close() {
        root.hidden = true;
        document.body.style.overflow = '';
    }
    function toggle() { root.hidden ? open() : close(); }

    // ── wiring ──
    document.addEventListener('keydown', function (e) {
        // ⌘K / Ctrl-K anywhere.
        if ((e.metaKey || e.ctrlKey) && (e.key === 'k' || e.key === 'K')) {
            e.preventDefault(); toggle(); return;
        }
        if (root.hidden) return;
        if (e.key === 'Escape') { e.preventDefault(); close(); }
        else if (e.key === 'ArrowDown') { e.preventDefault(); moveSel(1); }
        else if (e.key === 'ArrowUp') { e.preventDefault(); moveSel(-1); }
        else if (e.key === 'Enter') { e.preventDefault(); choose(); }
    });
    input.addEventListener('input', render);
    listEl.addEventListener('mousemove', function (e) {
        var it = e.target.closest('.pp-cmdk-item'); if (!it) return;
        var items = listEl.querySelectorAll('.pp-cmdk-item');
        var idx = Array.prototype.indexOf.call(items, it);
        if (idx === sel) return;
        items[sel] && items[sel].classList.remove('sel'); sel = idx; it.classList.add('sel');
    });
    listEl.addEventListener('click', function (e) {
        var it = e.target.closest('.pp-cmdk-item'); if (!it) return;
        e.preventDefault(); choose(parseInt(it.dataset.i, 10));
    });
    document.querySelectorAll('[data-pp-palette-open]').forEach(function (b) {
        b.addEventListener('click', function (e) { e.preventDefault(); open(); });
    });
    root.querySelectorAll('[data-pp-palette-close]').forEach(function (b) {
        b.addEventListener('click', close);
    });
    window.ppOpenPalette = open; // let anything else trigger it
}());
</script>
