'use strict';
// ═══════════════════════════════════════════════
// js/activity.js — Activity feed
//
// Read-only view projecting existing data (own journal, buddy journals,
// own gallery uploads, received gallery items) into a reverse-chronological
// feed of up to 100 cards. Creates no data, modifies no data.
// Comments and KOTs dispatch to existing p2pSendNote / p2pSendKot / etc.
// ═══════════════════════════════════════════════

const _ACTIVITY_MAX_ITEMS = 100;

// Filter state: Set of pubKeyB64 values to show. null = show all. '' = own posts.
let _afFilter = null; // null = no filter (show everything)
let _afBookmarkOnly = false;

function renderActivityPage() {
  const el = document.getElementById('activity-feed');
  if (!el) return;
  let items;
  try { items = _buildFeedItems(); } catch(e) { console.error('[activity] build error:', e); items = []; }

  // Apply user filter
  if (_afFilter) {
    items = items.filter(item => {
      if (!item.pubKeyB64) return _afFilter.has('');
      return _afFilter.has(item.pubKeyB64);
    });
  }
  // Hide buddy data if setting enabled
  if (localStorage.getItem('frj_hide_buddy_data') === '1') {
    items = items.filter(item => !item.pubKeyB64);
  }
  // Apply bookmark filter
  if (_afBookmarkOnly) {
    const bm = typeof getBookmarks === 'function' ? getBookmarks() : {};
    items = items.filter(item => {
      if (item.type === 'journal_own') return !!bm['journal:' + item.entry.id];
      if (item.type === 'gallery_own') return !!bm['gal:' + item.galleryId + ':' + item.itemId];
      if (item.type === 'gallery_buddy') return !!bm['recv:' + (item.senderSlice16 || '') + ':' + item.galleryId + ':' + item.itemId];
      if (item.type === 'comparison') return !!bm['comparison:' + item.comparison?.id];
      return false;
    });
  }
  const composer = _renderNewPostComposer();
  const filterBar = _renderFilterBar();
  if (!items.length) {
    el.innerHTML = `<div class="af-container">${composer}${filterBar}<div class="empty"><div class="icon">▤</div><p>${_afFilter ? 'No posts match this filter.' : 'No activity yet. Add a photo to get started!'}</p></div></div>`;
    _wireComposer(el);
    _wireFilterBar(el);
    return;
  }

  // Group items by day, then by user within each day
  // Comparisons are always standalone (never grouped into thumbnails)
  const dayMap = new Map(); // day string → Map<userKey, item[]>
  const standaloneByDay = new Map(); // day → [items that render standalone]
  for (const item of items) {
    const day = new Date(item.ts).toLocaleDateString([], { year: 'numeric', month: 'short', day: 'numeric' });
    if (item.type === 'comparison') {
      if (!standaloneByDay.has(day)) standaloneByDay.set(day, []);
      standaloneByDay.get(day).push(item);
      continue;
    }
    if (!dayMap.has(day)) dayMap.set(day, new Map());
    const userKey = item.pubKeyB64 || '_self';
    const userMap = dayMap.get(day);
    if (!userMap.has(userKey)) userMap.set(userKey, []);
    userMap.get(userKey).push(item);
  }

  // Collect all days in order
  const allDays = new Set([...dayMap.keys(), ...standaloneByDay.keys()]);
  const sortedDays = [...allDays].sort((a, b) => new Date(b).getTime() - new Date(a).getTime());

  let cardsHtml = '';
  for (const day of sortedDays) {
    const userMap = dayMap.get(day);
    const standalone = standaloneByDay.get(day) || [];
    const hasGrouped = userMap && [...userMap.values()].reduce((s, arr) => s + arr.length, 0) > 0;

    if (!hasGrouped && standalone.length === 1) {
      cardsHtml += `<div style="font-family:var(--mono);font-size:10px;color:var(--muted2);text-transform:uppercase;letter-spacing:.06em;padding:4px 0;margin-top:6px">${esc(day)}</div>`;
      cardsHtml += _renderFeedCard(standalone[0]);
      continue;
    }

    const totalItems = (hasGrouped ? [...userMap.values()].reduce((s, arr) => s + arr.length, 0) : 0) + standalone.length;
    if (totalItems === 1 && hasGrouped) {
      const singleItem = [...userMap.values()][0][0];
      cardsHtml += `<div style="font-family:var(--mono);font-size:10px;color:var(--muted2);text-transform:uppercase;letter-spacing:.06em;padding:4px 0;margin-top:6px">${esc(day)}</div>`;
      cardsHtml += _renderFeedCard(singleItem);
    } else {
      cardsHtml += `<div style="font-family:var(--mono);font-size:10px;color:var(--muted2);text-transform:uppercase;letter-spacing:.06em;padding:4px 0;margin-top:6px">${esc(day)}</div>`;
      // Render grouped photo items
      if (userMap) {
        for (const [userKey, userItems] of userMap) {
          if (userItems.length === 1) {
            cardsHtml += _renderFeedCard(userItems[0]);
          } else {
            cardsHtml += _renderUserDayGroup(userItems, day);
          }
        }
      }
      // Render standalone comparisons as full cards
      for (const si of standalone) {
        cardsHtml += _renderFeedCard(si);
      }
    }
  }

  el.innerHTML = `<div class="af-container">${composer}${filterBar}${cardsHtml}</div>`;
  _wireComposer(el);
  _wireFilterBar(el);
}

