'use strict';
// ═══════════════════════════════════════════════
// js/buddynet.js — Buddy Network UI
//
// Handles all UI interactions for the presence + handshake system.
// All state is owned by p2p.js; this file only reads/renders it.
// ═══════════════════════════════════════════════

// ── Presence toggle (legacy — kept for backward-compat if old buttons exist) ──

function updatePresenceToggleUI(state) {
  const offBtn  = document.getElementById('presence-btn-offline');
  const privBtn = document.getElementById('presence-btn-private');
  const pubBtn  = document.getElementById('presence-btn-public');
  if (offBtn && privBtn && pubBtn) {
    offBtn.className  = 'presence-btn' + (state === 'offline'  ? ' active-offline'  : '');
    privBtn.className = 'presence-btn' + (state === 'private'  ? ' active-private'  : '');
    pubBtn.className  = 'presence-btn' + (state === 'public'   ? ' active-public'   : '');
  }
  // Sync saved toggle bits so new Network/Buddies/Public UI reflects reality
  if (state === 'private') {
    localStorage.setItem('frj_buddies_enabled', '1');
    localStorage.setItem('frj_public_enabled',  '0');
  } else if (state === 'public') {
    localStorage.setItem('frj_buddies_enabled', '1');
    localStorage.setItem('frj_public_enabled',  '1');
  }
  updateNetworkToggleUI();
}

// ── Network / Buddies / Public toggles ────────────────────────────────────────

function getNetworkToggles() {
  return {
    network: localStorage.getItem('frj_p2p_offline') === '0',
    buddies: localStorage.getItem('frj_buddies_enabled') === '1', // default off — explicit opt-in
    public:  localStorage.getItem('frj_public_enabled')  === '1', // default off
  };
}

function updateNetworkToggleUI() {
  const t = getNetworkToggles();
  const states = {
    network: t.network,
    buddies: t.network && t.buddies,
    public:  t.network && t.buddies && t.public,
  };
  // Data-attribute selectors so multiple toggle banks (sidebar + contacts page +
  // devices page) all stay in sync. The old #net-... IDs are kept on the sidebar
  // for back-compat with anything that still looks them up, but lookups happen
  // by attribute now.
  for (const [name, on] of Object.entries(states)) {
    document.querySelectorAll(`[data-net-dot="${name}"]`).forEach(el => el.classList.toggle('on', on));
    document.querySelectorAll(`[data-net-btn="${name}"]`).forEach(el => el.classList.toggle('active', on));
    document.querySelectorAll(`[data-net-state="${name}"]`).forEach(el => { el.textContent = on ? 'ON' : 'OFF'; });
  }
  // Keep the inline dropdown toggles (in 👥 and 📱 menus) in sync too.
  if (typeof _renderBuddyDropdownToggles === 'function') _renderBuddyDropdownToggles();
  if (typeof _renderDeviceDropdownToggle === 'function') _renderDeviceDropdownToggle();
  // Dynamic status line(s) -- whichever toggle bank is visible
  document.querySelectorAll('[data-net-status]').forEach(line => {
    if (!t.network) {
      line.textContent = 'Tap Network then Buddies to connect with your buddies.';
      line.className = 'net-status-line offline';
    } else if (!t.buddies) {
      line.textContent = 'Network is on — tap Buddies to sync with your buddies.';
      line.className = 'net-status-line offline';
    } else if (!t.public) {
      line.textContent = 'Online with buddies. Tap Public to appear in Restorers Online.';
      line.className = 'net-status-line online';
    } else {
      line.textContent = 'Online and visible in Restorers Online.';
      line.className = 'net-status-line online';
    }
  });
}

// Called from app.js init — restores saved buddies/public state on startup.
// Network state is already handled by initSignaling().
function initNetworkToggles() {
  const t = getNetworkToggles();
  if (t.network) {
    const desired = t.buddies && t.public ? 'public'
                  : t.buddies             ? 'private'
                  :                          null;
    if (desired && typeof setPresenceState === 'function') setPresenceState(desired);
  }
  updateNetworkToggleUI();
}

async function toggleNetworkBtn() {
  const currentlyOffline = localStorage.getItem('frj_p2p_offline') !== '0';
  if (currentlyOffline) {
    // Require encryption before going online
    const encEnabled = typeof idbMetaGet === 'function' ? await idbMetaGet('encryptionEnabled') : false;
    if (!encEnabled) {
      if (typeof confirm2 === 'function') {
        confirm2('Encryption required', 'Your data must be encrypted before going online. Set up a password now?',
          () => { if (typeof showMigrationModal === 'function') showMigrationModal(); });
      }
      return;
    }
    // First-time privacy notice
    if (!localStorage.getItem('frj_online_warned')) {
      confirm2('Going online',
        'Your device will connect to the Forestorers signal server. The server cannot read your data, but it can see your IP address and when you connect. All buddy communication is end-to-end encrypted. Continue?',
        () => {
          localStorage.setItem('frj_online_warned', '1');
          localStorage.setItem('frj_p2p_offline', '0');
          if (typeof p2pSetOffline === 'function') p2pSetOffline(false);
          if (typeof _applyP2POffline === 'function') _applyP2POffline();
          updateNetworkToggleUI();
        });
      return;
    }
    // Turning network ON: connect WS only — do NOT auto-activate buddies/public
    localStorage.setItem('frj_p2p_offline', '0');
    if (typeof p2pSetOffline === 'function') p2pSetOffline(false);
    if (typeof _applyP2POffline === 'function') _applyP2POffline();
  } else {
    // Turning network OFF: also clear buddies + public
    localStorage.setItem('frj_p2p_offline', '1');
    localStorage.setItem('frj_buddies_enabled', '0');
    localStorage.setItem('frj_public_enabled',  '0');
    if (typeof p2pDeregisterPresenceOnly === 'function') p2pDeregisterPresenceOnly();
    if (typeof p2pSetOffline === 'function') p2pSetOffline(true);
    if (typeof _applyP2POffline === 'function') _applyP2POffline();
  }
  updateNetworkToggleUI();
}

function toggleBuddiesBtn() {
  const on = localStorage.getItem('frj_buddies_enabled') === '1';
  if (on) {
    // Turning buddies OFF: also turn off public
    localStorage.setItem('frj_buddies_enabled', '0');
    localStorage.setItem('frj_public_enabled',  '0');
    if (typeof p2pDeregisterPresenceOnly === 'function') p2pDeregisterPresenceOnly();
    if (typeof p2pDisconnectBuddies === 'function') p2pDisconnectBuddies();
  } else {
    // Turning buddies ON: activate private presence (requires network)
    localStorage.setItem('frj_buddies_enabled', '1');
    if (getNetworkToggles().network) {
      const desired = localStorage.getItem('frj_public_enabled') === '1' ? 'public' : 'private';
      if (typeof setPresenceState === 'function') setPresenceState(desired);
    }
  }
  if (typeof stampSettingsTs === 'function') stampSettingsTs();
  if (typeof syncAfterSave  === 'function') syncAfterSave();
  updateNetworkToggleUI();
}

