// app.js — Dashboard completo de Skoop
'use strict';

// ── ESTADO GLOBAL ─────────────────────────────────────────
const State = {
  communities: [],
  slug: null,
  stats: null,
  members: [],
  feed: [],
  endpoints: [],
  page: 'overview',
  memberFilter: '',
  memberSearch: '',
  memberSort: 'risk',
  memberPage: 0,
  PAGE_SIZE: 25,
  selectedEmailMember: null,
  emailTone: 'warm',
  // Equipo (Fase 5): estado de atención compartido
  collab: null,
  team: { states: {}, notes: {}, labels: [], roster: [] },
  attentionFilter: 'all',
};

// ── INIT ──────────────────────────────────────────────────
document.addEventListener('DOMContentLoaded', async () => {
  setupNav();
  setupToolbar();
  setupMembersTable();
  setupFeed();
  setupEmails();
  setupSentimentTab();
  setupPlanTab();
  setupCalendarTab();
  setupModal();
  setupClickDelegation();
  setupAttentionFilter();
  await loadPlan();
  await loadCollab();
  await revalidateLicense();
  await loadData();
  // Cargar sentimiento guardado para que las razones/prioridad lo usen de entrada.
  await loadStoredSentiment().catch(() => {});
});

// ── PLAN / FREEMIUM ───────────────────────────────────────
// Sin llave válida = modo gratis (data al 15% + IA bloqueada). La llave
// (gratis para F100K o de pago) la valida /api/validate y desbloquea todo.
const FREE_RATIO = 0.15;

async function loadPlan() {
  const s = await chrome.storage.local.get('skoop_license').catch(() => ({}));
  State.license = s.skoop_license || null;
  State.plan = (State.license && State.license.valid) ? (State.license.plan || 'pro') : 'free';
  renderFreeBanner();
}

function isPro() { return State.plan && State.plan !== 'free'; }

// Cuántos elementos ve un usuario gratis (15%, mínimo 5).
function freeCap(n) { return Math.max(5, Math.ceil((n || 0) * FREE_RATIO)); }

// Corta una lista al 15% en modo gratis. Devuelve { list, hidden, capped }.
function freeGate(arr) {
  const a = arr || [];
  if (isPro()) return { list: a, hidden: 0, capped: false };
  const cap = freeCap(a.length);
  if (cap >= a.length) return { list: a, hidden: 0, capped: false };
  return { list: a.slice(0, cap), hidden: a.length - cap, capped: true };
}

// Si es gratis, abre el upsell y devuelve false (corta la acción de IA).
function requirePro() {
  if (isPro()) return true;
  openUpsellModal();
  return false;
}

// Banner fijo de "modo gratis" debajo de la toolbar.
function renderFreeBanner() {
  const existing = q('free-banner');
  if (isPro()) { if (existing) existing.remove(); return; }
  if (existing) return;
  const bar = document.createElement('div');
  bar.id = 'free-banner';
  bar.innerHTML = `
    <span>🔓 <strong>Modo gratis</strong> — estás viendo el ${Math.round(FREE_RATIO * 100)}% de tu comunidad y la IA está bloqueada.</span>
    <button class="btn btn-purple btn-sm" data-act="upsell">🔑 Desbloquear todo</button>`;
  const toolbar = document.querySelector('.toolbar');
  if (toolbar && toolbar.parentNode) toolbar.parentNode.insertBefore(bar, toolbar.nextSibling);
}

window.__skoopUpsell = () => openUpsellModal();

// Tarjeta de candado que se inyecta al final de una lista limitada.
function lockCardHTML(hidden, what) {
  return `
    <div class="lock-card" data-act="upsell">
      <div class="lock-emoji">🔒</div>
      <div>
        <div class="lock-title">+${fmt(hidden)} ${esc(what)} bloqueados</div>
        <div class="lock-sub">Estás en modo gratis. Desbloquea toda tu comunidad con tu llave.</div>
      </div>
      <button class="btn btn-purple btn-sm" data-act="upsell">Desbloquear</button>
    </div>`;
}

// Pantalla de bloqueo para tabs 100% premium (Sentimiento, Plan, Calendario, Emails).
function lockedScreenHTML(title, desc) {
  return `
    <div class="card" style="padding:32px;text-align:center;max-width:520px;margin:24px auto">
      <div style="font-size:40px;margin-bottom:8px">🔒</div>
      <div style="font-weight:800;font-size:18px;margin-bottom:8px">${esc(title)} es Pro</div>
      <div style="color:var(--text-muted);line-height:1.6;font-size:14px;margin-bottom:18px">${desc}</div>
      <button class="btn btn-purple" data-act="upsell">🔑 Desbloquear con tu llave</button>
      <div style="color:var(--text-dim);font-size:12px;margin-top:12px">¿Eres alumna de FÓRMULA 100K? Pide tu llave gratis.</div>
    </div>`;
}

// Modal de upsell con activación de llave EN EL MISMO dashboard.
function openUpsellModal() {
  q('modal-content').innerHTML = `
    <div class="modal-member-name" style="margin-bottom:6px">🔑 Desbloquea Skoop completo</div>
    <p style="color:var(--text-muted);font-size:13px;line-height:1.6;margin:0 0 14px">
      En modo gratis ves solo el ${Math.round(FREE_RATIO * 100)}% de tu comunidad y la IA está bloqueada. Con tu llave desbloqueas:
    </p>
    <ul style="color:var(--text);font-size:13px;line-height:1.8;margin:0 0 16px;padding-left:18px">
      <li>👥 <strong>Todos</strong> tus miembros y el pipeline completo</li>
      <li>🤖 IA: resúmenes, sentimiento, mapa de problemas, posts y DMs</li>
      <li>🗓 Plan de hoy, Calendario semanal y Emails con IA</li>
      <li>📊 Quién se va, a quién escribir y exportaciones</li>
    </ul>
    <div style="background:var(--surface2);border:1px solid var(--surface3);border-radius:10px;padding:14px">
      <div style="font-weight:700;font-size:13px;margin-bottom:8px">Activa tu llave</div>
      <input id="upsell-email" type="email" placeholder="Tu email" style="width:100%;box-sizing:border-box;background:var(--surface);color:var(--text);border:1px solid var(--surface3);border-radius:8px;padding:9px;font-size:13px;margin-bottom:8px">
      <input id="upsell-key" type="text" placeholder="SKOOP-XXXX-XXXX-XXXX" style="width:100%;box-sizing:border-box;background:var(--surface);color:var(--text);border:1px solid var(--surface3);border-radius:8px;padding:9px;font-size:13px;font-family:monospace;text-transform:uppercase;margin-bottom:10px">
      <button class="btn btn-purple" style="width:100%" data-act="activate">Desbloquear</button>
      <div id="upsell-feedback" style="font-size:12px;margin-top:8px;min-height:16px"></div>
      <div style="color:var(--text-dim);font-size:12px;margin-top:6px">¿Eres alumna de FÓRMULA 100K? Pide tu llave gratis en la comunidad.</div>
    </div>
    <div style="background:var(--surface2);border:1px solid var(--surface3);border-radius:10px;padding:14px;margin-top:12px">
      <div style="font-weight:700;font-size:13px;margin-bottom:4px">¿No eres alumna?</div>
      <div style="color:var(--text-muted);font-size:12px;margin-bottom:10px">Consigue tu llave Pro por <strong>$17/mes</strong>. Te llega al instante y la activas aquí mismo.</div>
      <a href="https://buy.stripe.com/14A4gz9HMcO3bu21sL4Rq01" target="_blank" rel="noopener noreferrer" class="btn btn-purple" style="width:100%;box-sizing:border-box;display:block;text-align:center;text-decoration:none">Comprar Pro · $17/mes</a>
    </div>
    <div style="margin-top:14px"><button class="btn btn-ghost" data-act="close-modal">Ahora no</button></div>`;
  q('member-modal-overlay').classList.remove('hidden');
}

window.__skoopActivate = async () => {
  const email = (q('upsell-email')?.value || '').trim();
  const key = (q('upsell-key')?.value || '').trim();
  const out = q('upsell-feedback');
  if (!key) { if (out) { out.textContent = 'Pega tu llave.'; out.style.color = 'var(--red)'; } return; }
  if (out) { out.textContent = 'Validando…'; out.style.color = 'var(--text-muted)'; }
  try {
    const deviceId = await getDeviceId();
    const label = deviceLabel(State.collab);
    const r = await fetch(AI_BASE + '/validate', {
      method: 'POST', headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ email, key, deviceId, label }),
    });
    const data = await r.json();
    if (data.valid) {
      await chrome.storage.local.set({ skoop_license: { valid: true, key: data.key, plan: data.plan, name: data.name, email, activatedAt: Date.now() } });
      if (out) { out.textContent = `✓ Desbloqueado — plan ${(data.plan || '').toUpperCase()}`; out.style.color = 'var(--green)'; }
      setTimeout(() => location.reload(), 800);
    } else {
      if (out) { out.textContent = data.error || 'Llave no válida.'; out.style.color = 'var(--red)'; }
    }
  } catch (e) {
    if (out) { out.textContent = 'Error de conexión. Revisa tu internet.'; out.style.color = 'var(--red)'; }
  }
};

// Re-valida la licencia guardada contra el backend (hace efectiva la revocación
// de dispositivos y la baja de llaves). Si el backend la rechaza, cae a modo gratis.
// Si NO hay red, no degrada (mantiene el acceso para no romper el uso offline).
async function revalidateLicense() {
  if (!State.license || !State.license.key) return;
  try {
    const deviceId = await getDeviceId();
    const r = await fetch(AI_BASE + '/validate', {
      method: 'POST', headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({
        email: State.license.email || '', key: State.license.key,
        deviceId, label: deviceLabel(State.collab),
      }),
    });
    const d = await r.json();
    if (!d.valid) {
      await chrome.storage.local.remove('skoop_license');
      State.license = null;
      State.plan = 'free';
    }
  } catch (_) { /* sin red: no degradar */ }
}

// ── COMUNICACIÓN CON SERVICE WORKER ──────────────────────
function sw(type, extra = {}) {
  return chrome.runtime.sendMessage({ type, ...extra });
}

// ── EQUIPO: ESTADO DE ATENCIÓN COMPARTIDO (Fase 5) ───────
// El estado vive en el backend (Supabase) por cuenta = la llave. Todos los
// colaboradores que comparten la llave de Andrea ven y editan lo mismo.
const ATTN = {
  none:      { dot: '#4a5568', emoji: '⚪', label: 'Sin atender' },
  attending: { dot: '#f59e0b', emoji: '🟡', label: 'Atendiéndose' },
  done:      { dot: '#10b981', emoji: '🟢', label: 'Atendido' },
};

async function loadCollab() {
  const s = await chrome.storage.local.get('skoop_collab').catch(() => ({}));
  State.collab = s.skoop_collab || null;
}

function teamKey() { return State.license && State.license.key ? State.license.key : null; }

async function teamPull(slug) {
  const key = teamKey();
  if (!key || !slug) return null;
  const r = await fetch(AI_BASE + '/team-pull', {
    method: 'POST', headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({ key, slug }),
  });
  if (!r.ok) throw new Error('pull ' + r.status);
  return r.json();
}

async function teamPush(payload) {
  const key = teamKey();
  if (!key) throw new Error('Sin llave');
  const r = await fetch(AI_BASE + '/team-push', {
    method: 'POST', headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({ key, slug: State.slug, ...payload }),
  });
  const data = await r.json().catch(() => ({}));
  if (!r.ok) throw new Error(data.error || ('push ' + r.status));
  return data;
}

// Baja el estado del equipo, lo cachea local y lo fusiona en los miembros.
async function mergeTeam(slug) {
  const EMPTY = { states: {}, notes: {}, labels: [], roster: [] };
  if (!isPro() || !slug) { State.team = EMPTY; annotateMembers(); return; }
  const cacheKey = 'skoop_team_' + slug;
  let data = null;
  try {
    data = await teamPull(slug);
    if (data) await chrome.storage.local.set({ [cacheKey]: data }).catch(() => {});
  } catch {
    const c = await chrome.storage.local.get(cacheKey).catch(() => ({}));
    data = c[cacheKey] || null;
  }
  State.team = data || EMPTY;
  annotateMembers();
}

// Anota cada miembro con su estado de atención (_attn/_attnBy/_attnAt/_labelIds).
function annotateMembers() {
  const states = State.team.states || {};
  for (const m of State.members) {
    const st = states[m.id] || states[String(m.id)];
    m._attn = st ? (st.status || 'none') : 'none';
    m._attnBy = st ? st.attendedBy : null;
    m._attnAt = st ? st.attendedAt : null;
    m._labelIds = st && Array.isArray(st.labelIds) ? st.labelIds : [];
  }
}

function attnDot(status) {
  const a = ATTN[status] || ATTN.none;
  return `<span class="attn-dot" title="${esc(a.label)}" style="background:${a.dot}"></span>`;
}

function initialsOf(name) {
  return String(name || '').trim().split(/\s+/).map((w) => w[0]).slice(0, 2).join('').toUpperCase();
}

// Asegura que el colaborador tenga nombre (lo pide una vez, lo suma al roster).
async function ensureCollab() {
  if (State.collab) return State.collab;
  const name = (prompt('¿Cómo te llamas? (para firmar lo que atiendas en el equipo)') || '').trim();
  if (!name) return null;
  await chrome.storage.local.set({ skoop_collab: name }).catch(() => {});
  State.collab = name;
  teamPush({ type: 'roster-add', name }).catch(() => {});
  return name;
}

function reopenMember(id) {
  const m = State.members.find((x) => String(x.id) === String(id));
  if (m) openMemberModal(m);
}

function refreshAttentionViews() {
  if (State.page === 'pipeline') renderPipeline();
  else if (State.page === 'members') renderMembersTable();
}

function setupAttentionFilter() {
  const bar = q('attn-filter-bar');
  if (!bar) return;
  bar.querySelectorAll('[data-attn]').forEach((btn) => {
    btn.addEventListener('click', () => {
      bar.querySelectorAll('[data-attn]').forEach((b) => b.classList.remove('active'));
      btn.classList.add('active');
      State.attentionFilter = btn.dataset.attn;
      renderPipeline();
    });
  });
}

// ── Acciones del equipo (expuestas a los onclick del modal) ──
window.setAttention = async (id, status) => {
  if (!requirePro()) return;
  const by = (status === 'none') ? null : await ensureCollab();
  if (status !== 'none' && !by) return; // canceló el prompt del nombre
  const prev = State.team.states[id] || State.team.states[String(id)] || { labelIds: [] };
  State.team.states[id] = {
    ...prev, status,
    attendedBy: by,
    attendedAt: status === 'none' ? null : new Date().toISOString(),
    labelIds: prev.labelIds || [],
  };
  const m = State.members.find((x) => String(x.id) === String(id));
  if (m) { m._attn = status; m._attnBy = by; m._attnAt = State.team.states[id].attendedAt; }
  reopenMember(id);
  refreshAttentionViews();
  try { await teamPush({ type: 'status', memberId: id, status, attendedBy: by }); toast('Estado actualizado ✓'); }
  catch (e) { toast('⚠ No se pudo sincronizar: ' + e.message); }
};

window.addTeamNote = async (id) => {
  if (!requirePro()) return;
  const ta = q('team-note-input');
  const text = (ta && ta.value ? ta.value : '').trim();
  if (!text) return;
  const author = (await ensureCollab()) || '';
  const note = { author, body: text, createdAt: new Date().toISOString() };
  (State.team.notes[id] = State.team.notes[id] || State.team.notes[String(id)] || []).push(note);
  reopenMember(id);
  try { await teamPush({ type: 'note', memberId: id, author, body: text }); }
  catch (e) { toast('⚠ No se pudo sincronizar la nota'); }
};

window.toggleLabel = async (id, labelId) => {
  if (!requirePro()) return;
  const st = State.team.states[id] = (State.team.states[id] || State.team.states[String(id)] || { status: 'none', labelIds: [] });
  st.labelIds = st.labelIds || [];
  const i = st.labelIds.indexOf(labelId);
  if (i >= 0) st.labelIds.splice(i, 1); else st.labelIds.push(labelId);
  const m = State.members.find((x) => String(x.id) === String(id));
  if (m) m._labelIds = st.labelIds.slice();
  reopenMember(id);
  refreshAttentionViews();
  try { await teamPush({ type: 'label-toggle', memberId: id, labelId }); }
  catch (e) { toast('⚠ No se pudo sincronizar la etiqueta'); }
};

window.createLabel = async (id) => {
  if (!requirePro()) return;
  const name = (prompt('Nombre de la etiqueta (ej. VIP, Renovar, Pagó):') || '').trim();
  if (!name) return;
  const label = { id: 'l' + Date.now().toString(36) + Math.random().toString(36).slice(2, 6), name, color: '#8B6FE8' };
  State.team.labels.push(label);
  try { await teamPush({ type: 'label-save', label }); }
  catch (e) { toast('⚠ No se pudo guardar la etiqueta'); }
  window.toggleLabel(id, label.id);
};

