'use strict';
// ═══════════════════════════════════════════════
// js/events.js — Global event delegation
// Replaces all inline onclick=/oninput=/etc. for CSP compliance
// (script-src without 'unsafe-inline')
// v20260314a

// ── Click dispatcher ────────────────────────────────────────────────────────
document.addEventListener('click', function _dispatchClick(e) {
  // Feed thumbnails: stop propagation so parent feed-item doesn't navigate
  if (e.target.classList.contains('feed-thumb')) { e.stopPropagation(); return; }

  const el = e.target.closest('[data-action]');
  if (!el) return;

  const a  = el.dataset.action;
  const a0 = el.dataset.a0;
  const a1 = el.dataset.a1;
  const a2 = el.dataset.a2;
  const a3 = el.dataset.a3;

  switch (a) {
    // ── App / ToS ──────────────────────────────────────────────────────────
    case 'acceptTos':           acceptTos(); break;
    case 'exitApp':             location.href = 'https://www.google.com'; break;

    // ── Navigation ────────────────────────────────────────────────────────
    case 'tab':                 tab(a0); break;
    case 'toggleSidebar':       toggleSidebar(); break;
    case 'showCurtain':         showCurtain(); break;
    case 'openProfile':         openProfile(); break;
    case 'openProfileSidebar':  openProfile(); closeSidebar(); break;
    case 'tabSettings':         tab('settings'); closeSidebar(); break;
    case 'setTheme':            setTheme(a0); break;
    case 'shareMyCode':         if (typeof shareMyCode === 'function') shareMyCode(a0); break;
    case 'applyPermPreset':     if (typeof applyPermPreset === 'function') applyPermPreset(a0, a1); break;
    case 'shbDismiss':          shbDismiss(a0); break;
    case 'autoBackupSetup':     autoBackupSetup(); break;
    case 'autoBackupDisable':   autoBackupDisable(); break;
    case 'openCiRef':           openCiRef(a0); break;
    case 'ciRefPick':           ciRefPick(a0); break;

    // ── Modal ──────────────────────────────────────────────────────────────
    case 'closeModal':          closeModal(a0); break;
    case 'openModal':           closeSidebar(); openModal(a0); break;
    case 'selectSelf':          el.select(); break;

    // ── Sidebar ───────────────────────────────────────────────────────────
    case 'exportBackup':        exportBackup(); break;
    case 'importBackupPrompt':  importBackupPrompt(); break;
    case 'encryptLocalData':    closeSidebar(); showMigrationModal(); break;
    case 'encryptAndBackup':    encryptAndBackup(); break;
    case 'toggleBlurImages':    toggleBlurImages(); break;
    case 'toggleNetworkBtn':    toggleNetworkBtn(); break;
    case 'toggleBuddiesBtn':    toggleBuddiesBtn(); break;
    case 'togglePublicBtn':     togglePublicBtn(); break;
    case 'toggleSizingGuide':   toggleSizingGuide(e); break;
    case 'closeDropdowns':      closeDeviceDropdown(); closeBuddyDropdown(); _hideDropdownBackdrop(); break;

    // ── Header dots ───────────────────────────────────────────────────────
    case 'toggleDeviceDropdown': toggleDeviceDropdown(); break;
    case 'toggleBuddyDropdown':  toggleBuddyDropdown(); break;
    case 'closeDeviceDropdown':  closeDeviceDropdown(); break;
    case 'closeBuddyDropdown':   closeBuddyDropdown(); break;
    case 'toggleNotifDropdown': toggleNotifDropdown(); break;
    case 'closeNotifDropdown':  closeNotifDropdown(); break;
    case 'deleteNotif':         deleteNotif(a0); break;
    case 'clearAllNotifs':      clearAllNotifs(); break;
    case 'notifNavigate':       if (typeof _notifNavigate === 'function') _notifNavigate(a0); break;
    case 'saveNotifSettings':   saveNotifSettingsUI(); break;
    case 'openChatPopup':      openChatPopup(a0); closeBuddyDropdown(); break;
    case 'msgrOpen':               msgrOpenThread(a0); break;
    case 'msgrBack':               msgrBack(); break;
    case 'msgrSend':               sendMsgrMsg(); break;
    case 'msgrTogglePhotoPicker':  msgrTogglePhotoPicker(); break;
    case 'msgrOpenProfile':        msgrOpenProfile(); break;
    case 'showDemoProfile':        showDemoProfile(); break;
    case 'closeChatPopup':     closeChatPopup(); break;
    case 'closeChatModal':     closeChatPopup(); break;
    case 'sendChatPopupMsg':   sendChatPopupMsg(); break;
    case 'sendChatModalMsg':   sendChatPopupMsg(); break;
    case 'toggleChatPhotoPicker': if (typeof toggleChatPhotoPicker === 'function') toggleChatPhotoPicker(); break;
    case 'togglePeerChatPhotoPicker': if (typeof togglePeerChatPhotoPicker === 'function') togglePeerChatPhotoPicker(a0); break;
    case 'chatPhotoPickerTab':    if (typeof chatPhotoPickerTab === 'function') chatPhotoPickerTab(a0); break;
    case 'chatSendPhoto':         if (typeof chatSendPhoto === 'function') chatSendPhoto(a0, a1); break;
    case 'openBuddyProfileDropdown': closeBuddyDropdown(); openBuddyProfile(a0); break;

    // ── Activity feed ─────────────────────────────────────────────────────
    case 'activityViewOwnEntry': tab('journal'); setTimeout(() => selectEntry(a0), 50); break;
    case 'activityViewBuddyEntry': {
      const _ac = (typeof getContacts === 'function' ? getContacts() : []).find(x => x.pubKeyB64 === a0);
      const _ae = _ac?.journal?.entries?.find(x => x.id === a1);
      if (_ac && _ae) showContactEntryDetail(_ac, _ae, typeof isOnline === 'function' && isOnline(a0));
      break;
    }
    case 'activityViewOwnGallery':
      if (typeof _activityOpenOwnGalleryImage === 'function') _activityOpenOwnGalleryImage(a0, a1);
      break;
    case 'activityViewBuddyGallery':
      if (typeof _activityOpenBuddyGalleryImage === 'function') _activityOpenBuddyGalleryImage(a0, a1);
      break;
    case 'activitySendKot':
      if (typeof p2pSendKot === 'function') { p2pSendKot(a0, a1); renderActivityPage(); }
      break;
    case 'activitySendComment': {
      const _ta = document.querySelector(`.af-comment-input[data-peer="${a0}"][data-entry="${a1}"]`);
      const _txt = _ta?.value?.trim();
      if (_txt && typeof p2pSendNote === 'function') {
        const existing = typeof getSentNote === 'function' ? getSentNote(a0, a1) : null;
        p2pSendNote(a0, a1, _txt, existing ? new Date().toISOString() : null);
        if (typeof recordNoteSent === 'function') recordNoteSent(a0, a1, _txt);
        toast('Comment sent');
      }
      break;
    }
    case 'activitySendGalleryKot':
      if (typeof p2pSendGalleryKot === 'function') { p2pSendGalleryKot(a0, a1, a2); renderActivityPage(); }
      break;
    case 'activitySendGalleryComment': {
      const _gta = document.querySelector(`.af-comment-input[data-peer="${a0}"][data-item="${a2}"]`);
      const _gtxt = _gta?.value?.trim();
      if (_gtxt && typeof p2pSendGalleryPhotoNote === 'function') {
        p2pSendGalleryPhotoNote(a0, a1, a2, _gtxt);
        const sl16 = a0.slice(0, 16);
        if (typeof recordGallerySentNote === 'function') recordGallerySentNote(sl16, a1, a2, _gtxt);
        toast('Comment sent');
      }
      break;
    }

    // ── Journal ───────────────────────────────────────────────────────────
    case 'newEntry':            newEntry(); break;
    case 'journalBack':         _journalBack(); break;
    case 'clearJournalFilters': _clearJournalFilters(); break;
    case 'selectEntry':         selectEntry(a0); break;
    case 'tabSelectEntry':      tab('journal'); setTimeout(() => selectEntry(a0), 50); break;
    case 'openChatComparison': {
      try {
        const _cmpRef = JSON.parse(a0);
        if (typeof loadComparison === 'function') {
          tab('compare');
          setTimeout(() => loadComparison(_cmpRef), 100);
        }
      } catch(e) {}
      break;
    }
    case 'startEdit':           startEdit(a0); break;
    case 'cancelEdit':          cancelEdit(a0); break;
    case 'saveEntryForm':       saveEntryForm(a0); break;
    case 'deleteEntryConfirm':  deleteEntryConfirm(a0); break;
    case 'openLb':              openLb(a0); break;
    case 'webcamOpen':          webcamOpen(a0, a1); break;
    case 'removeImage':         removeImage(a0, a1); break;
    case 'adjustImage':         if (typeof adjustImage === 'function') adjustImage(a0, a1); break;
    case 'setCiValue':          _setCiValue(+a0); break;
    case 'toggleDevice':        toggleDevice(a0, a1, a2); break;
    case 'toggleLinkedGallery': if (typeof toggleLinkedGallery === 'function') toggleLinkedGallery(a0, a1); break;
    case 'sendNoteClick':       _sendNoteClick(a0, a1); break;
    case 'toggleKot':           _toggleKot(a0, a1); break;
    case 'cemSendComment': {
      const _cemTa = document.getElementById('cem-note-ta');
      if (!_cemTa) break;
      let _cemContent = _cemTa.value.replace(/<[^>]*>/g, '').replace(/[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]/g, '').trim();
      if (!_cemContent) break;
      if (_cemContent.length > 1000) _cemContent = _cemContent.slice(0, 1000);
      const _cemExisting = (typeof getSentNote === 'function') ? getSentNote(a0, a1) : null;
      const _cemEditedAt = _cemExisting ? new Date().toISOString() : null;
      if (typeof recordNoteSent === 'function') recordNoteSent(a0, a1, _cemContent);
      if (typeof p2pSendNote === 'function') p2pSendNote(a0, a1, _cemContent, _cemEditedAt);
      if (typeof toast === 'function') toast('Comment sent');
      // Re-render modal
      const _cemModal = document.getElementById('contactEntryModal');
      if (_cemModal?.classList.contains('open')) {
        const _cemC = typeof getContacts === 'function' ? getContacts().find(c => c.pubKeyB64 === a0) : null;
        const _cemJ = typeof getPeerJournal === 'function' ? getPeerJournal(a0) : null;
        const _cemE = _cemJ ? (_cemJ.entries || []).find(x => x.id === a1) : null;
        if (_cemC && _cemE) showContactEntryDetail(_cemC, _cemE, typeof isOnline === 'function' && isOnline(a0));
      }
      break;
    }
    case 'cemSendKot': {
      if (typeof getSentKot === 'function' && getSentKot(a0, a1)) break;
      if (typeof p2pSendKot === 'function') p2pSendKot(a0, a1);
      if (typeof toast === 'function') toast('KOT sent ✊');
      // Re-render modal
      const _kotModal = document.getElementById('contactEntryModal');
      if (_kotModal?.classList.contains('open')) {
        const _kotC = typeof getContacts === 'function' ? getContacts().find(c => c.pubKeyB64 === a0) : null;
        const _kotJ = typeof getPeerJournal === 'function' ? getPeerJournal(a0) : null;
        const _kotE = _kotJ ? (_kotJ.entries || []).find(x => x.id === a1) : null;
        if (_kotC && _kotE) showContactEntryDetail(_kotC, _kotE, typeof isOnline === 'function' && isOnline(a0));
      }
      break;
    }
    case 'sendSessionKot':      if (typeof p2pSendSessionKot === 'function') p2pSendSessionKot(a0, a1); break;
    case 'entryClick': {
      // Entry list row — own entry or buddy entry
      const pub = el.dataset.buddyPub;
      const id  = el.dataset.entryId;
      if (pub) selectBuddyEntry(pub, id); else selectEntry(id);
      break;
    }
    case 'tabNewEntry':         tab('journal'); newEntry(); break;

    // ── Webcam ────────────────────────────────────────────────────────────
    case 'webcamClose':         webcamClose(); break;
    case 'webcamCapture':       webcamCapture(); break;
    case 'webcamRetake':        webcamRetake(); break;
    case 'webcamUse':           webcamUse(); break;

    // ── Photo Transform ───────────────────────────────────────────────────
    case 'ptCancel':            _ptCancel(); break;
    case 'ptApply':             _ptApply(); break;
    case 'ptReset':             _ptReset(); break;
    case 'ptRotate':            _ptRotate(+a0); break;
    case 'ptSetCrop':           _ptSetCrop(a0 === 'null' ? null : +a0); break;
    case 'editGalleryPhoto': {
      const _gm = document.getElementById('galImgModal');
      const _gGalId  = _gm?.querySelector('#galimg-gallery-id')?.value;
      const _gItemId = _gm?.querySelector('#galimg-item-id')?.value;
      if (_gGalId && _gItemId && typeof editGalleryPhoto === 'function') editGalleryPhoto(_gGalId, _gItemId);
      break;
    }

    // ── Contacts / Buddy ──────────────────────────────────────────────────
    case 'openBuddyProfile':    openBuddyProfile(a0); break;
    case 'openBuddyTimeline':   if (typeof tlSelectUser === 'function') { closeModal('profileModal'); tlSelectUser(a0); tab('timeline'); } break;
    case 'viewContactJournal':  viewContactJournal(a0); break;
    case 'viewContactEntry':    viewContactEntry(a0, a1); break;
    case 'tabViewContactJournal': tab('contacts'); setTimeout(() => viewContactJournal(a0), 50); break;
    case 'sendChatMsg':         sendChatMsg(a0); break;
    case 'removeContact':       removeContact(a0); break;
    case 'toggleTrustContact':  toggleTrustContact(a0); break;
    case 'deleteBuddy':         deleteBuddy(a0); break;
    case 'dismissGhost':        dismissGhost(a0); break;
    case 'toggleContactPanel':  _toggleContactPanel(a0, a1); break;
    case 'toggleBuddyProfile':  _toggleBuddyProfile(a0); break;
    case 'copyToClipboard':     copyToClipboard(a0); break;
    case 'addContact':          addContact(); break;
    case 'showKeyBackup':       showKeyBackup(); break;
    case 'showKeyRestore':      showKeyRestore(); break;
    case 'revealKeyBackup':     revealKeyBackup(); break;
    case 'copyKeyBackup':       copyKeyBackup(); break;
    case 'hideKeyBackup':       hideKeyBackup(); break;
    case 'doKeyRestore':        doKeyRestore(); break;
    case 'hideKeyRestoreArea': {
      const kra = document.getElementById('key-restore-area');
      if (kra) kra.style.display = 'none';
      break;
    }
    case 'saveBuddyPermissionsUI': saveBuddyPermissionsUI(); break;
    case 'leaveQuietly':        leaveQuietly(); break;
    case 'unblockRestorer':     if (typeof unblockRestorer === 'function') unblockRestorer(a0); break;
    case 'showMigrationModal':  showMigrationModal(); break;
    case 'showUnlockModal': {
      if (typeof showUnlockModal === 'function') showUnlockModal();
      else if (typeof toast === 'function') toast('Please unlock from the sidebar', true);
      break;
    }

    // ── Buddy Network ─────────────────────────────────────────────────────
    case 'openRestorerProfile':  openRestorerProfile(a0, a1, a2, el.dataset.a3); break;
    case 'blockRestorer':        blockRestorer(a0, a1); break;
    case 'p2pCancelOffer':       p2pCancelOffer(a0); break;
    case 'openHandshakePanel':   closeModal('restorerProfileModal'); openHandshakePanel(a0, a1, a2); break;
    case 'confirmHandshake':     _confirmHandshakeFromModal(a0); break;
    case 'cancelHandshake': {
      if (typeof p2pCancelHandshake === 'function') p2pCancelHandshake(a0);
      closeModal('handshakeModal');
      break;
    }
    case 'goOnlinePublic':        if (typeof goOnlinePublic      === 'function') goOnlinePublic(); break;
    case 'turnOnBuddyNetwork':   if (typeof turnOnBuddyNetwork  === 'function') turnOnBuddyNetwork(); break;

    // ── Gallery ────────────────────────────────────────────────────────────
    case 'openBuddyGallery':          if (typeof openBuddyGallery       === 'function') openBuddyGallery(a0); break;
    case 'openGalleryFolder':         if (typeof openGalleryFolder      === 'function') openGalleryFolder(a0, a1); break;
    case 'openReceivedGallery':       if (typeof openReceivedGallery    === 'function') openReceivedGallery(a0, a1); break;
    case 'backToGalleryList':         if (typeof backToGalleryList      === 'function') backToGalleryList(); break;
    case 'openFavoritesFolder':      if (typeof openFavoritesFolder   === 'function') openFavoritesFolder(); break;
    case 'switchGalleryTab':          if (typeof switchGalleryTab       === 'function') switchGalleryTab(a0); break;
    case 'showCreateGalleryModal':    if (typeof showCreateGalleryModal === 'function') showCreateGalleryModal(); break;
    case 'editGalleryPermissions':    if (typeof editGalleryPermissions === 'function') editGalleryPermissions(a0); break;
    case 'renameGallery':            if (typeof renameGallery === 'function') renameGallery(a0); break;
    case 'toggleBookmark':           if (typeof toggleBookmark === 'function') toggleBookmark(a0); break;
    case 'bookmarkReceivedPhoto':    if (typeof bookmarkReceivedPhoto === 'function') bookmarkReceivedPhoto(a0, a1, a2, a3); break;
    case 'openBookmarksFolder':      if (typeof openBookmarksFolder === 'function') openBookmarksFolder(); break;
    case 'galClearFilter':           if (typeof galClearFilter === 'function') galClearFilter(); break;
    case 'saveNewGallery':            if (typeof saveNewGallery         === 'function') saveNewGallery(); break;
    case 'addPhotoToGallery':         if (typeof addPhotoToGallery      === 'function') addPhotoToGallery(a0); break;
    case 'openGalleryImageModal':     if (typeof openGalleryImageModal  === 'function') openGalleryImageModal(a0, a1, a2); break;
    case 'openLinkedGalleryImage':    if (typeof openLinkedGalleryImage === 'function') openLinkedGalleryImage(a0, a1); break;
    case 'saveGalleryItemNote':       if (typeof saveGalleryItemNote    === 'function') saveGalleryItemNote(); break;
    case 'saveGalleryPrivateNote':    if (typeof saveGalleryPrivateNote === 'function') saveGalleryPrivateNote(); break;
    case 'sendGalleryPhotoNote':      if (typeof sendGalleryPhotoNote   === 'function') sendGalleryPhotoNote(); break;
    case 'sendGalleryKot': {
      // Resolve owner pubKey from gallery received data, then send KOT
      const recv = typeof getGalleryReceived === 'function' ? getGalleryReceived() : {};
      const _kotOwnerPub = recv[a0]?.[a1]?.senderPubKey;
      if (_kotOwnerPub && typeof p2pSendGalleryKot === 'function') {
        p2pSendGalleryKot(_kotOwnerPub, a1, a2);
        if (typeof toast === 'function') toast('KOT sent ✊');
        // Update button to KOT'd state
        const _kotBtn = el;
        _kotBtn.textContent = '✊ KOT\'d';
        _kotBtn.disabled = true;
        _kotBtn.classList.add('kot-active');
      }
      break;
    }
    case 'saveGalleryItemTitle':      if (typeof saveGalleryItemTitle   === 'function') saveGalleryItemTitle(); break;
    case 'removeGalleryItemConfirm':  if (typeof removeGalleryItemConfirm === 'function') removeGalleryItemConfirm(a0, a1); break;
    case 'moveGalleryItem': {
      const _mItemId = document.getElementById('galimg-item-id')?.value;
      const _mGalId  = document.getElementById('galimg-gallery-id')?.value;
      if (_mItemId && _mGalId && typeof moveGalleryItem === 'function') {
        closeModal('galImgModal');
        moveGalleryItem(_mItemId, _mGalId);
      }
      break;
    }
    case 'deleteGalleryConfirm':      if (typeof deleteGalleryConfirm   === 'function') deleteGalleryConfirm(a0); break;
    case 'leaveReceivedGallery':      if (typeof leaveReceivedGallery   === 'function') leaveReceivedGallery(a0, a1); break;
    case 'rejoinGallery':             if (typeof rejoinGallery          === 'function') rejoinGallery(a0, a1); break;
    case 'acceptBuddyRequest':   if (typeof p2pAcceptBuddyRequest === 'function') p2pAcceptBuddyRequest(a0); break;
    case 'ignoreBuddyRequest':   if (typeof _ignoreBuddyRequest === 'function') _ignoreBuddyRequest(a0); break;
    case 'cancelPendingExchange': if (typeof p2pCancelPendingExchange === 'function') p2pCancelPendingExchange(a0); break;
    case 'revealRestorer':       if (typeof revealRestorer === 'function') revealRestorer(a0, a1, a2); break;

    // ── Data analytics page ───────────────────────────────────────────────
    case 'setDataSpan':  if (typeof _setDataSpan  === 'function') _setDataSpan(a0); break;
    case 'prevDataSpan': if (typeof _prevDataSpan === 'function') _prevDataSpan(); break;
    case 'nextDataSpan': if (typeof _nextDataSpan === 'function') _nextDataSpan(); break;

    // ── Dashboard ─────────────────────────────────────────────────────────
    case 'saveEditSession':      _saveEditSession(a0); break;
    case 'cancelEditSession':    _cancelEditSession(); break;
    case 'editTimerSession':     _editTimerSession(a0); break;
    case 'deleteTimerSession':   _deleteTimerSession(a0); break;
    case 'showAddSessionForm':   _showAddSessionForm(); break;
    case 'toggleDashGalPost':   if (typeof toggleDashGalPost === 'function') toggleDashGalPost(); break;
    case 'dashNewEntry':        tab('journal'); setTimeout(() => { if (typeof newEntry === 'function') newEntry(); }, 50); break;
    case 'cancelAddSession':     _cancelAddSession(); break;
    case 'saveNewSession':       _saveNewSession(); break;
    case 'showAllSessions':      _showAllSessions = true; _renderDashTimer(); break;
    case 'exportSessionsCsv':   _exportSessionsCsv(); break;
    case 'importSessionsCsv':   _importSessionsCsv(); break;
    case 'dismissAdoptedTimer':  _dismissAdoptedTimer(); break;
    case 'discardStaleTimer':    _discardStaleTimer(); break;
    case 'stopTimer':            stopTimer(); break;
    case 'startTimer':           startTimer(a0); break;
    case 'selectTimerDevice':    selectTimerDevice(a0, a1); break;
    case 'cmpResetTransform':    if (typeof _cmpResetTransform === 'function') _cmpResetTransform(a0); break;
    case 'saveComparison':       if (typeof saveComparison === 'function') saveComparison(); break;
    case 'loadComparison':       if (typeof loadComparison === 'function') loadComparison(a0); break;
    case 'deleteComparison':     if (typeof deleteComparison === 'function') deleteComparison(a0); break;
    case 'onboardingClick':      _onboardingClick(a0); break;
    case 'dismissOnboarding':    _dismissOnboarding(a0); break;
    case 'saveQuickLog':         saveQuickLog(); break;
    case 'deleteWeekStat':       deleteWeekStat(a0); break;
    case 'feedClick': {
      const pub = el.dataset.pub;
      const id  = el.dataset.id;
      if (pub) { tab('contacts'); setTimeout(() => viewContactJournal(pub), 50); }
      else     { tab('journal');  selectEntry(id); }
      break;
    }

    // ── Encryption / Password ─────────────────────────────────────────────
    case 'encMigDownloadBackup':  _encMigDownloadBackup(); break;
    case 'encMigStart':           _encMigStart(); break;
    case 'closeMigrationModal':   closeMigrationModal(); break;
    case 'openEncryptionSettings': openEncryptionSettings(); break;
    case 'cancelPendingPwChange': cancelPendingPwChange(); break;

    // ── Devices / Pairing ─────────────────────────────────────────────────
    case 'pairApplyEncPw':       _pairApplyEncPw(); break;
    case 'startPairingDisplay':  _startPairingDisplay(); break;
    case 'closePairModal':       closePairModal(); break;
    case 'submitJoinPairCode':   submitJoinPairCode(); break;
    case 'closeJoinPairModal':   closeJoinPairModal(); break;
    case 'removePairedDevice':   removePairedDevice(a0); break;
    case 'removePairedDeviceDropdown': removePairedDevice(a0); closeDeviceDropdown(); break;
    case 'syncAllDevices':       if (typeof syncNow === 'function') syncNow(); closeDeviceDropdown(); break;
    case 'applyPairingEncPw':    _applyPairingEncPw(); break;
    case 'skipPairingEncPw':     _skipPairingEncPw(); break;
    case 'openPairModal':        openPairModal(); break;
    case 'openJoinPairModal':    openJoinPairModal(); break;
    case 'cancelDeviceRemoval': {
      const rm = document.getElementById('_removal-request-modal');
      if (rm) rm.classList.remove('open');
      if (typeof _pendingRemovalFromId !== 'undefined') window._pendingRemovalFromId = null;
      break;
    }
    case 'confirmDeviceRemoval': _confirmDeviceRemoval(); break;

    // ── Profile ───────────────────────────────────────────────────────────
    case 'saveProfileForm':      saveProfileForm(); break;
    case 'pickAvatar':           pickAvatar(a0); break;
    case 'toggleAvatarPicker': {
      const picker = document.getElementById('pf-avatar-picker');
      if (picker) picker.classList.toggle('hidden');
      break;
    }
    case 'clearAvatar': {
      const preview = document.getElementById('pf-avatar-preview');
      const val     = document.getElementById('pf-avatar-val');
      if (preview) preview.textContent = '-';
      if (val)     val.value = '';
      break;
    }
    case 'tabJournal':           closeModal('profileModal'); tab('journal'); break;
    case 'checkForUpdates': {
      // Do the simplest thing that always works: append a cache-bust query string to the
      // current URL and reload. This bypasses the SW cache (the browser treats it as a
      // fresh navigation), refetches index.html with the new ?v=... script versions,
      // and triggers the SW to refetch sw.js on next load. Does NOT touch IDB / localStorage —
      // those are partitioned by origin, not by URL path or query string.
      const statusEl = document.getElementById('sw-update-status');
      if (statusEl) statusEl.textContent = 'Reloading with fresh assets…';
      // Belt-and-suspenders: kick the SW too (silently) so it gets a head start
      try {
        if (window._frjSwReg) { window._frjSwReg.update().catch(() => {}); }
        else if ('serviceWorker' in navigator) {
          navigator.serviceWorker.getRegistration().then(r => r && r.update().catch(() => {})).catch(() => {});
        }
      } catch(e) {}
      const url = new URL(window.location.href);
      url.searchParams.set('_swcb', Date.now().toString(36));
      setTimeout(() => { window.location.replace(url.toString()); }, 200);
      break;
    }
    case 'viewBuddyContacts':    closeModal('profileModal'); tab('contacts'); break;
    case 'viewBuddyJournal': {
      closeModal('profileModal');
      tab('journal');
      if (typeof getPeerJournal === 'function' && typeof selectBuddyEntry === 'function') {
        const _bj = getPeerJournal(a0);
        const _bjEntries = (_bj?.entries || []).slice().sort((x, y) => y.date.localeCompare(x.date));
        if (_bjEntries.length) selectBuddyEntry(a0, _bjEntries[0].id);
      }
      break;
    }
    case 'profileNameClick': {
      // In profileRow: own row → openProfile, buddy row → openBuddyProfile
      if (a0) openBuddyProfile(a0); else openProfile();
      break;
    }
    case 'toggleCollapsed':      el.parentElement.classList.toggle('collapsed'); break;
    case 'saveWatermarkSettingsPage': saveWatermarkSettingsPage(); break;
    case 'wipeAllData':          _wipeAllData(); break;

    // ── Quick Check ───────────────────────────────────────────────────────
    case 'qcAccept':             if (typeof qcAccept === 'function') qcAccept(a0); break;
    case 'qcDecline':            if (typeof qcDecline === 'function') qcDecline(a0); break;
    case 'qcCancel':             if (typeof confirm2 === 'function') confirm2('Cancel Quick Check?', 'This will end the schedule. Your spree record will be saved.', () => { if (typeof qcCancel === 'function') qcCancel(a0); }); break;
    case 'qcProposeModal':       _qcShowProposeModal(a0); break;
    case 'qcViewDetail':         _qcShowDetailModal(a0); break;

    // ── Workspace ─────────────────────────────────────────────────────────
    case 'wsDelZone':            wsDelZone(+a0); break;

    // ── Timeline rows ─────────────────────────────────────────────────────
    case 'tlRowClick': {
      const pub = el.dataset.buddyPub;
      const id  = el.dataset.entryId;
      if (pub) viewContactEntry(pub, id);
      else     { tab('journal'); selectEntry(id); }
      break;
    }

    // ── Timeline slideshow ────────────────────────────────────────────────
    case 'tlOpenSlideshow':    if (typeof tlOpenSlideshow === 'function') tlOpenSlideshow(); break;
    case 'tlCloseSlideshow':   if (typeof tlCloseSlideshow === 'function') tlCloseSlideshow(); break;

    // ── Lineups ───────────────────────────────────────────────────────────
    case 'lineupCreate':       if (typeof lineupCreate === 'function') lineupCreate(); break;
    case 'lineupEdit':         if (typeof lineupEdit === 'function') lineupEdit(a0); break;
    case 'lineupDelete':       if (typeof lineupDelete === 'function') lineupDelete(a0); break;
    case 'lineupCloseEditor':  if (typeof lineupCloseEditor === 'function') lineupCloseEditor(); break;
    case 'lineupSaveName':     if (typeof lineupSaveName === 'function') lineupSaveName(); break;
    case 'lineupMoveItem':     if (typeof lineupMoveItem === 'function') lineupMoveItem(a0, a1); break;
    case 'lineupRemoveItem':   if (typeof lineupRemoveItem === 'function') lineupRemoveItem(a0); break;
    case 'lineupOpenPicker':   if (typeof lineupOpenPicker === 'function') lineupOpenPicker(); break;
    case 'lineupClosePicker':  if (typeof lineupClosePicker === 'function') lineupClosePicker(); break;
    case 'lineupPickItem':     if (typeof lineupPickItem === 'function') lineupPickItem(a0, a1); break;

    // ── Sync / Device removal modal ───────────────────────────────────────
    case 'confirmDeviceRemovalSync': _confirmDeviceRemoval(); break;
    case 'cancelSyncDeviceRemoval': _cancelDeviceRemovalModal(); break;

    // ── Quick log badge ───────────────────────────────────────────────────
    case 'tabJournalEntry':      tab('journal'); selectEntry(a0); break;
    case 'entryBadgeOwn':        tab('journal'); setTimeout(() => selectEntry(a0), 50); break;
    case 'entryBadgeBuddy':      viewContactEntry(a0, a1); break;
  }
});

