const { div, h2, p, section, button, form, a, input, img, textarea, br, span, video: videoHyperaxe, audio: audioHyperaxe, table, tr, td, th, details, summary } = require("../server/node_modules/hyperaxe");
const { template, i18n, userLink, userLinkLabel, renderSpreadButton, renderContentActions } = require('./main_views');
const opinionCategories = require('../backend/opinion_categories');

const OPINION_TYPES = new Set(['bookmark','votes','feed','image','audio','video','document','torrent']);
const OPINION_ROUTES = {
  feed:        (id) => `/feed/opinions/${encodeURIComponent(id)}`,
  bookmark:    (id) => `/bookmarks/opinions/${encodeURIComponent(id)}`,
  image:       (id) => `/images/opinions/${encodeURIComponent(id)}`,
  audio:       (id) => `/audios/opinions/${encodeURIComponent(id)}`,
  torrent:     (id) => `/torrents/opinions/${encodeURIComponent(id)}`,
  video:       (id) => `/videos/opinions/${encodeURIComponent(id)}`,
  document:    (id) => `/documents/opinions/${encodeURIComponent(id)}`,
  votes:       (id) => `/votes/opinions/${encodeURIComponent(id)}`
};
const moment = require("../server/node_modules/moment");
const { renderUrl } = require('../backend/renderUrl');
const { letterOf } = require('./polls_view');
const { getConfig } = require("../configs/config-manager.js");
const { sanitizeHtml } = require('../backend/sanitizeHtml');

const renderMediaBlob = (value, fallbackSrc = null) => {
  if (!value) return fallbackSrc ? img({ src: fallbackSrc, class: 'post-image' }) : null
  const s = String(value).trim()
  if (!s) return fallbackSrc ? img({ src: fallbackSrc, class: 'post-image' }) : null
  if (s.startsWith('&')) return img({ src: `/blob/${encodeURIComponent(s)}`, class: 'post-image' })
  const mVideo = s.match(/\[video:[^\]]*\]\(\s*(&[^)\s]+\.sha256)\s*\)/)
  if (mVideo) return videoHyperaxe({ controls: true, class: 'post-video', src: `/blob/${encodeURIComponent(mVideo[1])}` })
  const mAudio = s.match(/\[audio:[^\]]*\]\(\s*(&[^)\s]+\.sha256)\s*\)/)
  if (mAudio) return audioHyperaxe({ controls: true, class: 'post-audio', src: `/blob/${encodeURIComponent(mAudio[1])}` })
  const mImg = s.match(/!\[[^\]]*\]\(\s*(&[^)\s]+\.sha256)\s*\)/)
  if (mImg) return img({ src: `/blob/${encodeURIComponent(mImg[1])}`, class: 'post-image' })
  return fallbackSrc ? img({ src: fallbackSrc, class: 'post-image' }) : null
}

const FEED_TEXT_MIN = Number(getConfig().feed?.minLength ?? 1);
const FEED_TEXT_MAX = Number(getConfig().feed?.maxLength ?? 280);

function cleanFeedText(t) {
  return typeof t === 'string' ? t.trim() : '';
}

function isValidFeedText(t) {
  const s = cleanFeedText(t);
  return s.length >= FEED_TEXT_MIN && s.length <= FEED_TEXT_MAX;
}

function capitalize(str) {
  return typeof str === 'string' && str.length ? str[0].toUpperCase() + str.slice(1) : '';
}
function sumAmounts(list = []) {
  return list.reduce((s, x) => s + (parseFloat(x.amount || 0) || 0), 0);
}

function pickActiveParliamentTerm(terms) {
  if (!terms.length) return null;
  const now = Date.now();
  const getC = t => t.value?.content || t.content || {};
  const isActive = t => {
    const c = getC(t);
    const s = new Date(c.startAt || 0).getTime();
    const e = new Date(c.endAt || 0).getTime();
    return s && e && s <= now && now < e;
  };
  const cmp = (a, b) => {
    const ca = getC(a);
    const cb = getC(b);
    const aAn = String(ca.method || '').toUpperCase() === 'ANARCHY' ? 1 : 0;
    const bAn = String(cb.method || '').toUpperCase() === 'ANARCHY' ? 1 : 0;
    if (aAn !== bAn) return aAn - bAn;
    const aC = new Date(ca.createdAt || ca.startAt || 0).getTime();
    const bC = new Date(cb.createdAt || cb.startAt || 0).getTime();
    if (aC !== bC) return aC - bC;
    const aS = new Date(ca.startAt || 0).getTime();
    const bS = new Date(cb.startAt || 0).getTime();
    if (aS !== bS) return aS - bS;
    return String(a.id || '').localeCompare(String(b.id || ''));
  };
  const active = terms.filter(isActive);
  if (active.length) return active.sort(cmp)[0];
  return terms.sort(cmp)[0];
}

function safeMsgId(x) {
  if (typeof x === 'string') return x;
  if (x && typeof x === 'object') return x.key || x.id || x.link || '';
  return '';
}

function normalizeSpreadLink(x) {
  const s = safeMsgId(x);
  return typeof s === 'string' && s.startsWith('thread:') ? s.slice(7) : s;
}

function stripHtml(s) {
  return String(s || '')
    .replace(/<[^>]*>/g, ' ')
    .replace(/\s+/g, ' ')
    .trim();
}

function excerptPostText(content, max = 220) {
  const raw = stripHtml(content?.text || content?.description || content?.title || '');
  if (!raw) return '';
  return raw.length > max ? raw.slice(0, max - 1) + '…' : raw;
}

const decodeMaybe = (s) => {
    try { return decodeURIComponent(String(s || '')); } catch { return String(s || ''); }
};