window.changeCollab = async () => {
  const name = (prompt('Tu nombre de colaborador:', State.collab || '') || '').trim();
  if (!name) return;
  await chrome.storage.local.set({ skoop_collab: name }).catch(() => {});
  State.collab = name;
  teamPush({ type: 'roster-add', name }).catch(() => {});
  if (window.__openMemberId) reopenMember(window.__openMemberId);
};

// ── CARGA DE DATOS ────────────────────────────────────────
async function loadData() {
  const communities = await sw('GET_COMMUNITIES');
  State.communities = communities || [];

  // Llenar selector
  const sel = q('community-select');
  sel.innerHTML = '';

  if (!State.communities.length) {
    sel.innerHTML = '<option value="">Sin datos — navega en Skool</option>';
    renderNoData();
    return;
  }

  State.communities.forEach((c) => {
    const opt = document.createElement('option');
    opt.value = c.slug;
    opt.textContent = `${c.name || c.slug} (${c.memberCount} mb)`;
    sel.appendChild(opt);
  });

  State.slug = State.communities[0].slug;
  sel.value = State.slug;
  sel.addEventListener('change', (e) => {
    State.slug = e.target.value;
    refreshAll();
  });

  await refreshAll();
}

async function refreshAll() {
  const slug = State.slug;
  if (!slug) return;

  const [stats, members, feed, endpoints] = await Promise.all([
    sw('GET_STATS', { slug }),
    sw('GET_MEMBERS', { slug }),
    sw('GET_FEED', { slug, limit: 100 }),
    sw('GET_ENDPOINTS', { slug }),
  ]);

  State.stats = stats;
  State.members = members || [];
  State.feed = feed || [];
  State.endpoints = endpoints || [];

  // Actualizar header
  setText('community-tag', stats?.name || slug);

  // Sync indicator
  if (stats?.lastSync) {
    setText('sync-label', 'Sync: ' + relTime(stats.lastSync));
    q('sync-dot').classList.add('active');
  } else {
    setText('sync-label', 'Navegando en Skool...');
  }

  // Estado de atención del equipo (compartido). No bloquea el render si falla.
  await mergeTeam(slug).catch(() => {});

  // Render página actual
  renderPage(State.page);
}

// ── NAVEGACIÓN ────────────────────────────────────────────
function setupNav() {
  document.querySelectorAll('.nav-item').forEach((el) => {
    el.addEventListener('click', () => navigateTo(el.dataset.page));
  });

  // Botones "Ver todos →" dentro de cards
  document.querySelectorAll('[data-page]').forEach((el) => {
    if (el.classList.contains('nav-item')) return;
    el.addEventListener('click', () => {
      const page = el.dataset.page;
      const filter = el.dataset.filter;
      if (filter) State.memberFilter = filter;
      navigateTo(page);
    });
  });

  q('btn-open-skool').addEventListener('click', () => {
    chrome.tabs.create({ url: 'https://www.skool.com' });
  });
}

function navigateTo(page) {
  State.page = page;

  // Nav items
  document.querySelectorAll('.nav-item').forEach((el) => {
    el.classList.toggle('active', el.dataset.page === page);
  });

  // Pages
  document.querySelectorAll('.page').forEach((el) => {
    el.classList.toggle('active', el.id === `page-${page}`);
    el.classList.toggle('hidden', el.id !== `page-${page}`);
  });

  // Títulos
  const titles = {
    overview: 'Overview',
    pipeline: 'Pipeline de Miembros',
    members: 'Todos los Miembros',
    feed: 'Feed de la Comunidad',
    sentiment: 'Sentimiento y Riesgo',
    plan: 'Plan de Hoy',
    calendar: 'Calendario de la Semana',
    emails: 'Emails de Reactivación IA',
  };
  setText('page-title', titles[page] || page);

  renderPage(page);
}

function renderPage(page) {
  switch (page) {
    case 'overview': renderOverview(); break;
    case 'pipeline': renderPipeline(); break;
    case 'members': renderMembersTable(); break;
    case 'feed': renderFeedPage(); break;
    case 'sentiment': renderSentimentPage(); break;
    case 'plan': renderPlanPage(); break;
    case 'calendar': renderCalendarPage(); break;
    case 'emails': renderEmailsPage(); break;
  }
}

// ── TOOLBAR ───────────────────────────────────────────────
function setupToolbar() {
  q('btn-refresh').addEventListener('click', async () => {
    await refreshAll();
    toast('✓ Datos actualizados');
  });

  q('btn-clear-all').addEventListener('click', async () => {
    if (!confirm('¿Borrar TODA la data capturada y empezar de cero?\nTendrás que navegar por Skool de nuevo para reindexar.')) return;
    await sw('CLEAR_ALL');
    toast('✓ Data borrada — recarga la extensión y navega en Skool');
    setTimeout(() => location.reload(), 1500);
  });

  q('btn-export-md').addEventListener('click', async () => {
    if (!requirePro()) return;
    if (!State.slug) return;
    const md = window.SkoopMdExport.generateCommunityMd({
      stats: State.stats,
      members: State.members,
      feed: State.feed,
      slug: State.slug,
    });
    downloadFile(md, `skoop-${State.slug}-${datestamp()}.md`, 'text/markdown');
    toast('✓ Archivo .md descargado — ábrelo con Claude');
  });

  const btnDiag = q('btn-diagnose-cancel');
  if (btnDiag) btnDiag.addEventListener('click', diagnoseCancelFields);

  const btnAiPosts = q('btn-ai-posts');
  if (btnAiPosts) btnAiPosts.addEventListener('click', generateDailyPosts);

  const btnBrain = q('btn-brain');
  if (btnBrain) btnBrain.addEventListener('click', openBrainEditor);
  const btnPriority = q('btn-priority');
  if (btnPriority) btnPriority.addEventListener('click', showRetentionPriority);
  const btnThemes = q('btn-themes');
  if (btnThemes) btnThemes.addEventListener('click', showThemes);
  const btnSummary = q('btn-summary');
  if (btnSummary) btnSummary.addEventListener('click', showWeeklySummary);

  const btnAiSent = q('btn-ai-sentiment');
  if (btnAiSent) btnAiSent.addEventListener('click', analyzeSentiment);

  const btnCsv = q('btn-export-csv');
  if (btnCsv) btnCsv.addEventListener('click', () => {
    if (!requirePro()) return;
    if (!State.members || !State.members.length) { toast('No hay miembros para exportar'); return; }
    const csv = buildMembersCsv(State.members);
    downloadFile('﻿' + csv, `skoop-${State.slug}-miembros-${datestamp()}.csv`, 'text/csv;charset=utf-8');
    toast(`✓ CSV con ${State.members.length} miembros descargado`);
  });
}

// ── FASE 4: cliente de IA (backend skoop-api + Claude) ────
const AI_BASE = 'https://skoop-api.vercel.app/api';

async function _aiKey() {
  const stored = await chrome.storage.local.get('skoop_license').catch(() => ({}));
  return stored.skoop_license?.key || null;
}

// Segundo Cerebro / voz del usuario (se guarda solo en el navegador).
async function _aiBrain() {
  const s = await chrome.storage.local.get('skoop_brain').catch(() => ({}));
  return s.skoop_brain || '';
}

// Editor de "Mi voz": sube/pega tu context.md / segundocerebro.md.
async function openBrainEditor() {
  const stored = await chrome.storage.local.get('skoop_brain').catch(() => ({}));
  const current = stored.skoop_brain || '';
  q('modal-content').innerHTML = `
    <div class="modal-member-name" style="margin-bottom:6px">🧠 Tu voz (Segundo Cerebro)</div>
    <p style="color:var(--text-muted);font-size:13px;line-height:1.5">Sube o pega tu <strong>context.md / segundocerebro.md</strong>. La IA (DM, posts y respuestas) escribirá en TU voz, valores y estilo. Se guarda solo en tu navegador, no se sube a ningún lado salvo cuando generas un mensaje.</p>
    <input type="file" accept=".md,.txt,.markdown" id="brain-file" style="margin:8px 0" data-change="brain-file">
    <textarea id="brain-text" placeholder="Pega aquí tu segundo cerebro…" style="width:100%;min-height:240px;background:var(--surface2);color:var(--text);border:1px solid var(--surface3);border-radius:8px;padding:10px;font-size:13px;font-family:inherit;box-sizing:border-box">${esc(current)}</textarea>
    <div style="display:flex;gap:8px;margin-top:10px;align-items:center;flex-wrap:wrap">
      <button class="btn btn-purple" data-act="save-brain">Guardar</button>
      <button class="btn btn-ghost" data-act="save-brain" data-a1="">Borrar</button>
      <button class="btn btn-ghost" data-act="close-modal">Cerrar</button>
      <span style="color:var(--text-muted);font-size:12px">${current ? `✓ Voz cargada (${current.length} caracteres)` : 'Sin voz — la IA usa español neutro por defecto'}</span>
    </div>`;
  q('member-modal-overlay').classList.remove('hidden');
}
window.__brainFile = (ev) => {
  const f = ev.target.files && ev.target.files[0];
  if (!f) return;
  const r = new FileReader();
  r.onload = () => { const ta = q('brain-text'); if (ta) ta.value = r.result; };
  r.readAsText(f);
};
window.__saveBrain = async (forceVal) => {
  const val = forceVal !== undefined ? forceVal : (q('brain-text')?.value || '');
  await chrome.storage.local.set({ skoop_brain: val });
  toast(val ? '✓ Voz guardada — la IA escribirá como tú' : '✓ Voz borrada');
  q('member-modal-overlay').classList.add('hidden');
};

// Llama un endpoint de IA. Lanza Error con mensaje amigable si falla.
async function aiCall(endpoint, body) {
  if (!isPro()) throw new Error('Activa tu llave para usar la IA (estás en modo gratis).');
  const key = await _aiKey();
  if (!key) throw new Error('No hay licencia activa. Activa Skoop en el popup primero.');
  let resp;
  try {
    resp = await fetch(`${AI_BASE}/${endpoint}`, {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ key, ...body }),
    });
  } catch (e) {
    throw new Error('No se pudo conectar con el servidor de IA. ¿Hay internet?');
  }
  const data = await resp.json().catch(() => ({}));
  if (!resp.ok) {
    if (resp.status === 404) throw new Error('El backend de IA aún no está desplegado (endpoint ' + endpoint + '). Ver docs/FASE4-API.md.');
    if (resp.status === 401) throw new Error('Licencia no válida para IA.');
    if (resp.status === 500 && /ANTHROPIC/i.test(data.error || '')) throw new Error('Falta configurar ANTHROPIC_API_KEY en Vercel.');
    throw new Error(data.error || `Error ${resp.status} del servidor de IA.`);
  }
  return data;
}

// Elige el objetivo del DM según el estado del miembro.
function _dmGoal(m) {
  if (m.subStatus === 'cancelling' || m.subStatus === 'canceled') return 'retener';
  if (m.riskLevel === 'atRisk' || m.riskLevel === 'churned') return 'reactivar';
  if (m.monthsInCommunity === 0) return 'bienvenida';
  return 'felicitar';
}

// 4.2 — Genera un DM personalizado con IA para un miembro y lo muestra en el modal.
window.generateAiDm = async (memberId) => {
  if (!requirePro()) return;
  const m = State.members.find((x) => String(x.id) === String(memberId));
  if (!m) return;
  const out = q('ai-dm-result');
  const goal = _dmGoal(m);
  const goalLabel = { retener: 'retención', reactivar: 'reactivación', bienvenida: 'bienvenida', felicitar: 'felicitación' }[goal];
  if (out) out.innerHTML = `<div style="color:var(--text-muted);font-size:13px;padding:10px">✨ Generando DM de ${goalLabel} con IA…</div>`;
  try {
    const { message } = await aiCall('ai-dm', {
      goal,
      brain: await _aiBrain(),
      member: {
        name: m.name, username: m.username, niche: m.niche,
        monthsInCommunity: m.monthsInCommunity, daysSinceLastPost: m.daysSinceLastPost,
        lastComment: m.lastCommentText, role: m.role, plan: m.planPrice,
        tier: m.tier, level: m.level,
      },
    });
    const wa = m.whatsapp ? `https://wa.me/${m.whatsapp.replace(/[^\d]/g, '')}?text=${encodeURIComponent(message)}` : null;
    if (out) out.innerHTML = `
      <div style="background:var(--surface2);border:1px solid var(--purple-dim);border-radius:8px;padding:12px;margin-top:10px">
        <div style="font-size:11px;color:var(--purple-light);text-transform:uppercase;letter-spacing:.5px;margin-bottom:6px">✨ DM de ${goalLabel} (IA)</div>
        <div style="font-size:14px;line-height:1.5;white-space:pre-wrap;color:var(--text)">${esc(message)}</div>
        <div style="display:flex;gap:8px;margin-top:10px;flex-wrap:wrap">
          <button class="btn btn-ghost btn-sm" data-act="copy" data-copy="${esc(message)}">Copiar</button>
          ${wa ? `<a class="btn btn-sm" href="${wa}" target="_blank" style="background:#25D366;color:#fff">📱 Enviar por WhatsApp</a>` : ''}
          <button class="btn btn-ghost btn-sm" data-act="gen-dm" data-a1="${esc(String(m.id))}">↻ Regenerar</button>
        </div>
      </div>`;
  } catch (e) {
    if (out) out.innerHTML = `<div style="color:var(--red);font-size:13px;padding:10px;background:var(--red-dim);border-radius:8px;margin-top:10px">⚠️ ${esc(e.message)}</div>`;
  }
};

// 4.3 — Genera borradores de posts diarios con IA y los muestra en el modal.
async function generateDailyPosts() {
  if (!requirePro()) return;
  const s = State.stats || {};
  const rb = s.riskBuckets || {};
  openInfoModal('✨ Posts del día (IA)', `<div style="color:var(--text-muted);font-size:13px;padding:10px">Generando 3 borradores con IA…</div>`);
  try {
    const { drafts } = await aiCall('ai-post', {
      brain: await _aiBrain(),
      context: {
        communityName: s.name || State.slug,
        totalMembers: s.totalMembers || State.members.length,
        activeCount: rb.active || 0,
        silentCount: rb.silent || 0,
        atRiskCount: (rb.atRisk || 0) + (rb.churned || 0),
        recentWins: (State.feed || []).slice(0, 5).map((p) => (p.content || '').slice(0, 120)).filter(Boolean),
      },
    });
    const html = (drafts || []).map((d, i) => `
      <div style="background:var(--surface2);border:1px solid var(--surface3);border-radius:8px;padding:12px;margin-bottom:10px">
        <div style="font-weight:700;margin-bottom:6px">${esc(d.title || `Borrador ${i + 1}`)}</div>
        <div style="font-size:14px;line-height:1.5;white-space:pre-wrap;color:var(--text)">${esc(d.body || '')}</div>
        <button class="btn btn-ghost btn-sm" style="margin-top:8px" data-act="copy" data-copy="${esc((d.title ? d.title + '\n\n' : '') + (d.body || ''))}">Copiar</button>
      </div>`).join('');
    openInfoModal('✨ Posts del día (IA)', html || '<div style="color:var(--text-dim)">La IA no devolvió borradores.</div>');
  } catch (e) {
    openInfoModal('✨ Posts del día (IA)', `<div style="color:var(--red);font-size:13px;padding:10px;background:var(--red-dim);border-radius:8px">⚠️ ${esc(e.message)}</div>`);
  }
}

// 4.1 — Analiza el sentimiento de posts + comentarios y PERSISTE los negativos
// para mostrarlos en el tab "Sentimiento". Devuelve los flagged (o null si error).
async function runSentimentAnalysis() {
  const all = await _collectFeedItems(120);
  if (!all.length) return { empty: true };
  const { results } = await aiCall('ai-sentiment', {
    comments: all.map((c) => ({ id: c.id, text: c.text, author: c.author })),
  });
  const byId = Object.fromEntries((results || []).map((r) => [String(r.id), r]));
  const flagged = all
    .map((c) => ({ ...c, ...(byId[c.id] || {}) }))
    .filter((c) => c.risk || c.sentiment === 'neg');

  // Persistir miembros con señal negativa (alimenta razones de churn + prioridad).
  const keyToMember = new Map();
  for (const m of State.members) {
    for (const k of [m.username, m.name, m.userId, m.id]) {
      if (k) keyToMember.set(String(k).toLowerCase(), m.id);
    }
  }
  const negMembers = {};
  for (const c of flagged) {
    const mid = keyToMember.get(String(c.author || '').toLowerCase());
    if (mid != null) negMembers[String(mid)] = { text: c.text, reason: c.reason || '' };
  }
  window.__negativeMembers = negMembers;

  // Guardar en storage para que el tab "Sentimiento" sea persistente.
  await chrome.storage.local.set({
    ['skoop_sentiment_' + State.slug]: {
      date: datestamp(), analyzedAt: Date.now(), total: all.length,
      flagged, negMembers,
    },
  });
  return { flagged, total: all.length };
}