function togglePublicBtn() {
  const on = localStorage.getItem('frj_public_enabled') === '1';
  if (on) {
    // Turning public OFF: downgrade to private presence
    localStorage.setItem('frj_public_enabled', '0');
    if (getNetworkToggles().network && localStorage.getItem('frj_buddies_enabled') !== '0') {
      if (typeof setPresenceState === 'function') setPresenceState('private');
    }
    if (typeof stampSettingsTs === 'function') stampSettingsTs();
    if (typeof syncAfterSave  === 'function') syncAfterSave();
  } else {
    // Turning public ON: warn first
    const warned = localStorage.getItem('frj_public_warned');
    const doEnable = () => {
      localStorage.setItem('frj_public_enabled',  '1');
      localStorage.setItem('frj_buddies_enabled', '1');
      if (getNetworkToggles().network) {
        if (typeof setPresenceState === 'function') setPresenceState('public');
      }
      if (typeof stampSettingsTs === 'function') stampSettingsTs();
      if (typeof syncAfterSave  === 'function') syncAfterSave();
      updateNetworkToggleUI();
    };
    if (!warned) {
      confirm2('Go public?',
        'Other public restorers will see that you are online. Your avatar and display name (if enabled) will be held in server memory so other restorers can find you, even when you\'re offline — until you go private or the server is next updated. Your identity is pseudonymous, but your presence reveals that someone at your IP address is using this app. You can go private again at any time, which immediately removes you from the directory.',
        () => { localStorage.setItem('frj_public_warned', '1'); localStorage.setItem('frj_online_warned', '1'); doEnable(); });
      return;
    }
    doEnable();
    return;
  }
  updateNetworkToggleUI();
}

// ── Compound network actions ──────────────────────────────────────────────────
// "Go online" — network + buddies + public in one tap (used by Restorer Network panel)

async function goOnlinePublic() {
  const encEnabled = typeof idbMetaGet === 'function' ? await idbMetaGet('encryptionEnabled') : false;
  if (!encEnabled) {
    if (typeof confirm2 === 'function') {
      confirm2('Encryption required', 'Your data must be encrypted before going online. Set up a password now?',
        () => { if (typeof showMigrationModal === 'function') showMigrationModal(); });
    }
    return;
  }

  const _doGoPublic = () => {
    localStorage.setItem('frj_p2p_offline',    '0');
    localStorage.setItem('frj_buddies_enabled', '1');
    localStorage.setItem('frj_public_enabled',  '1');
    if (typeof p2pSetOffline === 'function') p2pSetOffline(false);
    if (typeof _applyP2POffline === 'function') _applyP2POffline();
    if (getNetworkToggles().network) {
      if (typeof setPresenceState === 'function') setPresenceState('public');
    }
    if (typeof stampSettingsTs === 'function') stampSettingsTs();
    if (typeof syncAfterSave   === 'function') syncAfterSave();
    updateNetworkToggleUI();
    if (typeof updateRestorersOnlineUI === 'function') updateRestorersOnlineUI();
  };

  // Both warnings already acknowledged → instant toggle
  if (localStorage.getItem('frj_online_warned') && localStorage.getItem('frj_public_warned')) {
    _doGoPublic();
    return;
  }

  // First time (either or both unset) → one combined dialog
  if (typeof confirm2 === 'function') {
    confirm2('Go online & public?',
      'Your device will connect to the Forestorers signal server. The server cannot read your data, but it can see your IP address and when you connect. All buddy communication is end-to-end encrypted.\n\nOther public restorers will see that you are online. Your avatar and display name (if enabled) will be held in server memory so others can find you. Your identity is pseudonymous. You can go private again at any time.',
      () => {
        localStorage.setItem('frj_online_warned', '1');
        localStorage.setItem('frj_public_warned', '1');
        _doGoPublic();
      });
  }
}

// "Turn on buddy network" — network + buddies (not public) — used by Connect panel

async function turnOnBuddyNetwork() {
  const t = getNetworkToggles();
  if (t.network && t.buddies) return; // already on

  const encEnabled = typeof idbMetaGet === 'function' ? await idbMetaGet('encryptionEnabled') : false;
  if (!encEnabled) {
    if (typeof confirm2 === 'function') {
      confirm2('Encryption required', 'Your data must be encrypted before going online. Set up a password now?',
        () => { if (typeof showMigrationModal === 'function') showMigrationModal(); });
    }
    return;
  }

  const _doEnable = () => {
    localStorage.setItem('frj_p2p_offline',    '0');
    localStorage.setItem('frj_buddies_enabled', '1');
    if (typeof p2pSetOffline === 'function') p2pSetOffline(false);
    if (typeof _applyP2POffline === 'function') _applyP2POffline();
    if (getNetworkToggles().network) {
      const desired = localStorage.getItem('frj_public_enabled') === '1' ? 'public' : 'private';
      if (typeof setPresenceState === 'function') setPresenceState(desired);
    }
    if (typeof stampSettingsTs === 'function') stampSettingsTs();
    if (typeof syncAfterSave   === 'function') syncAfterSave();
    updateNetworkToggleUI();
  };

  if (!localStorage.getItem('frj_online_warned')) {
    if (typeof confirm2 === 'function') {
      confirm2('Going online',
        'Your device will connect to the Forestorers signal server. The server cannot read your data, but it can see your IP address and when you connect. All buddy communication is end-to-end encrypted. Continue?',
        () => { localStorage.setItem('frj_online_warned', '1'); _doEnable(); });
    }
    return;
  }
  _doEnable();
}

// ── Restorers Online row ──────────────────────────────────────────────────────

// Cache of contact pubKeyHashes so we can filter them from the restorers list.
// Invalidated whenever contacts change.
let _contactHashCache = null;
function invalidateContactHashCache() { _contactHashCache = null; }