function _renderUserDayGroup(userItems, day) {
  const first = userItems[0];
  const isSelf = !first.pubKeyB64;
  const name = isSelf
    ? (typeof getDisplayName === 'function' ? getDisplayName() || 'Me' : 'Me')
    : (first.buddyName || 'Buddy');
  const avatar = isSelf
    ? ((typeof getProfile === 'function' ? getProfile().avatar : '') || '🌱')
    : (first.buddyAvatar || '🌿');
  const count = userItems.length;

  const thumbs = userItems.map(item => {
    // Comparison items render differently in day groups
    if (item.type === 'comparison') {
      const cmp = item.comparison;
      const kA = _afResolveCmpKey(cmp, 'A');
      const sA = kA && typeof imgCache !== 'undefined' ? imgCache[kA] : null;
      return `<div style="cursor:pointer;border-radius:6px;overflow:hidden;border:1px solid var(--border2);background:var(--surface2)"
        data-action="loadComparison" data-a0="${esc(cmp.id)}">
        <div style="position:relative;aspect-ratio:1;overflow:hidden;background:var(--surface3);display:flex;align-items:center;justify-content:center">
          ${sA ? `<img src="${sA}" style="width:100%;height:100%;object-fit:cover;display:block;pointer-events:none">` : `<span style="font-size:20px">📊</span>`}
        </div>
        <div style="padding:3px 5px;font-family:var(--mono);font-size:9px;color:var(--muted2);overflow:hidden;text-overflow:ellipsis;white-space:nowrap">${esc(cmp.name || 'Comparison')}</div>
      </div>`;
    }

    const src = (typeof imgCache !== 'undefined' && imgCache[item.imgKey]) ? imgCache[item.imgKey] : null;
    let badge = '';
    if (item.type === 'gallery_own') {
      const kots = typeof getGalleryKotsForItem === 'function' ? getGalleryKotsForItem(item.galleryId, item.itemId) : [];
      const bn = typeof getGalleryBuddyNotes === 'function' ? getGalleryBuddyNotes() : {};
      const notes = bn[item.galleryId]?.[item.itemId] || [];
      if (kots.length) badge += `<span style="position:absolute;top:3px;left:3px;font-size:10px;background:rgba(0,0,0,.6);border-radius:4px;padding:1px 4px;color:#fff">✊${kots.length > 1 ? kots.length : ''}</span>`;
      if (notes.length) badge += `<span style="position:absolute;top:3px;right:3px;font-size:10px;background:rgba(0,0,0,.6);border-radius:4px;padding:1px 4px;color:#fff">💬${notes.length > 1 ? notes.length : ''}</span>`;
    } else if (item.type === 'gallery_buddy') {
      const kotSent = typeof getSentGalleryKot === 'function' && getSentGalleryKot(item.senderSlice16, item.galleryId, item.itemId);
      const noteSent = typeof getGallerySentNote === 'function' && getGallerySentNote(item.senderSlice16, item.galleryId, item.itemId);
      if (kotSent) badge += `<span style="position:absolute;top:3px;left:3px;font-size:10px;background:rgba(34,197,94,.8);border-radius:4px;padding:1px 4px;color:#fff">✊</span>`;
      if (noteSent) badge += `<span style="position:absolute;top:3px;right:3px;font-size:10px;background:rgba(37,99,235,.8);border-radius:4px;padding:1px 4px;color:#fff">💬</span>`;
    } else if (item.type === 'journal_own') {
      const kots = typeof getKotsForEntry === 'function' ? getKotsForEntry(item.entry.id) : [];
      const notes = typeof getNotesForEntry === 'function' ? getNotesForEntry(item.entry.id) : [];
      if (kots.length) badge += `<span style="position:absolute;top:3px;left:3px;font-size:10px;background:rgba(0,0,0,.6);border-radius:4px;padding:1px 4px;color:#fff">✊${kots.length > 1 ? kots.length : ''}</span>`;
      if (notes.length) badge += `<span style="position:absolute;top:3px;right:3px;font-size:10px;background:rgba(0,0,0,.6);border-radius:4px;padding:1px 4px;color:#fff">💬${notes.length > 1 ? notes.length : ''}</span>`;
    } else if (item.type === 'journal_buddy') {
      const kotSent = typeof getSentKot === 'function' && getSentKot(item.pubKeyB64, item.entry.id);
      const noteSent = typeof getSentNote === 'function' && getSentNote(item.pubKeyB64, item.entry.id);
      if (kotSent) badge += `<span style="position:absolute;top:3px;left:3px;font-size:10px;background:rgba(34,197,94,.8);border-radius:4px;padding:1px 4px;color:#fff">✊</span>`;
      if (noteSent) badge += `<span style="position:absolute;top:3px;right:3px;font-size:10px;background:rgba(37,99,235,.8);border-radius:4px;padding:1px 4px;color:#fff">💬</span>`;
    }

    let action, a0, a1;
    if (item.type === 'gallery_own') {
      action = 'activityViewOwnGallery'; a0 = item.galleryId; a1 = item.itemId;
    } else if (item.type === 'gallery_buddy') {
      action = 'activityViewBuddyGallery'; a0 = item.senderGalKey; a1 = item.itemId;
    } else if (item.type === 'journal_own') {
      action = 'activityViewOwnEntry'; a0 = item.entry.id; a1 = '';
    } else if (item.type === 'journal_buddy') {
      action = 'activityViewBuddyEntry'; a0 = item.pubKeyB64; a1 = item.entry?.id || '';
    } else {
      action = ''; a0 = ''; a1 = '';
    }

    const title = item.itemTitle || item.entry?.notes || '';

    return `<div style="cursor:pointer;border-radius:6px;overflow:hidden;border:1px solid var(--border2);background:var(--surface2)"
      ${action ? `data-action="${action}" data-a0="${esc(a0)}" data-a1="${esc(a1)}"` : ''}>
      <div style="position:relative;aspect-ratio:1;overflow:hidden;background:var(--surface3)">
        ${src
          ? `<img src="${src}" style="width:100%;height:100%;object-fit:cover;display:block;pointer-events:none">`
          : (item.imgKey ? `<canvas class="photo-canvas" data-img-key="${esc(item.imgKey)}" style="width:100%;height:100%;object-fit:cover;display:block"></canvas>` : '')}
        ${badge}
      </div>
      ${title ? `<div style="padding:3px 5px;font-family:var(--mono);font-size:9px;color:var(--muted2);overflow:hidden;text-overflow:ellipsis;white-space:nowrap">${esc(title)}</div>` : ''}
    </div>`;
  }).join('');

  return `<div class="panel" style="margin-bottom:14px">
    <div class="ph" style="padding:8px 14px">
      <span style="font-size:16px">${esc(avatar)}</span>
      <h2 style="flex:1;font-size:13px">${esc(name)}</h2>
      <span style="font-family:var(--mono);font-size:10px;color:var(--muted2)">${count} photo${count !== 1 ? 's' : ''}</span>
    </div>
    <div class="pb" style="padding:4px 10px 10px">
      <div style="display:grid;grid-template-columns:repeat(auto-fill,minmax(140px,1fr));gap:6px">
        ${thumbs}
      </div>
    </div>
  </div>`;
}

// ── New Post composer ─────────────────────────────────────────────────────────

function _renderNewPostComposer() {
  const profile = typeof getProfile === 'function' ? getProfile() : {};
  const name = typeof getDisplayName === 'function' ? (getDisplayName() || 'Me') : 'Me';
  const avatar = profile.avatar || '';
  const galleries = typeof getGalleries === 'function' ? getGalleries() : [];

  return `<div class="panel" style="margin-bottom:14px">
    <div class="ph">
      <span style="font-size:18px">${avatar ? esc(avatar) : '🌱'}</span>
      <h2 style="flex:1">${esc(name)}</h2>
      <span style="font-family:var(--mono);font-size:10px;color:var(--muted2);text-transform:uppercase;letter-spacing:.06em">New post</span>
    </div>
    <div class="pb">
      <div id="af-composer-preview" style="display:none;margin-bottom:10px;border-radius:7px;overflow:hidden;background:var(--surface3);border:1px solid var(--border)">
        <img id="af-composer-img" style="width:100%;max-height:280px;object-fit:contain;display:block">
      </div>
      <button id="af-composer-pick-btn" class="btn btn-ghost" style="width:100%;padding:20px;border:2px dashed var(--border2);border-radius:7px;font-size:13px;color:var(--muted2);margin-bottom:10px;transition:border-color .15s">
        📷 Choose a photo
      </button>
      <input type="text" id="af-composer-caption" class="input" placeholder="Title..." maxlength="120" style="margin-bottom:6px">
      <textarea id="af-composer-note" class="input" placeholder="Add a note..." maxlength="500" rows="2" style="margin-bottom:8px;resize:vertical;font-family:var(--body)"></textarea>
      <div style="display:flex;gap:8px;align-items:center">
        <select id="af-composer-gallery" class="input" style="flex:1;font-size:12px">
          ${(() => { const _last = localStorage.getItem('frj_last_gallery') || ''; return galleries.map(g => `<option value="${esc(g.id)}" ${g.id === _last ? 'selected' : ''}>${esc(g.title)}</option>`).join(''); })()}
          <option value="_new">+ Create new gallery</option>
        </select>
        <button id="af-composer-post-btn" class="btn btn-primary btn-sm" disabled>Post</button>
      </div>
    </div>
  </div>`;
}

