import maplibregl from "maplibre-gl";
import React from "react";
import { createRoot } from "react-dom/client";

const SW_ANIM_MS = 840;

import CardRenderer from "../Cards/CardRenderer";
import { cardTokens } from "../../theme/cardTokens";
import { getTemplateSpecForPost, getTemplateKeyForPost, adaptPostToTemplateData } from "./templateSpecs";
import { getMarkerColorForCategory } from "./postPriority";
import { loadAuth, guessUsernameFromToken } from "../../auth/authStorage";
import {
  recordPostShare,
  recordPostView,
  togglePostLike,
  fetchPostById,
  fetchPostComments,
  createPostComment,
  fetchPostLikeState,
  fetchProfile,
  fetchPostTruth,
  votePostTruth,
  editPost,
  deletePost,
  updatePostVisibility,
} from "../../api/client";
import { trackEvent } from "../../utils/analytics";
import { formatRelativeTime } from "../../utils/timezone";

const avatarCache = new Map();
const avatarInflight = new Map();
const HEATMAP_ENABLED = false;

/**
 * Convert avatar URL to small variant (160x160)
 * Asset URLs: ..._l.jpg -> ..._s.webp, ..._m.webp -> ..._s.webp
 */
function toSmallAvatarUrl(url) {
  if (!url || typeof url !== "string") return url;
  // Convert _l.jpg or _l.jpeg to _s.webp
  if (url.includes("_l.jpg") || url.includes("_l.jpeg")) {
    return url.replace(/_l\.jpe?g/i, "_s.webp");
  }
  // Convert _m.webp to _s.webp
  if (url.includes("_m.webp")) {
    return url.replace("_m.webp", "_s.webp");
  }
  return url;
}

function applyAvatar(el, username, url) {
  if (!el) return;
  const initial = (username || "U")[0]?.toUpperCase() || "U";
  el.innerHTML = "";
  if (url) {
    const img = document.createElement("img");
    img.src = toSmallAvatarUrl(url);
    img.alt = username || "user";
    el.appendChild(img);
  } else {
    el.textContent = initial;
  }
}

function resolvePostImage(post) {
  return normalizeImageUrl(
    post?.image_small ||
      post?.image_medium ||
      post?.image_large ||
      post?.image ||
      post?.media_url ||
      post?.image_url ||
      ""
  );
}

function refreshMarkerFromPost(map, postId, fresh) {
  if (!map || !fresh) return;
  const ref = map.__swMarkersRef;
  const list = ref?.current || [];
  for (const item of list) {
    if (!item || item.id !== postId) continue;
    // Preserve fetched truth_score if fresh data doesn't have it
    const oldTruth = item.post?.truth_score;
    const nextPost = { ...(item.post || {}), ...(fresh || {}) };
    if (Number.isFinite(oldTruth) && !Number.isFinite(Number(fresh?.truth_score))) {
      nextPost.truth_score = oldTruth;
    }
    item.post = nextPost;
    if (!item.el) continue;
    item.el.__post = nextPost;
    const nextKey = getTemplateKeyForPost(nextPost);
    const nextImg = resolvePostImage(nextPost);
    const nextTitle = (nextPost?.title || "").toString();
    if (
      item.el.__lastTemplateKey !== nextKey ||
      item.el.__lastImage !== nextImg ||
      item.el.__lastTitle !== nextTitle
    ) {
      if (item.type === "simple" && item.el.__renderSimple) {
        try { item.el.__renderSimple(); } catch {}
      } else if (item.el.__renderCompact) {
        try { item.el.__renderCompact(); } catch {}
      }
    }
  }
}

async function loadAvatar(username) {
  const key = (username || "").toLowerCase().trim();
  if (!key) return "";
  if (avatarCache.has(key)) return avatarCache.get(key);
  if (avatarInflight.has(key)) return avatarInflight.get(key);
  const p = (async () => {
    const profile = await fetchProfile(username);
    const url = (profile && profile.avatar_url) ? String(profile.avatar_url) : "";
    avatarCache.set(key, url);
    avatarInflight.delete(key);
    return url;
  })().catch(() => {
    avatarCache.set(key, "");
    avatarInflight.delete(key);
    return "";
  });
  avatarInflight.set(key, p);
  return p;
}

function hydrateAvatars(root) {
  if (!root) return;
  const nodes = root.querySelectorAll("[data-username]");
  nodes.forEach((el) => {
    const username = el.getAttribute("data-username") || "";
    if (!username) return;
    el.classList.add("sw-user-avatar");
    applyAvatar(el, username, avatarCache.get(username.toLowerCase().trim()) || "");
    loadAvatar(username).then((url) => {
      applyAvatar(el, username, url);
    });
  });
}

export function clearAllMarkers(markersRef, expandedElRef) {
  if (markersRef.current) {
    for (const m of markersRef.current) {
      try { if (m?.el && m.el.__swUnmount) m.el.__swUnmount(); } catch {}
      try { m.marker.remove(); } catch {}
    }
  }
  markersRef.current = [];
  if (expandedElRef) expandedElRef.current = null;
}

export function applyOcclusionForExpanded() {}
export function clearOcclusion(markersRef) {
  const list = markersRef.current || [];
  for (const item of list) {
    const el = item?.el;
    if (!el) continue;
    el.style.visibility = "visible";
    el.style.pointerEvents = "auto";
  }
}

function formatCount(value) {
  if (!Number.isFinite(value)) return "—";
  if (value < 1000) return String(Math.max(0, Math.floor(value)));
  if (value < 1000000) return `${(value / 1000).toFixed(1)}k`;
  return `${(value / 1000000).toFixed(1)}m`;
}

function readCount(post, keys) {
  for (const key of keys) {
    const v = post?.[key];
    if (Number.isFinite(v)) return v;
    const n = Number(v);
    if (!Number.isNaN(n) && Number.isFinite(n)) return n;
  }
  return null;
}

function updateStat(modalWrap, statKey, value) {
  const el = modalWrap.querySelector(`[data-stat="${statKey}"] .sw-stat-num`);
  if (el) el.textContent = formatCount(value);
}

function updateTruth(modalWrap, post) {
  const rail = modalWrap.querySelector(".sw-truth-rail");
  if (!rail) return;
  const score = truthScore(post);
  const marker = rail.querySelector(".sw-truth-rail-marker");
  const label = rail.querySelector(".sw-truth-rail-score");
  if (marker) marker.style.top = `${100 - score}%`;
  if (label) label.textContent = `${score}`;
}

function updateTruthVoteUI(modalWrap, stats, updateScore = false) {
  if (!modalWrap || !stats) return;
  const aiEl = modalWrap.querySelector("[data-truth-ai]");
  const userEl = modalWrap.querySelector("[data-truth-user]");
  const countEl = modalWrap.querySelector("[data-truth-count]");

  const aiScore = stats.ai_score !== null && Number.isFinite(stats.ai_score) ? Math.round(stats.ai_score) : null;
  const userScore = stats.user_score !== null && Number.isFinite(stats.user_score) ? Math.round(stats.user_score) : null;
  const totalVotes = Number.isFinite(stats.total_votes) ? stats.total_votes : (stats.TotalVotes || 0);

  if (aiEl) aiEl.textContent = aiScore !== null ? String(aiScore) : "—";
  if (userEl) userEl.textContent = userScore !== null ? String(userScore) : "—";
  if (countEl) countEl.textContent = `${totalVotes} votes`;

  // Only update the score display if explicitly requested AND we have a valid score
  if (updateScore && stats.truth_score !== null && Number.isFinite(stats.truth_score)) {
    const rail = modalWrap.querySelector(".sw-truth-rail");
    const marker = rail ? rail.querySelector(".sw-truth-rail-marker") : null;
    const label = rail ? rail.querySelector(".sw-truth-rail-score") : null;
    const truthScoreValue = Math.round(stats.truth_score);
    if (marker) marker.style.top = `${100 - truthScoreValue}%`;
    if (label) label.textContent = `${truthScoreValue}`;
  }

  const vote = (stats.user_vote || stats.UserVote || "").toString();
  const btns = modalWrap.querySelectorAll(".sw-truth-vote-btn");
  btns.forEach((btn) => {
    const v = btn.getAttribute("data-truth-vote");
    btn.classList.toggle("is-active", vote && v === vote);
  });
  const railUp = modalWrap.querySelector(".sw-truth-rail-btn-up");
  const railDown = modalWrap.querySelector(".sw-truth-rail-btn-down");
  if (railUp) railUp.classList.toggle("is-active", vote === "true");
  if (railDown) railDown.classList.toggle("is-active", vote === "false");
}

function isAuthed() {
  try {
    const auth = loadAuth();
    return !!(auth && auth.access_token);
  } catch {
    return false;
  }
}

function requestAuth(message, mode = "login") {
  try {
    window.dispatchEvent(
      new CustomEvent("sociowire:auth-required", {
        detail: { message, mode },
      })
    );
  } catch {}
  trackEvent("auth_required", { context: "map", mode: mode || "login" });
}

function applyPostDetails(modalWrap, post) {
  if (!modalWrap || !post) return;

  const title = (post?.title || post?.Title || "Untitled").toString();
  const summary = (post?.snippet || post?.body || "").toString();
  const img = heroImageForPost(post);
  const fullImg = fullImageForPost(post);
  const url = (post?.url || "").toString().trim();

  const titleEl = modalWrap.querySelector(".sw-modal-title");
  if (titleEl) titleEl.textContent = title;

  const summaryEl = modalWrap.querySelector(".sw-modal-summary");
  if (summaryEl) summaryEl.textContent = summary;

  // Skip hero replacement for camera and video posts - they have video content
  const isCamera = post?.content_type === 'camera';
  const isVideo = post?.content_type === 'video';
  if (!isCamera && !isVideo) {
    const heroGallery = collectGalleryImages(post);
    const heroImageSrc = heroGallery[0] || heroImageForPost(post);
    const heroFullSrc = heroGallery[0] || fullImageForPost(post);
    const heroHasGallery = heroGallery.length > 1;
    const hero = modalWrap.querySelector(".sw-modal-hero");
    const clusterHero = modalWrap.querySelector(".sw-cluster-hero-media");
    if (hero || clusterHero) {
      const target = hero || clusterHero;
      const heroImageHtml = heroImageSrc
        ? `<img src="${escapeHtml(heroImageSrc)}" data-fullsrc="${escapeHtml(heroFullSrc)}" alt="" loading="lazy" data-gallery-index="0" />`
        : `<div class="sw-modal-hero-fallback"><div class="sw-modal-hero-fallback-inner"><i class="${categoryIconClass(post)}"></i><span>${escapeHtml(normCatLabel(post))}</span></div></div>`;
      const arrowHtml = heroHasGallery
        ? `<button class="sw-modal-hero-arrow left" type="button" aria-label="Previous image"><i class="fa-solid fa-chevron-left"></i></button><button class="sw-modal-hero-arrow right" type="button" aria-label="Next image"><i class="fa-solid fa-chevron-right"></i></button>`
        : "";
      const indicatorHtml = heroHasGallery
        ? `<div class="sw-modal-gallery-indicator" aria-live="polite"><span class="sw-modal-gallery-current">1</span><span class="sw-modal-gallery-separator">/</span><span class="sw-modal-gallery-total">${heroGallery.length}</span></div>`
        : "";
      // DEBUG v0.0.22 - send debug to server
      try {
        fetch('/api/debug-log', {
          method: 'POST',
          headers: { 'Content-Type': 'application/json' },
          body: JSON.stringify({
            event: 'modal_gallery',
            post_id: post?.id,
            media_urls_raw: post?.media_urls,
            media_urls_count: Array.isArray(post?.media_urls) ? post.media_urls.length : 0,
            gallery_count: heroGallery.length,
            gallery_items: heroGallery,
            has_gallery: heroHasGallery,
            content_type: post?.content_type
          })
        }).catch(() => {});
      } catch {}
      target.innerHTML = `${heroImageHtml}${arrowHtml}${indicatorHtml}`;
      bindHeroLightbox(modalWrap);
      if (heroHasGallery) {
        initModalGallery(modalWrap, heroGallery);
      }
    }
  }

  const badge = modalWrap.querySelector(".sw-modal-badge");
  if (badge) badge.textContent = normCatLabel(post);

  const meta = modalWrap.querySelector(".sw-modal-meta");
  if (meta) {
    const author = (post?.author || post?.Author || "").toString().trim();
    const sub = (post?.sub_category || post?.subCategory || "").toString().trim();
    const relTime = formatRelativeTime(getPostTime(post));
    const metaLeft = author ? `by ${author}` : "";
    const metaRight = sub ? sub : "";
    meta.innerHTML = `
      ${author ? `<div class="sw-inline-avatar" data-username="${escapeHtml(author)}"></div>` : ""}
      ${metaLeft ? `<span class="sw-author-link" data-author="${escapeHtml(author)}" style="cursor: pointer; text-decoration: underline;">${escapeHtml(metaLeft)}</span>` : ""}
      ${metaRight ? `<span>• ${escapeHtml(metaRight)}</span>` : ""}
      <span>• ${escapeHtml(relTime)}</span>
    `;
    hydrateAvatars(modalWrap);
  }
  setModalTags(modalWrap, post);

  const cta = modalWrap.querySelector(".sw-cta-primary");
  if (cta) {
    if (url) {
      cta.removeAttribute("disabled");
      cta.setAttribute("data-url", url);
    } else {
      cta.setAttribute("disabled", "true");
      cta.setAttribute("data-url", "");
    }
  }

  const viewCount = readCount(post, ["view_count", "views", "viewCount"]);
  const likeCount = readCount(post, ["like_count", "likes", "likeCount"]);
  const shareCount = readCount(post, ["share_count", "shares", "shareCount"]);
  const commentCount = readCount(post, ["comment_count", "comments", "commentCount"]);
  updateStat(modalWrap, "views", viewCount);
  updateStat(modalWrap, "likes", likeCount);
  updateStat(modalWrap, "shares", shareCount);
  updateStat(modalWrap, "comments", commentCount);
  // Don't update truth here - let fetchPostTruth handle it to avoid race condition
}

function renderComments(container, items) {
  if (!container) return;
  if (!Array.isArray(items) || items.length === 0) {
    container.innerHTML = `<div class="sw-chat-line">— no comments yet</div>`;
    return;
  }
  container.innerHTML = items
    .map((c) => {
      const who = (c?.username || "anon").toString().trim();
      const body = (c?.body || "").toString().trim();
      const rawTs = (c?.created_at || "").toString().trim();
      // Format timestamp in user's timezone
      const ts = rawTs ? formatRelativeTime(rawTs) : "";
      return `
        <div class="sw-chat-bubble">
          <div class="sw-chat-head">
            <div class="sw-chat-avatar" data-username="${escapeHtml(who)}"></div>
            <div class="sw-chat-meta">${escapeHtml(who)}${ts ? ` · ${escapeHtml(ts)}` : ""}</div>
          </div>
          <div class="sw-chat-text">${escapeHtml(body)}</div>
        </div>
      `;
    })
    .join("");
  hydrateAvatars(container);
}