// ── Input delegation ────────────────────────────────────────────────────────
document.addEventListener('input', function _dispatchInput(e) {
  const id = e.target.id;
  if (id === 'ptZoom')          { _ptSetZoom(e.target.value); return; }
  if (id === 'ptRot')           { _ptSetRot(e.target.value); return; }
  if (id === 'ptOx')            { _ptSetOx(e.target.value); return; }
  if (id === 'ptOy')            { _ptSetOy(e.target.value); return; }
  if (id === 'journal-search')  { renderEntryList(); return; }
  if (id === 'ef-date') {
    const t = document.getElementById('ef-date-title');
    if (t && e.target.value) t.textContent = fmtMonthYear(e.target.value);
    return;
  }
});

// ── Change delegation ────────────────────────────────────────────────────────
document.addEventListener('change', function _dispatchChange(e) {
  const id   = e.target.id;
  const cls  = e.target.classList;
  const data = e.target.dataset;

  // Compare page
  if (id === 'cmp-a' || id === 'cmp-b') {
    if (typeof _populateComparePhotoSrc === 'function') { _populateComparePhotoSrc('a'); _populateComparePhotoSrc('b'); }
    renderCompare(); return;
  }
  if (id === 'cmp-a-src' || id === 'cmp-b-src') {
    if (typeof _cmpPopulating !== 'undefined' && _cmpPopulating) return; // ignore synthetic events during rebuild
    if (id === 'cmp-a-src' && typeof _cmpSrcA !== 'undefined') _cmpSrcA = e.target.value || 'entry';
    if (id === 'cmp-b-src' && typeof _cmpSrcB !== 'undefined') _cmpSrcB = e.target.value || 'entry';
    renderCompare(); return;
  }
  if (id === 'cmp-diff') { renderCompare(); return; }

  // Journal filters
  if (id === 'jf-ci-min' || id === 'jf-ci-max' || id === 'jf-device' || id === 'jf-photo-only' || id === 'jf-bookmarked') {
    renderEntryList(); return;
  }

  // Habit manual toggle (journal entry form)
  if (id === 'ef-habit-manual') { _toggleHabitManual(); return; }

  // File upload (photo slots — data-upload-id and data-upload-field set in template)
  if (e.target.type === 'file' && data.uploadId) {
    handleImageUpload(e.target, data.uploadId, data.uploadField); return;
  }

  // Buddy permissions matrix — enforce cell dependencies live
  if (id === 'pm-buddy-viewlatest' || id === 'pm-buddy-viewall') {
    if (typeof _updatePermMatrix === 'function') _updatePermMatrix('buddy');
    return;
  }
  if (id === 'pm-trusted-viewlatest' || id === 'pm-trusted-viewall') {
    if (typeof _updatePermMatrix === 'function') _updatePermMatrix('trusted');
    return;
  }

  // Public profile field checkboxes (data-pub-field set in template)
  if (data.pubField) { savePublicProfileField(data.pubField, e.target.checked); return; }

  // Per-badge public visibility (data-hide-badge = badge key; checked = visible)
  if (data.hideBadge) {
    if (typeof getPublicProfileSettings === 'function' && typeof savePublicProfileSettings === 'function') {
      const s = getPublicProfileSettings();
      const hidden = Array.isArray(s.hiddenPublicBadges) ? [...s.hiddenPublicBadges] : [];
      if (e.target.checked) {
        const idx = hidden.indexOf(data.hideBadge);
        if (idx !== -1) hidden.splice(idx, 1);
      } else {
        if (!hidden.includes(data.hideBadge)) hidden.push(data.hideBadge);
      }
      s.hiddenPublicBadges = hidden;
      savePublicProfileSettings(s);
    }
    return;
  }

  // Timeline user selector
  if (cls.contains('tl-user-select')) { tlSelectUser(e.target.value); return; }

  // Settings: vanish / watermark
  if (id === 'st-vanish-on' || id === 'st-vanish-ttl') { _saveVanishSettings(); return; }
  if (id === 'st-wm-style'  || id === 'st-wm-opacity') { _settingsWmChange();   return; }
  if (id === 'st-dash-show-all-imgs') { localStorage.setItem('frj_dash_show_all_imgs', e.target.checked ? '1' : '0'); if (typeof renderDash === 'function') renderDash(); return; }
  if (id === 'st-hide-buddy-data') { localStorage.setItem('frj_hide_buddy_data', e.target.checked ? '1' : '0'); if (typeof renderDash === 'function') renderDash(); if (typeof renderActivity === 'function') renderActivity(); return; }
});