// Lee el último análisis de sentimiento persistido para la comunidad activa.
async function loadStoredSentiment() {
  const key = 'skoop_sentiment_' + State.slug;
  const s = await chrome.storage.local.get(key).catch(() => ({}));
  const data = s[key] || null;
  if (data?.negMembers) window.__negativeMembers = data.negMembers;
  return data;
}

// Botón "Problemas/Sentimiento" del toolbar → corre el análisis y abre el tab.
async function analyzeSentiment() {
  if (!requirePro()) return;
  navigateTo('sentiment');
  await refreshSentiment(true);
}

// Render simple y seguro de Markdown → HTML (negritas, títulos, listas, saltos).
// Arregla el "parece markdown" (los ** y # crudos) en resumen, temas, posts, DM.
function mdToHtml(text) {
  if (!text) return '';
  const lines = String(text).replace(/\r/g, '').split('\n');
  const out = [];
  let inUl = false, inOl = false;
  const closeLists = () => { if (inUl) { out.push('</ul>'); inUl = false; } if (inOl) { out.push('</ol>'); inOl = false; } };
  const inline = (s) => esc(s)
    .replace(/\*\*([^*]+)\*\*/g, '<strong>$1</strong>')
    .replace(/(^|[^*])\*([^*\n]+)\*(?!\*)/g, '$1<em>$2</em>')
    .replace(/`([^`]+)`/g, '<code>$1</code>');
  for (let raw of lines) {
    const line = raw.trim();
    if (!line) { closeLists(); continue; }
    if (/^---+$/.test(line) || /^\*\*\*+$/.test(line)) { closeLists(); out.push('<hr style="border:none;border-top:1px solid var(--surface3);margin:10px 0">'); continue; }
    let m;
    if ((m = line.match(/^#{1,4}\s+(.*)$/))) { closeLists(); out.push(`<div style="font-weight:800;margin:8px 0 4px">${inline(m[1])}</div>`); continue; }
    if ((m = line.match(/^\d+[.)]\s+(.*)$/))) { if (!inOl) { closeLists(); out.push('<ol style="margin:4px 0 4px 20px;padding:0">'); inOl = true; } out.push(`<li style="margin:3px 0">${inline(m[1])}</li>`); continue; }
    if ((m = line.match(/^[-*•]\s+(.*)$/))) { if (!inUl) { closeLists(); out.push('<ul style="margin:4px 0 4px 20px;padding:0">'); inUl = true; } out.push(`<li style="margin:3px 0">${inline(m[1])}</li>`); continue; }
    closeLists();
    out.push(`<div style="margin:4px 0">${inline(line)}</div>`);
  }
  closeLists();
  return out.join('');
}

// Reúne TODO lo analizable del feed: comentarios capturados + posts (cuando no
// hay comentarios embebidos, los posts del feed ya traen señal de sentimiento:
// quejas, dudas, frustraciones). Cada item: { id, type, text, author, authorId }.
async function _collectFeedItems(limit = 120) {
  const raw = await sw('EXPORT_RAW', { slug: State.slug });
  const items = [];
  for (const c of Object.values(raw?.comments || {})) {
    if (c && c.content) items.push({ id: 'c_' + c.id, type: 'comentario', text: c.content, author: c.authorName || '', authorId: c.authorId || '' });
  }
  for (const p of Object.values(raw?.posts || {})) {
    if (p && p.content) items.push({ id: 'p_' + p.id, type: 'post', text: p.content, author: p.authorName || '', authorId: p.authorId || '' });
  }
  return items.slice(0, limit);
}

// Helper: muestra contenido arbitrario en el modal reutilizable.
function openInfoModal(title, bodyHtml) {
  q('modal-content').innerHTML = `
    <div class="modal-member-name" style="margin-bottom:10px">${esc(title)}</div>
    <div style="max-height:420px;overflow:auto">${bodyHtml}</div>
    <div style="margin-top:14px"><button class="btn btn-ghost" data-act="close-modal">Cerrar</button></div>`;
  q('member-modal-overlay').classList.remove('hidden');
}

// Abre la ficha de un miembro por id (usado por el priorizador).
window.openMemberById = (id) => {
  const m = State.members.find((x) => String(x.id) === String(id));
  if (m) openMemberModal(m);
};

// 🎯 Priorizador de retención — 100% client-side (no necesita backend).
// Cruza: por irse + rescatable + nivel 1 + riesgo + VIP, y rankea a quién
// escribir hoy. Cuando el sentiment esté desplegado, sumaremos "comentó negativo".
function retentionScore(m) {
  let score = 0;
  const reasons = [];
  if (m.subStatus === 'cancelling') { score += 100; reasons.push('<span class="status-badge badge-cancel">🔴 Por irse</span>'); }
  // 🆘🔥 Rescate URGENTE: canceló + primer mes / nivel 1. Va por encima de todo.
  if (isUrgentRescue(m)) {
    score += 80;
    reasons.push('<span class="status-badge" style="background:#dc2626;color:#fff">🆘🔥 Rescate urgente</span>');
  }
  // Pocos días para irse → súbele la prioridad (≤7d crítico, ≤14d alto).
  const dl = daysUntilLeave(m);
  if (m.subStatus === 'cancelling' && dl.days != null) {
    if (dl.days <= 7) score += 40;
    else if (dl.days <= 14) score += 20;
    reasons.push(`<span class="status-badge" style="background:#7f1d1d;color:#fff">⏳ ${dl.exact ? '' : '~'}${dl.days}d para irse</span>`);
  }
  if (isRescue(m)) { score += 60; reasons.push('<span class="status-badge filter-count-badge" style="background:#f59e0b;color:#1a1a1a">🆘 Rescatable</span>'); }
  if (m.riskLevel === 'atRisk') { score += 30; reasons.push('<span class="status-badge badge-atRisk">🔴 En riesgo</span>'); }
  else if (m.riskLevel === 'churned') { score += 22; reasons.push('<span class="status-badge badge-churned">⚫ Churn</span>'); }
  else if (m.riskLevel === 'silent') { score += 12; reasons.push('<span class="status-badge badge-silent">🟡 Silencio</span>'); }
  if (m.level != null && Number(m.level) <= 1) { score += 18; reasons.push('<span class="status-badge" style="background:var(--surface3);color:var(--text-muted)">🔰 Nivel 1</span>'); }
  if (window.__negativeMembers && window.__negativeMembers[String(m.id)]) { score += 25; reasons.push('<span class="status-badge" style="background:#ef444455;color:#fff">😠 Comentó negativo</span>'); }
  if (m.tier === 'VIP') { score += 15; reasons.push('<span class="tier-chip tier-vip">VIP</span>'); }
  return { score, reasons };
}

function showRetentionPriority() {
  if (!requirePro()) return;
  const members = State.members || [];
  if (!members.length) { openInfoModal('🎯 A quién escribir hoy', '<div style="color:var(--text-dim);padding:10px">Escanea miembros primero.</div>'); return; }
  const scored = members
    .map((m) => ({ m, ...retentionScore(m) }))
    .filter((x) => x.score > 0)
    .sort((a, b) => b.score - a.score)
    .slice(0, 30);
  if (!scored.length) { openInfoModal('🎯 A quién escribir hoy', '<div style="color:var(--green);padding:10px">✓ Nadie urgente hoy. Buen trabajo.</div>'); return; }
  const rows = scored.map(({ m, reasons }) => `
    <div style="display:flex;align-items:center;gap:10px;padding:10px;border-bottom:1px solid var(--surface3)">
      <div style="flex:1;min-width:0">
        <div style="font-weight:600">${esc(m.name || m.username || 'Miembro')}</div>
        <div style="display:flex;gap:4px;flex-wrap:wrap;margin-top:4px">${reasons.join(' ')}</div>
      </div>
      <div style="display:flex;gap:6px;flex-shrink:0">
        ${m.whatsapp ? `<a class="btn btn-sm" href="https://wa.me/${esc(m.whatsapp.replace(/[^\d]/g, ''))}" target="_blank" title="WhatsApp" style="background:#25D366;color:#fff">📱</a>` : ''}
        <button class="btn btn-purple btn-sm" data-act="open-member" data-a1="${esc(String(m.id))}">Ver · DM IA</button>
      </div>
    </div>`).join('');
  openInfoModal(`🎯 A quién escribir hoy — ${scored.length} prioridades`, rows);
}

// 🗺 Mapa de problemas (IA, Gemini) — agrupa comentarios capturados por tema.
// Guarda el último resultado por comunidad y lo deja descargable (.md).
function _themesKey() { return 'skoop_themes_' + State.slug; }

// Normaliza texto para emparejar una cita con su comentario original.
function _norm(s) { return String(s || '').toLowerCase().replace(/[“”"'.,;:!?¿¡()]/g, '').replace(/\s+/g, ' ').trim(); }

// Asegura que cada ejemplo sea {quote, author}. Si la IA no devolvió autor,
// lo busca emparejando la cita con el comentario original capturado.
function _enrichThemeAuthors(themes, source) {
  const idx = (source || []).map((c) => ({ n: _norm(c.text), author: c.author }));
  const findAuthor = (quote) => {
    const nq = _norm(quote);
    if (!nq) return '';
    const hit = idx.find((c) => c.n && (c.n.includes(nq) || nq.includes(c.n)));
    return hit ? hit.author : '';
  };
  return (themes || []).map((t) => ({
    ...t,
    examples: (t.examples || []).map((e) => {
      const quote = typeof e === 'string' ? e : (e && (e.quote || e.text)) || '';
      let author = (e && typeof e === 'object' && e.author) ? e.author : '';
      if (!author) author = findAuthor(quote);
      return { quote, author };
    }),
  }));
}

async function showThemes() {
  if (!requirePro()) return;
  const cacheKey = _themesKey();
  const s = await chrome.storage.local.get(cacheKey).catch(() => ({}));
  const cached = s[cacheKey];
  if (cached && Array.isArray(cached.themes) && cached.themes.length) {
    _paintThemes(cached.themes, cached.generatedAt, true);
  } else {
    runThemes();
  }
}

async function runThemes() {
  const all = (await _collectFeedItems(120)).map((c) => ({ id: c.id, text: c.text, author: c.author }));
  if (!all.length) { openInfoModal('🗺 Mapa de problemas', '<div style="color:var(--text-dim);padding:10px">No hay actividad capturada todavía. En el popup de Skool dale a <strong>Escanear feed</strong> y vuelve.</div>'); return; }
  openInfoModal('🗺 Mapa de problemas (IA)', `<div style="color:var(--text-muted);padding:10px">Agrupando ${all.length} comentarios por tema…</div>`);
  try {
    const { themes } = await aiCall('ai-themes', { comments: all });
    const enriched = _enrichThemeAuthors(themes || [], all);
    const generatedAt = Date.now();
    await chrome.storage.local.set({ [_themesKey()]: { themes: enriched, generatedAt } }).catch(() => {});
    _paintThemes(enriched, generatedAt, false);
  } catch (e) {
    openInfoModal('🗺 Mapa de problemas', `<div style="color:var(--red);padding:10px;background:var(--red-dim);border-radius:8px">⚠️ ${esc(e.message)}</div>`);
  }
}
window.__skoopRunThemes = runThemes;

function _paintThemes(themes, generatedAt, fromCache) {
  window.__skoopThemes = themes;
  const sevColor = { alta: '#ef4444', media: '#f59e0b', baja: '#94a3b8' };
  const bar = `
    <div style="display:flex;justify-content:space-between;align-items:center;gap:8px;margin-bottom:12px;flex-wrap:wrap">
      <div style="font-size:11px;color:var(--text-dim)">${fromCache ? '💾 Guardado' : '✨ Recién generado'}${generatedAt ? ' · ' + relTime(generatedAt) : ''}</div>
      <div style="display:flex;gap:6px">
        <button class="btn btn-ghost btn-sm" data-act="dl-themes">⬇️ Descargar (.md)</button>
        <button class="btn btn-purple btn-sm" data-act="run-themes">🔄 Regenerar</button>
      </div>
    </div>`;
  const html = bar + (themes || []).map((t) => `
    <div style="background:var(--surface2);border:1px solid var(--surface3);border-left:3px solid ${sevColor[t.severity] || '#94a3b8'};border-radius:8px;padding:12px;margin-bottom:10px">
      <div style="display:flex;justify-content:space-between;gap:8px"><strong>${esc(t.theme || '')}</strong><span style="color:${sevColor[t.severity] || '#94a3b8'};font-size:12px;white-space:nowrap">${esc(t.severity || '')} · ${t.count || 0}</span></div>
      ${t.suggestedAction ? `<div style="font-size:13px;margin-top:6px"><strong>Acción:</strong> ${esc(t.suggestedAction)}</div>` : ''}
      ${(t.examples || []).length ? `<div style="font-size:12px;color:var(--text-muted);margin-top:8px;display:flex;flex-direction:column;gap:6px">${t.examples.map((e) => `<div>“${esc(e.quote)}”${e.author ? ` <span style="color:var(--text-dim)">— ${esc(e.author)}</span>` : ''}</div>`).join('')}</div>` : ''}
    </div>`).join('');
  openInfoModal(`🗺 Mapa de problemas — ${(themes || []).length} temas`, html || '<div style="color:var(--text-dim)">Sin temas.</div>');
}

window.__skoopDownloadThemes = () => {
  const themes = window.__skoopThemes || [];
  const lines = [`# 🗺 Mapa de problemas — ${State.slug}`, `Generado: ${new Date().toLocaleString('es-MX')}`, ''];
  themes.forEach((t) => {
    lines.push(`## ${t.theme || ''}  (${t.severity || ''} · ${t.count || 0})`);
    if (t.suggestedAction) lines.push(`**Acción:** ${t.suggestedAction}`);
    (t.examples || []).forEach((e) => lines.push(`- “${e.quote}”${e.author ? ` — ${e.author}` : ''}`));
    lines.push('');
  });
  downloadTextFile(`mapa-problemas-${State.slug}-${datestamp()}.md`, lines.join('\n'));
};

// Descarga un archivo de texto desde el dashboard (sin backend).
function downloadTextFile(filename, content, mime = 'text/markdown') {
  const blob = new Blob([content], { type: `${mime};charset=utf-8` });
  const url = URL.createObjectURL(blob);
  const a = document.createElement('a');
  a.href = url; a.download = filename;
  document.body.appendChild(a); a.click(); a.remove();
  setTimeout(() => URL.revokeObjectURL(url), 1000);
  if (window.__skoopToast) window.__skoopToast('✓ Descargado');
}

// 📊 Resumen de salud semanal (IA, Gemini).
// El botón "📊 Resumen" del toolbar ahora abre el Plan de hoy (resumen diario
// CACHEADO + plan de acción con pendientes). Ya no se regenera en cada clic.
function showWeeklySummary() { navigateTo('plan'); }

// ── TAB: SENTIMIENTO Y RIESGO ─────────────────────────────
// Muestra los posts/comentarios negativos detectados (persistidos), con razón
// y un botón para que la IA redacte la respuesta. Mapea aparte el riesgo.
function setupSentimentTab() {
  const a = q('btn-run-sentiment'); if (a) a.addEventListener('click', () => refreshSentiment(true));
  const b = q('btn-themes-from-sentiment'); if (b) b.addEventListener('click', showThemes);
}

async function renderSentimentPage() {
  if (!isPro()) {
    const st = q('sentiment-status'); if (st) st.textContent = '🔒 Pro';
    q('sentiment-body').innerHTML = lockedScreenHTML('Sentimiento y Riesgo',
      'La IA analiza tus posts y comentarios para detectar quién está molesto o a punto de irse, y te arma el mapa de problemas. Desbloquéalo con tu llave.');
    return;
  }
  const data = await loadStoredSentiment();
  _paintSentiment(data);
}

// force=true → corre el análisis con IA; si no, pinta lo último guardado.
async function refreshSentiment(force) {
  if (force && !requirePro()) return;
  const status = q('sentiment-status');
  if (force) {
    if (status) status.textContent = 'Analizando con IA…';
    _paintSentimentLoading();
    try {
      const res = await runSentimentAnalysis();
      if (res?.empty) { _paintSentiment(null, 'empty'); return; }
      const data = await loadStoredSentiment();
      _paintSentiment(data);
      if (State.page === 'members') renderMembersTable();
      renderOverview();
    } catch (e) {
      q('sentiment-body').innerHTML = `<div class="card" style="padding:14px;color:var(--red)">⚠️ ${esc(e.message)}</div>`;
      if (status) status.textContent = 'Error';
    }
  } else {
    const data = await loadStoredSentiment();
    _paintSentiment(data);
  }
}