const _AF_POSITIONS = ['Front', 'Side', 'Erect Top', 'Erect Side', 'Frenulum', 'Scar', 'Forced Erect Coverage'];

function _buildSuggestionChips(galleryId, container, captionEl, pickBtn) {
  if (!container) return;
  const galleries = typeof getGalleries === 'function' ? getGalleries() : [];
  const gal = galleries.find(g => g.id === galleryId);
  const existing = new Set((gal?.items || []).map(i => (i.title || '').toLowerCase()));
  const chips = _AF_POSITIONS.map(p => {
    const done = existing.has(p.toLowerCase());
    return `<button class="btn btn-sm af-suggest-chip" style="font-size:11px;padding:3px 8px;margin:0;${done
      ? 'background:rgba(34,197,94,.15);border-color:rgba(34,197,94,.3);color:var(--green)'
      : ''}" data-title="${esc(p)}" ${done ? 'title="Already in gallery"' : ''}>${done ? '✓ ' : ''}${esc(p)}</button>`;
  }).join('');
  container.innerHTML = `<div style="display:flex;flex-wrap:wrap;gap:4px;margin-bottom:4px">${chips}</div>
    <div style="font-size:11px;color:var(--muted2);margin-bottom:4px">Tap a position to set the title and pick a photo.</div>`;
  // Wire chip clicks
  container.querySelectorAll('.af-suggest-chip').forEach(btn => {
    btn.addEventListener('click', () => {
      if (captionEl) captionEl.value = btn.dataset.title || '';
      pickBtn?.click();
    });
  });
}

function _wireComposer(root) {
  const pickBtn  = root.querySelector('#af-composer-pick-btn');
  const preview  = root.querySelector('#af-composer-preview');
  const imgEl    = root.querySelector('#af-composer-img');
  const postBtn  = root.querySelector('#af-composer-post-btn');
  const galSel   = root.querySelector('#af-composer-gallery');
  const caption  = root.querySelector('#af-composer-caption');
  if (!pickBtn || !postBtn) return;

  let _selectedFile = null;

  pickBtn.addEventListener('click', () => {
    const input = document.createElement('input');
    input.type = 'file'; input.accept = 'image/*'; input.style.display = 'none';
    document.body.appendChild(input);
    input.onchange = () => {
      document.body.removeChild(input);
      const file = input.files[0];
      if (!file) return;
      if (file.size > 10 * 1024 * 1024) { toast('Image too large (max 10 MB)', true); return; }
      _selectedFile = file;
      const url = URL.createObjectURL(file);
      imgEl.src = url;
      preview.style.display = '';
      pickBtn.textContent = '📷 Change photo';
      postBtn.disabled = false;
    };
    input.click();
  });

  postBtn.addEventListener('click', async () => {
    if (!_selectedFile) return;
    postBtn.disabled = true;
    postBtn.textContent = 'Posting...';

    let galleryId = galSel.value;

    // Create new gallery if selected
    if (galleryId === '_new') {
      const title = (caption.value.trim() || 'Gallery ' + new Date().toLocaleDateString());
      const newGal = {
        id: Date.now().toString(36) + Math.random().toString(36).slice(2, 6),
        title,
        createdAt: new Date().toISOString(),
        permissions: { tiers: [], contacts: [] },
        items: [],
      };
      const galleries = getGalleries();
      galleries.push(newGal);
      saveGalleries(galleries);
      galleryId = newGal.id;
    }

    try {
      const key = await storeBlob(_selectedFile);
      const galleries = getGalleries();
      const gi = galleries.findIndex(g => g.id === galleryId);
      if (gi < 0) { toast('Gallery not found', true); return; }
      const now = new Date().toISOString();
      const noteEl = root.querySelector('#af-composer-note');
      const item = {
        id: Date.now().toString(36) + Math.random().toString(36).slice(2, 6),
        imgKey: key,
        title: caption.value.trim().slice(0, 120),
        note: noteEl ? noteEl.value.trim().slice(0, 500) : '',
        addedAt: now,
        updatedAt: now,
      };
      galleries[gi].items.push(item);
      galleries[gi].updatedAt = now;
      saveGalleries(galleries);
      try { localStorage.setItem('frj_last_gallery', galleryId); } catch(e) {}
      // Warm imgCache
      const blob = await idbGet(key);
      if (blob && typeof imgCache !== 'undefined') {
        if (imgCache[key]) URL.revokeObjectURL(imgCache[key]);
        imgCache[key] = URL.createObjectURL(blob);
      }
      // Relay to buddies + sync to own devices
      if (typeof p2pRelayGalleryManifest === 'function') p2pRelayGalleryManifest(galleries[gi]);
      if (typeof p2pRelayGalleryPhoto === 'function') p2pRelayGalleryPhoto(galleries[gi], item.id, key);
      if (typeof syncAfterSave === 'function') syncAfterSave();
      toast('Posted!');
      renderActivityPage();
    } catch(e) {
      toast('Failed to post', true);
      console.error(e);
      postBtn.disabled = false;
      postBtn.textContent = 'Post';
    }
  });
}

// ── Filter bar ────────────────────────────────────────────────────────────────

function _renderFilterBar() {
  const contacts = typeof getContacts === 'function' ? getContacts() : [];
  const buddiesWithData = contacts.filter(c => {
    if (!c.pubKeyB64 || c.removedAt || c.forgottenAt) return false;
    const j = c.journal;
    if (j?.entries?.length) return true;
    // Also include if we have received galleries from them
    const recv = typeof getGalleryReceived === 'function' ? getGalleryReceived() : {};
    return !!recv[c.pubKeyB64.slice(0, 16)];
  });

  if (!buddiesWithData.length) return ''; // no buddies with data — no filter needed

  const profile = typeof getProfile === 'function' ? getProfile() : {};
  const myName = typeof getDisplayName === 'function' ? (getDisplayName() || 'Me') : 'Me';
  const myAvatar = profile.avatar || '🌱';
  const isActive = (key) => !_afFilter || _afFilter.has(key);

  return `<div style="display:flex;flex-wrap:wrap;gap:6px;margin-bottom:12px;align-items:center">
    <span style="font-family:var(--mono);font-size:10px;color:var(--muted2);text-transform:uppercase;letter-spacing:.06em;flex-shrink:0">Show</span>
    <button class="af-filter-chip${isActive('') ? ' active' : ''}" data-af-filter="">
      <span style="font-size:14px">${esc(myAvatar)}</span> ${esc(myName)}
    </button>
    ${buddiesWithData.map(c => {
      const name = c.journal?.displayName || c.nickname || 'Buddy';
      const avatar = c.journal?.avatar || '🌿';
      return `<button class="af-filter-chip${isActive(c.pubKeyB64) ? ' active' : ''}" data-af-filter="${esc(c.pubKeyB64)}">
        <span style="font-size:14px">${esc(avatar)}</span> ${esc(name)}
      </button>`;
    }).join('')}
    <button class="af-filter-chip${_afBookmarkOnly ? ' active' : ''}" data-af-bookmark-filter>🔖</button>
    ${(_afFilter || _afBookmarkOnly) ? `<button class="af-filter-chip" data-af-filter-clear style="color:var(--muted2);border-style:dashed">Clear</button>` : ''}
  </div>`;
}