// ── Keydown delegation ───────────────────────────────────────────────────────
document.addEventListener('keydown', function _dispatchKeydown(e) {
  // Escape closes the topmost open modal. The ToS gate is exempt (it is a legal
  // gate, not a dismissible dialog). Modals with teardown needs get their own
  // close functions so streams and state are cleaned up properly.
  if (e.key === 'Escape') {
    const open = [...document.querySelectorAll('.modal-overlay.open')].filter(m => m.id !== 'tosModal');
    const top = open[open.length - 1];
    if (top) {
      if (top.id === 'chatModal') { if (typeof closeChatPopup === 'function') closeChatPopup(); }
      else if (top.id === 'webcamModal') { if (typeof webcamClose === 'function') webcamClose(); }
      else top.classList.remove('open');
      return;
    }
  }
  if (e.target.id === 'pair-enc-pw' && e.key === 'Enter') { _pairApplyEncPw(); return; }
  if (e.target.id === 'peer-chat-input' && e.key === 'Enter') {
    const pub = e.target.dataset.chatPub;
    if (pub) sendChatMsg(pub);
    return;
  }
  if (e.target.id === 'chatPopupInput'  && e.key === 'Enter') { sendChatPopupMsg(); return; }
  if (e.target.id === 'chatModalInput'  && e.key === 'Enter') { sendChatPopupMsg(); return; }
  if (e.target.id === 'msgrInput'       && e.key === 'Enter') { sendMsgrMsg(); return; }
  if (e.target.dataset.deviceType && e.key === 'Enter') {
    const val = e.target.value.trim();
    if (val) { toggleDevice(val, e.target.dataset.deviceType, e.target.dataset.entryId); e.target.value = ''; }
    return;
  }
  // ESC closes the topmost open modal
  if (e.key === 'Escape') {
    const lightbox = document.getElementById('lightbox');
    if (lightbox?.classList.contains('open')) { lightbox.classList.remove('open'); return; }
    const modals = ['galImgModal', 'chatModal', 'profileModal', 'confirmModal', 'galCreateModal'];
    for (const id of modals) {
      const m = document.getElementById(id);
      if (m?.classList.contains('open')) { closeModal(id); return; }
    }
  }
});