function _paintSentimentLoading() {
  q('sentiment-body').innerHTML = `<div class="card" style="padding:16px;color:var(--text-muted)">✨ Analizando posts y comentarios con IA…</div>`;
}

function _paintSentiment(data, mode) {
  const body = q('sentiment-body');
  const status = q('sentiment-status');
  if (mode === 'empty' || !data) {
    if (status) status.textContent = data ? '' : 'Sin análisis aún';
    body.innerHTML = `<div class="card" style="padding:18px;color:var(--text-muted);line-height:1.6">
      ${mode === 'empty'
        ? 'No hay actividad capturada todavía. En el popup de Skool dale a <strong>Escanear feed</strong> y luego a <strong>✨ Analizar ahora</strong>.'
        : 'Aún no has corrido el análisis. Dale a <strong>✨ Analizar ahora</strong> para que la IA revise los posts y comentarios y marque los negativos.'}
    </div>`;
    return;
  }
  const flagged = data.flagged || [];
  if (status) status.textContent = `${flagged.length} en riesgo · ${data.total} analizados · ${relTime(data.analyzedAt)}`;
  window.__skoopComments = {};
  if (!flagged.length) {
    body.innerHTML = `<div class="card" style="padding:18px;color:var(--green)">✓ No se detectaron mensajes negativos o en riesgo entre los ${data.total} analizados.</div>`;
    return;
  }
  body.innerHTML = `<div style="display:flex;flex-direction:column;gap:10px">` + flagged.map((c) => {
    window.__skoopComments[c.id] = { text: c.text, author: c.author };
    return `
      <div class="card" style="padding:12px;border-left:3px solid #ef4444">
        <div style="display:flex;justify-content:space-between;gap:8px;align-items:center">
          <div style="font-size:12px;color:#ef4444;font-weight:700">🔴 ${esc(c.author || 'Miembro')} <span style="color:var(--text-dim);font-weight:400">· ${esc(c.type || '')}</span></div>
          ${c.reason ? `<div style="font-size:11px;color:var(--text-muted)">${esc(c.reason)}</div>` : ''}
        </div>
        <div style="font-size:13px;color:var(--text);margin-top:6px;line-height:1.5">“${esc(c.text)}”</div>
        <div style="display:flex;gap:6px;margin-top:8px">
          <button class="btn btn-purple btn-sm" data-act="draft-reply" data-a1="${esc(String(c.id))}">✨ Redactar respuesta</button>
          ${c.authorId ? `<button class="btn btn-ghost btn-sm" data-act="view-member" data-a1="${esc(String(c.authorId))}">Ver miembro</button>` : ''}
        </div>
        <div id="reply-out-${esc(String(c.id))}"></div>
      </div>`;
  }).join('') + `</div>`;
}

// ── TAB: PLAN DE HOY ──────────────────────────────────────
// Resumen de salud + plan de acción con pendientes marcables. Se genera UNA vez
// al día (cacheado por fecha); botón para regenerar. Incluye a quién rescatar
// primero e ideas de contenido del día.
function setupPlanTab() {
  const g = q('btn-gen-plan'); if (g) g.addEventListener('click', () => generateTodayPlan(true));
}

function _planKey() { return 'skoop_plan_' + State.slug; }

async function renderPlanPage() {
  if (!isPro()) {
    const st = q('plan-status'); if (st) st.textContent = '🔒 Pro';
    q('plan-body').innerHTML = lockedScreenHTML('Plan de hoy',
      'Cada día la IA te arma un briefing privado de salud + a quién rescatar primero + un post de reactivación listo para publicar. Desbloquéalo con tu llave.');
    return;
  }
  const key = _planKey();
  const s = await chrome.storage.local.get(key).catch(() => ({}));
  const plan = s[key];
  if (plan && plan.date === datestamp()) { _paintPlan(plan); }
  else { _paintPlanEmpty(plan); }
}

function _paintPlanEmpty(stale) {
  q('plan-status').textContent = stale ? `Último: ${stale.date}` : 'Sin plan de hoy';
  q('plan-body').innerHTML = `<div class="card" style="padding:18px;color:var(--text-muted);line-height:1.6">
    ${stale ? 'El plan guardado es de otro día. ' : ''}Dale a <strong>🔄 Generar plan de hoy</strong> para crear el resumen de salud y tu lista de acciones (a quién rescatar + ideas de contenido), según el último escaneo.
  </div>`;
}

async function generateTodayPlan(force) {
  if (!requirePro()) return;
  const status = q('plan-status');
  status.textContent = 'Generando con IA…';
  q('plan-body').innerHTML = `<div class="card" style="padding:16px;color:var(--text-muted)">✨ Armando tu plan de hoy (resumen + acciones + ideas)…</div>`;
  const s = State.stats || {};
  const rb = s.riskBuckets || {};
  try {
    // 1) Briefing privado (IA) + 2) ideas de contenido (IA) + 3) post de
    //    reactivación indirecto (IA, sin exponer números), en paralelo.
    const sentiment = await loadStoredSentiment();
    const brain = await _aiBrain();
    const recentWins = (State.feed || []).slice(0, 5).map((p) => (p.content || '').slice(0, 120)).filter(Boolean);
    const [summaryRes, postRes, reactRes] = await Promise.all([
      aiCall('ai-summary', { context: {
        communityName: s.name || State.slug,
        totalMembers: s.totalMembers || State.members.length,
        activeCount: rb.active || 0, silentCount: rb.silent || 0,
        atRiskCount: rb.atRisk || 0, churnedCount: rb.churned || 0,
        cancellingCount: State.members.filter((m) => m.subStatus === 'cancelling').length,
        rescueCount: State.members.filter(isRescue).length,
        topThemes: (sentiment?.flagged || []).slice(0, 5).map((f) => f.reason).filter(Boolean),
      } }),
      aiCall('ai-post', { brain, context: {
        communityName: s.name || State.slug,
        totalMembers: s.totalMembers || State.members.length,
        activeCount: rb.active || 0, silentCount: rb.silent || 0,
        atRiskCount: (rb.atRisk || 0) + (rb.churned || 0),
        recentWins,
      } }).catch(() => ({ drafts: [] })),
      // Post de reactivación: NO recibe los números de riesgo a propósito.
      aiCall('ai-post', { brain, goal: 'reactivation', context: {
        communityName: s.name || State.slug,
        recentWins,
      } }).catch(() => ({ drafts: [] })),
    ]);

    // 3) Acciones de rescate (cliente) — a quién escribir primero.
    const rescue = (State.members || [])
      .map((m) => ({ m, ...retentionScore(m) }))
      .filter((x) => x.score > 0)
      .sort((a, b) => b.score - a.score)
      .slice(0, 6);

    const items = [];
    rescue.forEach(({ m }, i) => {
      const dl = daysUntilLeave(m);
      const why = churnReasons(m).map((r) => r.label).slice(0, 2).join(' · ');
      items.push({ id: 'r' + i, kind: 'rescate', done: false, memberId: String(m.id),
        text: `Escríbele a ${m.name || m.username}${dl.days != null ? ` (${dl.exact ? '' : '~'}${dl.days}d para irse)` : ''}${why ? ` — ${why}` : ''}` });
    });
    (postRes?.drafts || []).slice(0, 3).forEach((d, i) => {
      items.push({ id: 'c' + i, kind: 'contenido', done: false, body: (d.title ? d.title + '\n\n' : '') + (d.body || ''),
        text: `Publica: ${d.title || 'idea de post'}` });
    });
    if ((rb.silent || 0) > 30) items.push({ id: 'h1', kind: 'salud', done: false, text: `Lanza un post de reactivación para los ${rb.silent} silenciosos (pregunta abierta o reto rápido).` });

    const reactivationPost = (reactRes?.drafts || [])[0] || null;
    const plan = { date: datestamp(), generatedAt: Date.now(), summary: summaryRes?.summary || '', items, reactivationPost,
      stats: { cancelling: State.members.filter((m) => m.subStatus === 'cancelling').length, silent: rb.silent || 0, atRisk: rb.atRisk || 0 } };
    await chrome.storage.local.set({ [_planKey()]: plan });
    _paintPlan(plan);
  } catch (e) {
    status.textContent = 'Error';
    q('plan-body').innerHTML = `<div class="card" style="padding:14px;color:var(--red)">⚠️ ${esc(e.message)}</div>`;
  }
}

function _paintPlan(plan) {
  q('plan-status').textContent = `Generado ${relTime(plan.generatedAt)} · ${plan.date}`;
  const done = (plan.items || []).filter((i) => i.done).length;
  const kindColor = { rescate: '#dc2626', contenido: '#8b5cf6', salud: '#f59e0b' };
  const kindLabel = { rescate: '🆘 Rescate', contenido: '📝 Contenido', salud: '💚 Salud' };
  const itemsHtml = (plan.items || []).map((it) => `
    <label class="plan-item${it.done ? ' done' : ''}" style="display:flex;gap:10px;align-items:flex-start;padding:10px;border-bottom:1px solid var(--surface3);cursor:pointer">
      <input type="checkbox" ${it.done ? 'checked' : ''} data-change="toggle-plan" data-a1="${esc(it.id)}" style="margin-top:3px;width:16px;height:16px;flex-shrink:0">
      <div style="flex:1;min-width:0">
        <div style="display:flex;gap:6px;align-items:center;flex-wrap:wrap">
          <span class="status-badge" style="background:${kindColor[it.kind] || 'var(--surface3)'};color:#fff;font-size:10px">${kindLabel[it.kind] || it.kind}</span>
          <span class="plan-item-text" style="font-size:13px">${esc(it.text)}</span>
        </div>
      </div>
      <div style="flex-shrink:0;display:flex;gap:6px">
        ${it.memberId ? `<button class="btn btn-ghost btn-sm" data-act="view-member" data-a1="${esc(it.memberId)}">Ver</button>` : ''}
        ${it.body ? `<button class="btn btn-ghost btn-sm" data-act="copy" data-copy="${esc(it.body)}">Copiar</button>` : ''}
      </div>
    </label>`).join('');

  const rp = plan.reactivationPost;
  const rpBody = rp ? ((rp.title ? rp.title + '\n\n' : '') + (rp.body || '')) : '';
  const reactivationHtml = rp ? `
    <div class="card" style="padding:16px;margin-bottom:14px;border:1px solid #8b5cf6;background:linear-gradient(180deg,rgba(139,92,246,0.10),transparent)">
      <div style="display:flex;justify-content:space-between;align-items:center;gap:8px;flex-wrap:wrap;margin-bottom:8px">
        <div style="font-weight:800;font-size:15px">📣 Post de reactivación <span style="font-weight:600;font-size:12px;color:#a78bfa">· listo para publicar</span></div>
        <div style="display:flex;gap:6px">
          <button class="btn btn-ghost btn-sm" data-act="dl-reactivation">⬇️ .txt</button>
          <button class="btn btn-purple btn-sm" data-act="copy" data-copy="${esc(rpBody)}">Copiar</button>
        </div>
      </div>
      <div style="font-size:11px;color:var(--text-dim);margin-bottom:8px">Reactiva sin señalar a nadie ni mencionar quién está en riesgo. Publícalo en el feed.</div>
      <div style="line-height:1.55;font-size:14px;color:var(--text);white-space:pre-wrap">${esc(rp.body || '')}</div>
    </div>` : '';

  q('plan-body').innerHTML = `
    <div class="card" style="padding:16px;margin-bottom:14px">
      <div style="font-weight:800;font-size:15px;margin-bottom:8px">📊 Salud de la comunidad <span style="font-weight:600;font-size:12px;color:var(--text-dim)">· 🔒 privado · solo para ti</span></div>
      <div style="line-height:1.55;font-size:14px;color:var(--text)">${mdToHtml(plan.summary) || '<span style="color:var(--text-dim)">Sin resumen.</span>'}</div>
    </div>
    ${reactivationHtml}
    <div class="card" style="padding:0">
      <div style="display:flex;justify-content:space-between;align-items:center;padding:14px 16px;border-bottom:1px solid var(--surface3)">
        <div style="font-weight:800;font-size:15px">✅ Acciones de hoy</div>
        <div style="font-size:12px;color:var(--text-muted)">${done}/${(plan.items || []).length} hechas</div>
      </div>
      ${itemsHtml || '<div style="padding:16px;color:var(--text-dim)">Sin acciones — ¡todo en orden!</div>'}
    </div>`;
}

window.__skoopDownloadReactivation = async () => {
  const key = _planKey();
  const s = await chrome.storage.local.get(key).catch(() => ({}));
  const rp = s[key] && s[key].reactivationPost;
  if (!rp) { if (window.__skoopToast) window.__skoopToast('No hay post de reactivación'); return; }
  downloadTextFile(`post-reactivacion-${State.slug}-${datestamp()}.txt`, rp.body || '', 'text/plain');
};

window.togglePlanItem = async (id, done) => {
  const key = _planKey();
  const s = await chrome.storage.local.get(key).catch(() => ({}));
  const plan = s[key];
  if (!plan) return;
  const it = (plan.items || []).find((x) => x.id === id);
  if (it) it.done = done;
  await chrome.storage.local.set({ [key]: plan });
  _paintPlan(plan);
};

// ── TAB: CALENDARIO DE LA SEMANA ──────────────────────────
// Calendario de 7 días de publicaciones/acciones para la comunidad, según el
// estado real (silenciosos, en riesgo, quejas). Cacheado por semana.
function setupCalendarTab() {
  const g = q('btn-gen-calendar'); if (g) g.addEventListener('click', () => generateWeekCalendar(true));
}

function _calKey() { return 'skoop_calendar_' + State.slug; }

// Lunes de la semana actual como YYYY-MM-DD (clave de caché semanal).
function _weekStart() {
  const d = new Date();
  const day = (d.getDay() + 6) % 7; // 0 = lunes
  d.setDate(d.getDate() - day);
  return d.toISOString().split('T')[0];
}

async function renderCalendarPage() {
  if (!isPro()) {
    const st = q('calendar-status'); if (st) st.textContent = '🔒 Pro';
    const body = q('calendar-body');
    if (body) body.innerHTML = lockedScreenHTML('Calendario semanal',
      'La IA arma 7 días de publicaciones y acciones para tu comunidad, según silenciosos, en riesgo y quejas detectadas. Desbloquéalo con tu llave.');
    return;
  }
  const key = _calKey();
  const s = await chrome.storage.local.get(key).catch(() => ({}));
  const cal = s[key];
  if (cal && cal.week === _weekStart()) _paintCalendar(cal);
  else _paintCalendarEmpty(cal);
}

function _paintCalendarEmpty(stale) {
  q('calendar-status').textContent = stale ? `Última semana: ${stale.week}` : 'Sin calendario esta semana';
  q('calendar-body').innerHTML = `<div class="card" style="padding:18px;color:var(--text-muted);line-height:1.6">
    ${stale ? 'El calendario guardado es de otra semana. ' : ''}Dale a <strong>🔄 Generar calendario de la semana</strong> para que la IA arme 7 días de publicaciones y acciones para la comunidad, según el último escaneo (silenciosos, en riesgo y quejas detectadas).
  </div>`;
}

async function generateWeekCalendar() {
  if (!requirePro()) return;
  const status = q('calendar-status');
  status.textContent = 'Generando con IA…';
  q('calendar-body').innerHTML = `<div class="card" style="padding:16px;color:var(--text-muted)">✨ Armando tu calendario semanal de la comunidad…</div>`;
  const s = State.stats || {};
  const rb = s.riskBuckets || {};
  try {
    const sentiment = await loadStoredSentiment();
    const { days } = await aiCall('ai-calendar', { brain: await _aiBrain(), context: {
      communityName: s.name || State.slug,
      totalMembers: s.totalMembers || State.members.length,
      activeCount: rb.active || 0, silentCount: rb.silent || 0,
      atRiskCount: (rb.atRisk || 0) + (rb.churned || 0),
      cancellingCount: State.members.filter((m) => m.subStatus === 'cancelling').length,
      topThemes: (sentiment?.flagged || []).slice(0, 6).map((f) => f.reason).filter(Boolean),
      recentWins: (State.feed || []).slice(0, 6).map((p) => (p.content || '').slice(0, 120)).filter(Boolean),
    } });
    const cal = { week: _weekStart(), generatedAt: Date.now(), days: days || [] };
    await chrome.storage.local.set({ [_calKey()]: cal });
    _paintCalendar(cal);
  } catch (e) {
    status.textContent = 'Error';
    q('calendar-body').innerHTML = `<div class="card" style="padding:14px;color:var(--red)">⚠️ ${esc(e.message)}</div>`;
  }
}