function _wireFilterBar(root) {
  root.querySelectorAll('.af-filter-chip[data-af-filter]').forEach(btn => {
    btn.addEventListener('click', (ev) => {
      ev.stopPropagation();
      const key = btn.dataset.afFilter; // '' for self, pubKeyB64 for buddy
      if (!_afFilter) {
        // First click: create filter with only this person selected
        _afFilter = new Set([key]);
      } else if (_afFilter.has(key)) {
        // Already selected — deselect
        _afFilter.delete(key);
        if (!_afFilter.size) _afFilter = null; // empty filter = show all
      } else {
        // Not selected — add to filter
        _afFilter.add(key);
      }
      renderActivityPage();
    });
  });
  const bmBtn = root.querySelector('[data-af-bookmark-filter]');
  if (bmBtn) {
    bmBtn.addEventListener('click', (ev) => {
      ev.stopPropagation();
      _afBookmarkOnly = !_afBookmarkOnly;
      renderActivityPage();
    });
  }
  const clearBtn = root.querySelector('[data-af-filter-clear]');
  if (clearBtn) {
    clearBtn.addEventListener('click', (ev) => {
      ev.stopPropagation();
      _afFilter = null;
      _afBookmarkOnly = false;
      renderActivityPage();
    });
  }
}

function _buildFeedItems() {
  const items = [];
  const contacts = typeof getContacts === 'function' ? getContacts() : [];

  // Source 1: Own journal entries with photos
  const ownEntries = typeof getEntries === 'function' ? getEntries() : [];
  for (const e of ownEntries) {
    if (!e.id || !e.date || !e.imgFront) continue;
    if (typeof imgURL === 'function') imgURL(e.imgFront); // trigger lazy load
    items.push({
      type: 'journal_own',
      ts: _afTs(e.updatedAt) || _afTs(e.date + 'T12:00:00'),
      entry: e,
      imgKey: e.imgFront,
      pubKeyB64: null
    });
  }

  // Source 2: Buddy journal entries with photos
  for (const c of contacts) {
    if (!c.pubKeyB64 || c.removedAt || c.forgottenAt) continue;
    const journal = c.journal;
    if (!journal?.entries?.length) continue;
    const name = journal.displayName || c.nickname || 'Buddy';
    const avatar = journal.avatar || '';
    const sl16 = c.pubKeyB64.slice(0, 16);
    for (const e of journal.entries) {
      if (!e.id || !e.date || !e.hasImgFront) continue;
      const cacheKey = `c_${sl16}_${e.id}_front`;
      if (typeof imgURL === 'function') imgURL(cacheKey); // trigger lazy load
      items.push({
        type: 'journal_buddy',
        ts: _afTs(e.updatedAt) || _afTs(e.date + 'T12:00:00'),
        entry: e,
        imgKey: cacheKey,
        pubKeyB64: c.pubKeyB64,
        buddyName: name,
        buddyAvatar: avatar
      });
    }
  }

  // Source 3: Own gallery uploads
  const galleries = typeof getGalleries === 'function' ? getGalleries() : [];
  for (const g of galleries) {
    for (const item of (g.items || [])) {
      if (!item.id || !item.imgKey) continue;
      if (typeof imgURL === 'function') imgURL(item.imgKey); // trigger lazy load
      items.push({
        type: 'gallery_own',
        ts: _afTs(item.addedAt) || _afTs(item.updatedAt) || Date.now(),
        galleryId: g.id,
        galleryTitle: g.title,
        itemId: item.id,
        imgKey: item.imgKey,
        itemTitle: item.title || '',
        itemNote: item.note || '',
        pubKeyB64: null
      });
    }
  }

  // Source 4: Received gallery items
  const received = typeof getGalleryReceived === 'function' ? getGalleryReceived() : {};
  for (const [senderSlice16, gals] of Object.entries(received)) {
    for (const [galId, gal] of Object.entries(gals)) {
      const senderPub = gal.senderPubKey || '';
      const c = contacts.find(x => x.pubKeyB64 === senderPub || x.pubKeyB64?.startsWith(senderSlice16));
      const name = c?.journal?.displayName || c?.nickname || senderSlice16.slice(0, 8);
      const avatar = c?.journal?.avatar || '';
      for (const [itemId, meta] of Object.entries(gal.items || {})) {
        if (!meta.idbKey) continue;
        if (typeof imgURL === 'function') imgURL(meta.idbKey); // trigger lazy load
        items.push({
          type: 'gallery_buddy',
          ts: _afTs(meta.receivedAt) || _afTs(meta.updatedAt) || Date.now(),
          galleryId: galId,
          galleryTitle: gal.title || '',
          itemId,
          imgKey: meta.idbKey,
          itemTitle: meta.title || '',
          itemNote: meta.note || '',
          senderSlice16,
          senderGalKey: `${senderSlice16}:${galId}`,
          pubKeyB64: senderPub,
          buddyName: name,
          buddyAvatar: avatar
        });
      }
    }
  }

  // Source 5: Saved comparisons
  try {
    const comparisons = typeof getSavedComparisons === 'function' ? getSavedComparisons() : [];
    const myGalIds = new Set((typeof getGalleries === 'function' ? getGalleries() : []).map(g => g.id));
    const myEntryIds = new Set((typeof getEntries === 'function' ? getEntries() : []).map(e => e.id));
    for (const cmp of comparisons) {
      if (!cmp?.id) continue;
      // Inline involvement check — only show comparisons that reference MY photos
      let involvesMe = false;
      // I created it (no creatorSl16 = pre-existing, always mine)
      if (!cmp.creatorSl16) involvesMe = true;
      // My journal entries
      if (cmp.entryA && myEntryIds.has(cmp.entryA)) involvesMe = true;
      if (cmp.entryB && myEntryIds.has(cmp.entryB)) involvesMe = true;
      // My gallery items
      if (cmp.srcA?.startsWith('gal:') && myGalIds.has(cmp.srcA.split(':')[1])) involvesMe = true;
      if (cmp.srcB?.startsWith('gal:') && myGalIds.has(cmp.srcB.split(':')[1])) involvesMe = true;
      if (!involvesMe) continue; // skip comparisons that don't involve me at all

      let isMine = !cmp.creatorSl16;
      if (!isMine) {
        if (cmp.entryA && myEntryIds.has(cmp.entryA)) isMine = true;
        else if (cmp.entryB && myEntryIds.has(cmp.entryB)) isMine = true;
        else if (cmp.srcA?.startsWith('gal:') && myGalIds.has(cmp.srcA.split(':')[1])) isMine = true;
        else if (cmp.srcB?.startsWith('gal:') && myGalIds.has(cmp.srcB.split(':')[1])) isMine = true;
      }
      items.push({
        type: 'comparison',
        ts: _afTs(cmp.createdAt) || Date.now(),
        comparison: cmp,
        pubKeyB64: !isMine && cmp.creatorSl16
          ? (contacts.find(c => c.pubKeyB64?.startsWith(cmp.creatorSl16))?.pubKeyB64 || null)
          : null
      });
    }
  } catch(e) { console.warn('[activity] comparison feed error:', e); }

  // Source 6: Check-ins — UI disabled, backend preserved for future use

  // Sort newest first, cap at 100
  items.sort((a, b) => b.ts - a.ts);
  return items.slice(0, _ACTIVITY_MAX_ITEMS);
}