// ── Peek photo (tap-vs-hold: quick tap → lightbox, long press → peek overlay) ──
let _peekTimer = null, _peekActive = false, _peekSrc = null;

function _startPeek(e) {
  const btn = e.target.closest('[data-peek-src]');
  if (!btn) return;
  _peekSrc = btn.dataset.peekSrc;
  _peekActive = false;
  _peekTimer = setTimeout(() => {
    _peekActive = true;
    if (typeof _peekPhoto === 'function') _peekPhoto(_peekSrc);
  }, 300);
}

function _endPeek() {
  if (_peekTimer) { clearTimeout(_peekTimer); _peekTimer = null; }
  if (_peekActive) {
    if (typeof _unpeekPhoto === 'function') _unpeekPhoto();
    _peekActive = false;
  } else if (_peekSrc) {
    if (typeof openLb === 'function') openLb(_peekSrc);
  }
  _peekSrc = null;
}

document.addEventListener('mousedown', _startPeek);
document.addEventListener('touchstart', function(e) {
  const btn = e.target.closest('[data-peek-src]');
  if (btn) { e.preventDefault(); _startPeek(e); }
}, { passive: false });
document.addEventListener('mouseup',     _endPeek);
document.addEventListener('touchend',    _endPeek);
document.addEventListener('touchcancel', _endPeek);

