(function (app) {
    'use strict';

    const DEBUG = false; // Set to true to enable verbose logging

    const dom = app.dom;
    const json = app.json;
    const util = app.util;

    const filesModule = app.modules.files = {};

    var hasFileSystemAccess = typeof window.showDirectoryPicker === 'function';

    function requireFileSystemAccess() {
        if (hasFileSystemAccess) { return true; }
        alert('This feature requires the File System Access API.\n\nPlease use a Chromium-based browser on desktop.');
        return false;
    }

    function hasFiles() {
        return Array.isArray(app.data.files) && app.data.files.length > 0;
    }

    // Three primary-button states: 'scan', 'verify', 'publish'
    var _primaryIntent = null; // null = auto-detect, or explicit 'scan'|'verify'|'publish'
    var _primaryHandler = null;

    function setPrimary(intent) {
        _primaryIntent = intent;
        updateToolbars();
    }

    function setScanningState(active) {
        var wrapper = document.querySelector('.table-wrapper');
        if (!wrapper) { return; }
        if (active) {
            wrapper.classList.add('scanning');
        } else {
            wrapper.classList.remove('scanning');
        }
    }

    function nowMs() {
        if (typeof performance !== 'undefined' && typeof performance.now === 'function') {
            return performance.now();
        }
        return Date.now();
    }

    function formatDuration(ms) {
        const totalSeconds = Math.max(0, Math.round(ms / 1000));
        const minutes = Math.floor(totalSeconds / 60);
        const seconds = totalSeconds % 60;
        if (minutes > 0) {
            return minutes + 'm ' + seconds.toString().padStart(2, '0') + 's';
        }
        return seconds + 's';
    }

    // ── File handle permission and refresh helpers ─────────────────────

    async function ensureDirHandlePermission(dirHandle) {
        if (!dirHandle || typeof dirHandle.requestPermission !== 'function') {
            return;
        }
        const permissionDescriptor = { mode: 'readwrite' };
        const current = await dirHandle.queryPermission(permissionDescriptor);
        if (current === 'granted') {
            return;
        }
        // Request permission (requires user gesture in most browsers)
        const result = await dirHandle.requestPermission(permissionDescriptor);
        if (result !== 'granted') {
            throw new Error('Read/write permission is required for the selected directory');
        }
    }

    async function getFreshFile(fileHandle, fallbackData) {
        if (!fileHandle || typeof fileHandle.getFile !== 'function') {
            throw new Error('No valid file handle');
        }

        // Check permission state before attempting to get file
        try {
            const current = await fileHandle.queryPermission({ mode: 'read' });
            if (current !== 'granted') {
                // Try to request permission (will fail without user gesture)
                try {
                    await fileHandle.requestPermission({ mode: 'read' });
                } catch (permErr) {
                    // Permission request failed (likely no user gesture), skip with warning
                    console.warn('[transmittal] Permission request failed for file:', fileHandle.name || 'unknown', permErr);
                }
                // Check permission again after potential request
                const newPerm = await fileHandle.queryPermission({ mode: 'read' });
                if (newPerm !== 'granted') {
                    if (fallbackData) {
                        // Use fallback data instead of throwing
                        console.log('[transmittal] Permission not granted, using fallback for ' + (fallbackData.path || fallbackData.name || 'unknown file'));
                        return fallbackData;
                    }
                    throw new Error('Permission denied for file access');
                }
            }
        } catch (err) {
            // queryPermission not supported or error
            console.warn('[transmittal] Permission check error:', err);
        }

        try {
            return await fileHandle.getFile();
        } catch (err) {
            if (err && err.name === 'NotReadableError') {
                if (fallbackData) {
                    console.log('[transmittal] NotReadableError, using fallback for ' + (fallbackData.path || fallbackData.name || 'unknown file'));
                    return fallbackData;
                }
                throw err;
            }
            throw err;
        }
    }

    function updateDirectoryIndicator(name) {
        var indicator = dom.qs('#selected-directory');
        if (indicator) {
            indicator.textContent = name ? name : '';
        }
        updateToolbars();
    }

    function sortFilesInPlace(list) {
        if (!Array.isArray(list)) {
            return;
        }
        list.sort(util.compareFilesByTrackingRevision);
    }

    function buildFileHandleMap(files) {
        var map = {};
        (files || []).forEach(function (f) {
            if (f.fileHandle) {
                var key = (f.path || f.name || '').toLowerCase();
                if (key) { map[key] = f.fileHandle; }
            }
        });
        return map;
    }

    function restoreFileHandles(files, handleMap) {
        (files || []).forEach(function (f) {
            var key = (f.path || f.name || '').toLowerCase();
            if (key && handleMap[key]) {
                f.fileHandle = handleMap[key];
            }
        });
    }

    function selfEntryDate() {
        var raw = (dom.qs('#date') || {}).value || '';
        var trimmed = raw.trim();
        if (/^\d{4}-\d{2}-\d{2}$/.test(trimmed)) { return trimmed; }
        var d = new Date(trimmed);
        if (!isNaN(d.getTime())) { return d.toISOString().slice(0, 10); }
        return trimmed;
    }

    function buildSelfEntry() {
        var tracking = (dom.qs('#tracking-number') || {}).value || '';
        var subject = (dom.qs('#subject') || {}).value || '';
        var title = (dom.qs('#title') || {}).value || '';
        var date = selfEntryDate();
        var purpose = (dom.qs('#purpose') || {}).value || '';
        var d = app.modules.data;
        var filename = (d && d.buildFileName)
            ? d.buildFileName({ trackingNumber: tracking, title: title, date: date, purpose: purpose }, { extension: 'html' })
            : '';
        return {
            _isSelf: true,
            path: filename ? ('./' + filename) : '',
            name: filename || '',
            trackingNumber: tracking,
            title: subject || title,
            revision: date,
            status: purpose,
            extension: 'html',
            size: 0,
            fileSize: 0,
            sha256: app.constants.SELF_HASH
        };
    }

    filesModule.buildSelfEntry = buildSelfEntry;

    function canonicalFilePayload(entry) {
        const relativePath = entry.path || entry.name || '';
        const filename = relativePath.split('/').pop() || entry.name || '';
        const pathOnly = relativePath.substring(0, relativePath.lastIndexOf('/')) || '';
        return {
            trackingNumber: entry.trackingNumber || '',
            revision: entry.revision || '',
            status: entry.status || '',
            title: entry.title || '',
            path: pathOnly,
            filename: filename,
            extension: entry.extension || '',
            sha256: entry.sha256 || '',
            fileSize: Number(entry.fileSize || entry.size || 0)
        };
    }

    function updateFilesInJson(files) {
        const data = json.parse();
        const envelope = { ...(data.envelope || {}) };
        const payload = { ...(data.payload || {}) };
        const presentation = { ...(data.presentation || {}) };
        
        const sorted = (Array.isArray(files) ? files : []).slice().sort(util.compareFilesByTrackingRevision);
        // Prepend self-entry, then regular files
        var selfEntry = buildSelfEntry();
        payload.files = [canonicalFilePayload(selfEntry)].concat(sorted.map(canonicalFilePayload));
        
        // Clear digest when files change (invalidates signatures)
        envelope.digest = '';
        envelope.digestedAt = '';
        envelope.signatures = [];
        
        json.setData({ envelope: envelope, payload: payload, presentation: presentation });
    }

    var ARCHIVE_DIR_NAME = '.archive';
    var MAX_DIRECTORY_DEPTH = 48;

    function isHiddenName(name) {
        return !!name && name.startsWith('.');
    }

    function shouldSkipDirectorySegment(name) {
        if (!name) {
            return false;
        }
        if (name === ARCHIVE_DIR_NAME) {
            return true;
        }
        return isHiddenName(name);
    }

    async function collectFilesRecursive(handle, relPath, out, depth) {
        const currentDepth = depth || 0;
        if (DEBUG) console.log('[transmittal] collectFilesRecursive:', relPath, 'depth:', currentDepth, 'kind:', handle.kind);
        
        if (currentDepth > MAX_DIRECTORY_DEPTH) {
            throw new Error('Directory nesting exceeds supported depth');
        }
        if (handle.kind === 'file') {
            if (isHiddenName(handle.name)) {
                if (DEBUG) console.log('[transmittal] Skipping hidden file:', relPath);
                return;
            }
            if (DEBUG) console.log('[transmittal] Adding file to collection:', relPath);
            out.push({ handle, path: relPath, name: handle.name });
            return;
        }
        if (handle.kind !== 'directory') {
            if (DEBUG) console.log('[transmittal] Unknown handle kind:', handle.kind, 'for', relPath);
            return;
        }
        if (shouldSkipDirectorySegment(handle.name) && relPath) {
            if (DEBUG) console.log('[transmittal] Skipping directory segment:', relPath);
            return;
        }
        try {
            if (DEBUG) console.log('[transmittal] Iterating directory:', relPath || 'root');
            let childCount = 0;
            for await (const child of handle.values()) {
                childCount++;
                if (DEBUG) console.log('[transmittal] Found child #' + childCount + ':', child.name, 'kind:', child.kind);
                if (shouldSkipDirectorySegment(child.name)) {
                    continue;
                }
                const childPath = relPath ? (relPath + '/' + child.name) : child.name;
                if (DEBUG) console.log('[transmittal] Recursing into:', childPath);
                await collectFilesRecursive(child, childPath, out, currentDepth + 1);
            }
            if (DEBUG) console.log('[transmittal] Finished iterating directory:', relPath || 'root', 'found', childCount, 'children');
        } catch (err) {
            if (err && err.name === 'NotFoundError') {
                console.error('[transmittal] NotFoundError during directory traversal:', relPath || handle.name || '', {
                    error: err,
                    message: err.message,
                    stack: err.stack,
                    handleName: handle.name,
                    handleKind: handle.kind
                });
                return;
            }
            console.error('[transmittal] Unexpected error during directory traversal:', relPath, err);
            throw err;
        }
    }

    async function collectDirectoryEntries(rootHandle) {
        const entries = [];
        
        if (!rootHandle || rootHandle.kind !== 'directory') {
            throw new Error('Invalid directory handle provided');
        }
        
        try {
            // Start directory scan
            if (DEBUG) console.log('[transmittal] Starting directory scan for:', rootHandle.name);
            
            for await (const child of rootHandle.values()) {
                if (shouldSkipDirectorySegment(child.name)) {
                    continue;
                }
                const childPath = child.name;
                try {
                    await collectFilesRecursive(child, childPath, entries, 1);
                } catch (err) {
                    if (err && err.name === 'NotFoundError') {
                        console.warn('[transmittal] directory entry missing', childPath, err);
                        continue;
                    }
                    throw err;
                }
            }
        } catch (err) {
            if (err && err.name === 'NotFoundError') {
                console.error('[transmittal] NotFoundError during directory scan', {
                    error: err,
                    message: err.message,
                    stack: err.stack,
                    handle: rootHandle,
                    handleName: rootHandle.name
                });
                // Return empty entries instead of throwing
                return entries;
            }
            console.error('[transmittal] Unexpected error during directory scan', err);
            throw err;
        }
        return entries;
    }

    // Promote shared helpers to filesModule for use by sub-modules
    filesModule.nowMs = nowMs;
    filesModule.formatDuration = formatDuration;
    filesModule.updateDirectoryIndicator = updateDirectoryIndicator;
    filesModule.sortFilesInPlace = sortFilesInPlace;
    filesModule.updateFilesInJson = updateFilesInJson;
    filesModule.collectDirectoryEntries = collectDirectoryEntries;

    async function ensureDirHandle() {
        if (typeof window.showDirectoryPicker !== 'function') {
            throw new Error('File System Access API showDirectoryPicker is required');
        }
        const handle = await window.showDirectoryPicker();
        
        // Log the handle details for debugging
        if (DEBUG) {
            console.log('[transmittal] Directory selected:', {
                name: handle.name,
                kind: handle.kind,
                hasQueryPermission: typeof handle.queryPermission === 'function',
                hasRequestPermission: typeof handle.requestPermission === 'function'
            });
        }

        async function ensureRwPermission(dirHandle) {
            if (!dirHandle) {
                throw new Error('Directory handle is undefined');
            }
            if (typeof dirHandle.requestPermission !== 'function') {
                return;
            }
            const permissionDescriptor = { mode: 'readwrite' };
            if (typeof dirHandle.queryPermission === 'function') {
                const current = await dirHandle.queryPermission(permissionDescriptor);
                if (current === 'granted') {
                    return;
                }
            }
            const result = await dirHandle.requestPermission(permissionDescriptor);
            if (result !== 'granted') {
                throw new Error('Read/write permission is required for the selected directory');
            }
        }

        await ensureRwPermission(handle);
        
        // Verify the handle is still valid after permission check
        if (!handle || handle.kind !== 'directory') {
            throw new Error('Directory handle became invalid after permission check');
        }

        app.data.selectedDirHandle = handle;
        updateDirectoryIndicator(handle.name);
        app.state.apply();
        
        if (DEBUG) console.log('[transmittal] Directory handle ready:', handle.name);
        return handle;
    }

    // Parse folder name: YYYY-MM-DD_TRACKING (STATUS) - TITLE
    // Wraps zddc.parseFolder, preserving this module's null-on-invalid contract.
    function parseFolderName(name) {
        var parsed = zddc.parseFolder(name);
        if (!parsed || !parsed.valid) { return null; }
        return {
            date:           parsed.date,
            trackingNumber: parsed.trackingNumber,
            status:         parsed.status,
            title:          parsed.title
        };
    }

    function populateFields(parsed) {
        if (!parsed) { return; }
        var map = {
            'date': parsed.date,
            'tracking-number': parsed.trackingNumber,
            'purpose': parsed.status,
            'subject': parsed.title
        };
        Object.keys(map).forEach(function (id) {
            var el = dom.qs('#' + id);
            if (el && map[id]) {
                el.value = map[id];
            }
        });
    }

    // Shared scan pipeline: collect → populate rows → hash with progress
    // onHash(item, hash) is called per file after hashing; return value sets cell content.
    async function scanEntries(dirHandle, onHash) {
        var entries = await collectDirectoryEntries(dirHandle);
        entries.sort(function (a, b) { return a.path.localeCompare(b.path); });

        // Phase 0: Ensure directory handle has permission
        await ensureDirHandlePermission(dirHandle);

        // Phase 1: merge with existing files
        var existingIndex = filesModule.buildExistingIndex(app.data.files || []);
        var existingPasteKeys = Object.keys(existingIndex);
        setScanningState(true);
        var hashCells = [];
        for (var i = 0; i < entries.length; i++) {
            var entry = entries[i];
            try {
                // Check and refresh handle permission before getFile
                var file = await getFreshFile(entry.handle);
                var parsed = (zddc.parseFilename(file.name) || {});
                var fileData = {
                    path: entry.path,
                    name: file.name,
                    size: file.size,
                    fileSize: file.size,
                    sha256: '',
                    trackingNumber: parsed.trackingNumber,
                    title: parsed.title,
                    revision: parsed.revision,
                    status: parsed.status,
                    extension: parsed.extension || zddc.splitExtension(file.name).extension,
                    fileHandle: entry.handle
                };
                var pasteKey = (fileData.trackingNumber || '').toLowerCase() + '|' + (fileData.revision || '').toLowerCase();
                if (existingPasteKeys.indexOf(pasteKey) === -1) {
                    app.data.files.push(fileData);
                    var hashCell = filesModule.renderSingleRow(fileData, app.data.files.length - 1);
                    hashCells.push({ cell: hashCell, fileData: fileData, handle: entry.handle });
                }
            } catch (err) {
                console.error('[transmittal] Error reading file:', entry.path, err);
            }
            if (i % 20 === 0) {
                await new Promise(function (r) { setTimeout(r, 0); });
            }
        }

        // Phase 2: hash each file with progress bar
        setScanningState(false);
        for (var j = 0; j < hashCells.length; j++) {
            var item = hashCells[j];
            try {
                var fill = item.cell ? item.cell.querySelector('.hash-progress-fill') : null;
                var onProgress = fill ? function (f) {
                    return function (loaded, total) {
                        var pct = total > 0 ? Math.round((loaded / total) * 100) : 0;
                        f.style.width = pct + '%';
                    };
                }(fill) : null;
                var file = await getFreshFile(item.handle);
                var hash = await util.hashFile(file, onProgress);
                item.fileData.sha256 = hash;
                if (onHash) {
                    onHash(item, hash);
                } else if (item.cell) {
                    item.cell.textContent = util.formatShortFileHash(hash);
                }
            } catch (err) {
                console.error('[transmittal] Error hashing file:', item.fileData.path, err);
                if (item.cell) { item.cell.textContent = 'error'; }
            }
        }
        return hashCells;
    }

    function finalizeAfterScan() {
        var handleMap = buildFileHandleMap(app.data.files);
        updateFilesInJson(app.data.files);
        filesModule.render();
        filesModule.loadFromJson({ filesOnly: true });
        restoreFileHandles(app.data.files, handleMap);
        app.state.apply();
    }

    async function selectDirectory(event) {
        if (DEBUG) console.log('[transmittal] ========== SELECT DIRECTORY STARTED ==========');
        const trigger = event && event.currentTarget instanceof HTMLElement ? event.currentTarget : null;
        if (trigger) {
            trigger.disabled = true;
            trigger.classList.add('opacity-60');
        }
        try {
            var dirHandle = app.data.selectedDirHandle;
            if (!dirHandle) {
                dirHandle = await ensureDirHandle();
            }
            populateFields(parseFolderName(dirHandle.name));
            await scanEntries(dirHandle);
            finalizeAfterScan();
            app.markDirty();
            setPrimary('publish');
        } catch (err) {
            if (err && (err.name === 'AbortError' || err.name === 'NotAllowedError')) {
                if (DEBUG) console.log('[transmittal] User cancelled directory selection');
            } else {
                console.error('[transmittal] selectDirectory failed', err);
            }
        } finally {
            setScanningState(false);
            if (trigger) {
                trigger.disabled = false;
                trigger.classList.remove('opacity-60');
            }
        }
    }

    async function writeFileToSelectedDir(filename, contents, mime) {
        if (!app.data.selectedDirHandle) {
            throw new Error('No directory selected');
        }
        const fileHandle = await app.data.selectedDirHandle.getFileHandle(filename, { create: true });
        const writable = await fileHandle.createWritable();
        await writable.write(new Blob([contents], { type: mime || 'text/plain' }));
        await writable.close();
    }

    filesModule.writeFileToSelectedDir = writeFileToSelectedDir;

    // ── Paste helpers ─────────────────────────────────────
    // Expected formats (tab-separated, 3-5 adjacent columns):
    //   3 cols: Tracking \t Title \t Revision+Status   ("A (IFR)")
    //   4 cols: Tracking \t Title \t Revision \t Status
    //   5 cols: Tracking \t Title \t Revision \t Status \t Extension
    var MAX_PASTE_COLS = 5;
    var HEADER_RE = /^(#|tracking|number|title|revision|status|ext)/i;

    function isHeaderLine(cols) {
        if (!cols || !cols.length) { return false; }
        return HEADER_RE.test((cols[0] || '').trim());
    }

    function columnsToFileRow(cols) {
        var tracking = (cols[0] || '').trim();
        if (!tracking) { return null; }
        var revision = (cols[2] || '').trim();
        var status = (cols[3] || '').trim();
        var extension = (cols[4] || '').trim().toLowerCase();

        // 3-column paste: split "A (IFR)" into revision "A" and status "IFR"
        if (cols.length <= 3 && revision) {
            var spaceIdx = revision.indexOf(' ');
            if (spaceIdx > 0) {
                status = revision.substring(spaceIdx + 1).trim().replace(/^\(+/, '').replace(/\)+$/, '');
                revision = revision.substring(0, spaceIdx).trim();
            }
        }
        return {
            trackingNumber: tracking,
            title: (cols[1] || '').trim(),
            revision: revision,
            status: status,
            extension: extension,
            path: '', name: '', size: 0, fileSize: 0, sha256: ''
        };
    }

    // Parse plain-text tab-separated clipboard data.
    // Returns { rows: [...], tooWide: false } or { rows: [], tooWide: true }.
    function parseClipboardText(text) {
        var lines = (text || '').split(/\r?\n/);
        var rows = [];
        for (var i = 0; i < lines.length; i++) {
            var line = lines[i].trim();
            if (!line) { continue; }
            var cols = line.split('\t');
            if (isHeaderLine(cols)) { continue; }
            if (cols.length > MAX_PASTE_COLS) {
                return { rows: [], tooWide: true, colCount: cols.length };
            }
            var row = columnsToFileRow(cols);
            if (row) { rows.push(row); }
        }
        return { rows: rows, tooWide: false };
    }

    function pasteFileKey(trackingNumber, revision) {
        return (trackingNumber || '').toLowerCase() + '|' + (revision || '').toLowerCase();
    }

    function buildExistingIndex(files) {
        var index = {};
        for (var i = 0; i < files.length; i++) {
            index[pasteFileKey(files[i].trackingNumber, files[i].revision)] = i;
        }
        return index;
    }
    filesModule.buildExistingIndex = buildExistingIndex;

    async function handlePasteFiles(mode, clipText) {
        var setStatus = app.modules.data && app.modules.data.setStatus;
        try {
            var text = clipText || await navigator.clipboard.readText();
            var result = parseClipboardText(text);
            if (result.tooWide) {
                if (setStatus) {
                    setStatus('Paste has ' + result.colCount + ' columns (max ' + MAX_PASTE_COLS + '). ' +
                        'Copy only: Tracking, Title, Revision, [Status], [Extension]', 'error');
                }
                return;
            }
            var rows = result.rows;
            if (!rows.length) {
                if (setStatus) { setStatus('No valid rows found. Expected tab-separated: Tracking, Title, Revision, [Status], [Ext]', 'error'); }
                return;
            }

            var added = 0, updated = 0;
            if (mode === 'new') {
                app.data.files = [];
                for (var i = 0; i < rows.length; i++) {
                    app.data.files.push(rows[i]);
                    added++;
                }
            } else {
                var index = buildExistingIndex(app.data.files || []);
                for (var j = 0; j < rows.length; j++) {
                    var row = rows[j];
                    var key = pasteFileKey(row.trackingNumber, row.revision);
                    if (index.hasOwnProperty(key)) {
                        var existing = app.data.files[index[key]];
                        existing.title = row.title;
                        existing.status = row.status;
                        if (row.extension) { existing.extension = row.extension; }
                        updated++;
                    } else {
                        app.data.files.push(row);
                        added++;
                    }
                }
            }

            filesModule.sortFilesInPlace(app.data.files);
            updateFilesInJson(app.data.files);
            filesModule.render();
            app.state.apply();
            app.markDirty();
            setPrimary('verify');

            var msg = mode === 'new'
                ? 'Replaced file list: ' + added + ' rows'
                : added + ' added, ' + updated + ' updated';
            if (setStatus) { setStatus(msg, 'success'); }
        } catch (err) {
            console.error('[transmittal] paste failed', err);
            if (setStatus) { setStatus('Paste failed: ' + (err && err.message ? err.message : err), 'error'); }
        }
    }

    // ── Toolbar visibility per state ────────────────────
    function updateToolbars() {
        var docState = app.state.detectState();
        buildBottomBar(docState);
    }

    function createMenuItem(label, handler, options) {
        var btn = document.createElement('button');
        btn.type = 'button';
        btn.className = 'dropdown-item' + ((options && options.danger) ? ' text-red-600' : '');
        btn.setAttribute('role', 'menuitem');
        btn.textContent = label;
        btn.addEventListener('click', handler);
        return btn;
    }

    function createSeparator(label) {
        var el = document.createElement('div');
        el.className = 'dropdown-separator';
        if (label) { el.textContent = label; }
        return el;
    }

    function applyPrimaryButton(btn, intent) {
        if (_primaryHandler) { btn.removeEventListener('click', _primaryHandler); }
        btn.disabled = false;
        btn.classList.remove('opacity-60');
        if (intent === 'publish') {
            btn.textContent = 'Publish';
            _primaryHandler = function () {
                document.dispatchEvent(new CustomEvent('transmittal:open-publish'));
            };
        } else if (intent === 'verify') {
            btn.textContent = 'Verify Directory';
            _primaryHandler = function () {
                if (!requireFileSystemAccess()) { return; }
                verifyFiles();
            };
        } else {
            btn.textContent = 'Scan Directory';
            _primaryHandler = function () {
                if (!requireFileSystemAccess()) { return; }
                selectDirectory({ currentTarget: null });
            };
        }
        btn.addEventListener('click', _primaryHandler);
    }

    function buildBottomBar(docState) {
        var primaryBtn = dom.qs('#bottom-primary');
        var dropdown = dom.qs('#bottom-dropdown');
        if (!primaryBtn || !dropdown) { return; }

        var isPublished = docState === 'published';
        var d = app.modules.data || {};

        dropdown.innerHTML = '';
        dropdown.classList.add('hidden');

        if (isPublished) {
            applyPrimaryButton(primaryBtn, 'verify');

            dropdown.appendChild(createMenuItem('Copy Table', function () {
                if (d.handleCopyTable) { d.handleCopyTable(); }
            }));
            dropdown.appendChild(createMenuItem('Copy JSON', function () {
                if (d.handleCopyJson) { d.handleCopyJson(); }
            }));
            dropdown.appendChild(createSeparator());
            dropdown.appendChild(createMenuItem('Add Signature', function () {
                if (app.modules.security && app.modules.security.addSignature) {
                    app.modules.security.addSignature();
                }
            }));
            dropdown.appendChild(createMenuItem('Acknowledge Receipt', function () {
                if (app.modules.security && app.modules.security.addSignature) {
                    app.modules.security.addSignature({ label: 'Received By' });
                }
            }));
            dropdown.appendChild(createSeparator());
            dropdown.appendChild(createMenuItem('Revise', function () {
                if (d.handleRevise) { d.handleRevise(); }
            }));
            dropdown.appendChild(createMenuItem('Import HTML', function () {
                if (d.handleImportHtml) { d.handleImportHtml(); }
            }));
            dropdown.appendChild(createMenuItem('Reset', function () {
                if (app.modules.reset && app.modules.reset.handleReset) {
                    app.modules.reset.handleReset();
                }
            }, { danger: true }));
            dropdown.appendChild(createSeparator());
            dropdown.appendChild(createMenuItem('Create Index', function () {
                if (!requireFileSystemAccess()) { return; }
                if (filesModule.generateArchiveRedirects) {
                    filesModule.generateArchiveRedirects().catch(function (err) {
                        console.error('[transmittal] create-index failed', err);
                    });
                }
            }));
        } else {
            // Edit: respect explicit intent, or auto-detect
            var intent = _primaryIntent;
            if (!intent) {
                intent = hasFiles() ? 'verify' : 'scan';
            }
            applyPrimaryButton(primaryBtn, intent);

            dropdown.appendChild(createMenuItem('Scan Directory', function () {
                if (!requireFileSystemAccess()) { return; }
                selectDirectory({ currentTarget: null });
            }));
            dropdown.appendChild(createMenuItem('Verify Directory', function () {
                if (!requireFileSystemAccess()) { return; }
                verifyFiles();
            }));
            dropdown.appendChild(createSeparator());
            dropdown.appendChild(createMenuItem('Publish', function () {
                document.dispatchEvent(new CustomEvent('transmittal:open-publish'));
            }));
            dropdown.appendChild(createMenuItem('Save Draft', function () {
                if (d.handleSaveHtmlDraft) { d.handleSaveHtmlDraft(); }
            }));
            dropdown.appendChild(createMenuItem('Create Folder', async function () {
                var setStatus = d.setStatus;
                try {
                    // Sync UI so payload reflects current form values
                    filesModule.syncUiToJson();
                    var data = json.parse();
                    var payload = (data && data.payload) || {};
                    if (!payload.trackingNumber) {
                        if (setStatus) { setStatus('Enter a tracking number first', 'error'); }
                        return;
                    }
                    var folderName = d.buildFolderName(payload);
                    // Sanitize for filesystem
                    folderName = folderName.replace(/[<>:"/\\|?*]+/g, '_').replace(/_+/g, '_');

                    // Prompt for staging directory
                    var stagingHandle = await window.showDirectoryPicker({ mode: 'readwrite' });
                    var newFolderHandle = await stagingHandle.getDirectoryHandle(folderName, { create: true });

                    // Set as selected directory for subsequent file operations
                    app.data.selectedDirHandle = newFolderHandle;
                    app.data.selectedDirName = folderName;
                    if (filesModule.updateDirectoryIndicator) {
                        filesModule.updateDirectoryIndicator();
                    }

                    // Save a draft into the new folder
                    var pub = app.modules.publish;
                    if (pub && typeof pub.syncUiToJson === 'function' && typeof pub.buildHtmlString === 'function') {
                        await pub.syncUiToJson({ sign: false, computeDigest: false });
                        var html = await pub.buildHtmlString();
                        var draftName = d.buildFileName(
                            ((json.parse() || {}).payload || {}),
                            { extension: 'html', draft: true }
                        );
                        await writeFileToSelectedDir(draftName, html, 'text/html');
                        // Verify both the folder and draft file exist
                        var warnings = [];
                        try {
                            await stagingHandle.getDirectoryHandle(folderName);
                        } catch (_) {
                            warnings.push('folder \"' + folderName + '\" could not be verified');
                        }
                        try {
                            await newFolderHandle.getFileHandle(draftName);
                        } catch (_) {
                            warnings.push('draft file \"' + draftName + '\" could not be verified');
                        }
                        if (warnings.length) {
                            if (setStatus) { setStatus('Warning: ' + warnings.join('; ') + '. Path may be too long for Windows.', 'error'); }
                        } else if (setStatus) {
                            setStatus('Draft saved to ' + folderName + '. Close this file and open ' + draftName + ' from the new folder.', 'success');
                        }
                    } else {
                        if (setStatus) { setStatus('Created folder: ' + folderName, 'success'); }
                    }
                } catch (err) {
                    if (err && err.name === 'AbortError') { return; }
                    console.error('[transmittal] create-folder failed', err);
                    if (setStatus) { setStatus('Create folder failed: ' + (err.message || err), 'error'); }
                }
            }));
            dropdown.appendChild(createSeparator());
            dropdown.appendChild(createMenuItem('Paste New Rows', async function () {
                var text;
                try { text = await navigator.clipboard.readText(); } catch (e) {
                    if (d.setStatus) { d.setStatus('Clipboard access denied', 'error'); }
                    return;
                }
                var result = parseClipboardText(text);
                if (result.tooWide) {
                    if (d.setStatus) {
                        d.setStatus('Paste has ' + result.colCount + ' columns (max ' + MAX_PASTE_COLS + '). ' +
                            'Copy only: Tracking, Title, Revision, [Status], [Extension]', 'error');
                    }
                    return;
                }
                if (!result.rows.length) {
                    if (d.setStatus) { d.setStatus('No valid rows on clipboard', 'error'); }
                    return;
                }
                if (confirm('Replace file list with ' + result.rows.length + ' rows from clipboard?')) {
                    handlePasteFiles('new', text);
                }
            }));
            dropdown.appendChild(createMenuItem('Paste Append Rows', function () {
                handlePasteFiles('append');
            }));
            dropdown.appendChild(createMenuItem('Copy Table', function () {
                if (d.handleCopyTable) { d.handleCopyTable(); }
            }));
            dropdown.appendChild(createMenuItem('Remove Files', function () {
                if (!app.data.files.length) {
                    if (d.setStatus) { d.setStatus('File list is already empty', 'error'); }
                    return;
                }
                if (confirm('Remove all ' + app.data.files.length + ' files from the list? Header info and remarks will be kept.')) {
                    app.data.files = [];
                    updateFilesInJson([]);
                    filesModule.render();
                    app.state.apply();
                    app.markDirty();
                    if (d.setStatus) { d.setStatus('File list cleared', 'success'); }
                }
            }));
            dropdown.appendChild(createSeparator());
            dropdown.appendChild(createMenuItem('Import HTML', function () {
                if (d.handleImportHtml) { d.handleImportHtml(); }
            }));
            dropdown.appendChild(createMenuItem('Copy JSON', function () {
                if (d.handleCopyJson) { d.handleCopyJson(); }
            }));
            dropdown.appendChild(createMenuItem('Paste JSON', function () {
                if (d.handleLoadFromClipboard) { d.handleLoadFromClipboard(); }
            }));
            dropdown.appendChild(createSeparator());
            dropdown.appendChild(createMenuItem('Reset', function () {
                if (app.modules.reset && app.modules.reset.handleReset) {
                    app.modules.reset.handleReset();
                }
            }, { danger: true }));
            dropdown.appendChild(createSeparator());
            dropdown.appendChild(createMenuItem('Create Index', function () {
                if (!requireFileSystemAccess()) { return; }
                if (filesModule.generateArchiveRedirects) {
                    filesModule.generateArchiveRedirects().catch(function (err) {
                        console.error('[transmittal] create-index failed', err);
                    });
                }
            }));
        }
    }

    // ── Bottom bar dropdown toggle ──────────────────────
    function initBottomBarToggle() {
        var toggle = dom.qs('#bottom-toggle');
        var menu = dom.qs('#bottom-dropdown');
        if (!toggle || !menu) { return; }

        toggle.addEventListener('click', function (e) {
            e.stopPropagation();
            var open = !menu.classList.contains('hidden');
            menu.classList.toggle('hidden', open);
            toggle.setAttribute('aria-expanded', String(!open));
        });

        menu.addEventListener('click', function () {
            menu.classList.add('hidden');
            toggle.setAttribute('aria-expanded', 'false');
        });

        document.addEventListener('click', function (e) {
            var container = dom.qs('#bottom-menu');
            if (container && !container.contains(e.target)) {
                menu.classList.add('hidden');
                toggle.setAttribute('aria-expanded', 'false');
            }
        });
    }


    async function refreshDirectory() {
        var dirHandle = app.data.selectedDirHandle;
        if (!dirHandle) { return; }
        try {
            // Ensure directory handle has permission before scanning
            await ensureDirHandlePermission(dirHandle);
            
            // Build expected-hash lookup from current JSON
            var expectedHashes = {};
            var currentData = json.parse();
            var loadedFiles = (currentData && currentData.payload && Array.isArray(currentData.payload.files)) ? currentData.payload.files : [];
            loadedFiles.forEach(function (f) {
                var key = (f.path ? f.path + '/' : '') + f.filename;
                if (f.sha256) { expectedHashes[key.toLowerCase()] = f.sha256.toLowerCase(); }
            });
            var hasExpected = Object.keys(expectedHashes).length > 0;
            var matchCount = 0;
            var mismatchCount = 0;

            var hashCells = await scanEntries(dirHandle, hasExpected ? function (item, hash) {
                if (!item.cell) { return; }
                var display = util.formatShortFileHash(hash);
                var fileKey = (item.fileData.path || item.fileData.name).toLowerCase();
                var expected = expectedHashes[fileKey];
                if (expected && expected === hash.toLowerCase()) {
                    item.cell.innerHTML = '<span class="hash-match" title="Hash matches expected value">\u2713</span> ' + display;
                    matchCount++;
                } else if (expected) {
                    item.cell.innerHTML = '<span class="hash-mismatch" title="Hash does NOT match expected value">\u2717</span> ' + display;
                    mismatchCount++;
                } else {
                    item.cell.textContent = display;
                }
            } : null);

            if (hasExpected && app.modules.data && app.modules.data.setStatus) {
                if (mismatchCount === 0 && matchCount > 0) {
                    app.modules.data.setStatus(matchCount + '/' + hashCells.length + ' files verified', 'success');
                } else if (mismatchCount > 0) {
                    app.modules.data.setStatus(mismatchCount + ' hash mismatch(es) \u2014 ' + matchCount + ' matched', 'error');
                }
            }

            finalizeAfterScan();
            if (!hasExpected) { app.markDirty(); }
        } catch (err) {
            console.error('[transmittal] Refresh failed', err);
        } finally {
            setScanningState(false);
        }
    }

    // ── Verify row helpers ────────────────────────────────────
    function findRowByFileIndex(idx) {
        var cell = document.querySelector('td[data-index="' + idx + '"]');
        return cell ? cell.closest('tr') : null;
    }

    function getHashCell(row) {
        return row ? row.querySelector('td:last-child') : null;
    }

    function setRowVerifyState(row, state) {
        if (!row) { return; }
        row.classList.remove('verify-match', 'verify-mismatch', 'verify-missing', 'verify-new', 'verify-progress');
        if (state) { row.classList.add('verify-' + state); }
    }

    function clearAllVerifyStates() {
        var rows = document.querySelectorAll('tr.verify-match, tr.verify-mismatch, tr.verify-missing, tr.verify-new, tr.verify-progress');
        rows.forEach(function (r) {
            r.classList.remove('verify-match', 'verify-mismatch', 'verify-missing', 'verify-new', 'verify-progress');
        });
    }

    function escapeHtml(str) {
        var div = document.createElement('div');
        div.textContent = str;
        return div.innerHTML;
    }

    function showPathDiff(trackingCell, expectedPath, actualPath) {
        var diffEl = document.createElement('div');
        diffEl.className = 'path-diff';
        var del = document.createElement('del');
        del.textContent = expectedPath;
        var ins = document.createElement('ins');
        ins.textContent = actualPath;
        diffEl.appendChild(del);
        diffEl.appendChild(document.createTextNode(' \u2192 '));
        diffEl.appendChild(ins);
        trackingCell.appendChild(diffEl);
    }

    // Hash every directory entry upfront, before any event-loop yields.
    // On file:// origins in Chromium, FileSystemFileHandle.getFile() fails after
    // macrotask boundaries have elapsed since the handle was created. The fix is
    // to call getFile() + hashFile() back-to-back for each entry in one tight
    // sequential pass, producing a complete index before any UI yields occur.
    // Returns { sizeIndex, nameIndex } where each entry has hash already computed.
    async function buildVerifyIndex(entries, onProgress) {
        var sizeIndex = {};  // fileSize → [candidate]
        var nameIndex = {};  // "tracking\trevision" → candidate
        var entryList = [];  // List of file metadata for later use
        for (var i = 0; i < entries.length; i++) {
            var entry = entries[i];
            try {
                var file = await entry.handle.getFile();
                var hash = await util.hashFile(file);
                var cand = {
                    handle: entry.handle,
                    path: entry.path,
                    name: file.name,
                    fileSize: file.size,
                    hash: hash,
                    matched: false
                };
                // Store file metadata for later use
                entryList.push({
                    handle: entry.handle,
                    path: entry.path,
                    name: file.name,
                    fileSize: file.size,
                    hash: hash,
                    parsed: (zddc.parseFilename(entry.name) || {})
                });
                // size index
                if (!sizeIndex[file.size]) { sizeIndex[file.size] = []; }
                sizeIndex[file.size].push(cand);
                // name index
                var parsed = (zddc.parseFilename(entry.name) || {});
                if (parsed.trackingNumber) {
                    var nkey = parsed.trackingNumber.toLowerCase() + '\t' + (parsed.revision || '').toLowerCase();
                    if (!nameIndex[nkey]) { nameIndex[nkey] = []; }
                    nameIndex[nkey].push(cand);
                }
                if (onProgress) { onProgress(i + 1, entries.length); }
            } catch (err) {
                console.warn('[transmittal] verify skip entry', entry.path, err);
            }
        }
        return { sizeIndex: sizeIndex, nameIndex: nameIndex, entryList: entryList };
    }

    // Find a matching directory entry by sha256 hash.
    function findByHash(sizeIndex, fileSize, expectedHash) {
        if (!expectedHash || fileSize == null) { return null; }
        var candidates = sizeIndex[fileSize];
        if (!candidates) { return null; }
        var target = expectedHash.toLowerCase();
        for (var c = 0; c < candidates.length; c++) {
            var cand = candidates[c];
            if (cand.matched) { continue; }
            if (cand.hash && cand.hash.toLowerCase() === target) {
                cand.matched = true;
                return cand;
            }
        }
        return null;
    }

    // Find a matching directory entry by tracking number + revision.
    function findByTrackingRevision(nameIndex, trackingNumber, revision) {
        var key = (trackingNumber || '').toLowerCase() + '\t' + (revision || '').toLowerCase();
        var candidates = nameIndex[key];
        if (!candidates || !candidates.length) { return null; }
        for (var i = 0; i < candidates.length; i++) {
            if (!candidates[i].matched) {
                candidates[i].matched = true;
                return candidates[i];
            }
        }
        return null;
    }

    // Count unmatched entries in a size index
    function countUnmatched(sizeIndex) {
        var count = 0;
        for (var size in sizeIndex) {
            var group = sizeIndex[size];
            for (var i = 0; i < group.length; i++) {
                if (!group[i].matched) { count++; }
            }
        }
        return count;
    }

    async function verifyFiles() {
        var isPublished = !!app.state.published;
        var setStatus = app.modules.data && app.modules.data.setStatus;
        try {
            var dirHandle = app.data.selectedDirHandle;
            if (!dirHandle) {
                dirHandle = await ensureDirHandle();
            }
            clearAllVerifyStates();
            if (setStatus) { setStatus('Hashing directory\u2026', 'info'); }

            var entries = await collectDirectoryEntries(dirHandle);
            var allFiles = app.data.files || [];

            // Hash every file upfront before any UI yields — on file:// origins,
            // Chromium invalidates FileSystemFileHandle after macrotask boundaries.
            var idx = await buildVerifyIndex(entries);
            var sizeIndex = idx.sizeIndex;
            var nameIndex = idx.nameIndex;
            var entryList = idx.entryList || [];

            // Build pasteKey index for existing files to avoid duplicates
            var existingIndex = buildExistingIndex(allFiles);
            var existingPasteKeys = Object.keys(existingIndex);

            if (isPublished) {
                // ── Published: read-only verification by hash ──
                var verified = 0;
                var missingCount = 0;

                for (var p = 0; p < allFiles.length; p++) {
                    var tf = allFiles[p];
                    var row = findRowByFileIndex(p);
                    var hCell = getHashCell(row);
                    var trackingCell = row ? row.querySelector('td[data-field="trackingNumber"]') : null;

                    setRowVerifyState(row, 'progress');
                    await new Promise(function (r) { setTimeout(r, 0); });

                    var fileSize = tf.fileSize != null ? tf.fileSize : tf.size;
                    var found = findByHash(sizeIndex, fileSize, tf.sha256);

                    if (found) {
                        verified++;
                        tf.fileHandle = found.handle;
                        setRowVerifyState(row, 'match');
                        if (hCell) {
                            hCell.innerHTML = '<span style="color:#166534;font-weight:700;">\u2713</span> ' + escapeHtml(util.formatShortFileHash(tf.sha256));
                        }
                        var expectedPath = tf.path || tf.name || '';
                        if (trackingCell && expectedPath && found.path && expectedPath !== found.path) {
                            showPathDiff(trackingCell, expectedPath, found.path);
                        }
                    } else {
                        missingCount++;
                        setRowVerifyState(row, 'missing');
                        if (hCell) {
                            hCell.innerHTML = '<span style="color:#92400e;">\u26A0 not found</span>';
                        }
                    }
                }

                var extra = countUnmatched(sizeIndex);
                var msg = verified + ' verified';
                if (missingCount) { msg += ', ' + missingCount + ' missing'; }
                if (extra) { msg += ', ' + extra + ' extra in directory'; }
                if (setStatus) {
                    setStatus(msg, missingCount ? 'error' : 'success');
                }
            } else {
                // ── Edit: match rows against directory ──
                // Rows WITH hash+size → match by size+hash
                // Rows WITHOUT hash → match by tracking+revision, then populate hash/size
                var verified = 0;
                var populated = 0;
                var notFound = 0;
                var dirty = false;

                for (var mi = 0; mi < allFiles.length; mi++) {
                    var mf = allFiles[mi];
                    var mRow = findRowByFileIndex(mi);
                    var mhCell = getHashCell(mRow);

                    setRowVerifyState(mRow, 'progress');
                    await new Promise(function (r) { setTimeout(r, 0); });

                    var hasHash = !!mf.sha256;
                    var mFileSize = mf.fileSize != null ? mf.fileSize : mf.size;

                    if (hasHash && mFileSize != null) {
                        var foundByHash = findByHash(sizeIndex, mFileSize, mf.sha256);
                        if (foundByHash) {
                            mf.fileHandle = foundByHash.handle;
                            verified++;
                            setRowVerifyState(mRow, 'match');
                            if (mhCell) {
                                mhCell.innerHTML = '<span style="color:#166534;font-weight:700;">\u2713</span> ' + escapeHtml(util.formatShortFileHash(mf.sha256));
                            }
                        } else {
                            mf.fileHandle = null;
                            notFound++;
                            setRowVerifyState(mRow, 'missing');
                            if (mhCell) {
                                mhCell.innerHTML = '<span style="color:#92400e;">\u26A0 not found</span>';
                            }
                        }
                    } else {
                        var foundByName = findByTrackingRevision(nameIndex, mf.trackingNumber, mf.revision);
                        if (foundByName) {
                            mf.fileHandle = foundByName.handle;
                            mf.path = foundByName.path;
                            mf.name = foundByName.name;
                            mf.size = foundByName.fileSize;
                            mf.fileSize = foundByName.fileSize;
                            mf.sha256 = foundByName.hash;
                            mf.extension = mf.extension || zddc.splitExtension(foundByName.name).extension;
                            populated++;
                            dirty = true;
                            setRowVerifyState(mRow, 'match');
                            if (mhCell) {
                                mhCell.innerHTML = '<span style="color:#166534;font-weight:700;">\u2713</span> ' + escapeHtml(util.formatShortFileHash(mf.sha256));
                            }
                        } else {
                            notFound++;
                            setRowVerifyState(mRow, 'missing');
                            if (mhCell) {
                                mhCell.innerHTML = '<span style="color:#92400e;">\u26A0 not found</span>';
                            }
                        }
                    }
                }

                if (dirty) {
                    updateFilesInJson(allFiles);
                    filesModule.render();
                    app.state.apply();
                    app.markDirty();
                }

                var editMsg = '';
                var parts = [];
                if (verified) { parts.push(verified + ' verified'); }
                if (populated) { parts.push(populated + ' matched'); }
                if (notFound) { parts.push(notFound + ' not found'); }
                editMsg = parts.join(', ') || 'No files to verify';

                // Add new files from directory that don't match existing pasteKeys
                var added = 0;
                for (var ei = 0; ei < entryList.length; ei++) {
                    var entryData = entryList[ei];
                    if (!entryData.parsed.trackingNumber && !entryData.parsed.revision) continue; // Skip files without ZDDC pattern
                    var pasteKey = (entryData.parsed.trackingNumber || '').toLowerCase() + '|' + (entryData.parsed.revision || '').toLowerCase();
                    if (existingPasteKeys.indexOf(pasteKey) === -1) {
                        // New file - use pre-hashed file data
                        var fileData = {
                            path: entryData.path,
                            name: entryData.name,
                            size: entryData.fileSize,
                            fileSize: entryData.fileSize,
                            sha256: '',
                            trackingNumber: entryData.parsed.trackingNumber,
                            title: entryData.parsed.title,
                            revision: entryData.parsed.revision,
                            status: entryData.parsed.status,
                            extension: entryData.parsed.extension || zddc.splitExtension(entryData.name).extension,
                            fileHandle: entryData.handle
                        };
                        app.data.files.push(fileData);
                        added++;
                    }
                }
                if (added > 0) {
                    dirty = true;
                    updateFilesInJson(app.data.files);
                    filesModule.render();
                    app.state.apply();
                    app.markDirty();
                    editMsg = (editMsg ? editMsg + ', ' : '') + added + ' new file(s) added';
                }

                if (setStatus) {
                    setStatus(editMsg, notFound && !added ? 'error' : 'success');
                }
            }
        } catch (err) {
            if (err && (err.name === 'AbortError' || err.name === 'NotAllowedError')) {
                if (DEBUG) console.log('[transmittal] User cancelled verify');
            } else {
                console.error('[transmittal] verifyFiles failed', err);
                if (setStatus) { setStatus('Verify failed: ' + (err.message || err), 'error'); }
            }
        }
    }

    document.addEventListener('transmittal:scan-directory', function () {
        selectDirectory({ currentTarget: dom.qs('#bottom-primary') });
    });

    document.addEventListener('transmittal:verify-directory', function () {
        verifyFiles();
    });

    filesModule.bindActionButtons = function bindActionButtons() {
        // Reveal the menu now that JS is running
        var bottomMenu = dom.qs('#bottom-menu');
        if (bottomMenu) { bottomMenu.hidden = false; }
        var noJsNotice = dom.qs('#no-js-notice');
        if (noJsNotice) { dom.show(noJsNotice, false); }
        initBottomBarToggle();
        updateToolbars();
    };

    filesModule.updateToolbars = updateToolbars;

    filesModule.syncUiToJson = function syncUiToJson() {
        const val = function (selector) {
            const el = dom.qs(selector);
            return el ? (el.value || '') : '';
        };

        function toIsoDate(value) {
            if (!value) {
                return '';
            }
            const trimmed = String(value).trim();
            if (/^\d{4}-\d{2}-\d{2}$/.test(trimmed)) {
                return trimmed;
            }
            const match = trimmed.match(/^(?:[A-Za-z]+\s+)?([A-Za-z]{3,})\s+(\d{1,2}),\s*(\d{4})$/);
            const months = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];
            if (match) {
                const monthIndex = months.indexOf(match[1].slice(0, 3));
                if (monthIndex >= 0) {
                    const day = String(parseInt(match[2], 10)).padStart(2, '0');
                    const month = String(monthIndex + 1).padStart(2, '0');
                    const year = match[3];
                    return year + '-' + month + '-' + day;
                }
            }
            const date = new Date(trimmed);
            if (!Number.isNaN(date.getTime())) {
                return date.toISOString().slice(0, 10);
            }
            return trimmed;
        }

        const data = json.parse();
        const envelope = { ...(data.envelope || {}) };
        const presentation = { ...(data.presentation || {}) };
        
        const payload = {
            version: 1,
            type: val('#type'),
            title: val('#title') || '',
            client: (dom.qs('#owner-name')?.textContent) || '',
            project: (dom.qs('#project-name')?.textContent) || '',
            projectNumber: (dom.qs('#project-number')?.textContent) || '',
            date: toIsoDate(val('#date')),
            trackingNumber: val('#tracking-number'),
            from: val('#from'),
            to: val('#to'),
            purpose: val('#purpose'),
            responseDue: val('#response-due'),
            subject: val('#subject'),
            remarks: val('#remarks'),
            files: (function () {
                var memFiles = Array.isArray(app.data.files) ? app.data.files : [];
                var sorted = memFiles.slice().sort(util.compareFilesByTrackingRevision).map(canonicalFilePayload);
                var self = buildSelfEntry();
                return [canonicalFilePayload(self)].concat(sorted);
            })()
        };
        
        const leftLogoEl = dom.qs('#left-logo');
        const rightLogoEl = dom.qs('#right-logo');
        presentation.leftLogo = leftLogoEl && leftLogoEl.src ? leftLogoEl.src : '';
        presentation.rightLogo = rightLogoEl && rightLogoEl.src ? rightLogoEl.src : '';
        
        json.setData({ envelope: envelope, payload: payload, presentation: presentation });
    };

    filesModule.loadFromJson = function loadFromJson(options) {
        const opts = options || {};
        const data = json.parse();
        const payload = (data && data.payload) || {};
        if (!opts.filesOnly) {
            const assignValue = function (selector, value) {
                const element = dom.qs(selector);
                if (element) {
                    element.value = value || '';
                }
            };
            const assignText = function (selector, value) {
                const element = dom.qs(selector);
                if (element) {
                    element.textContent = value || '';
                }
            };
            assignValue('#type', payload.type || 'Transmittal');
            var typeDisplay = dom.qs('#type-display');
            if (typeDisplay) { typeDisplay.textContent = payload.type || 'Transmittal'; }
            assignValue('#title', payload.title || '');
            assignText('#owner-name', payload.client || '');
            assignText('#project-name', payload.project || '');
            assignText('#project-number', payload.projectNumber || '');
            assignValue('#date', payload.date || '');
            assignValue('#tracking-number', payload.trackingNumber || '');
            assignValue('#from', payload.from || '');
            assignValue('#to', payload.to || '');
            assignValue('#purpose', payload.purpose || '');
            assignValue('#response-due', payload.responseDue || '');
            assignValue('#subject', payload.subject || '');
            const remarks = dom.qs('#remarks');
            if (remarks) {
                remarks.value = payload.remarks || '';
            }
            
            // Load logos from presentation data
            const presentation = (data && data.presentation) || {};
            const leftLogoEl = dom.qs('#left-logo');
            const rightLogoEl = dom.qs('#right-logo');
            if (leftLogoEl && presentation.leftLogo) {
                leftLogoEl.src = presentation.leftLogo;
            }
            if (rightLogoEl && presentation.rightLogo) {
                rightLogoEl.src = presentation.rightLogo;
            }
        }
        var SELF_HASH = app.constants.SELF_HASH;
        const files = Array.isArray(payload.files)
            ? payload.files.filter(function (f) { return f.sha256 !== SELF_HASH; })
            : [];
        app.data.files = files.map(function (entry) {
            const pathOnly = entry.path || '';
            const filename = entry.filename || '';
            const relativePath = pathOnly ? (pathOnly + '/' + filename) : filename;
            
            // When filename is empty (e.g., from pasted files), reconstruct from trackingNumber
            let baseName = filename || relativePath.split('/').pop() || '';
            if (!baseName && entry.trackingNumber) {
                baseName = zddc.joinExtension(entry.trackingNumber, entry.extension || '');
            }

            const fileSize = entry.fileSize || entry.size || 0;
            return {
                path: relativePath,
                name: baseName,
                size: fileSize,
                fileSize: fileSize,
                sha256: entry.sha256 || '',
                trackingNumber: entry.trackingNumber || '',
                title: entry.title || '',
                revision: entry.revision || '',
                status: entry.status || '',
                extension: zddc.splitExtension(baseName).extension
            };
        });
        sortFilesInPlace(app.data.files);
        // Don't call updateFilesInJson here - it clears digest/signatures
        // The files are already in the JSON, we're just loading them into app.data.files
        updateDirectoryIndicator(app.data.selectedDirHandle ? app.data.selectedDirHandle.name : '');
        
        // Render the table to populate it with file data
        filesModule.render();
        
        if (!opts.filesOnly && app.modules.markdown && typeof app.modules.markdown.refresh === 'function') {
            app.modules.markdown.refresh();
        }
        // Apply field visibility after loading data to ensure UI stays in sync
        if (!opts.filesOnly && app.modules.visibility && typeof app.modules.visibility.applyFieldVisibility === 'function') {
            app.modules.visibility.applyFieldVisibility();
        }
    };

    app.registerInit(function () {
        updateDirectoryIndicator(app.data.selectedDirHandle ? app.data.selectedDirHandle.name : '');
        filesModule.bindActionButtons();
        filesModule.setupTableEditing();
    });

    // Auto-load when served by zddc-server: the page lives at
    // /<...>/Staging/<folder>/transmittal.html and that folder IS the
    // working transmittal. Build an HTTP polyfill handle for it,
    // assign it as the selected directory, and run the same scan
    // pipeline the "Add Directory" button does.
    //
    // A 403 on the listing probe means the user can't list this folder —
    // transmittal needs `r` at minimum, so show a clear message rather
    // than silently leaving the editor empty.
    app.registerInit(async function () {
        if (typeof location === 'undefined') { return; }
        if (location.protocol !== 'http:' && location.protocol !== 'https:') { return; }
        if (app.data.selectedDirHandle) { return; }
        try {
            var probe = await window.zddc.source.detectServerRoot();
            if (probe.handle) {
                app.data.selectedDirHandle = probe.handle;
                updateDirectoryIndicator(probe.handle.name);
                // Run the same flow as the "Add Directory" button, minus
                // the click-event plumbing — selectDirectory will skip the
                // picker because selectedDirHandle is already set.
                await selectDirectory({ currentTarget: null });
                return;
            }
            if (probe.status === 403) {
                console.warn('[transmittal] no permission to list directory; transmittal needs `r` at minimum');
                updateDirectoryIndicator('— no permission to list this directory —');
            }
        } catch (err) {
            console.warn('[transmittal] HTTP auto-load failed:', err);
        }
    });
})(window.transmittalApp);