async function _getContactHashes() {
  if (_contactHashCache) return _contactHashCache;
  const contacts = (typeof getContacts === 'function') ? getContacts() : [];
  const hashes = new Set();
  for (const c of contacts) {
    // Skip tombstones -- a removed/forgotten contact should still be discoverable
    // in the public directory so they can be re-added later. The tombstone exists
    // only for device-sync propagation, not to permanently hide that identity.
    if (c.removedAt || c.forgottenAt) continue;
    try {
      const b64 = c.pubKeyB64.replace(/-/g, '+').replace(/_/g, '/');
      const raw = Uint8Array.from(atob(b64), ch => ch.charCodeAt(0));
      const digest = await crypto.subtle.digest('SHA-256', raw);
      const h = btoa(String.fromCharCode(...new Uint8Array(digest)))
        .replace(/\+/g, '-').replace(/\//g, '_').replace(/=/g, '');
      hashes.add(h.slice(0, 16));
    } catch(e) {}
  }
  _contactHashCache = hashes;
  return hashes;
}

// ── Buddy dropdown inline toggles ─────────────────────────────────────────
// Mirrors the sidebar's Buddies + Public toggles 1:1: same state lookup, same
// cascade, same visual semantics (green = on, gray = off). The "buddies"
// toggle sits at the TOP of the dropdown (right after the header); "public
// profile" sits at the BOTTOM.
// Re-runs on every updateNetworkToggleUI() / updateRestorersOnlineUI() call
// to survive the dropdown's innerHTML being wiped by _doUpdatePeerStatus.
function _renderBuddyDropdownToggles() {
  const dd = document.getElementById('buddyDropdown');
  if (!dd) return;
  const t = (typeof getNetworkToggles === 'function') ? getNetworkToggles() : { network: false, buddies: false, public: false };

  // Top: Online with buddies. Position AFTER the header (firstChild) so it
  // sits inside the dropdown's content area, not above the title bar.
  let topRow = document.getElementById('buddyDropdown-toggle-buddies');
  if (!topRow) {
    topRow = document.createElement('button');
    topRow.id = 'buddyDropdown-toggle-buddies';
    topRow.type = 'button';
    topRow.setAttribute('data-action', 'toggleBuddiesBtn');
    topRow.innerHTML = `<span class="dd-toggle-dot"></span><span class="dd-toggle-label">Online with buddies</span><span class="dd-toggle-state">OFF</span>`;
  }
  topRow.className = 'dd-toggle-row';
  // Always re-position to (firstChild + 1) so we survive innerHTML resets.
  const desiredTopPos = dd.firstChild ? dd.firstChild.nextSibling : null;
  if (topRow.parentNode !== dd || topRow.nextSibling !== desiredTopPos) {
    dd.insertBefore(topRow, desiredTopPos);
  }
  const buddiesOn = t.network && t.buddies;
  topRow.classList.toggle('on', buddiesOn);
  topRow.querySelector('.dd-toggle-state').textContent = buddiesOn ? 'ON' : 'OFF';

  // Bottom: Public profile (Listed in Restorers Online). Always last child.
  let botRow = document.getElementById('buddyDropdown-toggle-public');
  if (!botRow) {
    botRow = document.createElement('button');
    botRow.id = 'buddyDropdown-toggle-public';
    botRow.type = 'button';
    botRow.setAttribute('data-action', 'togglePublicBtn');
    botRow.innerHTML = `<span class="dd-toggle-dot"></span><span class="dd-toggle-label">Listed in Restorers Online</span><span class="dd-toggle-state">OFF</span>`;
  }
  botRow.className = 'dd-toggle-row';
  botRow.style.borderTop = '1px solid var(--border2)';
  botRow.style.borderBottom = 'none';
  // Always re-append to keep it at the end even after other sections insert.
  if (dd.lastChild !== botRow) dd.appendChild(botRow);
  const publicOn = buddiesOn && t.public;
  botRow.classList.toggle('on', publicOn);
  botRow.classList.toggle('disabled', !buddiesOn);
  botRow.querySelector('.dd-toggle-state').textContent = publicOn ? 'ON' : 'OFF';
}

async function updateRestorersOnlineUI() {
  // Always render the dropdown toggles -- they're independent of whether any
  // restorers are online and need to be present even when offline.
  _renderBuddyDropdownToggles();
  const onlineRow     = document.getElementById('restorers-online-row');
  const onlineSection = document.getElementById('buddyDropdown-restorers-online');
  const recentRow     = document.getElementById('restorers-recent-row');
  const recentSection = document.getElementById('buddyDropdown-restorers-recent');
  if (!onlineRow || !onlineSection) { _renderPendingOffers(); _renderIncomingRequests(); return; }

  const state = (typeof p2pGetPresenceState === 'function') ? p2pGetPresenceState() : 'offline';

  // Get both directory entries and live presence list
  const directory = (typeof p2pGetDirectory === 'function') ? p2pGetDirectory() : [];
  const liveList  = (typeof p2pGetRestorersOnline === 'function') ? p2pGetRestorersOnline() : [];

  // presence_list is the AUTHORITATIVE source for online status
  const liveHashes = new Set(liveList.map(r => r.hash).filter(Boolean));
  const livePresMap = new Map(); // hash → presenceId
  for (const r of liveList) { if (r.hash) livePresMap.set(r.hash, r.presenceId); }

  // Merge: directory provides profile data, presence_list provides online truth
  const merged = new Map();
  for (const d of directory) {
    if (!d.hash) continue;
    merged.set(d.hash, {
      hash: d.hash, emoji: d.emoji, displayName: d.displayName || null,
      lastSeen: d.lastSeen, online: liveHashes.has(d.hash), // presence_list is truth
      presenceId: livePresMap.get(d.hash) || null
    });
  }
  // Include live restorers not yet in directory (just went public)
  for (const r of liveList) {
    if (!r.hash || merged.has(r.hash)) continue;
    merged.set(r.hash, { hash: r.hash, emoji: r.emoji, displayName: null, lastSeen: Date.now(), online: true, presenceId: r.presenceId });
  }

  // Hide everything if offline or empty
  if (state === 'offline' || !merged.size) {
    onlineSection.style.display = 'none';
    if (recentSection) recentSection.style.display = 'none';
    _renderPendingOffers(); _renderIncomingRequests();
    return;
  }

  // Filter out existing buddies + blocked
  const buddyHashes = await _getContactHashes();
  const entries = [...merged.values()].filter(r => !buddyHashes.has(r.hash));

  // Split into online vs recently active (past 7 days, offline only)
  const SEVEN_DAYS = 7 * 24 * 60 * 60 * 1000;
  const now = Date.now();
  const onlineEntries = [];
  const recentEntries = [];

  for (const r of entries) {
    if (r.online) {
      onlineEntries.push(r);
    } else if (r.lastSeen && (now - r.lastSeen) < SEVEN_DAYS) {
      recentEntries.push(r);
    }
  }

  // Sort online: custom emoji first
  onlineEntries.sort((a, b) => {
    const aDefault = a.emoji === '🌿';
    const bDefault = b.emoji === '🌿';
    if (aDefault !== bDefault) return aDefault ? 1 : -1;
    return 0;
  });

  // Sort recent: most recently seen first, custom emoji tiebreaker
  recentEntries.sort((a, b) => {
    return (b.lastSeen || 0) - (a.lastSeen || 0);
  });

  const MAX_ONLINE = 16;
  const MAX_RECENT = 16;

  // ── Chip builder (dropdown — compact emoji squares, one tap → profile) ──
  // Idle = restorer is online but their tab/screen is hidden -- shown as amber.
  const _dropdownOnlineChip = r => {
    const dotColor = r.idle ? '#f97316' : '#22c55e';
    const titleSfx = r.idle ? ' (away)' : '';
    return `<div class="restorer-item" style="position:relative" data-online="1">
      <button class="restorer-emoji-btn" title="${r.displayName ? esc(r.displayName) + titleSfx : 'View profile' + titleSfx}"
        data-action="openRestorerProfile" data-a0="${esc(r.hash||'')}" data-a1="${esc(r.emoji)}" data-a2="${esc(r.displayName||'')}" data-a3="1">${esc(r.emoji)}<span style="position:absolute;bottom:-1px;right:-1px;width:7px;height:7px;background:${dotColor};border-radius:50%;border:1.5px solid var(--bg)"></span></button>
    </div>`;
  };
  const _dropdownRecentChip = r =>
    `<div class="restorer-item restorer-recent" style="position:relative;opacity:0.65">
      <button class="restorer-emoji-btn" title="${r.displayName ? esc(r.displayName) : 'View profile'}"
        data-action="openRestorerProfile" data-a0="${esc(r.hash||'')}" data-a1="${esc(r.emoji)}" data-a2="${esc(r.displayName||'')}" data-a3="0">${esc(r.emoji)}</button>
    </div>`;

  // ── Row builder (contacts page — expanded, one tap on body → profile) ──
  const _fmtLastSeen = ts => {
    if (!ts) return '';
    const days = (Date.now() - ts) / 86400000;
    if (days < 1/24) return 'seen just now';
    if (days < 1) return `seen ${Math.round(days * 24)}h ago`;
    return `seen ${Math.floor(days)}d ago`;
  };
  const _contactsRow = (r, isOnline) => {
    let ciBadge = '';
    try {
      const cache = JSON.parse(localStorage.getItem('frj_profile_cache') || '{}');
      const entry = cache[r.hash];
      if (entry && entry.ci != null && Date.now() - entry.cachedAt < 7 * 86400000) {
        // entry.ci comes from a peer; render-time escape as defence in depth
        // (it is also coerced to a bounded integer at storage time).
        ciBadge = ` <span class="ci-badge" style="font-size:9px;padding:1px 4px;vertical-align:middle">CI-${esc(String(entry.ci))}</span>`;
      }
    } catch(e) {}
    const idle = !!r.idle && isOnline;
    const statusColor = idle ? '#f97316' : (isOnline ? 'var(--green)' : 'var(--muted2)');
    const statusLabel = idle ? 'Away'
      : isOnline ? '<span style="display:inline-block;width:6px;height:6px;border-radius:50%;background:var(--green);margin-right:3px;vertical-align:middle"></span>Online now'
      : _fmtLastSeen(r.lastSeen);
    const idleDot = idle ? '<span style="display:inline-block;width:6px;height:6px;border-radius:50%;background:#f97316;margin-right:3px;vertical-align:middle"></span>' : '';
    return `<div class="restorer-item" style="display:flex;align-items:center;gap:10px;padding:5px 4px;border-radius:6px;cursor:pointer"
      data-action="openRestorerProfile" data-a0="${esc(r.hash||'')}" data-a1="${esc(r.emoji)}" data-a2="${esc(r.displayName||'')}" data-a3="${isOnline ? '1' : '0'}" data-online="${isOnline ? '1' : '0'}">
      <span style="font-size:22px;flex-shrink:0;pointer-events:none">${esc(r.emoji)}</span>
      <div style="flex:1;min-width:0;pointer-events:none">
        <div style="font-size:12px;font-weight:600;color:var(--text);white-space:nowrap;overflow:hidden;text-overflow:ellipsis">${r.displayName ? esc(r.displayName) : 'Restorer'}${ciBadge}</div>
        <div style="font-size:10px;color:${statusColor}">
          ${idleDot}${statusLabel}
        </div>
      </div>
      <button class="restorer-block-btn" title="Block" style="flex-shrink:0"
        data-action="blockRestorer" data-a0="${esc(r.hash||'')}" data-a1="${esc(r.emoji)}">✕</button>
    </div>`;
  };

  // ── Render Online section ──
  const cOnlineSection = document.getElementById('contacts-restorers-online-section');
  const cOnlineRow     = document.getElementById('contacts-restorers-online-row');
  const cRecentSection = document.getElementById('contacts-restorers-recent-section');
  const cRecentRow     = document.getElementById('contacts-restorers-recent-row');
  const cNudge         = document.getElementById('contacts-restorers-nudge');
  const cEmpty         = document.getElementById('contacts-restorers-empty');

  if (onlineEntries.length) {
    onlineSection.style.display = '';
    const shown = onlineEntries.slice(0, MAX_ONLINE);
    const overflow = onlineEntries.length - MAX_ONLINE;
    onlineRow.innerHTML = shown.map(_dropdownOnlineChip).join('') + (overflow > 0
      ? `<span style="font-family:var(--mono);font-size:10px;color:var(--muted2);align-self:center;padding:0 4px">+${overflow}</span>`
      : '');
    if (cOnlineSection) cOnlineSection.style.display = '';
    if (cOnlineRow) cOnlineRow.innerHTML = shown.map(r => _contactsRow(r, true)).join('');
  } else {
    onlineSection.style.display = 'none';
    if (cOnlineSection) cOnlineSection.style.display = 'none';
  }

  // ── Render Recently Active section ──
  if (recentSection && recentRow) {
    if (recentEntries.length) {
      recentSection.style.display = '';
      const shown = recentEntries.slice(0, MAX_RECENT);
      const overflow = recentEntries.length - MAX_RECENT;
      recentRow.innerHTML = shown.map(_dropdownRecentChip).join('') + (overflow > 0
        ? `<span style="font-family:var(--mono);font-size:10px;color:var(--muted2);align-self:center;padding:0 4px">+${overflow}</span>`
        : '');
      if (cRecentSection) cRecentSection.style.display = '';
      if (cRecentRow) cRecentRow.innerHTML = shown.map(r => _contactsRow(r, false)).join('');
    } else {
      recentSection.style.display = 'none';
      if (cRecentSection) cRecentSection.style.display = 'none';
    }
  }

  // ── Contacts page: nudge / empty state ──
  if (cNudge) cNudge.style.display = state === 'public' ? 'none' : '';
  if (cEmpty) {
    const hasContent = onlineEntries.length || recentEntries.length;
    cEmpty.style.display = (state === 'public' && !hasContent) ? '' : 'none';
  }
  // incoming/pending are self-managed by _renderIncomingRequests/_renderPendingOffers

  _renderPendingOffers();
  _renderIncomingRequests();
}

function _renderPendingOffers() {
  const pending = (typeof p2pGetPendingOffers === 'function') ? p2pGetPendingOffers() : [];
  const exchanges = (typeof p2pGetPendingExchanges === 'function') ? p2pGetPendingExchanges() : [];

  const _pendingHtml = pending.map(o => `
    <div style="display:flex;align-items:center;gap:8px;padding:4px 0">
      <span style="font-size:20px;flex-shrink:0">${esc(o.emoji)}</span>
      <span style="font-size:12px;color:var(--muted2);flex:1">Waiting for reply…</span>
      <button data-action="p2pCancelOffer" data-a0="${esc(o.presenceId)}" style="font-family:var(--mono);font-size:11px;color:var(--red,#ef4444);background:none;border:none;cursor:pointer;padding:4px 8px">Cancel</button>
    </div>`).join('');

  const _exchangeHtml = exchanges.map(e => {
    const age = Date.now() - e.sentAt;
    const expired = age > 7 * 86400000;
    const agoText = age < 3600000 ? `${Math.max(1, Math.round(age / 60000))}m ago`
      : age < 86400000 ? `${Math.round(age / 3600000)}h ago`
      : `${Math.floor(age / 86400000)}d ago`;
    return `<div style="display:flex;align-items:center;gap:8px;padding:4px 0${expired ? ';opacity:.5' : ''}">
      <span style="font-size:20px;flex-shrink:0">${esc(e.targetEmoji || '🌿')}</span>
      <div style="flex:1;min-width:0">
        <div style="font-size:12px;font-weight:600;color:var(--text);white-space:nowrap;overflow:hidden;text-overflow:ellipsis">${esc(e.targetName || 'Restorer')}</div>
        <div style="font-family:var(--mono);font-size:10px;color:var(--muted2)">${expired ? 'Expired' : 'Sent ' + agoText}</div>
      </div>
      <button data-action="cancelPendingExchange" data-a0="${esc(e.targetHash)}" style="font-family:var(--mono);font-size:11px;color:var(--red,#ef4444);background:none;border:none;cursor:pointer;padding:4px 8px">${expired ? 'Remove' : 'Cancel'}</button>
    </div>`;
  }).join('');

  const allHtml = _pendingHtml + _exchangeHtml;
  const hasContent = pending.length || exchanges.length;

  // ── Dropdown ──
  let pendingSection = document.getElementById('buddyDropdown-pending');
  if (hasContent) {
    if (!pendingSection) {
      pendingSection = document.createElement('div');
      pendingSection.id = 'buddyDropdown-pending';
      pendingSection.style.cssText = 'border-top:1px solid var(--border2);padding:6px 12px';
      const restorersSection = document.getElementById('buddyDropdown-restorers-recent')
        || document.getElementById('buddyDropdown-restorers-online');
      if (restorersSection && restorersSection.parentNode) {
        restorersSection.parentNode.insertBefore(pendingSection, restorersSection.nextSibling);
      }
    }
    pendingSection.innerHTML = `<div style="font-family:var(--mono);font-size:11px;color:var(--muted2);text-transform:uppercase;letter-spacing:.05em;margin-bottom:4px">Sent Requests</div>${allHtml}`;
    pendingSection.style.display = '';
  } else if (pendingSection) {
    pendingSection.style.display = 'none';
  }

  // ── Contacts page ──
  const cPending = document.getElementById('contacts-pending-section');
  if (cPending) {
    if (hasContent) {
      cPending.innerHTML = `<div style="font-family:var(--mono);font-size:11px;font-weight:700;letter-spacing:.06em;text-transform:uppercase;color:var(--muted2);margin-bottom:6px;margin-top:2px">Sent Requests</div>${allHtml}`;
      cPending.style.display = '';
    } else {
      cPending.innerHTML = '';
    }
  }
}

function _renderIncomingRequests() {
  const incoming = (typeof p2pGetIncomingRequests === 'function') ? p2pGetIncomingRequests() : [];
  // Header-button notification orb: visible whenever any request is pending,
  // hidden as soon as the user accepts/ignores them all.
  const reqDot = document.getElementById('buddyReqDot');
  if (reqDot) reqDot.style.display = incoming.length ? '' : 'none';
  const _incomingHtml = incoming.length ? incoming.map(r => `
    <div style="display:flex;align-items:center;gap:8px;padding:4px 0">
      <span style="font-size:20px;flex-shrink:0">${esc(r.fromEmoji || '🌿')}</span>
      <span style="font-size:12px;font-weight:600;flex:1;color:var(--text)">${esc(r.fromName || 'Restorer')}</span>
      <button data-action="acceptBuddyRequest" data-a0="${esc(r.fromHash)}" class="btn btn-primary btn-sm">Accept</button>
      <button data-action="ignoreBuddyRequest" data-a0="${esc(r.fromHash)}" style="font-family:var(--mono);font-size:11px;color:var(--muted2);background:none;border:none;cursor:pointer;padding:4px 8px">Ignore</button>
    </div>`).join('') : '';

  // ── Dropdown ──
  let incomingSection = document.getElementById('buddyDropdown-incoming');
  if (incoming.length) {
    if (!incomingSection) {
      incomingSection = document.createElement('div');
      incomingSection.id = 'buddyDropdown-incoming';
      incomingSection.style.cssText = 'border-top:1px solid var(--border2);padding:6px 12px';
      const after = document.getElementById('buddyDropdown-pending')
        || document.getElementById('buddyDropdown-restorers-recent')
        || document.getElementById('buddyDropdown-restorers-online');
      if (after && after.parentNode) after.parentNode.insertBefore(incomingSection, after.nextSibling);
    }
    incomingSection.innerHTML = `<div style="font-family:var(--mono);font-size:11px;color:var(--muted2);text-transform:uppercase;letter-spacing:.05em;margin-bottom:4px">Buddy Requests</div>${_incomingHtml}`;
    incomingSection.style.display = '';
  } else if (incomingSection) {
    incomingSection.style.display = 'none';
  }

  // ── Contacts page (inside Restorer Network panel -- secondary surface) ──
  const cIncoming = document.getElementById('contacts-incoming-section');
  if (cIncoming) {
    if (incoming.length) {
      cIncoming.innerHTML = `<div style="font-family:var(--mono);font-size:11px;font-weight:700;letter-spacing:.06em;text-transform:uppercase;color:var(--accent);margin-bottom:6px">Buddy Requests</div>${_incomingHtml}`;
      cIncoming.style.display = '';
    } else {
      cIncoming.innerHTML = '';
    }
  }

  // ── Contacts page top banner (primary surface, always visible) ──
  // Sits above My Buddies so it's reachable even when Restorer Network is
  // collapsed / the user is in private mode. Council Phase L+1: the persistent
  // surface that ensures incoming requests can't be missed.
  const cBanner = document.getElementById('contacts-incoming-banner');
  if (cBanner) {
    if (incoming.length) {
      cBanner.innerHTML = `
        <div class="panel" style="margin-bottom:1rem;border:1px solid var(--accent);background:rgba(99,102,241,.06)">
          <div class="ph" style="background:transparent;border-bottom:1px solid rgba(99,102,241,.2)">
            <h2 style="color:var(--accent)">
              ${incoming.length > 1 ? `${incoming.length} buddy requests` : 'Buddy request'}
            </h2>
          </div>
          <div style="padding:10px 14px;display:flex;flex-direction:column;gap:6px">
            ${_incomingHtml}
          </div>
        </div>`;
    } else {
      cBanner.innerHTML = '';
    }
  }
}

// Declined-request memory: honoring a decline needs to survive relay staleness.
// Even if the relay never drops the queued request (old relay, or the decline
// message is lost), a declined hash is suppressed locally so it never re-surfaces.
const _DECLINED_REQ_KEY = 'frj_declined_requests';
function _getDeclinedRequests() {
  try { return JSON.parse(localStorage.getItem(_DECLINED_REQ_KEY) || '[]'); } catch (e) { return []; }
}
function isRequestDeclined(hash) {
  return !!hash && _getDeclinedRequests().includes(hash);
}
function _markRequestDeclined(hash) {
  if (!hash) return;
  try {
    const d = _getDeclinedRequests();
    if (!d.includes(hash)) { d.push(hash); localStorage.setItem(_DECLINED_REQ_KEY, JSON.stringify(d.slice(-200))); }
  } catch (e) { /* non-fatal */ }
}
// Called if the user later re-adds / accepts a previously declined restorer.
function clearDeclinedRequest(hash) {
  try {
    const d = _getDeclinedRequests().filter(h => h !== hash);
    localStorage.setItem(_DECLINED_REQ_KEY, JSON.stringify(d));
  } catch (e) { /* non-fatal */ }
}

function _ignoreBuddyRequest(fromHash) {
  _markRequestDeclined(fromHash);
  // Ask the relay to drop the queued request so it stops redelivering on reconnect.
  if (typeof _sigSend === 'function') _sigSend({ type: 'buddy_request_decline', fromHash });
  if (typeof p2pGetIncomingRequests === 'function') {
    const reqs = p2pGetIncomingRequests();
    const idx = reqs.findIndex(r => r.fromHash === fromHash);
    if (idx !== -1) reqs.splice(idx, 1);
    if (typeof _persistIncomingRequests === 'function') _persistIncomingRequests();
  }
  _renderIncomingRequests();
}

// Tap-to-reveal: first tap shows name inline, second tap opens profile
let _revealedHash = null;
function revealRestorer(hash, emoji, displayName) {
  if (_revealedHash === hash) {
    // Second tap → open full profile (detect online from parent data attribute)
    _revealedHash = null;
    const item = _findRestorerItem(hash);
    const isOnline = item?.closest('[data-online]')?.dataset.online === '1' || item?.dataset.online === '1';
    openRestorerProfile(hash, emoji, displayName, isOnline ? '1' : '0');
    return;
  }
  // First tap → reveal name inline
  _revealedHash = hash;
  // Update items in both online and recent rows (dropdown + contacts page)
  const rows = [
    document.getElementById('restorers-online-row'),
    document.getElementById('restorers-recent-row'),
    document.getElementById('contacts-restorers-online-row'),
    document.getElementById('contacts-restorers-recent-row'),
  ];
  for (const row of rows) {
    if (!row) continue;
    const items = row.querySelectorAll('.restorer-emoji-btn');
    for (const btn of items) {
      const item = btn.closest('.restorer-item');
      if (!item) continue;
      const label = item.querySelector('.restorer-reveal-label');
      if (btn.dataset.a0 === hash) {
        // Reveal this one
        const isRecent = item.classList.contains('restorer-recent');
        if (isRecent) item.style.opacity = '0.85';
        if (!label) {
          const lbl = document.createElement('div');
          lbl.className = 'restorer-reveal-label';
          lbl.style.cssText = 'font-family:var(--mono);font-size:11px;color:var(--muted2);text-align:center;max-width:44px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;margin-top:1px';
          lbl.textContent = displayName || 'Restorer';
          item.appendChild(lbl);
        }
      } else {
        // Collapse previously revealed
        if (item.classList.contains('restorer-recent')) item.style.opacity = '0.55';
        if (label) label.remove();
      }
    }
  }
}

function _findRestorerItem(hash) {
  const rows = [
    document.getElementById('restorers-online-row'),
    document.getElementById('restorers-recent-row'),
    document.getElementById('contacts-restorers-online-row'),
    document.getElementById('contacts-restorers-recent-row'),
  ];
  for (const row of rows) {
    if (!row) continue;
    for (const btn of row.querySelectorAll('[data-action="revealRestorer"],[data-action="openRestorerProfile"]')) {
      if (btn.dataset.a0 === hash) return btn.closest('.restorer-item');
    }
  }
  return null;
}

// ── Block a restorer ──────────────────────────────────────────────────────────

function blockRestorer(hash, emoji) {
  if (!hash) {
    if (typeof toast === 'function') toast('Cannot block - no identity hash available', true);
    return;
  }
  if (typeof confirm2 === 'function') {
    confirm2(
      `Block ${emoji}?`,
      'This restorer will no longer appear in Restorers Online. The block is stored on this device.',
      () => {
        if (typeof p2pBlockPresence === 'function') p2pBlockPresence(hash);
        if (typeof toast === 'function') toast(`${emoji} blocked`);
      }
    );
  }
}

// ── Restorer profile view (tap emoji → see profile → action buttons) ──────────

async function openRestorerProfile(hash, emoji, displayName, onlineFlag) {
  const state = (typeof p2pGetPresenceState === 'function') ? p2pGetPresenceState() : 'offline';
  if (state === 'offline') {
    if (typeof toast === 'function') toast('Go online first to view restorers', true);
    return;
  }
  if (typeof closeBuddyDropdown === 'function') closeBuddyDropdown();

  const isOnline = onlineFlag === '1' || onlineFlag === true;

  let overlay = document.getElementById('restorerProfileModal');
  if (!overlay) {
    overlay = document.createElement('div');
    overlay.id = 'restorerProfileModal';
    overlay.className = 'modal-overlay';
    document.body.appendChild(overlay);
  }

  const safeHash  = esc(hash || '');
  const safeEmoji = esc(emoji);
  const safeName  = displayName ? esc(displayName) : 'Restorer';
  const statusLabel = isOnline
    ? '<span style="color:#22c55e;font-size:10px">● Online</span>'
    : '<span style="color:var(--muted2);font-size:10px">○ Offline</span>';

  overlay.innerHTML = `<div class="modal-box" style="max-width:320px;text-align:center">
    <div class="modal-hdr">
      <span style="font-size:14px">${safeEmoji} ${safeName}</span>
      <button data-action="closeModal" data-a0="restorerProfileModal">✕</button>
    </div>
    <div class="modal-body" id="restorerProfileBody" style="padding:16px 18px 8px">
      <div style="font-size:44px;margin:8px 0 6px">${safeEmoji}</div>
      <div style="font-size:15px;font-weight:600;margin-bottom:4px">${safeName}</div>
      ${statusLabel}
      ${isOnline && state === 'public'
        ? '<div style="font-family:var(--mono);font-size:11px;color:var(--muted2);margin-top:8px">Loading profile…</div>'
        : '<div style="margin-top:8px"></div>'
      }
    </div>
    <div style="padding:0 18px 16px;display:flex;gap:8px;justify-content:center">
      <button class="btn btn-ghost btn-sm" data-action="blockRestorer" data-a0="${safeHash}" data-a1="${safeEmoji}">Block</button>
      <button class="btn btn-primary btn-sm" data-action="openHandshakePanel" data-a0="${safeHash}" data-a1="${safeEmoji}" data-a2="${isOnline?'1':'0'}">Send Buddy Request</button>
    </div>
  </div>`;

  overlay.classList.add('open');

  // If online + we're public, supplement with real-time profile data
  if (isOnline && state === 'public' && typeof p2pRequestProfile === 'function') {
    // Find presenceId for this hash from the live list
    const liveList = (typeof p2pGetRestorersOnline === 'function') ? p2pGetRestorersOnline() : [];
    const liveEntry = liveList.find(r => r.hash === hash);
    if (liveEntry?.presenceId) {
      const payload = await p2pRequestProfile(liveEntry.presenceId);
      const body = document.getElementById('restorerProfileBody');
      if (!body || !overlay.classList.contains('open')) return;

      // Cache profile for directory CI badges (7-day TTL, self-pruning)
      if (payload && hash) {
        try {
          const cache = JSON.parse(localStorage.getItem('frj_profile_cache') || '{}');
          const now = Date.now();
          // Prune expired entries
          for (const k of Object.keys(cache)) {
            if (now - cache[k].cachedAt > 7 * 86400000) delete cache[k];
          }
          // Coerce peer-supplied fields: CI to a bounded integer, name to a
          // length-capped string. Prevents a malicious public restorer from
          // stashing markup that later renders in the directory (stored XSS).
          const _rawCi = Number(payload.ci);
          const _ci = (Number.isFinite(_rawCi) && _rawCi >= 0 && _rawCi <= 10) ? Math.floor(_rawCi) : null;
          const _nm = (typeof payload.displayName === 'string') ? payload.displayName.slice(0, 60) : null;
          cache[hash] = { ci: _ci, displayName: _nm, cachedAt: now };
          localStorage.setItem('frj_profile_cache', JSON.stringify(cache));
        } catch(e) {}
      }

      const name  = (payload && payload.displayName) ? esc(payload.displayName) : safeName;
      const rows  = [];
      if (payload) {
        if (payload.ci        != null) rows.push(`<tr><td style="color:var(--muted2)">Current CI</td><td>CI-${esc(String(payload.ci))}</td></tr>`);
        if (payload.goalCI    != null) rows.push(`<tr><td style="color:var(--muted2)">Goal CI</td><td>CI-${esc(String(payload.goalCI))}</td></tr>`);
        if (payload.startingCI!= null) rows.push(`<tr><td style="color:var(--muted2)">Starting CI</td><td>CI-${esc(String(payload.startingCI))}</td></tr>`);
        if (payload.startDate)         rows.push(`<tr><td style="color:var(--muted2)">Since</td><td>${esc(payload.startDate)}</td></tr>`);
      }

      body.innerHTML = `
        <div style="font-size:44px;margin:8px 0 6px">${safeEmoji}</div>
        <div style="font-size:15px;font-weight:600;margin-bottom:4px">${name}</div>
        ${statusLabel}
        ${rows.length
          ? `<table style="width:100%;font-size:12px;border-collapse:collapse;text-align:left;margin:8px 0 4px">
              ${rows.join('')}
             </table>`
          : `<p style="font-size:12px;color:var(--muted2);margin-top:8px;margin-bottom:4px">No additional info shared</p>`
        }`;
    }
  }
}

// ── Stage 1 — Send handshake offer panel ─────────────────────────────────────

function openHandshakePanel(hash, emoji, onlineFlag) {
  const state = (typeof p2pGetPresenceState === 'function') ? p2pGetPresenceState() : 'offline';
  if (state === 'offline') {
    if (typeof toast === 'function') toast('Go online first to send a buddy request', true);
    return;
  }

  // Close the buddy dropdown
  if (typeof closeBuddyDropdown === 'function') closeBuddyDropdown();

  const isOnline = onlineFlag === '1' || onlineFlag === true;

  // If target is online and both are public, check if we can use real-time handshake
  if (isOnline && state === 'public') {
    // Find their presenceId from the live list
    const liveList = (typeof p2pGetRestorersOnline === 'function') ? p2pGetRestorersOnline() : [];
    const liveEntry = liveList.find(r => r.hash === hash);
    if (liveEntry?.presenceId) {
      // Use existing real-time handshake offer flow
      if (typeof confirm2 === 'function') {
        confirm2(
          `Send buddy request to ${emoji}?`,
          "They won't be notified unless they've also sent one to you. Buddy requests are completely anonymous until both of you send one.",
          () => {
            if (typeof p2pSendHandshakeOffer === 'function') {
              p2pSendHandshakeOffer(liveEntry.presenceId, emoji);
              if (typeof toast === 'function') toast('Request sent! You\'ll be notified if they send one back.');
            }
          }
        );
      }
      return;
    }
  }

  // Async buddy request (works online or offline)
  if (typeof confirm2 === 'function') {
    confirm2(
      `Send buddy request to ${emoji}?`,
      isOnline
        ? "They'll be notified of your request. If they accept, you'll both automatically become buddies."
        : "They're currently offline. Your request will be delivered when they next come online.",
      () => {
        if (typeof p2pSendBuddyRequest === 'function') {
          // Resolve displayName from directory entries if available
          const _dir = typeof p2pGetDirectory === 'function' ? p2pGetDirectory() : [];
          const _de = _dir.find(r => r.hash === hash);
          p2pSendBuddyRequest(hash, emoji, _de?.displayName || null);
        }
      }
    );
  }
}

// ── Stage 2 — Match confirmation modal ───────────────────────────────────────

function showHandshakeMatchModal(handshakeId, role, theirPartial) {
  const modal = document.getElementById('handshakeModal');
  const body  = document.getElementById('handshake-modal-body');
  if (!modal || !body) return;

  const myProfile = (typeof getProfile === 'function') ? getProfile() : {};
  const myEmoji = myProfile.avatar || '🌿';
  const myName  = localStorage.getItem('frj_displayname') || 'You';

  const theirEmoji = theirPartial?.emoji || '👤';
  const theirName  = theirPartial?.displayName ? esc(theirPartial.displayName) : 'Restorer';
  const theirPrefix = theirPartial?.buddyCodePrefix ? esc(theirPartial.buddyCodePrefix) + '…' : '-';

  body.innerHTML = `
    <div style="display:flex;justify-content:center;gap:32px;margin:16px 0 20px">
      <div style="text-align:center">
        <div style="font-size:40px;margin-bottom:6px">${esc(myEmoji)}</div>
        <div style="font-family:var(--mono);font-size:11px;font-weight:600;color:var(--text)">${esc(myName)}</div>
        <div style="font-family:var(--mono);font-size:11px;color:var(--muted2);margin-top:2px">You</div>
      </div>
      <div style="display:flex;align-items:center;font-size:20px;color:var(--muted2)">↔</div>
      <div style="text-align:center">
        <div style="font-size:40px;margin-bottom:6px">${esc(theirEmoji)}</div>
        <div style="font-family:var(--mono);font-size:11px;font-weight:600;color:var(--text)">${theirName}</div>
        <div style="font-family:var(--mono);font-size:11px;color:var(--muted2);margin-top:2px">Code: ${theirPrefix}</div>
      </div>
    </div>
    <p style="font-size:12px;color:var(--muted);line-height:1.6;margin:0 0 18px 0">
      Is this your buddy? You should only make this connection with people you can trust.
      Confirm their buddy code prefix matches what you expect before accepting.
    </p>
    <div style="display:flex;gap:8px;justify-content:center">
      <button class="btn btn-ghost" data-action="cancelHandshake" data-a0="${esc(handshakeId)}">Cancel</button>
      <button class="btn btn-primary" data-action="confirmHandshake" data-a0="${esc(handshakeId)}">Confirm</button>
    </div>`;

  modal.classList.add('open');

  // Auto-cancel after 10 minutes
  setTimeout(() => {
    const m = document.getElementById('handshakeModal');
    if (m && m.classList.contains('open')) {
      if (typeof p2pCancelHandshake === 'function') p2pCancelHandshake(handshakeId);
      if (typeof closeModal === 'function') closeModal('handshakeModal');
    }
  }, 10 * 60 * 1000);
}

function _confirmHandshakeFromModal(handshakeId) {
  if (typeof closeModal === 'function') closeModal('handshakeModal');
  if (typeof p2pConfirmHandshake === 'function') p2pConfirmHandshake(handshakeId);
  if (typeof toast === 'function') toast('Confirmed! Waiting for buddy to confirm their side…');
}

// ── Public profile data modal ─────────────────────────────────────────────────

function showProfileDataModal(payload) {
  if (!payload) return;

  let overlay = document.getElementById('profileDataModal');
  if (!overlay) {
    overlay = document.createElement('div');
    overlay.id = 'profileDataModal';
    overlay.className = 'modal-overlay';
    overlay.innerHTML = `<div class="modal-box" style="max-width:340px;text-align:center">
      <div class="modal-hdr"><span>Public Profile</span><button data-action="closeModal" data-a0="profileDataModal">✕</button></div>
      <div class="modal-body" id="profileDataBody"></div>
    </div>`;
    document.body.appendChild(overlay);
  }

  const body = document.getElementById('profileDataBody');
  if (!body) return;

  const emoji = payload.emoji || '🌿';
  const name  = payload.displayName || 'Restorer';
  const rows = [];
  if (payload.ci        != null) rows.push(`<div><span style="font-family:var(--mono);font-size:11px;color:var(--muted2)">CURRENT CI</span><div>CI-${esc(String(payload.ci))}</div></div>`);
  if (payload.goalCI    != null) rows.push(`<div><span style="font-family:var(--mono);font-size:11px;color:var(--muted2)">GOAL CI</span><div>CI-${esc(String(payload.goalCI))}</div></div>`);
  if (payload.startingCI != null) rows.push(`<div><span style="font-family:var(--mono);font-size:11px;color:var(--muted2)">STARTING CI</span><div>CI-${esc(String(payload.startingCI))}</div></div>`);
  if (payload.startDate) rows.push(`<div><span style="font-family:var(--mono);font-size:11px;color:var(--muted2)">RESTORING SINCE</span><div>${esc(payload.startDate)}</div></div>`);

  body.innerHTML = `
    <div style="font-size:48px;margin:12px 0 6px">${esc(emoji)}</div>
    <div style="font-size:16px;font-weight:600;margin-bottom:16px">${esc(name)}</div>
    ${rows.length
      ? `<div style="display:grid;grid-template-columns:1fr 1fr;gap:10px;text-align:left;font-size:13px;margin-bottom:16px">${rows.join('')}</div>`
      : `<p style="font-size:12px;color:var(--muted2);margin-bottom:16px">No public info shared</p>`
    }`;

  overlay.classList.add('open');
}
