/* ============================================================================
 * apps/web/components/launch/index.tsx
 * ----------------------------------------------------------------------------
 * Cenora Launch — Founder IR-CRM (v3.3, full surface)
 *
 * Replaces the slim <LaunchModule> from cenora-bundle-6-capital.tsx.
 * This bundle owns the entire Launch admin surface — a horizontal sub-nav
 * with 7 screens:
 *
 *   Pipeline       — investor kanban + filter pills + page search
 *   Compose        — mail-merge composer with template + deck attach
 *   Engagement     — opens × time heatmap, per-slide bar chart, top engaged
 *   Templates      — saved email templates with merge tokens
 *   Link generator — opaque-token URLs, attach to investor, history
 *   Deck versions  — deck library with REAL drag-drop upload (in-memory)
 *   Settings       — sender, security, notifications, default deck
 *
 * PRODUCTION lift-out:
 *   • All CN_* arrays → API responses (apps/web/lib/api/launch.ts)
 *   • deckStore — swap the in-memory impl with multipart upload to
 *     POST /api/decks (PHP/MySQL on cPanel). Function signatures stay
 *     identical; no UI changes needed.
 *   • LaunchModule routes off internal sub-state, NOT URL. If the team
 *     wants /capital/launch/compose etc., wire each tab to push() in
 *     setLaunchScreen and read from useRouter().
 * ============================================================================ */