function _renderFeedCard(item) {
  try {
  switch (item.type) {
    case 'journal_own':    return _renderOwnJournalCard(item);
    case 'journal_buddy':  return _renderBuddyJournalCard(item);
    case 'gallery_own':    return _renderOwnGalleryCard(item);
    case 'gallery_buddy':  return _renderBuddyGalleryCard(item);
    case 'comparison':     return _renderComparisonCard(item);
    default: return '';
  }
  } catch(e) { console.warn('[activity] card render error:', e, item); return ''; }
}

// ── Helpers ───────────────────────────────────────────────────────────────────

// Normalize any timestamp (number ms, ISO string, date string) to epoch ms
function _afTs(v) {
  if (!v) return 0;
  if (typeof v === 'number') return v > 1e12 ? v : v * 1000; // seconds vs ms
  const d = new Date(v);
  return isNaN(d.getTime()) ? 0 : d.getTime();
}

function _activityTimeAgo(ts) {
  if (!ts) return '';
  const d = typeof ts === 'number' ? ts : new Date(ts).getTime();
  const sec = Math.floor((Date.now() - d) / 1000);
  if (sec < 60) return 'just now';
  if (sec < 3600) return `${Math.floor(sec / 60)}m ago`;
  if (sec < 86400) return `${Math.floor(sec / 3600)}h ago`;
  const days = Math.floor(sec / 86400);
  if (days === 1) return 'yesterday';
  if (days < 30) return `${days}d ago`;
  return new Date(d).toLocaleDateString([], { month: 'short', day: 'numeric' });
}


// ── Journal stats helper ──────────────────────────────────────────────────────

function _afJournalStats(e) {
  const stats = [];
  if (e.pctTugged != null) stats.push(`${Math.round(e.pctTugged * 7 / 100)}/7d tugged`);
  if (e.pctRetained != null) stats.push(`${Math.round(e.pctRetained * 7 / 100)}/7d retained`);
  if (e.avgHrsTugged != null) stats.push(`${e.avgHrsTugged}h/d tugging`);
  if (e.avgHrsRetained != null) stats.push(`${e.avgHrsRetained}h/d retaining`);
  const devs = [...(e.tuggingDevices || []), ...(e.retainingDevices || [])];

  let html = '';
  if (stats.length) {
    html += `<div style="display:flex;flex-wrap:wrap;gap:6px;margin-top:6px">
      ${stats.map(s => `<span style="font-family:var(--mono);font-size:11px;padding:3px 8px;border-radius:4px;background:var(--surface2);border:1px solid var(--border2);color:var(--muted);white-space:nowrap">${esc(s)}</span>`).join('')}
    </div>`;
  }
  if (devs.length) {
    html += `<div style="display:flex;flex-wrap:wrap;gap:4px;margin-top:4px">
      ${devs.map(d => `<span style="font-family:var(--mono);font-size:10px;padding:2px 6px;border-radius:4px;background:var(--accent2);border:1px solid rgba(37,99,235,.15);color:var(--accent);white-space:nowrap">${esc(d)}</span>`).join('')}
    </div>`;
  }
  if (e.supplements) {
    html += `<div style="font-size:11px;color:var(--muted2);margin-top:4px">💊 ${esc(e.supplements)}</div>`;
  }
  return html;
}

// ── Card photo helper ─────────────────────────────────────────────────────────

function _afPhoto(imgKey, action, a0, a1, bmKey) {
  const src = (typeof imgCache !== 'undefined' && imgCache[imgKey]) ? imgCache[imgKey] : null;
  const bm = bmKey && typeof getBookmarks === 'function' ? getBookmarks() : {};
  const bmRcvd = bmKey && typeof getBookmarksReceived === 'function' ? getBookmarksReceived() : {};
  const ownBm = !!(bmKey && bm[bmKey]);
  const rcvdBms = bmKey ? (bmRcvd[bmKey] || []) : [];
  const bmTotal = (ownBm ? 1 : 0) + rcvdBms.length;
  const bmBadge = bmTotal ? `<span style="position:absolute;top:8px;right:8px;font-size:16px;background:rgba(255,255,255,.85);border-radius:6px;padding:2px 4px;line-height:1;box-shadow:0 1px 3px rgba(0,0,0,.15)">🔖${bmTotal > 1 ? '<span style="font-size:11px;font-family:var(--mono)">' + bmTotal + '</span>' : ''}</span>` : '';
  if (src) {
    return `<div style="border-bottom:1px solid var(--border);cursor:pointer;position:relative" data-action="${action}" ${a0 ? `data-a0="${esc(a0)}"` : ''} ${a1 ? `data-a1="${esc(a1)}"` : ''}>
      <img src="${src}" alt="" loading="lazy" style="width:100%;max-height:50vh;object-fit:contain;display:block;background:var(--surface3)">
      ${bmBadge}
    </div>`;
  }
  return `<div style="border-bottom:1px solid var(--border);cursor:pointer;position:relative;display:flex;align-items:center;justify-content:center;min-height:120px;background:var(--surface3);color:var(--muted2);font-family:var(--mono);font-size:11px" data-action="${action}" ${a0 ? `data-a0="${esc(a0)}"` : ''} ${a1 ? `data-a1="${esc(a1)}"` : ''}>
    <canvas class="photo-canvas" data-img-key="${esc(imgKey)}" style="width:100%;max-height:50vh;object-fit:contain;display:block"></canvas>
    ${bmBadge}
  </div>`;
}

// ── Own journal card ──────────────────────────────────────────────────────────