const rewriteHashtagLinks = (html) => {
    const s = String(html || '');
    return s.replace(/href=(["'])(?:https?:\/\/[^"']+)?\/hashtag\/([^"'?#]+)([^"']*)\1/g, (m, q, tag, rest) => {
        const t = decodeMaybe(tag).replace(/^#/, '').trim().toLowerCase();
        const href = `/search?query=%23${encodeURIComponent(t)}`;
        return `href=${q}${href}${q}`;
    });
};

function renderUrlPreserveNewlines(text) {
  const s = String(text || '');
  const lines = s.split(/\r\n|\r|\n/);
  const out = [];
  for (let i = 0; i < lines.length; i++) {
    if (i) out.push(br());
    out.push(...renderUrl(lines[i]));
  }
  return out;
}

function getThreadIdFromPost(action) {
  const c = action.value?.content || action.content || {};
  const fork = safeMsgId(c.fork);
  const root = safeMsgId(c.root);
  return fork || root || safeMsgId(action);
}

function getReplyToIdFromPost(action, byId) {
  const c = action.value?.content || action.content || {};
  const root = safeMsgId(c.root);
  const branch = Array.isArray(c.branch) ? c.branch.filter(x => typeof x === 'string') : [];
  let best = '';
  let bestTs = -1;
  for (const id of branch) {
    const a = byId.get(id);
    if (a && (a.ts || 0) > bestTs) { best = id; bestTs = a.ts || 0; }
  }
  return best || root || '';
}

function buildActivityItemsWithPostThreads(deduped, allActions) {
  const byId = new Map();
  for (const a of allActions) {
    const id = safeMsgId(a);
    if (id) byId.set(id, a);
  }
  for (const a of deduped) {
    const id = safeMsgId(a);
    if (id) byId.set(id, a);
  }

  const groups = new Map();
  const out = [];

  for (const a of deduped) {
    if (a.type !== 'post') {
      out.push(a);
      continue;
    }
    const threadId = getThreadIdFromPost(a);
    if (!groups.has(threadId)) groups.set(threadId, []);
    groups.get(threadId).push(a);
  }

  for (const [threadId, posts] of groups.entries()) {
    const sortedDesc = posts.slice().sort((a, b) => (b.ts || 0) - (a.ts || 0));
    const hasReplies = sortedDesc.some(p => getThreadIdFromPost(p) !== safeMsgId(p)) || sortedDesc.length > 1;

    if (!hasReplies || sortedDesc.length === 1) {
      out.push(sortedDesc[0]);
      continue;
    }

    const latest = sortedDesc[0];
    let rootAction = byId.get(threadId);
    let rootId = threadId;

    if (!rootAction) {
      const asc = posts.slice().sort((a, b) => (a.ts || 0) - (b.ts || 0));
      rootAction = asc[0] || null;
      rootId = safeMsgId(rootAction) || threadId;
    } else {
      rootId = safeMsgId(rootAction) || threadId;
    }

    const replies = sortedDesc
      .filter(p => safeMsgId(p) !== rootId)
      .slice()
      .sort((a, b) => (a.ts || 0) - (b.ts || 0));

    out.push({
      id: `thread:${threadId}`,
      type: 'postThread',
      author: latest.author,
      ts: latest.ts,
      content: {
        threadId,
        root: rootAction
          ? {
              id: safeMsgId(rootAction),
              author: rootAction.author,
              text: excerptPostText(rootAction.value?.content || rootAction.content || {}, 240)
            }
          : null,
        replies: replies.map(p => ({
          id: safeMsgId(p),
          author: p.author,
          ts: p.ts,
          text: excerptPostText(p.value?.content || p.content || {}, 200)
        }))
      }
    });
  }

  out.sort((a, b) => (b.ts || 0) - (a.ts || 0));
  return out;
}

exports.renderActionCards = renderActionCards;
const SPREADABLE_TYPES = new Set([
  'post', 'audio', 'video', 'image', 'document', 'torrent', 'bookmark',
  'event', 'calendar', 'task', 'votes', 'vote', 'market', 'shop', 'shopProduct',
  'project', 'transfer', 'housing', 'job', 'report', 'industry', 'industryBuild', 'industryBlueprint',
  'chat', 'chatMessage', 'pad', 'padEntry', 'forum', 'map', 'poll', 'blog'
]);

function renderActionCards(actions, userId, allActions, spreadMap = new Map(), extras = {}) {
  const all = Array.isArray(allActions) ? allActions : actions;
  const byIdAll = new Map();
  for (const a0 of all) {
    const id0 = safeMsgId(a0);
    if (id0) byIdAll.set(id0, a0);
  }
  const profileById = new Map();
  for (const a0 of all) {
    if (!a0 || a0.type !== 'about') continue;
    const c0 = a0.value?.content || a0.content || {};
    const id0 = c0.about || a0.author || '';
    if (!id0) continue;
    if (a0.author && String(a0.author) !== String(id0)) continue;
    const name0 = (typeof c0.name === 'string' && c0.name.trim()) ? c0.name.trim() : id0;
    const image0 = (typeof c0.image === 'string' && c0.image.trim()) ? c0.image.trim() : '';
    const ts0 = a0.ts || 0;
    const prev = profileById.get(id0);
    if (!prev) {
      profileById.set(id0, { id: id0, name: name0, image: image0, ts: ts0 });
      continue;
    }
    const prevHasImg = !!prev.image;
    const newHasImg = !!image0;
    if (!prevHasImg && newHasImg) profileById.set(id0, { id: id0, name: name0, image: image0, ts: ts0 });
    else if (prevHasImg === newHasImg && ts0 > (prev.ts || 0)) profileById.set(id0, { id: id0, name: name0, image: image0, ts: ts0 });
  }
  const getProfile = (id) => {
    const p0 = profileById.get(id);
    return p0 ? { id: p0.id, name: p0.name, image: p0.image } : { id, name: id, image: '' };
  };
  const validActions = actions
    .filter(action => {
      const content = action.value?.content || action.content;
      if (!content || typeof content !== 'object') return false;
      if (content.type === 'tombstone') return false;
      if (content.type === 'post' && content.private === true) return false;
      if (content.type === 'task' && content.isPublic === "PRIVATE") return false;
      if (content.type === 'event' && content.isPublic === "private") return false;
      if ((content.type === 'feed' || action.type === 'feed') && !isValidFeedText(content.text)) return false;
      if (content.type === 'market') {
        if (content.stock === 0 && content.status !== 'SOLD') {
          return false;
        }
      }
      return true;
    })
    .sort((a, b) => b.ts - a.ts);

  const terms = validActions.filter(a => a.type === 'parliamentTerm');
  let chosenTerm = null;
  if (terms.length) chosenTerm = pickActiveParliamentTerm(terms);
  const deduped = chosenTerm
    ? validActions.filter(a => a.type !== 'parliamentTerm' || a === chosenTerm)
    : validActions;

  if (!deduped.length) {
    return div({ class: "no-actions" }, p(i18n.noActions));
  }

  const seenDocumentTitles = new Set();
  const items = buildActivityItemsWithPostThreads(deduped, all);

  const spreadOrdinalById = new Map();
  const spreadsByLink = new Map();

  for (const a of all) {
    if (!a || a.type !== 'spread') continue;
    const c = a.value?.content || a.content || {};
    const link = normalizeSpreadLink(c.spreadTargetId || c.vote?.link || '');
    const aId = safeMsgId(a);
    if (!link || !aId) continue;
    if (!spreadsByLink.has(link)) spreadsByLink.set(link, []);
    spreadsByLink.get(link).push(a);
  }

  for (const list of spreadsByLink.values()) {
    list.sort((a, b) => (a.ts || 0) - (b.ts || 0) || String(safeMsgId(a) || '').localeCompare(String(safeMsgId(b) || '')));
    for (let i = 0; i < list.length; i++) {
      spreadOrdinalById.set(safeMsgId(list[i]), i + 1);
    }
  }

  const shopTitleById = new Map();
  for (const a of all) {
    if (!a || a.type !== 'shop') continue;
    const c = a.value?.content || a.content || {};
    const id = a.id || a.key || '';
    if (id && c.title) {
      shopTitleById.set(id, c.title);
      if (a.rootId) shopTitleById.set(a.rootId, c.title);
    }
  }

  const cards = items.map(action => {
    const date = action.ts ? new Date(action.ts).toLocaleString() : "";
    const type = action.type || 'unknown';
    let skip = false;
    let headerText;
    if (type.startsWith('parliament')) {
      const sub = type.replace('parliament', '');
      headerText = `[PARLIAMENT · ${sub.toUpperCase()}]`;
    } else if (type.startsWith('courts')) {
      const rawSub = type.slice('courts'.length);
      const pretty = rawSub
        .replace(/^[_\s]+/, '')
        .replace(/[_\s]+/g, ' · ')
        .replace(/([a-z])([A-Z])/g, '$1 · $2');
      const finalSub = pretty || 'EVENT';
      headerText = `[COURTS · ${finalSub.toUpperCase()}]`;
    } else if (type === 'taskAssignment') {
      headerText = `[${String(i18n.typeTask || 'TASK').toUpperCase()} · ASSIGNMENT]`;
    } else if (type === 'shopProduct') {
      headerText = `[SHOP · PRODUCT]`;
    } else if (type === 'chat') {
      headerText = `[CHAT \u00b7 ${String(i18n.chatThreadsLabel).toUpperCase()}]`;
    } else if (type === 'pad') {
      headerText = `[PAD · ${String(i18n.padNew || 'NEW').toUpperCase()}]`;
    } else if (type === 'ubiClaim') {
      headerText = `[UBI · CLAIM]`;
    } else if (type === 'ubiclaimresult') {
      headerText = `[UBI · RESULT]`;
    } else {
      const typeLabel = i18n[`type${capitalize(type)}`] || type;
      headerText = `[${String(typeLabel).toUpperCase()}]`;
    }

    const content = action.value?.content || action.content || {};
    const cardBody = [];

    if (type === 'votes') {
      const { question, deadline, status, votes, totalVotes } = content;
      const commentCount =
        typeof action.commentCount === 'number'
          ? action.commentCount
          : (typeof content.commentCount === 'number' ? content.commentCount : 0);

      const votesList = votes && typeof votes === 'object'
        ? Object.entries(votes).map(([option, count]) => ({ option, count }))
        : [];

      const voteOutcome = (() => {
        const totalNum = Number(totalVotes || 0);
        const noResult = { text: i18n.voteNoQuorum || 'NO QUORUM', color: '#ffcc00' };
        if (totalNum < 2) return noResult;
        const entries = Object.entries(votes || {}).filter(([o]) => o !== 'FOLLOW_MAJORITY');
        const maxCount = entries.reduce((m, [, c]) => Math.max(m, Number(c) || 0), 0);
        const top = entries.filter(([, c]) => (Number(c) || 0) === maxCount);
        if (maxCount === 0 || top.length > 1) return noResult;
        const w = top[0][0];
        const label = i18n['vote' + w.split('_').map(x => x.charAt(0) + x.slice(1).toLowerCase()).join('')] || w;
        return { text: label, color: w === 'YES' ? '#4caf50' : w === 'NO' ? '#e53935' : '#ffcc00' };
      })();

      cardBody.push(
        div({ class: 'card-section votes' },
          div(
            { class: 'card-field' },
            span({ class: 'card-label' }, i18n.question + ':'),
            span({ class: 'card-value' }, question)
          ),
          div(
            { class: 'card-field' },
            span({ class: 'card-label' }, i18n.deadline + ':'),
            span({ class: 'card-value' }, deadline ? new Date(deadline).toLocaleString() : '')
          ),
          div(
            { class: 'card-field' },
            span({ class: 'card-label' }, i18n.voteTotalVotes + ':'),
            span({ class: 'card-value' }, totalVotes)
          ),
          div(
            { class: 'card-field' },
            span({ class: 'card-label' }, (i18n.voteResults || 'Results') + ':'),
            span({ class: 'card-value', style: `color:${voteOutcome.color};font-weight:bold;` }, voteOutcome.text)
          ),
          div(
            { class: 'card-field' },
            span({ class: 'card-label' }, i18n.voteCommentsLabel + ':'),
            span({ class: 'card-value' }, String(commentCount))
          ),
          table(
            tr(...votesList.map(({ option }) => th(i18n[option] || option))),
            tr(...votesList.map(({ count }) => td(count)))
          )
        )
      );
    }

    if (type === 'transfer') {
      const { from, to, concept, amount, deadline, status, confirmedBy } = content;
      cardBody.push(
        div({ class: 'card-section transfer' },
          div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.concept + ':'), span({ class: 'card-value' }, concept)),
          div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.amount + ':'), span({ class: 'card-value' }, amount)),
          div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.deadline + ':'), span({ class: 'card-value' }, deadline ? new Date(deadline).toLocaleString() : '')),
          div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.status + ':'), span({ class: 'card-value' }, status))
        )
      );
    }

    if (type === 'bankWallet') {
      const { address } = content;
      cardBody.push(
        div({ class: 'card-section banking-wallet' },
          div({ class: 'card-field' },
            span({ class: 'card-label' }, i18n.bankWalletConnected + ':' ),
            span({ class: 'card-value' }, address)
          )
        )
      );
    }

    if (type === 'bankClaim') {
      const { amount, epochId, allocationId, txid } = content;
      const amt = Number(amount || 0);
      cardBody.push(
        div({ class: 'card-section banking-claim' },
          div({ class: 'card-field' },
            span({ class: 'card-label' }, i18n.bankUbiReceived + ':' ),
            span({ class: 'card-value' }, `${amt.toFixed(6)} ECO`)
          ),
          epochId ? div({ class: 'card-field' },
            span({ class: 'card-label' }, i18n.bankEpochShort + ':' ),
            span({ class: 'card-value' }, epochId)
          ) : "",
          allocationId ? div({ class: 'card-field' },
            span({ class: 'card-label' }, i18n.bankAllocId + ':' ),
            span({ class: 'card-value' }, allocationId)
          ) : "",
          txid ? div({ class: 'card-field' },
            span({ class: 'card-label' }, i18n.bankTx + ':' ),
            a({ href: `https://ecoin.03c8.net/blockexplorer/search?q=${txid}`, target: '_blank' }, txid)
          ) : ""
        )
      );
    }

    if (type === 'ubiClaim') {
      const { pubId, amount, epochId, claimedAt } = content;
      const amt = Number(amount || 0);
      const inhabitantId = action.author || '';
      cardBody.push(
        div({ class: 'card-section banking-ubi' },
          div({ class: 'card-field' },
            span({ class: 'card-label' }, i18n.bankUbiInhabitant + ':'),
            span({ class: 'card-value' }, userLink(inhabitantId))
          ),
          pubId ? div({ class: 'card-field' },
            span({ class: 'card-label' }, i18n.bankUbiPub + ':'),
            span({ class: 'card-value' }, userLink(pubId))
          ) : "",
          div({ class: 'card-field' },
            span({ class: 'card-label' }, i18n.bankUbiClaimedAmount + ':'),
            span({ class: 'card-value' }, `${amt.toFixed(6)} ECO`)
          ),
          epochId ? div({ class: 'card-field' },
            span({ class: 'card-label' }, i18n.bankEpochShort + ':'),
            span({ class: 'card-value' }, epochId)
          ) : "",
          div({ class: 'card-field' },
            span({ class: 'card-label' }, i18n.status + ':'),
            span({ class: 'card-value' }, 'UNCONFIRMED')
          ),
          claimedAt ? div({ class: 'card-field' },
            span({ class: 'card-label' }, i18n.date + ':'),
            span({ class: 'card-value' }, moment(claimedAt).format('YYYY-MM-DD HH:mm:ss'))
          ) : ""
        )
      );
    }

    if (type === 'ubiclaimresult') {
      const { txid, userId: inhabitantId, amount, epochId } = content;
      const pubAuthor = action.author || action.value?.author || '';
      const amt = Number(amount || 0);
      cardBody.push(
        div({ class: 'card-section banking-ubi' },
          div({ class: 'card-field' },
            span({ class: 'card-label' }, i18n.bankUbiPub + ':'),
            span({ class: 'card-value' }, pubAuthor)
          ),
          div({ class: 'card-field' },
            span({ class: 'card-label' }, i18n.bankUbiInhabitant + ':'),
            span({ class: 'card-value' }, inhabitantId || '')
          ),
          div({ class: 'card-field' },
            span({ class: 'card-label' }, i18n.bankUbiClaimedAmount + ':'),
            span({ class: 'card-value' }, `${amt.toFixed(6)} ECO`)
          ),
          txid ? div({ class: 'card-field' },
            span({ class: 'card-label' }, i18n.bankTx + ':'),
            a({ href: `https://ecoin.03c8.net/blockexplorer/search?q=${txid}`, target: '_blank' }, txid)
          ) : ""
        )
      );
    }

    if (type === 'pixelia') {
      const { author } = content;
      cardBody.push(
        div({ class: 'card-section pixelia' },
          div({ class: 'card-field' },
            a({ href: `/author/${encodeURIComponent(author)}`, class: 'activityVotePost' }, author)
          )
        )
      );
    }

    if (type === 'larpHousePost') {
      const { text, house } = content;
      cardBody.push(
        div({ class: 'card-section post' },
          house ? a({ href: `/larp/${encodeURIComponent(house)}`, class: 'larp-house-link' },
            img({ src: `/assets/larp/images/${encodeURIComponent(String(house).toLowerCase())}.jpg`, alt: house, class: 'larp-house-mini' }),
            span(`L.A.R.P · ${String(house).toUpperCase()}`)
          ) : null,
          text ? p({ class: 'post-text post-text-pre' }, ...renderUrlPreserveNewlines(String(text))) : null
        )
      );
    }

    if (type === 'tribe') {
      const { title, image, description, isAnonymous } = content;
      if (isAnonymous === true) { skip = true; }
      cardBody.push(
        div({ class: 'card-section tribe' },
          h2({ class: 'tribe-title' },
            a({ href: `/tribe/${encodeURIComponent(action.id)}`, class: "user-link" }, title)
          ),
          image
            ? (/^(\/|https?:)/.test(String(image))
                ? img({ src: image, class: 'feed-image tribe-image' })
                : renderMediaBlob(image, null))
            : null,
          p({ class: 'tribe-description' }, ...renderUrl(description || ''))
        )
      );
    }
    if (type === 'curriculum') {
      const { author, name, description, photo, personalSkills, oasisSkills, educationalSkills, languages, professionalSkills, status, preferences, createdAt, updatedAt} = content;
      cardBody.push(
        div({ class: 'card-section curriculum' },
          h2(userLink(author, name)),
          div(
            { class: 'card-fields-container' },
            createdAt ?
              div(
                { class: 'card-field' },
                span({ class: 'card-label' }, i18n.cvCreatedAt + ':'),
                span({ class: 'card-value' }, moment(createdAt).format('YYYY-MM-DD HH:mm:ss'))
              )
              : "",
            updatedAt ?
              div(
                { class: 'card-field' },
                span({ class: 'card-label' }, i18n.cvUpdatedAt + ':'),
                span({ class: 'card-value' }, moment(updatedAt).format('YYYY-MM-DD HH:mm:ss'))
              )
              : ""
          ),
          status ? div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.cvStatusLabel + ':'), span({ class: 'card-value' }, status)) : "",
          preferences ? div({ class: 'card-field' }, span({ class: 'card-label' }, (i18n.cvPreferencesLabel || 'Preferences') + ':'), span({ class: 'card-value' }, preferences)) : "",
          languages ? div({ class: 'card-field' }, span({ class: 'card-label' }, (i18n.cvLanguagesLabel || 'Languages') + ':'), span({ class: 'card-value' }, languages.toUpperCase())) : "",
          photo ?
            [
              br(),
              img({ class: "cv-photo", src: `/blob/${encodeURIComponent(photo)}` }),
              br()
            ]
            : "",
          p(...renderUrl(description || "")),
          personalSkills && personalSkills.length
            ? div({ class: 'card-tags' }, personalSkills.map(skill =>
                a({ href: `/search?query=%23${encodeURIComponent(skill)}`, class: "tag-link" }, `#${skill}`)
              )) : "",
          oasisSkills && oasisSkills.length
            ? div({ class: 'card-tags' }, oasisSkills.map(skill =>
                a({ href: `/search?query=%23${encodeURIComponent(skill)}`, class: "tag-link" }, `#${skill}`)
              )) : "",
          educationalSkills && educationalSkills.length
            ? div({ class: 'card-tags' }, educationalSkills.map(skill =>
                a({ href: `/search?query=%23${encodeURIComponent(skill)}`, class: "tag-link" }, `#${skill}`)
              )) : "",
          professionalSkills && professionalSkills.length
            ? div({ class: 'card-tags' }, professionalSkills.map(skill =>
                a({ href: `/search?query=%23${encodeURIComponent(skill)}`, class: "tag-link" }, `#${skill}`)
              )) : ""
        )
      );
    }

    if (type === 'image') {
      const { url } = content;
      cardBody.push(
        div({ class: 'card-section image' },
          img({ src: `/blob/${encodeURIComponent(url)}`, class: 'post-image' })
        )
      );
    }

    if (type === 'map') {
      const { lat, lng, mapType, title } = content;
      const mapKey = action.id || action.key || '';
      cardBody.push(
        div({ class: 'card-section map' },
          title ? div({ class: 'card-field' }, span({ class: 'card-label' }, (i18n.mapTitle || 'Map') + ':'), span({ class: 'card-value' }, mapKey ? a({ href: `/maps/${encodeURIComponent(mapKey)}`, class: 'user-link' }, title) : title)) : "",
          div({ class: 'map-card-info' },
            span({ class: 'map-type-badge' }, mapType || 'SINGLE'),
            span({ class: 'map-coords' }, `${(parseFloat(lat) || 0).toFixed(4)}, ${(parseFloat(lng) || 0).toFixed(4)}`)
          ),
          content.description ? p({ class: 'map-description' }, content.description) : ""
        )
      );
    }

    if (type === 'mapMarker') {
      const { lat, lng, label, mapId } = content;
      cardBody.push(
        div({ class: 'card-section map' },
          span({ class: 'map-marker-dot' }, "●"),
          span(label || i18n.mapMarkerDefault || 'Marker'),
          span({ class: 'map-marker-coords' }, ` (${(parseFloat(lat) || 0).toFixed(2)}, ${(parseFloat(lng) || 0).toFixed(2)})`)
        )
      );
    }

    if (type === 'audio') {
      const { url, mimeType, title } = content;
      cardBody.push(
        div({ class: 'card-section audio' },
          title?.trim() ? h2({ class: 'audio-title' }, title) : "",
          url
            ? div({ class: "audio-container" },
                audioHyperaxe({
                  controls: true,
                  src: `/blob/${encodeURIComponent(url)}`,
                  type: mimeType
                })
              )
            : p(i18n.audioNoFile)
        )
      );
    }

    if (type === 'video') {
      const { url, mimeType, title } = content;
      cardBody.push(
        div({ class: 'card-section video' },
          title?.trim() ? h2({ class: 'video-title' }, title) : "",
          url
            ? div({ class: "video-container" },
                videoHyperaxe({
                  controls: true,
                  src: `/blob/${encodeURIComponent(url)}`,
                  type: mimeType,
                  preload: 'metadata',
                  width: '640',
                  height: '360'
                })
              )
            : p(i18n.videoNoFile)
        )
      );
    }

    if (type === 'torrent') {
      const { title } = content;
      cardBody.push(
        div({ class: 'card-section' },
          title ? div({ class: 'card-field' },
            span({ class: 'card-label' }, (i18n.torrentTitleLabel || 'Title') + ':'),
            span({ class: 'card-value' }, title)
          ) : null
        )
      );
    }

    if (type === 'document') {
      const { url, title, key } = content;
      const docKey = title ? `${action.author || ''}::${title.trim()}` : null;
      if (docKey && seenDocumentTitles.has(docKey)) {
        return null;
      }
      if (docKey) seenDocumentTitles.add(docKey);
      cardBody.push(
        div({ class: 'card-section document' },
          title?.trim() ? h2({ class: 'document-title' }, title) : "",
          div({
            id: `pdf-container-${key || url}`,
            class: 'pdf-viewer-container',
            'data-pdf-url': `/blob/${encodeURIComponent(url)}`
          })
        )
      );
    }

    if (type === 'bookmark') {
      const { url } = content;
      cardBody.push(
        div({ class: 'card-section bookmark' },
          h2(url ? p(a({ href: url, target: '_blank', class: "bookmark-url" }, url)) : "")
        )
      );
    }

    if (type === 'event') {
      const { title, description, date, location, price, attendees, isPublic } = content;
      cardBody.push(
        div({ class: 'card-section event' },
          div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.title + ':'), span({ class: 'card-value' }, title)),
          date ? div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.date + ':'), span({ class: 'card-value' }, new Date(date).toLocaleString())) : "",
          location ? div({ class: 'card-field' }, span({ class: 'card-label' }, (i18n.location || 'Location') + ':'), span({ class: 'card-value' }, location)) : "",
          typeof isPublic === 'boolean' ? div({ class: 'card-field' }, span({ class: 'card-label' }, (i18n.isPublic || 'Public') + ':'), span({ class: 'card-value' }, isPublic ? 'Yes' : 'No')) : "",
          price ? div({ class: 'card-field' }, span({ class: 'card-label' }, (i18n.price || 'Price') + ':'), span({ class: 'card-value' }, price + " ECO")) : "",
          br(),
          Array.isArray(attendees) ? h2({ class: 'card-label' }, (i18n.attendees || 'Attendees') + ': ' + attendees.length) : ""
        )
      );
    }

    if (type === 'task') {
      const { title, startTime, endTime, priority, status, author } = content;
      cardBody.push(
        div({ class: 'card-section task' },
          div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.title + ':'), span({ class: 'card-value' }, title)),
          priority ? div({ class: 'card-field' }, span({ class: 'card-label' }, (i18n.priority || 'Priority') + ':'), span({ class: 'card-value' }, priority)) : "",
          startTime ? div({ class: 'card-field' }, span({ class: 'card-label' }, (i18n.taskStartTimeLabel || 'Start') + ':'), span({ class: 'card-value' }, new Date(startTime).toLocaleString())) : "",
          endTime ? div({ class: 'card-field' }, span({ class: 'card-label' }, (i18n.taskEndTimeLabel || 'End') + ':'), span({ class: 'card-value' }, new Date(endTime).toLocaleString())) : "",
          status ? div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.status + ':'), span({ class: 'card-value' }, status)) : ""
        )
      );
    }
    
    if (type === 'taskAssignment') {
        const { title, added, removed } = content || {};
        const addList = Array.isArray(added) ? added : [];
        const remList = Array.isArray(removed) ? removed : [];
        const renderUserList = (ids) =>
            ids.map((id, i) => [i > 0 ? ', ' : '', userLink(id)]).flat();
        cardBody.push(
            div({ class: 'card-section task' },
                div({ class: 'card-field' },
                    span({ class: 'card-label' }, i18n.title + ':'),
                    span({ class: 'card-value' }, title || '')
                ),
                addList.length
                    ? div({ class: 'card-field' },
                        span({ class: 'card-label' }, (i18n.taskAssignedTo || 'Assigned to') + ':'),
                        span({ class: 'card-value' }, ...renderUserList(addList))
                    )
                    : '',
                remList.length
                    ? div({ class: 'card-field' },
                        span({ class: 'card-label' }, (i18n.taskUnassignedFrom || 'Unassigned from') + ':'),
                        span({ class: 'card-value' }, ...renderUserList(remList))
                    )
                    : ''
            )
        );
    }

    if (type === 'feed') {
      const { renderTextWithStyles } = require('../backend/renderTextWithStyles');
      const { text, refeeds } = content;
      if (!isValidFeedText(text)) return null;
      const safeText = cleanFeedText(text);
      const htmlText = safeText ? rewriteHashtagLinks(renderTextWithStyles(safeText)) : '';
      const refeedsNum = Number(refeeds || 0) || 0;
      cardBody.push(
        div({ class: 'card-section feed' },
          div({ class: 'feed-text', innerHTML: sanitizeHtml(htmlText) }),
          refeedsNum > 0
            ? h2({ class: 'card-field' },
                span({ class: 'card-label' }, i18n.tribeFeedRefeeds + ': '),
                span({ class: 'card-label' }, String(refeedsNum))
              )
            : null
        )
      );
    }

  if (type === 'post') {
      const { contentWarning, text } = content || {};
      const rawText = text || '';
      const POST_TRUNCATE_LEN = 300;
      const isTruncated = rawText.length > POST_TRUNCATE_LEN;
      const displayText = isTruncated ? rawText.slice(0, POST_TRUNCATE_LEN) + '…' : rawText;
      const isHtml = typeof displayText === 'string' && /<\/?[a-z][\s\S]*>/i.test(displayText);
      let bodyNode;
      if (isHtml) {
        const hasAnchor = /<a\b[^>]*>/i.test(displayText);
        const linkified = hasAnchor
          ? displayText
          : displayText.replace(
              /(https?:\/\/[^\s<]+)/g,
              (url) =>
                `<a href="${url}" target="_blank" rel="noopener noreferrer">${url}</a>`
            );
       bodyNode = div({ class: 'post-text post-text-clamped', innerHTML: sanitizeHtml(linkified) });
      } else {
        bodyNode = p({ class: 'post-text post-text-pre post-text-clamped' }, ...renderUrlPreserveNewlines(displayText));
      }
      const threadId = getThreadIdFromPost(action);
      const replyToId = getReplyToIdFromPost(action, byIdAll);
      const isReply = !!(threadId && threadId !== action.id);
      const ctxHref = isReply ? `/thread/${encodeURIComponent(threadId)}#${encodeURIComponent(replyToId || threadId)}` : '';
      const parent = isReply ? (byIdAll.get(replyToId) || byIdAll.get(threadId)) : null;
      const parentContent = parent ? (parent.value?.content || parent.content || {}) : {};
      const parentAuthor = parent?.author || '';
      const parentName = parent?.authorName || '';
      const parentText = parent ? excerptPostText(parentContent, 220) : '';
      cardBody.push(
        div({ class: 'card-section post' },
          isReply
            ? div(
                { class: 'reply-context' },
                span({ class: 'reply-context-label' },
                  a({ href: ctxHref, class: 'tag-link' }, i18n.inReplyTo || 'IN REPLY TO'),
                  parentAuthor ? span(' ', userLink(parentAuthor, parentName)) : ''
                ),
                parentText ? p({ class: 'post-text reply-context-text post-text-pre' }, ...renderUrlPreserveNewlines(parentText)) : ''
              )
            : '',
          contentWarning ? h2({ class: 'content-warning' }, contentWarning) : '',
          bodyNode,
          isTruncated && threadId
            ? div({ class: 'card-section-action' },
                a({ href: `/thread/${encodeURIComponent(threadId)}#${encodeURIComponent(action.id || threadId)}`, class: 'filter-btn' }, i18n.keepReading || 'Keep reading...')
              )
            : ''
        )
      );
    }

    if (type === 'postThread') {
        const c = action.content || {};
        const threadId = c.threadId;
        const href = `/thread/${encodeURIComponent(threadId)}#${encodeURIComponent(threadId)}`;
        const root = c.root;
        const replies = Array.isArray(c.replies) ? c.replies : [];
        const repliesAsc = replies.slice().sort((a, b) => (a.ts || 0) - (b.ts || 0));
        const limit = 5;
        const overflow = repliesAsc.length > limit;
        const show = repliesAsc.slice(Math.max(0, repliesAsc.length - limit));
        const lastId = repliesAsc.length ? repliesAsc[repliesAsc.length - 1].id : threadId;
        const viewMoreHref = `/thread/${encodeURIComponent(threadId)}#${encodeURIComponent(lastId)}`;
        return div({ class: 'trending-card post-thread' + (String(action.author) === String(userId) ? ' own-content' : '') },
            div({ class: 'card-header activity-card-header' },
                div({ class: 'card-chips-row' },
                    span({ class: 'pm-exposition-chip pm-exposition-whole' },
                        span({ class: 'pm-exposition-text' }, `${String(i18n.typePost || 'POST').toUpperCase()} · THREAD`)
                    )
                ),
                renderContentActions(threadId, href, {
                  author: action.author,
                  spread: spreadMap.get(threadId) || null,
                  ...favOptsFor('post', threadId, extras)
                })
            ),
            div({ class: 'card-body' },
		root && root.text
		    ? div({ class: 'card-section' },
			p({ class: 'post-text post-text-pre' }, ...renderUrlPreserveNewlines(root.text))
		    )
		    : '',
		div({ class: 'card-section' },
		show.map(r => {
		    const rDate = r.ts ? new Date(r.ts).toLocaleString() : '';
		    return div({ class: 'thread-reply-item' },
			div({ class: 'thread-reply' },
			    r.text ? p({ class: 'post-text post-text-pre' }, ...renderUrlPreserveNewlines(r.text)) : ''
			),
			div({ class: 'card-footer thread-reply-footer' },
			    span({ class: 'date-link' }, rDate),
			    userLink(r.author, action.authorNames && action.authorNames[r.author])
			)
		    );
		}),
            )
        ),
        p({ class: 'card-footer' },
            span({ class: 'date-link' }, `${action.ts ? new Date(action.ts).toLocaleString() : ''} ${i18n.performed} `),
            userLink(action.author, action.authorNames && action.authorNames[action.author])
        )
        );
    }

    if (type === 'chatThread') {
        const c = action.content || {};
        const chatRoot = c.chatRoot;
        const href = `/chats/${encodeURIComponent(chatRoot)}`;
        const chatTitle = c.title || chatRoot;
        const chatDesc = c.description || '';
        const replies = Array.isArray(c.replies) ? c.replies : [];
        const repliesAsc = replies.slice().sort((a, b) => (a.ts || 0) - (b.ts || 0));
        const limit = 6;
        const show = repliesAsc.slice(Math.max(0, repliesAsc.length - limit));
        return div({ class: 'trending-card post-thread chat-thread' + (String(action.author) === String(userId) ? ' own-content' : '') },
            div({ class: 'card-header activity-card-header' },
                div({ class: 'card-chips-row' },
                    span({ class: 'pm-exposition-chip pm-exposition-whole' },
                        span({ class: 'pm-exposition-text' }, `${String(i18n.typeChat || 'CHAT').toUpperCase()} \u00b7 ${String(i18n.chatThreadsLabel).toUpperCase()}`)
                    )
                ),
                renderContentActions(chatRoot, href, {
                  author: action.author,
                  reportTitle: chatTitle,
                  spread: spreadMap.get(chatRoot) || null,
                  ...favOptsFor('chat', chatRoot, extras)
                })
            ),
            div({ class: 'card-body' },
                div({ class: 'card-section chat' },
                    div({ class: 'card-field' }, a({ href, class: 'card-value user-link' }, chatTitle)),
                    chatDesc ? div({ class: 'card-field' }, span({ class: 'card-value' }, chatDesc)) : '',
                    div({ class: 'card-field chat-thread-meta' },
                        span({ class: 'card-label' }, `${i18n.chatParticipants}: `),
                        span({ class: 'card-value' }, String(c.members || 0)),
                        span({ class: 'card-label' }, ` · ${i18n.chatMessagesLabel}: `),
                        span({ class: 'card-value' }, String(c.messageCount || show.length))
                    )
                ),
                show.length
                    ? details({ class: 'chat-thread-details' },
                        summary({ class: 'comments-summary chat-thread-summary' }, i18n.keepReading || 'Keep reading...'),
                        div({ class: 'card-section' },
                            show.map(r => {
                                const rDate = r.ts ? new Date(r.ts).toLocaleString() : '';
                                return div({ class: 'thread-reply-item' },
                                    div({ class: 'thread-reply' },
                                        r.text ? p({ class: 'post-text thread-reply-text' }, ...renderUrlPreserveNewlines(r.text)) : ''
                                    ),
                                    div({ class: 'card-footer thread-reply-footer' },
                                        span({ class: 'date-link' }, rDate),
                                        userLink(r.author, action.authorNames && action.authorNames[r.author])
                                    )
                                );
                            })
                        )
                      )
                    : null
            ),
            p({ class: 'card-footer' },
                span({ class: 'date-link' }, `${action.ts ? new Date(action.ts).toLocaleString() : ''} ${i18n.performed} `),
                userLink(action.author, action.authorNames && action.authorNames[action.author])
            )
        );
    }

    if (type === 'forum') {
      const { root, category, title, text, key, rootTitle, rootKey } = content;
      if (!root) {
        const linkKey = key || action.id;
        const linkText = (title && String(title).trim()) ? title : '';
        cardBody.push(
          div({ class: 'card-section forum' },
            div({ class: 'card-field' },
              linkKey
                ? a({ href: `/forum/${encodeURIComponent(linkKey)}`, class: 'card-value user-link' }, linkText || linkKey)
                : span({ class: 'card-value' }, linkText)
            )
          )
        );
      } else {
        const rootId = typeof root === 'string' ? root : (root?.key || root?.id || '');
        const parentForum = byIdAll.get(rootId) || actions.find(a => a.type === 'forum' && !a.content?.root && (a.id === rootId || a.content?.key === rootId));
        const parentContent = parentForum ? (parentForum.value?.content || parentForum.content || {}) : {};
        const parentTitle = (parentContent?.title && String(parentContent.title).trim()) ? parentContent.title : ((rootTitle && String(rootTitle).trim()) ? rootTitle : '');
        const parentAuthor = parentForum?.author || '';
        const parentName = parentForum?.authorName || '';
        const hrefKey = rootKey || rootId;
        cardBody.push(
          div({ class: 'card-section forum' },
            div(
              { class: 'reply-context' },
              span({ class: 'reply-context-meta' },
                a({ href: `/forum/${encodeURIComponent(hrefKey)}`, class: 'tag-link' }, i18n.inReplyTo || 'IN REPLY TO'),
                parentAuthor ? span(' ', userLink(parentAuthor, parentName)) : ''
              ),
              parentTitle ? p({ class: 'post-text reply-context-text' }, parentTitle) : ''
            ),
            div({ class: 'card-field forum-reply-body' },
              p({ class: 'forum-reply-text' }, ...renderUrl(text))
            )
          )
        );
      }
    }

 if (type === 'spread') {
  const link = normalizeSpreadLink(content?.spreadTargetId || content?.vote?.link || '');
  const target = link ? (byIdAll.get(link) || byIdAll.get(decodeMaybe(link)) || byIdAll.get(encodeURIComponent(link))) : null;
  const tContent = target ? (target.value?.content || target.content || {}) : {};
  const spreadTitle =
    (typeof content?.spreadTitle === 'string' && content.spreadTitle.trim())
      ? content.spreadTitle.trim()
      : (typeof tContent?.title === 'string' && tContent.title.trim())
        ? tContent.title.trim()
        : (typeof tContent?.name === 'string' && tContent.name.trim())
          ? tContent.name.trim()
          : '';
  const spreadContentWarning =
    (typeof content?.spreadContentWarning === 'string' && content.spreadContentWarning.trim())
      ? content.spreadContentWarning.trim()
      : (typeof tContent?.contentWarning === 'string' && tContent.contentWarning.trim())
        ? tContent.contentWarning.trim()
        : '';
  const spreadText =
    (typeof content?.spreadText === 'string' && content.spreadText.trim())
      ? content.spreadText.trim()
      : excerptPostText(tContent, 700);
  const spreadOriginalAuthor =
    (typeof content?.spreadOriginalAuthor === 'string' && content.spreadOriginalAuthor.trim())
      ? content.spreadOriginalAuthor.trim()
      : (target?.author || '');
  const ord = spreadOrdinalById.get(safeMsgId(action)) || 0;
  const totalChron = link && spreadsByLink.has(link) ? spreadsByLink.get(link).length : 0;
  const label = (i18n.spreadChron || 'Spread') + ':';
  const value = ord && totalChron ? `${ord}/${totalChron}` : (ord ? String(ord) : '');
  const spreadExcerpt = spreadText || excerptPostText(content, 300);
  cardBody.push(
    div({ class: 'card-section vote' },
      spreadTitle ? h2({ class: 'post-title activity-spread-title' }, spreadTitle) : '',
      spreadContentWarning ? h2({ class: 'content-warning' }, spreadContentWarning) : '',
      spreadExcerpt
        ? div({ class: 'post-text activity-spread-text post-text-pre activity-spread-clamped' }, ...renderUrlPreserveNewlines(spreadExcerpt))
        : div({ class: 'post-text activity-spread-text activity-spread-missing' }, i18n.spreadContentUnavailable || 'Content not yet available (pending replication)'),
      spreadOriginalAuthor
        ? div({ class: 'card-field' },
            span({ class: 'card-label' }, (i18n.spreadBy || 'By') + ': '),
            span({ class: 'card-value' }, userLink(spreadOriginalAuthor))
          )
        : '',
      value
        ? div({ class: 'card-field' },
            span({ class: 'card-label' }, label),
            span({ class: 'card-value' }, value)
          )
        : '',
      link
        ? div({ class: 'card-section-action' },
            a({ href: `/thread/${encodeURIComponent(link)}#${encodeURIComponent(link)}`, class: 'filter-btn' }, i18n.viewDetails || 'View details')
          )
        : ''
    )
  );
}

    if (type === 'vote') {
      const { vote } = content;
      cardBody.push(
        div({ class: 'card-section vote' },
          p(
            a({ href: `/thread/${encodeURIComponent(vote.link)}#${encodeURIComponent(vote.link)}`, class: 'activityVotePost' }, vote.link)
          )
        )
      );
    }

    if (type === 'about') {
      const { about, name, image, description } = content;
      const imgId = image ? (typeof image === 'string' ? image : (image.link || '')) : '';
      cardBody.push(
        div({ class: 'card-section about' },
          imgId
            ? img({ src: `/blob/${encodeURIComponent(imgId)}`, alt: name, class: 'activity-avatar' })
            : img({ src: '/assets/images/default-avatar.png', alt: name, class: 'activity-avatar' }),
          h2(userLink(about, name)),
          description ? p({ class: 'tribe-side-description' }, ...renderUrlPreserveNewlines(String(description))) : null
        )
      );
    }

    if (type === 'pub') {
      const { address } = content || {};
      const { key } = address || {};
      const pr = getProfile(key || '');
      const src = pr.image ? `/blob/${encodeURIComponent(pr.image)}` : '/assets/images/default-avatar.png';
      cardBody.push(
        div({ class: 'card-section pub activity-pub' },
          br(),
          userLink(pr.id, pr.name),
          br(),
          img({ src, alt: pr.name || pr.id, class: 'activity-avatar' })
        )
     );
    }

    if (type === 'market') {
      const { item_type, title, price, status, deadline, stock, image, auctions_poll, seller } = content;
      const isSeller = seller && userId && seller === userId;
      cardBody.push(
        div({ class: 'card-section market' },
          div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.marketItemTitle + ':'), span({ class: 'card-value' }, title)),
          div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.marketItemType + ':'), span({ class: 'card-value' }, item_type.toUpperCase())),
          div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.marketItemStatus + ": " ), span({ class: 'card-value' }, status.toUpperCase())),
          div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.deadline + ':'), span({ class: 'card-value' }, deadline ? new Date(deadline).toLocaleString() : "")),
          div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.marketItemStock + ':'), span({ class: 'card-value' }, stock)),
          br(),
          image
            ? renderMediaBlob(image, '/assets/images/default-market.png')
            : img({ src: '/assets/images/default-market.png', alt: title, class: 'post-image' }),
          br(),
          div({ class: "market-card price" },
            p(`${i18n.marketItemPrice}: ${price} ECO`)
          ),
          item_type === 'auction' && status !== 'SOLD' && status !== 'DISCARDED' && !isSeller
            ? div({ class: "auction-info" },
                auctions_poll && auctions_poll.length > 0
                  ?
                      table({ class: 'auction-bid-table' },
                        tr(
                          th(i18n.marketAuctionBidTime),
                          th(i18n.marketAuctionUser),
                          th(i18n.marketAuctionBidAmount)
                        ),
                        ...(auctions_poll || []).map(bid => {
                          const [bidderId, bidAmount, bidTime] = bid.split(':');
                          return tr(
                            td(moment(bidTime).format('YYYY-MM-DD HH:mm:ss')),
                            td(a({ href: `/author/${encodeURIComponent(userId)}` }, userId)),
                            td(`${parseFloat(bidAmount).toFixed(6)} ECO`)
                          );
                        })
                      )
                  : p(i18n.marketNoBids),
                form({ method: "POST", action: `/market/bid/${encodeURIComponent(action.id)}` },
                  input({ type: "number", name: "bidAmount", step: "0.000001", min: "0.000001", placeholder: i18n.marketYourBid, required: true }),
                  br(),
                  button({ class: "buy-btn", type: "submit" }, i18n.marketPlaceBidButton)
                )
              ) : "",
          item_type === 'exchange' && status !== 'SOLD' && status !== 'DISCARDED' && !isSeller
            ? form({ method: "POST", action: `/market/buy/${encodeURIComponent(action.id)}` },
                button({ class: "buy-btn", type: "submit" }, i18n.marketActionsBuy)
              ) : ""
        )
      );
    }

    if (type === 'shop') {
      const { title, shortDescription, description, visibility, location } = content;
      const shopKey = action.id || action.key || '';
      const displayDesc = shortDescription || (description ? (description.length > 140 ? description.slice(0, 140) + "\u2026" : description) : "");
      cardBody.push(
        div({ class: 'card-section shop' },
          div({ class: 'card-field' }, span({ class: 'card-label' }, (i18n.shopTitle || 'Shop') + ':'), span({ class: 'card-value' }, shopKey ? a({ href: `/shops/${encodeURIComponent(shopKey)}`, class: 'user-link' }, title || shopKey) : (title || ''))),
          displayDesc ? div({ class: 'card-field' }, span({ class: 'card-label' }, (i18n.description || 'Description') + ':'), span({ class: 'card-value' }, displayDesc)) : "",
          visibility ? div({ class: 'card-field' }, span({ class: 'card-label' }, (i18n.shopVisibility || 'Visibility') + ':'), span({ class: 'card-value' }, visibility)) : "",
          location ? div({ class: 'card-field' }, span({ class: 'card-label' }, (i18n.shopLocation || 'Location') + ':'), span({ class: 'card-value' }, location)) : ""
        )
      );
    }

    if (type === 'shopProduct') {
      const { title, price, stock, shopId, image: prodImage } = content;
      const resolvedShopTitle = shopId ? (shopTitleById.get(shopId) || null) : null;
      const shopLabel = resolvedShopTitle || (shopId ? shopId.slice(0, 10) + '...' : '');
      const prodImageNode = renderMediaBlob(prodImage);
      cardBody.push(
        div({ class: 'card-section shop' },
          shopId ? div({ class: 'card-field' }, span({ class: 'card-label' }, (i18n.shopTitle || 'Shop') + ':'), span({ class: 'card-value' }, a({ href: `/shops/${encodeURIComponent(shopId)}`, class: 'user-link' }, shopLabel))) : '',
          div({ class: 'card-field' }, span({ class: 'card-label' }, (i18n.shopProductTitle || 'Product') + ':'), span({ class: 'card-value' }, title || '')),
          prodImageNode ? div({ class: 'card-field' }, prodImageNode) : '',
          div({ class: 'card-field' }, span({ class: 'card-label' }, (i18n.shopProductPrice || 'Price') + ':'), span({ class: 'card-value' }, `${Number(price || 0).toFixed(6)} ECO`)),
          div({ class: 'card-field' }, span({ class: 'card-label' }, (i18n.shopProductStock || 'Stock') + ':'), span({ class: 'card-value' }, String(stock || 0)))
        )
      );
    }

    if (type === 'industry') {
      const { name, sector, description, image, tags, membershipPolicy, status } = content;
      const facKey = action.id || action.key || '';
      const validTags = Array.isArray(tags) ? tags : [];
      const statusValue = (() => {
        const u = String(status || 'ACTIVE').toUpperCase();
        if (u === 'PAUSED') return i18n.industryStatusPaused || 'PAUSED';
        if (u === 'DISSOLVED') return i18n.industryStatusDissolved || 'DISSOLVED';
        return i18n.industryStatusActive || 'WORKING';
      })();
      const policyValue = String(i18n[`industryPolicy_${membershipPolicy}`] || membershipPolicy || '').toUpperCase();
      cardBody.push(
        div({ class: 'card-section tribe industry' },
          h2({ class: 'tribe-title' },
            a({ href: `/industry/${encodeURIComponent(facKey)}`, class: "user-link" }, name || (i18n.industryTitle || 'Industry'))
          ),
          div({ class: 'card-field-row' },
            sector ? div({ class: 'card-field' }, span({ class: 'card-label' }, String(i18n.industrySector || 'Sector').toUpperCase() + ':'), span({ class: 'card-value' }, String(sector).toUpperCase())) : "",
            status ? div({ class: 'card-field' }, span({ class: 'card-label' }, String(i18n.industryStatusLabel || 'Status').toUpperCase() + ':'), span({ class: 'card-value' }, statusValue)) : "",
            membershipPolicy ? div({ class: 'card-field' }, span({ class: 'card-label' }, String(i18n.industryMembershipPolicy || 'Membership policy').toUpperCase() + ':'), span({ class: 'card-value' }, policyValue)) : ""
          ),
          image
            ? (/^(\/|https?:)/.test(String(image))
                ? img({ src: image, class: 'feed-image tribe-image' })
                : renderMediaBlob(image, null))
            : null,
          p({ class: 'tribe-description' }, ...renderUrl(description || '')),
          validTags.length
            ? div({ class: 'card-tags' }, validTags.map(tag =>
              a({ href: `/search?query=%23${encodeURIComponent(tag)}`, class: "tag-link" }, `#${tag}`)))
            : ""
        )
      );
    }

    if (type === 'industryBuild') {
      const { title, notes, image, startDate, endDate, buildStatus } = content;
      const buildKey = action.id || action.key || '';
      const daysLeft = endDate ? Math.max(0, Math.ceil((new Date(endDate) - Date.now()) / 86400000)) : null;
      cardBody.push(
        div({ class: 'card-section tribe industry' },
          h2({ class: 'tribe-title' },
            a({ href: `/industry/build/${encodeURIComponent(buildKey)}`, class: "user-link" }, title || (i18n.industryBuild || 'Build'))
          ),
          (buildStatus || startDate || endDate) ? div({ class: 'card-field-row' },
            buildStatus ? div({ class: 'card-field' }, span({ class: 'card-label' }, String(i18n.industryStatusLabel || 'Status').toUpperCase() + ':'), span({ class: 'card-value' }, String(i18n['industryBuildStatus_' + buildStatus] || buildStatus).toUpperCase())) : "",
            startDate ? div({ class: 'card-field' }, span({ class: 'card-label' }, String(i18n.industryBuildStart || 'Start date').toUpperCase() + ':'), span({ class: 'card-value' }, moment(startDate).format('YYYY-MM-DD'))) : "",
            endDate ? div({ class: 'card-field' }, span({ class: 'card-label' }, String(i18n.industryBuildEnd || 'End date').toUpperCase() + ':'), span({ class: 'card-value' }, moment(endDate).format('YYYY-MM-DD'))) : "",
            daysLeft != null ? div({ class: 'card-field' }, span({ class: 'card-label' }, String(i18n.industryTimeLeft || 'Time left').toUpperCase() + ':'), span({ class: 'card-value' }, `${daysLeft}d`)) : ""
          ) : null,
          image
            ? (/^(\/|https?:)/.test(String(image))
                ? img({ src: image, class: 'feed-image tribe-image' })
                : renderMediaBlob(image, null))
            : null,
          notes ? p({ class: 'tribe-description' }, ...renderUrl(notes)) : ""
        )
      );
    }

    if (type === 'industryBlueprint') {
      const { name, facility, description, outKind, laborHours, image, estTotal } = content;
      cardBody.push(
        div({ class: 'card-section tribe industry' },
          h2({ class: 'tribe-title' },
            facility
              ? a({ href: `/industry/${encodeURIComponent(facility)}`, class: "user-link" }, name || (i18n.industryBlueprint || 'Blueprint'))
              : (name || (i18n.industryBlueprint || 'Blueprint'))
          ),
          div({ class: 'card-field-row' },
            outKind ? div({ class: 'card-field' }, span({ class: 'card-label' }, String(i18n.industryOutputKind || 'Product type').toUpperCase() + ':'), span({ class: 'card-value' }, String(i18n['industryKind_' + outKind] || outKind).toUpperCase())) : "",
            laborHours ? div({ class: 'card-field' }, span({ class: 'card-label' }, String(i18n.industryLaborHours || 'Labor hours').toUpperCase() + ':'), span({ class: 'card-value' }, String(laborHours))) : "",
            estTotal != null ? div({ class: 'card-field' }, span({ class: 'card-label' }, String(i18n.industryEstTotal || 'Estimated price').toUpperCase() + ':'), span({ class: 'card-value' }, `${Number(estTotal).toFixed(2)} ECO`)) : ""
          ),
          image
            ? (/^(\/|https?:)/.test(String(image))
                ? img({ src: image, class: 'feed-image tribe-image' })
                : renderMediaBlob(image, null))
            : null,
          description ? p({ class: 'tribe-description' }, ...renderUrl(description)) : ""
        )
      );
    }

    if (type === 'industryAllocation') {
      const { target, pot } = content;
      cardBody.push(
        div({ class: 'card-section industry' },
          div({ class: 'card-field' }, span({ class: 'card-label' }, (i18n.industryDistribution || 'Distribution') + ':'), span({ class: 'card-value' }, target ? a({ href: `/industry/build/${encodeURIComponent(target)}`, class: 'user-link' }, i18n.industryBuild || 'Build') : '')),
          div({ class: 'card-field' }, span({ class: 'card-label' }, (i18n.industryPot || 'Pot') + ':'), span({ class: 'card-value' }, `${Number(pot || 0).toFixed(6)} ECO`))
        )
      );
    }

    if (type === 'chat') {
      const { title, description, image, category, status } = content;
      const chatKey = action.id || action.key || '';
      const displayDesc = description ? (description.length > 140 ? description.slice(0, 140) + "\u2026" : description) : "";
      const chatImageNode = renderMediaBlob(image);
      cardBody.push(
        div({ class: 'card-section chat' },
          div({ class: 'card-field' },
            chatKey
              ? a({ href: `/chats/${encodeURIComponent(chatKey)}`, class: 'card-value user-link' }, title || chatKey)
              : span({ class: 'card-value' }, title || '')),
          displayDesc ? div({ class: 'card-field' }, span({ class: 'card-value' }, displayDesc)) : '',
          category ? div({ class: 'card-field' }, span({ class: 'card-label' }, (i18n.chatCategoryLabel || 'Category') + ':'), span({ class: 'card-value' }, category)) : ''
        )
      );
    }

    if (type === 'pad') {
      const padKey = action.id || action.key || '';
      const padTitle = content.title || action.title || '';
      cardBody.push(
        div({ class: 'card-section' },
          div({ class: 'card-field' }, padKey ? a({ href: `/pads/${encodeURIComponent(padKey)}`, class: 'card-value user-link' }, padTitle || padKey) : span({ class: 'card-value' }, padTitle || '')),
          content.deadline ? div({ class: 'card-field' }, span({ class: 'card-label' }, (i18n.padDeadlineLabel || 'Deadline') + ':'), span({ class: 'card-value' }, content.deadline)) : ''
        )
      );
    }

    if (type === 'calendar') {
      const calKey = action.id || action.key || '';
      const calTitle = content.title || action.title || '';
      cardBody.push(
        div({ class: 'card-section' },
          div({ class: 'card-field' }, calKey ? a({ href: `/calendars/${encodeURIComponent(calKey)}`, class: 'card-value user-link' }, calTitle || calKey) : span({ class: 'card-value' }, calTitle || ''))
        )
      );
    }

    if (type === 'poll') {
      const options = Array.isArray(content.options) ? content.options : [];
      const counts = (action.pollCounts && typeof action.pollCounts === 'object') ? action.pollCounts : {};
      const voters = Number(action.pollVoters) || 0;
      const pctOf = (opt) => (voters > 0 ? Math.round(((counts[opt] || 0) / voters) * 100) : 0);
      const step = (opt) => Math.round(pctOf(opt) / 5) * 5;
      cardBody.push(
        div({ class: 'card-section poll' },
          div({ class: 'card-field' },
            span({ class: 'card-value' }, content.question || '')
          ),
          options.length
            ? table({ class: 'poll-results' },
                ...options.map((opt, i) =>
                  tr(
                    td({ class: 'poll-result-option' }, `${letterOf(i)})`),
                    td({ class: 'poll-result-bar' },
                      div({ class: 'poll-bar' },
                        div({ class: `poll-bar-fill poll-bar-fill-${step(opt)} poll-hue-${i % 8}` })
                      )
                    ),
                    td({ class: 'poll-result-count' }, `${counts[opt] || 0} (${pctOf(opt)}%)`)
                  )
                )
              )
            : null,
          div({ class: 'card-field' },
            span({ class: 'card-label' }, `${i18n.pollVoters}: `),
            span({ class: 'card-value' }, String(voters))
          )
        )
      );
    }

    if (type === 'report') {
      const { title, confirmations, severity, status } = content;
      cardBody.push(
        div({ class: 'card-section report' },
          div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.title + ':'), span({ class: 'card-value' }, title)),
          status ? div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.status + ':'), span({ class: 'card-value' }, status)) : "",
          severity ? div({ class: 'card-field' }, span({ class: 'card-label' }, (i18n.severity || 'Severity') + ':'), span({ class: 'card-value' }, severity.toUpperCase())) : "",
          Array.isArray(confirmations) ? h2({ class: 'card-label' }, (i18n.transfersConfirmations) + ': ' + confirmations.length) : ""
        )
      );
    }

    if (type === 'project') {
      const {
        title, status, progress, goal, pledged,
        deadline, followers, backers, milestones,
        bounty, bountyAmount, bounty_currency,
        activity, activityActor
      } = content;

      const ratio = goal ? Math.min(100, Math.round((parseFloat(pledged || 0) / parseFloat(goal)) * 100)) : 0;
      const displayStatus = String(status || 'ACTIVE').toUpperCase();
      const followersCount = Array.isArray(followers) ? followers.length : 0;
      const backersCount = Array.isArray(backers) ? backers.length : 0;
      const backersTotal = sumAmounts(backers || []);
      const msCount = Array.isArray(milestones) ? milestones.length : 0;
      const lastMs = Array.isArray(milestones) && milestones.length ? milestones[milestones.length - 1] : null;
      const bountyVal = typeof bountyAmount !== 'undefined'
        ? bountyAmount
        : (typeof bounty === 'number' ? bounty : null);

      if (activity && activity.kind) {
        const tmpl =
          activity.kind === 'follow'
            ? (i18n.activityProjectFollow || '%OASIS% is now %ACTION% this project: %PROJECT%')
            : activity.kind === 'unfollow'
              ? (i18n.activityProjectUnfollow || '%OASIS% is now %ACTION% this project: %PROJECT%')
              : '%OASIS% performed an unknown action on %PROJECT%';

        const actionWord =
          activity.kind === 'follow'
            ? (i18n.following || 'FOLLOWING')
            : activity.kind === 'unfollow'
              ? (i18n.unfollowing || 'UNFOLLOWING')
              : 'ACTION';

        const actorId = activity.activityActor || '';
        const msgHtml = tmpl
          .replace('%OASIS%', `<a class="user-link" href="/author/${encodeURIComponent(actorId)}">${userLinkLabel(actorId)}</a>`)
          .replace('%PROJECT%', `<a class="user-link" href="/projects/${encodeURIComponent(action.tipId || action.id)}">${title || ''}</a>`)
          .replace('%ACTION%', `<strong>${actionWord}</strong>`);

        return div({ class: 'card card-rpg' },
          div({ class: 'card-header' },
            h2({ class: 'card-label' }, `[${(i18n.typeProject || 'PROJECT').toUpperCase()}]`),
            form({ method: "GET", action: `/projects/${encodeURIComponent(action.tipId || action.id)}` },
              button({ type: "submit", class: "filter-btn" }, i18n.viewDetails)
            )
          ),
          div(
            p({ innerHTML: sanitizeHtml(msgHtml) })
          ),
          p({ class: 'card-footer' },
            span({ class: 'date-link' }, `${action.ts ? new Date(action.ts).toLocaleString() : ''} ${i18n.performed} `),
            userLink(action.author, action.authorNames && action.authorNames[action.author])
          )
        );
      }

      cardBody.push(
        div({ class: 'card-section project' },
          title ? div({ class: 'card-field' },
            span({ class: 'card-label' }, i18n.title + ':'),
            span({ class: 'card-value' }, title)
          ) : "",
          typeof goal !== 'undefined' ? div({ class: 'card-field' },
            span({ class: 'card-label' }, i18n.projectGoal + ':'),
            span({ class: 'card-value' }, `${goal} ECO`)
          ) : "",
          typeof progress !== 'undefined' ? div({ class: 'card-field' },
            span({ class: 'card-label' }, i18n.projectProgress + ':'),
            span({ class: 'card-value' }, `${progress || 0}%`)
          ) : "",
          deadline ? div({ class: 'card-field' },
            span({ class: 'card-label' }, i18n.projectDeadline + ':'),
            span({ class: 'card-value' }, moment(deadline).format('YYYY/MM/DD HH:mm'))
          ) : "",
          div({ class: 'card-field' },
            span({ class: 'card-label' }, i18n.projectStatus + ':'),
            span({ class: 'card-value' }, i18n['projectStatus' + displayStatus] || displayStatus)
          ),
          div({ class: 'card-field' },
            span({ class: 'card-label' }, i18n.projectFunding + ':'),
            span({ class: 'card-value' }, `${ratio}%`)
          ),
          typeof pledged !== 'undefined' ? div({ class: 'card-field' },
            span({ class: 'card-label' }, i18n.projectPledged + ':'),
            span({ class: 'card-value' }, `${pledged || 0} ECO`)
          ) : "",
          div({ class: 'card-field' },
            span({ class: 'card-label' }, i18n.projectFollowers + ':'),
            span({ class: 'card-value' }, `${followersCount}`)
          ),
          div({ class: 'card-field' },
            span({ class: 'card-label' }, i18n.projectBackers + ':'),
            span({ class: 'card-value' }, `${backersCount} · ${backersTotal} ECO`)
          ),
          msCount ? div({ class: 'card-field' },
            span({ class: 'card-label' }, (i18n.projectMilestones || 'Milestones') + ':'),
            span({ class: 'card-value' }, `${msCount}${lastMs && lastMs.title ? ' · ' + lastMs.title : ''}`)
          ) : "",
          bountyVal != null ? div({ class: 'card-field' },
            span({ class: 'card-label' }, (i18n.projectBounty || 'Bounty') + ':'),
            span({ class: 'card-value' }, `${bountyVal} ${(bounty_currency || 'ECO').toUpperCase()}`)
          ) : ""
        )
      );
    }
      
    if (type === 'aiExchange') {
      const { ctx, lang, tags, rating } = content;
      const helpful = Number(action.helpfulVotes || 0);
      cardBody.push(
        div({ class: 'card-section ai-exchange' },
          Array.isArray(ctx) && ctx.length
            ? div({ class: 'card-field' }, span({ class: 'card-label' }, (i18n.aiSnippetsLearned || 'Snippets learned') + ':'), span({ class: 'card-value' }, String(ctx.length)))
            : null,
          lang
            ? div({ class: 'card-field' }, span({ class: 'card-label' }, (i18n.aiExchangeLang || 'Language') + ':'), span({ class: 'card-value' }, String(lang).toUpperCase()))
            : null,
          Array.isArray(tags) && tags.length
            ? div({ class: 'card-field' }, span({ class: 'card-label' }, (i18n.aiExchangeTags || 'Tags') + ':'),
                span({ class: 'card-value' }, tags.map(t => span({ class: 'ai-exchange-tag' }, '#' + t))))
            : null,
          rating > 0
            ? div({ class: 'card-field' }, span({ class: 'card-label' }, (i18n.aiExchangeRating || 'Rating') + ':'), span({ class: 'card-value' }, '★'.repeat(rating) + '☆'.repeat(Math.max(0, 5 - rating))))
            : null,
          div({ class: 'card-field' },
            span({ class: 'card-label' }, (i18n.aiExchangeHelpful || 'Helpful') + ':'),
            span({ class: 'card-value' }, String(helpful)),
            form({ method: 'POST', action: '/ai/exchange/vote', class: 'ai-exchange-vote-form' },
              input({ type: 'hidden', name: 'target', value: action.id }),
              input({ type: 'hidden', name: 'helpful', value: 'yes' }),
              input({ type: 'hidden', name: 'returnTo', value: '/activity' }),
              button({ type: 'submit', class: 'filter-btn' }, i18n.aiExchangeMarkHelpful || '+1 helpful')
            )
          )
        )
      );
    }

    if (type === 'karmaScore') {
      const { karmaScore } = content;
      cardBody.push(
        div({ class: 'card-section ai-exchange' },
          div({ class: 'card-field' },
            span({ class: 'card-label' }, i18n.bankingUserEngagementScore + ':'),
            span({ class: 'card-value' }, karmaScore)
          )
        )
      );
    }

    if (type === 'housing') {
      const { title, housing_type, property_type, price, place, status, rooms, size, capacity } = content;
      const isFree = String(housing_type || '').toLowerCase() === 'couchsurfing';
      cardBody.push(
        div({ class: 'card-section report' },
          div({ class: 'card-field' },
            span({ class: 'card-label' }, i18n.title + ':'),
            span({ class: 'card-value' }, title)
          ),
          housing_type ? div({ class: 'card-field' },
            span({ class: 'card-label' }, i18n.housingType + ':'),
            span({ class: 'card-value' }, String(housing_type).toUpperCase())
          ) : null,
          property_type ? div({ class: 'card-field' },
            span({ class: 'card-label' }, i18n.housingProperty + ':'),
            span({ class: 'card-value' }, String(property_type).toUpperCase())
          ) : null,
          div({ class: 'card-field' },
            span({ class: 'card-label' }, i18n.housingPrice + ':'),
            span({ class: 'card-value' }, isFree ? (i18n.housingFree || 'FREE') : `${Number(price || 0).toFixed(2)} ECO`)
          ),
          place ? div({ class: 'card-field' },
            span({ class: 'card-label' }, i18n.housingPlace + ':'),
            span({ class: 'card-value' }, place)
          ) : null,
          Number(rooms) > 0 ? div({ class: 'card-field' },
            span({ class: 'card-label' }, i18n.housingRooms + ':'),
            span({ class: 'card-value' }, String(rooms))
          ) : null,
          Number(size) > 0 ? div({ class: 'card-field' },
            span({ class: 'card-label' }, i18n.housingSize + ':'),
            span({ class: 'card-value' }, `${size} m²`)
          ) : null,
          Number(capacity) > 0 ? div({ class: 'card-field' },
            span({ class: 'card-label' }, i18n.housingCapacity + ':'),
            span({ class: 'card-value' }, String(capacity))
          ) : null,
          status ? div({ class: 'card-field' },
            span({ class: 'card-label' }, i18n.housingStatus + ':'),
            span({ class: 'card-value' }, String(status).toUpperCase())
          ) : null
        )
      );
    }

    if (type === 'job') {
      const { title, job_type, tasks, location, vacants, salary, status, subscribers } = content;
      cardBody.push(
        div({ class: 'card-section report' },
          div({ class: 'card-field' },
            span({ class: 'card-label' }, i18n.title + ':'),
            span({ class: 'card-value' }, title)
          ),
          salary ? div({ class: 'card-field' },
            span({ class: 'card-label' }, i18n.jobSalary + ':'),
            span({ class: 'card-value' }, salary + ' ECO')
          ) : null,
          status ? div({ class: 'card-field' },
            span({ class: 'card-label' }, i18n.jobStatus + ':'),
            span({ class: 'card-value' }, String(status).toUpperCase())
          ) : null,
          job_type ? div({ class: 'card-field' },
            span({ class: 'card-label' }, i18n.jobType + ':'),
            span({ class: 'card-value' }, String(job_type).toUpperCase())
          ) : null,
          location ? div({ class: 'card-field' },
            span({ class: 'card-label' }, i18n.jobLocation + ':'),
            span({ class: 'card-value' }, String(location).toUpperCase())
          ) : null,
          Number(vacants) > 0 ? div({ class: 'card-field' },
            span({ class: 'card-label' }, i18n.jobVacants + ':'),
            span({ class: 'card-value' }, String(vacants))
          ) : null,
          div({ class: 'card-field' },
            span({ class: 'card-label' }, i18n.jobSubscribers + ':'),
            span({ class: 'card-value' },
              Array.isArray(subscribers) && subscribers.length > 0
                ? `${subscribers.length}`
                : i18n.noSubscribers.toUpperCase()
            )
          )
        )
      );
    }

    if (type === 'parliamentCandidature') {
      const { targetType, targetId, targetTitle, method } = content;
      const link = targetType === 'tribe'
        ? a({ href: `/tribe/${encodeURIComponent(targetId)}`, class: 'user-link' }, targetTitle || targetId)
        : userLink(targetId);

      const methodUpper = String(
        i18n['parliamentMethod' + String(method || '').toUpperCase()] || method
      ).toUpperCase();

      cardBody.push(
        div({ class: 'card-section parliament' },
          div({ class: 'card-field' }, span({ class: 'card-label' }, String(i18n.parliamentGovMethod || 'METHOD').toUpperCase() + ':'), span({ class: 'card-value' }, methodUpper)),
          div({ class: 'card-field' }, span({ class: 'card-value' }, link))
        )
      );
    }

    if (type === 'parliamentTerm') {
      const { method, powerType, powerId, powerTitle, startAt, endAt } = content;
      const powerTypeNorm = String(powerType || '').toLowerCase();
      const winnerLink =
        powerTypeNorm === 'tribe'
          ? a({ href: `/tribe/${encodeURIComponent(powerId)}`, class: 'user-link' }, powerTitle || powerId)
          : powerTypeNorm === 'none' || !powerTypeNorm
            ? a({ href: `/parliament?filter=government`, class: 'user-link' }, (i18n.parliamentAnarchy || 'ANARCHY'))
            : userLink(powerId, powerTitle);

      cardBody.push(
        div({ class: 'card-section parliament' },
          startAt ? div({ class: 'card-field' }, span({ class: 'card-label' }, (i18n.parliamentElectionsStart.toUpperCase() || 'Elections start') + ':'), span({ class: 'card-value' }, new Date(startAt).toLocaleString())) : '',
          endAt ? div({ class: 'card-field' }, span({ class: 'card-label' }, (i18n.parliamentElectionsEnd.toUpperCase() || 'Elections end') + ':'), span({ class: 'card-value' }, new Date(endAt).toLocaleString())) : '',
          div({ class: 'card-field' }, span({ class: 'card-value' }, winnerLink))
        )
      );
    }

    if (type === 'parliamentProposal') {
      const { title, description, method, status, voteId, createdAt } = content;

      const methodUpper = String(
        i18n['parliamentMethod' + String(method || '').toUpperCase()] || method
      ).toUpperCase();

      cardBody.push(
        div({ class: 'card-section parliament' },
          title ? div({ class: 'card-field' }, span({ class: 'card-label' }, (i18n.parliamentProposalTitle.toUpperCase() || 'Title') + ':'), span({ class: 'card-value' }, title)) : '',
          description ? p({ class: 'card-section-text' }, description) : '',
          div({ class: 'card-field' }, span({ class: 'card-label' }, (i18n.parliamentGovMethod || 'Method') + ':'), span({ class: 'card-value' }, methodUpper)),
          createdAt ? div({ class: 'card-field' }, span({ class: 'card-label' }, (i18n.createdAt.toUpperCase() || 'Created at') + ':'), span({ class: 'card-value' }, new Date(createdAt).toLocaleString())) : '',
          voteId ? div({ class: 'card-field' }, span({ class: 'card-label' }, (i18n.parliamentOpenVote.toUpperCase() || 'Open vote') + ':'), a({ href: `/votes/${encodeURIComponent(voteId)}`, class: 'tag-link' }, i18n.viewDetails || 'View details')) : '',
          status ? div({ class: 'card-field' }, span({ class: 'card-label' }, (i18n.parliamentStatus.toUpperCase() || 'Status') + ':'), span({ class: 'card-value' }, status)) : ''
        )
      );
    }
    
    if (type === 'parliamentRevocation') {
      const { title, reasons, method, status, voteId, createdAt } = content;
      const methodUpper = String(
        i18n['parliamentMethod' + String(method || '').toUpperCase()] || method
      ).toUpperCase();

      cardBody.push(
        div({ class: 'card-section parliament' },
          title ? div({ class: 'card-field' }, span({ class: 'card-label' }, (i18n.parliamentProposalTitle.toUpperCase() || 'Title') + ':'), span({ class: 'card-value' }, title)) : '',
          reasons ? p({ class: 'card-section-text' }, reasons) : '',
          div({ class: 'card-field' }, span({ class: 'card-label' }, (i18n.parliamentGovMethod || 'Method') + ':'), span({ class: 'card-value' }, methodUpper)),
          createdAt ? div({ class: 'card-field' }, span({ class: 'card-label' }, (i18n.createdAt.toUpperCase() || 'Created at') + ':'), span({ class: 'card-value' }, new Date(createdAt).toLocaleString())) : '',
          voteId ? div({ class: 'card-field' }, span({ class: 'card-label' }, (i18n.parliamentOpenVote.toUpperCase() || 'Open vote') + ':'), a({ href: `/votes/${encodeURIComponent(voteId)}`, class: 'tag-link' }, i18n.viewDetails || 'View details')) : '',
          status ? div({ class: 'card-field' }, span({ class: 'card-label' }, (i18n.parliamentStatus.toUpperCase() || 'Status') + ':'), span({ class: 'card-value' }, status)) : ''
        )
      );
    }

    if (type === 'parliamentLaw') {
      const { question, description, method, proposer, enactedAt, votes } = content;
      const yes = Number(votes?.YES || 0);
      const total = Number(votes?.total || votes?.TOTAL || 0);

      const methodUpper = String(
        i18n['parliamentMethod' + String(method || '').toUpperCase()] || method
      ).toUpperCase();

      cardBody.push(
        div({ class: 'card-section parliament' },
          question ? div({ class: 'card-field' }, span({ class: 'card-label' }, (i18n.parliamentLawQuestion || 'Question') + ':'), span({ class: 'card-value' }, question)) : '',
          description ? p({ class: 'card-section-text' }, description) : '',
          div({ class: 'card-field' }, span({ class: 'card-label' }, (i18n.parliamentLawMethod || 'Method') + ':'), span({ class: 'card-value' }, methodUpper)),
          proposer ? div({ class: 'card-field' }, span({ class: 'card-label' }, (i18n.parliamentLawProposer || 'Proposer') + ':'), span({ class: 'card-value' }, userLink(proposer))) : '',
          enactedAt ? div({ class: 'card-field' }, span({ class: 'card-label' }, (i18n.parliamentLawEnacted || 'Enacted at') + ':'), span({ class: 'card-value' }, new Date(enactedAt).toLocaleString())) : '',
          (total || yes) ? div({ class: 'card-field' }, span({ class: 'card-label' }, (i18n.parliamentLawVotes || 'Votes') + ':'), span({ class: 'card-value' }, `${yes}/${total}`)) : ''
        )
      );
    }

    if (type.startsWith('courts')) {
      if (type === 'courtsCase') {
        const { title, method, accuser, status, answerBy, evidenceBy, decisionBy, needed, yes, total, voteId } = content;
        cardBody.push(
          div({ class: 'card-section courts' },
            title ? div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.courtsCaseTitle.toUpperCase() + ':'), span({ class: 'card-value' }, title)) : '',
            status ? div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.courtsThStatus.toUpperCase() + ':'), span({ class: 'card-value' }, status)) : '',
            method ? div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.courtsMethod.toUpperCase() + ':'), span({ class: 'card-value' }, String(i18n['courtsMethod' + String(method).toUpperCase()] || method).toUpperCase())) : '',
            answerBy ? div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.courtsThAnswerBy + ':'), span({ class: 'card-value' }, new Date(answerBy).toLocaleString())) : '',
            evidenceBy ? div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.courtsThEvidenceBy + ':'), span({ class: 'card-value' }, new Date(evidenceBy).toLocaleString())) : '',
            decisionBy ? div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.courtsThDecisionBy + ':'), span({ class: 'card-value' }, new Date(decisionBy).toLocaleString())) : '',
            accuser ? div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.courtsAccuser + ':'), span({ class: 'card-value' }, userLink(accuser))) : '',
            typeof needed !== 'undefined' ? div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.courtsVotesNeeded + ':'), span({ class: 'card-value' }, String(needed))) : '',
            (typeof yes !== 'undefined' || typeof total !== 'undefined') ? div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.courtsVotesSlashTotal + ':'), span({ class: 'card-value' }, `${Number(yes || 0)}/${Number(total || 0)}`)) : '',
            voteId ? div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.courtsOpenVote + ':'), a({ href: `/votes/${encodeURIComponent(voteId)}`, class: 'tag-link' }, i18n.viewDetails || 'View details')) : ''
          )
        );
      } else if (type === 'courtsNomination') {
        const { judgeId } = content;
        cardBody.push(
          div({ class: 'card-section courts' },
            judgeId ? div({ class: 'card-field' }, span({ class: 'card-label' }, i18n.courtsJudge + ':'), span({ class: 'card-value' }, userLink(judgeId))) : ''
          )
        );
      } else {
        skip = true;
      }
    }

    const viewHref = getViewDetailsAction(type, action);
    const isParliamentTarget =
      viewHref === '/parliament?filter=candidatures' ||
      viewHref === '/parliament?filter=government'   ||
      viewHref === '/parliament?filter=proposals'    ||
      viewHref === '/parliament?filter=revocations'  ||
      viewHref === '/parliament?filter=laws';

    const isCourtsTarget =
      viewHref === '/courts?filter=cases'    ||
      viewHref === '/courts?filter=mycases'  ||
      viewHref === '/courts?filter=actions'  ||
      viewHref === '/courts?filter=judges'   ||
      viewHref === '/courts?filter=history'  ||
      viewHref === '/courts?filter=rules'    ||
      viewHref === '/courts?filter=open';

    const parliamentFilter = isParliamentTarget ? (viewHref.split('filter=')[1] || '') : '';
    const courtsFilter     = isCourtsTarget     ? (viewHref.split('filter=')[1] || '') : '';

    if (skip) {
      return null;
    }

    const detailHref = (type !== 'aiExchange' && type !== 'bankWallet')
      ? (isParliamentTarget
          ? `/parliament?filter=${encodeURIComponent(parliamentFilter)}`
          : isCourtsTarget
            ? `/courts?filter=${encodeURIComponent(courtsFilter)}`
            : viewHref)
      : null;
    const msgId = safeMsgId(action.tipId || action.id || action.key);
    const isOwn = action.author && String(action.author) === String(userId);
    return div({ class: 'trending-card' + (isOwn ? ' own-content' : '') },
      div({ class: 'card-header activity-card-header' },
        div({ class: 'card-chips-row' },
          span({ class: 'pm-exposition-chip pm-exposition-whole' },
            span({ class: 'pm-exposition-text' }, String(type || '').toUpperCase())
          )
        ),
        renderContentActions(msgId, detailHref, {
          author: action.author,
          reportTitle: (content && (content.title || content.question || content.concept || content.name)) || '',
          spread: SPREADABLE_TYPES.has(type) ? (spreadMap.get(action.id) || null) : undefined,
          ...favOptsFor(type, msgId, extras)
        })
      ),
      ...cardBody,
      (() => {
        if (!OPINION_TYPES.has(type)) return null;
        const routeFn = OPINION_ROUTES[type];
        if (!routeFn) return null;
        const ops = (action.value?.content?.opinions) || (action.content?.opinions) || {};
        const opsTotal = Object.values(ops).reduce((s, n) => s + (Number(n) || 0), 0);
        return details({ class: 'opinions-voting-collapse' },
          summary({ class: 'opinions-summary' },
            span({ class: 'opinions-summary-icon' }, 'ꔍ'),
            span({ class: 'opinions-summary-count' }, `(${opsTotal})`)),
          div({ class: 'voting-buttons' },
            opinionCategories.map(cat =>
              form({ method: 'POST', action: `${routeFn(action.id)}/${cat}` },
                button({ class: 'vote-btn' }, `${String(i18n['vote' + cat.charAt(0).toUpperCase() + cat.slice(1)] || cat).toUpperCase()} [${ops[cat] || 0}]`)
              )
            )
          )
        );
      })(),
      (() => {
        const footerAuthorId = action.author || (content && content.proposer) || '';
        return p({ class: 'card-footer' },
          span({ class: 'date-link' }, `${date} ${i18n.performed} `),
          userLink(footerAuthorId, (action.authorNames && action.authorNames[footerAuthorId]) || getProfile(footerAuthorId).name)
        );
      })()
    );
  });

  const filteredCards = cards.filter(Boolean);
  if (!filteredCards.length) {
    return div({ class: "no-actions" }, p(i18n.noActions));
  }
  return filteredCards;
}