;/*__IIFE_WRAP_START__*/(function(){
console.log('[bundle-7-launch] starting');

/* ─── EXTENDED CONTACT MODEL ──────────────────────────────────────────────
 * Adds 'paused' stage. Re-exports as CN_LAUNCH_CONTACTS so we don't fight
 * the original CN_CONTACTS in cenora-bundle-6-capital.tsx (which downstream
 * <InvestorHubModule> still reads). */
type LStage =
  | "sent" | "opened" | "meeting" | "discussion"
  | "diligence" | "committed" | "signed" | "passed" | "paused"

type LContact = {
  id: string
  first: string; last: string
  firm: string; role: string
  email: string
  ticket: string; target: string
  stage: LStage
  sentAt: string
  lastOpen: string|null
  opens: number
  totalTime: number          /* seconds in deck */
  slidesViewed: number
  tags: string[]
  starred?: boolean
  token: string              /* opaque 6-char URL token */
  deckVersionId?: string|null  /* override; falls back to default deck */
}

/* Pre-seeded pipeline — 24 contacts across all 9 stages.
 * Hand-tuned for realistic ratios and money math (~$1.05M committed). */
const L_CONTACTS: LContact[] = [
  { id: "c01", first: "Sarah",   last: "Chen",      firm: "Sequoia Capital",       role: "Partner",            email: "sarah@sequoiacap.com",   ticket: "Strategic Seed",  target: "$500K – $1M",  stage: "discussion", sentAt: "May 14", lastOpen: "2 days ago", opens: 4, totalTime: 872,  slidesViewed: 16, tags: ["Tier 1", "AI thesis"], starred: true, token: "k7m2qp" },
  { id: "c02", first: "David",   last: "Park",      firm: "Acme Ventures",         role: "General Partner",    email: "david@acme.vc",          ticket: "Strategic Seed",  target: "$250K",        stage: "meeting",    sentAt: "May 18", lastOpen: "Yesterday",  opens: 3, totalTime: 492,  slidesViewed: 12, tags: ["Tier 1", "Multi-entity"], token: "j3n9wt" },
  { id: "c03", first: "Marcus",  last: "Rosenberg", firm: "Coastal Family Office", role: "Director",           email: "marcus@coastalfo.com",   ticket: "Capital Partner", target: "$500K – $1M",  stage: "diligence",  sentAt: "May 10", lastOpen: "3h ago",     opens: 7, totalTime: 1480, slidesViewed: 17, tags: ["Family Office", "PH ties"], starred: true, token: "h8s4lz" },
  { id: "c04", first: "Aisha",   last: "Kumar",     firm: "Antler",                role: "Operating Partner",  email: "aisha@antler.co",        ticket: "Bridge — SAFE",   target: "$100K",        stage: "opened",     sentAt: "May 22", lastOpen: "1 day ago",  opens: 2, totalTime: 217,  slidesViewed: 9,  tags: ["Bridge", "Quick"], token: "f2p7bd" },
  { id: "c05", first: "Mei Lin", last: "Tan",       firm: "Wavemaker Partners",    role: "Partner",            email: "meilin@wavemaker.vc",    ticket: "Strategic Seed",  target: "$300K",        stage: "discussion", sentAt: "May 12", lastOpen: "4 days ago", opens: 3, totalTime: 645,  slidesViewed: 14, tags: ["SEA", "Tier 2"], token: "g6c1xv" },
  { id: "c06", first: "Tom",     last: "Becker",    firm: "Individual · ex-CFO",   role: "Angel",              email: "tom.becker@gmail.com",   ticket: "Personal",        target: "$50K",         stage: "committed",  sentAt: "May 04", lastOpen: "Yesterday",  opens: 5, totalTime: 1180, slidesViewed: 17, tags: ["Operator", "CFO"], starred: true, token: "r5t8nm" },
  { id: "c07", first: "Reina",   last: "Hayashi",   firm: "Jafco Asia",            role: "Investment Manager", email: "reina@jafco.asia",       ticket: "Strategic Seed",  target: "$500K",        stage: "meeting",    sentAt: "May 19", lastOpen: "2 days ago", opens: 3, totalTime: 580,  slidesViewed: 14, tags: ["Japan", "Tier 1"], token: "w4y3qf" },
  { id: "c08", first: "Paolo",   last: "Aquino",    firm: "Foxmont Capital",       role: "Principal",          email: "paolo@foxmont.com",      ticket: "Strategic Seed",  target: "$250K",        stage: "diligence",  sentAt: "May 08", lastOpen: "Today",      opens: 6, totalTime: 1320, slidesViewed: 17, tags: ["PH ties", "Tier 1"], starred: true, token: "d9k6ph" },
  { id: "c09", first: "Hannah",  last: "Schmidt",   firm: "Heliconia Capital",     role: "Senior Associate",   email: "hannah@heliconia.sg",    ticket: "Strategic Seed",  target: "$400K",        stage: "opened",     sentAt: "May 21", lastOpen: "3 days ago", opens: 1, totalTime: 184,  slidesViewed: 8,  tags: ["SG", "Sovereign"], token: "z1v5gj" },
  { id: "c10", first: "Jin-Ho",  last: "Lee",       firm: "Strong Ventures",       role: "Partner",            email: "jinho@strongvc.com",     ticket: "Bridge — SAFE",   target: "$150K",        stage: "sent",       sentAt: "May 26", lastOpen: null,         opens: 0, totalTime: 0,    slidesViewed: 0,  tags: ["Korea", "Tier 2"], token: "b3u7eq" },
  { id: "c11", first: "Ben",     last: "Hoffmann",  firm: "Insight Partners",      role: "Vice President",     email: "ben@insightpartners.com",ticket: "Capital Partner", target: "$1M+",         stage: "sent",       sentAt: "May 27", lastOpen: null,         opens: 0, totalTime: 0,    slidesViewed: 0,  tags: ["Tier 1", "Late"], token: "x8m1sa" },
  { id: "c12", first: "Naya",    last: "Patel",     firm: "Inflection.vc",         role: "Founding Partner",   email: "naya@inflection.vc",     ticket: "Strategic Seed",  target: "$500K",        stage: "discussion", sentAt: "May 13", lastOpen: "1 day ago",  opens: 4, totalTime: 820,  slidesViewed: 15, tags: ["AI-native", "Tier 1"], token: "n2l9wb" },
  { id: "c13", first: "Ron",     last: "Velasco",   firm: "Kickstart Ventures",    role: "Partner",            email: "ron@kickstart.ph",       ticket: "Strategic Seed",  target: "$250K",        stage: "passed",     sentAt: "May 02", lastOpen: "May 06",     opens: 2, totalTime: 320,  slidesViewed: 10, tags: ["PH", "Pass: thesis"], token: "p7e4kc" },
  { id: "c14", first: "Elena",   last: "Volkov",    firm: "Number 8 Bio · Family", role: "Trustee",            email: "elena@n8bio-trust.com",  ticket: "Capital Partner", target: "$1M – $2M",    stage: "diligence",  sentAt: "May 06", lastOpen: "Today",      opens: 8, totalTime: 1660, slidesViewed: 17, tags: ["Family", "Anchor"], starred: true, token: "c5h2di" },
  { id: "c15", first: "Kenji",   last: "Watanabe",  firm: "Mizuho Capital",        role: "Director",           email: "kenji@mizuho-cap.jp",    ticket: "Capital Partner", target: "$1M",          stage: "signed",     sentAt: "Apr 28", lastOpen: "Yesterday",  opens: 5, totalTime: 990,  slidesViewed: 17, tags: ["Japan", "Anchor"], starred: true, token: "a3v6mp" },
  { id: "c16", first: "Olivia",  last: "King",      firm: "Crescent Cove",         role: "Investment Director",email: "olivia@crescentcove.sg", ticket: "Bridge — SAFE",   target: "$100K",        stage: "passed",     sentAt: "May 01", lastOpen: "May 04",     opens: 1, totalTime: 88,   slidesViewed: 5,  tags: ["Bridge", "Pass: stage"], token: "y6r2tn" },
  { id: "c17", first: "James",   last: "Whitford",  firm: "Whitford Holdings",     role: "Founder",            email: "james@whitfordco.com",   ticket: "Capital Partner", target: "$750K",        stage: "signed",     sentAt: "Apr 22", lastOpen: "3 days ago", opens: 6, totalTime: 1140, slidesViewed: 17, tags: ["Family Office", "Anchor"], starred: true, token: "u4q9ke" },
  { id: "c18", first: "Priya",   last: "Subramanian", firm: "Sequoia India",       role: "Principal",          email: "priya@sequoiaindia.com", ticket: "Strategic Seed",  target: "$400K",        stage: "committed",  sentAt: "May 09", lastOpen: "Today",      opens: 7, totalTime: 1290, slidesViewed: 17, tags: ["Tier 1", "India"], token: "i8w1mb" },
  { id: "c19", first: "Rachel",  last: "Okonkwo",   firm: "Pearl Ventures",        role: "Partner",            email: "rachel@pearl.vc",        ticket: "Strategic Seed",  target: "$300K",        stage: "diligence",  sentAt: "May 11", lastOpen: "5h ago",     opens: 6, totalTime: 1102, slidesViewed: 16, tags: ["AI thesis", "Tier 2"], token: "o3f7vx" },
  { id: "c20", first: "Yuki",    last: "Tanaka",    firm: "Aozora VC",             role: "Senior Associate",   email: "yuki@aozora.vc",         ticket: "Strategic Seed",  target: "$200K",        stage: "meeting",    sentAt: "May 17", lastOpen: "1 day ago",  opens: 2, totalTime: 410,  slidesViewed: 11, tags: ["Japan", "Tier 2"], token: "t5k4we" },
  { id: "c21", first: "Nathan",  last: "Reyes",     firm: "Reyes Family Capital",  role: "Principal",          email: "nathan@reyesfc.ph",      ticket: "Bridge — SAFE",   target: "$120K",        stage: "sent",       sentAt: "May 26", lastOpen: null,         opens: 0, totalTime: 0,    slidesViewed: 0,  tags: ["Family Office", "PH"], token: "m8s2jr" },
  { id: "c22", first: "Thomas",  last: "Aldridge",  firm: "Aldridge Family",       role: "Managing Trustee",   email: "thomas@aldridgefam.com", ticket: "Capital Partner", target: "$1M",          stage: "sent",       sentAt: "May 27", lastOpen: null,         opens: 0, totalTime: 0,    slidesViewed: 0,  tags: ["Family Office", "Anchor"], token: "l1d6av" },
  { id: "c23", first: "Kenji",   last: "Sato",      firm: "Ascent Partners",       role: "Partner",            email: "ksato@ascent.jp",        ticket: "Strategic Seed",  target: "$350K",        stage: "opened",     sentAt: "May 23", lastOpen: "2 days ago", opens: 1, totalTime: 152,  slidesViewed: 7,  tags: ["Japan", "Tier 2"], token: "e9b3hp" },
  { id: "c24", first: "Lena",    last: "Mostowska", firm: "Northwall Capital",     role: "Investment Director",email: "lena@northwall.eu",      ticket: "Strategic Seed",  target: "$400K",        stage: "paused",     sentAt: "May 05", lastOpen: "May 12",     opens: 3, totalTime: 540,  slidesViewed: 13, tags: ["EU", "Tier 2", "On hold"], token: "v7g5co" },
]

/* Stage definitions — full 9-stage order, including paused.
 * Keys match LStage union. Color maps to Badge variant. */
const L_STAGES: { key: LStage; label: string; color: string; barClass: string }[] = [
  { key: "sent",       label: "Sent",          color: "neutral", barClass: "bg-ink-faint" },
  { key: "opened",     label: "Opened",        color: "info",    barClass: "bg-info" },
  { key: "meeting",    label: "Meeting",       color: "accent",  barClass: "bg-accent" },
  { key: "discussion", label: "Discussion",    color: "accent",  barClass: "bg-accent-deep" },
  { key: "diligence",  label: "Diligence",     color: "warn",    barClass: "bg-warn" },
  { key: "committed",  label: "Committed",     color: "success", barClass: "bg-success" },
  { key: "signed",     label: "Signed",        color: "success", barClass: "bg-brand" },
  { key: "passed",     label: "Passed",        color: "neutral", barClass: "bg-divider-strong" },
  { key: "paused",     label: "Paused",        color: "warn",    barClass: "bg-warn/60" },
]

/* Stages that appear as kanban columns. Passed + paused get strips below. */
const L_KANBAN_STAGES: LStage[] = ["sent","opened","meeting","discussion","diligence","committed","signed"]

/* Pretty short-form for the deck */
const SLIDES = [
  { n:1,  title:"Cover" },        { n:2,  title:"Problem" },         { n:3,  title:"Solution" },
  { n:4,  title:"Why Now" },      { n:5,  title:"Org Scope" },       { n:6,  title:"Product" },
  { n:7,  title:"Demo" },         { n:8,  title:"Customers" },       { n:9,  title:"Traction" },
  { n:10, title:"GTM" },          { n:11, title:"Market" },          { n:12, title:"Business Model" },
  { n:13, title:"Competition" },  { n:14, title:"Team" },            { n:15, title:"Financials" },
  { n:16, title:"The Ask" },      { n:17, title:"Roadmap" },
]

/* ============================================================================
 * DECK STORE — in-memory; production swap is a single file (POST /api/decks)
 * ============================================================================ */
type DeckRecord = {
  id: string
  name: string
  type: "pptx"|"pdf"|"docx"|"keynote"
  currentVersion: string
  sizeKB: number
  uploadedAt: string
  uploadedBy: string
  status: "active"|"draft"|"archived"
  isDefault: boolean
  sentCount: number
  lastSentAt: string|null
  audience: string
  description: string
  versions: { v: string; uploadedAt: string; uploadedBy: string; sizeKB: number; note: string; blob?: Blob }[]
  thumbColor: string  /* visual placeholder */
}

let _deckSeq = 100
const _deckListeners = new Set<() => void>()
let _decks: DeckRecord[] = [
  {
    id: "deck-001",
    name: "Cenora Seed Deck",
    type: "pptx",
    currentVersion: "v3.2",
    sizeKB: 4820,
    uploadedAt: "May 12, 2026",
    uploadedBy: "Maya C.",
    status: "active",
    isDefault: true,
    sentCount: 12,
    lastSentAt: "May 24, 2026",
    audience: "Institutional VCs",
    description: "Primary deck — full 17-slide narrative for Tier-1 institutional conversations. Master version, used unless an investor has an override.",
    versions: [
      { v: "v3.2", uploadedAt: "May 12, 2026", uploadedBy: "Maya C.", sizeKB: 4820, note: "Updated TAM slide; refreshed traction chart through April." },
      { v: "v3.1", uploadedAt: "Apr 30, 2026", uploadedBy: "Maya C.", sizeKB: 4710, note: "Team slide — added Marcus & Priya headshots." },
      { v: "v3.0", uploadedAt: "Apr 15, 2026", uploadedBy: "Maria A.", sizeKB: 4660, note: "Major restructure — narrative reordered around 'Org Scope'." },
    ],
    thumbColor: "#0D4A4A",
  },
  {
    id: "deck-002",
    name: "Executive Summary · 1-pager",
    type: "pdf",
    currentVersion: "v2.1",
    sizeKB: 380,
    uploadedAt: "May 20, 2026",
    uploadedBy: "Maya C.",
    status: "active",
    isDefault: false,
    sentCount: 8,
    lastSentAt: "May 26, 2026",
    audience: "Cold outreach · first touch",
    description: "Tight one-page summary for cold outreach. Lands faster than the full deck — investors who like it ask for v3.x.",
    versions: [
      { v: "v2.1", uploadedAt: "May 20, 2026", uploadedBy: "Maya C.", sizeKB: 380, note: "Bumped headline metric, tightened ask paragraph." },
      { v: "v2.0", uploadedAt: "Apr 28, 2026", uploadedBy: "Maya C.", sizeKB: 410, note: "Full rewrite — replaced two-pager with single-page." },
    ],
    thumbColor: "#1A6B6B",
  },
  {
    id: "deck-003",
    name: "Family Office Variant",
    type: "pptx",
    currentVersion: "v1.4",
    sizeKB: 5200,
    uploadedAt: "May 16, 2026",
    uploadedBy: "Maya C.",
    status: "active",
    isDefault: false,
    sentCount: 4,
    lastSentAt: "May 22, 2026",
    audience: "Family offices · trustees",
    description: "Same narrative, but heavier emphasis on multi-generational scope, governance, and capital-partner terms. Lighter on growth-stage VC vocabulary.",
    versions: [
      { v: "v1.4", uploadedAt: "May 16, 2026", uploadedBy: "Maya C.", sizeKB: 5200, note: "Added governance & ownership slide." },
      { v: "v1.3", uploadedAt: "May 02, 2026", uploadedBy: "Maya C.", sizeKB: 5050, note: "Capital-partner terms slide rewritten." },
    ],
    thumbColor: "#D4A96A",
  },
  {
    id: "deck-004",
    name: "Technical Deep-dive",
    type: "pdf",
    currentVersion: "v1.0",
    sizeKB: 1840,
    uploadedAt: "May 03, 2026",
    uploadedBy: "Maya C.",
    status: "active",
    isDefault: false,
    sentCount: 3,
    lastSentAt: "May 18, 2026",
    audience: "AI-thesis VCs · technical PMs",
    description: "Architecture, AI agents, data model. Sent after the main deck when an investor asks 'how does it actually work'.",
    versions: [
      { v: "v1.0", uploadedAt: "May 03, 2026", uploadedBy: "Maya C.", sizeKB: 1840, note: "First public version." },
    ],
    thumbColor: "#2A6FDB",
  },
  {
    id: "deck-005",
    name: "Data Room Index",
    type: "docx",
    currentVersion: "v0.4",
    sizeKB: 120,
    uploadedAt: "May 10, 2026",
    uploadedBy: "Maya C.",
    status: "draft",
    isDefault: false,
    sentCount: 1,
    lastSentAt: "May 12, 2026",
    audience: "Diligence-stage only",
    description: "Index of the data room — financials, contracts, cap table, IP. Shared only after a meeting + intent signal.",
    versions: [
      { v: "v0.4", uploadedAt: "May 10, 2026", uploadedBy: "Maya C.", sizeKB: 120, note: "Added Q1 financials section reference." },
    ],
    thumbColor: "#27704F",
  },
]

const deckStore = {
  list(): DeckRecord[] { return [..._decks] },
  active(): DeckRecord[] { return _decks.filter(d => d.status !== "archived") },
  get(id: string): DeckRecord|undefined { return _decks.find(d => d.id === id) },
  getDefault(): DeckRecord|undefined { return _decks.find(d => d.isDefault) ?? _decks[0] },
  subscribe(fn: () => void) { _deckListeners.add(fn); return () => _deckListeners.delete(fn) },
  _notify() { _deckListeners.forEach(fn => fn()) },

  /* Add a brand-new deck from a File (drag-drop / upload). Returns the new record. */
  upload(file: File, meta: { name?: string; audience?: string; description?: string } = {}): DeckRecord {
    const type = inferDeckType(file.name)
    const rec: DeckRecord = {
      id: "deck-" + (_deckSeq++),
      name: meta.name?.trim() || file.name.replace(/\.[^.]+$/, ""),
      type,
      currentVersion: "v1.0",
      sizeKB: Math.max(1, Math.round(file.size / 1024)),
      uploadedAt: "Just now",
      uploadedBy: "Maya C.",
      status: "active",
      isDefault: false,
      sentCount: 0,
      lastSentAt: null,
      audience: meta.audience || "Custom",
      description: meta.description || "Uploaded — no description yet.",
      versions: [{ v: "v1.0", uploadedAt: "Just now", uploadedBy: "Maya C.", sizeKB: Math.max(1, Math.round(file.size / 1024)), note: "Initial upload.", blob: file }],
      thumbColor: ["#0D4A4A","#1A6B6B","#D4A96A","#2A6FDB","#27704F","#C88A2E","#5C1F2A"][_decks.length % 7],
    }
    _decks = [rec, ..._decks]
    deckStore._notify()
    return rec
  },

  /* Push a new version onto an existing deck. */
  addVersion(deckId: string, file: File, note: string): DeckRecord|undefined {
    const d = _decks.find(x => x.id === deckId); if (!d) return
    const nextV = bumpVersion(d.currentVersion)
    const ver = { v: nextV, uploadedAt: "Just now", uploadedBy: "Maya C.", sizeKB: Math.max(1, Math.round(file.size / 1024)), note: note || "Updated.", blob: file }
    d.versions = [ver, ...d.versions]
    d.currentVersion = nextV
    d.sizeKB = ver.sizeKB
    d.uploadedAt = "Just now"
    _decks = [..._decks]
    deckStore._notify()
    return d
  },

  setDefault(deckId: string) {
    _decks = _decks.map(d => ({ ...d, isDefault: d.id === deckId }))
    deckStore._notify()
  },

  archive(deckId: string, archived = true) {
    _decks = _decks.map(d => d.id === deckId ? { ...d, status: archived ? "archived" : "active" } as DeckRecord : d)
    deckStore._notify()
  },

  remove(deckId: string) {
    _decks = _decks.filter(d => d.id !== deckId)
    /* If default was deleted, promote the first remaining active deck. */
    if (!_decks.some(d => d.isDefault) && _decks.length > 0) _decks[0].isDefault = true
    deckStore._notify()
  },

  /* Update editable metadata (name / audience / description). */
  patch(deckId: string, patch: Partial<DeckRecord>) {
    _decks = _decks.map(d => d.id === deckId ? { ...d, ...patch } : d)
    deckStore._notify()
  },

  /* Trigger browser download of the current version (if a real Blob exists; else stub). */
  download(deckId: string) {
    const d = _decks.find(x => x.id === deckId); if (!d) return
    const v = d.versions[0]; const blob = v?.blob
    if (blob) {
      const url = URL.createObjectURL(blob)
      const a = document.createElement("a")
      a.href = url; a.download = `${d.name} ${v.v}.${d.type}`
      document.body.appendChild(a); a.click(); a.remove()
      setTimeout(() => URL.revokeObjectURL(url), 1000)
    } else {
      (window as any).cenoraToast?.(`${d.name} ${v?.v ?? ""}`, { variant: "info", sub: "Seed decks aren't downloadable in the demo — upload a real file to test." })
    }
  },
}
;(globalThis as any).deckStore = deckStore

function inferDeckType(name: string): DeckRecord["type"] {
  const ext = name.split(".").pop()?.toLowerCase() ?? ""
  if (ext === "pdf") return "pdf"
  if (ext === "docx" || ext === "doc") return "docx"
  if (ext === "key" || ext === "keynote") return "keynote"
  return "pptx"
}
function bumpVersion(v: string): string {
  const m = v.match(/^v(\d+)\.(\d+)$/); if (!m) return v + ".1"
  return `v${m[1]}.${parseInt(m[2]) + 1}`
}
function fmtSize(kb: number): string {
  if (kb < 1024) return kb + " KB"
  return (kb / 1024).toFixed(1) + " MB"
}

/* React hook to subscribe to deckStore changes. */
function useDecks(): DeckRecord[] {
  const [, force] = React.useReducer(x => x + 1, 0)
  React.useEffect(() => deckStore.subscribe(force), [])
  return deckStore.list()
}

/* ============================================================================
 * TEMPLATES — email templates with merge tokens
 * ============================================================================ */
type EmailTemplate = {
  id: string; name: string; use: string
  subject: string; body: string
  lastSent: string
}
const L_TEMPLATES: EmailTemplate[] = [
  {
    id: "t-intro",
    name: "Cold intro · first touch",
    use: "Investor who hasn't seen Cenora before. Warm but not familiar.",
    subject: "Cenora — the AI-native ERP. Quick look?",
    body: "Hi {{first_name}},\n\nI'm raising a seed round for Cenora — the AI-native operating platform we're building for multi-entity businesses. We have early signal from {{firm}}-type funds and would love your take.\n\nYour personal deck (opens in browser, takes ~6 min):\n  → {{deck_url}}\n\nHappy to grab 20 min next week if it resonates.\n\n— Maya",
    lastSent: "May 26",
  },
  {
    id: "t-warm",
    name: "Warm intro · referred by",
    use: "Mutual connection introduced you. Shorter, more direct.",
    subject: "Following up on the intro — Cenora",
    body: "Hi {{first_name}},\n\nThanks for the intro from our mutual contact. Sharing the Cenora deck — would love your perspective from {{firm}}.\n\n  → {{deck_url}}\n\nGrab time on my calendar if you want to chat: {{calendar_url}}\n\n— Maya",
    lastSent: "May 24",
  },
  {
    id: "t-followup",
    name: "Follow-up · 3-day nudge",
    use: "Investor opened the deck but hasn't replied yet.",
    subject: "Quick follow-up — any questions on Cenora?",
    body: "Hi {{first_name}},\n\nNoticed you spent a few minutes with the deck — glad it caught your eye. Happy to answer anything.\n\nIf the timing's right for {{firm}}, I'd love to grab 20 min: {{calendar_url}}\n\nIf not — no worries, would love to keep in touch.\n\n— Maya",
    lastSent: "May 25",
  },
  {
    id: "t-meeting",
    name: "Meeting confirmation",
    use: "Right after they accept a meeting invite.",
    subject: "Confirming our meeting — Cenora",
    body: "Hi {{first_name}},\n\nLooking forward to chatting on the day. I'll walk you through the product and a few customer stories.\n\nIf helpful, the full deck is here for reference: {{deck_url}}\n\n— Maya",
    lastSent: "May 20",
  },
  {
    id: "t-diligence",
    name: "Diligence — sending data room",
    use: "After they've signaled real interest. Shares data room access.",
    subject: "Cenora data room access",
    body: "Hi {{first_name}},\n\nAs promised — here's the Cenora data room. Financials, cap table, contracts, and product details all live here.\n\n  → {{deck_url}}\n\nReach out with any questions. Excited about where this could go with {{firm}}.\n\n— Maya",
    lastSent: "May 12",
  },
]

let _templates: EmailTemplate[] = [...L_TEMPLATES]
const _tmplListeners = new Set<() => void>()
const tmplStore = {
  list(): EmailTemplate[] { return [..._templates] },
  get(id: string) { return _templates.find(t => t.id === id) },
  save(t: EmailTemplate) {
    if (_templates.some(x => x.id === t.id)) {
      _templates = _templates.map(x => x.id === t.id ? t : x)
    } else {
      _templates = [t, ..._templates]
    }
    _tmplListeners.forEach(fn => fn())
  },
  remove(id: string) {
    _templates = _templates.filter(t => t.id !== id)
    _tmplListeners.forEach(fn => fn())
  },
  subscribe(fn: () => void) { _tmplListeners.add(fn); return () => _tmplListeners.delete(fn) },
}
function useTemplates(): EmailTemplate[] {
  const [, force] = React.useReducer(x => x + 1, 0)
  React.useEffect(() => tmplStore.subscribe(force), [])
  return tmplStore.list()
}

/* Merge `{{token}}` references with contact data. */
function mergeBody(text: string, c: LContact|null): string {
  if (!c) return text
  return text
    .replace(/\{\{first_name\}\}/g, c.first)
    .replace(/\{\{last_name\}\}/g,  c.last)
    .replace(/\{\{firm\}\}/g,       c.firm)
    .replace(/\{\{deck_url\}\}/g,   `cenora.app/i/${c.token}`)
    .replace(/\{\{calendar_url\}\}/g, "cenora.app/calendar/kim")
    .replace(/\{\{location\}\}/g,   "Manila")
}

function fmtMinutes(sec: number): string {
  if (!sec) return "0s"
  if (sec < 60) return sec + "s"
  const m = Math.floor(sec / 60), s = sec % 60
  return m + "m" + (s ? " " + s + "s" : "")
}

/* Random 6-char URL-safe token. */
function newToken(len = 6): string {
  const alpha = "abcdefghijkmnpqrstuvwxyz23456789"
  let out = ""
  for (let i = 0; i < len; i++) out += alpha[Math.floor(Math.random() * alpha.length)]
  return out
}

/* ============================================================================
 * <LaunchModule>  —  routing shell with sub-nav
 * ============================================================================ */
type LScreen = "pipeline"|"compose"|"engagement"|"templates"|"links"|"decks"|"settings"

function LaunchModule() {
  /* Shared state at the module root — survives tab switches. */
  const [screen, setScreen] = React.useState<LScreen>("pipeline")
  const [contacts, setContacts] = React.useState<LContact[]>(L_CONTACTS)
  const [composePreselect, setComposePreselect] = React.useState<string[]>([])

  const goCompose = (preselected: string[] = []) => {
    setComposePreselect(preselected)
    setScreen("compose")
  }

  const crumbs = [
    { label: "Capital" },
    { label: "Launch" },
    { label: L_SCREEN_LABELS[screen] },
  ]

  /* Per established pattern: TopBar carries NO action buttons. Each screen
   * renders its own <ActionBar> (title · status · actions · search · filters)
   * directly below the sub-nav and owns its own scroll region. */
  return (
    <>
      <TopBar breadcrumb={crumbs} />
      <LaunchSubNav active={screen} onChange={setScreen} />

      {screen === "pipeline"   && <LaunchPipeline   contacts={contacts} setContacts={setContacts} onCompose={goCompose} />}
      {screen === "compose"    && <LaunchCompose    contacts={contacts} preselect={composePreselect} onDone={() => setScreen("pipeline")} />}
      {screen === "engagement" && <LaunchEngagement contacts={contacts} />}
      {screen === "templates"  && <LaunchTemplates  onCompose={goCompose} />}
      {screen === "links"      && <LaunchLinks      contacts={contacts} />}
      {screen === "decks"      && <LaunchDecks      contacts={contacts} />}
      {screen === "settings"   && <LaunchSettings />}
    </>
  )
}

const L_SCREEN_LABELS: Record<LScreen, string> = {
  pipeline:   "Pipeline",
  compose:    "Compose",
  engagement: "Engagement",
  templates:  "Templates",
  links:      "Link generator",
  decks:      "Deck versions",
  settings:   "Settings",
}

/* ─── Horizontal sub-nav ─────────────────────────────────────────────── */
function LaunchSubNav({ active, onChange }: { active: LScreen; onChange: (s: LScreen) => void }) {
  const decks = useDecks()
  const tmpls = useTemplates()

  const items: { key: LScreen; label: string; icon: string; count?: number }[] = [
    { key: "pipeline",   label: "Pipeline",       icon: "kanban", count: L_CONTACTS.length },
    { key: "compose",    label: "Compose",        icon: "mail" },
    { key: "engagement", label: "Engagement",     icon: "chart" },
    { key: "templates",  label: "Templates",      icon: "file",  count: tmpls.length },
    { key: "links",      label: "Link generator", icon: "link" },
    { key: "decks",      label: "Deck versions",  icon: "layers", count: decks.length },
    { key: "settings",   label: "Settings",       icon: "settings" },
  ]

  return (
    <div className="shrink-0 bg-surface border-b border-divider px-5 flex items-center gap-0.5 overflow-x-auto">
      {items.map(it => {
        const isActive = active === it.key
        return (
          <button key={it.key}
            onClick={() => onChange(it.key)}
            className={cn(
              "h-10 px-3.5 inline-flex items-center gap-2 text-[12px] border-b-2 transition-colors whitespace-nowrap relative -mb-px",
              isActive
                ? "border-brand text-brand font-semibold"
                : "border-transparent text-ink-soft hover:text-ink font-medium"
            )}>
            <Icon name={it.icon as any} size={13} strokeWidth={isActive ? 2.25 : 1.75} />
            <span>{it.label}</span>
            {typeof it.count === "number" && (
              <span className={cn(
                "ml-0.5 inline-flex items-center justify-center min-w-[16px] h-[16px] px-1 rounded-full text-[9px] font-bold font-mono",
                isActive ? "bg-brand text-white" : "bg-cream-deep text-ink-mute"
              )}>{it.count}</span>
            )}
          </button>
        )
      })}
    </div>
  )
}

/* ─── SANDBOX EXPORTS ────────────────────────────────────────────────── */
;(globalThis as any).LaunchModule = LaunchModule
;(globalThis as any).L_CONTACTS = L_CONTACTS
;(globalThis as any).L_STAGES = L_STAGES
;(globalThis as any).L_KANBAN_STAGES = L_KANBAN_STAGES
;(globalThis as any).SLIDES = SLIDES
;(globalThis as any).fmtSize = fmtSize
;(globalThis as any).fmtMinutes = fmtMinutes
;(globalThis as any).mergeBody = mergeBody
;(globalThis as any).newToken = newToken
;(globalThis as any).useDecks = useDecks
;(globalThis as any).useTemplates = useTemplates
;(globalThis as any).tmplStore = tmplStore

})();/*__IIFE_WRAP_END__*/