function _renderOwnJournalCard(item) {
  const e = item.entry;
  const profile = typeof getProfile === 'function' ? getProfile() : {};
  const name = typeof getDisplayName === 'function' ? (getDisplayName() || 'Me') : 'Me';
  const avatar = profile.avatar || '';

  const kots = typeof getKotsForEntry === 'function' ? getKotsForEntry(e.id) : [];
  const kotBadge = kots.length ? `<span style="font-size:12px;color:var(--muted)">✊ ${kots.length}</span>` : '';

  const notes = typeof getNotesForEntry === 'function' ? getNotesForEntry(e.id) : [];
  const commentsHtml = notes.length ? notes.map(n =>
    `<div class="af-comment"><span class="af-comment-author">${esc(n.fromName || 'Buddy')}</span> ${esc(n.content)}` +
    `<span class="af-comment-ts">${_activityTimeAgo(n.timestamp)}</span></div>`
  ).join('') : '';

  return `<div class="panel" style="margin-bottom:14px">
    <div class="ph" data-action="activityViewOwnEntry" data-a0="${esc(e.id)}" style="cursor:pointer">
      <span style="font-size:18px">${avatar ? esc(avatar) : '🌱'}</span>
      <h2 style="flex:1">${esc(name)}</h2>
      <span style="font-family:var(--mono);font-size:10px;color:var(--muted2);text-transform:uppercase;letter-spacing:.04em">Journal</span>
      <span style="font-family:var(--mono);font-size:10px;color:var(--muted2)">${_activityTimeAgo(item.ts)}</span>
    </div>
    ${_afPhoto(item.imgKey, 'activityViewOwnEntry', e.id, null, 'journal:' + e.id)}
    <div class="pb" style="padding:10px 14px">
      ${e.ci != null ? `<span class="ci-badge" style="margin-right:6px">CI-${e.ci}</span>` : ''}
      ${kotBadge}
      ${_afJournalStats(e)}
      ${e.notes ? `<div style="margin-top:8px;font-size:13px;color:var(--text);white-space:pre-wrap;line-height:1.5">${esc(e.notes)}</div>` : ''}
      ${commentsHtml ? `<div style="border-top:1px solid var(--border);margin-top:8px;padding-top:8px">${commentsHtml}</div>` : ''}
    </div>
  </div>`;
}

// ── Buddy journal card ────────────────────────────────────────────────────────

function _renderBuddyJournalCard(item) {
  const e = item.entry;

  const kotSent = typeof getSentKot === 'function' && getSentKot(item.pubKeyB64, e.id);
  const kotBtn = kotSent
    ? '<button class="btn btn-sm kot-btn kot-active" disabled>✊ KOT\'d</button>'
    : `<button class="btn btn-sm kot-btn" data-action="activitySendKot" data-a0="${esc(item.pubKeyB64)}" data-a1="${esc(e.id)}">✊ KOT</button>`;

  const sentNote = typeof getSentNote === 'function' ? getSentNote(item.pubKeyB64, e.id) : null;
  const commentVal = sentNote ? sentNote.content : '';
  const commentLabel = sentNote ? 'Edit' : 'Comment';

  // Received comments on this buddy's entry (from the buddy — notes they left for us)
  const rcvdNotes = typeof getNotesForEntry === 'function' ? getNotesForEntry(e.id) : [];
  const rcvdCommentsHtml = rcvdNotes.length ? rcvdNotes.map(n =>
    `<div class="af-comment"><span class="af-comment-author">${esc(n.fromName || 'Buddy')}</span> ${esc(n.content)}` +
    `<span class="af-comment-ts">${_activityTimeAgo(n.timestamp)}</span></div>`
  ).join('') : '';

  return `<div class="panel" style="margin-bottom:14px">
    <div class="ph" data-action="activityViewBuddyEntry" data-a0="${esc(item.pubKeyB64)}" data-a1="${esc(e.id)}" style="cursor:pointer">
      <span style="font-size:18px">${item.buddyAvatar ? esc(item.buddyAvatar) : '🌿'}</span>
      <h2 style="flex:1">${esc(item.buddyName)}</h2>
      <span style="font-family:var(--mono);font-size:10px;color:var(--muted2);text-transform:uppercase;letter-spacing:.04em">Journal</span>
      <span style="font-family:var(--mono);font-size:10px;color:var(--muted2)">${_activityTimeAgo(item.ts)}</span>
    </div>
    ${_afPhoto(item.imgKey, 'activityViewBuddyEntry', item.pubKeyB64, e.id, null)}
    <div class="pb" style="padding:10px 14px">
      ${e.ci != null ? `<span class="ci-badge" style="margin-right:6px">CI-${e.ci}</span>` : ''}
      ${kotBtn}
      ${_afJournalStats(e)}
      ${e.notes ? `<div style="margin-top:8px;font-size:13px;color:var(--text);white-space:pre-wrap;line-height:1.5">${esc(e.notes)}</div>` : ''}
      ${rcvdCommentsHtml ? `<div style="border-top:1px solid var(--border);margin-top:8px;padding-top:8px">${rcvdCommentsHtml}</div>` : ''}
      ${sentNote ? `<div style="border-top:1px solid var(--border);margin-top:8px;padding-top:6px;font-size:12px;color:var(--muted2)"><span style="font-weight:600;color:var(--text)">You:</span> ${esc(sentNote.content)}</div>` : ''}
      <div style="display:flex;align-items:flex-start;gap:6px;margin-top:8px">
        <textarea class="af-comment-input" data-peer="${esc(item.pubKeyB64)}" data-entry="${esc(e.id)}"
          placeholder="${sentNote ? 'Edit your comment...' : 'Leave a comment...'}" maxlength="1000" rows="1">${esc(commentVal)}</textarea>
        <button class="btn btn-sm btn-primary" data-action="activitySendComment"
          data-a0="${esc(item.pubKeyB64)}" data-a1="${esc(e.id)}">${commentLabel}</button>
      </div>
    </div>
  </div>`;
}

// ── Own gallery card ──────────────────────────────────────────────────────────

function _renderOwnGalleryCard(item) {
  const profile = typeof getProfile === 'function' ? getProfile() : {};
  const name = typeof getDisplayName === 'function' ? (getDisplayName() || 'Me') : 'Me';
  const avatar = profile.avatar || '';

  const kots = typeof getGalleryKotsForItem === 'function' ? getGalleryKotsForItem(item.galleryId, item.itemId) : [];
  const kotBadge = kots.length ? `<span style="font-size:12px;color:var(--muted)">✊ ${kots.length}</span>` : '';

  const buddyNotes = typeof getGalleryBuddyNotes === 'function' ? getGalleryBuddyNotes() : {};
  const itemNotes = buddyNotes[item.galleryId]?.[item.itemId] || [];
  const contacts = typeof getContacts === 'function' ? getContacts() : [];
  const commentsHtml = itemNotes.length ? itemNotes.map(n => {
    const c = contacts.find(x => x.pubKeyB64?.startsWith(n.senderSlice16));
    const cName = c?.journal?.displayName || c?.nickname || 'Buddy';
    return `<div class="af-comment"><span class="af-comment-author">${esc(cName)}</span> ${esc(n.text)}` +
      `<span class="af-comment-ts">${_activityTimeAgo(n.sentAt)}</span></div>`;
  }).join('') : '';

  return `<div class="panel" style="margin-bottom:14px">
    <div class="ph" data-action="activityViewOwnGallery" data-a0="${esc(item.galleryId)}" data-a1="${esc(item.itemId)}" style="cursor:pointer">
      <span style="font-size:18px">${avatar ? esc(avatar) : '🌱'}</span>
      <h2 style="flex:1">${esc(name)}</h2>
      <span style="font-family:var(--mono);font-size:10px;color:var(--muted2);text-transform:uppercase;letter-spacing:.04em">🖼 ${esc(item.galleryTitle || 'Gallery')}</span>
      <span style="font-family:var(--mono);font-size:10px;color:var(--muted2)">${_activityTimeAgo(item.ts)}</span>
    </div>
    ${_afPhoto(item.imgKey, 'activityViewOwnGallery', item.galleryId, item.itemId, 'gal:' + item.galleryId + ':' + item.itemId)}
    <div class="pb" style="padding:10px 14px">
      ${item.itemTitle ? `<div style="font-size:13px;font-weight:600;color:var(--text);margin-bottom:4px">${esc(item.itemTitle)}</div>` : ''}
      ${item.itemNote ? `<div style="font-size:13px;color:var(--text);white-space:pre-wrap;line-height:1.5">${esc(item.itemNote)}</div>` : ''}
      ${kotBadge ? `<div style="margin-top:6px">${kotBadge}</div>` : ''}
      ${commentsHtml ? `<div style="border-top:1px solid var(--border);margin-top:8px;padding-top:8px">${commentsHtml}</div>` : ''}
    </div>
  </div>`;
}