const FAV_KIND_BY_TYPE = {
  image: 'images', audio: 'audios', video: 'videos', document: 'documents',
  bookmark: 'bookmarks', torrent: 'torrents', event: 'events', task: 'tasks',
  report: 'reports', votes: 'votes', poll: 'polls', market: 'market',
  housing: 'housing', job: 'jobs', project: 'projects', shop: 'shops',
  chat: 'chats', chatThread: 'chats', pad: 'pads', calendar: 'calendars',
  map: 'maps', forum: 'forum', transfer: 'transfers', post: 'blogs'
};

const favOptsFor = (type, id, extras = {}) => {
  const favKind = FAV_KIND_BY_TYPE[type];
  if (!favKind) return {};
  const index = extras.favIndex instanceof Map ? extras.favIndex : null;
  return {
    favKind,
    isFavorite: index ? index.get(String(id)) === favKind : false,
    returnTo: extras.returnTo || '/activity'
  };
};

function getViewDetailsAction(type, action) {
  const id = encodeURIComponent(safeMsgId(action.tipId || action.id || action.key || action));
  switch (type) {
    case 'parliamentCandidature':   return `/parliament?filter=candidatures`;
    case 'parliamentTerm':          return `/parliament?filter=government`;
    case 'parliamentProposal':      return `/parliament?filter=proposals`;
    case 'parliamentRevocation':    return `/parliament?filter=revocations`;
    case 'parliamentLaw':           return `/parliament?filter=laws`;
    case 'courtsCase':              return `/courts/cases/${encodeURIComponent(action.id)}`;
    case 'courtsEvidence':          return `/courts?filter=actions`;
    case 'courtsAnswer':            return `/courts?filter=actions`;
    case 'courtsVerdict':           return `/courts?filter=actions`;
    case 'courtsSettlement':        return `/courts?filter=actions`;
    case 'courtsSettlementProposal':return `/courts?filter=actions`;
    case 'courtsSettlementAccepted':return `/courts?filter=actions`;
    case 'courtsNomination':        return `/courts?filter=judges`;
    case 'courtsNominationVote':    return `/courts?filter=judges`;
    case 'spread': {
      const link = normalizeSpreadLink(action.content?.spreadTargetId || action.content?.vote?.link || '');
      return link ? `/thread/${encodeURIComponent(link)}#${encodeURIComponent(link)}` : `/activity`;
    }
    case 'post': {
      const root = action.content && action.content.root;
      return root
        ? `/blogs/${encodeURIComponent(root)}#${id}`
        : `/blogs/${id}`;
    }
    case 'feed':       return `/feed/${id}`;
    case 'vote':       return `/thread/${encodeURIComponent(action.content.vote.link)}#${encodeURIComponent(action.content.vote.link)}`;
    case 'votes':      return `/votes/${id}`;
    case 'transfer':   return `/transfers/${id}`;
    case 'pixelia':    return `/pixelia`;
    case 'poll':       return `/polls/${id}`;
    case 'larpHousePost': return action.content?.house ? `/larp/${encodeURIComponent(action.content.house)}` : `/activity`;
    case 'tribe':      return `/tribe/${id}`;
    case 'curriculum': return `/inhabitant/${encodeURIComponent(action.author)}`;
    case 'karmaScore': return `/author/${encodeURIComponent(action.author)}`;
    case 'map':        return `/maps/${id}`;
    case 'mapMarker':  return `/maps/${encodeURIComponent(action.content?.mapId || id)}`;
    case 'image':      return `/images/${id}`;
    case 'audio':      return `/audios/${id}`;
    case 'video':      return `/videos/${id}`;
    case 'torrent':    return `/torrents/${id}`;
    case 'forum':      return `/forum/${encodeURIComponent(action.content?.key || action.tipId || action.id)}`;
    case 'document':   return `/documents/${id}`;
    case 'bookmark':   return `/bookmarks/${id}`;
    case 'event':      return `/events/${id}`;
    case 'task':       return `/tasks/${id}`;
    case 'taskAssignment': return `/tasks/${encodeURIComponent(action.content?.taskId || action.tipId || action.id)}`;
    case 'about':      return `/author/${encodeURIComponent(action.author)}`;
    case 'pub':        return `/invites`;
    case 'market':     return `/market/${id}`;
    case 'shop':       return `/shops/${id}`;
    case 'shopProduct': return `/shops/product/${id}`;
    case 'chat':       return `/chats/${id}`;
    case 'pad':        return `/pads/${id}`;
    case 'calendar':   return `/calendars/${id}`;
    case 'housing':    return `/housing/${id}`;
    case 'job':        return `/jobs/${id}`;
    case 'project':    return `/projects/${id}`;
    case 'industry':   return `/industry/${id}`;
    case 'industryBuild': return `/industry/build/${id}`;
    case 'industryBlueprint': return `/industry/blueprint/${id}`;
    case 'industryAllocation': return action.content?.target ? `/industry/build/${encodeURIComponent(action.content.target)}` : '/industry?filter=BUILDS';
    case 'report':     return `/reports/${id}`;
    case 'bankWallet': return `/wallet`;
    case 'bankClaim':  return `/banking${action.content?.epochId ? `/epoch/${encodeURIComponent(action.content.epochId)}` : ''}`;
    case 'ubiClaim':   return action.content?.transferId ? `/transfers/${encodeURIComponent(action.content.transferId)}` : `/transfers?filter=ubi`;
    default:           return `/activity`;
  }
}