function mountCard(container, spec, data, theme, post) {
  let root = container.__swReactRoot;
  if (!root) {
    root = createRoot(container);
    container.__swReactRoot = root;
  }
  const tokens = cardTokens?.[theme] || cardTokens.blue;
  const score = truthScore(post || data || {});
  const templateKey = getTemplateKeyForPost(post || {});
  const isPolaroid = templateKey === 'polaroid';

  // Build children array
  const children = [
    React.createElement(CardRenderer, {
      key: "card",
      spec,
      data,
      themeTokens: tokens,
      onAction: (action, value) => {
        if (action?.type === "open_url" && value) {
          try { window.open(value, "_blank"); } catch {}
        }
      },
      className: "",
    }),
  ];

  // For polaroid: just a small round indicator at bottom right
  // For others: full truth bar on the side
  if (isPolaroid) {
    children.push(
      React.createElement(
        "div",
        {
          key: "truth-dot",
          className: "sw-truth-dot",
          style: {
            position: 'absolute',
            bottom: 6,
            right: 6,
            width: 18,
            height: 18,
            borderRadius: '50%',
            background: truthColor(score),
            display: 'flex',
            alignItems: 'center',
            justifyContent: 'center',
            fontSize: 9,
            fontWeight: 700,
            color: '#fff',
            boxShadow: '0 1px 3px rgba(0,0,0,0.3)',
          }
        },
        `${score}`
      )
    );
  } else {
    children.push(
      React.createElement(
        "div",
        { key: "truth-bar", className: "sw-truth-mini-bar" },
        React.createElement("div", {
          className: "sw-truth-mini-marker",
          style: { top: `${100 - score}%` },
        })
      ),
      React.createElement(
        "div",
        { key: "truth-score", className: "sw-truth-mini-score", style: { background: truthColor(score) } },
        `${score}`
      )
    );
  }

  root.render(
    React.createElement(
      "div",
      {
        className: "sw-card-wrap",
        style: { width: spec?.size?.w, height: spec?.size?.h },
      },
      ...children
    )
  );

  const rawScore = Number(
    post?.truth_score ??
      post?.truthScore ??
      post?.truth_ai ??
      post?.truthAi ??
      post?.ai_score ??
      post?.AIScore
  );
  const hasAI = Number.isFinite(
    Number(post?.truth_ai ?? post?.truthAi ?? post?.ai_score ?? post?.AIScore)
  );
  const hasScore = Number.isFinite(rawScore) && !(rawScore === 50 && !hasAI);
  if (!hasScore && post?.id) {
    fetchPostTruth(post.id).then((stats) => {
      if (!stats) return;
      const nextScore = Number(stats.truth_score ?? stats.TruthScore);
      if (!Number.isFinite(nextScore)) return;
      post.truth_score = nextScore;
      const markerEl = container.querySelector(".sw-truth-mini-marker");
      const scoreEl = container.querySelector(".sw-truth-mini-score");
      if (markerEl) markerEl.style.top = `${100 - nextScore}%`;
      if (scoreEl) {
        scoreEl.style.background = truthColor(nextScore);
        scoreEl.textContent = `${Math.round(nextScore)}`;
      }
    });
  }

  return () => {
    try { root.unmount(); } catch {}
    container.__swReactRoot = null;
  };
}