function _paintCalendar(cal) {
  q('calendar-status').textContent = `Semana del ${cal.week} · generado ${relTime(cal.generatedAt)}`;
  const goalColor = (g) => /reactiv|retenci|riesgo/i.test(g) ? '#dc2626' : /celebra/i.test(g) ? '#22c55e' : /valor|recurso/i.test(g) ? '#8b5cf6' : '#f59e0b';
  const cards = (cal.days || []).map((d) => `
    <div class="card" style="padding:14px;display:flex;flex-direction:column;gap:6px">
      <div style="display:flex;justify-content:space-between;align-items:center;gap:8px">
        <div style="font-weight:800;font-size:14px">${esc(d.day || '')}</div>
        ${d.goal ? `<span class="status-badge" style="background:${goalColor(d.goal)};color:#fff;font-size:10px">${esc(d.goal)}</span>` : ''}
      </div>
      ${d.type ? `<div style="font-size:11px;color:var(--text-muted);text-transform:uppercase;letter-spacing:.4px">${esc(d.type)}</div>` : ''}
      ${d.idea ? `<div style="font-size:13px;color:var(--text)">${esc(d.idea)}</div>` : ''}
      ${d.hook ? `<div style="font-size:13px;background:var(--surface2);border-radius:6px;padding:8px;color:var(--text)"><span style="color:var(--text-dim);font-size:11px">Gancho:</span><br>${esc(d.hook)}</div>` : ''}
      ${d.cta ? `<div style="font-size:12px;color:var(--text-muted)"><strong>CTA:</strong> ${esc(d.cta)}</div>` : ''}
      <button class="btn btn-ghost btn-sm" style="align-self:flex-start;margin-top:2px" data-act="copy" data-copy="${esc([d.hook, d.idea, d.cta].filter(Boolean).join('\n\n'))}">Copiar</button>
    </div>`).join('');
  q('calendar-body').innerHTML = `<div class="calendar-grid">${cards || '<div style="color:var(--text-dim);padding:16px">Sin días.</div>'}</div>`;
}

// ✨ Responder por ti (IA, Claude) — redacta una respuesta a un comentario.
window.__skoopComments = {};
window.draftReply = async (cid) => {
  if (!requirePro()) return;
  const c = window.__skoopComments[cid];
  if (!c) return;
  const out = q('reply-out-' + cid);
  if (out) out.innerHTML = '<div style="color:var(--text-muted);font-size:12px;padding:6px">✨ Redactando…</div>';
  try {
    const { reply } = await aiCall('ai-reply', {
      comment: { text: c.text, author: c.author },
      context: { communityName: State.stats?.name || State.slug, niche: '' },
      brain: await _aiBrain(),
    });
    if (out) out.innerHTML = `
      <div style="background:var(--surface3);border-radius:6px;padding:8px;margin-top:6px;font-size:13px;white-space:pre-wrap">${esc(reply)}</div>
      <div style="display:flex;gap:6px;margin-top:6px">
        <button class="btn btn-ghost btn-sm" data-act="copy" data-copy="${esc(reply)}">Copiar</button>
        <button class="btn btn-ghost btn-sm" data-act="draft-reply" data-a1="${esc(cid)}">↻</button>
      </div>`;
  } catch (e) {
    if (out) out.innerHTML = `<div style="color:var(--red);font-size:12px;padding:6px">⚠️ ${esc(e.message)}</div>`;
  }
};

// 🚨 Quién se va a ir — panel ACCIONABLE (reemplaza el diagnóstico técnico que
// confundía). Lista a quien canceló, ordenado por urgencia, con días restantes,
// razones y botones de contacto. Los datos crudos de billing quedan detrás de
// "ver datos técnicos" (solo para depurar la detección).
function diagnoseCancelFields() {
  if (!requirePro()) return;
  const members = State.members || [];
  if (!members.length) { toast('No hay miembros — escanea primero'); return; }

  const leaving = members
    .filter((m) => m.subStatus === 'cancelling' || m.subStatus === 'canceled')
    .map((m) => ({ m, ...retentionScore(m), dl: daysUntilLeave(m) }))
    .sort((a, b) => b.score - a.score);

  const urgentCount = leaving.filter((x) => isUrgentRescue(x.m)).length;

  const rows = leaving.map(({ m, dl }) => {
    const reasons = churnReasons(m);
    const dText = dl.days != null ? `${dl.exact ? '' : '~'}${dl.days}d` : 's/f';
    const dColor = dl.days != null && dl.days <= 7 ? '#dc2626' : dl.days != null && dl.days <= 14 ? '#f59e0b' : 'var(--text-muted)';
    return `
    <div style="display:flex;align-items:center;gap:10px;padding:10px;border-bottom:1px solid var(--surface3)">
      <div style="width:48px;text-align:center;flex-shrink:0">
        <div style="font-weight:800;font-size:15px;color:${dColor}">${esc(dText)}</div>
        <div style="font-size:9px;color:var(--text-dim)">${dl.days != null ? (dl.exact ? 'exacto' : 'aprox') : 'sin fecha'}</div>
      </div>
      <div style="flex:1;min-width:0">
        <div style="font-weight:600">${esc(m.name || m.username || 'Miembro')}${isUrgentRescue(m) ? ' <span class="status-badge" style="background:#dc2626;color:#fff">🆘🔥</span>' : ''}</div>
        <div class="reason-row" style="margin-top:3px">${reasonChipsHTML(reasons)}</div>
      </div>
      <div style="display:flex;gap:6px;flex-shrink:0">
        ${m.whatsapp ? `<a class="btn btn-sm" href="https://wa.me/${esc(m.whatsapp.replace(/[^\d]/g, ''))}" target="_blank" style="background:#25D366;color:#fff">📱</a>` : ''}
        <button class="btn btn-purple btn-sm" data-act="open-member" data-a1="${esc(String(m.id))}">Ver · DM</button>
      </div>
    </div>`;
  }).join('');

  q('modal-content').innerHTML = `
    <div class="modal-member-name" style="margin-bottom:4px">🚨 Quién se va a ir (${leaving.length})</div>
    <p style="color:var(--text-muted);font-size:13px;line-height:1.5;margin:0 0 10px">
      ${urgentCount ? `<strong style="color:#dc2626">${urgentCount} en rescate urgente</strong> (primer mes o nivel 1 que ya cancelaron) — escríbeles HOY. ` : ''}
      Ordenadas por urgencia. Los días salen exactos cuando Skool los expone; si no, los estimo por el ciclo de cobro (marcados <em>~aprox</em>).
    </p>
    ${leaving.length ? `<div style="max-height:420px;overflow:auto;border:1px solid var(--surface3);border-radius:8px">${rows}</div>`
      : '<div style="color:var(--green);padding:12px">✓ Nadie con cancelación detectada ahora mismo.</div>'}
    <div style="margin-top:14px;display:flex;gap:8px;align-items:center;flex-wrap:wrap">
      <button class="btn btn-ghost" data-act="close-modal">Cerrar</button>
      <button class="btn btn-ghost btn-sm" data-act="tech-diag" style="color:var(--text-dim)">🔧 Ver datos técnicos (dev)</button>
    </div>`;
  q('member-modal-overlay').classList.remove('hidden');
}

// Panel técnico original (solo dev): campos crudos de billing para afinar la
// detección de cancelación. Accesible desde "ver datos técnicos".
window.__skoopTechDiag = function _techFieldScan() {
  const members = State.members || [];
  if (!members.length) { toast('No hay miembros — escanea primero'); return; }

  const RELEVANT = /(cancel|status|period|expire|expir|sub|paid|paying|renew|bill|plan|ltv|trial|valid|active|churn|tier|premium|vip|standard|membership|mb[a-z]|mm[a-z])/i;
  const fields = {}; // key -> { count, samples:Set, matchedCancel }
  let withMetadata = 0;

  for (const m of members) {
    const raw = m._raw || {};
    const buckets = [raw.member?.metadata, raw.metadata, raw.member, raw].filter((o) => o && typeof o === 'object');
    let any = false;
    for (const bucket of buckets) {
      for (const [k, v] of Object.entries(bucket)) {
        if (v == null || typeof v === 'object') continue;
        if (!RELEVANT.test(k)) continue;
        any = true;
        if (!fields[k]) fields[k] = { count: 0, samples: new Set(), cancel: /cancel/i.test(k) };
        fields[k].count++;
        if (fields[k].samples.size < 4) fields[k].samples.add(String(v).slice(0, 60));
      }
    }
    if (any) withMetadata++;
  }

  const sorted = Object.entries(fields).sort((a, b) => {
    if (a[1].cancel !== b[1].cancel) return a[1].cancel ? -1 : 1; // cancel-* arriba
    return b[1].count - a[1].count;
  });

  const detected = members.filter((m) => m.subStatus === 'cancelling').length;
  const detectedCanceled = members.filter((m) => m.subStatus === 'canceled').length;

  const rows = sorted.map(([k, info]) => `
    <tr style="${info.cancel ? 'background:#ef444415' : ''}">
      <td style="font-family:monospace;color:${info.cancel ? '#ef4444' : 'var(--purple-light)'};font-weight:600">${esc(k)}${info.cancel ? ' 🎯' : ''}</td>
      <td style="color:var(--text-muted)">${info.count}</td>
      <td style="color:var(--text-muted);font-size:12px">${esc([...info.samples].join('  ·  '))}</td>
    </tr>`).join('');

  q('modal-content').innerHTML = `
    <div class="modal-member-name" style="margin-bottom:6px">🔍 Diagnóstico de campos (billing / cancelación)</div>
    <p style="color:var(--text-muted);font-size:13px;line-height:1.5">
      Escaneé <strong>${members.length}</strong> miembros (${withMetadata} con metadata). Detección actual de "por irse":
      <strong style="color:#ef4444">${detected} cancelando</strong> · ${detectedCanceled} cancelados.<br>
      Las filas marcadas 🎯 contienen <em>"cancel"</em> en su nombre. Si ves un campo que indica cancelación
      y NO está detectándose, pásame su nombre y valor de ejemplo y afino la regla.
    </p>
    ${sorted.length ? `
      <div style="max-height:380px;overflow:auto;border:1px solid var(--surface3);border-radius:8px;margin-top:8px">
        <table class="members-table" style="font-size:13px">
          <thead><tr><th>Campo</th><th>Miembros</th><th>Ejemplos</th></tr></thead>
          <tbody>${rows}</tbody>
        </table>
      </div>` : `<p style="color:var(--text-dim)">No se encontraron campos de metadata. ¿Ya escaneaste a las miembros?</p>`}
    <div style="margin-top:16px;display:flex;gap:8px"><button class="btn btn-ghost" data-act="diag-back">← Volver</button><button class="btn btn-ghost" data-act="close-modal">Cerrar</button></div>
  `;
  q('member-modal-overlay').classList.remove('hidden');
};
window.__skoopDiagBack = () => diagnoseCancelFields();

// Construye un CSV de miembros con las columnas ricas de Fase 1.
function buildMembersCsv(members) {
  const riskLabel = { active: 'Activo', silent: 'Silencio', atRisk: 'En riesgo', churned: 'Churn', unknown: 'Sin datos' };
  const subLabel = { cancelling: 'Por irse (canceló, activa)', canceled: 'Cancelada', active: 'Activa' };
  const headers = ['Nombre', 'Usuario', 'Email', 'WhatsApp', 'Instagram', 'Nicho', 'Rol', 'Nivel', 'Tier', 'Estado', 'Suscripción', 'Cancela el', 'Días para irse', 'Se unió', 'Meses en la comunidad', 'Días sin post', 'Posts', 'Comentarios', 'LTV (USD)', 'Plan'];
  const rows = members.map((m) => [
    m.name || '', m.username || '', m.email || '', m.whatsapp || '', m.instagram || '', m.niche || '',
    m.role || '', m.level ?? '', m.tier || '', riskLabel[m.riskLevel] || '',
    m.subStatus ? (subLabel[m.subStatus] || m.subStatus) : '',
    m.cancelAt ? formatDate(m.cancelAt) : '', m.daysUntilCancel ?? '',
    m.joinedAt ? formatDate(m.joinedAt) : '', m.monthsInCommunity ?? '',
    m.daysSinceLastPost ?? '', m.postCount ?? '', m.commentCount ?? '',
    m.ltv != null ? m.ltv : '', m.planPrice != null ? m.planPrice : '',
  ]);
  return [headers, ...rows].map((r) => r.map(_csvCell).join(',')).join('\r\n');
}