exports.getViewDetailsAction = getViewDetailsAction;

exports.activityView = (actions, filter, userId, q = '', extras = {}) => {
  const spreadMap = (extras && extras.spreadMap) || new Map();
  const title = filter === 'mine' ? i18n.yourActivity : i18n.globalActivity;
  const desc = i18n.activityDesc;

  const activityTypes = [
    { type: 'all',       label: i18n.allButton },
    { type: 'mine',      label: i18n.mineButton },
    { type: 'recent',    label: i18n.typeRecent },
    { type: 'top',       label: i18n.typeTop },
    { type: 'inhabitants', label: i18n.typeInhabitants },
    { type: 'tribe',     label: i18n.typeTribe },
    { type: 'larp',      label: i18n.typeLarp },
    { type: 'parliament',label: i18n.typeParliament },
    { type: 'courts',    label: i18n.typeCourts },
    { type: 'votes',     label: i18n.typeVotes },
    { type: 'event',     label: i18n.typeEvent },
    { type: 'calendar',  label: i18n.typeCalendar },
    { type: 'task',      label: i18n.typeTask },
    { type: 'report',    label: i18n.typeReport },
    { type: 'post',      label: i18n.typePost },
    { type: 'feed',      label: i18n.typeFeed },
    { type: 'chat',      label: i18n.typeChat },
    { type: 'pad',       label: i18n.typePad },
    { type: 'forum',     label: i18n.typeForum },
    { type: 'map',       label: i18n.typeMap },
    { type: 'banking',   label: i18n.typeBanking },
    { type: 'market',    label: i18n.typeMarket },
    { type: 'project',   label: i18n.typeProject },
    { type: 'industry',  label: i18n.typeIndustry },
    { type: 'housing',   label: i18n.typeHousing },
    { type: 'job',       label: i18n.typeJob },
    { type: 'shop',      label: i18n.typeShop },
    { type: 'transfer',  label: i18n.typeTransfer },
    { type: 'audio',     label: i18n.typeAudio },
    { type: 'bookmark',  label: i18n.typeBookmark },
    { type: 'document',  label: i18n.typeDocument },
    { type: 'image',     label: i18n.typeImage },
    { type: 'torrent',   label: i18n.typeTorrent },
    { type: 'video',     label: i18n.typeVideo }
  ];

  const FILTER_META = new Set(['all', 'mine', 'recent']);
  const GROUP_SUBTYPES = {
    parliament: ['parliamentCandidature', 'parliamentTerm', 'parliamentProposal', 'parliamentRevocation', 'parliamentLaw'],
    courts:     ['courtsCase', 'courtsNomination', 'courtsNominationVote'],
    banking:    ['bankWallet', 'bankClaim', 'ubiClaim'],
    task:       ['task', 'taskAssignment'],
    votes:      ['votes', 'poll'],
    shop:       ['shop', 'shopProduct'],
    larp:       ['larpHousePost'],
    inhabitants:['about'],
    chat:       ['chat', 'chatThread'],
    industry:   ['industry', 'industryBuild', 'industryBlueprint', 'industryAllocation']
  };
  const ALLOWED_TYPES = new Set();
  for (const { type } of activityTypes) {
    if (FILTER_META.has(type)) continue;
    if (GROUP_SUBTYPES[type]) GROUP_SUBTYPES[type].forEach(t => ALLOWED_TYPES.add(t));
    else ALLOWED_TYPES.add(type);
  }
  actions = actions.filter(action => ALLOWED_TYPES.has(action.type));

  let filteredActions;
  if (filter === 'mine') {
    filteredActions = actions.filter(action => action.author === userId && action.type !== 'tombstone');
  } else if (filter === 'recent') {
    const now = Date.now();
    filteredActions = actions.filter(action => action.type !== 'tombstone' && action.ts && now - action.ts < 24 * 60 * 60 * 1000);
  } else if (filter === 'banking') {
    filteredActions = actions.filter(action => action.type !== 'tombstone' && (action.type === 'bankWallet' || action.type === 'bankClaim' || action.type === 'ubiClaim'));
  } else if (filter === 'tribe') {
    filteredActions = actions.filter(action => action.type === 'tribe');
  } else if (filter === 'larp') {
    filteredActions = actions.filter(action => action.type === 'larpHousePost');
  } else if (filter === 'inhabitants') {
    filteredActions = actions.filter(action => action.type === 'about');
  } else if (filter === 'parliament') {
    filteredActions = actions.filter(action => ['parliamentCandidature','parliamentTerm','parliamentProposal','parliamentRevocation','parliamentLaw'].includes(action.type));
  } else if (filter === 'courts') {
    filteredActions = actions.filter(action => {
      const t = String(action.type || '').toLowerCase();
      return t === 'courtscase' || t === 'courtsnomination' || t === 'courtsnominationvote';
    });
  } else if (filter === 'task') {
    filteredActions = actions.filter(action => action.type !== 'tombstone' && (action.type === 'task' || action.type === 'taskAssignment'));
  } else if (filter === 'torrent') {
    filteredActions = actions.filter(action => action.type === 'torrent');
  } else if (filter === 'chat') {
    filteredActions = actions.filter(action => (action.type === 'chat' || action.type === 'chatThread') && action.type !== 'tombstone');
  } else if (filter === 'industry') {
    filteredActions = actions.filter(action => ['industry', 'industryBuild', 'industryBlueprint', 'industryAllocation'].includes(action.type) && action.type !== 'tombstone');
  } else {
    filteredActions = actions.filter(action =>
      (action.type === filter
        || filter === 'all'
        || (filter === 'shop' && action.type === 'shopProduct')
        || (filter === 'votes' && action.type === 'poll'))
      && action.type !== 'tombstone');
  }

  const larpEarliest = new Map();
  for (const action of filteredActions) {
    if (action.type !== 'tribe') continue;
    const c = action.value?.content || action.content || {};
    const larpTag = (Array.isArray(c.tags) ? c.tags : []).find(x => String(x).startsWith('larp-'));
    if (!larpTag) continue;
    const ts = action.ts || 0;
    const prev = larpEarliest.get(larpTag);
    if (!prev || ts < prev.ts) larpEarliest.set(larpTag, { id: action.id, ts });
  }
  if (larpEarliest.size) {
    filteredActions = filteredActions.filter(action => {
      if (action.type !== 'tribe') return true;
      const c = action.value?.content || action.content || {};
      const larpTag = (Array.isArray(c.tags) ? c.tags : []).find(x => String(x).startsWith('larp-'));
      if (!larpTag) return true;
      return larpEarliest.get(larpTag).id === action.id;
    });
  }

  const aboutLatest = new Map();
  for (const action of filteredActions) {
    if (action.type !== 'about') continue;
    const c = action.value?.content || action.content || {};
    const subject = c.about || action.author;
    if (!subject) continue;
    const ts = action.ts || 0;
    const prev = aboutLatest.get(subject);
    if (!prev || ts > prev.ts) aboutLatest.set(subject, { id: action.id, ts });
  }
  if (aboutLatest.size) {
    filteredActions = filteredActions.filter(action => {
      if (action.type !== 'about') return true;
      const c = action.value?.content || action.content || {};
      const subject = c.about || action.author;
      if (!subject) return true;
      return aboutLatest.get(subject).id === action.id;
    });
  }

  const qs = String(q || '').trim();
  if (qs) {
    const qn = qs.toLowerCase();
    filteredActions = filteredActions.filter(a0 => {
      const t = String(a0.type || '').toLowerCase();
      const author = String(a0.author || '').toLowerCase();
      const id0 = String(a0.id || '').toLowerCase();
      const c0 = a0.value?.content || a0.content || {};
      const blob = [
        t, author, id0,
        c0.text, c0.title, c0.name, c0.description, c0.contentWarning,
        c0.about, c0.contact,
        c0.spreadTitle, c0.spreadText, c0.spreadOriginalAuthor,
        c0.vote?.link,
        c0.address?.host, c0.address?.key
      ].filter(Boolean).join(' ').toLowerCase();
      return blob.includes(qn);
    });
  }

  const MODULE_SUB_FILTERS = {
    audio:   { url: '/audios',     filters: ['all', 'mine', 'recent', 'top', 'favorites'] },
    video:   { url: '/videos',     filters: ['all', 'mine', 'recent', 'top', 'favorites'] },
    image:   { url: '/images',     filters: ['all', 'mine', 'recent', 'top', 'favorites'] },
    document:{ url: '/documents',  filters: ['all', 'mine', 'recent', 'top', 'favorites'] },
    bookmark:{ url: '/bookmarks',  filters: ['all', 'mine', 'recent', 'top', 'favorites'] },
    torrent: { url: '/torrents',   filters: ['all', 'mine', 'recent', 'top', 'favorites'] },
    map:     { url: '/maps',       filters: ['all', 'mine', 'recent'] },
    forum:   { url: '/forum',      filters: ['all', 'mine', 'recent', 'top'] },
    event:   { url: '/events',     filters: ['all', 'mine', 'recent', 'top'] },
    task:    { url: '/tasks',      filters: ['all', 'mine', 'assigned', 'open', 'closed'] },
    votes:   { url: '/votes',      filters: ['all', 'mine', 'recent', 'top'] },
    transfer:{ url: '/transfers',  filters: ['all', 'mine', 'pending', 'unconfirmed', 'closed'] },
    market:  { url: '/market',     filters: ['all', 'mine', 'exchange', 'auctions', 'for sale', 'sold'] },
    shop:    { url: '/shops',      filters: ['all', 'mine', 'recent'] },
    job:     { url: '/jobs',       filters: ['ALL', 'MINE', 'REMOTE', 'PRESENCIAL', 'OPEN', 'CLOSED'] },
    project: { url: '/projects',   filters: ['all', 'mine', 'active', 'completed'] },
    industry:{ url: '/industry',   filters: ['ALL', 'MINE', 'ACTIVE', 'PAUSED', 'DISSOLVED', 'BLUEPRINTS', 'BUILDS', 'MEMBER', 'RULES'] },
    chat:    { url: '/chats',      filters: ['all', 'mine'] },
    pad:     { url: '/pads',       filters: ['all', 'mine'] },
    calendar:{ url: '/calendars',  filters: ['all', 'mine'] },
    report:  { url: '/reports',    filters: ['all', 'mine', 'recent'] },
    curriculum:{ url: '/cv',       filters: ['view', 'edit'] }
  };

  const sub = MODULE_SUB_FILTERS[filter];

  let html = template(
    title,
    section(
      div({ class: 'tags-header' },
        h2(i18n.activityList),
        p(desc)
      ),
      div({ class: 'activity-filter-grid' },
        ...(() => {
          const COLUMNS = [
            ['all', 'mine', 'recent', 'top'],
            ['inhabitants', 'tribe', 'larp', 'parliament', 'courts'],
            ['votes', 'event', 'calendar', 'task', 'report'],
            ['banking', 'market', 'housing', 'project', 'industry', 'job', 'shop', 'transfer'],
            ['post', 'feed', 'chat', 'pad', 'forum', 'map'],
            ['audio', 'bookmark', 'document', 'image', 'torrent', 'video']
          ];
          const byType = new Map(activityTypes.map(t => [t.type, t]));
          const placed = new Set(COLUMNS.flat());
          const leftovers = activityTypes.filter(t => !placed.has(t.type));
          return COLUMNS
            .map(col => col.map(type => byType.get(type)).filter(Boolean))
            .concat(leftovers.length ? [leftovers] : [])
            .filter(col => col.length);
        })().map(col =>
          div({ class: 'activity-filter-col' },
            col.map(({ type, label }) =>
              form({ method: 'GET', action: '/activity' },
                input({ type: 'hidden', name: 'filter', value: type }),
                button({ type: 'submit', class: filter === type ? 'filter-btn active' : 'filter-btn' }, label)
              )
            )
          )
        )
      ),
      sub
        ? div({ class: 'activity-sub-filter' },
            sub.filters.map(f => a({ href: `${sub.url}?filter=${encodeURIComponent(f)}`, class: 'filter-btn' }, String(f).toUpperCase()))
          )
        : null,
      div({ class: 'filters' },
        form({ method: 'GET', action: '/activity', class: 'filter-box' },
          input({ type: 'hidden', name: 'filter', value: filter }),
          input({ type: 'text', name: 'q', value: qs, placeholder: i18n.activitySearchPlaceholder, class: 'filter-box__input' }),
          div({ class: 'filter-box__controls' },
            button({ type: 'submit', class: 'filter-box__button' }, i18n.searchButton)
          )
        )
      ),
    section({ class: 'feed-container' }, renderActionCards(filteredActions, userId, actions, spreadMap, extras))
    )
  );

  const hasDocument = actions.some(a => a && a.type === 'document');
  if (hasDocument) {
    html += `
      <script type="module" src="/js/pdf.min.mjs"></script>
      <script src="/js/pdf-viewer.js"></script>
    `;
  }
  return html;
};