// ── Prevent context-menu / drag on protected images ─────────────────────────
document.addEventListener('contextmenu', function(e) {
  if (e.target.dataset.noContext) e.preventDefault();
});
document.addEventListener('dragstart', function(e) {
  if (e.target.dataset.noDrag) e.preventDefault();
});

// ── Quick Check modals ──────────────────────────────────────────────────────

function _qcShowProposeModal(pubKeyB64) {
  const c = typeof getContacts === 'function' ? getContacts().find(x => x.pubKeyB64 === pubKeyB64) : null;
  const name = c?.journal?.displayName || c?.nickname || 'Buddy';
  const cadences = ['daily', 'weekly', 'monthly', 'quarterly', 'yearly'];
  const html = `<div class="modal-box" style="max-width:400px">
    <div class="modal-hdr"><span>📅 New Quick Check with ${esc(name)}</span><button data-action="closeModal" data-a0="confirmModal">✕</button></div>
    <div class="modal-body" style="padding:16px">
      <div class="fg" style="margin-bottom:12px">
        <label style="font-size:12px;font-weight:600">What kind of photo?</label>
        <input type="text" id="qc-description" class="input" placeholder="e.g. Front photo, Progress shot…" maxlength="120" style="font-size:13px">
      </div>
      <div class="fg" style="margin-bottom:16px">
        <label style="font-size:12px;font-weight:600">How often?</label>
        <select id="qc-cadence" class="input" style="font-size:13px">
          ${cadences.map(c => `<option value="${c}">${c.charAt(0).toUpperCase() + c.slice(1)}</option>`).join('')}
        </select>
      </div>
      <button class="btn btn-primary" id="qc-propose-submit" style="width:100%">Propose Quick Check</button>
    </div>
  </div>`;
  const modal = document.getElementById('confirmModal');
  if (!modal) return;
  modal.innerHTML = html;
  modal.classList.add('open');
  modal.querySelector('#qc-propose-submit')?.addEventListener('click', () => {
    const desc = modal.querySelector('#qc-description')?.value?.trim();
    const cadence = modal.querySelector('#qc-cadence')?.value;
    if (!desc) { toast('Enter a photo description', true); return; }
    if (typeof qcPropose === 'function') qcPropose(pubKeyB64, desc, cadence);
    modal.classList.remove('open');
  });
}

