(function() {
    'use strict';
    // ZDDC landing page — project picker.
    //
    // Two stacked sections:
    //   1. Groups (saved bundles of projects)   — click row to open, edit, or delete
    //   2. Projects (live list from the server) — click row to open one project directly
    //
    // "Select-mode" (entered via "+ New group" or a group's edit ✏ button) shows
    // checkboxes on each project row, a name input, and an action bar with
    // Save / Open visible-checked / Cancel. In default (rest) mode there are no
    // checkboxes; clicking anything just opens the archive.
    //
    // Storage: groups persist in localStorage under `zddc_landing_groups` as
    // an array of { name: string, projects: string[] }. Old `zddc_landing_presets`
    // entries are migrated once on init (project list only — filter/sort state
    // from the old preset model is dropped).

    // ── State ────────────────────────────────────────────────────────────────

    var allProjects = [];                  // [{name, title, url}] from server
    var groups = [];                       // [{name, projects: [name,...]}] from localStorage
    var selected = new Set();              // checked project names (only used in select-mode)
    var columnFilters = { pn: '', pt: '' };
    var columnFilterASTs = { pn: null, pt: null };
    var sortField = 'name';                // 'name' | 'title'
    var sortDirection = 'asc';
    var loadError = null;                  // user-facing error string
    var loadErrorKind = null;              // 'static' | 'auth' | 'non-json' | 'network'

    // selectMode === null            → default mode (click to open)
    // selectMode === { kind: 'create' }
    // selectMode === { kind: 'edit', originalName: '...' }
    var selectMode = null;

    var GROUPS_KEY = 'zddc_landing_groups';
    var LEGACY_PRESETS_KEY = 'zddc_landing_presets';
    var DEFAULT_SORT_FIELD = 'name';
    var DEFAULT_SORT_DIRECTION = 'asc';

    // ── URL state ────────────────────────────────────────────────────────────
    // Only filters and sort persist in the URL. Selection (`?projects=`) used
    // to live here for save-as-preset workflows; with click-to-open + named
    // groups it adds noise and isn't shareable in any useful way (groups are
    // localStorage-only per user).

    function urlSerialize() {
        var p = new URLSearchParams();
        if (columnFilters.pn) p.set('pn', columnFilters.pn);
        if (columnFilters.pt) p.set('pt', columnFilters.pt);
        if (sortField !== DEFAULT_SORT_FIELD) p.set('sort', sortField);
        if (sortDirection !== DEFAULT_SORT_DIRECTION) p.set('dir', sortDirection);
        // Preserve channel selector from existing URL if present.
        var v = new URLSearchParams(location.search).get('v');
        if (v) p.set('v', v);
        var qs = p.toString();
        return qs ? '?' + qs : '';
    }

    function urlPush() {
        var qs = urlSerialize();
        if (qs === location.search) return;
        try {
            history.replaceState(null, '', location.pathname + qs);
        } catch (e) { /* file:// protocol restrictions */ }
    }

    function urlRestore() {
        var p = new URLSearchParams(location.search);
        if (p.has('pn')) {
            columnFilters.pn = p.get('pn');
            columnFilterASTs.pn = parseFilterAST(columnFilters.pn);
        }
        if (p.has('pt')) {
            columnFilters.pt = p.get('pt');
            columnFilterASTs.pt = parseFilterAST(columnFilters.pt);
        }
        if (p.has('sort')) {
            var s = p.get('sort');
            if (s === 'name' || s === 'title') sortField = s;
        }
        if (p.has('dir')) {
            var d = p.get('dir');
            if (d === 'asc' || d === 'desc') sortDirection = d;
        }
    }

    function parseFilterAST(text) {
        if (!text) return null;
        try { return zddc.filter.parse(text); } catch (e) { return null; }
    }

    // ── Server fetch ─────────────────────────────────────────────────────────

    async function fetchProjects() {
        var base = location.origin + location.pathname.replace(/\/[^\/]*$/, '/');
        try {
            var resp = await fetch(base, {
                headers: { 'Accept': 'application/json' },
                cache: 'no-cache',
                credentials: 'same-origin'
            });
            if (!resp.ok) throw new Error('HTTP ' + resp.status);

            var ctype = resp.headers.get('Content-Type') || '';
            var body = await resp.text();
            var trimmed = body.trim();
            var looksLikeJson = trimmed.startsWith('[') || trimmed.startsWith('{');
            if (!ctype.toLowerCase().includes('json') && !looksLikeJson) {
                console.warn('Project-list endpoint returned non-JSON', {
                    requested: base,
                    finalUrl: resp.url,
                    redirected: resp.redirected,
                    contentType: ctype,
                    bodyStart: trimmed.slice(0, 200)
                });
                if (resp.redirected) {
                    loadErrorKind = 'auth';
                    throw new Error("The request was redirected to " + resp.url + ' — likely to an auth/login page. Sign in and reload.');
                }
                if (/<title>\s*Loading\s+ZDDC/i.test(trimmed) || /<title>\s*Loading\s+Archive/i.test(trimmed)) {
                    loadErrorKind = 'static';
                    throw new Error("This deployment doesn't expose a project list. The server is serving static stubs without a zddc-server backend.");
                }
                loadErrorKind = 'non-json';
                throw new Error("The server at " + base + " returned HTML where a JSON project list was expected. Its zddc-server may be too old (no Accept: application/json dispatch on /), a reverse proxy is stripping the header, or the static site at the root has shadowed the API endpoint.");
            }

            var data = JSON.parse(body);
            if (!Array.isArray(data)) throw new Error('Expected a JSON array of projects, got ' + typeof data);
            allProjects = data.map(function(p) {
                return {
                    name: String(p.name || ''),
                    title: String(p.title || ''),
                    url: String(p.url || '')
                };
            }).filter(function(p) { return p.name; });
            return true;
        } catch (e) {
            loadError = e.message || String(e);
            return false;
        }
    }

    // ── Filter / sort ────────────────────────────────────────────────────────

    function visibleProjects() {
        var rows = allProjects.slice();
        if (columnFilterASTs.pn && columnFilterASTs.pn.length > 0) {
            rows = rows.filter(function(r) { return zddc.filter.matches(r.name, columnFilterASTs.pn); });
        }
        if (columnFilterASTs.pt && columnFilterASTs.pt.length > 0) {
            rows = rows.filter(function(r) { return zddc.filter.matches(r.title || '', columnFilterASTs.pt); });
        }
        rows.sort(function(a, b) {
            var av = (a[sortField] || '').toString();
            var bv = (b[sortField] || '').toString();
            if (sortField === 'title') {
                if (!av && bv) return 1;
                if (av && !bv) return -1;
            }
            var cmp = av.localeCompare(bv, undefined, { numeric: true, sensitivity: 'base' });
            return cmp * (sortDirection === 'desc' ? -1 : 1);
        });
        return rows;
    }

    // ── Rendering ────────────────────────────────────────────────────────────

    function render() {
        renderActionBar();
        renderGroups();
        renderProjects();
        renderProjectCount();
        renderGroupCount();
    }

    function renderActionBar() {
        var bar = document.getElementById('selectActionBar');
        var title = document.getElementById('selectModeTitle');
        var input = document.getElementById('groupNameInput');
        var newBtn = document.getElementById('newGroupBtn');
        if (!bar) return;

        if (!selectMode) {
            bar.classList.add('hidden');
            if (newBtn) newBtn.disabled = false;
            return;
        }

        bar.classList.remove('hidden');
        if (newBtn) newBtn.disabled = true;

        if (selectMode.kind === 'create') {
            title.textContent = 'New group:';
            if (document.activeElement !== input) input.value = '';
        } else {
            title.textContent = 'Editing:';
            if (document.activeElement !== input) input.value = selectMode.originalName || '';
        }
    }

    function renderGroups() {
        var container = document.getElementById('groupsContainer');
        if (!container) return;
        if (groups.length === 0) {
            container.innerHTML = '<div class="groups-empty">No saved groups yet. Use <strong>+ New group</strong> to bundle a set of projects.</div>';
            return;
        }
        var html = '<table class="groups-table">';
        html += '<tbody>';
        for (var i = 0; i < groups.length; i++) {
            var g = groups[i];
            var n = escapeHtml(g.name);
            var count = g.projects.length;
            html += '<tr class="groups-row" data-name="' + n + '" onclick="LandingApp.openGroup(event)">';
            html +=   '<td class="groups-row-name">' + n + '</td>';
            html +=   '<td class="groups-row-count">' + count + ' project' + (count === 1 ? '' : 's') + '</td>';
            html +=   '<td class="groups-row-actions">';
            html +=     '<button class="groups-btn-edit" data-name="' + n + '" onclick="event.stopPropagation(); LandingApp.startEditGroup(event)" title="Edit group">✎</button>';
            html +=     '<button class="groups-btn-delete" data-name="' + n + '" onclick="event.stopPropagation(); LandingApp.deleteGroup(event)" title="Delete group">×</button>';
            html +=   '</td>';
            html += '</tr>';
        }
        html += '</tbody></table>';
        container.innerHTML = html;
    }

    function renderProjects() {
        var container = document.getElementById('projectListContainer');
        if (loadError) {
            var heading, help;
            if (loadErrorKind === 'static') {
                heading = 'This server doesn\'t list projects';
                help = 'You\'re on a static deployment (Caddy serving stubs) — there\'s no zddc-server backend here to enumerate projects. '
                    + 'Open a project directly via its URL (e.g. <code>/&lt;project&gt;/Archive/</code>), or ask whoever sent you this link for the project URL they meant.';
            } else if (loadErrorKind === 'auth') {
                heading = 'Sign-in required';
                help = 'The server bounced this request to an auth page. Sign in there, then reload this URL.';
            } else {
                heading = 'Couldn\'t load the project list';
                help = 'Reload the page to try again. If this keeps happening, the server may be down or your link may be stale.';
            }
            container.innerHTML =
                '<div class="project-list-empty">'
                + '<h3>' + escapeHtml(heading) + '</h3>'
                + '<p>' + escapeHtml(loadError) + '</p>'
                + '<p class="landing-empty-help">' + help + '</p>'
                + '</div>';
            return;
        }
        if (allProjects.length === 0) {
            container.innerHTML =
                '<div class="project-list-empty">'
                + '<h3>No projects to show</h3>'
                + '<p>Either you don\'t have access to any projects on this server yet, or none have been set up.</p>'
                + '<p class="landing-empty-help">If someone shared this link with you, ask them which project administrator can grant your account access — and double-check that you\'re signed in with the same email they expected.</p>'
                + '</div>';
            return;
        }

        var rows = visibleProjects();
        var anyTitles = allProjects.some(function(p) { return p.title; });
        var inSelect = !!selectMode;
        var visibleSelected = inSelect ? rows.filter(function(r) { return selected.has(r.name); }).length : 0;
        var headerCheckedState = !inSelect ? 'unchecked'
            : visibleSelected === 0 ? 'unchecked'
            : visibleSelected === rows.length ? 'checked' : 'indeterminate';

        var html = '<table class="project-table' + (inSelect ? ' is-select-mode' : '') + '">';
        html += '<thead>';
        html +=   '<tr class="project-table-headers">';
        if (inSelect) {
            html += '<th class="project-table-checkbox-col">'
                +     '<input type="checkbox" id="headerCheckbox" '
                +       (headerCheckedState === 'checked' ? 'checked ' : '')
                +       'onclick="LandingApp.toggleHeaderCheckbox()" '
                +       'title="Check / uncheck all visible projects">'
                +   '</th>';
        }
        html +=     '<th class="project-table-name-col" data-sort="name" onclick="LandingApp.toggleSort(\'name\')">'
            +        'Project number ' + sortIndicator('name')
            +      '</th>';
        if (anyTitles) {
            html += '<th class="project-table-title-col" data-sort="title" onclick="LandingApp.toggleSort(\'title\')">'
                +     'Title ' + sortIndicator('title')
                +   '</th>';
        }
        html +=   '</tr>';
        html +=   '<tr class="project-table-filters">';
        if (inSelect) html += '<th></th>';
        html +=     '<th><input type="text" class="column-filter ' + (columnFilters.pn ? 'filter-active' : '') + '" '
            +        'data-column="pn" placeholder="filter…" '
            +        'value="' + escapeHtml(columnFilters.pn) + '" '
            +        'oninput="LandingApp.onColumnFilterInput(event)"></th>';
        if (anyTitles) {
            html += '<th><input type="text" class="column-filter ' + (columnFilters.pt ? 'filter-active' : '') + '" '
                +     'data-column="pt" placeholder="filter…" '
                +     'value="' + escapeHtml(columnFilters.pt) + '" '
                +     'oninput="LandingApp.onColumnFilterInput(event)"></th>';
        }
        html +=   '</tr>';
        html += '</thead>';

        var colspan = (inSelect ? 1 : 0) + 1 + (anyTitles ? 1 : 0);
        if (rows.length === 0) {
            html += '<tbody><tr><td colspan="' + colspan + '" class="project-table-no-match">'
                +    'No projects match the current filters.'
                +  '</td></tr></tbody>';
        } else {
            html += '<tbody>';
            for (var i = 0; i < rows.length; i++) {
                var r = rows[i];
                var isSel = inSelect && selected.has(r.name);
                html += '<tr class="project-table-row' + (isSel ? ' is-selected' : '') + '" '
                    +     'data-name="' + escapeHtml(r.name) + '" onclick="LandingApp.onProjectRowClick(event)">';
                if (inSelect) {
                    html += '<td class="project-table-checkbox-col"><input type="checkbox" value="' + escapeHtml(r.name) + '"'
                        +     (isSel ? ' checked' : '')
                        +     ' onclick="event.stopPropagation(); LandingApp.toggleByCheckbox(event)"></td>';
                }
                html +=   '<td class="project-table-name-col">' + escapeHtml(r.name) + '</td>';
                if (anyTitles) {
                    html += '<td class="project-table-title-col">' + (r.title ? escapeHtml(r.title) : '<span class="project-table-no-title">—</span>') + '</td>';
                }
                html += '</tr>';
            }
            html += '</tbody>';
        }
        html += '</table>';
        container.innerHTML = html;

        var headerCb = document.getElementById('headerCheckbox');
        if (headerCb) headerCb.indeterminate = headerCheckedState === 'indeterminate';
    }

    function sortIndicator(field) {
        if (sortField !== field) return '<span class="sort-indicator">↕</span>';
        return '<span class="sort-indicator active">' + (sortDirection === 'asc' ? '▲' : '▼') + '</span>';
    }

    function renderProjectCount() {
        var el = document.getElementById('projectCount');
        if (!el) return;
        if (loadError || allProjects.length === 0) { el.textContent = ''; return; }
        var rows = visibleProjects();
        var base = rows.length === allProjects.length
            ? '(' + allProjects.length + ')'
            : '(' + rows.length + ' of ' + allProjects.length + ')';
        if (selectMode) {
            base = base + ' — ' + selected.size + ' checked';
        }
        el.textContent = base;
    }

    function renderGroupCount() {
        var el = document.getElementById('groupCount');
        if (!el) return;
        el.textContent = groups.length === 0 ? '' : '(' + groups.length + ')';
    }

    // ── Events / actions ─────────────────────────────────────────────────────

    function toggleSort(field) {
        if (sortField === field) {
            sortDirection = sortDirection === 'asc' ? 'desc' : 'asc';
        } else {
            sortField = field;
            sortDirection = 'asc';
        }
        urlPush();
        render();
    }

    function onColumnFilterInput(e) {
        var col = e.target.getAttribute('data-column');
        var val = e.target.value;
        columnFilters[col] = val;
        columnFilterASTs[col] = parseFilterAST(val);
        urlPush();
        renderProjects();
        renderProjectCount();
        var sel = document.querySelector('.column-filter[data-column="' + col + '"]');
        if (sel) {
            sel.focus();
            sel.setSelectionRange(sel.value.length, sel.value.length);
        }
    }

    function onProjectRowClick(e) {
        var row = e.target.closest('.project-table-row');
        if (!row) return;
        var name = row.getAttribute('data-name');
        if (!name) return;
        if (selectMode) {
            // In select-mode the row toggles its checkbox.
            if (selected.has(name)) selected.delete(name);
            else selected.add(name);
            render();
        } else {
            // Default mode: click opens that single project directly.
            openArchiveWith([name]);
        }
    }

    function toggleByCheckbox(e) {
        if (!selectMode) return;
        var cb = e.target;
        var name = cb.value;
        if (cb.checked) selected.add(name);
        else selected.delete(name);
        render();
    }

    function toggleHeaderCheckbox() {
        if (!selectMode) return;
        var cb = document.getElementById('headerCheckbox');
        if (!cb) return;
        var rows = visibleProjects();
        if (cb.checked) rows.forEach(function(r) { selected.add(r.name); });
        else rows.forEach(function(r) { selected.delete(r.name); });
        render();
    }

    // Navigation hook — tests replace this via LandingApp._setNavigate.
    // (Patching window.location.href is unreliable in modern engines.)
    var navigate = function(url) { location.href = url; };

    function openArchiveWith(names) {
        if (!names || names.length === 0) return;
        var base = location.pathname.replace(/\/[^\/]*$/, '/');
        var v = new URLSearchParams(location.search).get('v');

        if (names.length === 1) {
            // Single project → canonical project-subtree URL so the user
            // can edit the address bar to swap archive.html for
            // working/, staging/, reviewing/, etc. zddc-server's
            // availability.go auto-serves the right tool at each.
            // Multi-project (the `else` branch) keeps the ?projects=
            // form because there's no single subtree root.
            var url = base + encodeURIComponent(names[0]) + '/archive.html';
            if (v) url += '?v=' + encodeURIComponent(v);
            navigate(url);
            return;
        }

        var params = ['projects=' + names.map(encodeURIComponent).join(',')];
        if (v) params.push('v=' + encodeURIComponent(v));
        navigate(base + 'archive.html?' + params.join('&'));
    }

    function openGroup(e) {
        var row = e.target.closest('.groups-row');
        if (!row) return;
        var name = row.getAttribute('data-name');
        var g = groups.find(function(x) { return x.name === name; });
        if (!g) return;
        // Drop projects the user no longer has access to (server-side ACL may
        // have changed since the group was saved).
        var accessible = new Set(allProjects.map(function(p) { return p.name; }));
        var openable = g.projects.filter(function(p) { return accessible.has(p); });
        if (openable.length === 0) {
            showWarning('Group "' + name + '" has no projects you currently have access to.');
            return;
        }
        if (openable.length < g.projects.length) {
            // Open with what we can; warn but don't block.
            console.warn('Skipping inaccessible projects in group', name, g.projects.filter(function(p) { return !accessible.has(p); }));
        }
        openArchiveWith(openable);
    }

    function dismissWarning() {
        var el = document.getElementById('accessWarningBanner');
        if (el) el.classList.add('hidden');
    }

    function showWarning(message) {
        var el = document.getElementById('accessWarningBanner');
        var txt = document.getElementById('accessWarningText');
        if (!el || !txt) return;
        txt.textContent = message;
        el.classList.remove('hidden');
    }

    // ── Select-mode (create / edit groups) ───────────────────────────────────

    function startCreateGroup() {
        selectMode = { kind: 'create' };
        selected = new Set();
        render();
        var input = document.getElementById('groupNameInput');
        if (input) input.focus();
    }

    function startEditGroup(e) {
        var btn = e.target.closest('.groups-btn-edit');
        if (!btn) return;
        var name = btn.getAttribute('data-name');
        var g = groups.find(function(x) { return x.name === name; });
        if (!g) return;
        selectMode = { kind: 'edit', originalName: g.name };
        selected = new Set(g.projects);
        render();
        var input = document.getElementById('groupNameInput');
        if (input) input.focus();
    }

    function deleteGroup(e) {
        var btn = e.target.closest('.groups-btn-delete');
        if (!btn) return;
        var name = btn.getAttribute('data-name');
        if (!confirm('Delete group "' + name + '"?')) return;
        groups = groups.filter(function(g) { return g.name !== name; });
        persistGroups();
        render();
    }

    function cancelSelect() {
        selectMode = null;
        selected = new Set();
        render();
    }

    function saveGroup() {
        if (!selectMode) return;
        var input = document.getElementById('groupNameInput');
        if (!input) return;
        var name = (input.value || '').trim();
        if (!name) {
            input.focus();
            return;
        }
        var projects = Array.from(selected).sort();
        if (projects.length === 0) {
            alert('Select at least one project before saving the group.');
            return;
        }
        if (selectMode.kind === 'create') {
            // Reject duplicate names so two groups can't share an identity.
            if (groups.some(function(g) { return g.name === name; })) {
                alert('A group named "' + name + '" already exists. Pick a different name or edit that group instead.');
                return;
            }
            groups.push({ name: name, projects: projects });
        } else {
            // Editing: rename if name changed (and the new name doesn't collide).
            if (name !== selectMode.originalName && groups.some(function(g) { return g.name === name; })) {
                alert('A group named "' + name + '" already exists. Pick a different name.');
                return;
            }
            groups = groups.map(function(g) {
                return g.name === selectMode.originalName
                    ? { name: name, projects: projects }
                    : g;
            });
        }
        persistGroups();
        selectMode = null;
        selected = new Set();
        render();
    }

    function openSelectedVisible() {
        if (!selectMode) return;
        // Rule: open only those that are currently visible (filtered in) AND
        // checked. Filter-hidden but checked items are intentionally left out.
        var visibleNames = new Set(visibleProjects().map(function(r) { return r.name; }));
        var openable = Array.from(selected).filter(function(n) { return visibleNames.has(n); });
        if (openable.length === 0) {
            alert('No checked projects are currently visible. Adjust filters or check more projects to open.');
            return;
        }
        openArchiveWith(openable);
    }

    // ── Persistence ──────────────────────────────────────────────────────────

    function loadGroups() {
        var raw;
        try { raw = localStorage.getItem(GROUPS_KEY); }
        catch (e) { raw = null; }
        if (raw) {
            try {
                var parsed = JSON.parse(raw);
                groups = Array.isArray(parsed) ? parsed.filter(isValidGroup) : [];
                return;
            } catch (e) { /* fall through to legacy */ }
        }
        // One-shot migration: convert old `zddc_landing_presets` (which carried
        // filter+sort state alongside the project list) to plain groups.
        try {
            var legacy = localStorage.getItem(LEGACY_PRESETS_KEY);
            if (!legacy) return;
            var legacyParsed = JSON.parse(legacy);
            if (!Array.isArray(legacyParsed)) return;
            groups = legacyParsed.map(function(p) {
                var projects = (p && p.state && Array.isArray(p.state.projects)) ? p.state.projects : [];
                return { name: String(p && p.name || ''), projects: projects };
            }).filter(isValidGroup);
            persistGroups();
        } catch (e) { groups = []; }
    }

    function isValidGroup(g) {
        return g && typeof g.name === 'string' && g.name.length > 0
            && Array.isArray(g.projects);
    }

    function persistGroups() {
        try { localStorage.setItem(GROUPS_KEY, JSON.stringify(groups)); }
        catch (e) { /* private mode / quota */ }
    }

    // ── Project mode ─────────────────────────────────────────────────────────
    //
    // The same landing tool serves at /<project> as the project-workspace
    // page. Mode is determined from location.pathname:
    //
    //   /                  → 'picker' (existing behavior)
    //   /<single-segment>  → 'project'
    //   /index.html        → 'picker' (file:// + standalone-served root)
    //   anything else      → 'picker' (best-effort fallback)
    //
    // Project mode shows the four canonical lifecycle-stage cards, a
    // "browse all files" link, and a Master Deliverables List section
    // with direct links to any parties currently in archive/. The party
    // list is fetched from <project>/<archive>/?json=1; failures fall
    // back to the static "no parties yet" copy.

    function detectMode() {
        if (typeof location === 'undefined') return 'picker';
        var path = location.pathname || '/';
        // Strip any trailing /index.html so the deployment-root case
        // matches even on file:// or behind some servers.
        var trimmed = path.replace(/\/index\.html$/, '/');
        if (trimmed === '' || trimmed === '/') return 'picker';
        // Single non-slash, non-dot segment → project root.
        var parts = trimmed.split('/').filter(Boolean);
        if (parts.length === 1 && parts[0].indexOf('.') === -1) {
            return 'project';
        }
        return 'picker';
    }

    function projectFromPath() {
        var parts = (location.pathname || '/').split('/').filter(Boolean);
        return parts[0] || '';
    }

    // Render the project-workspace view: title, four stage links, MDL
    // section. Stage hrefs use the no-trailing-slash form so the server
    // routes them to each canonical default tool (mdedit for working/,
    // transmittal for staging/, etc.). Browse-all and the archive deep
    // link use the slash form to land on the directory listing.
    async function renderProjectMode() {
        var project = projectFromPath();
        if (!project) return;

        // Hide picker, show project view.
        var picker = document.getElementById('pickerView');
        var projectView = document.getElementById('projectView');
        if (picker) picker.classList.add('hidden');
        if (projectView) projectView.classList.remove('hidden');

        document.title = project + ' — ZDDC';
        var titleEl = document.getElementById('projectName');
        if (titleEl) titleEl.textContent = project;

        var p = encodeURIComponent(project);
        var stages = [
            { id: 'stageArchive',   href: '/' + p + '/archive' },
            { id: 'stageWorking',   href: '/' + p + '/working' },
            { id: 'stageStaging',   href: '/' + p + '/staging' },
            { id: 'stageReviewing', href: '/' + p + '/reviewing' },
        ];
        for (var i = 0; i < stages.length; i++) {
            var a = document.getElementById(stages[i].id);
            if (a) a.setAttribute('href', stages[i].href);
        }

        var browseAll = document.getElementById('browseAllLink');
        if (browseAll) {
            browseAll.setAttribute('href', '/' + p + '/');
            browseAll.textContent = 'Browse all files →';
        }

        // MDL card. Same shape as the stage cards above, but
        // interactive: a <select> populated with party folders and an
        // Open button that opens the chosen party's MDL. The view
        // auto-renders at any archive/<party>/mdl/ URL even when the
        // folder doesn't exist on disk (zddc-server commit 3fc3717),
        // so we offer the operator-supplied party list directly AND
        // a "type a new party name" affordance via a free-text last
        // option.
        var mdlSelect = document.getElementById('mdlPartySelect');
        var mdlOpenBtn = document.getElementById('mdlOpenBtn');
        var mdlHint    = document.getElementById('mdlHint');
        if (!mdlSelect || !mdlOpenBtn) return;

        // Wire the Open button regardless of fetch outcome — even if
        // party enumeration fails, an operator can still navigate by
        // typing the party folder name in the URL bar.
        function openSelectedMdl() {
            var party = mdlSelect.value;
            if (!party) return;
            // No trailing slash: per the convention, the no-slash form
            // serves the tables tool with the MDL view. The slash form
            // would serve browse, which is not what the user wants when
            // they click "Open MDL".
            var url = '/' + p + '/archive/' + encodeURIComponent(party) + '/mdl';
            window.location.assign(url);
        }
        mdlOpenBtn.addEventListener('click', openSelectedMdl);
        mdlSelect.addEventListener('change', function () {
            mdlOpenBtn.disabled = !mdlSelect.value;
        });
        // Enter inside the select also opens.
        mdlSelect.addEventListener('keydown', function (e) {
            if (e.key === 'Enter') {
                e.preventDefault();
                openSelectedMdl();
            }
        });

        var parties = await fetchParties(p);
        // Repopulate the select. mdlSelect starts with a single
        // "Loading…" option; replace its contents either way.
        mdlSelect.innerHTML = '';
        if (parties == null) {
            // Network error or unauthenticated. Leave the select
            // disabled but visible; user can still navigate via URL.
            var optErr = document.createElement('option');
            optErr.value = '';
            optErr.textContent = '(could not enumerate parties)';
            mdlSelect.appendChild(optErr);
            mdlSelect.disabled = true;
            mdlOpenBtn.disabled = true;
            return;
        }
        if (parties.length === 0) {
            // No parties yet, but per the hint the URL still works for
            // any party name. Give a placeholder option that disables
            // Open until the user has typed something — except we
            // don't have a text input. So just say "(none yet)" and
            // disable. Operator can still navigate via the URL bar.
            var optNone = document.createElement('option');
            optNone.value = '';
            optNone.textContent = '(no party folders yet)';
            mdlSelect.appendChild(optNone);
            mdlSelect.disabled = true;
            mdlOpenBtn.disabled = true;
            if (mdlHint) {
                mdlHint.innerHTML =
                    'No <code>archive/&lt;party&gt;/</code> folders yet. The MDL view still '
                  + 'auto-renders at any such URL, even before the folder exists — type a '
                  + 'party name into the URL bar (or wait for the first transmittal) to start editing.';
            }
            return;
        }
        // Populate the select with each party.
        var optPlaceholder = document.createElement('option');
        optPlaceholder.value = '';
        optPlaceholder.textContent = 'Choose a party…';
        mdlSelect.appendChild(optPlaceholder);
        for (var j = 0; j < parties.length; j++) {
            var opt = document.createElement('option');
            opt.value = parties[j].name;
            opt.textContent = parties[j].name;
            mdlSelect.appendChild(opt);
        }
        mdlSelect.disabled = false;
        // Open button stays disabled until the user picks something.
        mdlOpenBtn.disabled = true;
    }

    // Returns an array of {name, url} for each party folder in the
    // project's archive/, sorted by name. Returns null if the listing
    // can't be fetched (offline, 4xx, or non-JSON response). Returns
    // [] if the listing succeeds but archive/ is empty / has no
    // visible party folders.
    async function fetchParties(projectURL) {
        try {
            var resp = await fetch('/' + projectURL + '/archive/', {
                headers: { 'Accept': 'application/json' },
                cache: 'no-cache',
                credentials: 'same-origin'
            });
            if (!resp.ok) return null;
            var ctype = resp.headers.get('Content-Type') || '';
            if (!ctype.toLowerCase().includes('json')) return null;
            var data = await resp.json();
            if (!Array.isArray(data)) return null;
            // Server emits directories with trailing "/" on the name.
            // Filter to dirs only, strip the slash for display.
            var out = [];
            for (var i = 0; i < data.length; i++) {
                var e = data[i];
                if (!e.is_dir) continue;
                var nm = String(e.name || '').replace(/\/$/, '');
                if (!nm) continue;
                if (nm.charAt(0) === '.' || nm.charAt(0) === '_') continue;
                out.push({ name: nm, url: e.url || ('/' + projectURL + '/archive/' + encodeURIComponent(nm) + '/') });
            }
            out.sort(function (a, b) { return a.name < b.name ? -1 : a.name > b.name ? 1 : 0; });
            return out;
        } catch (e) {
            return null;
        }
    }

    // ── Bootstrap ────────────────────────────────────────────────────────────

    async function init() {
        if (detectMode() === 'project') {
            await renderProjectMode();
            return;
        }
        await initPicker();
    }

    async function initPicker() {
        loadGroups();
        urlRestore();

        var ok = await fetchProjects();
        if (ok) {
            // No URL-restored selection in the new model, but warn about
            // groups that reference inaccessible projects so the user knows
            // why their group is shorter than expected when opened.
            var accessibleNames = new Set(allProjects.map(function(p) { return p.name; }));
            var ghostlyGroups = groups.filter(function(g) {
                return g.projects.some(function(p) { return !accessibleNames.has(p); });
            });
            if (ghostlyGroups.length > 0) {
                console.info('Some saved groups reference projects you no longer have access to; they will open with the accessible subset only.', ghostlyGroups.map(function(g) { return g.name; }));
            }
        }

        render();

        // Wire up keyboard shortcuts in the action-bar input: Enter saves,
        // Escape cancels.
        var input = document.getElementById('groupNameInput');
        if (input) {
            input.addEventListener('keydown', function(e) {
                if (e.key === 'Enter') { e.preventDefault(); saveGroup(); }
                else if (e.key === 'Escape') { e.preventDefault(); cancelSelect(); }
            });
        }
    }

    function escapeHtml(text) {
        var div = document.createElement('div');
        div.textContent = String(text == null ? '' : text);
        return div.innerHTML;
    }

    document.addEventListener('DOMContentLoaded', init);

    // Public API for inline handlers.
    window.LandingApp = {
        init: init,
        toggleByCheckbox: toggleByCheckbox,
        toggleHeaderCheckbox: toggleHeaderCheckbox,
        toggleSort: toggleSort,
        onColumnFilterInput: onColumnFilterInput,
        onProjectRowClick: onProjectRowClick,
        openGroup: openGroup,
        startCreateGroup: startCreateGroup,
        startEditGroup: startEditGroup,
        deleteGroup: deleteGroup,
        cancelSelect: cancelSelect,
        saveGroup: saveGroup,
        openSelectedVisible: openSelectedVisible,
        dismissWarning: dismissWarning,
        // Project-mode entry points (also tested directly).
        detectMode: detectMode,
        renderProjectMode: renderProjectMode,
        // Test-only: override the navigation function (avoids the messy
        // browser-locked-down state of window.location).
        _setNavigate: function(fn) { navigate = fn; }
    };

})();