// ── Gallery modal helpers (set gallery state before opening modal) ─────────────

function _activityOpenOwnGalleryImage(galleryId, itemId) {
  _galOpenId = galleryId;
  if (typeof openGalleryImageModal === 'function') openGalleryImageModal(itemId, 'mine');
}

function _activityOpenBuddyGalleryImage(senderGalKey, itemId) {
  const [sl16, galId] = (senderGalKey || '').split(':');
  _galOpenSender = sl16;
  _galOpenGalId = galId;
  if (typeof openGalleryImageModal === 'function') openGalleryImageModal(itemId, 'received', senderGalKey);
}

// ── Buddy gallery card ────────────────────────────────────────────────────────

function _renderBuddyGalleryCard(item) {
  const kotSent = typeof getSentGalleryKot === 'function'
    && getSentGalleryKot(item.senderSlice16, item.galleryId, item.itemId);
  const kotBtn = kotSent
    ? '<button class="btn btn-sm kot-btn kot-active" disabled>✊ KOT\'d</button>'
    : `<button class="btn btn-sm kot-btn" data-action="activitySendGalleryKot"
        data-a0="${esc(item.pubKeyB64)}" data-a1="${esc(item.galleryId)}" data-a2="${esc(item.itemId)}">✊ KOT</button>`;

  const sentNote = typeof getGallerySentNote === 'function'
    ? getGallerySentNote(item.senderSlice16, item.galleryId, item.itemId) : null;
  const commentVal = sentNote ? sentNote.text : '';
  const commentLabel = sentNote ? 'Edit' : 'Comment';

  return `<div class="panel" style="margin-bottom:14px">
    <div class="ph" data-action="activityViewBuddyGallery" data-a0="${esc(item.senderGalKey)}" data-a1="${esc(item.itemId)}" style="cursor:pointer">
      <span style="font-size:18px">${item.buddyAvatar ? esc(item.buddyAvatar) : '🌿'}</span>
      <h2 style="flex:1">${esc(item.buddyName)}</h2>
      <span style="font-family:var(--mono);font-size:10px;color:var(--muted2);text-transform:uppercase;letter-spacing:.04em">🖼 ${esc(item.galleryTitle || 'Gallery')}</span>
      <span style="font-family:var(--mono);font-size:10px;color:var(--muted2)">${_activityTimeAgo(item.ts)}</span>
    </div>
    ${_afPhoto(item.imgKey, 'activityViewBuddyGallery', item.senderGalKey, item.itemId, 'recv:' + (item.senderSlice16 || '') + ':' + item.galleryId + ':' + item.itemId)}
    <div class="pb" style="padding:10px 14px">
      ${item.itemTitle ? `<div style="font-size:13px;font-weight:600;color:var(--text);margin-bottom:4px">${esc(item.itemTitle)}</div>` : ''}
      ${item.itemNote ? `<div style="font-size:13px;color:var(--text);white-space:pre-wrap;line-height:1.5;margin-bottom:4px">${esc(item.itemNote)}</div>` : ''}
      ${kotBtn}
      <div style="display:flex;align-items:flex-start;gap:6px;margin-top:8px">
        <textarea class="af-comment-input" data-sender-gal="${esc(item.senderGalKey)}" data-item="${esc(item.itemId)}" data-peer="${esc(item.pubKeyB64)}"
          placeholder="Leave a comment..." maxlength="500" rows="1">${esc(commentVal)}</textarea>
        <button class="btn btn-sm btn-primary" data-action="activitySendGalleryComment"
          data-a0="${esc(item.pubKeyB64)}" data-a1="${esc(item.galleryId)}" data-a2="${esc(item.itemId)}">
          ${commentLabel}</button>
      </div>
    </div>
  </div>`;
}

// ── Comparison card ──────────────────────────────────────────────────────────

// Self-contained photo key resolver for comparisons (no compare.js dependency)
function _afResolveCmpKey(cmp, side) {
  const src = side === 'A' ? cmp.srcA : cmp.srcB;
  const entryId = side === 'A' ? cmp.entryA : cmp.entryB;
  const storedKey = side === 'A' ? cmp.keyA : cmp.keyB;

  if (!src || src === 'entry') {
    // Own journal entry
    if (entryId) {
      const e = typeof getEntry === 'function' ? getEntry(entryId) : null;
      if (e?.imgFront) return e.imgFront;
      // Check buddy journals
      const contacts = typeof getContacts === 'function' ? getContacts() : [];
      for (const c of contacts) {
        const be = c.journal?.entries?.find(x => x.id === entryId);
        if (be?.hasImgFront) return `c_${c.pubKeyB64.slice(0,16)}_${entryId}_front`;
      }
    }
    return storedKey || null;
  }

  const parts = src.split(':');
  if (parts[0] === 'gal') {
    // Own gallery
    const gals = typeof getGalleries === 'function' ? getGalleries() : [];
    const g = gals.find(x => x.id === parts[1]);
    if (g) { const it = g.items.find(i => i.id === parts[2]); if (it?.imgKey) return it.imgKey; }
    // Received gallery
    const recv = typeof getGalleryReceived === 'function' ? getGalleryReceived() : {};
    for (const [sl16, byGal] of Object.entries(recv)) {
      const idbKey = byGal[parts[1]]?.items?.[parts[2]]?.idbKey;
      if (idbKey) return idbKey;
    }
    // Use creator's sl16
    if (cmp.creatorSl16) return `gal_${cmp.creatorSl16}_${parts[1]}_${parts[2]}`;
  }
  if (parts[0] === 'buddy') return `c_${parts[1].slice(0,16)}_${parts[2]}_front`;
  if (parts[0] === 'brecv') {
    const recv = typeof getGalleryReceived === 'function' ? getGalleryReceived() : {};
    const idbKey = recv[parts[1]]?.[parts[2]]?.items?.[parts[3]]?.idbKey;
    return idbKey || `gal_${parts[1]}_${parts[2]}_${parts[3]}`;
  }

  return storedKey || null;
}