function _csvCell(v) {
  const s = String(v ?? '');
  return /[",\r\n]/.test(s) ? '"' + s.replace(/"/g, '""') + '"' : s;
}

// ── OVERVIEW ──────────────────────────────────────────────
function renderOverview() {
  const { stats, members, feed, endpoints } = State;
  if (!stats) { renderNoData(); return; }

  const { totalMembers, riskBuckets, totalPosts } = stats;

  setText('kpi-total', fmt(totalMembers));
  setText('kpi-active', fmt(riskBuckets?.active || 0));
  setText('kpi-silent', fmt(riskBuckets?.silent || 0));
  setText('kpi-risk', fmt(riskBuckets?.atRisk || 0));
  setText('kpi-churned', fmt(riskBuckets?.churned || 0));
  setText('kpi-posts', fmt(totalPosts));

  // Donut chart
  renderDonut(riskBuckets, totalMembers);

  // 🆘 Rescate prioritario — a quién escribir primero (urgentes arriba), usando
  // el mismo score que el priorizador (cancela + primer mes/nivel 1 + pocos días).
  const scoredAll = members
    .map((m) => ({ m, ...retentionScore(m) }))
    .filter((x) => x.score > 0)
    .sort((a, b) => b.score - a.score);
  const realUrgent = scoredAll.length;
  // Gratis: solo una probada (3) del rescate prioritario.
  const scored = scoredAll.slice(0, isPro() ? 10 : 3);
  const rescueHidden = isPro() ? 0 : Math.max(0, realUrgent - scored.length);

  const riskList = q('risk-list-overview');
  if (scored.length === 0) {
    riskList.innerHTML = `<div style="color:var(--text-dim);font-size:12px;padding:8px 0">Nadie urgente por rescatar ahora. 🎉</div>`;
  } else {
    riskList.innerHTML = scored.map(({ m, reasons }) => {
      const initials = (m.name || '?').split(' ').map((w) => w[0]).slice(0, 2).join('');
      return `
        <div class="mini-member" data-mid="${esc(String(m.id))}" style="cursor:pointer">
          <div class="mini-avatar">${m.avatarUrl ? `<img src="${esc(m.avatarUrl)}" alt="">` : esc(initials)}</div>
          <div class="mini-info" style="min-width:0">
            <div class="mini-name">${esc(m.name || m.username || 'Sin nombre')}</div>
            <div style="display:flex;gap:3px;flex-wrap:wrap;margin-top:3px">${reasons.slice(0, 3).join(' ')}</div>
          </div>
          ${m.whatsapp ? `<a class="btn btn-sm" href="https://wa.me/${esc(m.whatsapp.replace(/[^\d]/g, ''))}" target="_blank" style="background:#25D366;color:#fff;flex-shrink:0">📱</a>` : ''}
        </div>`;
    }).join('');
    riskList.querySelectorAll('.mini-member').forEach((el) => {
      el.addEventListener('click', (e) => {
        if (e.target.closest('a')) return; // no abrir modal al tocar el link de WhatsApp
        const m = members.find((x) => String(x.id) === el.dataset.mid);
        if (m) openMemberModal(m);
      });
    });
    if (rescueHidden > 0) riskList.innerHTML += lockCardHTML(rescueHidden, 'por rescatar');
  }

  // Feed preview
  const feedEl = q('feed-overview');
  const recentPosts = feed.slice(0, 5);
  if (recentPosts.length === 0) {
    feedEl.innerHTML = `<div style="color:var(--text-dim);font-size:12px;padding:8px 0">Sin posts indexados. Navega al Feed de tu comunidad.</div>`;
  } else {
    feedEl.innerHTML = recentPosts.map((p) => `
      <div class="feed-preview-item">
        <span class="feed-preview-author">${esc(p.authorName || 'Miembro')}</span>
        <span class="feed-preview-time">${p.createdAt ? relTime(new Date(p.createdAt).getTime()) : ''}</span>
        ${p.content ? `<div class="feed-preview-text">${esc(p.content)}</div>` : ''}
      </div>
    `).join('');
  }

  // Endpoints
  setText('endpoint-count-tag', `${endpoints.length} endpoints`);
  const endpointsEl = q('endpoints-overview');
  endpointsEl.innerHTML = endpoints.slice(0, 20).map((e) => `
    <div class="endpoint-chip">
      <span class="method">${esc(e.method)}</span>${esc(e.url)}
    </div>
  `).join('') || '<span style="color:var(--text-dim);font-size:12px">Navega en Skool para descubrir endpoints.</span>';
}

function renderDonut(buckets, total) {
  if (!buckets || !total) return;

  const data = [
    { label: 'Activos', count: buckets.active || 0, color: '#10b981' },
    { label: 'Silenciosos', count: buckets.silent || 0, color: '#f59e0b' },
    { label: 'En riesgo', count: buckets.atRisk || 0, color: '#ef4444' },
    { label: 'Churn', count: buckets.churned || 0, color: '#4a5568' },
  ].filter((d) => d.count > 0);

  const svg = q('donut-chart');
  const cx = 90, cy = 90, r = 68, stroke = 16;
  const circumference = 2 * Math.PI * r;
  const multi = data.length > 1;
  // Pequeño separador entre segmentos (en unidades de longitud de arco).
  const gap = multi ? circumference * 0.018 : 0;

  // Aclara un color hex para el gradiente (look con volumen).
  const lighten = (hex, amt) => {
    const n = parseInt(hex.slice(1), 16);
    const cl = (v) => Math.max(0, Math.min(255, Math.round(v + amt)));
    return `#${(cl(n >> 16) << 16 | cl((n >> 8) & 255) << 8 | cl(n & 255)).toString(16).padStart(6, '0')}`;
  };

  const defs = data.map((d, i) => `
    <linearGradient id="donut-grad-${i}" x1="0" y1="0" x2="1" y2="1">
      <stop offset="0%" stop-color="${lighten(d.color, 38)}"/>
      <stop offset="100%" stop-color="${d.color}"/>
    </linearGradient>`).join('');

  let offset = 0;
  const arcs = data.map((d, i) => {
    const seg = (d.count / total) * circumference;
    const dash = Math.max(seg - gap, multi ? 1.5 : seg);
    const arc = `<circle
      cx="${cx}" cy="${cy}" r="${r}"
      fill="none"
      stroke="url(#donut-grad-${i})"
      stroke-width="${stroke}"
      stroke-linecap="round"
      stroke-dasharray="${dash} ${circumference - dash}"
      stroke-dashoffset="${-offset}"
      transform="rotate(-90 ${cx} ${cy})"
    />`;
    offset += seg;
    return arc;
  });

  svg.innerHTML = `
    <defs>
      ${defs}
      <filter id="donut-shadow" x="-20%" y="-20%" width="140%" height="140%">
        <feDropShadow dx="0" dy="2" stdDeviation="3" flood-color="#000" flood-opacity="0.45"/>
      </filter>
    </defs>
    <circle cx="${cx}" cy="${cy}" r="${r}" fill="none" stroke="#191e33" stroke-width="${stroke}"/>
    <g filter="url(#donut-shadow)">${arcs.join('')}</g>
    <text x="${cx}" y="${cy - 4}" text-anchor="middle" fill="#f1f5f9" font-size="26" font-weight="800" letter-spacing="-0.5">${fmt(total)}</text>
    <text x="${cx}" y="${cy + 15}" text-anchor="middle" fill="#64748b" font-size="11" font-weight="600" letter-spacing="0.5">miembros</text>
  `;

  const legend = q('donut-legend');
  legend.innerHTML = data.map((d) => {
    const pct = Math.round((d.count / total) * 100);
    return `
    <div class="legend-item">
      <div class="legend-dot" style="background:${d.color}"></div>
      <span class="legend-label">${d.label}</span>
      <span class="legend-count">${fmt(d.count)} <span class="legend-pct">${pct}%</span></span>
    </div>`;
  }).join('');
}

function renderNoData() {
  setText('kpi-total', '—');
  setText('kpi-active', '—');
  setText('kpi-silent', '—');
  setText('kpi-risk', '—');
  setText('kpi-churned', '—');
  setText('kpi-posts', '—');
}

// ── PIPELINE ──────────────────────────────────────────────
function renderPipeline() {
  const { members } = State;

  const columns = [
    { id: 'active', label: '🟢 Activos', color: '#10b981' },
    { id: 'silent', label: '🟡 Silenciosos', color: '#f59e0b' },
    { id: 'atRisk', label: '🔴 En riesgo', color: '#ef4444' },
    { id: 'churned', label: '⚫ Churn', color: '#4a5568' },
    { id: 'unknown', label: '❔ Sin datos', color: '#374151' },
  ];

  const af = State.attentionFilter || 'all';
  const passesAttn = (m) => af === 'all' || (m._attn || 'none') === af;

  const board = q('pipeline-board');
  board.innerHTML = columns.map((col) => {
    const colAll = members
      .filter((m) => (m.riskLevel || 'unknown') === col.id)
      .filter(passesAttn)
      .sort((a, b) => (b.daysSinceLastPost || 0) - (a.daysSinceLastPost || 0));
    const colReal = colAll.length;
    const colGate = freeGate(colAll);
    const colMembers = colGate.list.slice(0, 40);

    const cards = colMembers.map((m) => {
      const mini = (m._attn === 'attending' || m._attn === 'done') && m._attnBy
        ? `<span class="attn-mini" title="${m._attn === 'done' ? 'Atendido' : 'Atendiéndose'} por ${esc(m._attnBy)}">${esc(initialsOf(m._attnBy))}</span>`
        : '';
      return `
      <div class="pipeline-card" data-id="${esc(m.id)}">
        <div class="pipeline-card-name">${attnDot(m._attn)}${esc(m.name || m.username || 'Sin nombre')}${mini}</div>
        <div class="pipeline-card-meta">
          ${m.daysSinceLastPost != null ? `${m.daysSinceLastPost}d sin post` : 'Sin datos de actividad'}
          ${m.postCount ? ` · ${m.postCount} posts` : ''}
        </div>
      </div>`;
    }).join('');

    return `
      <div class="pipeline-col">
        <div class="pipeline-header">
          <div class="pipeline-title">
            <span style="width:8px;height:8px;border-radius:50%;background:${col.color};display:inline-block"></span>
            ${col.label}
          </div>
          <span class="pipeline-count">${colReal}</span>
        </div>
        <div class="pipeline-cards" id="pipe-${col.id}">
          ${cards || '<div style="color:var(--text-dim);font-size:11px;padding:8px">Sin miembros</div>'}
          ${colGate.capped ? lockCardHTML(colReal - colMembers.length, 'bloqueados') : ''}
        </div>
      </div>
    `;
  }).join('');

  // Click en tarjeta → modal
  board.querySelectorAll('.pipeline-card').forEach((el) => {
    el.addEventListener('click', () => {
      const member = members.find((m) => m.id === el.dataset.id);
      if (member) openMemberModal(member);
    });
  });
}

// ── TABLA DE MIEMBROS ────────────────────────────────────
function setupMembersTable() {
  q('member-search').addEventListener('input', (e) => {
    State.memberSearch = e.target.value.toLowerCase();
    State.memberPage = 0;
    renderMembersTable();
  });

  q('member-sort').addEventListener('change', (e) => {
    State.memberSort = e.target.value;
    renderMembersTable();
  });

  document.querySelectorAll('#page-members .filter-btn').forEach((btn) => {
    if (btn.id === 'btn-states-legend') return; // no es un filtro, abre la leyenda
    btn.addEventListener('click', () => {
      document.querySelectorAll('#page-members .filter-btn').forEach((b) => {
        if (b.id !== 'btn-states-legend') b.classList.remove('active');
      });
      btn.classList.add('active');
      State.memberFilter = btn.dataset.filter;
      State.memberPage = 0;
      renderMembersTable();
    });
  });

  q('btn-prev-page').addEventListener('click', () => {
    if (State.memberPage > 0) { State.memberPage--; renderMembersTable(); }
  });

  q('btn-next-page').addEventListener('click', () => {
    State.memberPage++;
    renderMembersTable();
  });

  const btnLegend = q('btn-states-legend');
  if (btnLegend) btnLegend.addEventListener('click', (e) => { e.preventDefault(); showStatesLegend(); });
}

// ℹ️ Explica qué significa cada estado (la duda de "no sé qué es Silencio").
function showStatesLegend() {
  const row = (color, title, desc) => `
    <div style="display:flex;gap:10px;padding:8px 0;border-bottom:1px solid var(--surface3)">
      <div style="font-size:18px;line-height:1.2">${color}</div>
      <div><div style="font-weight:700">${title}</div><div style="color:var(--text-muted);font-size:13px">${desc}</div></div>
    </div>`;
  openInfoModal('ℹ️ ¿Qué significa cada estado?', `
    <p style="color:var(--text-muted);font-size:13px;margin:0 0 8px">El estado se calcula por los <strong>días sin actividad</strong> (último post o comentario en la comunidad):</p>
    ${row('🟢', 'Activo', 'Actividad en los últimos <strong>7 días</strong>. Todo bien.')}
    ${row('🟡', 'Silencio', 'Entre <strong>8 y 30 días</strong> sin actividad. Se están empezando a enfriar — todavía se recuperan fácil con un mensaje.')}
    ${row('🔴', 'En riesgo', 'Entre <strong>31 y 60 días</strong> sin actividad. Necesitan un empujón ya.')}
    ${row('⚫', 'Churn', 'Más de <strong>60 días</strong> sin actividad. Probablemente ya se desconectaron.')}
    ${row('🔴', 'Por irse', 'Aparece en la pestaña "Cancelling" de Skool: <strong>canceló pero su acceso sigue activo</strong> hasta el fin del periodo pagado. Son las más urgentes de retener.')}
    ${row('🆘', 'Rescatable', 'Nueva (≤2 meses) que ya se apagó. Vale mucho la pena reactivarla antes de que se enfríe del todo.')}
    ${row('🆘🔥', 'Rescate urgente', 'Canceló <strong>+</strong> es su primer mes o nivel 1. Hay que escribirle <strong>hoy</strong>.')}
  `);
}

// Nuevos (≤2 meses) que ya se apagaron — prioridad de rescate.
function isRescue(m) {
  return m.monthsInCommunity != null && m.monthsInCommunity <= 2 &&
    (m.riskLevel === 'silent' || m.riskLevel === 'atRisk' || m.riskLevel === 'churned');
}

// 🆘🔥 Rescate URGENTE — el cruce que pidió Andrea: canceló (se va) + es su
// primer mes + está en nivel 1 + le quedan pocos días. Son las que hay que
// rescatar YA. Basta con "está cancelando" + (primer mes O nivel 1) para
// marcarla urgente; los días restantes elevan aún más la prioridad.
function isUrgentRescue(m) {
  if (m.subStatus !== 'cancelling') return false;
  const firstMonth = m.monthsInCommunity != null && m.monthsInCommunity <= 1;
  const level1 = m.level != null && Number(m.level) <= 1;
  return firstMonth || level1;
}

// Días restantes antes de perder acceso. Exacto si Skool lo expone (cancelAt /
// accessUntil); si no, lo ESTIMA por el ciclo de cobro (mensual/anual) a partir
// de la fecha de ingreso. Devuelve { days, exact }.
function daysUntilLeave(m) {
  if (m.daysUntilCancel != null) return { days: m.daysUntilCancel, exact: true };
  if (m.subStatus !== 'cancelling' || !m.joinedAt) return { days: null, exact: false };
  const joined = new Date(m.joinedAt);
  if (Number.isNaN(joined.getTime())) return { days: null, exact: false };
  const now = new Date();
  const next = new Date(joined);
  if (m.planInterval === 'year') {
    next.setFullYear(now.getFullYear());
    if (next <= now) next.setFullYear(now.getFullYear() + 1);
  } else {
    // mensual por defecto: próximo aniversario de día-de-mes
    next.setFullYear(now.getFullYear(), now.getMonth(), joined.getDate());
    if (next <= now) next.setMonth(next.getMonth() + 1);
  }
  const days = Math.max(0, Math.ceil((next - now) / 86400000));
  return { days, exact: false };
}

// Razones posibles de churn para una miembro — chips cortos y deterministas.
// 'kind' colorea el chip. Negativo = comentario marcado por el análisis de
// sentimiento (se persiste en window.__negativeMembers tras correrlo).
function churnReasons(m) {
  const r = [];
  if (m.subStatus === 'cancelling') r.push({ label: '💳 Canceló', kind: 'cancel' });
  else if (m.subStatus === 'canceled') r.push({ label: '💳 Ya canceló', kind: 'cancel' });
  if (m.monthsInCommunity != null && m.monthsInCommunity <= 1) r.push({ label: '🌱 Primer mes', kind: 'new' });
  if (m.level != null && Number(m.level) <= 1) r.push({ label: '🔰 Nivel 1', kind: 'level' });
  if (window.__negativeMembers && window.__negativeMembers[String(m.id)]) r.push({ label: '😠 Mensaje negativo', kind: 'neg' });
  if (m.daysSinceLastPost != null && m.daysSinceLastPost > 30) r.push({ label: `💤 ${m.daysSinceLastPost}d inactiva`, kind: 'inactive' });
  return r;
}

function reasonChipsHTML(reasons) {
  const bg = { cancel: '#ef444433', new: '#22c55e33', level: 'var(--surface3)', neg: '#ef444455', inactive: '#f59e0b33' };
  return reasons.map((r) => `<span class="reason-chip" style="background:${bg[r.kind] || 'var(--surface3)'}">${esc(r.label)}</span>`).join(' ');
}

function _setFilterBadge(id, count) {
  const el = q(id);
  if (!el) return;
  if (count > 0) { el.textContent = count; el.classList.remove('hidden'); }
  else el.classList.add('hidden');
}

function renderMembersTable() {
  let list = State.members;

  // Fase 2: badge con el conteo de "por irse" en el filtro.
  const cancellingCount = State.members.filter((m) => m.subStatus === 'cancelling').length;
  _setFilterBadge('cancel-count-badge', cancellingCount);
  // Rescate: nuevos (≤2 meses) que ya se apagaron.
  _setFilterBadge('rescue-count-badge', State.members.filter(isRescue).length);

  // Filtros: riesgo, 'cancelling' (por irse), 'rescue' (nuevos apagados), 'level1'
  if (State.memberFilter === 'cancelling') {
    list = list.filter((m) => m.subStatus === 'cancelling' || m.subStatus === 'canceled');
  } else if (State.memberFilter === 'rescue') {
    list = list.filter(isRescue);
  } else if (State.memberFilter === 'level1') {
    list = list.filter((m) => Number(m.level) <= 1 && m.level != null);
  } else if (State.memberFilter) {
    list = list.filter((m) => m.riskLevel === State.memberFilter);
  }

  // Búsqueda
  if (State.memberSearch) {
    list = list.filter((m) => {
      const name = (m.name || '').toLowerCase();
      const user = (m.username || '').toLowerCase();
      return name.includes(State.memberSearch) || user.includes(State.memberSearch);
    });
  }

  // Ordenar
  switch (State.memberSort) {
    case 'urgent':
      list = list.map((m) => ({ m, s: retentionScore(m).score }))
        .sort((a, b) => b.s - a.s || (b.m.daysSinceLastPost ?? 0) - (a.m.daysSinceLastPost ?? 0))
        .map((x) => x.m);
      break;
    case 'days':
      list = list.sort((a, b) => (b.daysSinceLastPost ?? 999) - (a.daysSinceLastPost ?? 999));
      break;
    case 'level':
      list = list.sort((a, b) => (a.level ?? 999) - (b.level ?? 999));
      break;
    case 'name':
      list = list.sort((a, b) => (a.name || '').localeCompare(b.name || ''));
      break;
    case 'posts':
      list = list.sort((a, b) => (b.postCount || 0) - (a.postCount || 0));
      break;
    case 'risk':
    default: {
      const order = { churned: 0, atRisk: 1, silent: 2, active: 3, unknown: 4 };
      list = list.sort((a, b) => (order[a.riskLevel] ?? 5) - (order[b.riskLevel] ?? 5));
    }
  }

  // Modo gratis: solo el 15% de la lista visible.
  const realTotal = list.length;
  const gate = freeGate(list);
  list = gate.list;

  const total = list.length;
  setText('member-count-label', gate.capped
    ? `${fmt(total)} de ${fmt(realTotal)} (modo gratis · 15%)`
    : `${total} miembro${total !== 1 ? 's' : ''}`);

  // Paginación
  const maxPage = Math.max(0, Math.ceil(total / State.PAGE_SIZE) - 1);
  if (State.memberPage > maxPage) State.memberPage = maxPage;

  const start = State.memberPage * State.PAGE_SIZE;
  const page = list.slice(start, start + State.PAGE_SIZE);

  setText('pagination-label', `Página ${State.memberPage + 1} de ${Math.max(1, maxPage + 1)}`);
  q('btn-prev-page').disabled = State.memberPage === 0;
  q('btn-next-page').disabled = State.memberPage >= maxPage;

  const tbody = q('members-tbody');

  if (page.length === 0) {
    tbody.innerHTML = `<tr><td colspan="7" style="text-align:center;color:var(--text-dim);padding:32px">Sin miembros con estos filtros.</td></tr>`;
    return;
  }

  tbody.innerHTML = page.map((m) => {
    const initials = (m.name || '?').split(' ').map((w) => w[0]).slice(0, 2).join('');
    const avatarHTML = m.avatarUrl
      ? `<img src="${esc(m.avatarUrl)}" alt="" data-initials="${esc(initials)}">`
      : initials;

    const riskConfig = {
      active: { label: '🟢 Activo', cls: 'badge-active' },
      silent: { label: '🟡 Silencio', cls: 'badge-silent' },
      atRisk: { label: '🔴 En riesgo', cls: 'badge-atRisk' },
      churned: { label: '⚫ Churn', cls: 'badge-churned' },
    };
    const risk = riskConfig[m.riskLevel] || { label: '❔', cls: '' };

    // Fase 2: chip "Por irse" + días restantes (exactos o estimados) + razones.
    let cancelBadge = '';
    if (m.subStatus === 'cancelling') {
      const dl = daysUntilLeave(m);
      const d = dl.days != null ? ` ${dl.exact ? '' : '~'}${dl.days}d` : '';
      const urgent = isUrgentRescue(m);
      cancelBadge = `<span class="status-badge ${urgent ? '' : 'badge-cancel'}" ${urgent ? 'style="background:#dc2626;color:#fff"' : ''} title="Canceló pero su acceso sigue activo${dl.days != null ? ` (${dl.exact ? '' : 'aprox. '}${dl.days} días restantes)` : ' — Skool no expone la fecha exacta de corte'}">${urgent ? '🆘🔥 Rescate urgente' : '🔴 Por irse'}${d}</span>`;
    } else if (m.subStatus === 'canceled') {
      cancelBadge = `<span class="status-badge badge-cancel" title="Suscripción cancelada">⚫ Canceló</span>`;
    }
    // Razones de churn (chips) — se muestran para quien va a irse o está en riesgo real.
    const reasons = (m.subStatus === 'cancelling' || m.subStatus === 'canceled' || m.riskLevel === 'atRisk' || m.riskLevel === 'churned') ? churnReasons(m) : [];
    const reasonsHTML = reasons.length ? `<div class="reason-row">${reasonChipsHTML(reasons)}</div>` : '';

    return `
      <tr style="cursor:pointer" data-id="${esc(m.id)}">
        <td>
          <div class="td-member">
            <div class="td-avatar">${avatarHTML}</div>
            <div>
              <div class="td-name">${attnDot(m._attn)}${esc(m.name || 'Sin nombre')}${m.tier ? ` <span class="tier-chip tier-${esc(m.tier.toLowerCase())}">${esc(m.tier)}</span>` : ''}</div>
              ${m.username ? `<div class="td-username">@${esc(m.username)}</div>` : ''}
            </div>
          </div>
        </td>
        <td><span class="status-badge ${risk.cls}">${risk.label}</span>${cancelBadge ? '<br>' + cancelBadge : ''}${reasonsHTML}</td>
        <td style="color:var(--text-muted)">${m.level != null ? `Nivel ${esc(String(m.level))}${Number(m.level) <= 1 ? ' 🔰' : ''}` : (m.role ? esc(m.role) : '—')}</td>
        <td style="color:var(--text-muted)">${m.joinedAt ? formatDate(m.joinedAt) + (m.monthsInCommunity != null ? ` · <strong>${m.monthsInCommunity}m</strong>` : '') : '—'}</td>
        <td style="font-weight:600;color:${m.daysSinceLastPost > 30 ? 'var(--red)' : 'inherit'}">
          ${m.daysSinceLastPost != null ? m.daysSinceLastPost + 'd' : '—'}
        </td>
        <td style="color:var(--text-muted)">${m.niche ? `🎯 ${esc(m.niche)}` : (m.postCount ?? '—')}</td>
        <td style="white-space:nowrap">
          ${m.whatsapp ? `<a class="btn btn-ghost btn-sm" href="https://wa.me/${esc(m.whatsapp.replace(/[^\d]/g, ''))}" target="_blank" title="Escribir por WhatsApp" style="color:#25D366;padding:4px 7px">📱</a>` : ''}
          <button class="btn btn-ghost btn-sm btn-email-from-table" data-id="${esc(m.id)}">✉</button>
        </td>
      </tr>
    `;
  }).join('');

  // Candado al final de la última página en modo gratis.
  if (gate.capped && State.memberPage >= maxPage) {
    tbody.innerHTML += `<tr><td colspan="7" style="padding:0;background:transparent">${lockCardHTML(realTotal - total, 'miembros')}</td></tr>`;
  }

  // Click en fila → modal
  tbody.querySelectorAll('tr[data-id]').forEach((row) => {
    row.addEventListener('click', (e) => {
      if (e.target.closest('a, .btn-email-from-table')) return;
      const member = State.members.find((m) => m.id === row.dataset.id);
      if (member) openMemberModal(member);
    });
  });

  // Botón email directo
  tbody.querySelectorAll('.btn-email-from-table').forEach((btn) => {
    btn.addEventListener('click', (e) => {
      e.stopPropagation();
      const member = State.members.find((m) => m.id === btn.dataset.id);
      if (member) {
        State.selectedEmailMember = member;
        navigateTo('emails');
        renderEmailsPage(member);
      }
    });
  });
}

// ── FEED ──────────────────────────────────────────────────
function setupFeed() {
  q('feed-search').addEventListener('input', (e) => {
    const q2 = e.target.value.toLowerCase();
    renderFeedPage(q2);
  });
}

function renderFeedPage(searchQuery) {
  let posts = State.feed;
  const sq = searchQuery || q('feed-search').value.toLowerCase();

  if (sq) {
    posts = posts.filter((p) =>
      (p.content || '').toLowerCase().includes(sq) ||
      (p.authorName || '').toLowerCase().includes(sq)
    );
  }

  const realPosts = posts.length;
  const feedGate = freeGate(posts);
  posts = feedGate.list;
  setText('feed-count-label', feedGate.capped ? `${fmt(posts.length)} de ${fmt(realPosts)} (modo gratis · 15%)` : `${posts.length} posts`);

  const container = q('feed-grid');
  if (posts.length === 0) {
    container.innerHTML = `<div style="color:var(--text-dim);font-size:13px;padding:24px;text-align:center">Sin posts indexados${sq ? ' con esa búsqueda' : ''}.<br/>Navega al Feed de tu comunidad en Skool.</div>`;
    return;
  }

  container.innerHTML = posts.slice(0, 60).map((p) => {
    const initials = (p.authorName || '?').split(' ').map((w) => w[0]).slice(0, 2).join('');
    const avatarHTML = p.authorAvatarUrl
      ? `<img src="${esc(p.authorAvatarUrl)}" alt="" data-initials="${esc(initials)}">`
      : initials;

    return `
      <div class="feed-card">
        <div class="feed-card-header">
          <div class="feed-author-avatar">${avatarHTML}</div>
          <div class="feed-author-info">
            <div class="feed-author-name">${esc(p.authorName || 'Miembro')}</div>
            <div class="feed-post-time">${p.createdAt ? relTime(new Date(p.createdAt).getTime()) : ''}</div>
          </div>
          ${p.isPinned ? '<span style="color:var(--purple-light);font-size:11px;font-weight:600">📌 Fijado</span>' : ''}
        </div>
        ${p.content ? `<div class="feed-card-body">${esc(p.content.slice(0, 500))}${p.content.length > 500 ? '...' : ''}</div>` : ''}
        <div class="feed-card-stats">
          ${p.likesCount ? `❤️ ${p.likesCount}` : ''}
          ${p.commentsCount ? `💬 ${p.commentsCount} comentarios` : ''}
        </div>
      </div>
    `;
  }).join('');

  if (feedGate.capped) container.innerHTML += lockCardHTML(realPosts - posts.length, 'posts');
}

// ── EMAILS IA ────────────────────────────────────────────
function setupEmails() {
  // Tone buttons
  document.querySelectorAll('.opt-btn').forEach((btn) => {
    btn.addEventListener('click', () => {
      document.querySelectorAll('.opt-btn').forEach((b) => b.classList.remove('active'));
      btn.classList.add('active');
      State.emailTone = btn.dataset.tone;
    });
  });

  q('btn-generate-email').addEventListener('click', () => {
    const m = State.selectedEmailMember;
    if (!m) return;
    const email = generateEmail(m);
    q('email-output').value = email;
  });

  q('btn-copy-email').addEventListener('click', () => {
    const text = q('email-output').value;
    if (!text) return;
    navigator.clipboard.writeText(text).then(() => toast('✓ Email copiado'));
  });

  q('btn-copy-prompt').addEventListener('click', () => {
    const m = State.selectedEmailMember;
    if (!m) return;
    const prompt = buildClaudePrompt(m);
    navigator.clipboard.writeText(prompt).then(() => toast('✓ Prompt copiado — pégalo en Claude'));
  });
}

function renderEmailsPage(preselected) {
  if (!isPro()) {
    const sec = q('page-emails');
    if (sec) sec.innerHTML = lockedScreenHTML('Emails con IA',
      'Genera emails de reactivación personalizados para tus miembros en riesgo, en tu voz. Desbloquéalo con tu llave.');
    return;
  }
  const { members } = State;

  // Priorizar at-risk y churned
  const prioritized = members
    .filter((m) => ['atRisk', 'churned', 'silent'].includes(m.riskLevel))
    .sort((a, b) => {
      const order = { churned: 0, atRisk: 1, silent: 2 };
      return (order[a.riskLevel] ?? 3) - (order[b.riskLevel] ?? 3) ||
             (b.daysSinceLastPost ?? 0) - (a.daysSinceLastPost ?? 0);
    });

  const list = q('email-member-list');
  list.innerHTML = prioritized.slice(0, 40).map((m) => {
    const initials = (m.name || '?').split(' ').map((w) => w[0]).slice(0, 2).join('');
    const badge = m.riskLevel === 'churned' ? '⚫' : m.riskLevel === 'atRisk' ? '🔴' : '🟡';
    return `
      <div class="email-member-item" data-id="${esc(m.id)}">
        <div class="mini-avatar">${esc(initials)}</div>
        <div>
          <div class="email-member-name">${badge} ${esc(m.name || m.username || 'Sin nombre')}</div>
          <div class="email-member-days">${m.daysSinceLastPost != null ? `${m.daysSinceLastPost} días sin actividad` : 'Actividad desconocida'}</div>
        </div>
      </div>
    `;
  }).join('') || '<div style="color:var(--text-dim);font-size:12px;padding:16px">Sin miembros en riesgo detectados.</div>';

  // Click en miembro
  list.querySelectorAll('.email-member-item').forEach((el) => {
    el.addEventListener('click', () => {
      list.querySelectorAll('.email-member-item').forEach((e) => e.classList.remove('selected'));
      el.classList.add('selected');
      const m = State.members.find((mb) => mb.id === el.dataset.id);
      if (m) selectEmailMember(m);
    });
  });

  // Preseleccionar si viene de tabla / modal. Importante: seleccionar SIEMPRE
  // al miembro aunque no esté en la lista priorizada (ej. miembro activo o
  // "por irse" que no cae en atRisk/churned/silent) — antes el botón ✉ no hacía
  // nada en esos casos porque dependía de encontrar el item en la lista.
  const target = preselected || State.selectedEmailMember;
  if (target) {
    const item = list.querySelector(`[data-id="${esc(target.id)}"]`);
    list.querySelectorAll('.email-member-item').forEach((e) => e.classList.remove('selected'));
    if (item) item.classList.add('selected');
    selectEmailMember(target);
  }
}

// Abre el generador de email para un miembro concreto (desde la ficha o la tabla).
window.openEmailFor = (id) => {
  const m = State.members.find((x) => String(x.id) === String(id));
  if (!m) return;
  State.selectedEmailMember = m;
  closeModal();
  navigateTo('emails');
  renderEmailsPage(m);
};

function selectEmailMember(m) {
  State.selectedEmailMember = m;

  q('email-placeholder').classList.add('hidden');
  q('email-generator').classList.remove('hidden');

  const initials = (m.name || '?').split(' ').map((w) => w[0]).slice(0, 2).join('');
  setText('email-avatar', initials);
  setText('email-member-name', m.name || m.username || 'Sin nombre');

  const meta = [];
  if (m.riskLevel === 'churned') meta.push('⚫ Churn probable');
  else if (m.riskLevel === 'atRisk') meta.push('🔴 En riesgo');
  else if (m.riskLevel === 'silent') meta.push('🟡 Silencioso');
  if (m.daysSinceLastPost != null) meta.push(`${m.daysSinceLastPost} días sin actividad`);
  if (m.role) meta.push(m.role);

  setText('email-member-meta', meta.join(' · ') || '—');

  // Auto-generar
  q('email-output').value = generateEmail(m);
}

function generateEmail(m) {
  const tone = State.emailTone;
  const name = m.name ? m.name.split(' ')[0] : 'amiga';
  const days = m.daysSinceLastPost;
  const includeDays = q('opt-days').checked;
  const includeName = q('opt-name').checked;
  const includeCta = q('opt-cta').checked;

  const saludo = includeName ? `Hola ${name},` : 'Hola,';

  const inactividad = includeDays && days != null
    ? `Han pasado ${days} días desde que te vi por la comunidad,`
    : 'Hace un tiempo que no te vemos por aquí,';

  const templates = {
    warm: `${saludo}

${inactividad} y quería escribirte directamente.

Sé que la vida se pone intensa, y a veces necesitamos un empujón para volver al ritmo. En la comunidad han estado pasando cosas increíbles: miembros alcanzando sus primeros 1K seguidores, otros consiguiendo sus primeras ventas, y el nivel de las conversaciones ha subido mucho.

Tienes todo lo que necesitas dentro de F100K. Solo necesitas volver.

${includeCta ? `¿Hay algo específico que te detuvo? Responde este mensaje y hablamos. Estoy aquí para asegurarme de que saques el máximo provecho de tu membresía.` : ''}

Con cariño,
Andrea`,

    urgent: `${saludo}

${inactividad} y tengo algo importante que contarte.

Esta semana activamos [INSERTAR NOVEDAD O RECURSO NUEVO]. Sería una pena que te lo perdieras después de todo lo que has invertido.

${includeCta ? `Entra hoy a la comunidad → [LINK] y pon tu nombre en el hilo de presentación. Solo eso. Ya el resto viene solo.` : ''}

Te espero adentro,
Andrea`,

    personal: `${saludo}

Te escribo porque me importas, y porque noto cuando alguien desaparece.

${inactividad} y eso me hizo pensar en ti. ¿Estás bien? ¿Hubo algo en la comunidad que no funcionó para ti?

Tu voz y tu presencia le añaden valor a todos los demás. No tienes que tener todo resuelto para aparecer — de hecho, los mejores momentos en la comunidad pasan cuando alguien llega con una pregunta real.

${includeCta ? `¿Puedo hacer algo para que volver sea más fácil? Solo responde este mensaje.` : ''}

Un abrazo,
Andrea`,
  };

  return templates[tone] || templates.warm;
}

function buildClaudePrompt(m) {
  const name = m.name || m.username || 'la miembro';
  const days = m.daysSinceLastPost;
  const community = State.stats?.name || State.slug || 'mi comunidad';

  return `Eres el asistente de Andrea, fundadora de FÓRMULA 100K, una comunidad de creadores de contenido hispanos.

Contexto del miembro:
- Nombre: ${name}
- Comunidad: ${community}
- Días sin actividad: ${days != null ? days : 'desconocido'}
- Nivel de riesgo: ${m.riskLevel || 'desconocido'}
- Posts totales: ${m.postCount ?? 'sin datos'}

Redacta un email de reactivación personalizado con el tono de Andrea: directo, cálido, auténtico, sin relleno. El email debe:
1. Mencionar específicamente cuánto tiempo lleva sin aparecer
2. Recordarle algo concreto del valor de la comunidad
3. Hacerle una pregunta directa (¿qué te detuvo?)
4. Terminar con un CTA claro y sin presión

NO uses frases genéricas de marketing. Escribe como si fuera un mensaje de WhatsApp formal.
Máximo 200 palabras.`;
}

// ── DELEGACIÓN DE CLICKS ──────────────────────────────────
// Manifest V3 bloquea los handlers inline (onclick="...") por CSP: el HTML que
// inyectamos con innerHTML jamás dispara su onclick. Por eso TODOS los botones
// generados por JS usan data-act="<acción>" (+ data-a1/data-a2 para argumentos)
// y este único listener delegado en document los ejecuta.
function setupClickDelegation() {
  document.addEventListener('click', (ev) => {
    const el = ev.target.closest('[data-act]');
    if (!el) return;
    const { act, a1, a2 } = el.dataset;
    const copy = (txt) =>
      navigator.clipboard
        .writeText(txt || '')
        .then(() => window.__skoopToast && window.__skoopToast('✓ Copiado'));
    switch (act) {
      case 'upsell': window.__skoopUpsell && window.__skoopUpsell(); break;
      case 'activate': window.__skoopActivate && window.__skoopActivate(); break;
      case 'close-modal': closeModal(); break;
      case 'save-brain': window.__saveBrain && window.__saveBrain('a1' in el.dataset ? a1 : undefined); break;
      case 'copy': ev.preventDefault(); copy(el.dataset.copy); break;
      case 'dl-themes': window.__skoopDownloadThemes && window.__skoopDownloadThemes(); break;
      case 'run-themes': window.__skoopRunThemes && window.__skoopRunThemes(); break;
      case 'dl-reactivation': window.__skoopDownloadReactivation && window.__skoopDownloadReactivation(); break;
      case 'gen-dm': window.generateAiDm && window.generateAiDm(a1); break;
      case 'draft-reply': window.draftReply && window.draftReply(a1); break;
      case 'view-member': ev.preventDefault(); window.openMemberById && window.openMemberById(a1); break;
      case 'open-member': closeModal(); window.openMemberById && window.openMemberById(a1); break;
      case 'open-email': window.openEmailFor && window.openEmailFor(a1); break;
      case 'tech-diag': window.__skoopTechDiag && window.__skoopTechDiag(); break;
      case 'diag-back': window.__skoopDiagBack && window.__skoopDiagBack(); break;
      case 'set-attention': window.setAttention && window.setAttention(a1, a2); break;
      case 'toggle-label': window.toggleLabel && window.toggleLabel(a1, a2); break;
      case 'create-label': window.createLabel && window.createLabel(a1); break;
      case 'add-note': window.addTeamNote && window.addTeamNote(a1); break;
      case 'change-collab': ev.preventDefault(); window.changeCollab && window.changeCollab(); break;
    }
  });

  // Delegación de 'change' (inputs/checkboxes inyectados — onchange inline también
  // está bloqueado por CSP).
  document.addEventListener('change', (ev) => {
    const el = ev.target.closest('[data-change]');
    if (!el) return;
    const change = el.dataset.change;
    if (change === 'brain-file') window.__brainFile && window.__brainFile(ev);
    else if (change === 'toggle-plan') window.togglePlanItem && window.togglePlanItem(el.dataset.a1, el.checked);
  });

  // Fallback de avatares rotos: los 'error' de <img> no burbujean, así que se
  // capturan en fase de captura (onerror inline también lo bloquea la CSP).
  document.addEventListener('error', (ev) => {
    const img = ev.target;
    if (img && img.tagName === 'IMG' && img.dataset && 'initials' in img.dataset) {
      const parent = img.parentElement;
      if (parent) parent.textContent = img.dataset.initials;
    }
  }, true);
}

// ── MODAL DE MIEMBRO ──────────────────────────────────────
function setupModal() {
  q('modal-close').addEventListener('click', closeModal);
  q('member-modal-overlay').addEventListener('click', (e) => {
    if (e.target === q('member-modal-overlay')) closeModal();
  });
}

// ── Bloques de equipo dentro del modal del miembro ──
function attentionBlockHTML(m) {
  const cur = m._attn || 'none';
  const seg = ['none', 'attending', 'done'].map((s) => {
    const a = ATTN[s];
    return `<button class="attn-seg${cur === s ? ' active' : ''}" data-act="set-attention" data-a1="${esc(String(m.id))}" data-a2="${s}">${a.emoji} ${a.label}</button>`;
  }).join('');
  const who = (cur !== 'none' && m._attnBy)
    ? `<div class="attn-who">${cur === 'done' ? 'Atendido' : 'Atendiéndose'} por <strong>${esc(m._attnBy)}</strong>${m._attnAt ? ` · ${relTime(new Date(m._attnAt).getTime())}` : ''}</div>`
    : '';
  const me = State.collab
    ? `Tú: <strong>${esc(State.collab)}</strong> · <a href="#" data-act="change-collab">cambiar</a>`
    : `<a href="#" data-act="change-collab">✍ Pon tu nombre de colaborador</a>`;
  return `
    <div class="modal-section-title">Atención</div>
    <div class="attn-seg-row">${seg}</div>
    ${who}
    <div class="attn-me">${me}</div>`;
}

function labelsBlockHTML(m) {
  const ids = m._labelIds || [];
  const chips = (State.team.labels || []).map((l) => {
    const on = ids.includes(l.id);
    const style = on ? `background:${esc(l.color)};border-color:${esc(l.color)};color:#fff` : '';
    return `<button class="label-chip${on ? ' on' : ''}" style="${style}" data-act="toggle-label" data-a1="${esc(String(m.id))}" data-a2="${esc(l.id)}">${on ? '✓ ' : ''}${esc(l.name)}</button>`;
  }).join('');
  return `
    <div class="modal-section-title">Etiquetas</div>
    <div class="label-row">
      ${chips}
      <button class="label-chip add" data-act="create-label" data-a1="${esc(String(m.id))}">+ etiqueta</button>
    </div>`;
}

function notesBlockHTML(m) {
  const notes = (State.team.notes && (State.team.notes[m.id] || State.team.notes[String(m.id)])) || [];
  const items = notes.map((n) => `
    <div class="team-note">
      <div class="team-note-head"><strong>${esc(n.author || '—')}</strong>${n.createdAt ? ` · ${relTime(new Date(n.createdAt).getTime())}` : ''}</div>
      <div class="team-note-body">${esc(n.body)}</div>
    </div>`).join('');
  return `
    <div class="modal-section-title">Notas del equipo</div>
    <div class="team-notes">${items || '<div style="color:var(--text-dim);font-size:12px">Sin notas todavía. Deja la primera.</div>'}</div>
    <div class="team-note-add">
      <textarea id="team-note-input" rows="2" placeholder="Escribe una nota para el equipo..."></textarea>
      <button class="btn btn-purple btn-sm" data-act="add-note" data-a1="${esc(String(m.id))}">Agregar</button>
    </div>`;
}

function teamBlocksHTML(m) {
  if (!isPro()) {
    return `
      <div class="modal-section-title">Atención del equipo</div>
      <div class="lock-card" data-act="upsell" style="margin:0">
        <div class="lock-emoji">🔒</div>
        <div>
          <div class="lock-title">Marca atendido, etiqueta y deja notas</div>
          <div class="lock-sub">Función Pro — para ti y tu equipo con tu llave.</div>
        </div>
      </div>`;
  }
  return attentionBlockHTML(m) + labelsBlockHTML(m) + notesBlockHTML(m);
}

function openMemberModal(m) {
  window.__openMemberId = m.id;
  const initials = (m.name || '?').split(' ').map((w) => w[0]).slice(0, 2).join('');
  const avatarHTML = m.avatarUrl
    ? `<img src="${esc(m.avatarUrl)}" alt="" data-initials="${esc(initials)}">`
    : initials;

  const riskConfig = {
    active: { label: '🟢 Activo', cls: 'badge-active' },
    silent: { label: '🟡 Silencioso', cls: 'badge-silent' },
    atRisk: { label: '🔴 En riesgo', cls: 'badge-atRisk' },
    churned: { label: '⚫ Churn probable', cls: 'badge-churned' },
  };
  const risk = riskConfig[m.riskLevel] || { label: '❔ Sin datos', cls: '' };

  q('modal-content').innerHTML = `
    <div class="modal-member-header">
      <div class="modal-avatar">${avatarHTML}</div>
      <div>
        <div class="modal-member-name">${esc(m.name || 'Sin nombre')}</div>
        <div class="modal-member-meta">
          ${m.username ? `@${esc(m.username)} · ` : ''}
          <span class="status-badge ${risk.cls}" style="font-size:11px">${risk.label}</span>
        </div>
      </div>
    </div>

    ${m.subStatus === 'cancelling' ? `
      <div class="cancel-banner">
        🔴 <strong>Canceló su suscripción</strong> — su acceso sigue activo${m.daysUntilCancel != null ? ` <strong>${m.daysUntilCancel} día${m.daysUntilCancel === 1 ? '' : 's'}</strong> más` : ''}${m.cancelAt ? ` (hasta el ${formatDate(m.cancelAt)})` : ''}.
        ${m.whatsapp ? `Escríbele YA para retenerla.` : ''}
      </div>
    ` : (m.subStatus === 'canceled' ? `
      <div class="cancel-banner cancel-banner-gone">⚫ Suscripción cancelada${m.cancelAt ? ` el ${formatDate(m.cancelAt)}` : ''}.</div>
    ` : '')}

    ${(m.subStatus !== 'cancelling' && isRescue(m)) ? `
      <div class="rescue-banner">
        🆘 <strong>Rescatable</strong> — entró hace ${m.monthsInCommunity} mes${m.monthsInCommunity === 1 ? '' : 'es'} y ya lleva ${m.daysSinceLastPost} días sin actividad${Number(m.level) <= 1 ? ' · nivel 1 🔰' : ''}. Reactívala antes de que se enfríe del todo.
      </div>
    ` : ''}

    <div class="modal-stats">
      <div class="modal-stat">
        <div class="modal-stat-number" style="color:${m.daysSinceLastPost > 30 ? 'var(--red)' : 'var(--text)'}">
          ${m.daysSinceLastPost != null ? m.daysSinceLastPost : '—'}
        </div>
        <div class="modal-stat-label">Días sin post</div>
      </div>
      <div class="modal-stat">
        <div class="modal-stat-number">${m.postCount ?? '—'}</div>
        <div class="modal-stat-label">Posts totales</div>
      </div>
      <div class="modal-stat">
        <div class="modal-stat-number">${m.commentCount ?? '—'}</div>
        <div class="modal-stat-label">Comentarios</div>
      </div>
    </div>

    <div class="modal-section-title">Información</div>
    <div class="modal-timeline">
      ${m.role ? `<div class="modal-timeline-item"><div class="timeline-dot"></div><span>Rol: <strong>${esc(m.role)}</strong></span></div>` : ''}
      ${m.level ? `<div class="modal-timeline-item"><div class="timeline-dot"></div><span>Nivel: <strong>${esc(m.level)}</strong></span></div>` : ''}
      ${m.joinedAt ? `<div class="modal-timeline-item"><div class="timeline-dot"></div><span>Se unió: <strong>${formatDate(m.joinedAt)}</strong></span></div>` : ''}
      ${m.lastSeenAt ? `<div class="modal-timeline-item"><div class="timeline-dot"></div><span>Última actividad: <strong>${relTime(new Date(m.lastSeenAt).getTime())}</strong></span></div>` : ''}
      ${m.monthsInCommunity != null ? `<div class="modal-timeline-item"><div class="timeline-dot"></div><span>En la comunidad: <strong>${m.monthsInCommunity} meses</strong></span></div>` : ''}
      ${m.lastCommentAt ? `<div class="modal-timeline-item"><div class="timeline-dot"></div><span>Último comentario: <strong>${relTime(m.lastCommentAt)}</strong>${m.lastCommentText ? ` — “${esc(m.lastCommentText)}”` : ''}</span></div>` : ''}
    </div>

    ${(m.whatsapp || m.instagram || m.niche || m.tier || m.ltv != null || (m.planPrice != null && m.planPrice !== m.ltv)) ? `
      <div class="modal-section-title">Contacto y plan</div>
      <div class="modal-timeline">
        ${m.tier ? `<div class="modal-timeline-item"><div class="timeline-dot"></div><span>Tier: <span class="tier-chip tier-${esc(m.tier.toLowerCase())}">${esc(m.tier)}</span></span></div>` : ''}
        ${m.whatsapp ? `<div class="modal-timeline-item"><div class="timeline-dot"></div><span>WhatsApp: <a href="https://wa.me/${esc(m.whatsapp.replace(/[^\d]/g, ''))}" target="_blank" style="color:#25D366;font-weight:600">${esc(m.whatsapp)}</a></span></div>` : ''}
        ${m.instagram ? `<div class="modal-timeline-item"><div class="timeline-dot"></div><span>Instagram: <a href="https://instagram.com/${esc(m.instagram.replace(/^@/, ''))}" target="_blank" style="color:#E1306C;font-weight:600">${esc(m.instagram)}</a></span></div>` : ''}
        ${m.niche ? `<div class="modal-timeline-item"><div class="timeline-dot"></div><span>Nicho: <strong>${esc(m.niche)}</strong></span></div>` : ''}
        ${m.ltv != null ? `<div class="modal-timeline-item"><div class="timeline-dot"></div><span>Valor (LTV): <strong>$${m.ltv}</strong></span></div>` : ''}
        ${(m.planPrice != null && m.planPrice !== m.ltv) ? `<div class="modal-timeline-item"><div class="timeline-dot"></div><span>Plan: <strong>${typeof m.planPrice === 'number' ? '$' + m.planPrice : esc(m.planPrice)}</strong></span></div>` : ''}
      </div>
    ` : ''}

    ${(m.surveyAnswers && m.surveyAnswers.length) ? `
      <div class="modal-section-title">Respuestas de ingreso</div>
      <div style="font-size:13px;display:flex;flex-direction:column;gap:8px">
        ${m.surveyAnswers.map((a) => `<div><div style="color:var(--text-dim);font-size:12px">${esc(a.question)}</div><div style="color:var(--text)">${esc(a.answer)}</div></div>`).join('')}
      </div>
    ` : ''}

    ${teamBlocksHTML(m)}

    <div style="margin-top:20px;display:flex;gap:8px;flex-wrap:wrap">
      ${m.whatsapp ? `<a class="btn btn-purple" href="https://wa.me/${esc(m.whatsapp.replace(/[^\d]/g, ''))}" target="_blank" style="background:#25D366;border-color:#25D366">📱 WhatsApp</a>` : ''}
      <button class="btn btn-purple" data-act="gen-dm" data-a1="${esc(String(m.id))}">✨ DM con IA</button>
      <button class="btn btn-ghost" data-act="open-email" data-a1="${esc(String(m.id))}">✉ Email</button>
      <button class="btn btn-ghost" data-act="close-modal">Cerrar</button>
    </div>
    <div id="ai-dm-result"></div>
  `;

  q('member-modal-overlay').classList.remove('hidden');
}

// Exponer navigateTo + toast para los onclick inline del modal
window.navigateTo = navigateTo;
window.__skoopToast = (msg) => toast(msg);

function closeModal() {
  q('member-modal-overlay').classList.add('hidden');
}

// ── HELPERS ───────────────────────────────────────────────
function q(id) { return document.getElementById(id); }
function setText(id, val) { const el = q(id); if (el) el.textContent = val; }
function esc(str) {
  return String(str || '').replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;').replace(/'/g, '&#39;');
}