function escapeHtml(str) {
  return String(str ?? "")
    .replace(/&/g, "&amp;")
    .replace(/</g, "&lt;")
    .replace(/>/g, "&gt;")
    .replace(/"/g, "&quot;");
}

// Extract camera ID from Quebec 511 URL like https://www.quebec511.info/Carte/Fenetres/FenetreVideo.html?id=3978
function extractCameraId(url) {
  if (!url) return null;
  const match = url.match(/[?&]id=(\d+)/);
  return match ? match[1] : null;
}

// Build stream proxy URL for camera
function getCameraStreamUrl(postData) {
  const embedUrl = normalizeMediaUrl(postData?.embed_url || postData?.video_url || '');
  const cameraId = extractCameraId(embedUrl);
  if (cameraId) {
    return `/live/api/stream/${cameraId}`;
  }
  return embedUrl;
}

function renderTagPills(tags) {
  if (!Array.isArray(tags) || tags.length === 0) return "";
  const uniq = [];
  const seen = new Set();
  for (const raw of tags) {
    const t = (raw || "").toString().trim().toLowerCase();
    if (!t || seen.has(t)) continue;
    seen.add(t);
    uniq.push(t);
    if (uniq.length >= 6) break;
  }
  if (uniq.length === 0) return "";
  return `
    <div class="sw-modal-tags">
      ${uniq.map((t) => `<span class="sw-modal-tag">#${escapeHtml(t)}</span>`).join("")}
    </div>
  `;
}

function setModalTags(modalWrap, post) {
  if (!modalWrap) return;
  const tagsHtml = renderTagPills(post?.event_tags || post?.tags);
  const existing = modalWrap.querySelector(".sw-modal-tags");
  if (tagsHtml) {
    if (existing) {
      existing.outerHTML = tagsHtml;
    } else {
      const titleEl = modalWrap.querySelector(".sw-modal-title") || modalWrap.querySelector(".sw-cluster-title");
      if (titleEl) titleEl.insertAdjacentHTML("afterend", tagsHtml);
    }
  } else if (existing) {
    existing.remove();
  }
}

function bindHeroLightbox(modalWrap) {
  if (!modalWrap) return;
  const img = modalWrap.querySelector(".sw-modal-hero img, .sw-cluster-hero-media img");
  if (!img || img.dataset.zoomBound === "1") return;
  img.dataset.zoomBound = "1";
  img.style.cursor = "zoom-in";
  img.addEventListener("click", (e) => {
    e.preventDefault();
    e.stopPropagation();
    const src = (img.getAttribute("data-fullsrc") || img.getAttribute("src") || "").trim();
    if (src) {
      openImageLightbox(src);
    }
  });
}

function initModalGallery(modalWrap, gallery) {
  if (!modalWrap || !Array.isArray(gallery) || gallery.length === 0) return;
  const heroImg = modalWrap.querySelector(".sw-modal-hero img, .sw-cluster-hero-media img");
  if (!heroImg) return;
  const arrows = modalWrap.querySelectorAll(".sw-modal-hero-arrow");
  let activeIndex = 0;
  const indicator = modalWrap.querySelector(".sw-modal-gallery-indicator");
  const currentIndicator = indicator?.querySelector(".sw-modal-gallery-current");
  const totalIndicator = indicator?.querySelector(".sw-modal-gallery-total");
  if (totalIndicator) {
    totalIndicator.textContent = String(gallery.length);
  }
  const setActive = (idx) => {
    if (!Number.isFinite(idx)) return;
    const normalized = (idx + gallery.length) % gallery.length;
    activeIndex = normalized;
    heroImg.src = gallery[normalized];
    heroImg.dataset.fullsrc = gallery[normalized];
    heroImg.dataset.galleryIndex = String(normalized);
    if (currentIndicator) {
      currentIndicator.textContent = String(normalized + 1);
    }
  };
  arrows.forEach((btn) => {
    btn.addEventListener("click", (event) => {
      event.stopPropagation();
      const direction = btn.classList.contains("left") ? -1 : 1;
      const nextIndex = (activeIndex + direction + gallery.length) % gallery.length;
      setActive(nextIndex);
    });
  });
  setActive(0);
  let touchStartX = 0;
  const touchThreshold = 30;
  heroImg.addEventListener("touchstart", (event) => {
    if (!event.touches?.length) return;
    touchStartX = event.touches[0].clientX;
  });
  heroImg.addEventListener("touchend", (event) => {
    if (!event.changedTouches?.length) return;
    const dx = event.changedTouches[0].clientX - touchStartX;
    if (Math.abs(dx) < touchThreshold) return;
    const direction = dx > 0 ? -1 : 1;
    const nextIndex = (activeIndex + direction + gallery.length) % gallery.length;
    setActive(nextIndex);
  });
}

function openImageLightbox(src) {
  if (!src) return;
  const overlay = document.createElement("div");
  overlay.className = "sw-image-lightbox";
  overlay.style.position = "fixed";
  overlay.style.inset = "0";
  overlay.style.background = "rgba(8,10,16,0.86)";
  overlay.style.zIndex = "6000"; /* Image lightbox - above post modal (5000), below auth (10000) */
  overlay.style.display = "flex";
  overlay.style.alignItems = "center";
  overlay.style.justifyContent = "center";
  overlay.style.padding = "16px";
  const img = document.createElement("img");
  img.src = src;
  img.alt = "";
  img.style.maxWidth = "96vw";
  img.style.maxHeight = "96vh";
  img.style.objectFit = "contain";
  img.style.borderRadius = "14px";
  img.style.boxShadow = "0 20px 50px rgba(0,0,0,0.45)";
  overlay.appendChild(img);

  const close = (opts = {}) => {
    try { document.removeEventListener("keydown", onKey); } catch {}
    try { window.removeEventListener("popstate", onPop); } catch {}
    if (overlay.parentNode) overlay.parentNode.removeChild(overlay);
    // When closing image, go back in history to return to post (not close post)
    if (!opts.skipHistory && overlay.__swPushed) {
      overlay.__swPushed = false;
      try { history.back(); } catch {}
    }
  };
  const onKey = (e) => {
    // ESC closes image and returns to post (via history.back)
    if (e.key === "Escape") close();
  };
  const onPop = () => close({ skipHistory: true });
  overlay.addEventListener("click", close);
  document.addEventListener("keydown", onKey);
  window.addEventListener("popstate", onPop);

  // Push history for image so back button closes image (returns to post)
  try {
    history.pushState({ sw: "image" }, "");
    overlay.__swPushed = true;
  } catch {}
  document.body.appendChild(overlay);
}

function normCatLabel(post) {
  const c = (post?.category || "").toString().toUpperCase();
  if (c === "NEWS") return "NEWS";
  if (c === "EVENT" || c === "EVENTS") return "EVENTS";
  if (c === "FRIENDS") return "FRIENDS";
  if (c === "MARKET") return "MARKET";
  return "NEWS";
}

function categoryIconClass(post) {
  const c = (post?.category || "NEWS").toString().toUpperCase();
  switch (c) {
    case "EVENT":
    case "EVENTS":
      return "fa-regular fa-calendar";
    case "FRIENDS":
      return "fa-solid fa-user-group";
    case "MARKET":
      return "fa-solid fa-store";
    default:
      return "fa-regular fa-newspaper";
  }
}

function isClusterMainPost(post) {
  const clusterPostId = Number(post?.cluster_post_id);
  const postId = Number(post?.post_id || post?.id);
  if (Number.isFinite(clusterPostId) && Number.isFinite(postId) && clusterPostId > 0 && postId > 0) {
    return clusterPostId === postId;
  }
  const key = (post?.cluster_key || post?.clusterKey || "").toString().trim();
  const members = Array.isArray(post?.cluster_members) ? post.cluster_members : [];
  if (key && members.length > 0) return true;
  const url = (post?.url || "").toString();
  return url.includes("/cluster/");
}

function dispatchClusterHeatmap(post, opts = {}) {
  if (!post) return;
  const postId = Number(post?.id || post?.post_id || 0);
  const members = Array.isArray(post?.cluster_members) ? post.cluster_members : [];
  const items = Array.isArray(post?.cluster_items) ? post.cluster_items : [];
  const points = items
    .map((it) => ({ lat: Number(it?.lat), lon: Number(it?.lon) }))
    .filter((p) => Number.isFinite(p.lat) && Number.isFinite(p.lon));
  window.dispatchEvent(
    new CustomEvent("sociowire:cluster-heatmap", {
      detail: {
        clusterKey: post?.cluster_key || post?.clusterKey || "",
        postId,
        members,
        points,
        items,
        post,
        center: { lat: Number(post?.lat), lon: Number(post?.lon) },
        mode: opts.mode || "click",
        skipPan: !!opts.skipPan,
      },
    })
  );
}

function dispatchClusterHeatmapClear() {
  window.dispatchEvent(new CustomEvent("sociowire:cluster-heatmap-clear"));
}

function buildClusterItemsHtml(items) {
  if (!Array.isArray(items) || items.length === 0) {
    return `<div class="sw-cluster-source-empty">No related reports yet.</div>`;
  }
  const rows = items.slice(0, 6).map((item) => {
    const title = escapeHtml(item?.title || "Untitled");
    const source = escapeHtml(item?.source || "");
    const url = escapeHtml(item?.url || "");
    const meta = source ? `<span class="sw-cluster-source">${source}</span>` : "";
    const href = url ? ` data-related-url="${url}"` : "";
    return `
      <div class="sw-cluster-source-item"${href}>
        <div class="sw-cluster-source-title">${title}</div>
        ${meta}
      </div>
    `;
  });
  return rows.join("");
}

function heroImageForPost(post) {
  const raw =
    post?.image_medium ||
    post?.image_large ||
    post?.image_small ||
    post?.image ||
    post?.thumbnail_url ||
    post?.media_url ||
    "";
  const v = normalizeImageUrl(raw);
  if (v) return v;
  const cat = normCatLabel(post);
  return `/og/category?cat=${encodeURIComponent(cat)}`;
}

function fullImageForPost(post) {
  const raw =
    post?.image_large ||
    post?.image_medium ||
    post?.image_small ||
    post?.image ||
    post?.media_url ||
    "";
  return normalizeImageUrl(raw);
}

function collectGalleryImages(post) {
  const result = [];
  const seen = new Set();
  const add = (value) => {
    const normalized = normalizeImageUrl(value);
    if (!normalized) return;
    if (seen.has(normalized)) return;
    seen.add(normalized);
    result.push(normalized);
  };
  // Check both media_urls (frontend) and image_urls (backend API)
  if (Array.isArray(post?.media_urls)) {
    post.media_urls.forEach(add);
  }
  if (Array.isArray(post?.image_urls)) {
    post.image_urls.forEach(add);
  }
  ["image_large", "image_medium", "image_small", "image", "media_url"].forEach((key) => {
    add(post?.[key]);
  });
  return result;
}

function getPostTime(post) {
  const published =
    post?.published_at ||
    post?.publishedAt ||
    post?.PublishedAt ||
    "";
  if (published) {
    const d = new Date(String(published));
    if (!Number.isNaN(d.getTime())) {
      const now = Date.now();
      if (d.getTime() <= now + 5 * 60 * 1000) {
        return published;
      }
    }
  }
  return (
    post?.created_at ||
    post?.CreatedAt ||
    post?.createdAt ||
    published
  );
}

// Known trusted sources get higher base scores
const SOURCE_REPUTATION = {
  // Major news agencies
  'reuters': 85, 'ap': 85, 'afp': 82,
  // Government/official
  'gov': 80, 'gouv': 80, 'canada.ca': 82, 'quebec.ca': 80,
  // Major outlets
  'bbc': 78, 'cbc': 78, 'radio-canada': 78, 'nytimes': 76, 'theguardian': 75,
  'lapresse': 74, 'ledevoir': 74, 'journaldemontreal': 70,
  // Tech
  'techcrunch': 72, 'theverge': 70, 'arstechnica': 74,
  // Social/user content (lower trust)
  'twitter': 45, 'x.com': 45, 'facebook': 48, 'reddit': 50, 'tiktok': 42,
};

// Clickbait/sensational words reduce score
const CLICKBAIT_WORDS = [
  'shocking', 'unbelievable', 'you won\'t believe', 'mind-blowing', 'insane',
  'choquant', 'incroyable', 'hallucinant', 'fou', 'dingue',
  'breaking', 'urgent', 'exclusive', 'leaked', 'secret',
  '!!!', '???', 'exposed', 'révélé', 'scandale'
];

// Credibility indicators increase score
const CREDIBILITY_WORDS = [
  'study', 'research', 'according to', 'report', 'official',
  'étude', 'recherche', 'selon', 'rapport', 'officiel',
  'confirmed', 'confirmé', 'announced', 'annoncé', 'statement'
];

function calculateInitialTruthScore(post) {
  let score = 55; // Base score
  let factors = 0;

  // 1. Source reputation (weight: 40%)
  const source = (post?.source || post?.Source || '').toLowerCase();
  const domain = (post?.domain || post?.url || '').toLowerCase();
  for (const [key, rep] of Object.entries(SOURCE_REPUTATION)) {
    if (source.includes(key) || domain.includes(key)) {
      score = rep;
      factors++;
      break;
    }
  }

  // 2. Author info (weight: 15%)
  const author = post?.author || post?.Author || '';
  if (author) {
    score += 3; // Has attributed author
    factors++;
    // Verified authors get bonus
    if (post?.author_verified || post?.verified) {
      score += 5;
    }
  }

  // 3. Content analysis (weight: 25%)
  const title = (post?.title || post?.headline || '').toLowerCase();
  const content = (post?.content || post?.description || post?.summary || '').toLowerCase();
  const text = title + ' ' + content;

  // Check for clickbait
  let clickbaitCount = 0;
  for (const word of CLICKBAIT_WORDS) {
    if (text.includes(word.toLowerCase())) {
      clickbaitCount++;
    }
  }
  if (clickbaitCount > 0) {
    score -= Math.min(clickbaitCount * 4, 15); // Max -15 for clickbait
    factors++;
  }

  // Check for credibility indicators
  let credibilityCount = 0;
  for (const word of CREDIBILITY_WORDS) {
    if (text.includes(word.toLowerCase())) {
      credibilityCount++;
    }
  }
  if (credibilityCount > 0) {
    score += Math.min(credibilityCount * 3, 10); // Max +10 for credibility
    factors++;
  }

  // 4. Content quality indicators (weight: 20%)
  // Longer content tends to be more detailed/trustworthy
  const contentLength = (content || '').length;
  if (contentLength > 500) score += 3;
  else if (contentLength > 200) score += 1;
  else if (contentLength < 50) score -= 3;

  // Has image/media (more complete reporting)
  if (post?.image || post?.media_url || post?.image_url) {
    score += 2;
  }

  // Has URL to original source
  if (post?.url || post?.link) {
    score += 2;
  }

  // 5. Engagement signals (if available)
  const likes = Number(post?.like_count || post?.likes || 0);
  const shares = Number(post?.share_count || post?.shares || 0);
  const views = Number(post?.view_count || post?.views || 0);

  // High engagement can be positive or negative - slight positive bias for established content
  if (views > 1000 && likes > 50) {
    score += 2;
  }

  // Clamp to valid range
  return clamp(Math.round(score), 25, 85); // Never go below 25 or above 85 without AI verification
}

function truthScore(post) {
  // First check if we have an AI-verified score
  const raw = Number(
    post?.truth_score ??
      post?.truthScore ??
      post?.truth_ai ??
      post?.truthAi ??
      post?.ai_score ??
      post?.AIScore
  );
  if (Number.isFinite(raw)) {
    return clamp(Math.round(raw), 0, 100);
  }
  // Calculate initial score from available data
  return calculateInitialTruthScore(post);
}

function truthColor(score) {
  const s = clamp(score, 0, 100);
  const hue = Math.round((s / 100) * 120);
  return `hsl(${hue} 80% 42%)`;
}

function clamp(v, min, max) {
  return Math.min(Math.max(v, min), max);
}

function normalizeImageUrl(raw) {
  const value = (raw || "").toString().trim();
  if (!value) return "";
  if (value.startsWith("//")) return `https:${value}`;
  if (value.startsWith("http://")) return `https://${value.slice(7)}`;
  return value;
}

function normalizeMediaUrl(raw) {
  const value = (raw || "").toString().trim();
  if (!value) return "";
  if (value.startsWith("//")) return `https:${value}`;
  if (value.startsWith("http://")) return `https://${value.slice(7)}`;
  return value;
}

function closeCenteredOverlay(map, opts = {}) {
  try {
    const ov = map?.__swCenteredOverlay;
    if (!ov) return;
    if (!opts.force && ov.openedAt && Date.now() - ov.openedAt < 700) return;
    if (ov.closing) return;
    ov.closing = true;
    const skipHistory = !!opts.skipHistory || opts.reason === "switch";

    if (ov.onKey) window.removeEventListener("keydown", ov.onKey);
    if (ov.onAuth) window.removeEventListener("sociowire:auth-changed", ov.onAuth);
    if (ov.onPop) window.removeEventListener("popstate", ov.onPop);

    // animate out
    try {
      const mw = ov.modalWrapRef;
      if (mw) {
        mw.style.opacity = "0";
        mw.style.transform = "scale(0.96)";
      }
      if (ov.backdrop) ov.backdrop.style.opacity = "0";
    } catch {}
    try { if (ov.unmountFull) ov.unmountFull(); } catch {}
    try { if (ov.unmountMini) ov.unmountMini(); } catch {}
    try {
      const b = ov.backdrop;
      if (b) setTimeout(() => { try { b.remove(); } catch {} }, 300);
    } catch {}

    map.__swCenteredOverlay = null;

    try {
      const st = ov.interactionState || {};
      if (map) {
        if (st.dragPan) map.dragPan?.enable?.();
        if (st.scrollZoom) map.scrollZoom?.enable?.();
        if (st.doubleClickZoom) map.doubleClickZoom?.enable?.();
        if (st.touchZoomRotate) map.touchZoomRotate?.enable?.();
      }
    } catch {}

    if (!skipHistory && ov.historyPushed && !ov.historyClosed) {
      ov.historyClosed = true;
      try { window.history.back(); } catch {}
    }
    if (map) {
      try {
        map.__swForceFetchAt = Date.now();
        map.fire("moveend");
      } catch {}
    }
  } catch {}
}

function openCameraOverlay({ map, post, theme, fullScreen, interactionState }) {
  const postData = post || {};
  const title = postData.title || "Live Camera";
  const region = postData.region || "";
  const embedUrl = normalizeMediaUrl(postData.embed_url || "");
  const videoUrl = normalizeMediaUrl(postData.video_url || "");
  const lat = postData.lat;
  const lon = postData.lon;
  const postID = postData.id || 0;

  trackEvent("camera_open", {
    post_id: postID,
    source: postData.source || "",
    region: region,
  });

  const backdrop = document.createElement("div");
  backdrop.className = "sw-centered-backdrop sw-camera-backdrop";
  if (fullScreen) backdrop.classList.add("sw-modal-fullscreen");
  backdrop.style.cssText = `
    position: fixed;
    inset: 0;
    z-index: 5000;
    background: rgba(0,0,0,0.85);
    display: flex;
    align-items: center;
    justify-content: center;
    padding: 12px;
  `;

  const modalWrap = document.createElement("div");
  modalWrap.className = "sw-camera-modal";
  modalWrap.style.cssText = `
    width: 100%;
    max-width: 900px;
    max-height: 90vh;
    background: rgba(15, 23, 42, 0.98);
    border-radius: 16px;
    border: 1px solid rgba(148, 163, 184, 0.6);
    box-shadow: 0 20px 60px rgba(0,0,0,0.7), 0 0 30px rgba(56,189,248,0.3);
    overflow: hidden;
    display: flex;
    flex-direction: column;
  `;

  const coordsInfo = (lat && lon) ? `(${Number(lat).toFixed(4)}, ${Number(lon).toFixed(4)})` : "";

  modalWrap.innerHTML = `
    <div class="sw-camera-modal-header" style="display:flex;align-items:center;gap:0.75rem;padding:0.75rem 1rem;background:rgba(15,23,42,0.95);border-bottom:1px solid rgba(148,163,184,0.3);">
      <div style="width:42px;height:42px;border-radius:12px;background:linear-gradient(135deg,rgba(56,189,248,0.25),rgba(59,130,246,0.25));border:1px solid rgba(56,189,248,0.6);display:flex;align-items:center;justify-content:center;">
        <i class="fa-solid fa-video" style="font-size:18px;color:#38bdf8;"></i>
      </div>
      <div style="flex:1;min-width:0;">
        <div style="font-size:1rem;font-weight:800;color:#f8fafc;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;">${escapeHtml(title)}</div>
        <div style="font-size:0.78rem;color:#94a3b8;">${escapeHtml(region)}</div>
      </div>
      <div style="display:flex;align-items:center;gap:0.35rem;padding:0.3rem 0.7rem;border-radius:999px;background:rgba(239,68,68,0.85);color:#fff;font-size:0.7rem;font-weight:800;letter-spacing:0.08em;">
        <span style="width:6px;height:6px;border-radius:50%;background:#fff;animation:sw-camera-pulse 1.5s ease-in-out infinite;"></span>
        LIVE
      </div>
      <button class="sw-camera-close" style="width:36px;height:36px;border-radius:50%;border:1px solid rgba(148,163,184,0.4);background:rgba(15,23,42,0.8);color:#e5e7eb;font-size:1.1rem;display:flex;align-items:center;justify-content:center;cursor:pointer;">
        <i class="fa-solid fa-times"></i>
      </button>
    </div>
    <div style="flex:1;display:flex;flex-direction:column;min-height:0;overflow:auto;">
      <div style="position:relative;width:100%;padding-top:56.25%;background:linear-gradient(135deg,rgba(15,23,42,1),rgba(30,41,59,1));">
        <div style="position:absolute;inset:0;display:flex;flex-direction:column;align-items:center;justify-content:center;gap:1.5rem;padding:2rem;">
          <div style="width:80px;height:80px;border-radius:50%;background:linear-gradient(135deg,rgba(56,189,248,0.2),rgba(59,130,246,0.2));border:2px solid rgba(56,189,248,0.4);display:flex;align-items:center;justify-content:center;">
            <i class="fa-solid fa-video" style="font-size:32px;color:#38bdf8;"></i>
          </div>
          <div style="text-align:center;">
            <div style="font-size:1.1rem;font-weight:700;color:#f8fafc;margin-bottom:0.5rem;">Live Traffic Camera</div>
            <div style="font-size:0.85rem;color:#94a3b8;">Click below to view the live stream</div>
          </div>
          ${(embedUrl || videoUrl)
            ? `<a href="${escapeHtml(embedUrl || videoUrl)}" target="_blank" rel="noopener noreferrer" style="display:inline-flex;align-items:center;gap:0.5rem;padding:0.75rem 1.5rem;background:linear-gradient(135deg,#0ea5e9,#3b82f6);color:#fff;font-weight:700;font-size:0.9rem;border-radius:999px;text-decoration:none;box-shadow:0 4px 15px rgba(14,165,233,0.4);">
                <i class="fa-solid fa-external-link-alt"></i>
                Open Live Stream
              </a>`
            : `<div style="color:#94a3b8;font-size:0.85rem;">Stream URL unavailable</div>`
          }
        </div>
      </div>
      <div style="padding:0.75rem 1rem;border-top:1px solid rgba(148,163,184,0.2);">
        <div style="display:flex;align-items:center;gap:0.5rem;font-size:0.8rem;color:#94a3b8;margin-bottom:0.5rem;">
          <i class="fa-solid fa-map-marker-alt" style="color:#38bdf8;"></i>
          <span>${escapeHtml(region)}</span>
          ${coordsInfo ? `<span style="opacity:0.7;font-size:0.75rem;">${coordsInfo}</span>` : ""}
        </div>
      </div>
      <div style="display:flex;gap:0.5rem;padding:0.75rem 1rem;border-top:1px solid rgba(148,163,184,0.2);flex-wrap:wrap;">
        <button class="sw-camera-share sw-wall-btn" style="border:1px solid rgba(56,189,248,0.35);background:rgba(15,23,42,0.6);color:#e2f2ff;font-weight:800;font-size:0.75rem;padding:0.35rem 0.6rem;border-radius:999px;cursor:pointer;">
          <i class="fa-solid fa-share-nodes"></i> Share
        </button>
        ${embedUrl ? `<a href="${escapeHtml(embedUrl)}" target="_blank" rel="noopener noreferrer" class="sw-wall-btn" style="border:1px solid rgba(56,189,248,0.35);background:rgba(15,23,42,0.6);color:#e2f2ff;font-weight:800;font-size:0.75rem;padding:0.35rem 0.6rem;border-radius:999px;cursor:pointer;text-decoration:none;">
          <i class="fa-solid fa-external-link"></i> Open External
        </a>` : ""}
      </div>
    </div>
  `;

  const closeModal = () => {
    try {
      backdrop.remove();
      map.__swCenteredOverlay = null;
      if (interactionState.dragPan) map.dragPan?.enable?.();
      if (interactionState.scrollZoom) map.scrollZoom?.enable?.();
      if (interactionState.doubleClickZoom) map.doubleClickZoom?.enable?.();
      if (interactionState.touchZoomRotate) map.touchZoomRotate?.enable?.();
    } catch {}
  };

  backdrop.addEventListener("click", (e) => {
    if (e.target === backdrop) closeModal();
  });

  const closeBtn = modalWrap.querySelector(".sw-camera-close");
  if (closeBtn) closeBtn.addEventListener("click", closeModal);

  const shareBtn = modalWrap.querySelector(".sw-camera-share");
  if (shareBtn) {
    shareBtn.addEventListener("click", () => {
      const shareUrl = embedUrl || videoUrl || "";
      if (navigator.share) {
        navigator.share({ title, url: shareUrl }).catch(() => {});
      } else if (navigator.clipboard && shareUrl) {
        navigator.clipboard.writeText(shareUrl);
      }
    });
  }

  document.addEventListener("keydown", function escHandler(e) {
    if (e.key === "Escape") {
      closeModal();
      document.removeEventListener("keydown", escHandler);
    }
  });

  backdrop.appendChild(modalWrap);
  document.body.appendChild(backdrop);
  map.__swCenteredOverlay = { el: backdrop, postId: postID };
}

export function openCenteredOverlay({ map, post, theme, fullScreen = false }) {
  if (!map) return;

  // Weather posts open in a dedicated WeatherModal
  if (post?.content_type === "weather") {
    try {
      window.dispatchEvent(new CustomEvent("weather-open", { detail: post }));
    } catch {}
    return;
  }

  // Use the React PostDetailModal for ALL post types (including live, camera, video, etc.)
  try {
    window.dispatchEvent(new CustomEvent("sw:openPostDetail", { detail: { post } }));
  } catch {}
  return;

  // LEGACY CODE BELOW - kept for reference but not used anymore
  closeCenteredOverlay(map, { force: true, reason: "switch", skipHistory: true });

  const interactionState = {
    dragPan: map.dragPan && map.dragPan.isEnabled ? map.dragPan.isEnabled() : null,
    scrollZoom: map.scrollZoom && map.scrollZoom.isEnabled ? map.scrollZoom.isEnabled() : null,
    doubleClickZoom: map.doubleClickZoom && map.doubleClickZoom.isEnabled ? map.doubleClickZoom.isEnabled() : null,
    touchZoomRotate: map.touchZoomRotate && map.touchZoomRotate.isEnabled ? map.touchZoomRotate.isEnabled() : null,
  };
  try { if (map.dragPan) map.dragPan.disable(); } catch {}
  try { if (map.scrollZoom) map.scrollZoom.disable(); } catch {}
  try { if (map.doubleClickZoom) map.doubleClickZoom.disable(); } catch {}
  try { if (map.touchZoomRotate) map.touchZoomRotate.disable(); } catch {}

  let postData = { ...(post || {}) };

  // Debug: log post data to see if content_type is present
  console.log('[openCenteredOverlay] postData:', {
    id: postData.id,
    content_type: postData.content_type,
    source: postData.source,
    title: postData.title?.substring(0, 30)
  });

  // Camera and video posts use the regular modal with video content
  const isCamera = postData.content_type === 'camera';
  const isVideo = postData.content_type === 'video';
  if (isCamera) {
    console.log('[openCenteredOverlay] Opening camera as regular post with video');
  }
  if (isVideo) {
    console.log('[openCenteredOverlay] Opening saved video post:', postData.video_url);
  }

  const data = adaptPostToTemplateData(postData, { size: "full" });
  const cat = normCatLabel(postData);
  const templateKey = getTemplateKeyForPost(postData);
  const relTime = formatRelativeTime(getPostTime(postData));
  const author = (postData?.author || postData?.Author || "").toString().trim();
  // Check if current user is the author
  let currentUsername = "";
  let authToken = "";
  try {
    const auth = loadAuth();
    authToken = auth?.access_token || "";
    // Username comes from JWT token, not stored directly
    currentUsername = guessUsernameFromToken(authToken) || "";
  } catch {}
  const isAuthor = !!(currentUsername && author && currentUsername.toLowerCase() === author.toLowerCase());
  const sub = (postData?.sub_category || postData?.subCategory || "").toString().trim();
  const metaLeft = author ? `by ${author}` : "";
  const metaRight = sub ? sub : "";
  const heroGallery = collectGalleryImages(postData);
  const img = heroGallery[0] || heroImageForPost(postData);
  const heroFullSrc = heroGallery[0] || fullImageForPost(postData);
  const heroHasGallery = heroGallery.length > 1;
  const truth = truthScore(postData);
  const url = data?.url || "";
  const postID = postData?.id || postData?.ID || postData?.post_id || 0;
  const viewCount = readCount(postData, ["view_count", "views", "viewCount"]);
  const likeCount = readCount(postData, ["like_count", "likes", "likeCount"]);
  const shareCount = readCount(postData, ["share_count", "shares", "shareCount"]);
  const commentCount = readCount(postData, ["comment_count", "comments", "commentCount"]);
  const isCluster = isClusterMainPost(postData);
  const clusterItems = Array.isArray(postData?.cluster_items) ? postData.cluster_items : [];
  const clusterCount = Array.isArray(postData?.cluster_members) ? postData.cluster_members.length : 0;
  const tagPills = renderTagPills(postData?.event_tags || postData?.tags);
  const heatmapPill = HEATMAP_ENABLED
    ? `<div class="sw-stat-pill sw-heatmap-pill" data-action="cluster-heatmap">🔥 Heat map</div>`
    : "";
  trackEvent("post_open", {
    post_id: postID,
    source: postData?.source || postData?.Source || "",
    category: postData?.category || postData?.Category || "",
    context: "map",
    fullscreen: !!fullScreen,
  });

  // Backdrop catches outside click to close
  const backdrop = document.createElement("div");
  try { backdrop.classList.add("sw-centered-backdrop","sw-enter"); } catch {}
  backdrop.classList.add("sw-centered-backdrop");
  backdrop.className = "sw-centered-backdrop";
  if (fullScreen) backdrop.classList.add("sw-modal-fullscreen");
  backdrop.style.position = "fixed";
  backdrop.style.inset = "0";
  backdrop.style.zIndex = "5000";
  backdrop.style.background = "rgba(0,0,0,0.08)";
  backdrop.style.backdropFilter = "blur(1px)";
  backdrop.style.display = "flex";
  backdrop.style.alignItems = "center";
  backdrop.style.justifyContent = "center";
  backdrop.style.padding = "12px";
  backdrop.style.pointerEvents = "auto";

  // Wrapper uses existing styling hooks
  const modalWrap = document.createElement("div");
  modalWrap.classList.add("sw-centered-modal");
  modalWrap.className = "post-pin--expanded sw-centered-modal";
  if (fullScreen) modalWrap.classList.add("sw-modal-fullscreen");
  modalWrap.style.pointerEvents = "auto";

  // Keep size behavior from your CSS (max-width/min(92vw,...), max-height etc.)
  if (isCluster) {
    modalWrap.innerHTML = `
      <div class="post-card sw-expanded-shell sw-modal sw-cluster-modal" data-template="${escapeHtml(templateKey)}" data-category="${escapeHtml((postData?.category || "").toString().toUpperCase())}">
        <div class="sw-modal-toprow">
          <div class="sw-modal-left">
            <div class="sw-modal-badge">MAIN NEWS</div>
            <div class="sw-modal-meta">
              ${author ? `<div class="sw-modal-author-avatar" data-username="${escapeHtml(author)}"></div>` : ""}
              ${metaLeft ? `<span class="sw-author-link" data-author="${escapeHtml(author)}" style="cursor: pointer; text-decoration: underline;">${escapeHtml(metaLeft)}</span>` : ""}
              ${metaRight ? `<span>• ${escapeHtml(metaRight)}</span>` : ""}
              <span>• ${escapeHtml(relTime)}</span>
            </div>
          </div>

          <button class="sw-modal-x" type="button" aria-label="Close">✕</button>
        </div>

        <div class="sw-cluster-title">${escapeHtml(postData?.cluster_title || postData?.title || "Untitled")}</div>
        ${tagPills}

        <div class="sw-cluster-body">
          <div class="sw-cluster-left">
            <div class="sw-cluster-hero">
              <div class="sw-truth-badge" data-score="${truth}" data-post-id="${postID}" style="
                position: absolute;
                top: 12px;
                left: 12px;
                z-index: 10;
                display: flex;
                flex-direction: column;
                align-items: center;
                gap: 6px;
                padding: 8px 6px;
                background: rgba(15, 23, 42, 0.7);
                border-radius: 12px;
                backdrop-filter: blur(8px);
              ">
                <button class="sw-truth-vote-btn sw-truth-upvote" type="button" data-vote="true" style="
                  width: 28px;
                  height: 28px;
                  border-radius: 50%;
                  border: 1px solid rgba(52, 211, 153, 0.4);
                  background: rgba(52, 211, 153, 0.15);
                  color: #34d399;
                  cursor: pointer;
                  display: flex;
                  align-items: center;
                  justify-content: center;
                  transition: all 0.2s ease;
                  padding: 0;
                ">
                  <i class="fa-solid fa-thumbs-up" style="font-size: 11px;"></i>
                </button>
                <div class="sw-truth-badge-score" style="
                  width: 42px;
                  height: 42px;
                  border-radius: 50%;
                  background: ${truthColor(truth)};
                  display: flex;
                  align-items: center;
                  justify-content: center;
                  font-size: 16px;
                  font-weight: 700;
                  color: #fff;
                  box-shadow: 0 2px 8px rgba(0,0,0,0.3);
                ">${truth}</div>
                <button class="sw-truth-vote-btn sw-truth-downvote" type="button" data-vote="false" style="
                  width: 28px;
                  height: 28px;
                  border-radius: 50%;
                  border: 1px solid rgba(239, 68, 68, 0.4);
                  background: rgba(239, 68, 68, 0.15);
                  color: #f87171;
                  cursor: pointer;
                  display: flex;
                  align-items: center;
                  justify-content: center;
                  transition: all 0.2s ease;
                  padding: 0;
                ">
                  <i class="fa-solid fa-thumbs-down" style="font-size: 11px;"></i>
                </button>
                <span style="font-size: 8px; color: rgba(255,255,255,0.5); text-transform: uppercase; letter-spacing: 0.5px;">Truth</span>
              </div>
              <div class="sw-cluster-hero-media">
                ${
                  img
                    ? `<img src="${escapeHtml(img)}" data-fullsrc="${escapeHtml(fullImageForPost(postData))}" alt="" loading="lazy" />`
                    : `<div class="sw-modal-hero-fallback"><div class="sw-modal-hero-fallback-inner"><i class="${categoryIconClass(postData)}"></i><span>${escapeHtml(normCatLabel(postData))}</span></div></div>`
                }
              </div>
            </div>
            <div class="sw-cluster-actions">
              <button class="post-card-btn" type="button" data-action="like">Like</button>
              <button class="post-card-btn" type="button" data-action="share">Share</button>
              <button class="post-card-btn" type="button">Fix</button>
            </div>
          </div>

          <div class="sw-cluster-right">
            <div class="sw-cluster-related-title">Related reports ${clusterCount ? `(${clusterCount})` : ""}</div>
            <div class="sw-cluster-related-list">
              ${buildClusterItemsHtml(clusterItems)}
            </div>
            <div class="sw-cluster-generated">Generated news update</div>
            <div class="sw-cluster-summary">
              ${escapeHtml(data?.summary || postData?.snippet || "")}
            </div>
          </div>
        </div>

        <div class="sw-stat-row">
          <div class="sw-stat-pill" data-stat="views">👁 <span class="sw-stat-num">${formatCount(viewCount)}</span></div>
          <div class="sw-stat-pill" data-stat="likes">❤️ <span class="sw-stat-num">${formatCount(likeCount)}</span></div>
          <div class="sw-stat-pill" data-stat="shares">🔁 <span class="sw-stat-num">${formatCount(shareCount)}</span></div>
          <div class="sw-stat-pill" data-stat="comments">💬 <span class="sw-stat-num">${formatCount(commentCount)}</span></div>
          ${heatmapPill}
        </div>

        <div class="post-card-comments sw-livechat">
          <div class="sw-livechat-title">Live chat / comments</div>
          <div class="sw-livechat-body">
            <div class="sw-chat-line">— loading…</div>
          </div>
          <div class="sw-livechat-inputrow">
            <input class="sw-livechat-input" placeholder="Write a comment…" />
            <button class="sw-livechat-post" type="button">POST</button>
          </div>
        </div>

      </div>
    `;
  } else {
    modalWrap.innerHTML = `
      <div class="post-card sw-expanded-shell sw-modal" data-template="${escapeHtml(templateKey)}" data-category="${escapeHtml((postData?.category || "").toString().toUpperCase())}">
        <div class="sw-modal-toprow">
          <div class="sw-modal-left">
            ${isCamera ? `
            <div class="sw-modal-badge" style="background:linear-gradient(135deg,#dc2626,#ef4444);display:flex;align-items:center;gap:0.4rem;">
              <span style="width:6px;height:6px;border-radius:50%;background:#fff;animation:pulse 1.5s infinite;"></span>
              LIVE CAMERA
            </div>
            ` : `
            <div class="sw-modal-badge">${escapeHtml(cat)}</div>
            `}
            <div class="sw-modal-meta">
              ${author ? `<div class="sw-modal-author-avatar" data-username="${escapeHtml(author)}"></div>` : ""}
              ${metaLeft ? `<span class="sw-author-link" data-author="${escapeHtml(author)}" style="cursor: pointer; text-decoration: underline;">${escapeHtml(metaLeft)}</span>` : ""}
              ${metaRight ? `<span>• ${escapeHtml(metaRight)}</span>` : ""}
              <span>• ${escapeHtml(relTime)}</span>
            </div>
          </div>

          <button class="sw-modal-x" type="button" aria-label="Close">✕</button>
        </div>

        <div class="sw-modal-hero-row">
          ${!isCamera ? `
          <div class="sw-truth-badge" data-score="${truth}" data-post-id="${postID}" style="
            display: flex;
            flex-direction: column;
            align-items: center;
            gap: 6px;
            padding: 8px 6px;
            background: rgba(15, 23, 42, 0.7);
            border-radius: 12px;
            backdrop-filter: blur(8px);
          ">
            <button class="sw-truth-vote-btn sw-truth-upvote" type="button" data-vote="true" style="
              width: 28px;
              height: 28px;
              border-radius: 50%;
              border: 1px solid rgba(52, 211, 153, 0.4);
              background: rgba(52, 211, 153, 0.15);
              color: #34d399;
              cursor: pointer;
              display: flex;
              align-items: center;
              justify-content: center;
              transition: all 0.2s ease;
              padding: 0;
            ">
              <i class="fa-solid fa-thumbs-up" style="font-size: 11px;"></i>
            </button>
            <div class="sw-truth-badge-score" style="
              width: 42px;
              height: 42px;
              border-radius: 50%;
              background: ${truthColor(truth)};
              display: flex;
              align-items: center;
              justify-content: center;
              font-size: 16px;
              font-weight: 700;
              color: #fff;
              box-shadow: 0 2px 8px rgba(0,0,0,0.3);
            ">${truth}</div>
            <button class="sw-truth-vote-btn sw-truth-downvote" type="button" data-vote="false" style="
              width: 28px;
              height: 28px;
              border-radius: 50%;
              border: 1px solid rgba(239, 68, 68, 0.4);
              background: rgba(239, 68, 68, 0.15);
              color: #f87171;
              cursor: pointer;
              display: flex;
              align-items: center;
              justify-content: center;
              transition: all 0.2s ease;
              padding: 0;
            ">
              <i class="fa-solid fa-thumbs-down" style="font-size: 11px;"></i>
            </button>
            <span style="font-size: 8px; color: rgba(255,255,255,0.5); text-transform: uppercase; letter-spacing: 0.5px;">Truth</span>
          </div>
          ` : ''}
          <div class="sw-modal-hero" ${(isCamera || isVideo) ? 'style="flex:1;max-width:100%;"' : ''}>
            ${isCamera ? `
              <div class="sw-camera-hero" style="position:relative;width:100%;padding-top:56.25%;background:#000;border-radius:12px;overflow:hidden;">
                <video
                  controls
                  autoplay
                  muted
                  playsinline
                  loop
                  style="position:absolute;inset:0;width:100%;height:100%;object-fit:contain;background:#000;"
                  src="${escapeHtml(getCameraStreamUrl(postData))}"
                >
                  <source src="${escapeHtml(getCameraStreamUrl(postData))}" type="video/mp4">
                </video>
                <div class="sw-camera-live-badge" style="position:absolute;top:12px;left:12px;display:flex;align-items:center;gap:6px;padding:4px 10px;background:rgba(220,38,38,0.9);border-radius:4px;font-size:11px;font-weight:700;color:#fff;text-transform:uppercase;">
                  <span style="width:6px;height:6px;border-radius:50%;background:#fff;animation:pulse 1.5s infinite;"></span>
                  LIVE
                </div>
              </div>
            ` : isVideo ? `
              <div class="sw-video-hero" style="position:relative;width:100%;padding-top:56.25%;background:#000;border-radius:12px;overflow:hidden;">
                <video
                  controls
                  playsinline
                  style="position:absolute;inset:0;width:100%;height:100%;object-fit:contain;background:#000;"
                  src="${escapeHtml(normalizeMediaUrl(postData.video_url || ''))}"
                >
                  <source src="${escapeHtml(normalizeMediaUrl(postData.video_url || ''))}" type="video/mp4">
                </video>
                <div class="sw-video-badge" style="position:absolute;top:12px;left:12px;display:flex;align-items:center;gap:6px;padding:4px 10px;background:rgba(59,130,246,0.9);border-radius:4px;font-size:11px;font-weight:700;color:#fff;text-transform:uppercase;">
                  <i class="fa-solid fa-video"></i>
                  RECORDED
                </div>
              </div>
            ` : img
                ? `<img src="${escapeHtml(img)}" data-fullsrc="${escapeHtml(heroFullSrc)}" alt="" loading="lazy" data-gallery-index="0" />${heroHasGallery ? `<button class="sw-modal-hero-arrow left" type="button" aria-label="Previous image"><i class="fa-solid fa-chevron-left"></i></button><button class="sw-modal-hero-arrow right" type="button" aria-label="Next image"><i class="fa-solid fa-chevron-right"></i></button><div class="sw-modal-gallery-indicator" aria-live="polite"><span class="sw-modal-gallery-current">1</span><span class="sw-modal-gallery-separator">/</span><span class="sw-modal-gallery-total">${heroGallery.length}</span></div>` : ''}`
                : `<div class="sw-modal-hero-fallback"><div class="sw-modal-hero-fallback-inner"><i class="${categoryIconClass(postData)}"></i><span>${escapeHtml(normCatLabel(postData))}</span></div></div>`
            }
          </div>
        </div>
        <div class="sw-modal-title">${escapeHtml(data?.headline || postData?.title || "Untitled")}</div>
        ${tagPills}

        <div class="sw-modal-summary">
          ${escapeHtml(data?.summary || postData?.snippet || "")}
        </div>

        <div class="sw-stat-row">
          <div class="sw-stat-pill" data-stat="views">👁 <span class="sw-stat-num">${formatCount(viewCount)}</span></div>
          <div class="sw-stat-pill" data-stat="likes">❤️ <span class="sw-stat-num">${formatCount(likeCount)}</span></div>
          <div class="sw-stat-pill" data-stat="shares">🔁 <span class="sw-stat-num">${formatCount(shareCount)}</span></div>
          <div class="sw-stat-pill" data-stat="comments">💬 <span class="sw-stat-num">${formatCount(commentCount)}</span></div>
          ${heatmapPill}
        </div>

        <div class="sw-modal-cta-row">
          ${isCamera ? `
          <a class="sw-cta-primary sw-cta-camera" href="${escapeHtml(normalizeMediaUrl(postData.video_url || postData.embed_url || ''))}" target="_blank" rel="noopener" style="display:inline-flex;align-items:center;gap:0.5rem;text-decoration:none;background:linear-gradient(135deg,#dc2626,#ef4444);">
            <i class="fa-solid fa-video"></i> Watch Live Stream
          </a>
          ` : url ? `
          <a class="sw-cta-primary" href="${escapeHtml(url)}" target="_blank" rel="noopener" style="display:inline-flex;align-items:center;gap:0.5rem;text-decoration:none;">
            <i class="fa-solid fa-external-link"></i> Open source
          </a>
          ` : ``}

          <div class="sw-cta-actions">
            ${!isCamera ? `<button class="post-card-btn" type="button">Contact</button>` : ''}
            <button class="post-card-btn" type="button" data-action="like">Like</button>
            <button class="post-card-btn" type="button" data-action="share">Share</button>
            ${!isCamera ? `<button class="post-card-btn" type="button">Fix</button>` : ''}
          </div>
        </div>

        ${isAuthor ? `
        <div class="sw-modal-author-actions" style="display:flex;gap:0.5rem;flex-wrap:wrap;padding:0.5rem 0;border-top:1px solid rgba(255,255,255,0.1);margin-top:0.5rem;">
          <button class="post-card-btn sw-author-btn" type="button" data-action="edit-post" style="background:rgba(59,130,246,0.2);border-color:rgba(59,130,246,0.4);">
            <i class="fa-solid fa-pen"></i> Edit
          </button>
          <button class="post-card-btn sw-author-btn" type="button" data-action="visibility" style="background:rgba(139,92,246,0.2);border-color:rgba(139,92,246,0.4);">
            <i class="fa-solid fa-lock"></i> Visibility
          </button>
          <button class="post-card-btn sw-author-btn sw-danger-btn" type="button" data-action="delete-post" style="background:rgba(239,68,68,0.2);border-color:rgba(239,68,68,0.4);color:#fca5a5;">
            <i class="fa-solid fa-trash"></i> Delete
          </button>
        </div>
        <div class="sw-modal-edit-panel" style="display:none;padding:0.75rem;background:rgba(15,23,42,0.8);border-radius:8px;margin-top:0.5rem;">
          <input type="text" class="sw-edit-title" placeholder="Title" style="width:100%;padding:0.5rem;background:rgba(30,41,59,0.8);border:1px solid rgba(148,163,184,0.3);border-radius:6px;color:#e2e8f0;margin-bottom:0.5rem;" />
          <textarea class="sw-edit-snippet" placeholder="Description" rows="3" style="width:100%;padding:0.5rem;background:rgba(30,41,59,0.8);border:1px solid rgba(148,163,184,0.3);border-radius:6px;color:#e2e8f0;resize:vertical;"></textarea>
          <div style="display:flex;gap:0.5rem;margin-top:0.5rem;">
            <button class="post-card-btn" type="button" data-action="save-edit" style="background:rgba(34,197,94,0.3);border-color:rgba(34,197,94,0.5);">Save</button>
            <button class="post-card-btn" type="button" data-action="cancel-edit">Cancel</button>
          </div>
        </div>
        <div class="sw-modal-visibility-panel" style="display:none;padding:0.75rem;background:rgba(15,23,42,0.8);border-radius:8px;margin-top:0.5rem;">
          <label style="display:block;margin-bottom:0.5rem;color:#94a3b8;font-size:0.85rem;">Post Visibility</label>
          <select class="sw-visibility-select" style="width:100%;padding:0.5rem;background:rgba(30,41,59,0.8);border:1px solid rgba(148,163,184,0.3);border-radius:6px;color:#e2e8f0;">
            <option value="public">Public</option>
            <option value="friends">Friends Only</option>
            <option value="private">Private (Only Me)</option>
            <option value="group">Group</option>
            <option value="custom">Custom</option>
          </select>
          <input type="text" class="sw-visibility-group" placeholder="Group ID (if Group selected)" style="display:none;width:100%;padding:0.5rem;background:rgba(30,41,59,0.8);border:1px solid rgba(148,163,184,0.3);border-radius:6px;color:#e2e8f0;margin-top:0.5rem;" />
          <input type="text" class="sw-visibility-users" placeholder="Usernames (comma separated, if Custom)" style="display:none;width:100%;padding:0.5rem;background:rgba(30,41,59,0.8);border:1px solid rgba(148,163,184,0.3);border-radius:6px;color:#e2e8f0;margin-top:0.5rem;" />
          <label style="display:flex;align-items:center;gap:0.5rem;margin-top:0.5rem;color:#94a3b8;font-size:0.85rem;">
            <input type="checkbox" class="sw-visibility-apply-images" checked /> Apply to images too
          </label>
          <div style="display:flex;gap:0.5rem;margin-top:0.5rem;">
            <button class="post-card-btn" type="button" data-action="save-visibility" style="background:rgba(34,197,94,0.3);border-color:rgba(34,197,94,0.5);">Save</button>
            <button class="post-card-btn" type="button" data-action="cancel-visibility">Cancel</button>
          </div>
        </div>
        <div class="sw-modal-delete-confirm" style="display:none;padding:0.75rem;background:rgba(127,29,29,0.5);border-radius:8px;margin-top:0.5rem;text-align:center;">
          <p style="color:#fca5a5;margin-bottom:0.5rem;">Are you sure you want to delete this post?</p>
          <div style="display:flex;gap:0.5rem;justify-content:center;">
            <button class="post-card-btn sw-danger-btn" type="button" data-action="confirm-delete" style="background:rgba(239,68,68,0.4);border-color:rgba(239,68,68,0.6);color:#fca5a5;">Yes, Delete</button>
            <button class="post-card-btn" type="button" data-action="cancel-delete">Cancel</button>
          </div>
        </div>
        ` : ''}

        <div class="post-card-comments sw-livechat">
          <div class="sw-livechat-title">Live chat / comments</div>
          <div class="sw-livechat-body">
            <div class="sw-chat-line">— loading…</div>
          </div>
          <div class="sw-livechat-inputrow">
            <input class="sw-livechat-input" placeholder="Write a comment…" />
            <button class="sw-livechat-post" type="button">POST</button>
          </div>
        </div>

      </div>
    `;
  }

  bindHeroLightbox(modalWrap);
  if (heroHasGallery) {
    initModalGallery(modalWrap, heroGallery);
  }

  backdrop.appendChild(modalWrap);
  document.body.appendChild(backdrop);
  hydrateAvatars(modalWrap);
  const relatedLinks = modalWrap.querySelectorAll("[data-related-url]");
  relatedLinks.forEach((el) => {
    el.addEventListener("click", () => {
      const target = el.getAttribute("data-related-url");
      if (!target) return;
      try {
        window.open(target, "_blank", "noopener");
      } catch {
        window.location.href = target;
      }
    });
  });
  // Animate in using CSS classes only (no inline transitions that persist)
  requestAnimationFrame(() => {
    backdrop.classList.add("sw-open");
    modalWrap.classList.add("sw-open");
    // After animation completes, remove any inline transition to prevent jumps
    setTimeout(() => {
      try {
        modalWrap.style.transition = "none";
        backdrop.style.transition = "none";
      } catch {}
    }, 350);
  });

  // outside click closes (ignore drag/scroll on map)
  let backdropDown = null;
  const trackDown = (e) => {
    backdropDown = {
      x: e.clientX || 0,
      y: e.clientY || 0,
      t: Date.now(),
    };
  };
  const shouldCloseBackdrop = (e) => {
    if (map?.__swCenteredOverlay?.openedAt) {
      if (Date.now() - map.__swCenteredOverlay.openedAt < 350) return false;
    }
    if (!backdropDown) return true;
    const dx = (e.clientX || 0) - backdropDown.x;
    const dy = (e.clientY || 0) - backdropDown.y;
    const dist = Math.hypot(dx, dy);
    const dt = Date.now() - backdropDown.t;
    return dist < 8 && dt < 700;
  };
  backdrop.addEventListener("pointerdown", trackDown);
  backdrop.addEventListener("mousedown", trackDown);
  backdrop.addEventListener("touchstart", (e) => {
    const touch = e.touches && e.touches[0];
    if (!touch) return;
    trackDown({ clientX: touch.clientX, clientY: touch.clientY });
  }, { passive: true });
  backdrop.addEventListener("click", (e) => {
    if (!shouldCloseBackdrop(e)) return;
    closeCenteredOverlay(map, { force: true });
  });
  // inside click doesn't close
  const stopPropagation = (e) => e.stopPropagation();
  modalWrap.addEventListener("click", stopPropagation);
  modalWrap.addEventListener("pointerdown", stopPropagation);
  modalWrap.addEventListener("mousedown", stopPropagation);
  modalWrap.addEventListener("touchstart", stopPropagation, { passive: true });

  // close button
  const xbtn = modalWrap.querySelector(".sw-modal-x");
  if (xbtn) xbtn.addEventListener("click", () => closeCenteredOverlay(map, { force: true }));

  // ESC closes
  const onKey = (e) => {
    if (e.key === "Escape") closeCenteredOverlay(map, { force: true });
  };
  window.addEventListener("keydown", onKey);

  // Back button support WITHOUT changing URL
  // We push a fake history state so back button closes the modal
  let onPop = null;
  let historyPushed = false;
  try {
    // Only push history state without changing URL
    window.history.pushState({ swModal: true, postId: postID }, "");
    historyPushed = true;

    onPop = (e) => {
      // Check if we're returning to the modal state (from image) or actually closing
      const state = e.state || window.history.state;

      // If state still has swModal=true, we're just returning from image to post
      // Don't close the post modal
      if (state && state.swModal === true && state.postId === postID) {
        return; // Stay in post modal
      }

      // Otherwise, we're closing the modal
      const ov = map.__swCenteredOverlay;
      if (ov && ov.postId === postID) {
        ov.historyClosed = true;
        closeCenteredOverlay(map, { skipHistory: true, force: true });
      }
    };
    window.addEventListener("popstate", onPop);
  } catch {}

  // CTA open url
  const cta = modalWrap.querySelector(".sw-cta-primary");
  if (cta) {
    cta.addEventListener("click", () => {
      const u = cta.getAttribute("data-url") || "";
      if (!u) return;
      try { window.open(u, "_blank", "noopener,noreferrer"); } catch {}
    });
  }

  if (postID > 0 && !modalWrap.__swViewed) {
    modalWrap.__swViewed = true;
    recordPostView(postID).then((res) => {
      const counts = res?.counts;
      if (counts) {
        updateStat(modalWrap, "views", counts.view_count);
        updateStat(modalWrap, "likes", counts.like_count);
        updateStat(modalWrap, "shares", counts.share_count);
        updateStat(modalWrap, "comments", counts.comment_count);
      }
    });
  }

  let liked = false;
  const likeBtn = modalWrap.querySelector('[data-action="like"]');
  const commentInput = modalWrap.querySelector(".sw-livechat-input");
  const commentBtn = modalWrap.querySelector(".sw-livechat-post");

  const applyAuthUI = () => {
    const authed = isAuthed();
    if (likeBtn) {
      likeBtn.disabled = false;
      likeBtn.textContent = authed && liked ? "Liked" : "Like";
    }
    if (commentInput) {
      commentInput.readOnly = !authed;
      commentInput.placeholder = authed ? "Write a comment…" : "Sign in to comment";
    }
  };

  // Fetch initial like state
  if (likeBtn && postID > 0 && isAuthed()) {
    fetchPostLikeState(postID).then((res) => {
      if (res?.ok) {
        liked = !!res.liked;
        likeBtn.textContent = liked ? "Liked" : "Like";
      }
    });
  }

  if (likeBtn && postID > 0) {
    likeBtn.addEventListener("click", async () => {
      if (!isAuthed()) {
        requestAuth("Please sign in or create a free account to like this post.");
        return;
      }
      trackEvent("post_like_click", { post_id: postID, context: "map" });
      likeBtn.disabled = true;
      const res = await togglePostLike(postID, !liked);
      if (res?.ok) {
        liked = !!res.liked;
        likeBtn.textContent = liked ? "Liked" : "Like";
        const counts = res?.counts;
        if (counts) {
          updateStat(modalWrap, "likes", counts.like_count);
          updateStat(modalWrap, "shares", counts.share_count);
          updateStat(modalWrap, "comments", counts.comment_count);
          updateStat(modalWrap, "views", counts.view_count);
        }
      }
      likeBtn.disabled = false;
    });
  }

  const shareBtn = modalWrap.querySelector('[data-action="share"]');
  if (shareBtn && postID > 0) {
    shareBtn.addEventListener("click", async () => {
      const shareUrl = `${window.location.origin}/p/${postID}`;
      const shareTitle = postData?.title || postData?.Title || "SocioWire";
      trackEvent("post_share_click", { post_id: postID, context: "map" });
      try {
        if (navigator.share) {
          await navigator.share({ title: shareTitle, url: shareUrl });
        } else if (navigator.clipboard) {
          await navigator.clipboard.writeText(shareUrl);
        }
      } catch {}
      const res = await recordPostShare(postID);
      const counts = res?.counts;
      if (counts) {
        updateStat(modalWrap, "views", counts.view_count);
        updateStat(modalWrap, "shares", counts.share_count);
      }
    });
  }

  // Truth vote handlers (thumbs up/down in truth badge)
  const truthBadgeNew = modalWrap.querySelector('.sw-truth-badge');
  if (truthBadgeNew && postID > 0) {
    const downvoteBtn = truthBadgeNew.querySelector('.sw-truth-downvote');
    const upvoteBtn = truthBadgeNew.querySelector('.sw-truth-upvote');
    const scoreDisplay = truthBadgeNew.querySelector('.sw-truth-badge-score');
    let currentVote = null;
    let isVoting = false;

    const updateButtonStyles = () => {
      if (upvoteBtn) {
        upvoteBtn.style.background = currentVote === 'true' ? 'rgba(52, 211, 153, 0.5)' : 'rgba(52, 211, 153, 0.15)';
        upvoteBtn.style.borderColor = currentVote === 'true' ? 'rgba(52, 211, 153, 0.8)' : 'rgba(52, 211, 153, 0.4)';
      }
      if (downvoteBtn) {
        downvoteBtn.style.background = currentVote === 'false' ? 'rgba(239, 68, 68, 0.5)' : 'rgba(239, 68, 68, 0.15)';
        downvoteBtn.style.borderColor = currentVote === 'false' ? 'rgba(239, 68, 68, 0.8)' : 'rgba(239, 68, 68, 0.4)';
      }
    };

    const updateTruthUI = (newScore) => {
      if (scoreDisplay && Number.isFinite(newScore)) {
        const score = Math.round(newScore);
        scoreDisplay.textContent = String(score);
        scoreDisplay.style.background = truthColor(score);
      }
    };

    const doTruthVote = async (vote) => {
      if (!isAuthed()) {
        requestAuth("Please sign in or create a free account to vote on truth.");
        return;
      }
      // Skip if already voted this way
      if (currentVote === vote) return;
      if (isVoting) return;
      isVoting = true;

      // Visual feedback
      if (vote === 'true' && upvoteBtn) {
        upvoteBtn.style.transform = 'scale(1.2)';
      } else if (vote === 'false' && downvoteBtn) {
        downvoteBtn.style.transform = 'scale(1.2)';
      }

      try {
        trackEvent("truth_vote_click", { post_id: postID, vote, context: "map" });
        const stats = await votePostTruth(postID, vote);
        if (stats) {
          currentVote = vote;
          updateButtonStyles();
          if (stats.truth_score !== null && Number.isFinite(stats.truth_score)) {
            updateTruthUI(stats.truth_score);
          }
        }
      } catch (err) {
        console.error('Truth vote error:', err);
      }

      // Reset transform
      setTimeout(() => {
        if (upvoteBtn) upvoteBtn.style.transform = '';
        if (downvoteBtn) downvoteBtn.style.transform = '';
        isVoting = false;
      }, 200);
    };

    // Fetch initial vote state
    if (isAuthed()) {
      fetchPostTruth(postID).then((stats) => {
        if (stats?.user_vote) {
          currentVote = stats.user_vote.toString();
          updateButtonStyles();
        }
      }).catch(() => {});
    }

    if (downvoteBtn) {
      downvoteBtn.addEventListener('click', (e) => {
        e.stopPropagation();
        doTruthVote('false');
      });
    }
    if (upvoteBtn) {
      upvoteBtn.addEventListener('click', (e) => {
        e.stopPropagation();
        doTruthVote('true');
      });
    }
  }

  // Author action handlers (edit, visibility, delete)
  if (isAuthor && postID > 0) {
    const editBtn = modalWrap.querySelector('[data-action="edit-post"]');
    const visibilityBtn = modalWrap.querySelector('[data-action="visibility"]');
    const deleteBtn = modalWrap.querySelector('[data-action="delete-post"]');
    const editPanel = modalWrap.querySelector('.sw-modal-edit-panel');
    const visibilityPanel = modalWrap.querySelector('.sw-modal-visibility-panel');
    const deleteConfirm = modalWrap.querySelector('.sw-modal-delete-confirm');
    const editTitleInput = modalWrap.querySelector('.sw-edit-title');
    const editSnippetInput = modalWrap.querySelector('.sw-edit-snippet');
    const visibilitySelect = modalWrap.querySelector('.sw-visibility-select');
    const visibilityGroup = modalWrap.querySelector('.sw-visibility-group');
    const visibilityUsers = modalWrap.querySelector('.sw-visibility-users');
    const visibilityApplyImages = modalWrap.querySelector('.sw-visibility-apply-images');

    const hideAllPanels = () => {
      if (editPanel) editPanel.style.display = 'none';
      if (visibilityPanel) visibilityPanel.style.display = 'none';
      if (deleteConfirm) deleteConfirm.style.display = 'none';
    };

    // Edit button
    if (editBtn && editPanel) {
      editBtn.addEventListener('click', () => {
        hideAllPanels();
        editTitleInput.value = postData?.title || '';
        editSnippetInput.value = postData?.snippet || '';
        editPanel.style.display = 'block';
      });
    }

    // Save edit
    const saveEditBtn = modalWrap.querySelector('[data-action="save-edit"]');
    if (saveEditBtn) {
      saveEditBtn.addEventListener('click', async () => {
        saveEditBtn.disabled = true;
        saveEditBtn.textContent = 'Saving...';
        try {
          const res = await editPost(postID, {
            title: editTitleInput.value.trim(),
            snippet: editSnippetInput.value.trim(),
          }, authToken);
          if (res?.ok) {
            // Update local postData
            postData.title = editTitleInput.value.trim();
            postData.snippet = editSnippetInput.value.trim();
            // Update modal display
            const titleEl = modalWrap.querySelector('.sw-modal-title');
            const summaryEl = modalWrap.querySelector('.sw-modal-summary');
            if (titleEl) titleEl.textContent = postData.title;
            if (summaryEl) summaryEl.textContent = postData.snippet;
            hideAllPanels();
            // Dispatch update event
            try {
              window.dispatchEvent(new CustomEvent('live-post-updated', { detail: res }));
            } catch {}
          }
        } catch (err) {
          console.error('Edit post error:', err);
        }
        saveEditBtn.disabled = false;
        saveEditBtn.textContent = 'Save';
      });
    }

    // Cancel edit
    const cancelEditBtn = modalWrap.querySelector('[data-action="cancel-edit"]');
    if (cancelEditBtn) {
      cancelEditBtn.addEventListener('click', hideAllPanels);
    }

    // Visibility button
    if (visibilityBtn && visibilityPanel) {
      visibilityBtn.addEventListener('click', () => {
        hideAllPanels();
        visibilitySelect.value = postData?.visibility || 'public';
        visibilityGroup.value = postData?.group_id || '';
        visibilityUsers.value = '';
        // Show/hide conditional inputs
        visibilityGroup.style.display = visibilitySelect.value === 'group' ? 'block' : 'none';
        visibilityUsers.style.display = visibilitySelect.value === 'custom' ? 'block' : 'none';
        visibilityPanel.style.display = 'block';
      });
    }

    // Visibility select change
    if (visibilitySelect) {
      visibilitySelect.addEventListener('change', () => {
        visibilityGroup.style.display = visibilitySelect.value === 'group' ? 'block' : 'none';
        visibilityUsers.style.display = visibilitySelect.value === 'custom' ? 'block' : 'none';
      });
    }

    // Save visibility
    const saveVisibilityBtn = modalWrap.querySelector('[data-action="save-visibility"]');
    if (saveVisibilityBtn) {
      saveVisibilityBtn.addEventListener('click', async () => {
        saveVisibilityBtn.disabled = true;
        saveVisibilityBtn.textContent = 'Saving...';
        try {
          const payload = {
            visibility: visibilitySelect.value,
            group_id: visibilityGroup.value.trim(),
            users: visibilityUsers.value.split(',').map(u => u.trim()).filter(Boolean),
          };
          if (visibilityApplyImages?.checked) {
            payload.image_visibility = visibilitySelect.value;
            payload.image_group_id = visibilityGroup.value.trim();
            payload.image_users = payload.users;
          }
          await updatePostVisibility(postID, payload, authToken);
          postData.visibility = visibilitySelect.value;
          hideAllPanels();
        } catch (err) {
          console.error('Update visibility error:', err);
        }
        saveVisibilityBtn.disabled = false;
        saveVisibilityBtn.textContent = 'Save';
      });
    }

    // Cancel visibility
    const cancelVisibilityBtn = modalWrap.querySelector('[data-action="cancel-visibility"]');
    if (cancelVisibilityBtn) {
      cancelVisibilityBtn.addEventListener('click', hideAllPanels);
    }

    // Delete button
    if (deleteBtn && deleteConfirm) {
      deleteBtn.addEventListener('click', () => {
        hideAllPanels();
        deleteConfirm.style.display = 'block';
      });
    }

    // Confirm delete
    const confirmDeleteBtn = modalWrap.querySelector('[data-action="confirm-delete"]');
    if (confirmDeleteBtn) {
      confirmDeleteBtn.addEventListener('click', async () => {
        if (!postID || postID <= 0) {
          alert('Cannot delete: invalid post ID');
          return;
        }
        confirmDeleteBtn.disabled = true;
        confirmDeleteBtn.textContent = 'Deleting...';
        try {
          const res = await deletePost(postID, authToken);
          if (res?.ok) {
            // Force remove backdrop and close modal
            try {
              const backdrop = document.querySelector('.sw-centered-backdrop');
              if (backdrop) backdrop.remove();
            } catch {}
            closeCenteredOverlay(map, { force: true });
            // Then dispatch delete event
            try {
              window.dispatchEvent(new CustomEvent('live-post-deleted', { detail: { post_id: postID } }));
            } catch {}
          } else {
            alert(res?.error || 'Failed to delete post. Please try again.');
            confirmDeleteBtn.disabled = false;
            confirmDeleteBtn.textContent = 'Yes, Delete';
          }
        } catch (err) {
          alert('Error deleting post: ' + (err?.message || 'Unknown error'));
          confirmDeleteBtn.disabled = false;
          confirmDeleteBtn.textContent = 'Yes, Delete';
        }
      });
    }

    // Cancel delete
    const cancelDeleteBtn = modalWrap.querySelector('[data-action="cancel-delete"]');
    if (cancelDeleteBtn) {
      cancelDeleteBtn.addEventListener('click', hideAllPanels);
    }
  }

  const heatBtn = modalWrap.querySelector('[data-action="cluster-heatmap"]');
  if (HEATMAP_ENABLED && heatBtn && isCluster) {
    heatBtn.addEventListener("click", () => {
      closeCenteredOverlay(map, { force: true });
      dispatchClusterHeatmap(postData, { mode: "click" });
    });
  }

  // Only refetch if initial data is missing critical fields
  const hasCompleteData = postData?.title && (postData?.published_at || postData?.created_at);
  if (postID > 0 && !hasCompleteData) {
    fetchPostById(postID).then((fresh) => {
      if (!fresh) return;
      postData = { ...postData, ...fresh };
      applyPostDetails(modalWrap, postData);
      refreshMarkerFromPost(map, postID, postData);
    });
  }

  const commentsBody = modalWrap.querySelector(".sw-livechat-body");
  if (commentsBody && postID > 0) {
    fetchPostComments(postID, 20).then((items) => renderComments(commentsBody, items));
  }
  if (commentBtn && commentInput && postID > 0) {
    commentBtn.addEventListener("click", async () => {
      if (!isAuthed()) {
        requestAuth("Please sign in or create a free account to comment.");
        return;
      }
      const body = (commentInput.value || "").trim();
      if (!body) return;
      trackEvent("post_comment_submit", { post_id: postID, context: "map" });
      commentBtn.disabled = true;
      const res = await createPostComment(postID, body);
      if (res?.ok) {
        commentInput.value = "";
        const existing = await fetchPostComments(postID, 20);
        renderComments(commentsBody, existing);
        const counts = res?.counts;
        if (counts) {
          updateStat(modalWrap, "comments", counts.comment_count);
          updateStat(modalWrap, "likes", counts.like_count);
          updateStat(modalWrap, "shares", counts.share_count);
          updateStat(modalWrap, "views", counts.view_count);
        }
      }
      commentBtn.disabled = false;
    });
    commentInput.addEventListener("focus", () => {
      if (!isAuthed()) {
        requestAuth("Please sign in or create a free account to comment.");
      }
    });
  }

  if (postID > 0 && isAuthed()) {
    fetchPostLikeState(postID).then((res) => {
      if (res?.ok && res.liked) {
        liked = true;
        if (likeBtn) likeBtn.textContent = "Liked";
      }
    });
  }

  // Old badge click handler removed - using thumbs up/down buttons instead

  applyAuthUI();
  const onAuth = () => {
    if (postID > 0 && isAuthed()) {
      fetchPostLikeState(postID).then((res) => {
        if (res?.ok && res.liked) {
          liked = true;
        } else {
          liked = false;
        }
        applyAuthUI();
      });
    } else {
      liked = false;
      applyAuthUI();
    }
  };
  window.addEventListener("sociowire:auth-changed", onAuth);

  let unmountFull = null;

  map.__swCenteredOverlay = {
    postId: postData?.id,
    modalWrapRef: modalWrap,
    backdrop,
    interactionState,
    onKey,
    onPop,
    onAuth,
    unmountFull,
    unmountMini: null,
    historyPushed,
    historyClosed: false,
    openedAt: Date.now(),
  };
}

function hashPostId(id) {
  // Simple hash function for stable offset calculation
  const str = String(id);
  let hash = 0;
  for (let i = 0; i < str.length; i++) {
    const char = str.charCodeAt(i);
    hash = ((hash << 5) - hash) + char;
    hash = hash & hash; // Convert to 32bit integer
  }
  return Math.abs(hash);
}

function calculateSmartOffset(post, markersRef, map) {
  if (!map || !markersRef.current) return { x: 0, y: 0 };

  const lat = typeof post.lat === "number" ? post.lat : typeof post.latitude === "number" ? post.latitude : null;
  const lon = typeof post.lon === "number" ? post.lon : typeof post.lng === "number" ? post.lng : null;
  if (lat == null || lon == null) return { x: 0, y: 0 };

  // Si le post est très récent (< 10 secondes), offset vers le HAUT pour compenser l'anchor bottom
  const createdAt = post?.created_at || post?.CreatedAt || post?.createdAt;
  if (createdAt) {
    const ageSeconds = (Date.now() - new Date(createdAt).getTime()) / 1000;
    if (ageSeconds < 10) {
      return { x: 0, y: -110 }; // Offset vers le haut (négatif = monte)
    }
  }

  const currentPoint = map.project([lon, lat]);
  const PROXIMITY_THRESHOLD = 100; // pixels - larger to detect more overlaps

  // Check distance in geographic coordinates (more stable across zoom levels)
  const nearbyCount = markersRef.current.filter(marker => {
    if (!marker.post || marker.post.id === post.id) return false;

    const mLat = typeof marker.post.lat === "number" ? marker.post.lat : marker.post.latitude;
    const mLon = typeof marker.post.lon === "number" ? marker.post.lon : marker.post.lng;
    if (mLat == null || mLon == null) return false;

    // Geographic distance (rough approximation)
    const latDiff = Math.abs(lat - mLat);
    const lonDiff = Math.abs(lon - mLon);
    const geoDist = Math.sqrt(latDiff * latDiff + lonDiff * lonDiff);

    // Small threshold for geographic clustering (~100m at equator)
    return geoDist < 0.001;
  }).length;

  if (nearbyCount === 0) return { x: 0, y: 0 };

  // Use deterministic hash-based offset so it's stable across zoom/pan
  const hash = hashPostId(post.id);
  const angle = (hash % 8) * 45 * (Math.PI / 180); // 8 positions, 45° apart
  const offsetMagnitude = 25 + ((hash % 3) * 15); // 25, 40, or 55 pixels

  return {
    x: Math.cos(angle) * offsetMagnitude,
    y: Math.sin(angle) * offsetMagnitude
  };
}

export function createMarkerForPost(post, mapRef, markersRef, expandedElRef, theme = "blue") {
  const map = mapRef.current;
  if (!map) return;

  const lat =
    typeof post.lat === "number"
      ? post.lat
      : typeof post.latitude === "number"
      ? post.latitude
      : null;

  const lon =
    typeof post.lon === "number"
      ? post.lon
      : typeof post.lng === "number"
      ? post.lng
      : null;

  if (lat == null || lon == null) return;

  const lngLat = [lon, lat];

  // Calculate smart offset for nearby markers
  // DISABLED: Using geographic coordinate variation instead of pixel offset
  // const offset = calculateSmartOffset(post, markersRef, map);
  const offset = { x: 0, y: 0 };

  const root = document.createElement("div");
  root.classList.add("sw-appear");
  requestAnimationFrame(() => root.classList.add("sw-appear-in"));
  root.className = "post-pin post-pin--compact";
  root.style.zIndex = "100";
  root.__post = post;

  function unmountIfAny() {
    if (root.__swUnmount) {
      try { root.__swUnmount(); } catch {}
      root.__swUnmount = null;
    }
  }

  function renderCompact() {
    if (!root.__swCompactReady) {
      root.className = "post-pin post-pin--compact sw-appear sw-appear-in";
      root.style.zIndex = "100";

      // Add connecting line if offset is significant
      // DISABLED: No longer using pixel offsets
      const hasOffset = false;
      const lineHTML = '';

      root.innerHTML = `
        <div class="sw-template-mini-wrap"></div>
        <div class="post-pin-pointer-small"></div>
        ${lineHTML}
      `;
      root.__swCompactReady = true;
    }

    const wrap = root.querySelector(".sw-template-mini-wrap");
    if (wrap) {
      const activePost = root.__post || post;
      const spec = getTemplateSpecForPost(activePost, "mini");
      const data = adaptPostToTemplateData(activePost, { size: "mini" });
      root.__lastTemplateKey = getTemplateKeyForPost(activePost);
      root.__lastImage = data?.image || "";
      root.__lastTitle = data?.headline || "";
      root.__swUnmount = mountCard(wrap, spec, data, theme, activePost);
    }

    clearOcclusion(markersRef);
  }

  renderCompact();
  root.__renderCompact = renderCompact;

  // Bring marker to front on hover so card is above other markers (compact cards = 200)
  root.addEventListener("mouseenter", () => {
    root.style.zIndex = "200";
  });
  root.addEventListener("mouseleave", () => {
    root.style.zIndex = "100";
  });

  if (HEATMAP_ENABLED && isClusterMainPost(post)) {
    const handleEnter = () => {
      dispatchClusterHeatmap(root.__post || post, { mode: "hover", skipPan: true });
    };
    const handleLeave = () => {
      dispatchClusterHeatmapClear();
    };
    root.addEventListener("mouseenter", handleEnter);
    root.addEventListener("mouseleave", handleLeave);
    root.__swClusterHover = { handleEnter, handleLeave };
  }

  const marker = new maplibregl.Marker({
    element: root,
    anchor: "bottom",
    offset: [offset.x, offset.y]
  })
    .setLngLat(lngLat)
    .addTo(map);

  markersRef.current.push({ id: post.id, marker, el: root, post, type: "full" });

  root.addEventListener("click", (ev) => {
    ev.stopPropagation();

    // Use root.__post to get the most up-to-date post data (may have been refreshed)
    const currentPost = root.__post || post;

    const existing = map.__swCenteredOverlay;
    if (existing && existing.postId === currentPost?.id) {
      if (existing.openedAt && Date.now() - existing.openedAt < 800) return;
      closeCenteredOverlay(map, { force: true });
      return;
    }

    openCenteredOverlay({ map, post: currentPost, theme, fullScreen: false });
    if (expandedElRef) expandedElRef.current = null;
  });
}

/**
 * Weather marker - shows temperature and weather icon directly on map
 */
const WEATHER_ICONS = {
  sun: "☀️", moon: "🌙", "cloud-sun": "⛅", "cloud-moon": "☁️",
  cloud: "☁️", smog: "🌫️", "cloud-rain": "🌧️", "cloud-showers-heavy": "🌧️",
  snowflake: "❄️", "cloud-sun-rain": "🌦️", "cloud-meatball": "🌨️",
  bolt: "⛈️", "temperature-half": "🌡️"
};

function weatherCodeToIcon(code, isDay = true) {
  if (code === 0) return isDay ? "sun" : "moon";
  if (code <= 2) return isDay ? "cloud-sun" : "cloud-moon";
  if (code === 3) return "cloud";
  if (code <= 48) return "smog";
  if (code <= 67) return "cloud-rain";
  if (code <= 77) return "snowflake";
  if (code <= 82) return "cloud-sun-rain";
  if (code <= 86) return "cloud-meatball";
  if (code <= 99) return "bolt";
  return "temperature-half";
}

function parseWeatherFromTitle(title) {
  // Parse "☀️ Montreal: 5°C - Clear sky" or "⛅ Paris: 12°C - Partly cloudy"
  const match = title?.match(/:\s*(-?\d+)°C/);
  const temp = match ? parseInt(match[1], 10) : null;

  // Try to get city name
  const cityMatch = title?.match(/^[^\w]*([A-Za-z\s]+):/);
  const city = cityMatch ? cityMatch[1].trim() : "";

  // Detect weather from emoji
  let icon = "temperature-half";
  if (title?.includes("☀️")) icon = "sun";
  else if (title?.includes("🌙")) icon = "moon";
  else if (title?.includes("⛅")) icon = "cloud-sun";
  else if (title?.includes("☁️")) icon = "cloud";
  else if (title?.includes("🌫️")) icon = "smog";
  else if (title?.includes("🌧️")) icon = "cloud-rain";
  else if (title?.includes("❄️")) icon = "snowflake";
  else if (title?.includes("🌦️")) icon = "cloud-sun-rain";
  else if (title?.includes("🌨️")) icon = "cloud-meatball";
  else if (title?.includes("⛈️")) icon = "bolt";

  return { temp, city, icon };
}

export function createWeatherMarkerForPost(post, mapRef, markersRef, expandedElRef, theme = "blue") {
  try {
    const map = mapRef?.current;
    if (!map) return;

    const lat = typeof post?.lat === "number" ? post.lat : typeof post?.latitude === "number" ? post.latitude : null;
    const lon = typeof post?.lon === "number" ? post.lon : typeof post?.lng === "number" ? post.lng : null;
    if (lat == null || lon == null) return;

    const lngLat = [lon, lat];
    const { temp, city, icon } = parseWeatherFromTitle(post?.title || "");
    const emoji = WEATHER_ICONS[icon] || "🌡️";
    const tempDisplay = temp !== null ? `${temp}°` : "";
    const cityDisplay = city || post?.city || "";

    const root = document.createElement("div");
    root.className = "sw-weather-pill sw-appear";
    root.style.zIndex = "5";
    root.style.cursor = "pointer";
    root.__post = post;

    requestAnimationFrame(() => {
      try { root.classList.add("sw-appear-in"); } catch {}
    });

    root.innerHTML = `
      <div style="
        display: flex;
        align-items: center;
        gap: 3px;
        padding: 4px 8px;
        background: rgba(14, 165, 233, 0.75);
        border-radius: 12px;
        border: 1px solid rgba(255,255,255,0.4);
        box-shadow: 0 2px 6px rgba(0,0,0,0.25);
      ">
        <span style="font-size:12px;line-height:1;">${emoji}</span>
        <span style="font-size:11px;font-weight:600;color:#fff;">${tempDisplay}</span>
      </div>
    `;

    // Click to open weather modal
    root.addEventListener("click", (e) => {
      e.stopPropagation();
      try {
        window.dispatchEvent(new CustomEvent("weather-open", { detail: post }));
      } catch {}
    });

    // Hover effect - subtle
    root.addEventListener("mouseenter", () => {
      try {
        root.style.opacity = "1";
      } catch {}
    });
    root.addEventListener("mouseleave", () => {
      try {
        root.style.opacity = "";
      } catch {}
    });

    const marker = new window.maplibregl.Marker({ element: root, anchor: "bottom" })
      .setLngLat(lngLat)
      .addTo(map);

    const id = post?.id || post?.ID || `weather-${Date.now()}-${Math.random().toString(36).slice(2)}`;
    if (!Array.isArray(markersRef.current)) markersRef.current = [];

    // Remove existing marker with same id
    const existing = markersRef.current.findIndex((m) => m?.id === id);
    if (existing >= 0) {
      try { markersRef.current[existing]?.marker?.remove?.(); } catch {}
      markersRef.current.splice(existing, 1);
    }

    // Add with same structure as other markers (type, el, marker, post, id)
    markersRef.current.push({
      id,
      marker,
      el: root,
      post: root.__post,
      type: "weather"
    });
  } catch (err) {
    console.warn("createWeatherMarkerForPost error:", err);
  }
}

/**
 * Crée un marqueur simple (juste icône circulaire) pour les posts secondaires
 * S'ouvre en modal au clic, affiche mini-carte au hover
 */
export function createSimpleMarkerForPost(post, mapRef, markersRef, expandedElRef, theme = "blue") {
  const map = mapRef.current;
  if (!map) return;

  const lat =
    typeof post.lat === "number"
      ? post.lat
      : typeof post.latitude === "number"
      ? post.latitude
      : null;

  const lon =
    typeof post.lon === "number"
      ? post.lon
      : typeof post.lng === "number"
      ? post.lng
      : null;

  if (lat == null || lon == null) return;

  const lngLat = [lon, lat];

  // Pas d'offset pour les marqueurs simples (ils sont petits)
  const root = document.createElement("div");
  root.classList.add("sw-appear");
  requestAnimationFrame(() => root.classList.add("sw-appear-in"));
  const isWeatherPost = post?.content_type === 'weather';
  root.className = "post-pin--simple sw-appear sw-appear-in";
  root.style.cursor = "pointer";
  root.style.display = "flex";
  root.style.flexDirection = "column";
  root.style.alignItems = "center";
  root.style.zIndex = isWeatherPost ? "10" : "2";
  root.__post = post;

  function renderSimple() {
    const activePost = root.__post || post;
    const isCamera = activePost?.content_type === 'camera';
    const isVideo = activePost?.content_type === 'video';
    const isLive = activePost?.content_type === 'live';
    const isWeather = activePost?.content_type === 'weather';
    const liveColor = 'linear-gradient(135deg, rgba(220,38,38,0.95), rgba(239,68,68,0.95))';
    const livePin = 'rgba(220,38,38,0.95)';
    const weatherColor = 'linear-gradient(135deg, #0ea5e9 0%, #3b82f6 100%)';
    const weatherPin = '#3b82f6';
    const color = isLive
      ? liveColor
      : (isCamera || isVideo)
      ? 'linear-gradient(135deg, rgba(56, 189, 248, 0.9), rgba(59, 130, 246, 0.9))'
      : isWeather
      ? weatherColor
      : getMarkerColorForCategory(activePost?.category);
    const pinColor = isLive
      ? livePin
      : (isCamera || isVideo)
      ? 'rgba(56, 189, 248, 0.9)'
      : isWeather
      ? weatherPin
      : getMarkerColorForCategory(activePost?.category);
    const img = normalizeImageUrl(activePost?.image_small || "");
    root.__lastTemplateKey = getTemplateKeyForPost(activePost);
    root.__lastImage = img || "";
    root.__lastTitle = (activePost?.title || "").toString();

    // Weather emoji based on weather_icon or title
    let weatherEmoji = '🌡️';
    if (isWeather) {
      const icon = activePost?.weather_icon || '';
      const title = activePost?.title || '';
      if (icon === 'sun' || title.includes('☀️')) weatherEmoji = '☀️';
      else if (icon === 'moon' || title.includes('🌙')) weatherEmoji = '🌙';
      else if (icon === 'cloud-sun' || title.includes('⛅')) weatherEmoji = '⛅';
      else if (icon === 'cloud' || title.includes('☁️')) weatherEmoji = '☁️';
      else if (icon === 'smog' || title.includes('🌫️')) weatherEmoji = '🌫️';
      else if (icon.includes('rain') || title.includes('🌧️')) weatherEmoji = '🌧️';
      else if (icon === 'snowflake' || title.includes('❄️')) weatherEmoji = '❄️';
      else if (title.includes('🌦️')) weatherEmoji = '🌦️';
      else if (title.includes('🌨️')) weatherEmoji = '🌨️';
      else if (icon === 'bolt' || title.includes('⛈️')) weatherEmoji = '⛈️';
    }
    // Parse temp from title if not available: "☀️ Montreal: 5°C - Clear"
    let weatherTemp = '';
    if (isWeather) {
      if (activePost?.temperature != null) {
        weatherTemp = `${Math.round(activePost.temperature)}°`;
      } else {
        const titleMatch = (activePost?.title || '').match(/:\s*(-?\d+)°/);
        if (titleMatch) weatherTemp = `${titleMatch[1]}°`;
      }
    }

    // Weather city name for label
    const weatherCity = isWeather ? (activePost?.city || (activePost?.title || '').split(':')[0].replace(/^[^\w\s]+/, '').trim()) : '';

    // Camera/video thumbnail URL
    const mediaThumbnail = (isCamera || isVideo) ? (activePost?.thumbnail_url || activePost?.image_small || '') : '';

    // Camera pin - square thumbnail with red LED that pulses (bigger)
    // Video pin - square thumbnail with play button overlay (bigger, no red LED)
    root.innerHTML = isCamera ? `
        <div style="position:relative;">
          <div style="
            width: 64px;
            height: 48px;
            border-radius: 6px;
            background: #0ea5e9;
            border: 2px solid #0ea5e9;
            box-shadow: 0 3px 10px rgba(0,0,0,0.4);
            overflow: hidden;
          ">
            ${mediaThumbnail ? `<img src="${mediaThumbnail}" style="width:100%;height:100%;object-fit:cover;" onerror="this.style.display='none';" />` : ``}
          </div>
          <div style="
            position: absolute;
            top: -6px;
            right: -6px;
            width: 14px;
            height: 14px;
            border-radius: 50%;
            background: #ef4444;
            border: 2px solid #fff;
            box-shadow: 0 0 10px #ef4444;
            animation: pulse 1.5s infinite;
          "></div>
          <div style="
            width: 0;
            height: 0;
            border-left: 8px solid transparent;
            border-right: 8px solid transparent;
            border-top: 12px solid #0ea5e9;
            margin: -2px auto 0;
          "></div>
        </div>
    ` : isVideo ? `
        <div style="position:relative;">
          <div style="
            width: 64px;
            height: 48px;
            border-radius: 6px;
            background: #0ea5e9;
            border: 2px solid #0ea5e9;
            box-shadow: 0 3px 10px rgba(0,0,0,0.4);
            overflow: hidden;
            position: relative;
          ">
            ${mediaThumbnail ? `<img src="${mediaThumbnail}" style="width:100%;height:100%;object-fit:cover;" onerror="this.style.display='none';" />` : ``}
            <div style="
              position: absolute;
              top: 50%;
              left: 50%;
              transform: translate(-50%, -50%);
              width: 26px;
              height: 26px;
              border-radius: 50%;
              background: rgba(14, 165, 233, 0.9);
              display: flex;
              align-items: center;
              justify-content: center;
              box-shadow: 0 2px 8px rgba(0,0,0,0.35);
            ">
              <i class="fa-solid fa-play" style="font-size:10px;color:#fff;margin-left:2px;"></i>
            </div>
          </div>
          <div style="
            width: 0;
            height: 0;
            border-left: 8px solid transparent;
            border-right: 8px solid transparent;
            border-top: 12px solid #0ea5e9;
            margin: -2px auto 0;
          "></div>
        </div>
    ` : `
        <div class="sw-simple-marker ${isLive ? 'sw-live-marker' : ''} ${isWeather ? 'sw-weather-marker' : ''}" style="
          ${isWeather ? 'width: auto; height: auto; padding: 4px 8px; border-radius: 12px;' : 'width: 32px; height: 32px; border-radius: 50%;'}
          background: ${isWeather ? 'rgba(14, 165, 233, 0.75)' : color};
          border: ${isWeather ? '1px solid rgba(255,255,255,0.4)' : isLive ? '2px solid rgba(255,255,255,0.9)' : '2px solid white'};
          box-shadow: 0 2px 6px rgba(0,0,0,0.25);
          display: flex;
          align-items: center;
          justify-content: center;
          ${isWeather ? 'gap: 3px;' : ''}
          overflow: hidden;
          transition: transform 0.2s ease, box-shadow 0.2s ease;
        ">
          ${isWeather ? `
            <span style="font-size:12px;line-height:1;">${weatherEmoji}</span>
            <span style="font-size:11px;font-weight:600;color:#fff;">${weatherTemp}</span>
          ` : isLive ? `<i class="fa-solid fa-broadcast-tower" style="font-size:14px;color:#fff;"></i>` : img ? `<img src="${img}" alt="" style="width:100%;height:100%;object-fit:cover;" />` : `
            <div style="
              width: 12px;
              height: 12px;
              border-radius: 50%;
              background: white;
              opacity: 0.9;
            "></div>
          `}
        </div>
        ${!isWeather ? `<div class="sw-simple-pin-point" style="
          width: 0;
          height: 0;
          border-left: 8px solid transparent;
          border-right: 8px solid transparent;
          border-top: 16px solid ${pinColor};
          filter: drop-shadow(0 2px 4px rgba(0,0,0,0.3));
          margin-top: -2px;
          z-index: 1;
        "></div>` : ''}
    `;

    // Add hover card for all marker types
    root.innerHTML += `
      <div class="sw-simple-hover-card" style="
        position: absolute;
        bottom: 100%;
        left: 50%;
        transform: translateX(-50%) translateY(-6px) scale(0.9);
        opacity: 0;
        pointer-events: none;
        transition: opacity 0.2s ease, transform 0.2s ease;
        z-index: 1000;
        min-width: 200px;
        max-width: 280px;
        background: rgba(8, 20, 40, 0.97);
        backdrop-filter: blur(10px);
        border-radius: 12px;
        padding: 10px 12px;
        border: 1px solid rgba(90, 190, 255, 0.7);
        box-shadow: 0 4px 20px rgba(0,0,0,0.5);
        white-space: normal;
      ">
        ${isCamera ? `<div style="display:inline-flex;align-items:center;gap:4px;padding:2px 6px;margin-bottom:4px;border-radius:999px;background:rgba(239,68,68,0.85);color:#fff;font-size:9px;font-weight:800;">
          <span style="width:5px;height:5px;border-radius:50%;background:#fff;"></span>LIVE
        </div>` : isWeather ? `<div style="display:inline-flex;align-items:center;gap:4px;padding:2px 6px;margin-bottom:4px;border-radius:999px;background:linear-gradient(135deg,#0ea5e9,#3b82f6);color:#fff;font-size:9px;font-weight:800;">
          ${weatherEmoji} WEATHER
        </div>` : ""}
        <div style="font-weight: 900; font-size: 12px; color: #e8f5ff; margin-bottom: 4px;">
          ${escapeHtml(activePost?.title || "Untitled")}
        </div>
        <div style="font-size: 10px; color: rgba(232, 245, 255, 0.8);">
          ${escapeHtml((activePost?.snippet || "").substring(0, 80))}${(activePost?.snippet || "").length > 80 ? "..." : ""}
        </div>
        <div style="margin-top: 6px; font-size: 9px; color: rgba(232, 245, 255, 0.6);">
          ${isCamera ? `<i class="fa-solid fa-video" style="margin-right:4px;"></i>` : isWeather ? `${weatherEmoji} ` : ""}${normCatLabel(activePost)} • ${isCamera ? (activePost?.region || "Quebec") : isWeather ? (activePost?.city || '') : formatRelativeTime(activePost?.created_at || activePost?.CreatedAt)}
        </div>
      </div>
    `;
  }

  renderSimple();
  root.__renderSimple = renderSimple;

  const isCluster = HEATMAP_ENABLED && isClusterMainPost(post);
  const markerEl = root.querySelector(".sw-simple-marker");
  const hoverCard = root.querySelector(".sw-simple-hover-card");

  // Hover effects - bring marker to front so hover card is above other markers (simple pins = 150)
  root.addEventListener("mouseenter", () => {
    root.style.zIndex = "150";
    if (isCluster) {
      dispatchClusterHeatmap(root.__post || post, { mode: "hover", skipPan: true });
    }
    if (markerEl) {
      markerEl.style.transform = "scale(1.2)";
      markerEl.style.boxShadow = "0 4px 16px rgba(0,0,0,0.5)";
    }
    if (hoverCard) {
      hoverCard.style.opacity = "1";
      hoverCard.style.transform = "translateX(-50%) translateY(-6px) scale(1)";
    }
  });

  root.addEventListener("mouseleave", () => {
    root.style.zIndex = isWeatherPost ? "10" : "2";
    if (isCluster) {
      dispatchClusterHeatmapClear();
    }
    if (markerEl) {
      markerEl.style.transform = "scale(1)";
      markerEl.style.boxShadow = "0 2px 8px rgba(0,0,0,0.3)";
    }
    if (hoverCard) {
      hoverCard.style.opacity = "0";
      hoverCard.style.transform = "translateX(-50%) translateY(-6px) scale(0.9)";
    }
  });

  // Click ouvre le modal
  root.addEventListener("click", (ev) => {
    ev.stopPropagation();

    // Use root.__post to get the most up-to-date post data (may have been refreshed)
    const currentPost = root.__post || post;

    // Debug logging
    console.log('[SimpleMarker click] currentPost:', {
      id: currentPost?.id,
      content_type: currentPost?.content_type,
      source: currentPost?.source,
      hasRootPost: !!root.__post,
      rootPostContentType: root.__post?.content_type
    });

    const existing = map.__swCenteredOverlay;
    if (existing && existing.postId === currentPost?.id) {
      if (existing.openedAt && Date.now() - existing.openedAt < 800) return;
      closeCenteredOverlay(map, { force: true });
      return;
    }

    openCenteredOverlay({ map, post: currentPost, theme, fullScreen: false });
    if (expandedElRef) expandedElRef.current = null;
  });

  const marker = new maplibregl.Marker({
    element: root,
    anchor: "bottom", // La pointe du pin est à la position exacte
  })
    .setLngLat(lngLat)
    .addTo(map);

  markersRef.current.push({ id: post.id, marker, el: root, post, type: "simple" });
}

/**
 * ISLAND MARKER: Creates a circular avatar marker for user islands
 * Islands are displayed with avatar, username, and 🏝️ badge
 */
export function createIslandMarker(island, mapRef, markersRef, onIslandClick, theme = "blue") {
  const map = mapRef.current;
  if (!map) return;

  const lat = island.virtual_y;  // Virtual coords
  const lon = island.virtual_x;

  if (lat == null || lon == null) {
    console.warn("[createIslandMarker] Missing virtual coordinates", island);
    return;
  }

  const root = document.createElement("div");
  root.classList.add("island-marker", "sw-appear");

  // Fade-in animation
  requestAnimationFrame(() => root.classList.add("sw-appear-in"));

  // Marker circulaire avec avatar
  root.innerHTML = `
    <div class="island-marker-container" style="
      display: flex;
      flex-direction: column;
      align-items: center;
      position: relative;
      cursor: pointer;
    ">
      <div class="island-avatar" style="
        width: 80px;
        height: 80px;
        border-radius: 50%;
        background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
        border: 3px solid white;
        box-shadow: 0 4px 12px rgba(0,0,0,0.4);
        display: flex;
        align-items: center;
        justify-content: center;
        overflow: hidden;
        transition: transform 0.2s ease, box-shadow 0.2s ease;
      ">
        ${island.avatar_url
          ? `<img src="${toSmallAvatarUrl(island.avatar_url)}" style="width:100%;height:100%;object-fit:cover;" alt="${island.username}" />`
          : `<div style="color:white;font-size:32px;font-weight:bold;">${island.username[0].toUpperCase()}</div>`
        }
      </div>
      <div class="island-badge" style="
        position: absolute;
        top: -5px;
        left: 50%;
        transform: translateX(-50%);
        background: rgba(102, 126, 234, 0.95);
        color: white;
        padding: 2px 8px;
        border-radius: 10px;
        font-size: 10px;
        font-weight: bold;
        letter-spacing: 0.5px;
        box-shadow: 0 2px 6px rgba(0,0,0,0.3);
      ">🏝️ ISLAND</div>
      <div class="island-username" style="
        margin-top: 8px;
        text-align: center;
        font-weight: bold;
        color: white;
        font-size: 12px;
        text-shadow: 0 2px 4px rgba(0,0,0,0.6);
      ">@${island.username}</div>
      <div class="island-pin-point" style="
        width: 0;
        height: 0;
        border-left: 10px solid transparent;
        border-right: 10px solid transparent;
        border-top: 20px solid rgba(102, 126, 234, 0.8);
        margin: 0 auto;
        margin-top: 4px;
        filter: drop-shadow(0 2px 4px rgba(0,0,0,0.3));
      "></div>
    </div>
  `;

  // Hover effect
  const avatar = root.querySelector(".island-avatar");
  root.addEventListener("mouseenter", () => {
    if (avatar) {
      avatar.style.transform = "scale(1.1)";
      avatar.style.boxShadow = "0 6px 20px rgba(102, 126, 234, 0.6)";
    }
  });

  root.addEventListener("mouseleave", () => {
    if (avatar) {
      avatar.style.transform = "scale(1)";
      avatar.style.boxShadow = "0 4px 12px rgba(0,0,0,0.4)";
    }
  });

  // Click handler
  root.addEventListener("click", (ev) => {
    ev.stopPropagation();
    console.log("[Island Marker] Clicked:", island.username, island.id);
    if (onIslandClick) {
      onIslandClick(island);
    }
  });

  const marker = new maplibregl.Marker({
    element: root,
    anchor: "bottom",
  })
    .setLngLat([lon, lat])
    .addTo(map);

  markersRef.current.push({
    id: island.id,
    marker,
    el: root,
    island,
    type: "island"
  });

  console.log("[Island Marker] Created for", island.username, "at", [lon, lat]);
}