function _renderComparisonCard(item) {
  const cmp = item.comparison;
  const isMine = !item.pubKeyB64;
  const profile = typeof getProfile === 'function' ? getProfile() : {};
  const myName = typeof getDisplayNameSync === 'function' ? (getDisplayNameSync() || 'Me') : 'Me';
  const myAvatar = profile.avatar || '🌱';

  let creatorName, creatorAvatar;
  if (isMine) {
    creatorName = myName;
    creatorAvatar = myAvatar;
  } else {
    const contacts = typeof getContacts === 'function' ? getContacts() : [];
    const c = contacts.find(x => x.pubKeyB64 === item.pubKeyB64);
    creatorName = c?.journal?.displayName || c?.nickname || 'Buddy';
    creatorAvatar = c?.journal?.avatar || '🌿';
  }

  // Resolve photos — use simple key resolution, no compare.js dependency
  const kA = _afResolveCmpKey(cmp, 'A');
  const kB = _afResolveCmpKey(cmp, 'B');
  const srcA = kA ? (typeof imgURL === 'function' ? imgURL(kA) : null) : null;
  const srcB = kB ? (typeof imgURL === 'function' ? imgURL(kB) : null) : null;
  const tA = cmp.transformA || {};
  const tB = cmp.transformB || {};
  // Convert px offsets to % so transforms scale to any container size
  // Original compare viewport is ~250px wide — use % of that as reference
  const _pxToPct = (px) => ((px || 0) / 250 * 100).toFixed(1);
  const tfA = `transform:translate(${_pxToPct(tA.ox)}%,${_pxToPct(tA.oy)}%) scale(${tA.scale||1}) rotate(${tA.rot||0}deg)`;
  const tfB = `transform:translate(${_pxToPct(tB.ox)}%,${_pxToPct(tB.oy)}%) scale(${tB.scale||1}) rotate(${tB.rot||0}deg)`;

  const bmKey = `comparison:${cmp.id}`;
  const bookmarks = typeof getBookmarks === 'function' ? getBookmarks() : {};
  const isBookmarked = !!bookmarks[bmKey];

  return `<div class="panel" style="margin-bottom:14px">
    <div class="ph" data-action="loadComparison" data-a0="${esc(cmp.id)}" style="cursor:pointer">
      <span style="font-size:18px">${esc(creatorAvatar)}</span>
      <h2 style="flex:1">${esc(creatorName)}</h2>
      <span style="font-family:var(--mono);font-size:10px;color:var(--muted2);text-transform:uppercase;letter-spacing:.04em">📊 Comparison</span>
      <span style="font-family:var(--mono);font-size:10px;color:var(--muted2)">${_activityTimeAgo(item.ts)}</span>
    </div>
    <div style="display:flex;gap:2px;cursor:pointer" data-action="loadComparison" data-a0="${esc(cmp.id)}">
      <div style="flex:1;aspect-ratio:3/4;overflow:hidden;background:var(--surface3);position:relative">
        ${srcA ? `<img src="${srcA}" style="position:absolute;inset:0;width:100%;height:100%;object-fit:contain;${tfA};transform-origin:center center;pointer-events:none">` : (kA ? `<canvas class="photo-canvas" data-img-key="${esc(kA)}" style="position:absolute;inset:0;width:100%;height:100%;display:block;${tfA};transform-origin:center center"></canvas>` : '')}
      </div>
      <div style="flex:1;aspect-ratio:3/4;overflow:hidden;background:var(--surface3);position:relative">
        ${srcB ? `<img src="${srcB}" style="position:absolute;inset:0;width:100%;height:100%;object-fit:contain;${tfB};transform-origin:center center;pointer-events:none">` : (kB ? `<canvas class="photo-canvas" data-img-key="${esc(kB)}" style="position:absolute;inset:0;width:100%;height:100%;display:block;${tfB};transform-origin:center center"></canvas>` : '')}
      </div>
    </div>
    <div class="pb" style="padding:10px 14px">
      <div style="font-size:13px;font-weight:600;margin-bottom:2px">${esc(cmp.name)}</div>
      <div style="font-family:var(--mono);font-size:10px;color:var(--muted2)">${esc(cmp.descA || '?')} vs ${esc(cmp.descB || '?')}</div>
      <div style="display:flex;gap:6px;margin-top:8px">
        <button class="btn btn-sm ${isBookmarked ? '' : 'btn-ghost'}" data-action="toggleBookmark" data-a0="${esc(bmKey)}" style="${isBookmarked ? 'background:var(--accent2);border-color:var(--accent);color:var(--accent)' : ''}">🔖</button>
        <button class="btn btn-sm btn-ghost" data-action="loadComparison" data-a0="${esc(cmp.id)}">Open</button>
      </div>
    </div>
  </div>`;
}

function _renderCheckInCard(item) {
  const { mutualPhotos, revealed, ts } = item;
  const photoGrid = mutualPhotos.map(p => {
    const src = p.imgKey && typeof imgURL === 'function' ? imgURL(p.imgKey) : null;
    return `<div style="text-align:center">
      <div style="width:100%;aspect-ratio:3/4;border-radius:6px;overflow:hidden;background:var(--surface3);border:1px solid var(--border2)${!revealed && !p.isOwn ? ';filter:blur(20px)' : ''}">
        ${src ? `<img src="${src}" style="width:100%;height:100%;object-fit:cover" data-no-context="1" data-no-drag="1">` :
          `<div style="width:100%;height:100%;display:flex;align-items:center;justify-content:center;font-size:28px">${revealed ? '📷' : '🔒'}</div>`}
      </div>
      <div style="font-family:var(--mono);font-size:10px;color:var(--muted2);margin-top:4px">${esc(p.name)}</div>
    </div>`;
  }).join('');

  return `<div class="panel" style="margin-bottom:14px">
    <div class="ph" style="display:flex;align-items:center;gap:8px;padding:10px 14px">
      <span style="font-size:16px">📋</span>
      <span style="font-weight:600;font-size:13px;color:var(--text)">Check-In</span>
      <span style="font-family:var(--mono);font-size:10px;color:var(--muted2);margin-left:auto">${_activityTimeAgo(ts)}</span>
    </div>
    <div class="pb" style="padding:10px 14px">
      ${revealed
        ? `<div style="font-size:12px;color:var(--green);margin-bottom:8px;font-weight:600">🔓 Mutual reveal — ${mutualPhotos.length} people checked in</div>`
        : `<div style="font-size:12px;color:#f59e0b;margin-bottom:8px">⏳ Waiting for trusted buddies to check in…</div>`
      }
      <div style="display:grid;grid-template-columns:repeat(${Math.min(mutualPhotos.length, 4)},1fr);gap:8px">
        ${photoGrid}
      </div>
    </div>
  </div>`;
}