function _qcShowDetailModal(qcId) {
  const qcs = typeof getQuickChecks === 'function' ? getQuickChecks() : [];
  const qc = qcs.find(q => q.id === qcId);
  if (!qc) return;
  const c = typeof getContacts === 'function' ? getContacts().find(x => x.pubKeyB64 === qc.buddyPubKey) : null;
  const name = c?.journal?.displayName || c?.nickname || 'Buddy';
  const avatar = c?.journal?.avatar || '📋';
  const status = typeof qcDeadlineStatus === 'function' ? qcDeadlineStatus(qc) : 'idle';
  const timeLeft = typeof qcTimeLeft === 'function' ? qcTimeLeft(qc) : '';
  const cadLabel = typeof _QC_CADENCES !== 'undefined' ? (_QC_CADENCES[qc.cadence]?.label || qc.cadence) : qc.cadence;

  const mySrc = qc.myImgKey && typeof imgURL === 'function' ? imgURL(qc.myImgKey) : null;
  const peerSrc = qc.peerImgKey && typeof imgURL === 'function' ? imgURL(qc.peerImgKey) : null;

  const photoSection = qc.revealed
    ? `<div style="display:grid;grid-template-columns:1fr 1fr;gap:8px;margin-bottom:12px">
        <div style="text-align:center">
          ${mySrc ? `<img src="${mySrc}" style="width:100%;border-radius:6px;aspect-ratio:3/4;object-fit:cover" data-no-context="1" data-no-drag="1">` : '<div style="width:100%;aspect-ratio:3/4;background:var(--surface2);border-radius:6px;display:flex;align-items:center;justify-content:center;color:var(--muted2)">📷</div>'}
          <div style="font-family:var(--mono);font-size:10px;color:var(--muted2);margin-top:4px">You</div>
        </div>
        <div style="text-align:center">
          ${peerSrc ? `<img src="${peerSrc}" style="width:100%;border-radius:6px;aspect-ratio:3/4;object-fit:cover" data-no-context="1" data-no-drag="1">` : '<div style="width:100%;aspect-ratio:3/4;background:var(--surface2);border-radius:6px;display:flex;align-items:center;justify-content:center;color:var(--muted2)">📷</div>'}
          <div style="font-family:var(--mono);font-size:10px;color:var(--muted2);margin-top:4px">${esc(name)}</div>
        </div>
      </div>`
    : qc.mySubmittedAt
      ? `<div style="text-align:center;padding:20px 0;color:var(--muted2);font-size:13px">
          <div style="font-size:28px;margin-bottom:8px">⏳</div>
          Waiting for ${esc(name)} to submit…<br>
          ${mySrc ? `<div style="margin-top:10px;filter:blur(12px);pointer-events:none"><img src="${mySrc}" style="width:60%;border-radius:6px;aspect-ratio:3/4;object-fit:cover"></div>` : ''}
        </div>`
      : `<div style="text-align:center;padding:16px 0">
          <button class="btn btn-primary" id="qc-detail-upload" style="font-size:14px;padding:10px 24px">📸 Submit Photo</button>
        </div>`;

  const historyRows = (qc.history || []).slice(-10).reverse().map(h => {
    const pDate = new Date(h.period).toLocaleDateString([], { month: 'short', day: 'numeric' });
    return `<div style="display:flex;justify-content:space-between;font-size:11px;padding:3px 0;border-bottom:1px solid var(--border2)">
      <span>${pDate}</span>
      <span style="color:${h.success ? 'var(--green)' : '#ef4444'}">${h.success ? '✓' : '✕'} ${h.success ? 'spree ' + h.spreeAtEnd : 'missed'}</span>
    </div>`;
  }).join('');

  const html = `<div class="modal-box" style="max-width:420px">
    <div class="modal-hdr"><span>${esc(avatar)} Quick Check with ${esc(name)}</span><button data-action="closeModal" data-a0="confirmModal">✕</button></div>
    <div class="modal-body" style="padding:16px">
      <div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:12px">
        <div>
          <div style="font-size:13px;font-weight:600;color:var(--text)">${esc(qc.description)}</div>
          <div style="font-family:var(--mono);font-size:11px;color:var(--muted2)">${cadLabel} · ${timeLeft || 'starting'}</div>
        </div>
        <div style="text-align:right">
          <div style="font-size:20px;font-weight:700;color:var(--text)">🔥 ${qc.spree || 0}</div>
          <div style="font-family:var(--mono);font-size:10px;color:var(--muted2)">best: ${qc.bestSpree || 0}</div>
        </div>
      </div>
      ${photoSection}
      ${historyRows ? `<details style="margin-top:8px"><summary style="font-family:var(--mono);font-size:11px;color:var(--muted2);cursor:pointer">History</summary><div style="margin-top:6px">${historyRows}</div></details>` : ''}
      <div style="margin-top:12px;text-align:center">
        <button class="btn btn-ghost btn-sm" data-action="qcCancel" data-a0="${esc(qc.id)}" style="color:#ef4444">Cancel Quick Check</button>
      </div>
    </div>
  </div>`;

  const modal = document.getElementById('confirmModal');
  if (!modal) return;
  modal.innerHTML = html;
  modal.classList.add('open');

  // Wire upload button
  const uploadBtn = modal.querySelector('#qc-detail-upload');
  if (uploadBtn) {
    uploadBtn.addEventListener('click', () => {
      const input = document.createElement('input');
      input.type = 'file'; input.accept = 'image/*'; input.style.display = 'none';
      document.body.appendChild(input);
      input.addEventListener('change', async () => {
        document.body.removeChild(input);
        if (input.files[0]) {
          uploadBtn.disabled = true; uploadBtn.textContent = 'Submitting…';
          if (typeof qcSubmitPhoto === 'function') await qcSubmitPhoto(qcId, input.files[0]);
          modal.classList.remove('open');
        }
      });
      input.click();
    });
  }
}