function fmt(n) { return n != null ? Number(n).toLocaleString('es-MX') : '—'; }

function relTime(ts) {
  if (!ts) return '';
  const diff = Date.now() - ts;
  const m = Math.floor(diff / 60000);
  const h = Math.floor(diff / 3600000);
  const d = Math.floor(diff / 86400000);
  if (m < 1) return 'ahora';
  if (m < 60) return `hace ${m}m`;
  if (h < 24) return `hace ${h}h`;
  if (d === 1) return 'ayer';
  if (d < 7) return `hace ${d} días`;
  if (d < 30) return `hace ${Math.floor(d / 7)} semanas`;
  return `hace ${Math.floor(d / 30)} meses`;
}

function formatDate(dateStr) {
  if (!dateStr) return '—';
  try {
    return new Date(dateStr).toLocaleDateString('es-MX', { year: 'numeric', month: 'short', day: 'numeric' });
  } catch { return String(dateStr); }
}

function datestamp() { return new Date().toISOString().split('T')[0]; }

function downloadFile(content, filename, mime) {
  const blob = new Blob([content], { type: mime });
  const url = URL.createObjectURL(blob);
  const a = document.createElement('a');
  a.href = url; a.download = filename; a.click();
  setTimeout(() => URL.revokeObjectURL(url), 1000);
}

function toast(msg, ms = 2500) {
  const el = q('toast');
  el.textContent = msg;
  el.classList.remove('hidden');
  clearTimeout(el._t);
  el._t = setTimeout(() => el.classList.add('hidden'), ms);
}

function miniMemberHTML(m) {
  const initials = (m.name || '?').split(' ').map((w) => w[0]).slice(0, 2).join('');
  const badgeMap = { active: 'badge-active', silent: 'badge-silent', atRisk: 'badge-atRisk', churned: 'badge-churned' };
  const labelMap = { active: '🟢', silent: '🟡', atRisk: '🔴', churned: '⚫' };
  return `
    <div class="mini-member">
      <div class="mini-avatar">${m.avatarUrl ? `<img src="${esc(m.avatarUrl)}" alt="">` : esc(initials)}</div>
      <div class="mini-info">
        <div class="mini-name">${esc(m.name || m.username || 'Sin nombre')}</div>
        <div class="mini-meta">${m.daysSinceLastPost != null ? `${m.daysSinceLastPost}d sin post` : 'Sin datos'}</div>
      </div>
      <span class="mini-badge ${badgeMap[m.riskLevel] || ''}">${labelMap[m.riskLevel] || '❔'}</span>
    </div>
  `;
}
