/* globals React, ReactDOM */
const { useState, useEffect, useCallback, useRef, useMemo } = React;

// ═══════════════════════════════════════════════════════════
//  CONSTANTS
// ═══════════════════════════════════════════════════════════
// Stages are rendered by color + label only — no icons. agreement_signed + closed_lost
// are protected (renamable but not deletable) since they are the terminal outcomes the
// rest of the pipeline reports against.
const DEFAULT_LEAD_STAGES = [
  { id:"new_lead",     label:"New Lead",     color:"#64748b" },
  { id:"contacted",    label:"Contacted",    color:"#38bdf8" },
  { id:"nurturing",    label:"Nurturing",    color:"#818cf8" },
  { id:"disqualified", label:"Disqualified", color:"#f87171" },
];
const DEFAULT_OPP_STAGES = [
  { id:"qualified",        label:"Qualified Lead",       color:"#60a5fa", staleDays:14 },
  { id:"intro_call",       label:"Intro Call",           color:"#a78bfa", staleDays:7  },
  { id:"fdd_sent",         label:"FDD Sent",             color:"#f59e0b", staleDays:7  },
  { id:"fdd_signed",       label:"FDD Signed",           color:"#fb923c", staleDays:10 },
  { id:"fdd_review_call",  label:"FDD Review Call",      color:"#f472b6", staleDays:7  },
  { id:"application",      label:"Application",          color:"#34d399", staleDays:5  },
  { id:"validation",       label:"Validation",           color:"#2dd4bf", staleDays:10 },
  { id:"ceo_qa",           label:"CEO Q&A",              color:"#818cf8", staleDays:7  },
  { id:"intake_form",      label:"Intake Form",          color:"#c084fc", staleDays:5  },
  { id:"discovery_day",    label:"Discovery Day",        color:"#facc15", staleDays:14 },
  { id:"agreement_sent",   label:"Agreement Sent",       color:"#fb7185", staleDays:7  },
  { id:"agreement_signed", label:"Agreement Signed",     color:"#4ade80", protected:true },
  { id:"closed_lost",      label:"Closed Lost",          color:"#64748b", protected:true },
];
const PROTECTED_STAGE_IDS = new Set(["agreement_signed", "closed_lost"]);

// U.S. states + DC, used when restricting territory by state (off-limits / registration status).
const US_STATES = [
  "Alabama","Alaska","Arizona","Arkansas","California","Colorado","Connecticut","Delaware",
  "Florida","Georgia","Hawaii","Idaho","Illinois","Indiana","Iowa","Kansas","Kentucky",
  "Louisiana","Maine","Maryland","Massachusetts","Michigan","Minnesota","Mississippi",
  "Missouri","Montana","Nebraska","Nevada","New Hampshire","New Jersey","New Mexico",
  "New York","North Carolina","North Dakota","Ohio","Oklahoma","Oregon","Pennsylvania",
  "Rhode Island","South Carolina","South Dakota","Tennessee","Texas","Utah","Vermont",
  "Virginia","Washington","West Virginia","Wisconsin","Wyoming","District of Columbia",
];

// Statuses available when restricting a state for franchise registration compliance.
// Restricted statuses (not_registered / pending_registration / sold_out) lock the territory
// from edits and trigger an AI territory-conflict flag on matching leads/opps.
const RESTRICTION_TYPES = {
  not_registered:       { label:"Not Registered",        color:"#dc2626", icon:"🚫" },
  pending_registration: { label:"Pending Registration",  color:"#f59e0b", icon:"⏳" },
  sold_out:             { label:"Sold Out",              color:"#facc15", icon:"💰" },
};

// Full territory status taxonomy. Three non-restricted ownership states (active / available /
// resale) plus the restricted compliance states above. Used by the territory list, conflict
// detection, and franchisee-assignment validation.
//   active:    Currently owned by one or more franchisees. Exclusive — a conflict if a
//              new lead/opp targets it. Default state for a newly-drawn territory.
//   available: Pre-created, unsold territory that's open for awarding. NOT a conflict —
//              a lead pointing here is good news. Franchisees cannot be assigned to it.
//   resale:    Owned by franchisee(s) who might consider a resale conversation. Still a
//              conflict (territory is occupied) but flagged as "resale possible" so the
//              rep can have a different conversation.
const TERRITORY_STATUSES = {
  active:               { label:"Active",                 color:"#4ade80", icon:"🟢", restricted:false, conflict:true,  assignable:true,  desc:"Currently owned by a franchisee. Exclusive — flags conflicts." },
  available:            { label:"Available",              color:"#60a5fa", icon:"📦", restricted:false, conflict:false, assignable:false, desc:"Pre-created and unsold. Open to award. No conflict on inbound leads." },
  resale:               { label:"Resale",                 color:"#a78bfa", icon:"🔄", restricted:false, conflict:true,  assignable:true,  desc:"Owned, but franchisee may entertain a resale offer. Flags as 'resale possible.'" },
  not_registered:       { label:"Not Registered",         color:"#dc2626", icon:"🚫", restricted:true,  conflict:true,  assignable:false, desc:"Brand isn't registered in this state. Compliance lockout." },
  pending_registration: { label:"Pending Registration",   color:"#f59e0b", icon:"⏳", restricted:true,  conflict:true,  assignable:false, desc:"Registration filed — waiting on the state. Cannot discuss yet." },
  sold_out:             { label:"Sold Out",               color:"#facc15", icon:"💰", restricted:true,  conflict:true,  assignable:false, desc:"All units in this market are sold. No more awards here." },
};
// Map a territory record to its status id. Legacy records used only `restricted: boolean`
// and `restrictionType` — fall back to those so existing data still resolves correctly.
const territoryStatus = (t) => {
  if (!t) return "active";
  if (t.status && TERRITORY_STATUSES[t.status]) return t.status;
  if (t.restricted && t.restrictionType && TERRITORY_STATUSES[t.restrictionType]) return t.restrictionType;
  if (t.restricted) return "not_registered";
  return "active";
};

// Fetch a state's actual polygon outline from Nominatim (OpenStreetMap).
// Returns a shape compatible with our territory data model (polygon or multipolygon).
async function fetchStatePolygon(stateName) {
  try {
    const res = await fetch(`https://nominatim.openstreetmap.org/search?q=${encodeURIComponent(stateName + ', USA')}&format=json&polygon_geojson=1&limit=1`);
    const data = await res.json();
    const geo = data[0]?.geojson;
    if (!geo) return null;
    if (geo.type === "Polygon") {
      return { kind:"polygon", latlngs: geo.coordinates[0].map(([lng,lat]) => [lat,lng]) };
    }
    if (geo.type === "MultiPolygon") {
      return {
        kind: "multipolygon",
        components: geo.coordinates.map((poly,i) => ({
          name: `${stateName} part ${i+1}`,
          latlngs: poly[0].map(([lng,lat]) => [lat,lng]),
        })),
      };
    }
    return null;
  } catch { return null; }
}

// Sub-reasons collected when an opp moves to the closed_lost stage.
const LOST_REASONS = [
  { id:"ghosted",        label:"Ghosted",        color:"#94a3b8", icon:"👻", desc:"No response after multiple follow-ups" },
  { id:"confirmed_lost", label:"Confirmed Lost", color:"#f87171", icon:"✕",  desc:"They explicitly said no — pass on this brand" },
  { id:"not_ready",      label:"Not Ready",      color:"#60a5fa", icon:"⏸",  desc:"Timing isn't right — may reopen later" },
];

// Timezone options for any timezone dropdown (settings + scheduling availability).
// Grouped by region for readability. The sentinel value "AUTO" means "match the user's system timezone",
// resolved at read time via Intl.DateTimeFormat().resolvedOptions().timeZone.
const SYSTEM_TIMEZONE = (() => { try { return Intl.DateTimeFormat().resolvedOptions().timeZone || "America/New_York"; } catch { return "America/New_York"; } })();
const resolveTz = (tz) => tz === "AUTO" ? SYSTEM_TIMEZONE : (tz || SYSTEM_TIMEZONE);
const TIMEZONE_GROUPS = [
  { label:"Auto", zones:[ ["AUTO", `Auto (match my computer — ${SYSTEM_TIMEZONE})`] ] },
  { label:"North America", zones:[
    ["America/Adak","Hawaii–Aleutian (Adak)"],
    ["Pacific/Honolulu","Hawaii (Honolulu)"],
    ["America/Anchorage","Alaska (Anchorage)"],
    ["America/Los_Angeles","Pacific (Los Angeles)"],
    ["America/Tijuana","Pacific (Tijuana)"],
    ["America/Phoenix","Mountain — no DST (Phoenix)"],
    ["America/Denver","Mountain (Denver)"],
    ["America/Edmonton","Mountain (Edmonton)"],
    ["America/Chicago","Central (Chicago)"],
    ["America/Mexico_City","Central (Mexico City)"],
    ["America/Winnipeg","Central (Winnipeg)"],
    ["America/New_York","Eastern (New York)"],
    ["America/Toronto","Eastern (Toronto)"],
    ["America/Indiana/Indianapolis","Eastern (Indianapolis)"],
    ["America/Halifax","Atlantic (Halifax)"],
    ["America/St_Johns","Newfoundland (St. John's)"],
  ]},
  { label:"South America", zones:[
    ["America/Bogota","Colombia (Bogotá)"],
    ["America/Lima","Peru (Lima)"],
    ["America/Caracas","Venezuela (Caracas)"],
    ["America/Santiago","Chile (Santiago)"],
    ["America/Sao_Paulo","Brazil (São Paulo)"],
    ["America/Argentina/Buenos_Aires","Argentina (Buenos Aires)"],
  ]},
  { label:"Europe", zones:[
    ["Europe/London","UK (London)"],
    ["Europe/Dublin","Ireland (Dublin)"],
    ["Europe/Lisbon","Portugal (Lisbon)"],
    ["Europe/Madrid","Spain (Madrid)"],
    ["Europe/Paris","France (Paris)"],
    ["Europe/Amsterdam","Netherlands (Amsterdam)"],
    ["Europe/Brussels","Belgium (Brussels)"],
    ["Europe/Berlin","Germany (Berlin)"],
    ["Europe/Zurich","Switzerland (Zurich)"],
    ["Europe/Rome","Italy (Rome)"],
    ["Europe/Vienna","Austria (Vienna)"],
    ["Europe/Stockholm","Sweden (Stockholm)"],
    ["Europe/Oslo","Norway (Oslo)"],
    ["Europe/Copenhagen","Denmark (Copenhagen)"],
    ["Europe/Helsinki","Finland (Helsinki)"],
    ["Europe/Warsaw","Poland (Warsaw)"],
    ["Europe/Prague","Czech Republic (Prague)"],
    ["Europe/Athens","Greece (Athens)"],
    ["Europe/Istanbul","Turkey (Istanbul)"],
    ["Europe/Moscow","Russia (Moscow)"],
    ["Europe/Kyiv","Ukraine (Kyiv)"],
  ]},
  { label:"Africa", zones:[
    ["Africa/Casablanca","Morocco (Casablanca)"],
    ["Africa/Lagos","Nigeria (Lagos)"],
    ["Africa/Johannesburg","South Africa (Johannesburg)"],
    ["Africa/Cairo","Egypt (Cairo)"],
    ["Africa/Nairobi","Kenya (Nairobi)"],
  ]},
  { label:"Middle East", zones:[
    ["Asia/Jerusalem","Israel (Jerusalem)"],
    ["Asia/Beirut","Lebanon (Beirut)"],
    ["Asia/Riyadh","Saudi Arabia (Riyadh)"],
    ["Asia/Dubai","UAE (Dubai)"],
    ["Asia/Tehran","Iran (Tehran)"],
  ]},
  { label:"Asia", zones:[
    ["Asia/Karachi","Pakistan (Karachi)"],
    ["Asia/Kolkata","India (Kolkata)"],
    ["Asia/Colombo","Sri Lanka (Colombo)"],
    ["Asia/Kathmandu","Nepal (Kathmandu)"],
    ["Asia/Dhaka","Bangladesh (Dhaka)"],
    ["Asia/Bangkok","Thailand (Bangkok)"],
    ["Asia/Jakarta","Indonesia (Jakarta)"],
    ["Asia/Singapore","Singapore"],
    ["Asia/Kuala_Lumpur","Malaysia (Kuala Lumpur)"],
    ["Asia/Manila","Philippines (Manila)"],
    ["Asia/Hong_Kong","Hong Kong"],
    ["Asia/Shanghai","China (Shanghai)"],
    ["Asia/Taipei","Taiwan (Taipei)"],
    ["Asia/Seoul","South Korea (Seoul)"],
    ["Asia/Tokyo","Japan (Tokyo)"],
  ]},
  { label:"Oceania", zones:[
    ["Australia/Perth","Australia (Perth)"],
    ["Australia/Adelaide","Australia (Adelaide)"],
    ["Australia/Brisbane","Australia (Brisbane)"],
    ["Australia/Sydney","Australia (Sydney)"],
    ["Pacific/Auckland","New Zealand (Auckland)"],
    ["Pacific/Fiji","Fiji"],
  ]},
  { label:"UTC", zones:[ ["UTC","UTC"] ] },
];

// Default scheduling event types — seeded for each brand on first run / schema migration.
// `isDefault: true` makes them non-deletable (but deactivatable) to match the stage-editor pattern.
const DEFAULT_EVENT_TYPES = [
  { slug:"intro-call",          name:"Intro Call",             durationMin:15, description:"Quick qualifying call to learn about the candidate's goals and franchise interest.",                                                location:"phone", color:"#60a5fa", icon:"📞", linkedStageHint:"qualified",       confirmationSubject:"Confirmed: Intro Call with {BRAND}",         confirmationBody:"Hi {FIRST_NAME},\n\nThanks for booking the intro call. We'll talk on {DATE} at {TIME}.\n\nLooking forward to it,\n{REP_NAME}" },
  { slug:"franchise-overview",  name:"Franchise Overview Call",durationMin:60, description:"First video call in the discovery process — a full general overview of the franchise opportunity.",                                location:"video", color:"#34d399", icon:"🎥", linkedStageHint:"qualified",       confirmationSubject:"Confirmed: Franchise Overview with {BRAND}", confirmationBody:"Hi {FIRST_NAME},\n\nLooking forward to walking you through the {BRAND} opportunity on {DATE} at {TIME}. I'll send a video link beforehand.\n\n{REP_NAME}" },
  { slug:"fdd-review",          name:"FDD Review",             durationMin:60, description:"Walk through the Franchise Disclosure Document section by section.",                                                                 location:"video", color:"#f59e0b", icon:"📄", linkedStageHint:"fdd_review_call", confirmationSubject:"Confirmed: FDD Review for {BRAND}",          confirmationBody:"Hi {FIRST_NAME},\n\nWe're set for the FDD Review on {DATE} at {TIME}. I'll send a video link before the meeting.\n\n{REP_NAME}" },
  { slug:"territory-review",    name:"Territory Review",       durationMin:30, description:"Map review of available territories and demographic fit.",                                                                          location:"video", color:"#10b981", icon:"🗺️", linkedStageHint:"qualified",       confirmationSubject:"Confirmed: Territory Review for {BRAND}",    confirmationBody:"Hi {FIRST_NAME},\n\nWe'll walk through territory options on {DATE} at {TIME}.\n\n{REP_NAME}" },
];

const DEFAULT_AVAILABILITY = {
  weeklyHours: {
    mon: [{start:"09:00", end:"17:00"}],
    tue: [{start:"09:00", end:"17:00"}],
    wed: [{start:"09:00", end:"17:00"}],
    thu: [{start:"09:00", end:"17:00"}],
    fri: [{start:"09:00", end:"17:00"}],
    sat: [],
    sun: [],
  },
  timeZone: "AUTO",
  minNoticeHours: 24,
  maxAdvanceDays: 60,
  defaultBufferMin: 15,
  blackoutDates: [],
};

// ─── Discovery Day & Franchisee constants ─────────────────────────────────
// Default checklists seeded when a new Discovery Day is created. Items are grouped
// into stages (pre/day-of/post) so the UI can render them as progress bands. Reps
// can add/remove/rename anything per event after creation.
const DEFAULT_DDAY_CHECKLIST = [
  // Pre-event prep
  { category:"pre",    label:"Send pre-Discovery-Day welcome packet + agenda" },
  { category:"pre",    label:"Confirm candidate attendance (1 week out)" },
  { category:"pre",    label:"Confirm corporate attendee calendar holds" },
  { category:"pre",    label:"Reserve conference room / book video bridge" },
  { category:"pre",    label:"Confirm catering or meal logistics (if in-person)" },
  { category:"pre",    label:"Print FDD copies + territory maps (if in-person)" },
  { category:"pre",    label:"Send video link + dial-in info (if virtual)" },
  { category:"pre",    label:"Hotel + transport booked for candidate (if in-person)" },
  { category:"pre",    label:"Final-prep call with rep 24h before" },
  // Day-of agenda
  { category:"day_of", label:"Welcome + brand history overview" },
  { category:"day_of", label:"Tour of headquarters / kitchen / training space" },
  { category:"day_of", label:"Operations deep-dive presentation" },
  { category:"day_of", label:"Marketing + lead-gen walkthrough" },
  { category:"day_of", label:"Financial performance + Item 19 walk-through" },
  { category:"day_of", label:"Q&A with CEO / leadership" },
  { category:"day_of", label:"Meet existing franchisees (validation)" },
  { category:"day_of", label:"Territory + buildout discussion" },
  { category:"day_of", label:"Closing pitch + next-steps conversation" },
  // Post-event follow-up
  { category:"post",   label:"Thank-you email + recap within 24h" },
  { category:"post",   label:"Send post-Discovery-Day decision deadline" },
  { category:"post",   label:"Debrief with corporate attendees" },
  { category:"post",   label:"Move candidate to Agreement Sent (if go)" },
  { category:"post",   label:"Document feedback / objections in CRM" },
];
const DDAY_CHECKLIST_CATEGORIES = [
  { id:"pre",    label:"Pre-Event Prep",    icon:"📋", color:"#60a5fa" },
  { id:"day_of", label:"Day-of Agenda",     icon:"🎯", color:"#f59e0b" },
  { id:"post",   label:"Post-Event Follow-Up", icon:"✉️", color:"#4ade80" },
];
const DDAY_STATUSES = {
  draft:     { label:"Draft",     color:"#94a3b8", icon:"📝" },
  confirmed: { label:"Confirmed", color:"#60a5fa", icon:"📅" },
  completed: { label:"Completed", color:"#4ade80", icon:"✓" },
  cancelled: { label:"Cancelled", color:"#f87171", icon:"✕" },
};
const FRANCHISEE_STATUSES = {
  awarded:        { label:"Awarded",        color:"#60a5fa", icon:"🎉", desc:"Agreement signed; pre-opening." },
  in_buildout:    { label:"In Buildout",    color:"#f59e0b", icon:"🏗️", desc:"Permits, construction, or buildout underway." },
  in_training:    { label:"In Training",    color:"#a78bfa", icon:"🎓", desc:"Owner-operator or staff training in progress." },
  open:           { label:"Open",           color:"#4ade80", icon:"🟢", desc:"Open and operating." },
  multi_unit:     { label:"Multi-Unit",     color:"#fb923c", icon:"⭐", desc:"Operates two or more units." },
  transferring:   { label:"Transferring",   color:"#facc15", icon:"🔄", desc:"In the process of selling / transferring ownership." },
  closed:         { label:"Closed",         color:"#94a3b8", icon:"🔒", desc:"Permanently closed." },
};

const BOOKING_STATUSES = {
  scheduled: { label:"Scheduled", color:"#60a5fa" },
  completed: { label:"Completed", color:"#4ade80" },
  cancelled: { label:"Cancelled", color:"#94a3b8" },
  no_show:   { label:"No-Show",   color:"#f87171" },
};

// Notification system — types, severity, default UI surface.
// Banner notifications appear at the top of screen and auto-dismiss after 15s.
// All notifications land in the Notifications tab regardless.
const NOTIFICATION_TYPES = {
  envelope_viewed:   { category:"docusign", severity:"normal",   isBanner:true,  label:"Envelope viewed",       format:(d)=>`${d.candidateName} opened the ${d.docLabel}` },
  envelope_signed:   { category:"docusign", severity:"normal",   isBanner:true,  label:"Envelope signed",       format:(d)=>`${d.candidateName} signed the ${d.docLabel}!` },
  envelope_declined: { category:"docusign", severity:"critical", isBanner:true,  label:"Envelope declined",     format:(d)=>`${d.candidateName} declined the ${d.docLabel}` },
  envelope_idle:     { category:"docusign", severity:"normal",   isBanner:false, label:"Envelope idle",         format:(d)=>`${d.candidateName}'s ${d.docLabel} has been waiting ${d.days}d for signature` },
  new_lead:          { category:"lead",     severity:"normal",   isBanner:true,  label:"New lead created",      format:(d)=>`New lead added: ${d.candidateName}` },
  hot_lead_dark:     { category:"lead",     severity:"critical", isBanner:true,  label:"Hot lead going dark",   format:(d)=>`Hot lead going dark: ${d.candidateName} (score ${d.score}, ${d.days}d silent)` },
  ai_quota_75:       { category:"system",   severity:"normal",   isBanner:false, label:"AI quota 75%",          format:(d)=>`You've used 75% of your monthly AI calls (${d.used}/${d.cap})` },
  ai_quota_90:       { category:"system",   severity:"normal",   isBanner:true,  label:"AI quota 90%",          format:(d)=>`90% of monthly AI calls used (${d.used}/${d.cap}) — upgrade to keep things flowing` },
  ai_quota_100:      { category:"system",   severity:"critical", isBanner:true,  label:"AI quota exhausted",    format:(d)=>`AI quota exhausted (${d.used}/${d.cap}). New calls disabled until billing renewal` },
  payment_failed:    { category:"system",   severity:"critical", isBanner:true,  label:"Payment failed",        format:(d)=>`Payment failed for your ${d.tier} subscription. Update billing to avoid interruption` },
  integration_off:   { category:"system",   severity:"critical", isBanner:true,  label:"Integration disconnected",format:(d)=>`${d.integration} disconnected` },
  export_ready:      { category:"system",   severity:"normal",   isBanner:false, label:"Data export ready",     format:(d)=>`Your data export is ready` },
  app_updated:       { category:"system",   severity:"normal",   isBanner:false, label:"App updated",           format:(d)=>`${BRAND.name} updated to ${d.version}` },
  booking_created:      { category:"scheduling", severity:"normal",   isBanner:true,  label:"Booking created",       format:(d)=>`${d.eventTypeName} booked with ${d.candidateName} for ${d.when}` },
  booking_upcoming_24h: { category:"scheduling", severity:"normal",   isBanner:true,  label:"Upcoming meeting",      format:(d)=>`Tomorrow: ${d.eventTypeName} with ${d.candidateName} at ${d.when}` },
  booking_starting_1h:  { category:"scheduling", severity:"normal",   isBanner:true,  label:"Meeting starting soon", format:(d)=>`Starting in ~1h: ${d.eventTypeName} with ${d.candidateName}` },
  booking_completed:    { category:"scheduling", severity:"normal",   isBanner:false, label:"Meeting completed",     format:(d)=>`${d.eventTypeName} with ${d.candidateName} marked completed` },
  tasks_due_today:   { category:"system",   severity:"normal",   isBanner:true,  label:"Tasks due today",       format:(d)=>`📋 ${d.count} task${d.count===1?"":"s"} due today — see What's Next for the morning recap` },
  digest_morning:    { category:"digest",   severity:"normal",   isBanner:true,  label:"Morning digest",        format:(d)=>`Good morning — ${d.topPriority}` },
  digest_weekly:     { category:"digest",   severity:"normal",   isBanner:true,  label:"Weekly digest",         format:(d)=>`This week: ${d.signed} signed · ${d.added} new leads · ${d.lost} lost` },
  digest_monthly:    { category:"digest",   severity:"normal",   isBanner:true,  label:"Monthly digest",        format:(d)=>`This month: ${d.signed} signed · ${d.lost} lost · ${d.convRate}% conv rate` },
};
// Merge tags exposed in template editors. Grouped for picker UI.
// Resolved at fill-time against the active record + brand + rep settings + system clock.
// Supports {TAG|fallback} syntax for default values when the field is empty.
const MERGE_TAG_GROUPS = [
  { label:"Contact", tags:[
    ["FIRST_NAME","First name","Sarah"],
    ["LAST_NAME","Last name","Lee"],
    ["FULL_NAME","Full name","Sarah Lee"],
    ["FIRST_NAMES_ALL","All first names (contact + business partners, e.g. \"Joe, Sam, and Levy\")","Sarah, Joe, and Levy"],
    ["EMAIL","Email address","sarah@example.com"],
    ["PHONE","Phone","555-0142"],
    ["COMPANY","Company","Acme Holdings"],
  ]},
  { label:"Pipeline", tags:[
    ["STAGE","Current stage","Qualified"],
    ["TERRITORY","Territory","Austin TX"],
    ["INVESTMENT_LEVEL","Investment range","$500k–$1M"],
    ["SOURCE","Lead source","Referral"],
    ["DAYS_IN_PIPELINE","Days since first contact","14"],
    ["SCORE","AI score","78"],
    ["ASSIGNED_TO","Assigned rep","You"],
  ]},
  { label:"Brand & Rep", tags:[
    ["BRAND","Brand name","Your Brand"],
    ["BRAND_WEBSITE","Brand website","https://yourbrand.com"],
    ["REP_NAME","Rep name","You"],
    ["REP_EMAIL","Rep email","rep@example.com"],
    ["REP_PHONE","Rep phone","555-0100"],
    ["REP_TITLE","Rep title","Franchise Development"],
  ]},
  { label:"Date & Time", tags:[
    ["TODAY","Today's date (short)",""],
    ["TODAY_LONG","Today's date (long)",""],
    ["MONTH","Current month",""],
    ["YEAR","Current year",""],
  ]},
  { label:"Links", tags:[
    ["SCHEDULING_LINK","Booking link","https://yourbrand.com/book"],
    ["FDD_LINK","FDD link","https://yourbrand.com/fdd"],
    ["UNSUBSCRIBE_LINK","Unsubscribe link","https://yourbrand.com/unsubscribe"],
  ]},
];
const ALL_MERGE_TAGS = MERGE_TAG_GROUPS.flatMap(g => g.tags.map(([k,d,sample]) => ({key:k, desc:d, sample})));

// Build merge-tag context from the active record + brand + settings. Empty strings let
// the {KEY|fallback} syntax kick in to provide a default.
function templateContext(rec, brand, settings) {
  const r = rec || {};
  const s = settings || {};
  const b = brand || {};
  const days = r.createdAt ? Math.max(0, Math.floor((Date.now() - new Date(r.createdAt).getTime()) / 86400000)) : "";
  // Combine the contact's first name with each partner's first name into a natural-language list.
  // e.g. ["Joe"] → "Joe"; ["Joe","Sam"] → "Joe and Sam"; ["Joe","Sam","Levy"] → "Joe, Sam, and Levy".
  const partnerFirsts = ((r.partners||[]).map(p => (p.firstName||"").trim()).filter(Boolean));
  const allFirsts = [r.firstName, ...partnerFirsts].filter(Boolean);
  const firstNamesAll = allFirsts.length === 0 ? "" :
    allFirsts.length === 1 ? allFirsts[0] :
    allFirsts.length === 2 ? `${allFirsts[0]} and ${allFirsts[1]}` :
    `${allFirsts.slice(0,-1).join(", ")}, and ${allFirsts[allFirsts.length-1]}`;
  return {
    FIRST_NAME: r.firstName || "",
    LAST_NAME: r.lastName || "",
    FULL_NAME: `${r.firstName||""} ${r.lastName||""}`.trim(),
    FIRST_NAMES_ALL: firstNamesAll,
    EMAIL: r.email || "",
    PHONE: r.phone || "",
    COMPANY: r.company || "",
    STAGE: r.stage || "",
    TERRITORY: r.territory || "",
    INVESTMENT_LEVEL: r.investmentLevel || "", // legacy alias for old templates
    NET_WORTH: r.netWorth || r.investmentLevel || "",
    LIQUIDITY: r.liquidity || "",
    SOURCE: r.source || "",
    DAYS_IN_PIPELINE: days ? String(days) : "",
    SCORE: "",
    ASSIGNED_TO: r.assignedTo || s.repName || "",
    BRAND: b.name || s.companyName || "",
    BRAND_WEBSITE: b.website || "",
    REP_NAME: s.repName || "",
    REP_EMAIL: s.companyEmail || "",
    REP_PHONE: s.companyPhone || "",
    REP_TITLE: s.repTitle || "Franchise Development",
    SCHEDULING_LINK: "",
    FDD_LINK: "",
    UNSUBSCRIBE_LINK: "",
  };
}

// Convert a structured block array to an HTML string. Used both for the live preview and
// for persistence — every save serializes blocks → HTML stored on the template `body`.
// Map of font-family CSS value -> Google Fonts identifier (family + standard weights).
// Used to inject the right <link rel="stylesheet"> so modern fonts actually render in
// email clients that honor remote stylesheets (Apple Mail, web Gmail, etc.).
const GOOGLE_FONT_MAP = {
  "Inter": "Inter:wght@300;400;500;600;700;800",
  "Roboto": "Roboto:wght@300;400;500;700;900",
  "'Open Sans'": "Open+Sans:wght@300;400;600;700;800",
  "Lato": "Lato:wght@300;400;700;900",
  "Poppins": "Poppins:wght@300;400;500;600;700;800;900",
  "Montserrat": "Montserrat:wght@300;400;500;600;700;800;900",
  "Raleway": "Raleway:wght@300;400;500;600;700;800",
  "Nunito": "Nunito:wght@300;400;600;700;800;900",
  "'Source Sans 3'": "Source+Sans+3:wght@300;400;600;700;800",
  "'Work Sans'": "Work+Sans:wght@300;400;500;600;700;800",
  "Manrope": "Manrope:wght@300;400;500;600;700;800",
  "'DM Sans'": "DM+Sans:wght@400;500;700",
  "'Plus Jakarta Sans'": "Plus+Jakarta+Sans:wght@300;400;500;600;700;800",
  "Quicksand": "Quicksand:wght@300;400;500;600;700",
  "Mulish": "Mulish:wght@300;400;500;600;700;800",
  "Oswald": "Oswald:wght@300;400;500;600;700",
  "'Bebas Neue'": "Bebas+Neue",
  "'Playfair Display'": "Playfair+Display:wght@400;500;600;700;800;900",
  "Merriweather": "Merriweather:wght@300;400;700;900",
  "Lora": "Lora:wght@400;500;600;700",
  "'Crimson Pro'": "Crimson+Pro:wght@300;400;500;600;700;800",
  "'EB Garamond'": "EB+Garamond:wght@400;500;600;700;800",
  "'Cormorant Garamond'": "Cormorant+Garamond:wght@300;400;500;600;700",
  "'DM Serif Display'": "DM+Serif+Display",
  "'Libre Baskerville'": "Libre+Baskerville:wght@400;700",
  "Cabin": "Cabin:wght@400;500;600;700",
  "Karla": "Karla:wght@300;400;500;600;700;800",
  "Rubik": "Rubik:wght@300;400;500;600;700;800;900",
  "'JetBrains Mono'": "JetBrains+Mono:wght@300;400;500;600;700;800",
  "'Fira Code'": "Fira+Code:wght@300;400;500;600;700",
};
// Walk a block tree and return any `<link rel="stylesheet">` tags for Google Fonts used.
function collectFontLinks(blocks) {
  const used = new Set();
  const walk = (list) => {
    (list || []).forEach(b => {
      if (b && b.fontFamily) {
        // Match the first quoted or bareword token at the start of fontFamily.
        const m = b.fontFamily.match(/^([^,]+)/);
        if (m) {
          const first = m[1].trim();
          if (GOOGLE_FONT_MAP[first]) used.add(GOOGLE_FONT_MAP[first]);
        }
      }
      if (b && b.type === "row" && Array.isArray(b.children)) b.children.forEach(col => walk(col));
    });
  };
  walk(blocks);
  if (!used.size) return "";
  const families = [...used].map(s => `family=${s}`).join("&");
  return `<link href="https://fonts.googleapis.com/css2?${families}&display=swap" rel="stylesheet">`;
}
// Walk a block tree (top-level + row children) and emit a single `<style>` tag with the
// @media (max-width:480px) rule for any block whose author specified a mobile font-size
// override. Returns "" when nothing needs overriding.
function collectMobileFontStyles(blocks) {
  const rules = [];
  let hasAnyRow = false;
  const walk = (list) => {
    (list || []).forEach(b => {
      if (b && b.id && b.fontSizeMobile) rules.push(`.bk-${b.id}{font-size:${b.fontSizeMobile}px !important;}`);
      if (b && b.type === "row") {
        hasAnyRow = true;
        if (Array.isArray(b.children)) b.children.forEach(col => walk(col));
      }
    });
  };
  walk(blocks);
  // Row responsive behavior: rows without `noStack` collapse columns to full-width below
  // 480px (the standard mobile-email breakpoint). `no-stack` rows keep horizontal layout.
  if (hasAnyRow) {
    rules.push(`.stack-cols .row-col{display:block !important;width:100% !important;padding:0 0 8px 0 !important;}`);
    rules.push(`.no-stack .row-col{display:table-cell !important;}`);
  }
  return rules.length ? `<style>@media (max-width:480px){${rules.join("")}}</style>` : "";
}
function blocksToHtml(blocks, themeColor, opts = {}) {
  if (!Array.isArray(blocks)) return "";
  const c = themeColor || "#3b82f6";
  // opts.prevType / opts.isFirst / opts.isLast let a single-block render know its position
  // (used by the editor canvas, which renders blocks one at a time per React block).
  // opts.wrapperPadH/V are the surrounding container's padding values — needed so square
  // banners + per-block square backgrounds can pull out via negative margin to fill edges.
  const optsHas = (k) => Object.prototype.hasOwnProperty.call(opts, k);
  const {
    prevType: optsPrevType = null,
    isFirst: optsIsFirst = false,
    isLast:  optsIsLast  = false,
    nextHasBg: optsNextHasBg = false,
    wrapperPadH = 22,
    wrapperPadV = 20,
  } = opts;
  const fontDecl = (b) => `${b.fontFamily?`font-family:${b.fontFamily};`:""}${b.fontWeight?`font-weight:${b.fontWeight};`:""}`;
  // Per-block row background — any leaf block can sit on a colored "band". Rounded = inner
  // card with rounded corners; square = full-bleed band that extends to wrapper edges.
  // Uses a single-cell <table> so `valign="middle"` gives true vertical centering of the
  // wrapped content (h1, p, etc.) — flex/line-height tricks were unreliable across clients.
  // When the next sibling also has a visible bg (row-bg or banner), bottom margin drops to 0
  // so adjacent colored bands stack flush — no white gap.
  const wrapBlockBg = (b, inner, nextHasVisibleBg) => {
    // Background can be a solid color, a gradient, OR an image. When an image is set we
    // shorthand background to include image + position + size, layered over the color (which
    // acts as a fallback for clients that strip image backgrounds, e.g. Outlook desktop).
    const bgImage   = b.blockBgImage ? `url('${b.blockBgImage}')` : "";
    const bgColor   = b.blockBg || "";
    const bgGrad    = b.blockBgGradient || "";
    if (!bgImage && !bgColor && !bgGrad) return inner;
    const bgPos     = b.blockBgPosition || "center center";
    const bgSize    = b.blockBgSize || "cover";
    const bgRepeat  = b.blockBgRepeat || "no-repeat";
    const sharp = (b.blockBgCorner || "square") !== "rounded";
    const radius = sharp ? 0 : 8;
    const inset = b.blockBgInset != null ? b.blockBgInset : 15;
    const iTop  = b.blockBgInsetTop    != null ? b.blockBgInsetTop    : inset;
    const iBot  = b.blockBgInsetBottom != null ? b.blockBgInsetBottom : inset;
    const iSide = b.blockBgInsetSides  != null ? b.blockBgInsetSides  : inset;
    // Cell padding — inside the colored band, around the content. padV/padH override the
    // defaults so the rep can tighten or expand breathing room around their text.
    const pV = b.padV != null ? b.padV : 18;
    const pH = sharp
      ? (b.padH != null ? Math.max(b.padH, wrapperPadH) : wrapperPadH)
      : (b.padH != null ? b.padH : 18);
    const bottomMargin = sharp ? (nextHasVisibleBg ? 0 : 16) : iBot;
    const topMargin = sharp ? 0 : iTop;
    const widthCss = sharp ? `width:calc(100% + ${2*wrapperPadH}px);` : "";
    const mLeft  = sharp ? -wrapperPadH : iSide;
    const mRight = sharp ? 0 : iSide;
    // Compose the background CSS — when an image is present, image rides on top of the
    // gradient or color so the image is the visible layer and the color/gradient fills any
    // transparent areas.
    let bgCss = "";
    if (bgImage) {
      const layerBelow = bgGrad || bgColor || "transparent";
      bgCss = `background:${bgImage} ${bgPos}/${bgSize} ${bgRepeat}, ${layerBelow};`;
      if (bgColor && !bgGrad) bgCss += `background-color:${bgColor};`;
    } else {
      bgCss = `background:${bgGrad || bgColor};`;
    }
    // Square mode: table expands past the wrapper edges (positive width override + negative
    // left margin). Rounded mode: table shrinks by 2*inset and uses positive margins so the
    // band reads as a floating card inside the content background.
    const finalWidthCss = sharp ? widthCss : `width:calc(100% - ${2*inset}px);`;
    const sideCss = sharp ? `margin:0 0 ${bottomMargin}px ${mLeft}px` : `margin:${topMargin}px ${mRight}px ${bottomMargin}px ${mLeft}px`;
    return `<table role="presentation" cellpadding="0" cellspacing="0" border="0" style="${finalWidthCss}${bgCss}border-radius:${radius}px;border-collapse:collapse;${sideCss}"><tr><td valign="middle" style="padding:${pV}px ${pH}px;vertical-align:middle;text-align:center;">${inner}</td></tr></table>`;
  };
  return blocks.map((b, idx) => {
    const align = b.align || "left";
    const pad = b.padding != null ? b.padding : 16;
    // Use opts ONLY when the caller explicitly supplied them (editor canvas renders one block
    // at a time and passes opts). Otherwise compute from idx so saveSnapshot — which iterates
    // the full blocks array — gets correct first/last/prev info even when array length is 1.
    const _isFirst = optsHas('isFirst') ? !!optsIsFirst : idx === 0;
    const _isLast  = optsHas('isLast')  ? !!optsIsLast  : idx === blocks.length - 1;
    const _prevType = optsHas('prevType') ? optsPrevType : (idx > 0 ? blocks[idx - 1].type : null);
    // Does the next sibling also have a visible bg (banner / row-bg / bleed image)? If yes,
    // flush bottom margin so the two visual bands stack with no white gap.
    const _nextBlock = idx < blocks.length - 1 ? blocks[idx + 1] : null;
    const blockHasVisBg = (x) => !!x && (x.type === "banner" || x.type === "imageBanner" || x.blockBg || x.blockBgGradient || x.blockBgImage || (x.type === "image" && x.width === "bleed") || (x.type === "spacer" && x.bgColor));
    const _nextHasBg = optsHas('nextHasBg') ? !!optsNextHasBg : blockHasVisBg(_nextBlock);
    // Subtle indent for paragraphs / headers below a header or banner — visually nests the
    // child text under the heading above. 14px on the side that text flows from.
    const indentPx = ((b.type === "para" || b.type === "header") && (_prevType === "header" || _prevType === "banner")) ? 14 : 0;
    const indentCss = indentPx ? (align === "right" ? `padding-right:${indentPx}px;` : align === "center" ? "" : `padding-left:${indentPx}px;`) : "";
    switch (b.type) {
      case "banner": {
        const bg = b.bgGradient || b.bgColor || c;
        // cornerStyle: "square" (default, full-bleed) | "rounded" (inset card with adjustable
        // gap from the content background edges). Rounded mode uses `b.inset` (default 16px)
        // for all four sides; square mode pulls back to the wrapper edges via negative margins.
        const sharp = b.cornerStyle !== "rounded";
        const radius = sharp ? 0 : 12;
        // Universal inset is the fallback. Square mode honors vertical insets too (but the
        // sides always bleed to email edges via negative margins). Rounded honors all four.
        const inset = b.inset != null ? b.inset : (sharp ? 0 : 15);
        const iTop = b.insetTop != null ? b.insetTop : inset;
        const iBot = b.insetBottom != null ? b.insetBottom : inset;
        const iSide = b.insetSides != null ? b.insetSides : inset;
        const mH = sharp ? -wrapperPadH : iSide;
        // For square mode: if user has set a vertical inset > 0, it overrides the auto
        // first/last full-bleed extension. Otherwise default to extending edge-to-edge.
        const mTop = sharp ? (iTop > 0 ? iTop : (_isFirst ? -wrapperPadV : 0)) : iTop;
        const mBot = sharp ? (iBot > 0 ? iBot : (_isLast ? -wrapperPadV : (_nextHasBg ? 0 : pad))) : iBot;
        const banPadV = b.padV != null ? b.padV : 32;
        // Square/bleed mode: horizontal padding is fixed at wrapper padding (matches the
        // card's content gutter so text doesn't run under the email edge). Only rounded
        // honors a custom padH so the rep can dial in the card's inner side padding.
        const banPadH = sharp ? wrapperPadH : (b.padH != null ? b.padH : 20);
        const titleHtml = `<h2 style="margin:0;font-size:${b.fontSize||24}px;line-height:1.15;${fontDecl(b)||"font-weight:800;"}">${b.title||"Headline"}</h2>`;
        const subHtml = b.subtitle ? `<p style="margin:8px 0 0;opacity:.9;font-size:${b.subtitleFontSize||14}px;line-height:1.3">${b.subtitle}</p>` : "";
        // Square mode extends BOTH sides past the email-card edges. width:calc(100% + 2x)
        // + a single negative left margin pushes the band out to the left and lets the
        // calc'd extra width reach past the right edge symmetrically. Same trick wrapBlockBg
        // uses — without it the right side stops at the card edge and leaves a white gap.
        const tWidth = sharp ? `width:calc(100% + ${2*wrapperPadH}px);` : `width:100%;`;
        const mLeft = sharp ? -wrapperPadH : mH;
        return `<table role="presentation" cellpadding="0" cellspacing="0" border="0" style="${tWidth}border-collapse:collapse;background:${bg};color:${b.color||"#fff"};border-radius:${radius}px;margin:${mTop}px ${mH}px ${mBot}px ${mLeft}px"><tr><td valign="middle" align="center" style="padding:${banPadV}px ${banPadH}px;vertical-align:middle;text-align:center;">${titleHtml}${subHtml}</td></tr></table>`;
      }
      case "header": {
        const hasRowBg = b.blockBg || b.blockBgGradient || b.blockBgImage;
        const lh = b.lineHeight != null ? `line-height:${b.lineHeight};` : (hasRowBg ? "line-height:1.15;" : "");
        const innerMargin = hasRowBg ? "margin:0;" : `margin:0 0 ${pad}px;`;
        const cls = b.fontSizeMobile ? `bk-${b.id||"x"}` : "";
        const h1 = `<h1${cls?` class="${cls}"`:""} style="color:${b.color||"#1e293b"};font-size:${b.fontSize||28}px;${innerMargin}${lh}${fontDecl(b)||"font-weight:800;"}text-align:${align};${indentCss}">${b.text||"Your Headline"}</h1>`;
        if (hasRowBg) return wrapBlockBg(b, h1, _nextHasBg);
        // No row bg — wrap in a padded div so padV/padH have a visible effect. Defaults
        // match the property-form's PadControls defaults (18px each) so the slider readout
        // matches the actual rendered padding.
        const pV = b.padV != null ? b.padV : 18;
        const pH = (b.align === "center") ? 0 : (b.padH != null ? b.padH : 18);
        return `<div style="padding:${pV}px ${pH}px">${h1}</div>`;
      }
      case "para": {
        const hasRowBg = b.blockBg || b.blockBgGradient || b.blockBgImage;
        const lh = b.lineHeight != null ? `line-height:${b.lineHeight};` : (hasRowBg ? "line-height:1.45;" : "line-height:1.7;");
        const innerMargin = hasRowBg ? "margin:0;" : `margin:0 0 ${pad}px;`;
        const cls = b.fontSizeMobile ? `bk-${b.id||"x"}` : "";
        const p = `<p${cls?` class="${cls}"`:""} style="color:${b.color||"#475569"};${lh}font-size:${b.fontSize||15}px;${innerMargin}${fontDecl(b)}text-align:${align};${indentCss}">${(b.text||"Your paragraph copy here.").replace(/\n/g,"<br/>")}</p>`;
        if (hasRowBg) return wrapBlockBg(b, p, _nextHasBg);
        const pV = b.padV != null ? b.padV : 18;
        const pH = (b.align === "center") ? 0 : (b.padH != null ? b.padH : 18);
        return `<div style="padding:${pV}px ${pH}px">${p}</div>`;
      }
      case "btn": {
        const bg = b.bgGradient || b.bgColor || c;
        const hasRowBg = b.blockBg || b.blockBgGradient;
        const innerMargin = hasRowBg ? "margin:0;" : `margin:${pad}px 0;`;
        // Corner radius — "sharp"=2, "soft"=8 (default), "pill"=99. Numeric override wins.
        const cornerMap = { sharp: 2, soft: 8, pill: 99 };
        const btnRadius = b.btnRadius != null ? b.btnRadius : (cornerMap[b.btnCornerStyle] != null ? cornerMap[b.btnCornerStyle] : 8);
        const btnPadV = b.btnPadV != null ? b.btnPadV : 13;
        const btnPadH = b.btnPadH != null ? b.btnPadH : 30;
        return wrapBlockBg(b, `<p style="text-align:${align};${innerMargin}"><a href="${b.url||"#"}" style="display:inline-block;background:${bg};color:${b.color||"#fff"};padding:${btnPadV}px ${btnPadH}px;border-radius:${btnRadius}px;text-decoration:none;${fontDecl(b)||"font-weight:700;"}font-size:${b.fontSize||15}px">${b.text||"Call to Action"}</a></p>`, _nextHasBg);
      }
      case "divider": {
        const style = b.style || "line";
        const color = b.color || "#e2e8f0";
        // Decorative dividers: borderStyle + thickness for variety. Image style swaps the
        // line for an <img> centered between the surrounding blocks (great for ornamental
        // section breaks like the Unlayer "lines border" or "top border" graphics).
        if (style === "image" && b.imageSrc) {
          return `<div style="text-align:center;margin:${pad}px 0"><img src="${b.imageSrc}" alt="" style="max-width:100%;height:auto;display:inline-block;"/></div>`;
        }
        const borderStyle = style === "dashed" ? "dashed" : style === "dotted" ? "dotted" : style === "double" ? "double" : "solid";
        const thickness = style === "double" ? 3 : (b.thickness || 1);
        const widthCss = b.dividerWidth ? `width:${b.dividerWidth};margin-left:auto;margin-right:auto;` : "";
        return `<hr style="border:none;border-top:${thickness}px ${borderStyle} ${color};margin:${pad}px 0;${widthCss}"/>`;
      }
      case "logo": {
        const w = Math.max(16, Math.min(400, b.width || 96));
        const align = b.align || "center";
        const placeholder = `https://placehold.co/${w*2}x${w*2}/${c.slice(1)}/ffffff?text=LOGO`;
        return `<div style="text-align:${align};margin:0 0 ${pad}px;"><img src="${b.src||placeholder}" alt="${b.alt||"Logo"}" style="display:inline-block;width:${w}px;max-width:100%;height:auto;"/></div>`;
      }
      case "footer": {
        const align = b.align || "center";
        const color = b.color || "#767676";
        const fs = b.fontSize || 12;
        const lines = [];
        if (b.companyName) lines.push(`<div>${b.companyName}</div>`);
        if (b.companyAddress) lines.push(`<div>${b.companyAddress}</div>`);
        if (b.showUnsubscribe) lines.push(`<div style="margin-top:6px"><a href="{UNSUBSCRIBE_URL|#}" style="color:${color};text-decoration:underline">Unsubscribe</a> &nbsp;·&nbsp; <a href="{REPORT_ABUSE_URL|#}" style="color:${color};text-decoration:underline">Report abuse</a></div>`);
        return wrapBlockBg(b, `<div style="text-align:${align};color:${color};font-size:${fs}px;line-height:150%;font-family:arial,helvetica,sans-serif;margin:0 0 ${pad}px;">${lines.join("")}</div>`, _nextHasBg);
      }
      case "spacer": {
        // Spacer extends to the wrapper edges by default (negative horizontal margins) so a
        // colored spacer reads as a solid band, matching how the banner's "square" mode works.
        // Top/bottom margins are also pulled in when it sits flush against the wrapper edges
        // (first/last block) so there's no white gap above/below a colored spacer.
        const mH = -wrapperPadH;
        const mTop = _isFirst ? -wrapperPadV : 0;
        const mBot = _isLast  ? -wrapperPadV : 0;
        return `<div aria-hidden="true" style="height:${b.height||24}px;line-height:${b.height||24}px;font-size:1px;background:${b.bgColor||"transparent"};margin:${mTop}px ${mH}px ${mBot}px ${mH}px;">&nbsp;</div>`;
      }
      case "imageBanner":
      case "image": {
        // Image sizing: S/M/L = 25/50/75% of container, Fit = 100%, Custom = pixel value, and
        // Bleed = extend past the content backdrop padding to the email edges (mirrors the
        // banner block's Square mode — also extends top/bottom when this is the first/last
        // block in the layout). The user's own banner image can use Bleed as an alternative
        // to the styled Hero Banner block.
        // imageBanner = a separate library entry that hides the size picker and always renders
        // in bleed mode — easy access for users uploading pre-designed banners.
        const sizePct = { small:25, medium:50, large:75 };
        const w = b.type === "imageBanner" ? "bleed" : (b.width || "fit");
        const align = b.align || "center";
        const isBleed = w === "bleed";
        const widthCss = w === "fit" ? "width:100%;height:auto;"
                       : w === "custom" ? `width:${b.widthCustom||300}px;max-width:100%;height:auto;`
                       : isBleed ? `width:calc(100% + ${2*wrapperPadH}px);height:auto;display:block;margin-left:-${wrapperPadH}px;`
                       : `width:${sizePct[w]||50}%;height:auto;`;
        const wrapAlign = align === "center" ? "text-align:center;" : align === "right" ? "text-align:right;" : "text-align:left;";
        // Bleed mode bakes the negative margins onto the <img> directly + drops the wrapper's
        // text-align (irrelevant since the image fills the row). Bottom margin still respects
        // _nextHasBg / _isLast for flush stacking with adjacent colored content.
        const mTop = isBleed && _isFirst ? -wrapperPadV : 0;
        const mBot = isBleed && _isLast ? -wrapperPadV : (_nextHasBg ? 0 : pad);
        const imgRadius = isBleed ? 0 : 8;
        // Placeholder: wide-banner aspect for bleed / imageBanner (since those modes ARE banners
        // and a 400×400 square gets enormous when stretched to full canvas width). Square for
        // ordinary image sizes where the user is likely uploading a more flexible asset.
        const placeholderUrl = isBleed
          ? `https://placehold.co/1200x300/${c.slice(1)}/ffffff?text=Banner+Image`
          : `https://placehold.co/400x400/${c.slice(1)}/ffffff?text=Image`;
        const imgTag = `<img src="${b.src||placeholderUrl}" alt="${b.alt||""}" style="${widthCss}border-radius:${imgRadius}px;${isBleed?"":"display:inline-block;"}${b.url?"cursor:pointer;":""}"/>`;
        const linked = b.url ? `<a href="${b.url}" style="text-decoration:none">${imgTag}</a>` : imgTag;
        if (isBleed) {
          // Bleed images can't be wrapped in a row-bg table (they need to extend past the wrapper);
          // skip wrapBlockBg and let the bleed visual carry the band itself.
          return `<div style="margin:${mTop}px 0 ${mBot}px 0">${linked}</div>`;
        }
        // Non-bleed image: drop bottom margin when the next block has a visible bg so a colored
        // band (or another image/banner) sits flush. Wrap in row-bg if configured.
        const imgBottomMargin = _nextHasBg ? 0 : pad;
        return wrapBlockBg(b, `<div style="${wrapAlign}margin:0 0 ${imgBottomMargin}px">${linked}</div>`, _nextHasBg);
      }
      case "row": {
        // Email-client-safe row: a <table> with one <tr> and N <td> columns. Width per cell is
        // proportional to its colSpan (so a merged cell with span=2 in a 3-slot row takes 2/3
        // of the width). vertical-align:middle keeps content visually centered between cells.
        const cols = Math.max(1, Math.min(3, b.cols || 2));
        const children = (b.children || []).slice(0, cols);
        const spans = (b.colSpans && b.colSpans.length === children.length) ? b.colSpans : children.map(() => 1);
        const totalSpan = spans.reduce((a,s)=>a+s,0) || cols;
        const gap = b.gap != null ? b.gap : 12;
        // noStack: when true, the row keeps columns horizontal even on narrow viewports — useful
        // for icon strips and feature grids. Default behavior matches typical email-builder
        // responsive collapse: columns stack vertically below ~520px.
        const tableCls = b.noStack ? "no-stack" : "stack-cols";
        const cells = children.map((col, i) => {
          const span = spans[i] || 1;
          const widthPct = Math.floor((span / totalSpan) * 100);
          const inner = blocksToHtml(col || [], themeColor);
          const pl = i === 0 ? 0 : Math.round(gap/2);
          const pr = i === children.length - 1 ? 0 : Math.round(gap/2);
          return `<td class="row-col" width="${widthPct}%" valign="middle" style="width:${widthPct}%;padding:0 ${pr}px 0 ${pl}px;vertical-align:middle">${inner || ""}</td>`;
        }).join("");
        return `<table role="presentation" class="${tableCls}" cellpadding="0" cellspacing="0" border="0" width="100%" style="width:100%;border-collapse:collapse;margin:0 0 ${pad}px"><tr>${cells}</tr></table>`;
      }
      case "html":
        return b.html || "";
      default: return "";
    }
  }).join("\n");
}

// Preset gradients exposed in the gradient picker button. Each entry is a label + the raw
// CSS `linear-gradient(...)` value. Used for banner / button backgrounds.
const GRADIENT_PRESETS = [
  { label:"None",          value:"" },
  { label:"Ocean",         value:"linear-gradient(135deg,#3b82f6 0%,#7c3aed 100%)" },
  { label:"Sunset",        value:"linear-gradient(135deg,#f97316 0%,#ec4899 100%)" },
  { label:"Mint",          value:"linear-gradient(135deg,#4ade80 0%,#14b8a6 100%)" },
  { label:"Royal",         value:"linear-gradient(135deg,#7c3aed 0%,#1e3a8a 100%)" },
  { label:"Peach",         value:"linear-gradient(135deg,#fbbf24 0%,#f43f5e 100%)" },
  { label:"Forest",        value:"linear-gradient(135deg,#065f46 0%,#84cc16 100%)" },
  { label:"Slate",         value:"linear-gradient(135deg,#334155 0%,#0f172a 100%)" },
  { label:"Cherry",        value:"linear-gradient(135deg,#dc2626 0%,#831843 100%)" },
  { label:"Tropical",      value:"linear-gradient(135deg,#06b6d4 0%,#84cc16 100%)" },
  { label:"Lavender",      value:"linear-gradient(135deg,#a78bfa 0%,#f9a8d4 100%)" },
  { label:"Gold",          value:"linear-gradient(135deg,#fde047 0%,#a16207 100%)" },
  { label:"Aurora",        value:"linear-gradient(135deg,#22d3ee 0%,#a855f7 50%,#ec4899 100%)" },
  { label:"Midnight",      value:"linear-gradient(135deg,#1e1b4b 0%,#3b0764 100%)" },
];

// Email-safe and modern font families. "Web-safe" set ships with most mail clients; modern
// stacks below the divider may fall back gracefully in clients that lack the primary font.
const FONT_FAMILIES = [
  // Email-safe — render natively on all major clients without webfont loading.
  { label:"System default",     value:"Helvetica,Arial,sans-serif", category:"safe" },
  { label:"Arial",              value:"Arial,Helvetica,sans-serif", category:"safe" },
  { label:"Arial Black",        value:"'Arial Black',Arial,sans-serif", category:"safe" },
  { label:"Arial Narrow",       value:"'Arial Narrow',Arial,sans-serif", category:"safe" },
  { label:"Helvetica",          value:"Helvetica,Arial,sans-serif", category:"safe" },
  { label:"Verdana",            value:"Verdana,Geneva,sans-serif", category:"safe" },
  { label:"Tahoma",             value:"Tahoma,Geneva,sans-serif", category:"safe" },
  { label:"Trebuchet MS",       value:"'Trebuchet MS',Helvetica,sans-serif", category:"safe" },
  { label:"Geneva",             value:"Geneva,Verdana,sans-serif", category:"safe" },
  { label:"Lucida Sans",        value:"'Lucida Sans Unicode','Lucida Grande',Helvetica,sans-serif", category:"safe" },
  { label:"Impact",             value:"Impact,Charcoal,'Arial Black',sans-serif", category:"safe" },
  { label:"Calibri",            value:"Calibri,Candara,Segoe,Arial,sans-serif", category:"safe" },
  { label:"Segoe UI",           value:"'Segoe UI',Tahoma,Geneva,Verdana,sans-serif", category:"safe" },
  { label:"Optima",             value:"Optima,Candara,'Trebuchet MS',sans-serif", category:"safe" },
  { label:"Avenir",             value:"Avenir,'Avenir Next',Helvetica,sans-serif", category:"safe" },
  // Serif
  { label:"Georgia",            value:"Georgia,serif", category:"safe" },
  { label:"Times New Roman",    value:"'Times New Roman',Times,serif", category:"safe" },
  { label:"Times",              value:"Times,'Times New Roman',serif", category:"safe" },
  { label:"Garamond",           value:"Garamond,'Times New Roman',serif", category:"safe" },
  { label:"Palatino",           value:"'Palatino Linotype',Palatino,serif", category:"safe" },
  { label:"Book Antiqua",       value:"'Book Antiqua',Palatino,serif", category:"safe" },
  { label:"Baskerville",        value:"Baskerville,'Baskerville Old Face',Georgia,serif", category:"safe" },
  { label:"Cambria",            value:"Cambria,Georgia,serif", category:"safe" },
  { label:"Didot",              value:"Didot,'Bodoni MT',Georgia,serif", category:"safe" },
  // Monospace
  { label:"Courier New",        value:"'Courier New',Courier,monospace", category:"safe" },
  { label:"Courier",            value:"Courier,'Courier New',monospace", category:"safe" },
  { label:"Lucida Console",     value:"'Lucida Console',Monaco,monospace", category:"safe" },
  { label:"Monaco",             value:"Monaco,'Lucida Console',monospace", category:"safe" },
  // Modern webfonts — load Google-style families that most modern email clients support; older clients fall back gracefully.
  { label:"Inter",              value:"Inter,system-ui,Helvetica,Arial,sans-serif", category:"modern" },
  { label:"Roboto",             value:"Roboto,Helvetica,Arial,sans-serif", category:"modern" },
  { label:"Open Sans",          value:"'Open Sans',Helvetica,Arial,sans-serif", category:"modern" },
  { label:"Lato",               value:"Lato,Helvetica,Arial,sans-serif", category:"modern" },
  { label:"Poppins",            value:"Poppins,Helvetica,Arial,sans-serif", category:"modern" },
  { label:"Montserrat",         value:"Montserrat,Helvetica,Arial,sans-serif", category:"modern" },
  { label:"Raleway",            value:"Raleway,Helvetica,Arial,sans-serif", category:"modern" },
  { label:"Nunito",             value:"Nunito,Helvetica,Arial,sans-serif", category:"modern" },
  { label:"Source Sans 3",      value:"'Source Sans 3','Source Sans Pro',Helvetica,sans-serif", category:"modern" },
  { label:"Work Sans",          value:"'Work Sans',Helvetica,Arial,sans-serif", category:"modern" },
  { label:"Manrope",            value:"Manrope,Helvetica,Arial,sans-serif", category:"modern" },
  { label:"DM Sans",            value:"'DM Sans',Helvetica,Arial,sans-serif", category:"modern" },
  { label:"Plus Jakarta Sans",  value:"'Plus Jakarta Sans',Helvetica,Arial,sans-serif", category:"modern" },
  { label:"Quicksand",          value:"Quicksand,Helvetica,Arial,sans-serif", category:"modern" },
  { label:"Mulish",             value:"Mulish,Helvetica,Arial,sans-serif", category:"modern" },
  { label:"Oswald",             value:"Oswald,Impact,sans-serif", category:"modern" },
  { label:"Bebas Neue",         value:"'Bebas Neue',Impact,sans-serif", category:"modern" },
  { label:"Playfair Display",   value:"'Playfair Display',Georgia,serif", category:"modern" },
  { label:"Merriweather",       value:"Merriweather,Georgia,serif", category:"modern" },
  { label:"Lora",               value:"Lora,Georgia,serif", category:"modern" },
  { label:"Crimson Pro",        value:"'Crimson Pro','Crimson Text',Georgia,serif", category:"modern" },
  { label:"EB Garamond",        value:"'EB Garamond',Garamond,Georgia,serif", category:"modern" },
  { label:"Cormorant Garamond", value:"'Cormorant Garamond',Garamond,Georgia,serif", category:"modern" },
  { label:"DM Serif Display",   value:"'DM Serif Display','Playfair Display',Georgia,serif", category:"modern" },
  { label:"Libre Baskerville",  value:"'Libre Baskerville',Baskerville,Georgia,serif", category:"modern" },
  { label:"Cabin",              value:"Cabin,Helvetica,Arial,sans-serif", category:"modern" },
  { label:"Karla",              value:"Karla,Helvetica,Arial,sans-serif", category:"modern" },
  { label:"Rubik",              value:"Rubik,Helvetica,Arial,sans-serif", category:"modern" },
  { label:"JetBrains Mono",     value:"'JetBrains Mono','Source Code Pro',Courier,monospace", category:"modern" },
  { label:"Fira Code",          value:"'Fira Code','Source Code Pro',Courier,monospace", category:"modern" },
];
const FONT_WEIGHTS = [
  { label:"Light (300)",    value:300 },
  { label:"Regular (400)",  value:400 },
  { label:"Medium (500)",   value:500 },
  { label:"Semi-bold (600)",value:600 },
  { label:"Bold (700)",     value:700 },
  { label:"Extra-bold (800)",value:800 },
  { label:"Black (900)",    value:900 },
];

// Resolve merge tags. Supports {KEY|fallback} for default when field is empty.
function fillMergeTags(str, ctx) {
  if (!str) return "";
  const d = new Date();
  const now = {
    TODAY: d.toLocaleDateString("en-US",{month:"short",day:"numeric",year:"numeric"}),
    TODAY_LONG: d.toLocaleDateString("en-US",{weekday:"long",month:"long",day:"numeric",year:"numeric"}),
    MONTH: d.toLocaleDateString("en-US",{month:"long"}),
    YEAR: String(d.getFullYear()),
  };
  return str.replace(/\{([A-Z_]+)(?:\|([^}]*))?\}/g, (_, key, fallback) => {
    const v = (ctx && ctx[key]) || now[key] || "";
    if (v) return v;
    return fallback != null ? fallback : `{${key}}`;
  });
}

// Pre-designed marketing templates shown in the Gallery sub-tab. Clicking one opens the
// editor with content prefilled so reps don't start from a blank canvas.
const STARTER_GALLERY = [
  { id:"g_newsletter", name:"Monthly Newsletter", category:"marketing", icon:"📰", color:"#3b82f6", subject:"{BRAND} — This Month at a Glance",
    body:`<div style="background:#3b82f6;color:#fff;padding:32px 20px;border-radius:12px;text-align:center;margin:0 0 20px"><h2 style="margin:0 0 8px;font-size:24px;font-weight:800">This Month at {BRAND}</h2><p style="margin:0;opacity:.9;font-size:14px">Wins, openings, and what's ahead</p></div><h1 style="color:#1e293b;font-size:24px;margin:0 0 12px;font-weight:800">Hi {FIRST_NAME},</h1><p style="color:#475569;line-height:1.7;font-size:15px;margin:0 0 16px">Here's a recap of what happened across the {BRAND} network this month.</p><hr style="border:none;border-top:1px solid #e2e8f0;margin:24px 0"/><p style="color:#475569;line-height:1.7;font-size:15px;margin:0 0 16px"><strong>🎉 New Openings:</strong> We welcomed 4 new franchisees this month.</p><p style="color:#475569;line-height:1.7;font-size:15px;margin:0 0 16px"><strong>📈 Top Markets:</strong> Texas, Florida, and Arizona led in candidate interest.</p><p style="text-align:center;margin:28px 0"><a href="#" style="display:inline-block;background:#3b82f6;color:#fff;padding:13px 30px;border-radius:8px;text-decoration:none;font-weight:700;font-size:15px">Read the Full Update</a></p>` },
  { id:"g_promo", name:"Limited-Time Promo", category:"marketing", icon:"🎯", color:"#f59e0b", subject:"Limited Time: Reduced Franchise Fee This Quarter",
    body:`<div style="background:#f59e0b;color:#fff;padding:36px 20px;border-radius:12px;text-align:center;margin:0 0 20px"><h2 style="margin:0 0 8px;font-size:28px;font-weight:800">$10K Off Franchise Fee</h2><p style="margin:0;opacity:.95;font-size:15px">Sign by quarter-end. No conditions.</p></div><p style="color:#475569;line-height:1.7;font-size:15px;margin:0 0 16px">Hi {FIRST_NAME}, we're running a quarter-end incentive — sign your agreement before the end of the quarter and the franchise fee is reduced by $10,000.</p><p style="text-align:center;margin:28px 0"><a href="#" style="display:inline-block;background:#f59e0b;color:#fff;padding:13px 30px;border-radius:8px;text-decoration:none;font-weight:700;font-size:15px">Claim Your Discount</a></p>` },
  { id:"g_welcome", name:"Welcome Series #1", category:"marketing", icon:"👋", color:"#4ade80", subject:"Welcome to {BRAND}, {FIRST_NAME}!",
    body:`<h1 style="color:#1e293b;font-size:28px;margin:0 0 16px;font-weight:800">Welcome aboard, {FIRST_NAME}! 🎉</h1><p style="color:#475569;line-height:1.7;font-size:15px;margin:0 0 16px">We are thrilled to have you join the {BRAND} family. Over the next few days you'll receive a series of onboarding emails covering everything you need to know.</p><p style="color:#475569;line-height:1.7;font-size:15px;margin:0 0 16px"><strong>What to expect this week:</strong></p><p style="color:#475569;line-height:1.7;font-size:15px;margin:0 0 16px">1. Portal access (today)<br/>2. Training schedule (Day 2)<br/>3. Your FBC introduction call (Day 3)</p><p style="text-align:center;margin:24px 0"><a href="#" style="display:inline-block;background:#4ade80;color:#fff;padding:13px 30px;border-radius:8px;text-decoration:none;font-weight:700;font-size:15px">Access Your Portal</a></p>` },
  { id:"g_event", name:"Discovery Day Invite", category:"follow_up", icon:"📅", color:"#a78bfa", subject:"You're Invited: {BRAND} Discovery Day",
    body:`<div style="background:linear-gradient(135deg,#a78bfa,#7c3aed);color:#fff;padding:36px 20px;border-radius:12px;text-align:center;margin:0 0 20px"><p style="margin:0 0 4px;font-size:11px;letter-spacing:.1em;opacity:.9;text-transform:uppercase">You're Invited</p><h2 style="margin:0 0 8px;font-size:28px;font-weight:800">{BRAND} Discovery Day</h2><p style="margin:0;opacity:.95;font-size:14px">Meet the team. Tour the operation. Sign a partnership.</p></div><p style="color:#475569;line-height:1.7;font-size:15px;margin:0 0 16px">Hi {FIRST_NAME}, congratulations — you've reached the Discovery Day stage. This is the final step before we partner.</p><p style="color:#475569;line-height:1.7;font-size:15px;margin:0 0 16px"><strong>Available dates:</strong> [DATES]<br/><strong>Location:</strong> [ADDRESS]</p><p style="text-align:center;margin:28px 0"><a href="#" style="display:inline-block;background:#7c3aed;color:#fff;padding:13px 30px;border-radius:8px;text-decoration:none;font-weight:700;font-size:15px">RSVP Now</a></p>` },
  { id:"g_reengage", name:"Win-Back: Dormant Lead", category:"follow_up", icon:"⚡", color:"#fb923c", subject:"Still curious about {BRAND}?",
    body:`<h1 style="color:#1e293b;font-size:26px;margin:0 0 16px;font-weight:800">Hey {FIRST_NAME}, are we still on?</h1><p style="color:#475569;line-height:1.7;font-size:15px;margin:0 0 16px">It's been a few weeks since we last connected about {BRAND}. Life gets busy — totally understand.</p><p style="color:#475569;line-height:1.7;font-size:15px;margin:0 0 16px">If now isn't the right time, just hit reply with "pause" and I'll check back in 6 months. If you're ready to pick up where we left off, click below and we'll get a 15-min call on the books.</p><p style="text-align:center;margin:24px 0"><a href="#" style="display:inline-block;background:#fb923c;color:#fff;padding:13px 30px;border-radius:8px;text-decoration:none;font-weight:700;font-size:15px">Pick a Time</a></p>` },
  { id:"g_followup", name:"FDD Follow-Up Nudge", category:"follow_up", icon:"📄", color:"#60a5fa", subject:"Did the FDD answer your questions?",
    body:`<p style="color:#475569;line-height:1.7;font-size:15px;margin:0 0 16px">Hi {FIRST_NAME},</p><p style="color:#475569;line-height:1.7;font-size:15px;margin:0 0 16px">Quick nudge — wanted to make sure the {BRAND} FDD landed and answered the questions you had. A few sections candidates usually have follow-up questions on:</p><p style="color:#475569;line-height:1.7;font-size:15px;margin:0 0 16px">• <strong>Item 7</strong> — Total investment range<br/>• <strong>Item 19</strong> — Historical financial performance<br/>• <strong>Item 20</strong> — Number of operating units</p><p style="color:#475569;line-height:1.7;font-size:15px;margin:0 0 16px">Want to jump on a 30-min FDD Review Call this week? I'll walk you through the highlights.</p><p style="text-align:center;margin:24px 0"><a href="#" style="display:inline-block;background:#60a5fa;color:#fff;padding:12px 28px;border-radius:8px;text-decoration:none;font-weight:700;font-size:15px">Book FDD Review</a></p>` },
  // ── Phase-5 additions: showcase blocks for the new primitives ─────────────────────────
  { id:"g_event_invite", name:"Event Invitation (Bold)", category:"marketing", icon:"🎉", color:"#7c3aed", subject:"You're Invited — {BRAND} Open House",
    color:"#7c3aed",
    blocks:[
      { id:"e_logo", type:"logo", src:"", alt:"Logo", width:96, align:"center", padding:8 },
      { id:"e_h1", type:"header", text:"YOU'RE INVITED", fontSize:48, fontSizeMobile:32, lineHeight:1.05, align:"center", color:"#ffffff", fontFamily:"'Bebas Neue',Impact,sans-serif", blockBgGradient:"linear-gradient(135deg,#7c3aed,#3b82f6)", blockBgCorner:"square", padding:32 },
      { id:"e_h2", type:"header", text:"to the {BRAND} Open House", fontSize:22, lineHeight:1.3, align:"center", color:"#1e293b", padding:16 },
      { id:"e_p", type:"para", text:"Hi {FIRST_NAME} — come tour the operation, meet the team, and find out what makes {BRAND} a category leader.", fontSize:16, lineHeight:1.6, align:"center", padding:8 },
      { id:"e_div", type:"divider", style:"dashed", color:"#a78bfa", thickness:2, padding:20 },
      { id:"e_btn", type:"btn", text:"RSVP NOW", url:"#", bgGradient:"linear-gradient(90deg,#7c3aed,#3b82f6)", color:"#ffffff", btnCornerStyle:"pill", btnRadius:99, btnPadV:14, btnPadH:36, fontSize:14, fontFamily:"Inter,system-ui,sans-serif", fontWeight:800, align:"center", padding:8 },
      { id:"e_foot", type:"footer", companyName:"{COMPANY_NAME|{BRAND}}", companyAddress:"{COMPANY_ADDRESS}", showUnsubscribe:true, color:"#94a3b8", fontSize:11, align:"center", padding:8 },
    ]
  },
  { id:"g_feature_grid", name:"Three Features (Icon Grid)", category:"marketing", icon:"⚡", color:"#0ea5e9", subject:"3 reasons franchisees pick {BRAND}",
    color:"#0ea5e9",
    blocks:[
      { id:"f_logo", type:"logo", width:120, align:"center", padding:12 },
      { id:"f_h1", type:"header", text:"Why {BRAND}?", fontSize:36, fontSizeMobile:26, align:"center", color:"#0f172a", fontFamily:"'Playfair Display',Georgia,serif", padding:8 },
      { id:"f_p", type:"para", text:"Three reasons franchisees pick us — and stay.", fontSize:16, align:"center", color:"#475569", padding:16 },
      { id:"f_row", type:"row", cols:3, gap:16, noStack:false, children:[
        [{ id:"f_c1h", type:"header", text:"📈 Growth", fontSize:18, align:"center", color:"#0ea5e9", padding:4 }, { id:"f_c1p", type:"para", text:"Average unit revenue up 22% YoY across the system.", fontSize:13, align:"center", color:"#475569", padding:8 }],
        [{ id:"f_c2h", type:"header", text:"🛟 Support", fontSize:18, align:"center", color:"#0ea5e9", padding:4 }, { id:"f_c2p", type:"para", text:"Field-business consultant per region, 24/7 escalation line.", fontSize:13, align:"center", color:"#475569", padding:8 }],
        [{ id:"f_c3h", type:"header", text:"🏆 Brand", fontSize:18, align:"center", color:"#0ea5e9", padding:4 }, { id:"f_c3p", type:"para", text:"Top-5 in our category for three years running.", fontSize:13, align:"center", color:"#475569", padding:8 }],
      ]},
      { id:"f_div", type:"divider", style:"line", color:"#e2e8f0", padding:24 },
      { id:"f_btn", type:"btn", text:"See the FDD", url:"#", bgColor:"#0ea5e9", color:"#ffffff", btnCornerStyle:"pill", btnRadius:99, fontSize:14, align:"center", padding:8 },
      { id:"f_foot", type:"footer", companyName:"{COMPANY_NAME|{BRAND}}", companyAddress:"{COMPANY_ADDRESS}", showUnsubscribe:true, fontSize:11, padding:8 },
    ]
  },
  { id:"g_anniversary", name:"Anniversary Offer (Bright)", category:"marketing", icon:"🥂", color:"#fdd95c", subject:"It's been one year — here's a thank-you",
    color:"#fdd95c",
    blocks:[
      { id:"a_logo", type:"logo", width:80, align:"center", padding:10 },
      { id:"a_h1", type:"header", text:"ONE YEAR TOGETHER", fontSize:54, fontSizeMobile:36, lineHeight:1.05, align:"center", color:"#1a1a1a", fontFamily:"'DM Serif Display','Playfair Display',Georgia,serif", blockBg:"#fdd95c", blockBgCorner:"square", padding:32 },
      { id:"a_h2", type:"header", text:"Thank you, {FIRST_NAME}.", fontSize:22, align:"center", color:"#1a1a1a", padding:16 },
      { id:"a_p", type:"para", text:"To celebrate a full year of partnership, we're sending you a special gift on us — no strings.", fontSize:16, lineHeight:1.7, align:"center", padding:12 },
      { id:"a_div", type:"divider", style:"double", color:"#1a1a1a", padding:18 },
      { id:"a_btn", type:"btn", text:"CLAIM YOUR GIFT", url:"#", bgColor:"#1a1a1a", color:"#fdd95c", btnCornerStyle:"sharp", btnRadius:2, btnPadV:16, btnPadH:38, fontSize:13, fontFamily:"Inter,system-ui,sans-serif", fontWeight:900, align:"center", padding:10 },
      { id:"a_foot", type:"footer", companyName:"{COMPANY_NAME|{BRAND}}", companyAddress:"{COMPANY_ADDRESS}", showUnsubscribe:true, color:"#5a5a5a", fontSize:11, padding:8 },
    ]
  },
  { id:"g_webinar", name:"Webinar Sign-Up", category:"marketing", icon:"🎥", color:"#10b981", subject:"Live this Thursday — {BRAND} Q&A",
    color:"#10b981",
    blocks:[
      { id:"w_logo", type:"logo", width:96, align:"center", padding:8 },
      { id:"w_h1", type:"header", text:"Live Q&A this Thursday", fontSize:34, fontSizeMobile:24, align:"center", color:"#ffffff", blockBg:"#064e3b", blockBgCorner:"square", padding:36 },
      { id:"w_p", type:"para", text:"Bring your toughest questions about {BRAND} franchising. Our CEO + Director of FBC will answer them live for 60 minutes.", fontSize:16, lineHeight:1.6, align:"center", padding:14 },
      { id:"w_spacer", type:"spacer", height:16 },
      { id:"w_row", type:"row", cols:3, gap:0, noStack:true, children:[
        [{ id:"w_d1", type:"header", text:"🗓", fontSize:32, align:"center", padding:4 }, { id:"w_d1p", type:"para", text:"Thursday 2pm ET", fontSize:13, align:"center", color:"#475569", padding:4 }],
        [{ id:"w_d2", type:"header", text:"⏱", fontSize:32, align:"center", padding:4 }, { id:"w_d2p", type:"para", text:"60 minutes", fontSize:13, align:"center", color:"#475569", padding:4 }],
        [{ id:"w_d3", type:"header", text:"💻", fontSize:32, align:"center", padding:4 }, { id:"w_d3p", type:"para", text:"Zoom — link sent on RSVP", fontSize:13, align:"center", color:"#475569", padding:4 }],
      ]},
      { id:"w_div", type:"divider", style:"line", color:"#10b981", thickness:2, padding:22 },
      { id:"w_btn", type:"btn", text:"Save my seat", url:"#", bgColor:"#10b981", color:"#ffffff", btnCornerStyle:"pill", btnRadius:99, fontSize:15, align:"center", padding:8 },
      { id:"w_foot", type:"footer", companyName:"{COMPANY_NAME|{BRAND}}", companyAddress:"{COMPANY_ADDRESS}", showUnsubscribe:true, fontSize:11, padding:8 },
    ]
  },
  { id:"g_welcome_rich", name:"Onboarding Welcome (Branded)", category:"marketing", icon:"🌟", color:"#3b82f6", subject:"Welcome to {BRAND}, {FIRST_NAME}!",
    color:"#3b82f6",
    blocks:[
      { id:"o_logo", type:"logo", width:120, align:"center", padding:14 },
      { id:"o_h1", type:"header", text:"Welcome, {FIRST_NAME}!", fontSize:40, fontSizeMobile:28, lineHeight:1.1, align:"center", color:"#ffffff", blockBgGradient:"linear-gradient(135deg,#3b82f6,#6366f1)", blockBgCorner:"square", fontFamily:"Inter,system-ui,sans-serif", padding:36 },
      { id:"o_p1", type:"para", text:"You're officially in the family. Here's everything you need to start your journey with {BRAND}.", fontSize:16, lineHeight:1.7, align:"center", padding:12 },
      { id:"o_div", type:"divider", style:"dashed", color:"#3b82f6", thickness:2, padding:20 },
      { id:"o_h2", type:"header", text:"Your first 7 days", fontSize:22, align:"left", color:"#1e293b", padding:6 },
      { id:"o_p2", type:"para", text:"<b>Day 1</b> — Portal access<br/><b>Day 2</b> — Training schedule<br/><b>Day 3</b> — FBC intro call<br/><b>Day 5</b> — Vendor accounts<br/><b>Day 7</b> — First check-in", fontSize:15, lineHeight:1.9, align:"left", padding:10 },
      { id:"o_btn", type:"btn", text:"Open your portal", url:"#", bgColor:"#3b82f6", color:"#ffffff", btnCornerStyle:"soft", btnRadius:8, fontSize:15, align:"center", padding:16 },
      { id:"o_foot", type:"footer", companyName:"{COMPANY_NAME|{BRAND}}", companyAddress:"{COMPANY_ADDRESS}", showUnsubscribe:true, fontSize:11, padding:8 },
    ]
  },
  { id:"g_thank_you", name:"Thank You · Post Discovery Day", category:"follow_up", icon:"🙏", color:"#f97316", subject:"Thank you for joining us, {FIRST_NAME}",
    color:"#f97316",
    blocks:[
      { id:"t_h1", type:"header", text:"Thank you for coming.", fontSize:36, fontSizeMobile:26, align:"center", color:"#ffffff", blockBg:"#f97316", blockBgCorner:"square", fontFamily:"'Playfair Display',Georgia,serif", padding:36 },
      { id:"t_p1", type:"para", text:"Hi {FIRST_NAME} — it was a pleasure hosting you at Discovery Day. The team is genuinely excited about the prospect of partnering with you.", fontSize:16, lineHeight:1.7, align:"left", padding:12 },
      { id:"t_p2", type:"para", text:"Here are your next steps:", fontSize:15, lineHeight:1.7, align:"left", padding:6 },
      { id:"t_p3", type:"para", text:"1. <b>Review the agreement</b> we'll send within 48 hours<br/>2. <b>Confirm financing</b> letter from your lender<br/>3. <b>Schedule the signing call</b> when you're ready", fontSize:15, lineHeight:1.9, align:"left", padding:10 },
      { id:"t_div", type:"divider", style:"dotted", color:"#f97316", thickness:2, padding:18 },
      { id:"t_btn", type:"btn", text:"Schedule signing call", url:"#", bgColor:"#f97316", color:"#ffffff", btnCornerStyle:"pill", btnRadius:99, fontSize:14, align:"center", padding:10 },
      { id:"t_foot", type:"footer", companyName:"{COMPANY_NAME|{BRAND}}", companyAddress:"{COMPANY_ADDRESS}", showUnsubscribe:true, fontSize:11, padding:8 },
    ]
  },
];

// Default folders seeded for new brands. Once seeded, folders become editable
// per-brand storage (see `ff4_template_folders` and the `templateFolders` state),
// supporting create / rename / delete / nesting (parentId) and a Private/Group
// scope flag intended to power shared-folder semantics when the SaaS proxy ships.
const TEMPLATE_FOLDERS = [
  { id:"marketing", label:"Marketing", icon:"📣", color:"#3b82f6" },
  { id:"follow_up", label:"Follow-up", icon:"↻",  color:"#60a5fa" },
];
const FOLDER_ICONS = ["📁","📂","📣","↻","✉️","📧","📬","📨","💌","📥","📤","✨","⭐","🎯","🏆","🎁","🔥","💡","💼","📌","📋","🗂","🏷","🔖","📞","🤝","🎉","💬","🚀","🌐","🔒"];

const AUTOMATION_TRIGGERS = [
  { id:"new_lead",          label:"New Lead Created" },
  { id:"lead_stage_change", label:"Lead Stage Changes" },
  { id:"opp_stage_change",  label:"Opportunity Stage Changes" },
  { id:"score_drops",       label:"Opportunity Score Drops Below…" },
  { id:"no_activity",       label:"No Activity for N Days" },
  { id:"stale",             label:"Opportunity Becomes Stale" },
  { id:"note_added",        label:"Note Added" },
];
const AUTOMATION_ACTIONS = [
  { id:"send_template", label:"Send Email Template" },
  { id:"notify",        label:"In-App Notification" },
  { id:"change_stage",  label:"Change Stage" },
  { id:"assign_broker", label:"Assign Broker" },
  { id:"flag",          label:"Add Risk Flag" },
];
const CONDITION_OPS = ["equals","not equals","contains","greater than","less than","is empty","is not empty"];

// Anthropic model options exposed in Settings → AI.
// Haiku is default everywhere: fast and ~3× cheaper than Sonnet, ~15× cheaper than Opus.
const AI_MODELS = [
  { id: "claude-haiku-4-5",  label: "Haiku 4.5",  desc: "Fast & cheap — default" },
  { id: "claude-sonnet-4-6", label: "Sonnet 4.6", desc: "Balanced reasoning · ~3× cost" },
  { id: "claude-opus-4-7",   label: "Opus 4.7",   desc: "Maximum reasoning · ~15× cost" },
];

const DEFAULT_SETTINGS = {
  // Profile
  repName: "", repRole: "Franchise Development Rep", repEmail: "", repPhone: "",
  // Company
  companyName: "", companyWebsite: "", companyPhone: "", companyEmail: "", companyAddress: "",
  // Locale
  timezone: "AUTO", dateFormat: "MMM D, YYYY", timeFormat: "12h", currency: "USD", language: "en", weekStartsOn: "Sunday",
  // Appearance
  theme: "dark", density: "comfortable", accentColor: "#3b82f6", sidebarDefault: "open", showWelcomeBanner: true,
  // Notifications
  inAppToasts: true, toastDuration: "medium", soundEnabled: false, emailDigest: "daily", browserPush: false,
  notifyOnStale: true, notifyOnNewLead: true, notifyOnBrokerReply: true, notifyOnScoreDrop: false,
  // Pipeline
  defaultView: "list", showScoreRings: true, showStaleBadges: true, autoStageOnDocusign: true,
  highScoreThreshold: 75, lowScoreThreshold: 30, autoConvertOnFddSigned: false, requireTerritoryOnNew: false,
  // AI — per-feature model choice. Haiku 4.5 default everywhere = cheapest path.
  aiModelScore: "claude-haiku-4-5", aiModelWhatsNext: "claude-haiku-4-5", aiModelSummary: "claude-haiku-4-5",
  aiModelOrganize: "claude-haiku-4-5", aiModelFDD: "claude-haiku-4-5",
  autoScoreOnLoad: true, autoScoreOnNote: true, aiSummaryAutoOpen: false,
  aiOrganizeFrequency: "manual", aiToneHint: "professional", aiClassifyNotes: true,
  // Email & Templates
  emailSignature: "", fromName: "", defaultEmailFooter: "", trackingPixel: false, unsubscribeLink: true,
  bccArchive: "", defaultMergeBrandColor: true,
  // Integrations
  twilioSid: "", sendgridKey: "", zapierWebhook: "", slackWebhook: "",
  // Brokers
  showBrokerInLeads: false, defaultCommissionStructure: "50% of franchise fee", autoAssignBroker: false,
  brokerNotifications: true, brokerEmailCC: false, requireBrokerOnLargeDeals: false,
  // Security
  twoFactorEnabled: false, sessionTimeout: 60, passwordExpiry: 90, auditLog: true, ipAllowlist: "",
  // Advanced
  webhookUrl: "", apiAccessEnabled: false, debugMode: false, betaFeatures: false, dataRetentionDays: 730,
  experimentalKanbanLanes: false, useFunctionalUpdates: true,
  // ─── Configurable taxonomies (Phase 1: lead intake) ─────────────────────────
  // Net worth ranges in $100k increments up to a $2M+ ceiling, liquidity in $50k
  // increments to $500k+. The defaults are surfaced in lead/opp dropdowns and
  // can be reordered / renamed / extended in Settings → Lead Intake.
  netWorthOptions: ["Under $100k","$100k–$200k","$200k–$300k","$300k–$400k","$400k–$500k","$500k–$600k","$600k–$700k","$700k–$800k","$800k–$900k","$900k–$1M","$1M–$1.1M","$1.1M–$1.2M","$1.2M–$1.3M","$1.3M–$1.4M","$1.4M–$1.5M","$1.5M–$1.6M","$1.6M–$1.7M","$1.7M–$1.8M","$1.8M–$1.9M","$1.9M–$2M","$2M+"],
  liquidityOptions: ["Under $50k","$50k–$100k","$100k–$150k","$150k–$200k","$200k–$250k","$250k–$300k","$300k–$350k","$350k–$400k","$400k–$450k","$450k–$500k","$500k+"],
  // Lead sources — each entry: {id, name, type, config?}. `type` drives the integration setup
  // wizard ("manual" / "website_form" / "linkedin_lead_gen" / "meta_lead_ads" / "email_parser" /
  // "google_forms" / "zapier" / "webhook" / "csv_import"). Only `manual` sources are
  // available immediately; other types persist their config but stay "Setup pending" until
  // the SaaS proxy lights up the connector.
  leadSources: [
    { id:"manual_referral",   name:"Referral",      type:"manual" },
    { id:"manual_website",    name:"Website",       type:"manual" },
    { id:"manual_linkedin",   name:"LinkedIn",      type:"manual" },
    { id:"manual_trade_show", name:"Trade Show",    type:"manual" },
    { id:"manual_cold",       name:"Cold Outreach", type:"manual" },
  ],
  // Custom record fields — let admins extend leads/opps/brokers with extra boxes (text,
  // textarea, number, dropdown, date, checkbox). Native fields (firstName, email, phone,
  // etc.) are not represented here and cannot be edited or deleted; only user-added
  // fields live in these arrays. Each field: {id, key, label, type, options?, placeholder?, required?}.
  customFields: { leads: [], opps: [], brokers: [] },
  // Seats (placeholder until SaaS multi-seat ships). Drives the Assigned To dropdown.
  // Each entry: {id, name, email, brandAccess: [brandId...]}. brandAccess [] = all brands.
  seats: [],
};

function ToggleSwitch({ checked, onChange, disabled=false }) {
  return (
    <button type="button" onClick={()=>!disabled && onChange(!checked)} disabled={disabled}
      style={{ background: disabled?"#1a2030":(checked?"#4ade80":"#233045"), border:"none", width:42, height:24, borderRadius:13, cursor:disabled?"not-allowed":"pointer", position:"relative", transition:"background .2s", fontFamily:"inherit", padding:0, opacity:disabled?0.5:1, flexShrink:0 }}>
      <div style={{ position:"absolute", top:3, left: checked?21:3, width:18, height:18, borderRadius:"50%", background:"#fff", transition:"left .15s", boxShadow:"0 1px 3px rgba(0,0,0,.3)" }}/>
    </button>
  );
}

function SettingRow({ label, description, badge, children }) {
  return (
    <div style={{ display:"flex", alignItems:"center", gap:14, padding:"13px 0", borderTop:`1px solid #152030` }}>
      <div style={{flex:1, minWidth:0}}>
        <div style={{ fontSize:13, fontWeight:700, color:"#dde4f0", display:"flex", alignItems:"center", gap:7 }}>
          {label}
          {badge && <span style={{ background:badge.bg||"#1a1908", color:badge.color||"#facc15", border:`1px solid ${(badge.color||"#facc15")}33`, borderRadius:4, padding:"1px 7px", fontSize:9, fontWeight:700, letterSpacing:".04em" }}>{badge.text}</span>}
        </div>
        {description && <div style={{ fontSize:11, color:"#4a5870", marginTop:3, lineHeight:1.55 }}>{description}</div>}
      </div>
      <div style={{flexShrink:0}}>{children}</div>
    </div>
  );
}

function SettingsSection({ title, children, subtitle }) {
  return (
    <div style={{marginBottom:28}}>
      <div style={{fontSize:11, fontWeight:800, color:"#94a3b8", letterSpacing:".08em", textTransform:"uppercase", marginBottom:2}}>{title}</div>
      {subtitle && <div style={{fontSize:11, color:"#4a5870", marginBottom:6}}>{subtitle}</div>}
      <div>{children}</div>
    </div>
  );
}

// ═══════════════════════════════════════════════════════════
//  SETTINGS: LEAD INTAKE  (net worth, liquidity, lead sources)
// ═══════════════════════════════════════════════════════════
function RangeListEditor({ label, description, valueKey, settings, setSetting, helpText }) {
  const list = (settings[valueKey] && settings[valueKey].length) ? settings[valueKey] : (DEFAULT_SETTINGS[valueKey]||[]);
  const [draft, setDraft] = React.useState("");
  const save = (next) => setSetting(valueKey, next);
  return (
    <SettingsSection title={label} subtitle={description}>
      <div style={{display:"flex",flexDirection:"column",gap:4,marginBottom:10}}>
        {list.map((opt, i) => (
          <div key={i} style={{display:"flex",alignItems:"center",gap:6,background:"#090f1c",border:`1px solid ${C.border}`,borderRadius:7,padding:"6px 9px"}}>
            <span style={{fontSize:10,color:C.dim,fontWeight:700,minWidth:18}}>{i+1}.</span>
            <input value={opt} onChange={e=>{ const n=[...list]; n[i]=e.target.value; save(n); }} style={inp({flex:1,boxSizing:"border-box",fontSize:12,padding:"5px 9px"})}/>
            <button onClick={()=>{ if (i>0){ const n=[...list]; [n[i-1],n[i]]=[n[i],n[i-1]]; save(n); } }} disabled={i===0} title="Move up" style={{...btn(C.dim,C.muted),padding:"2px 7px",fontSize:11,opacity:i===0?0.4:1}}>↑</button>
            <button onClick={()=>{ if (i<list.length-1){ const n=[...list]; [n[i+1],n[i]]=[n[i],n[i+1]]; save(n); } }} disabled={i===list.length-1} title="Move down" style={{...btn(C.dim,C.muted),padding:"2px 7px",fontSize:11,opacity:i===list.length-1?0.4:1}}>↓</button>
            <button onClick={()=>save(list.filter((_,j)=>j!==i))} title="Delete option" style={{...btn("#1a0808","#f87171"),padding:"2px 7px",fontSize:11}}>🗑</button>
          </div>
        ))}
      </div>
      <div style={{display:"flex",gap:7}}>
        <input value={draft} onChange={e=>setDraft(e.target.value)} placeholder={helpText||"+ Add a new range…"} style={inp({flex:1,boxSizing:"border-box",fontSize:12})} onKeyDown={e=>{ if(e.key==="Enter" && draft.trim()){ save([...list, draft.trim()]); setDraft(""); } }}/>
        <button onClick={()=>{ if (draft.trim()){ save([...list, draft.trim()]); setDraft(""); } }} disabled={!draft.trim()} style={{...btn("#091c09","#4ade80",!!draft.trim()),opacity:draft.trim()?1:0.5,cursor:draft.trim()?"pointer":"not-allowed"}}>+ Add</button>
        <button onClick={()=>{ if (confirm("Reset to default ranges?")) save(DEFAULT_SETTINGS[valueKey]||[]); }} title="Reset to default ranges" style={btn(C.dim,C.muted)}>↺ Reset</button>
      </div>
    </SettingsSection>
  );
}

// Lead-source integration types — surface common ways frandev teams ingest leads. Each entry
// describes the connector visually + has a "setup" callout (real activation happens once the
// SaaS proxy ships; for now the configuration persists and the source becomes available in
// dropdowns immediately, while the actual data pipe is marked "Setup pending").
const LEAD_SOURCE_TYPES = [
  { id:"manual",            icon:"✋", label:"Manual",                   desc:"Reps enter leads by hand. No setup required.",                                    instant:true },
  { id:"website_form",      icon:"🌐", label:"Website Form",             desc:"Paste an embed snippet onto your franchise inquiry page.",                        instant:false },
  { id:"linkedin_lead_gen", icon:"💼", label:"LinkedIn Lead Gen Forms",  desc:"Connect a LinkedIn Lead Gen Form via OAuth to auto-import inquiries.",            instant:false },
  { id:"meta_lead_ads",     icon:"📣", label:"Meta Lead Ads",            desc:"Connect Facebook / Instagram Lead Ads to pull inquiries straight in.",            instant:false },
  { id:"google_forms",      icon:"📋", label:"Google Forms",             desc:"Sync a Google Form's responses into the CRM.",                                    instant:false },
  { id:"email_parser",      icon:"✉️", label:"Email Parser",             desc:"Forward inquiry emails to a unique inbox we provide. We parse + create leads.",   instant:false },
  { id:"zapier",            icon:"⚡", label:"Zapier / Make",            desc:"Use Zapier or Make.com to map any source's payload into a lead.",                 instant:false },
  { id:"webhook",           icon:"🔗", label:"Custom Webhook",           desc:"POST JSON to a generated URL. For custom CRMs, internal tools, or no-code stacks.", instant:false },
  { id:"csv_import",        icon:"📂", label:"CSV Import",               desc:"One-off — upload a CSV of leads to bulk-create them. Setup is per import.",       instant:true },
];

function LeadSourceConnectModal({ source, onSave, onCancel }) {
  const [picked, setPicked] = useState(source.type || null);
  const [name, setName] = useState(source.name || "");
  const cfg = LEAD_SOURCE_TYPES.find(t => t.id === picked);
  return (
    <div onClick={onCancel} style={{position:"fixed",inset:0,background:"#000c",zIndex:1100,display:"flex",alignItems:"center",justifyContent:"center",padding:16}}>
      <div onClick={e=>e.stopPropagation()} style={{background:C.panel,border:`1px solid ${C.border}`,borderRadius:14,width:"100%",maxWidth:560,maxHeight:"90vh",display:"flex",flexDirection:"column"}}>
        <div style={{display:"flex",alignItems:"center",gap:11,padding:"16px 20px",borderBottom:`1px solid ${C.border}`}}>
          <span style={{fontSize:22}}>🔌</span>
          <div style={{flex:1}}>
            <h3 style={{margin:0,fontSize:15,fontWeight:900,color:"#f0f6ff"}}>Add Lead Source</h3>
            <div style={{fontSize:11,color:C.muted,marginTop:2}}>Pick a name and (optionally) wire up an integration.</div>
          </div>
          <button onClick={onCancel} title="Close" style={btn(C.dim,C.muted)}>✕</button>
        </div>
        <div style={{padding:"14px 20px",overflowY:"auto",flex:1}}>
          <div style={{fontSize:9,fontWeight:800,color:C.dim,letterSpacing:".08em",marginBottom:6}}>NAME</div>
          <input value={name} onChange={e=>setName(e.target.value)} placeholder="e.g. Trade Show 2026" style={inp({width:"100%",boxSizing:"border-box",marginBottom:14})} autoFocus/>
          <div style={{fontSize:9,fontWeight:800,color:C.dim,letterSpacing:".08em",marginBottom:6}}>HOW DO LEADS COME IN?</div>
          <div style={{display:"flex",flexDirection:"column",gap:7}}>
            {LEAD_SOURCE_TYPES.map(t => {
              const sel = picked === t.id;
              return (
                <button key={t.id} onClick={()=>setPicked(t.id)} style={{background:sel?"#162035":"#090f1c",border:`1.5px solid ${sel?C.accent+"88":C.border}`,borderRadius:9,padding:"11px 13px",cursor:"pointer",fontFamily:"inherit",textAlign:"left",color:C.text,display:"flex",alignItems:"center",gap:11}}>
                  <span style={{fontSize:20,flexShrink:0}}>{t.icon}</span>
                  <div style={{flex:1,minWidth:0}}>
                    <div style={{display:"flex",alignItems:"center",gap:7}}>
                      <div style={{fontSize:12,fontWeight:800,color:sel?C.accent:C.text}}>{t.label}</div>
                      {!t.instant && <span style={{fontSize:8,fontWeight:800,color:"#facc15",background:"#1a1908",border:"1px solid #facc1533",borderRadius:4,padding:"1px 5px",letterSpacing:".05em"}}>SETUP PENDING</span>}
                    </div>
                    <div style={{fontSize:11,color:C.muted,marginTop:2,lineHeight:1.4}}>{t.desc}</div>
                  </div>
                </button>
              );
            })}
          </div>
          {cfg && !cfg.instant && (
            <div style={{background:"#1a1908",border:"1px solid #facc1533",borderRadius:8,padding:"10px 13px",marginTop:12,fontSize:11,color:"#facc15",lineHeight:1.5}}>
              ⚠️ <strong style={{color:"#fde68a"}}>Connector activation</strong> arrives with multi-seat SaaS. Your source will be saved and selectable in dropdowns right away; the live data pipe lights up when the proxy ships.
            </div>
          )}
        </div>
        <div style={{display:"flex",justifyContent:"flex-end",gap:8,padding:"12px 20px",borderTop:`1px solid ${C.border}`}}>
          <button onClick={onCancel} style={btn(C.dim,C.muted)}>Cancel</button>
          <button onClick={()=>name.trim() && picked && onSave({ ...source, name: name.trim(), type: picked })} disabled={!name.trim() || !picked} style={{...btn("#091c09","#4ade80",!!(name.trim()&&picked)),opacity:(name.trim()&&picked)?1:0.5,cursor:(name.trim()&&picked)?"pointer":"not-allowed"}}>{source.id?"Save":"Add Source"}</button>
        </div>
      </div>
    </div>
  );
}

function LeadIntakeSettings({ settings, setSetting }) {
  const [editingSource, setEditingSource] = useState(null);
  const sources = settings.leadSources || [];
  const saveSource = (s) => {
    const next = s.id ? sources.map(x => x.id===s.id?{...x,...s}:x) : [...sources, {...s, id: Math.random().toString(36).slice(2,10)}];
    setSetting("leadSources", next);
    setEditingSource(null);
  };
  const deleteSource = (id) => { if (confirm("Delete this lead source? Existing leads keep their value as plain text.")) setSetting("leadSources", sources.filter(s => s.id !== id)); };
  return (
    <>
      <RangeListEditor label="Net Worth Ranges" description="Dropdown options offered when a rep sets a candidate's net worth on a lead or opp." valueKey="netWorthOptions" settings={settings} setSetting={setSetting} helpText="e.g. $2M–$3M"/>
      <RangeListEditor label="Liquidity Ranges"  description="Dropdown options offered when a rep sets a candidate's liquid cash on a lead or opp." valueKey="liquidityOptions" settings={settings} setSetting={setSetting} helpText="e.g. $500k–$1M"/>
      <SettingsSection title="Lead Sources" subtitle="Where your leads come from. Adding a new source opens a connector picker (manual, website form, LinkedIn, Meta, email parser, etc.).">
        <div style={{display:"flex",flexDirection:"column",gap:6,marginBottom:11}}>
          {sources.map(s => {
            const t = LEAD_SOURCE_TYPES.find(x => x.id === s.type) || LEAD_SOURCE_TYPES[0];
            return (
              <div key={s.id} style={{display:"flex",alignItems:"center",gap:10,background:"#090f1c",border:`1px solid ${C.border}`,borderRadius:8,padding:"9px 12px"}}>
                <span style={{fontSize:18,flexShrink:0}}>{t.icon}</span>
                <div style={{flex:1,minWidth:0}}>
                  <div style={{display:"flex",alignItems:"center",gap:7}}>
                    <div style={{fontSize:13,fontWeight:700,color:C.text}}>{s.name}</div>
                    {!t.instant && <span style={{fontSize:8,fontWeight:800,color:"#facc15",background:"#1a1908",border:"1px solid #facc1533",borderRadius:4,padding:"1px 5px",letterSpacing:".05em"}}>SETUP PENDING</span>}
                  </div>
                  <div style={{fontSize:10,color:C.muted,marginTop:1}}>{t.label}</div>
                </div>
                <button onClick={()=>setEditingSource(s)} title="Edit source" style={btn(C.dim,C.muted)}>✏️</button>
                <button onClick={()=>deleteSource(s.id)} title="Delete source" style={btn("#1a0808","#f87171")}>🗑</button>
              </div>
            );
          })}
        </div>
        <button onClick={()=>setEditingSource({ type:"manual" })} style={btn("#091c09","#4ade80",true)} title="Add a new lead source + pick how leads get in">+ Add Lead Source</button>
      </SettingsSection>
      {editingSource && <LeadSourceConnectModal source={editingSource} onSave={saveSource} onCancel={()=>setEditingSource(null)}/>}
    </>
  );
}

// ═══════════════════════════════════════════════════════════
//  SETTINGS: CUSTOM FIELDS
// ═══════════════════════════════════════════════════════════
// Built-in fields that admins are not allowed to alter — these power existing UI throughout
// the app, so editing/removing them would break flows. Surface them as read-only chips so
// admins know what's already covered before adding more.
const BUILTIN_FIELDS = {
  leads: ["First Name","Last Name","Email","Phone","Company","Location","Territory","Net Worth","Liquidity","Lead Source","Assigned To","Broker","Stage"],
  opps:  ["First Name","Last Name","Email","Phone","Company","Location","Territory","Net Worth","Liquidity","Lead Source","Assigned To","Broker","Stage"],
  brokers: ["First Name","Last Name","Email","Phone","Network","Specialty","Commission","Notes"],
};

function CustomFieldEditor({ field, onSave, onCancel }) {
  const [form, setForm] = useState(field || { label:"", key:"", type:"text", options:[], placeholder:"", required:false, span:1 });
  const set = (k,v) => setForm(f => ({...f, [k]: v}));
  const [optDraft, setOptDraft] = useState("");
  const valid = (form.label||"").trim() && (form.key||"").trim() && (form.type !== "select" || (form.options||[]).length > 0);
  return (
    <div onClick={onCancel} style={{position:"fixed",inset:0,background:"#000c",zIndex:1100,display:"flex",alignItems:"center",justifyContent:"center",padding:16}}>
      <div onClick={e=>e.stopPropagation()} style={{background:C.panel,border:`1px solid ${C.border}`,borderRadius:14,width:"100%",maxWidth:500,maxHeight:"90vh",display:"flex",flexDirection:"column"}}>
        <div style={{display:"flex",alignItems:"center",gap:11,padding:"16px 20px",borderBottom:`1px solid ${C.border}`}}>
          <span style={{fontSize:22}}>➕</span>
          <div style={{flex:1}}>
            <h3 style={{margin:0,fontSize:15,fontWeight:900,color:"#f0f6ff"}}>{field?.id?"Edit Custom Field":"New Custom Field"}</h3>
            <div style={{fontSize:11,color:C.muted,marginTop:2}}>Add an extra box to this entity's profile + edit form.</div>
          </div>
          <button onClick={onCancel} title="Close" style={btn(C.dim,C.muted)}>✕</button>
        </div>
        <div style={{padding:"14px 20px",overflowY:"auto",flex:1,display:"flex",flexDirection:"column",gap:11}}>
          <div>
            <div style={{fontSize:9,fontWeight:800,color:C.dim,letterSpacing:".08em",marginBottom:5}}>LABEL</div>
            <input value={form.label||""} onChange={e=>{ set("label", e.target.value); if (!field?.id) set("key", e.target.value.toLowerCase().replace(/[^a-z0-9]+/g,"_").replace(/^_|_$/g,"")); }} placeholder="e.g. Spouse Name, Funding Source" style={inp({width:"100%",boxSizing:"border-box"})} autoFocus/>
          </div>
          <div>
            <div style={{fontSize:9,fontWeight:800,color:C.dim,letterSpacing:".08em",marginBottom:5}}>KEY (internal identifier — letters, digits, underscore only)</div>
            <input value={form.key||""} onChange={e=>set("key", e.target.value.replace(/[^a-zA-Z0-9_]/g,""))} placeholder="e.g. spouse_name" disabled={!!field?.id} style={inp({width:"100%",boxSizing:"border-box",opacity:field?.id?0.6:1})}/>
          </div>
          <div>
            <div style={{fontSize:9,fontWeight:800,color:C.dim,letterSpacing:".08em",marginBottom:5}}>FIELD TYPE</div>
            <select value={form.type} onChange={e=>set("type", e.target.value)} style={inp({width:"100%",boxSizing:"border-box"})}>
              <option value="text">Text (single line)</option>
              <option value="textarea">Textarea (multi-line)</option>
              <option value="number">Number</option>
              <option value="date">Date</option>
              <option value="select">Dropdown</option>
              <option value="checkbox">Checkbox</option>
            </select>
          </div>
          {form.type === "select" && (
            <div>
              <div style={{fontSize:9,fontWeight:800,color:C.dim,letterSpacing:".08em",marginBottom:5}}>OPTIONS</div>
              <div style={{display:"flex",flexDirection:"column",gap:4,marginBottom:7}}>
                {(form.options||[]).map((o, i) => (
                  <div key={i} style={{display:"flex",gap:5,alignItems:"center"}}>
                    <input value={o} onChange={e=>{ const n=[...form.options]; n[i]=e.target.value; set("options", n); }} style={inp({flex:1,boxSizing:"border-box",fontSize:12,padding:"5px 9px"})}/>
                    <button onClick={()=>set("options", form.options.filter((_,j)=>j!==i))} title="Remove option" style={{...btn("#1a0808","#f87171"),padding:"2px 7px",fontSize:11}}>🗑</button>
                  </div>
                ))}
              </div>
              <div style={{display:"flex",gap:5}}>
                <input value={optDraft} onChange={e=>setOptDraft(e.target.value)} placeholder="+ Add option" style={inp({flex:1,boxSizing:"border-box",fontSize:12})} onKeyDown={e=>{ if(e.key==="Enter" && optDraft.trim()){ set("options", [...(form.options||[]), optDraft.trim()]); setOptDraft(""); } }}/>
                <button onClick={()=>{ if (optDraft.trim()){ set("options", [...(form.options||[]), optDraft.trim()]); setOptDraft(""); } }} disabled={!optDraft.trim()} style={{...btn("#091420","#60a5fa",!!optDraft.trim()),opacity:optDraft.trim()?1:0.5,cursor:optDraft.trim()?"pointer":"not-allowed"}}>+ Add</button>
              </div>
            </div>
          )}
          {form.type !== "checkbox" && (
            <div>
              <div style={{fontSize:9,fontWeight:800,color:C.dim,letterSpacing:".08em",marginBottom:5}}>PLACEHOLDER (optional)</div>
              <input value={form.placeholder||""} onChange={e=>set("placeholder", e.target.value)} placeholder="Hint shown when the field is empty" style={inp({width:"100%",boxSizing:"border-box"})}/>
            </div>
          )}
          <div style={{display:"flex",alignItems:"center",gap:11}}>
            <label style={{display:"flex",alignItems:"center",gap:7,fontSize:12,color:C.text,cursor:"pointer"}}>
              <input type="checkbox" checked={!!form.required} onChange={e=>set("required", e.target.checked)}/>
              <span>Required</span>
            </label>
            <label style={{display:"flex",alignItems:"center",gap:7,fontSize:12,color:C.text,cursor:"pointer"}}>
              <input type="checkbox" checked={form.span===2} onChange={e=>set("span", e.target.checked?2:1)}/>
              <span>Full-width on edit form</span>
            </label>
          </div>
        </div>
        <div style={{display:"flex",justifyContent:"flex-end",gap:8,padding:"12px 20px",borderTop:`1px solid ${C.border}`}}>
          <button onClick={onCancel} style={btn(C.dim,C.muted)}>Cancel</button>
          <button onClick={()=>valid && onSave(form)} disabled={!valid} style={{...btn("#091c09","#4ade80",valid),opacity:valid?1:0.5,cursor:valid?"pointer":"not-allowed"}}>{field?.id?"Save":"Add Field"}</button>
        </div>
      </div>
    </div>
  );
}

function CustomFieldsSettings({ settings, setSetting }) {
  const [tab, setTab] = useState("leads");
  const [editing, setEditing] = useState(null);
  const fields = ((settings.customFields||{})[tab]||[]);
  const save = (f) => {
    const next = f.id ? fields.map(x => x.id===f.id?{...x,...f}:x) : [...fields, {...f, id: Math.random().toString(36).slice(2,10)}];
    setSetting("customFields", { ...(settings.customFields||{}), [tab]: next });
    setEditing(null);
  };
  const del = (id) => { if (confirm("Delete this custom field? Values stored on existing records remain in localStorage but stop displaying.")) {
    setSetting("customFields", { ...(settings.customFields||{}), [tab]: fields.filter(f => f.id !== id) });
  }};
  return (
    <>
      <SettingsSection title="Built-in fields (not editable)" subtitle={`These ship with ${BRAND.name} and power existing flows — they can't be removed or renamed.`}>
        <div style={{display:"flex",flexWrap:"wrap",gap:5,marginBottom:6}}>
          {(BUILTIN_FIELDS[tab]||[]).map(f => (
            <span key={f} style={{background:"#090f1c",border:`1px solid ${C.border}`,borderRadius:14,padding:"3px 10px",fontSize:11,color:C.muted}}>🔒 {f}</span>
          ))}
        </div>
      </SettingsSection>
      <SettingsSection title="Entity">
        <div style={{display:"flex",gap:5,background:"#090f1c",border:`1px solid ${C.border}`,borderRadius:9,padding:3,maxWidth:380}}>
          {[["leads","Leads"],["opps","Opportunities"],["brokers","Brokers"]].map(([k,l])=>(
            <button key={k} onClick={()=>setTab(k)} style={{flex:1,background:tab===k?"#162035":"transparent",border:"none",borderRadius:6,padding:"7px 10px",color:tab===k?C.accent:C.muted,fontSize:12,fontWeight:tab===k?800:600,cursor:"pointer",fontFamily:"inherit"}}>{l}</button>
          ))}
        </div>
      </SettingsSection>
      <SettingsSection title={`Custom fields on ${tab === "leads" ? "Leads" : tab === "opps" ? "Opportunities" : "Brokers"}`} subtitle="Rendered alongside built-in fields on the edit form and profile.">
        {fields.length === 0 ? (
          <div style={{fontSize:12,color:C.muted,fontStyle:"italic",padding:"12px 0"}}>No custom fields yet. Add one to capture data the built-in fields don't cover (e.g. Spouse Name, Funding Source, Discovery Day Date).</div>
        ) : (
          <div style={{display:"flex",flexDirection:"column",gap:6,marginBottom:11}}>
            {fields.map(f => (
              <div key={f.id} style={{display:"flex",alignItems:"center",gap:10,background:"#090f1c",border:`1px solid ${C.border}`,borderRadius:8,padding:"9px 12px"}}>
                <div style={{flex:1,minWidth:0}}>
                  <div style={{display:"flex",alignItems:"center",gap:7}}>
                    <div style={{fontSize:13,fontWeight:700,color:C.text}}>{f.label}</div>
                    {f.required && <span style={{fontSize:8,color:"#f87171",fontWeight:800,background:"#1a0808",border:"1px solid #f8717133",borderRadius:4,padding:"1px 5px",letterSpacing:".05em"}}>REQUIRED</span>}
                  </div>
                  <div style={{fontSize:10,color:C.muted,marginTop:2}}>{f.type} · key: <code style={{color:"#60a5fa"}}>{f.key}</code>{f.type==="select" && f.options?.length ? ` · ${f.options.length} options` : ""}</div>
                </div>
                <button onClick={()=>setEditing(f)} title="Edit field" style={btn(C.dim,C.muted)}>✏️</button>
                <button onClick={()=>del(f.id)} title="Delete field" style={btn("#1a0808","#f87171")}>🗑</button>
              </div>
            ))}
          </div>
        )}
        <button onClick={()=>setEditing({})} style={btn("#091c09","#4ade80",true)}>+ Add Custom Field</button>
      </SettingsSection>
      {editing && <CustomFieldEditor field={editing} onSave={save} onCancel={()=>setEditing(null)}/>}
    </>
  );
}

// ═══════════════════════════════════════════════════════════
//  SETTINGS: TEAM & SEATS
// ═══════════════════════════════════════════════════════════
function TeamSeatsSettings({ settings, setSetting, brands, activeBrand }) {
  const [editing, setEditing] = useState(null);
  const seats = settings.seats || [];
  const save = (s) => {
    const next = s.id ? seats.map(x => x.id===s.id?{...x,...s}:x) : [...seats, {...s, id: Math.random().toString(36).slice(2,10)}];
    setSetting("seats", next);
    setEditing(null);
  };
  const del = (id) => { if (confirm("Remove this team member? They'll disappear from Assigned To dropdowns.")) setSetting("seats", seats.filter(s => s.id !== id)); };
  const activeBrandName = brands.find(b => b.id === activeBrand)?.name || "active brand";
  return (
    <>
      <SettingsSection title="You" subtitle="Always available in Assigned To dropdowns.">
        <div style={{background:"#090f1c",border:`1px solid ${C.border}`,borderRadius:8,padding:"9px 12px",display:"flex",alignItems:"center",gap:10}}>
          <Ava name={settings.repName || "You"} size={32}/>
          <div style={{flex:1,minWidth:0}}>
            <div style={{fontSize:13,fontWeight:700,color:C.text}}>{settings.repName || "(set your name in Profile)"}</div>
            <div style={{fontSize:10,color:C.muted}}>{settings.repRole}{settings.repEmail?` · ${settings.repEmail}`:""}</div>
          </div>
          <span style={{fontSize:9,fontWeight:800,color:"#4ade80",background:"#091c09",border:"1px solid #4ade8055",borderRadius:4,padding:"1px 6px",letterSpacing:".05em"}}>OWNER</span>
        </div>
      </SettingsSection>
      <SettingsSection title="Additional Seats" subtitle={`Team members below appear in the Assigned To dropdown on the ${activeBrandName} pipeline (and any others they have access to). Once SaaS multi-seat launches, each seat will get its own login and permission set.`}>
        {seats.length === 0 ? (
          <div style={{fontSize:12,color:C.muted,fontStyle:"italic",padding:"12px 0"}}>No additional team members yet. Add one to make their name selectable in Assigned To dropdowns.</div>
        ) : (
          <div style={{display:"flex",flexDirection:"column",gap:6,marginBottom:11}}>
            {seats.map(s => {
              const all = !s.brandAccess?.length;
              const accessLabel = all ? `All brands (${brands.length})` : `${s.brandAccess.length} of ${brands.length} brands`;
              const roleLabel = (SEAT_ROLES.find(r => r.id === s.role)?.label) || "Frandev Rep";
              const perms = s.permissions || permsForRole(s.role || "frandev_rep");
              const defaultPerms = new Set(permsForRole(s.role || "frandev_rep"));
              const matches = perms.length === defaultPerms.size && perms.every(p => defaultPerms.has(p));
              return (
                <div key={s.id} style={{display:"flex",alignItems:"center",gap:10,background:"#090f1c",border:`1px solid ${C.border}`,borderRadius:8,padding:"9px 12px"}}>
                  <Ava name={s.name||"?"} size={32}/>
                  <div style={{flex:1,minWidth:0}}>
                    <div style={{display:"flex",alignItems:"center",gap:7}}>
                      <div style={{fontSize:13,fontWeight:700,color:C.text}}>{s.name}</div>
                      <span style={{fontSize:9,fontWeight:800,color:"#a78bfa",background:"#1a1429",border:"1px solid #a78bfa33",borderRadius:4,padding:"1px 5px",letterSpacing:".05em"}}>{roleLabel.toUpperCase()}</span>
                      {!matches && s.role !== "custom" && <span title="Permissions diverge from the role default for this seat" style={{fontSize:9,fontWeight:800,color:"#facc15",background:"#1a1908",border:"1px solid #facc1533",borderRadius:4,padding:"1px 5px",letterSpacing:".05em"}}>CUSTOMIZED</span>}
                    </div>
                    <div style={{fontSize:10,color:C.muted,marginTop:1}}>{s.email||"(no email)"}{s.phone?` · ${s.phone}`:""} · {accessLabel} · {perms.length}/{PERMISSIONS.length} permissions</div>
                  </div>
                  <button onClick={()=>setEditing(s)} title="Edit seat" style={btn(C.dim,C.muted)}>✏️</button>
                  <button onClick={()=>del(s.id)} title="Remove seat" style={btn("#1a0808","#f87171")}>🗑</button>
                </div>
              );
            })}
          </div>
        )}
        <button onClick={()=>setEditing({ brandAccess: [activeBrand] })} style={btn("#091c09","#4ade80",true)}>+ Add Team Member</button>
        <div style={{fontSize:10,color:"#facc15",background:"#1a1908",border:"1px solid #facc1533",borderRadius:6,padding:"8px 12px",marginTop:12,lineHeight:1.5}}>⚠️ Multi-seat sign-in arrives with SaaS. Today, seats here populate dropdowns; they don't get their own login yet.</div>
      </SettingsSection>
      {editing && <TeamSeatEditor seat={editing} brands={brands} onSave={save} onCancel={()=>setEditing(null)}/>}
    </>
  );
}

// Seat role tiers — picking a role pre-fills the permission matrix below, but every
// permission is still individually adjustable per seat (admins can grant a Frandev Rep
// extra access at creation time, or tighten a View-Only seat later). "Custom" starts
// with no permissions checked so the admin builds a bespoke set from scratch.
const SEAT_ROLES = [
  { id:"admin",        label:"Admin",        desc:"Full access. Can add/remove brands, seats, integrations, and edit anything." },
  { id:"frandev_rep",  label:"Frandev Rep",  desc:"Default for a sales rep. Manages leads/opps, brokers, templates, and scheduling." },
  { id:"lead_caller",  label:"Lead Caller",  desc:"Inbound caller — can view leads, log calls, and update stage, but can't send agreements." },
  { id:"view_only",    label:"View Only",    desc:"Read-only across the brand. Useful for execs, accountants, and observers." },
  { id:"custom",       label:"Custom",       desc:"Hand-picked permissions. Start blank and tick the exact actions this seat can perform." },
];

// Full permission catalog, grouped into logical sections that admins can scan top-down.
// Adding a new permission later just means appending to this list and updating each
// role's default set below. The id is stored on `seat.permissions`.
const PERMISSIONS = [
  // Pipeline
  { id:"leads.view",       group:"Pipeline",      label:"View leads",                 desc:"See the leads list and lead profiles." },
  { id:"leads.create",     group:"Pipeline",      label:"Create leads",               desc:"Add new leads manually or via import." },
  { id:"leads.edit",       group:"Pipeline",      label:"Edit leads",                 desc:"Update lead fields, stage, and notes." },
  { id:"leads.delete",     group:"Pipeline",      label:"Delete leads",               desc:"Permanently remove a lead from the brand." },
  { id:"opps.view",        group:"Pipeline",      label:"View opportunities",         desc:"See the opportunities list and profiles." },
  { id:"opps.create",      group:"Pipeline",      label:"Create opportunities",       desc:"Convert leads or create opps from scratch." },
  { id:"opps.edit",        group:"Pipeline",      label:"Edit opportunities",         desc:"Update opp fields, stage, notes, partners." },
  { id:"opps.delete",      group:"Pipeline",      label:"Delete opportunities",       desc:"Permanently remove an opportunity." },
  { id:"stages.change",    group:"Pipeline",      label:"Move records between stages", desc:"Drag/drop or stage-change leads & opps." },
  // Communications & Documents
  { id:"comms.send",       group:"Communications",label:"Send emails & SMS",          desc:"Send templates and messages from the CRM." },
  { id:"comms.docusign",   group:"Communications",label:"Send DocuSign envelopes",    desc:"Send FDDs and agreements via DocuSign." },
  { id:"comms.calls",      group:"Communications",label:"Log calls + call tracker",   desc:"Record call notes, advance the touchpoint counter." },
  { id:"templates.view",   group:"Communications",label:"View templates",             desc:"Browse the template library." },
  { id:"templates.edit",   group:"Communications",label:"Create & edit templates",    desc:"Build new templates or update existing ones." },
  // Brokers
  { id:"brokers.view",     group:"Brokers",       label:"View brokers & networks",    desc:"Access the Broker Hub." },
  { id:"brokers.edit",     group:"Brokers",       label:"Edit brokers & networks",    desc:"Add or update brokers and networks." },
  { id:"brokers.delete",   group:"Brokers",       label:"Delete brokers & networks",  desc:"Remove brokers/networks permanently." },
  { id:"brokers.assign",   group:"Brokers",       label:"Assign brokers to deals",    desc:"Link or unlink a broker on an opportunity." },
  // Scheduling
  { id:"scheduling.view",  group:"Scheduling",    label:"View scheduling hub",        desc:"See bookings and event types." },
  { id:"scheduling.book",  group:"Scheduling",    label:"Create & cancel bookings",   desc:"Book meetings for candidates and reschedule." },
  { id:"scheduling.config",group:"Scheduling",    label:"Edit availability & event types", desc:"Change weekly hours, event types, blackouts." },
  // Reports & Analytics
  { id:"reports.view",     group:"Reports",       label:"View analytics & reports",   desc:"Open the Analytics tab and saved reports." },
  { id:"reports.generate", group:"Reports",       label:"Generate new reports",       desc:"Create custom or AI-strategy reports." },
  { id:"reports.delete",   group:"Reports",       label:"Delete reports",             desc:"Remove saved reports from the brand." },
  // Admin
  { id:"settings.edit",    group:"Admin",         label:"Edit settings",              desc:"Change pipeline, AI, email, and integration settings." },
  { id:"brands.manage",    group:"Admin",         label:"Manage brands",              desc:"Add, edit, or delete brands and FDD data." },
  { id:"seats.manage",     group:"Admin",         label:"Manage team seats & roles",  desc:"Invite team members and change permissions." },
  { id:"integrations.manage", group:"Admin",      label:"Manage integrations",        desc:"Connect DocuSign, calendars, lead sources, etc." },
];

// Default permission set per role. "Custom" intentionally starts empty so the admin
// builds the set from scratch. Admins can still tweak any role's permissions per seat.
const DEFAULT_PERMS_BY_ROLE = {
  admin: PERMISSIONS.map(p => p.id),
  frandev_rep: [
    "leads.view","leads.create","leads.edit","opps.view","opps.create","opps.edit","stages.change",
    "comms.send","comms.docusign","comms.calls","templates.view","templates.edit",
    "brokers.view","brokers.edit","brokers.assign",
    "scheduling.view","scheduling.book","scheduling.config",
    "reports.view","reports.generate",
  ],
  lead_caller: [
    "leads.view","leads.create","leads.edit","opps.view","stages.change",
    "comms.calls","templates.view",
    "brokers.view","scheduling.view","scheduling.book","reports.view",
  ],
  view_only: [
    "leads.view","opps.view","templates.view","brokers.view","scheduling.view","reports.view",
  ],
  custom: [],
};
const permsForRole = (role) => DEFAULT_PERMS_BY_ROLE[role] || DEFAULT_PERMS_BY_ROLE.frandev_rep;

function TeamSeatEditor({ seat, brands, onSave, onCancel }) {
  // Initialize form with seat data + role default permissions. For existing seats we
  // honor whatever was saved on the seat (so admin-customized permissions stick); for
  // new seats we seed from the role default and let the admin tweak before saving.
  const initial = seat?.id ? seat : { role: "frandev_rep", ...seat, permissions: permsForRole(seat?.role || "frandev_rep") };
  const [form, setForm] = useState(initial);
  const set = (k,v) => setForm(f => ({...f, [k]: v}));
  // When the admin switches role, snap permissions to that role's default — but they're
  // free to toggle individual entries afterwards. Indicator below the picker shows when
  // the current matrix diverges from the role default ("Customized").
  const applyRole = (roleId) => setForm(f => ({...f, role: roleId, permissions: permsForRole(roleId)}));
  const togglePerm = (pid) => {
    const cur = form.permissions || [];
    set("permissions", cur.includes(pid) ? cur.filter(x => x !== pid) : [...cur, pid]);
  };
  const toggleGroup = (group, on) => {
    const groupIds = PERMISSIONS.filter(p => p.group === group).map(p => p.id);
    const cur = new Set(form.permissions || []);
    if (on) groupIds.forEach(id => cur.add(id));
    else groupIds.forEach(id => cur.delete(id));
    set("permissions", [...cur]);
  };
  const emailIssue = validateEmail(form.email);
  const phoneIssue = validatePhone(form.phone);
  const valid = (form.name||"").trim().length > 0
    && (form.email||"").trim().length > 0 && !emailIssue
    && !phoneIssue
    && !!form.role;
  const toggleBrand = (bid) => {
    const cur = form.brandAccess || [];
    set("brandAccess", cur.includes(bid) ? cur.filter(x => x !== bid) : [...cur, bid]);
  };
  // Group permissions for the matrix UI
  const groupedPerms = PERMISSIONS.reduce((acc, p) => { (acc[p.group] = acc[p.group] || []).push(p); return acc; }, {});
  const groupOrder = ["Pipeline","Communications","Brokers","Scheduling","Reports","Admin"];
  // Detect if the current matrix matches the role default exactly
  const currentSet = new Set(form.permissions || []);
  const defaultSet = new Set(permsForRole(form.role));
  const matchesRoleDefault = currentSet.size === defaultSet.size && [...currentSet].every(id => defaultSet.has(id));
  return (
    <div onClick={onCancel} style={{position:"fixed",inset:0,background:"#000c",zIndex:1100,display:"flex",alignItems:"center",justifyContent:"center",padding:16}}>
      <div onClick={e=>e.stopPropagation()} style={{background:C.panel,border:`1px solid ${C.border}`,borderRadius:14,width:"100%",maxWidth:520,maxHeight:"90vh",display:"flex",flexDirection:"column"}}>
        <div style={{display:"flex",alignItems:"center",gap:11,padding:"16px 20px",borderBottom:`1px solid ${C.border}`}}>
          <span style={{fontSize:22}}>👤</span>
          <div style={{flex:1}}>
            <h3 style={{margin:0,fontSize:15,fontWeight:900,color:"#f0f6ff"}}>{seat?.id?"Edit Seat":"Add Team Member"}</h3>
            <div style={{fontSize:11,color:C.muted,marginTop:2}}>Drives the Assigned To dropdown across brands they can access.</div>
          </div>
          <button onClick={onCancel} title="Close" style={btn(C.dim,C.muted)}>✕</button>
        </div>
        <div style={{padding:"14px 20px",overflowY:"auto",flex:1,display:"flex",flexDirection:"column",gap:11}}>
          <div>
            <div style={{fontSize:9,fontWeight:800,color:C.dim,letterSpacing:".08em",marginBottom:5}}>NAME <span style={{color:"#f87171"}}>*</span></div>
            <input value={form.name||""} onChange={e=>set("name", e.target.value)} placeholder="e.g. Alex Kim" style={inp({width:"100%",boxSizing:"border-box"})} autoFocus/>
          </div>
          <div>
            <div style={{fontSize:9,fontWeight:800,color:C.dim,letterSpacing:".08em",marginBottom:5}}>EMAIL <span style={{color:"#f87171"}}>*</span></div>
            <div style={{position:"relative"}}>
              <input value={form.email||""} onChange={e=>set("email", e.target.value)} placeholder="alex@yourorg.com" style={inp({width:"100%",boxSizing:"border-box", paddingRight: emailIssue?32:undefined, borderColor: emailIssue ? "#fb923c66" : undefined})}/>
              {emailIssue && <span title={emailIssue} style={{position:"absolute",right:9,top:"50%",transform:"translateY(-50%)",fontSize:13,color:"#fb923c",cursor:"help"}}>⚠️</span>}
            </div>
            {emailIssue && <div style={{fontSize:10,color:"#fb923c",marginTop:3,lineHeight:1.4}}>{emailIssue}</div>}
            <div style={{fontSize:10,color:C.dim,marginTop:3,lineHeight:1.4}}>This is the email used to log in when SaaS multi-seat launches.</div>
          </div>
          <div>
            <div style={{fontSize:9,fontWeight:800,color:C.dim,letterSpacing:".08em",marginBottom:5}}>PHONE (optional)</div>
            <div style={{position:"relative"}}>
              <input value={form.phone||""} onChange={e=>set("phone", e.target.value)} placeholder="e.g. 555-123-4567" style={inp({width:"100%",boxSizing:"border-box", paddingRight: phoneIssue?32:undefined, borderColor: phoneIssue ? "#fb923c66" : undefined})}/>
              {phoneIssue && <span title={phoneIssue} style={{position:"absolute",right:9,top:"50%",transform:"translateY(-50%)",fontSize:13,color:"#fb923c",cursor:"help"}}>⚠️</span>}
            </div>
            {phoneIssue && <div style={{fontSize:10,color:"#fb923c",marginTop:3,lineHeight:1.4}}>{phoneIssue}</div>}
          </div>
          <div>
            <div style={{fontSize:9,fontWeight:800,color:C.dim,letterSpacing:".08em",marginBottom:5,display:"flex",alignItems:"center",gap:8}}>
              <span>ROLE <span style={{color:"#f87171"}}>*</span></span>
              {!matchesRoleDefault && form.role !== "custom" && <span title="Permissions below have been customized from the role default." style={{fontSize:8,fontWeight:800,color:"#facc15",background:"#1a1908",border:"1px solid #facc1533",borderRadius:4,padding:"1px 5px",letterSpacing:".05em"}}>CUSTOMIZED</span>}
            </div>
            <div style={{display:"flex",flexDirection:"column",gap:6}}>
              {SEAT_ROLES.map(r => {
                const sel = form.role === r.id;
                return (
                  <button key={r.id} onClick={()=>applyRole(r.id)} style={{background:sel?"#162035":"#090f1c",border:`1.5px solid ${sel?C.accent+"88":C.border}`,borderRadius:9,padding:"9px 12px",textAlign:"left",cursor:"pointer",fontFamily:"inherit",color:C.text}}>
                    <div style={{fontSize:12,fontWeight:800,color:sel?C.accent:C.text}}>{r.label}</div>
                    <div style={{fontSize:10,color:C.muted,marginTop:2,lineHeight:1.4}}>{r.desc}</div>
                  </button>
                );
              })}
            </div>
            <div style={{fontSize:10,color:C.dim,marginTop:7,lineHeight:1.4}}>Picking a role pre-selects its default permissions below. Every permission stays editable — admins can grant extra access at creation, or tighten later.</div>
          </div>
          <div>
            <div style={{display:"flex",alignItems:"center",gap:8,marginBottom:6}}>
              <div style={{fontSize:9,fontWeight:800,color:C.dim,letterSpacing:".08em"}}>PERMISSIONS</div>
              {!matchesRoleDefault && form.role !== "custom" && (
                <button onClick={()=>set("permissions", permsForRole(form.role))} title={`Reset to the default permissions for "${(SEAT_ROLES.find(r=>r.id===form.role)?.label)||form.role}"`} style={{...btn(C.dim,C.muted),padding:"3px 8px",fontSize:10,marginLeft:"auto"}}>↺ Reset to role default</button>
              )}
            </div>
            <div style={{display:"flex",flexDirection:"column",gap:8}}>
              {groupOrder.map(group => {
                const items = groupedPerms[group] || [];
                if (!items.length) return null;
                const allChecked = items.every(p => currentSet.has(p.id));
                const someChecked = items.some(p => currentSet.has(p.id));
                return (
                  <div key={group} style={{background:"#090f1c",border:`1px solid ${C.border}`,borderRadius:8,padding:"10px 12px"}}>
                    <div style={{display:"flex",alignItems:"center",gap:9,marginBottom:7}}>
                      <input
                        type="checkbox"
                        checked={allChecked}
                        ref={el => { if (el) el.indeterminate = someChecked && !allChecked; }}
                        onChange={e => toggleGroup(group, e.target.checked)}
                      />
                      <div style={{fontSize:11,fontWeight:800,color:C.text,letterSpacing:".05em",textTransform:"uppercase"}}>{group}</div>
                      <div style={{fontSize:10,color:C.muted,marginLeft:"auto"}}>{items.filter(p => currentSet.has(p.id)).length} / {items.length}</div>
                    </div>
                    <div style={{display:"flex",flexDirection:"column",gap:4,paddingLeft:21}}>
                      {items.map(p => {
                        const on = currentSet.has(p.id);
                        return (
                          <label key={p.id} style={{display:"flex",alignItems:"flex-start",gap:9,padding:"4px 6px",borderRadius:5,cursor:"pointer"}}>
                            <input type="checkbox" checked={on} onChange={()=>togglePerm(p.id)} style={{marginTop:2}}/>
                            <div style={{flex:1,minWidth:0}}>
                              <div style={{fontSize:12,color:on?C.text:C.muted,fontWeight:on?600:500}}>{p.label}</div>
                              <div style={{fontSize:10,color:C.dim,marginTop:1,lineHeight:1.4}}>{p.desc}</div>
                            </div>
                          </label>
                        );
                      })}
                    </div>
                  </div>
                );
              })}
            </div>
          </div>
          <div>
            <div style={{fontSize:9,fontWeight:800,color:C.dim,letterSpacing:".08em",marginBottom:5}}>BRAND ACCESS</div>
            <div style={{fontSize:10,color:C.muted,marginBottom:6,lineHeight:1.4}}>Pick which brands this person appears in. Leave all unchecked to grant access to every brand (including ones added later).</div>
            <div style={{display:"flex",flexDirection:"column",gap:5}}>
              {brands.map(b => (
                <label key={b.id} style={{display:"flex",alignItems:"center",gap:9,padding:"7px 10px",background:"#090f1c",border:`1px solid ${C.border}`,borderRadius:7,cursor:"pointer"}}>
                  <input type="checkbox" checked={(form.brandAccess||[]).includes(b.id)} onChange={()=>toggleBrand(b.id)}/>
                  <span style={{fontSize:14}}>{b.emoji||"🏢"}</span>
                  <span style={{fontSize:12,color:C.text}}>{b.name}</span>
                </label>
              ))}
            </div>
          </div>
        </div>
        <div style={{display:"flex",justifyContent:"flex-end",gap:8,padding:"12px 20px",borderTop:`1px solid ${C.border}`}}>
          <button onClick={onCancel} style={btn(C.dim,C.muted)}>Cancel</button>
          <button onClick={()=>valid && onSave(form)} disabled={!valid} style={{...btn("#091c09","#4ade80",valid),opacity:valid?1:0.5,cursor:valid?"pointer":"not-allowed"}}>{seat?.id?"Save":"Add Seat"}</button>
        </div>
      </div>
    </div>
  );
}

// ═══════════════════════════════════════════════════════════
//  STORAGE
// ═══════════════════════════════════════════════════════════
const S = {
  async get(k)   { try { const v=localStorage.getItem(k); return v?JSON.parse(v):null; } catch { return null; } },
  async set(k,v) { try { localStorage.setItem(k,JSON.stringify(v)); } catch {} },
};

// ═══════════════════════════════════════════════════════════
//  HELPERS
// ═══════════════════════════════════════════════════════════
const uid       = ()  => Math.random().toString(36).slice(2,10);
const nowIso    = ()  => new Date().toISOString();
const fmtDate   = iso => iso ? new Date(iso).toLocaleDateString("en-US",{month:"short",day:"numeric",year:"numeric"}) : "";
const fmtTime   = iso => iso ? new Date(iso).toLocaleTimeString("en-US",{hour:"numeric",minute:"2-digit"}) : "";
const daysSince = iso => iso ? Math.floor((Date.now()-new Date(iso))/86400000) : 999;

// ── Contact-info validation ───────────────────────────────
// Lightweight format checks for emails and US phone numbers. Returns null if the value is
// empty (so empty fields don't trigger an alert — the warning is only for *populated* but
// *malformed* data). Returns an explanation string when the value looks invalid.
// Email: must have local@domain.tld with at least one dot in the domain. No exotic chars.
// Phone: stripped of non-digits, must be 10 (US) or 11 starting with 1; reject 0/1 leading
//   area codes, repeated single digit, common placeholders, and clearly-too-short strings.
const validateEmail = (raw) => {
  const v = (raw||"").trim();
  if (!v) return null;
  if (v.length > 254) return "Email is too long";
  if (/\s/.test(v)) return "Email contains spaces";
  // Must be exactly one @, with a local part and a TLD-bearing domain.
  if (!/^[^\s@]+@[^\s@]+\.[A-Za-z]{2,}$/.test(v)) return "Email format looks wrong";
  // Common typos in the TLD or domain
  if (/@.*\.\.+/.test(v)) return "Email has consecutive dots";
  if (/(@gmial\.|@gnail\.|@gmaill?\.|@yaho\.|@hotnail\.|@outloo\.|@.{1,}\.con$|@.{1,}\.cmo$)/i.test(v)) return "Possible domain typo (gmial / .con / etc.)";
  return null;
};
const validatePhone = (raw) => {
  const v = (raw||"").trim();
  if (!v) return null;
  const digits = v.replace(/\D/g, "");
  if (digits.length === 0) return "No digits in phone";
  if (digits.length < 10) return `Only ${digits.length} digit${digits.length===1?"":"s"} — US phones need 10`;
  if (digits.length > 11) return "Too many digits";
  // 11-digit numbers must start with country code 1
  if (digits.length === 11 && !digits.startsWith("1")) return "11-digit phone must start with country code 1";
  const tenDigit = digits.length === 11 ? digits.slice(1) : digits;
  // Area codes cannot start with 0 or 1
  if (/^[01]/.test(tenDigit)) return "Area code can't start with 0 or 1";
  // Exchange (4th-6th digit) cannot start with 0 or 1 either
  if (/^.{3}[01]/.test(tenDigit)) return "Exchange (digits 4–6) can't start with 0 or 1";
  // Obvious placeholders
  if (/^(\d)\1{9}$/.test(tenDigit)) return "Looks like a placeholder (all same digit)";
  if (/^1234567890$|^0000000000$/.test(tenDigit)) return "Looks like a placeholder";
  return null;
};

// Period range for Analytics overview — returns {start,end,prevStart,prevEnd,label,prevLabel,isCurrent}.
// For current periods we cap `end` at "now" and compare to the matching first-N-days of the previous
// period (so MTD May 1-20 → compares to Apr 1-20). For navigated past periods, we compare full→full.
function getPeriodRange(mode, anchorIso, customRange) {
  const now = new Date();
  const a = anchorIso ? new Date(anchorIso) : now;
  let start, end, prevStart, prevEnd, label, prevLabel;
  if (mode === "custom" && customRange) {
    start = new Date(customRange.from + "T00:00:00");
    end   = new Date(customRange.to   + "T23:59:59");
    const lenMs = end.getTime() - start.getTime();
    prevEnd   = new Date(start.getTime() - 1);
    prevStart = new Date(start.getTime() - lenMs - 1);
    label = `${start.toLocaleDateString("en-US",{month:"short",day:"numeric"})} – ${end.toLocaleDateString("en-US",{month:"short",day:"numeric"})}`;
    prevLabel = "prior window";
  } else if (mode === "week") {
    const dow = (a.getDay() + 6) % 7;
    start = new Date(a.getFullYear(), a.getMonth(), a.getDate() - dow);
    end   = new Date(start.getTime() + 7*86400000 - 1);
    prevStart = new Date(start.getTime() - 7*86400000);
    prevEnd   = new Date(start.getTime() - 1);
    label = `Week of ${start.toLocaleDateString("en-US",{month:"short",day:"numeric"})}`;
    prevLabel = `prev week`;
  } else if (mode === "quarter") {
    const q = Math.floor(a.getMonth()/3);
    start = new Date(a.getFullYear(), q*3, 1);
    end   = new Date(a.getFullYear(), q*3+3, 1) - 1; end = new Date(end);
    prevStart = new Date(a.getFullYear(), q*3-3, 1);
    prevEnd   = new Date(a.getFullYear(), q*3, 1) - 1; prevEnd = new Date(prevEnd);
    label = `Q${q+1} ${a.getFullYear()}`;
    prevLabel = `Q${q===0?4:q} ${q===0?a.getFullYear()-1:a.getFullYear()}`;
  } else if (mode === "year") {
    start = new Date(a.getFullYear(), 0, 1);
    end   = new Date(a.getFullYear()+1, 0, 1) - 1; end = new Date(end);
    prevStart = new Date(a.getFullYear()-1, 0, 1);
    prevEnd   = new Date(a.getFullYear(), 0, 1) - 1; prevEnd = new Date(prevEnd);
    label = `${a.getFullYear()}`;
    prevLabel = `${a.getFullYear()-1}`;
  } else { // month (default)
    start = new Date(a.getFullYear(), a.getMonth(), 1);
    end   = new Date(a.getFullYear(), a.getMonth()+1, 1) - 1; end = new Date(end);
    prevStart = new Date(a.getFullYear(), a.getMonth()-1, 1);
    prevEnd   = new Date(a.getFullYear(), a.getMonth(), 1) - 1; prevEnd = new Date(prevEnd);
    label = a.toLocaleDateString("en-US",{month:"long",year:"numeric"});
    prevLabel = new Date(a.getFullYear(), a.getMonth()-1, 1).toLocaleDateString("en-US",{month:"short"});
  }
  // For the *current* period, cap end at now and shrink prev window to the matching N days.
  const isCurrent = now >= start && now <= end;
  if (isCurrent && mode !== "custom") {
    const elapsed = now.getTime() - start.getTime();
    end = now;
    prevEnd = new Date(prevStart.getTime() + elapsed);
  }
  return { start, end, prevStart, prevEnd, label, prevLabel, isCurrent, mode };
}
// Shift the anchor by ±1 period unit. Used by the prev/next buttons on the overview.
function shiftPeriod(mode, anchorIso, direction) {
  const a = new Date(anchorIso);
  if (mode === "week")    a.setDate(a.getDate() + 7 * direction);
  else if (mode === "month")   a.setMonth(a.getMonth() + direction);
  else if (mode === "quarter") a.setMonth(a.getMonth() + 3 * direction);
  else if (mode === "year")    a.setFullYear(a.getFullYear() + direction);
  return a.toISOString();
}
// Pct change formatter — handles zero/new-flow cases gracefully.
function pctChange(cur, prev) {
  if (prev === 0 && cur === 0) return { label: "—", color: "#64748b", neutral: true };
  if (prev === 0 && cur > 0)   return { label: "↑ new", color: "#4ade80" };
  if (cur === 0 && prev > 0)   return { label: "↓ −100%", color: "#f87171" };
  const pct = Math.round(((cur - prev) / prev) * 100);
  if (pct === 0) return { label: "no change", color: "#64748b", neutral: true };
  return pct > 0 ? { label: `↑ ${pct}%`, color: "#4ade80" } : { label: `↓ ${Math.abs(pct)}%`, color: "#f87171" };
}
// Heuristic touchpoint classifier — runs instantly, free, ~85% accurate.
// "Touchpoint" = two-way conversation OR one-way INBOUND communication from the candidate.
function heuristicClassifyTouchpoint(text) {
  const raw = (text||"").trim();
  const t = raw.toLowerCase();
  if (!t) return false;
  // Strong negatives — outbound only, attempts, internal plans, observations
  if (/\b(voicemail|no answer|didn'?t pick up|didn'?t answer|missed (calls?|the call))\b/.test(t)) return false;
  if (/^(promised|will |need to|todo|to do|going to|plan to|i'?ll|i need|let me|reminder)\b/i.test(raw)) return false;
  // Strong positives — two-way conversations
  if (/\b(spoke|talked|chat(?:ted)?) (with|to)\b/.test(t)) return true;
  if (/\b(call|meeting|conversation|chat) (with|w\/)\b/.test(t)) return true;
  if (/\b(had an?|on an?) (call|meeting|chat)\b/.test(t)) return true;
  if (/\b(met (with|at|in)|meeting with|came to|attended|joined|connected (with|via|on))\b/.test(t)) return true;
  if (/\b(intro|review|validation|qualifying|discovery) call\b/.test(t) && !/\b(scheduled|planned|will|going to|set up)\b/.test(t)) return true;
  if (/\b(discovery day)\b/.test(t) && /\b(attended|came|confirmed)/.test(t)) return true;
  // Strong positives — one-way inbound from the candidate
  if (/\b(heard back|heard from|got (a |an )?(reply|response)|reply from|response from)\b/.test(t)) return true;
  if (/\b(emailed me|emailed us|texted me|texted us|messaged me|messaged us|reached out|got in touch)\b/.test(t)) return true;
  if (/\b(she said|he said|they said|told me|told us|mentioned (to|that)|asked (us|me|about|for|if))\b/.test(t)) return true;
  if (/\b(submitted|signed|confirmed for|inquired|requested|replied)\b/.test(t)) return true;
  // Default: ambiguous → conservatively NOT a touchpoint (safer for stale detection)
  return false;
}

// AI classification — refines the heuristic guess. Returns boolean.
// Falls through to mock (heuristic) when no API key is set.
// Heuristic call-attempt detector — instant, free, ~85% accurate.
// "Call attempt" = the rep made a phone call, whether or not they connected (includes voicemails, no-answers).
function heuristicClassifyCall(text) {
  const raw = (text||"").trim();
  const t = raw.toLowerCase();
  if (!t) return false;
  // Future-intent → not a call yet
  if (/^(promised|will |going to|plan to|i'?ll|i need|need to|reminder|should|scheduling|trying to schedule)\b/i.test(raw)) return false;
  // Direct call language
  if (/\b(called|calling|phoned|rang|dialed)\b/.test(t) && !/\b(will call|going to call|plan to call|need to call|should call|to call (next|tomorrow|later|back))\b/.test(t)) return true;
  if (/\btried (to call|calling)\b/.test(t)) return true;
  // Voicemail / no answer (counts as attempt)
  if (/\b(voicemail|vm\b|left (a |him a |her a |them a )?message|no answer|didn'?t pick up|no one answered|missed (the )?call)\b/.test(t)) return true;
  // Explicit phone communication
  if (/\b(phone call|on the phone|over the phone|phone conversation|on a call|conference call|phoned in|phoned us)\b/.test(t)) return true;
  // Named call types (intro/review/validation calls etc.) — but only past-tense / done, not scheduled
  if (/\b(intro|review|validation|qualifying|discovery|kickoff|follow-?up|sales|status|update) call\b/.test(t) && !/\b(scheduled|planning|plan|will|going to|set up|book(ed)?)\b/.test(t)) return true;
  return false;
}

// Combined AI classifier — single API call returns both axes. ~30 extra output tokens vs. the
// old yes/no flow — effectively free with Haiku.
async function classifyNote(text) {
  const prompt = `Classify this CRM note on two axes. Reply with JSON only.\n\nNote: "${(text||"").replace(/"/g,"'").slice(0,400)}"\n\nReturn: {"isTouchpoint": <bool>, "isCallAttempt": <bool>}\n- isTouchpoint: true if note describes receiving communication FROM the candidate (two-way exchange OR they emailed/messaged/voicemailed us). False for outbound-only, observations, or plans.\n- isCallAttempt: true if the rep attempted a phone call (connected, voicemail, or no-answer). False for emails, in-person meetings, future intentions, or sent messages.`;
  try {
    const raw = await callClaude([{role:"user", content: prompt}], "", 32, "claude-haiku-4-5");
    const json = JSON.parse(raw.replace(/```json|```/g,"").trim());
    return { isTouchpoint: !!json.isTouchpoint, isCallAttempt: !!json.isCallAttempt };
  } catch {
    return { isTouchpoint: heuristicClassifyTouchpoint(text), isCallAttempt: heuristicClassifyCall(text) };
  }
}

// ── Task helpers ──────────────────────────────────────────────
// Normalize a task string into a stable token bag for cheap similarity checks.
const tokenizeTask = (s) => new Set(String(s||"").toLowerCase().replace(/[^a-z0-9\s]/g," ").split(/\s+/).filter(w => w.length > 2));
// Jaccard similarity on token bags. Returns 0..1. Used to skip near-dupe AI tasks before saving.
function taskSimilarity(a, b) {
  const A = tokenizeTask(a), B = tokenizeTask(b);
  if (!A.size || !B.size) return 0;
  let inter = 0; A.forEach(t => { if (B.has(t)) inter++; });
  return inter / (A.size + B.size - inter);
}
// Parse natural-language relative dates from a note. Returns days from today as an int,
// or null if no date hint is found. Used by mock task extraction.
function parseDueDays(noteText) {
  const t = String(noteText||"").toLowerCase();
  const m1 = t.match(/\b(\d+)\s+(day|week|month)s?\s+(?:from (?:today|now)|out)\b/);
  if (m1) { const n = parseInt(m1[1]); return m1[2]==="month" ? n*30 : m1[2]==="week" ? n*7 : n; }
  const m2 = t.match(/\b(?:in|by)\s+(?:a|an|one|two|three|four|five|six|seven)?\s*(\d+)?\s*(day|week|month)s?\b/);
  if (m2) { const n = parseInt(m2[1]||"1"); return m2[2]==="month" ? n*30 : m2[2]==="week" ? n*7 : n; }
  if (/\b(tomorrow)\b/.test(t)) return 1;
  if (/\b(next week)\b/.test(t)) return 7;
  if (/\b(end of (?:the )?week)\b/.test(t)) return 5;
  if (/\b(next month)\b/.test(t)) return 30;
  if (/\b(by friday)\b/.test(t)) { const dow = new Date().getDay(); return ((5 - dow + 7) % 7) || 7; }
  if (/\b(by monday)\b/.test(t)) { const dow = new Date().getDay(); return ((1 - dow + 7) % 7) || 7; }
  if (/\b(today)\b/.test(t)) return 0;
  return null;
}
// Convert a days-from-now offset to a YYYY-MM-DD string (local-time).
function offsetToYMD(days) {
  const d = new Date(Date.now() + (days||0)*86400000);
  const yyyy = d.getFullYear(); const mm = String(d.getMonth()+1).padStart(2,"0"); const dd = String(d.getDate()).padStart(2,"0");
  return `${yyyy}-${mm}-${dd}`;
}
const todayYMD = () => offsetToYMD(0);
// Compare YYYY-MM-DD strings — returns negative if a is before b, 0 if same, positive if after.
const cmpYMD = (a,b) => a < b ? -1 : a > b ? 1 : 0;
// Heuristic mock — extract simple action verbs + due offsets from a note. Returns
// { newTasks: [{text, dueDays}], completedTaskIndices: [...] }.
function mockExtractTaskOps(noteText, existingTasks) {
  const t = String(noteText||"").trim();
  const lower = t.toLowerCase();
  const open = (existingTasks||[]).filter(x => !x.completed);
  // Detect which existing open tasks the new note describes as DONE.
  const completedTaskIndices = [];
  open.forEach((task, idx) => {
    const txt = task.text.toLowerCase();
    // Hits if the note contains a past-tense indicator about the same subject.
    const subject = txt.replace(/^(?:send|share|schedule|follow up|set up|retry|check|book)\s+/i,"").trim();
    if (/\b(sent|delivered|emailed|shared|provided|forwarded|completed|done|finished|booked|scheduled|wrapped up|finalized)\b/.test(lower) && subject && lower.includes(subject.split(" ")[0])) completedTaskIndices.push(idx);
    else if (/\b(sent|delivered|emailed|shared|provided|forwarded)\b/.test(lower) && /^send/.test(txt) && lower.includes(txt.replace(/^send\s+/,""))) completedTaskIndices.push(idx);
    else if (/\bbook(?:ed)?|\bscheduled\b/.test(lower) && /^schedule/.test(txt)) completedTaskIndices.push(idx);
    else if (/\bspoke|talked|called|connected|reached/.test(lower) && /^(retry call|follow up|call)/.test(txt)) completedTaskIndices.push(idx);
  });
  // Extract NEW tasks (skip noting nothing actionable / very short).
  const candidates = [];
  if (t.length >= 25) {
    if (/\bpromised\b|\bwill send\b|\bsend (?:over|them|him|her)\b|\bi['']ll send\b|\bsending\b/i.test(t)) candidates.push("Send promised material");
    if (/\bschedule\b|\bbook (?:a |the )?(?:call|meeting|demo)\b|\bset up (?:a |the )?call\b/i.test(t)) candidates.push("Schedule follow-up call");
    if (/\bfollow ?up\b|\bcircle back\b|\bcheck in (?:with|on)\b/i.test(t)) candidates.push("Follow up with candidate");
    if (/\bvoicemail\b|\bno answer\b|\bdidn['']t pick up\b|\bmissed (?:the )?call\b/i.test(t)) candidates.push("Retry call");
    if (/\bsend (?:the )?fdd\b|\bforward (?:the )?fdd\b/i.test(t)) candidates.push("Send FDD");
    if (/\binvest(?:ment)?\s+(?:details|breakdown|range)\b|\broi\b/i.test(t)) candidates.push("Share investment breakdown");
    if (/\bterritory\b/i.test(t) && /\b(?:check|confirm|verify|availab|review)\b/i.test(t)) candidates.push("Check territory availability");
    if (/\breference[s]?\b|\bvalidat(?:e|ion)\b/i.test(t) && /\b(?:call|connect|introduce)\b/i.test(t)) candidates.push("Set up validation call");
    if (/\bagreement\b/i.test(t) && /\bsend\b/i.test(t)) candidates.push("Send agreement");
  }
  const baseDue = parseDueDays(t);
  const newTasks = candidates.slice(0,2).map(text => ({ text, dueDays: baseDue != null ? baseDue : 3 }));
  return { newTasks, completedTaskIndices };
}
// Real AI extraction + completion-detection in one call. Returns
// { newTasks: [{text, dueDays}], completedTaskIndices: [...] }.
async function extractTaskOpsAi(noteText, recName, stage, existingTasks) {
  const open = (existingTasks||[]).filter(x => !x.completed).slice(0,8);
  const openSummary = open.map((t,i)=>`${i}: ${t.text}`).join("\n") || "(none)";
  const prompt = `New CRM note on ${recName||"a candidate"} (stage: ${stage||"-"}).\nNote: "${(noteText||"").replace(/"/g,"'").slice(0,500)}"\nExisting open tasks (by index):\n${openSummary}\n\nReturn JSON: {"newTasks":[{"text":"<3-6 word task>","dueDays":<int days from today>}],"completedTaskIndices":[<index of any existing tasks this note completes>]}\nMax 2 newTasks. Skip near-duplicates of existing tasks. If note mentions a date hint ("next week", "by Friday", "in 3 days"), reflect it in dueDays; else default 3. completedTaskIndices lists indices of existing tasks the note finished. Return JSON only.`;
  const raw = await callClaude([{role:"user", content: prompt}], "You extract action items from CRM notes and detect which existing tasks the note completes. Reply with JSON only.", 180, "claude-haiku-4-5");
  try {
    const parsed = JSON.parse(raw.replace(/```json|```/g,"").trim());
    const newTasks = Array.isArray(parsed.newTasks) ? parsed.newTasks.filter(t => t && typeof t.text === "string" && t.text.trim()).slice(0,2).map(t => ({ text: t.text.trim(), dueDays: Number.isInteger(t.dueDays) ? Math.max(0, Math.min(120, t.dueDays)) : 3 })) : [];
    const completedTaskIndices = Array.isArray(parsed.completedTaskIndices) ? parsed.completedTaskIndices.filter(i => Number.isInteger(i) && i >= 0 && i < open.length) : [];
    return { newTasks, completedTaskIndices };
  } catch { return { newTasks: [], completedTaskIndices: [] }; }
}

// ── Scheduling helpers ────────────────────────────────────────
// Format a Date as YYYYMMDDTHHMMSSZ (UTC, no separators) — Google Calendar's "dates" param.
const toGCalDateString = (d) => d.toISOString().replace(/[-:]/g,"").replace(/\.\d{3}/,"");
const buildGoogleCalUrl = ({ title, startISO, endISO, description, location }) => {
  const start = toGCalDateString(new Date(startISO));
  const end   = toGCalDateString(new Date(endISO));
  const qs = new URLSearchParams({
    action: "TEMPLATE",
    text: title || "Meeting",
    dates: `${start}/${end}`,
    details: description || "",
    location: location || "",
  });
  return `https://calendar.google.com/calendar/render?${qs.toString()}`;
};
const buildOutlookCalUrl = ({ title, startISO, endISO, description, location }) => {
  const qs = new URLSearchParams({
    path: "/calendar/action/compose",
    rru: "addevent",
    subject: title || "Meeting",
    startdt: startISO,
    enddt: endISO,
    body: description || "",
    location: location || "",
  });
  return `https://outlook.live.com/calendar/0/deeplink/compose?${qs.toString()}`;
};
// .ics builder — escape commas, semicolons, newlines per RFC 5545.
const escapeIcs = (s) => (s||"").replace(/\\/g,"\\\\").replace(/;/g,"\\;").replace(/,/g,"\\,").replace(/\n/g,"\\n");
const buildIcs = ({ uid, title, startISO, endISO, description, location }) => {
  const dt = (s) => new Date(s).toISOString().replace(/[-:]/g,"").replace(/\.\d{3}/,"");
  return [
    "BEGIN:VCALENDAR",
    "VERSION:2.0",
    `PRODID:-//${BRAND.name}//Scheduling//EN`,
    "CALSCALE:GREGORIAN",
    "METHOD:PUBLISH",
    "BEGIN:VEVENT",
    `UID:${uid}@${BRAND.nameLower}`,
    `DTSTAMP:${dt(new Date().toISOString())}`,
    `DTSTART:${dt(startISO)}`,
    `DTEND:${dt(endISO)}`,
    `SUMMARY:${escapeIcs(title)}`,
    `DESCRIPTION:${escapeIcs(description)}`,
    `LOCATION:${escapeIcs(location)}`,
    "END:VEVENT",
    "END:VCALENDAR",
  ].join("\r\n");
};
const downloadIcs = (filename, icsText) => {
  const blob = new Blob([icsText], { type: "text/calendar;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();
  URL.revokeObjectURL(url);
};
// Generate available time slots for one day, respecting weekly hours, buffers, and existing bookings.
// dayISO = "YYYY-MM-DD" (in the rep's local interpretation). Returns an array of {startISO, endISO} slots.
function generateAvailableSlots({ availability, existingBookings, durationMin, dayISO }) {
  const DAY_KEYS = ["sun","mon","tue","wed","thu","fri","sat"];
  const [Y, M, D] = dayISO.split("-").map(Number);
  const dayDate = new Date(Y, M-1, D);
  const dayKey = DAY_KEYS[dayDate.getDay()];
  const windows = (availability?.weeklyHours?.[dayKey]) || [];
  if (windows.length === 0) return [];
  // Blackout check
  if ((availability?.blackoutDates||[]).includes(dayISO)) return [];
  // Min-notice + max-advance gates
  const now = Date.now();
  const minNoticeMs = (availability?.minNoticeHours||0) * 3600_000;
  const maxAdvanceMs = (availability?.maxAdvanceDays||60) * 86400_000;
  const buffer = (availability?.defaultBufferMin||0) * 60_000;
  const slotStep = 30 * 60_000; // 30-min increments
  const dur = durationMin * 60_000;
  const out = [];
  const bookingsForDay = (existingBookings||[]).filter(b => b.status !== "cancelled" && b.startISO?.slice(0,10) === dayISO).map(b => ({ s: new Date(b.startISO).getTime(), e: new Date(b.endISO).getTime() }));
  for (const win of windows) {
    const [sh, sm] = win.start.split(":").map(Number);
    const [eh, em] = win.end.split(":").map(Number);
    const winStart = new Date(Y, M-1, D, sh, sm).getTime();
    const winEnd   = new Date(Y, M-1, D, eh, em).getTime();
    for (let t = winStart; t + dur <= winEnd; t += slotStep) {
      // Min-notice + max-advance
      if (t - now < minNoticeMs) continue;
      if (t - now > maxAdvanceMs) continue;
      // Buffer-aware conflict check against existing bookings
      const blocked = bookingsForDay.some(b => (t < b.e + buffer) && (t + dur + buffer > b.s));
      if (blocked) continue;
      out.push({ startISO: new Date(t).toISOString(), endISO: new Date(t + dur).toISOString() });
    }
  }
  return out;
}

// Last "real" contact from the candidate — most recent note tagged isTouchpoint === true.
// Fallback to createdAt for records that have no touchpoints (we've never heard from them).
const lastContactDate = (rec) => {
  const notes = rec?.notes||[];
  const touchpoints = notes.filter(n => n.isTouchpoint === true);
  return touchpoints.length ? touchpoints[touchpoints.length-1].at : (rec?.createdAt || null);
};
// Lead-only stale tiers based on lastContactDate. 7+ = stale, 14+ = urgent red flag.
// Deterministic lead-quality score 0–100. Cheap, no AI call. Reflects data completeness
// + engagement signals. Useful as a pool-health proxy when comparing lead cohorts.
const computeLeadScore = (lead) => {
  if (!lead) return 0;
  let s = 0;
  if (lead.email) s += 15;
  if (lead.phone) s += 15;
  // Net worth + liquidity replaced single "investmentLevel" — split into two signals so
  // a candidate with both gets full credit; legacy `investmentLevel` falls back to 15.
  if (lead.netWorth)  s += 12;
  if (lead.liquidity) s += 12;
  if (!lead.netWorth && !lead.liquidity && lead.investmentLevel) s += 15;
  if (lead.territory) s += 15;
  if (lead.source) s += 10;
  if (lead.location) s += 10;
  s += Math.min(15, (lead.notes||[]).length * 3);
  return Math.min(100, s);
};

const leadStaleLevel = (rec) => {
  const d = daysSince(lastContactDate(rec));
  if (d >= 14) return "urgent";
  if (d >= 7)  return "stale";
  return null;
};

// ═══════════════════════════════════════════════════════════
//  CLAUDE API
// ═══════════════════════════════════════════════════════════
const getApiHeaders = () => ({
  "Content-Type": "application/json",
  "x-api-key": localStorage.getItem("ff_api_key") || "",
  "anthropic-version": "2023-06-01",
  "anthropic-dangerous-allow-browser": "true",
});
const hasApiKey = () => !!(localStorage.getItem("ff_api_key")||"").trim();
const mockDelay = () => new Promise(r => setTimeout(r, 600 + Math.random()*500));

// Hand-tuned mock responses for the seeded tutorial pipeline. Used whenever no API key is set,
// so AI features can be tested end-to-end without a live key. Mock responses are STATIC — they
// won't reflect record edits made after seeding.
function mockClaudeResponse(messages) {
  const promptText = (messages||[]).map(m => typeof m.content === "string" ? m.content : (Array.isArray(m.content)?m.content.map(c=>c.text||"").join("\n"):"")).join("\n");
  const extract = (namePattern) => {
    const re = new RegExp(`id:(\\w+)\\s*\\|\\s*${namePattern}`, "i");
    const m = promptText.match(re);
    return m ? m[1] : null;
  };

  // Touchpoint classification — used by the note-tagging pipeline.
  if (promptText.includes("Did this CRM note describe receiving communication")) {
    const noteMatch = promptText.match(/Note: "([^"]+)"/);
    const noteText = noteMatch ? noteMatch[1] : "";
    return heuristicClassifyTouchpoint(noteText) ? "yes" : "no";
  }
  // Task extraction + completion-detection from a freshly-saved CRM note (combined call).
  if (promptText.includes("completedTaskIndices") && promptText.includes("dueDays")) {
    const noteMatch = promptText.match(/Note: "([^"]+)"/);
    const noteText = noteMatch ? noteMatch[1] : "";
    // Parse the existing open tasks block out of the prompt to reconstruct indices.
    const openBlockMatch = promptText.match(/Existing open tasks \(by index\):\n([\s\S]*?)\n\nReturn JSON/);
    const existing = [];
    if (openBlockMatch && !openBlockMatch[1].trim().startsWith("(none)")) {
      openBlockMatch[1].split("\n").forEach(line => {
        const m = line.match(/^(\d+):\s*(.+)$/);
        if (m) existing.push({ text: m[2].trim(), completed: false });
      });
    }
    return JSON.stringify(mockExtractTaskOps(noteText, existing));
  }
  // Two-axis classifier (touchpoint + call attempt)
  if (promptText.includes("Classify this CRM note on two axes")) {
    const noteMatch = promptText.match(/Note: "([^"]+)"/);
    const noteText = noteMatch ? noteMatch[1] : "";
    return JSON.stringify({
      isTouchpoint: heuristicClassifyTouchpoint(noteText),
      isCallAttempt: heuristicClassifyCall(noteText),
    });
  }

  // FDD PDF parse
  if (promptText.includes("Extract FDD financial data")) {
    return JSON.stringify({
      franchiseFee:"$45,000", royaltyRate:"6%", adFundRate:"2%",
      investmentMin:"$285,000", investmentMax:"$520,000",
      avgGrossSales:"$1,150,000", avgEbitda:"$210,000", avgCOGS:"32%", avgLaborPct:"26%", avgNetProfit:"$185,000",
      item19Notes:"Mock data — Item 19 based on 87 of 124 stores reporting 12+ months. Top quartile averaged $1.6M, bottom quartile $720k.",
    });
  }

  // AI Organize (pipeline audit) — match this BEFORE generic What's Next match
  if (promptText.includes("pipeline audit") || promptText.includes("Identify issues across the whole pipeline")) {
    const gregId = extract("Greg"), bradId = extract("Brad"), lindaId = extract("Linda"),
          tomId = extract("Tom"),   mayaId = extract("Maya"), elenaId = extract("Elena"),
          rachelId = extract("Rachel");
    return JSON.stringify({
      disqualify: [
        gregId   && { id: gregId,   reason: "Credit score 580 is well below the 680 brand minimum, and Greg has been evasive about updated financials despite multiple asks. This isn't recoverable." },
        bradId   && { id: bradId,   reason: "Ghosted after the intro call — said he'd 'circle back this week' 9 days ago. One last outreach, then drop." },
        rachelId && { id: rachelId, reason: "Two of three validation calls came back lukewarm and Rachel went quiet 5 days ago. The concerns are real — likely a soft no." },
      ].filter(Boolean),
      wrong_stage: [
        bradId && { id: bradId, suggestedStage: "qualified", reason: "Radio-silent post-intro-call; shouldn't be accruing Intro Call staleness. Move back to Qualified for re-qualification or into Nurturing." },
      ].filter(Boolean),
      missing_materials: [
        lindaId && { id: lindaId, item: "Item 19 supplemental breakdown deck", reason: "Promised 10 days ago. Linda is likely waiting on this before scheduling the FDD review call." },
        tomId   && { id: tomId,   item: "Personal Financial Statement template", reason: "Promised when his application went in 6 days ago. Sending the proper PFS format gives him the best shot at presenting his thin financials." },
        mayaId  && { id: mayaId,  item: "Territory exclusivity map and brand guidelines PDF", reason: "Maya asked for both when the agreement went out 8 days ago. Eight days of silence on a sent agreement is a yellow flag — this may be the friction." },
        elenaId && { id: elenaId, item: "Pacific NW territory investment range breakdown", reason: "Committed 2 days ago for her next call. Elena is your freshest hot lead — don't slow her down with small materials." },
      ].filter(Boolean),
      habits: [
        { title: "Promising follow-up materials and not delivering", detail: "There are at least 4 outstanding promised deliverables (Linda's Item 19 deck, Tom's PFS template, Maya's territory/brand assets, Elena's investment breakdown). Build the habit of sending during the call or capturing as a task before hanging up." },
        { title: "Letting opportunities go stale before re-engaging", detail: "Roughly half the pipeline is past its stale threshold. Block 10 minutes each morning to triage stale opps — re-engage, change stage, or close them out. Don't let them rot in place." },
        { title: "Holding onto unqualified candidates too long", detail: "Greg has clear disqualifying signals yet still occupies pipeline space. Define explicit kill criteria up front so you free capacity for better-fit candidates." },
      ],
    });
  }

  // What's Next
  if (promptText.includes("most urgent action") || promptText.includes("find the single most important action") || promptText.includes("most important action RIGHT NOW")) {
    if (promptText.includes("Maya") && promptText.includes("agreement_sent")) {
      return JSON.stringify({
        task: "Send Maya the territory map + brand guidelines, then nudge for signature",
        person: "Maya Chen (Tutorial)",
        reason: "Maya's agreement has been out 8 days. She specifically asked for the territory map and brand guidelines a week ago and hasn't received them — that may be why she's stalling. Knock both out today and close the loop.",
        action_type: "send_fdd",
        urgency: "high",
      });
    }
    return JSON.stringify({
      task: "Disqualify Greg Foster — credit score below threshold",
      person: "Greg Foster (Tutorial)",
      reason: "Credit score of 580 is well below your 680 minimum, and Greg has been evasive about updated financials despite multiple asks. Stop spending time here and free up the pipeline.",
      action_type: "disqualify",
      urgency: "high",
    });
  }

  // AI Score (per-opp)
  if (promptText.includes("Rate this franchise candidate")) {
    // Simplified scoring: just total + summary + flag
    const nameMatch = promptText.match(/Rate this franchise candidate[^\n]*\n\n(\S+)\s+(\S+(?:\s+\(\w+\))?)/);
    const stageMatch = promptText.match(/\|\s*Stage\s*\S+\s*\((\d+)d\)/);
    const name = nameMatch ? `${nameMatch[1]} ${nameMatch[2]}` : "Candidate";
    const days = stageMatch ? +stageMatch[1] : 5;
    const isStale = /STALE|⚠/i.test(promptText) || days >= 10;
    const hasCreditIssue = /credit score|evasive|disqualify/i.test(promptText) && /Greg|580/i.test(promptText);
    const isHot = /scheduled FDD review|Discovery Day|signed within 48/i.test(promptText) && !isStale;
    const h = [...name].reduce((a,c)=>a+c.charCodeAt(0),0);
    const total = hasCreditIssue ? 18+(h%8) : isHot ? 80+(h%15) : isStale ? 38+(h%15) : 60+(h%20);
    return JSON.stringify({
      total,
      summary: hasCreditIssue ? `${name} shows red flags — consider disqualifying.` : isStale ? `${name} has stalled; re-engage immediately.` : isHot ? `${name} is moving fast — protect the momentum.` : `${name} is progressing on cadence.`,
      flag: hasCreditIssue ? "Credit profile below brand minimum" : isStale ? `${days}-day stage staleness` : null,
    });
  }

  // AI Summary (per-candidate review)
  if (promptText.includes("Franchise candidate review for") || /^Review\s+\S+\s+\S/m.test(promptText)) {
    const nameMatch = promptText.match(/Franchise candidate review for (.+?)\./) || promptText.match(/^Review\s+(.+?)\./m);
    const name = nameMatch ? nameMatch[1] : "this candidate";
    return `## Executive Summary
${name} is progressing through the franchise development pipeline. This is a **mock summary** generated without a live API key — for real, candidate-specific analysis, add your Anthropic API key in the banner at the top of the app.

## Key Highlights
- Currently engaged in the franchise development process
- Profile, territory, and investment range are documented
- Recent notes reflect both progress and open commitments

## ⚠️ Missing / Incomplete Items
- Confirm all promised follow-up materials have been delivered
- Verify financial documentation is complete and current
- Loop in the assigned broker (if any) on next steps

## 🎯 Next Steps
1. Re-read the most recent 2–3 notes for any open promises
2. Schedule a touchpoint within 3 business days
3. Confirm the requirements to advance to the next stage are clear

## 🚩 Risk Flags
None surfaced by the mock summarizer. Real AI analysis will surface stage-specific risks tailored to ${name}'s notes once your API key is added.`;
  }

  // Fallback
  return JSON.stringify({ error: "Mock AI mode: unrecognized prompt pattern. Add your Anthropic API key for real responses." });
}

async function callClaude(messages, system="", maxTokens=250, model="claude-haiku-4-5") {
  if (!hasApiKey()) { await mockDelay(); return mockClaudeResponse(messages); }
  const body = { model, max_tokens:maxTokens, messages };
  if (system) body.system = system;
  const r = await fetch("https://api.anthropic.com/v1/messages",{method:"POST",headers:getApiHeaders(),body:JSON.stringify(body)});
  const d = await r.json();
  return d.content?.map(c=>c.text||"").join("")||"";
}
async function callClaudeWithPDF(base64, prompt, maxTokens=900, model="claude-haiku-4-5") {
  if (!hasApiKey()) { await mockDelay(); return mockClaudeResponse([{role:"user",content:prompt}]); }
  const r = await fetch("https://api.anthropic.com/v1/messages",{method:"POST",headers:getApiHeaders(),
    body:JSON.stringify({model, max_tokens:maxTokens, messages:[{role:"user",content:[
      {type:"document",source:{type:"base64",media_type:"application/pdf",data:base64}},{type:"text",text:prompt}
    ]}]})});
  const d = await r.json();
  return d.content?.map(c=>c.text||"").join("")||"";
}

// ═══════════════════════════════════════════════════════════
//  DESIGN TOKENS
// ═══════════════════════════════════════════════════════════
// Palette — `dim` was previously #233045 (only ~10% brighter than the panel bg) which made
// field labels and helper text nearly invisible. Bumped to a readable mid-grey-blue that
// still reads as secondary against the panel without hurting legibility.
const C = { bg:"#070c14", panel:"#0c1422", border:"#152030", accent:"#3b82f6", text:"#dde4f0", muted:"#a8b6cd", dim:"#7889a8" };

// Brand logo — simple latte art rosetta inside a coffee cup, viewed from above.
// (The chai/latte motif is FranChai-flavored; revisit visual treatment if the
// active brand in branding.js ever flips to "frantio" permanently.)
// `size` is the bounding box; `tone` controls the latte cream color.
function BrandLogo({ size = 32, tone = "#e8d5b7", cupColor = "#8b6f47" }) {
  return (
    <svg width={size} height={size} viewBox="0 0 64 64" style={{flexShrink:0}} aria-label={`${BRAND.name} logo`}>
      {/* cup rim */}
      <circle cx="32" cy="32" r="28" fill="#3a2817" stroke={cupColor} strokeWidth="2"/>
      {/* coffee surface */}
      <circle cx="32" cy="32" r="25" fill="#4a3424"/>
      {/* latte art rosetta — a vertical stem with paired leaf curves */}
      <g fill={tone}>
        {/* central stem */}
        <path d="M32 11 Q33 32 32 53 Q31 32 32 11 Z"/>
        {/* leaf pairs, top to bottom — each is a mirrored teardrop */}
        <path d="M32 18 Q22 22 24 27 Q29 25 32 22 Z"/>
        <path d="M32 18 Q42 22 40 27 Q35 25 32 22 Z"/>
        <path d="M32 26 Q19 30 22 36 Q28 34 32 30 Z"/>
        <path d="M32 26 Q45 30 42 36 Q36 34 32 30 Z"/>
        <path d="M32 35 Q21 39 24 44 Q29 42 32 38 Z"/>
        <path d="M32 35 Q43 39 40 44 Q35 42 32 38 Z"/>
        <path d="M32 43 Q25 46 27 50 Q30 49 32 46 Z"/>
        <path d="M32 43 Q39 46 37 50 Q34 49 32 46 Z"/>
        {/* heart tip at bottom */}
        <path d="M32 49 Q29 52 32 55 Q35 52 32 49 Z"/>
      </g>
    </svg>
  );
}
const inp = (ex={}) => ({ background:"#090f1c", border:`1px solid ${C.border}`, borderRadius:8, padding:"9px 13px", color:C.text, fontSize:13, outline:"none", fontFamily:"inherit", ...ex });
// When `bg` is C.dim we map it to a darker panel-tinted color so the foreground icons/text
// pop instead of competing with a light-grey button background (poor contrast on a dark UI).
const btn = (bg,color,glow=false) => ({ background: bg===C.dim ? "#0f1a2e" : bg, border:`1.5px solid ${color}44`, borderRadius:9, padding:"8px 16px", color, cursor:"pointer", fontSize:12, fontWeight:700, fontFamily:"inherit", transition:"all .15s", ...(glow?{boxShadow:`0 0 14px ${color}33`}:{}) });

// ═══════════════════════════════════════════════════════════
//  TOOLTIP LAYER — hijacks `title=""` for a consistent, fast custom tooltip.
//  Native `title` tooltips are slow (~1.5s delay), positioned inconsistently,
//  and sometimes don't appear at all on React-rendered elements. This layer
//  reads the title attr on hover, suppresses the native tooltip by stashing
//  the value in `data-tip`, and renders our own fixed-position pill.
// ═══════════════════════════════════════════════════════════
function TooltipLayer() {
  const [tip, setTip] = useState(null); // { text, rect }
  const tipRef = useRef(null);
  const [pos, setPos] = useState(null); // { left, top } final, clamped to viewport
  useEffect(() => {
    let hoverEl = null;
    let showTimer = null;
    const hide = () => {
      if (hoverEl) {
        const stash = hoverEl.getAttribute("data-tip");
        if (stash != null && !hoverEl.getAttribute("title")) {
          hoverEl.setAttribute("title", stash);
          hoverEl.removeAttribute("data-tip");
        }
      }
      hoverEl = null;
      clearTimeout(showTimer);
      setTip(null);
      setPos(null);
    };
    const onOver = (e) => {
      let el = e.target;
      while (el && el !== document.body && !(el.getAttribute && el.getAttribute("title"))) el = el.parentElement;
      if (!el || el === document.body) return;
      if (el === hoverEl) return;
      if (hoverEl) hide();
      hoverEl = el;
      const text = el.getAttribute("title");
      if (!text) return;
      el.setAttribute("data-tip", text);
      el.removeAttribute("title");
      clearTimeout(showTimer);
      showTimer = setTimeout(() => {
        if (!hoverEl) return;
        const r = hoverEl.getBoundingClientRect();
        setTip({ text, rect: { left: r.left, top: r.top, right: r.right, bottom: r.bottom, width: r.width, height: r.height } });
        setPos(null);
      }, 350);
    };
    const onOut = (e) => {
      if (!hoverEl) return;
      const to = e.relatedTarget;
      if (to && hoverEl.contains(to)) return;
      hide();
    };
    document.addEventListener("mouseover", onOver, true);
    document.addEventListener("mouseout", onOut, true);
    window.addEventListener("scroll", hide, true);
    window.addEventListener("blur", hide);
    return () => {
      document.removeEventListener("mouseover", onOver, true);
      document.removeEventListener("mouseout", onOut, true);
      window.removeEventListener("scroll", hide, true);
      window.removeEventListener("blur", hide);
      clearTimeout(showTimer);
    };
  }, []);
  // After tooltip element renders, measure and clamp position to viewport
  useEffect(() => {
    if (!tip || !tipRef.current) return;
    const tipR = tipRef.current.getBoundingClientRect();
    const { rect } = tip;
    const vw = window.innerWidth, vh = window.innerHeight;
    const pad = 8, gap = 8;
    // Try above the target, centered horizontally
    let left = rect.left + rect.width/2 - tipR.width/2;
    let top  = rect.top - tipR.height - gap;
    // If clipped at top, flip to below
    if (top < pad) top = rect.bottom + gap;
    // Clamp horizontally within viewport
    if (left < pad) left = pad;
    if (left + tipR.width > vw - pad) left = vw - pad - tipR.width;
    // Clamp vertically (rare; if both above and below clip, prefer in-bounds)
    if (top + tipR.height > vh - pad) top = Math.max(pad, vh - pad - tipR.height);
    setPos({ left, top });
  }, [tip]);
  if (!tip) return null;
  return (
    <div ref={tipRef} style={{position:"fixed", left:pos?.left ?? -9999, top:pos?.top ?? -9999, visibility: pos ? "visible" : "hidden", background:"#0c1422", color:"#f0f6ff", border:"1px solid #2a3a55", borderRadius:6, padding:"5px 10px", fontSize:11, fontWeight:600, pointerEvents:"none", zIndex:99999, whiteSpace:"nowrap", boxShadow:"0 4px 14px #000a", fontFamily:"'Sora',system-ui,sans-serif", letterSpacing:".01em", maxWidth:"calc(100vw - 16px)"}}>{tip.text}</div>
  );
}

// ═══════════════════════════════════════════════════════════
//  SMALL COMPONENTS
// ═══════════════════════════════════════════════════════════
function Ava({ name="?", size=34 }) {
  const i = name.trim().split(" ").map(w=>w[0]||"").join("").slice(0,2).toUpperCase()||"?";
  const h = [...name].reduce((a,c)=>a+c.charCodeAt(0),0)%360;
  return <div style={{width:size,height:size,borderRadius:"50%",background:`hsl(${h},40%,28%)`,color:"#fff",display:"flex",alignItems:"center",justifyContent:"center",fontSize:size*.36,fontWeight:800,flexShrink:0,border:`2px solid ${C.dim}`}}>{i}</div>;
}
function SBadge({ stageId, stages=[] }) {
  const s = stages.find(x=>x.id===stageId)||{color:"#64748b",icon:"◎",label:stageId};
  return <span style={{background:s.color+"22",color:s.color,border:`1px solid ${s.color}44`,borderRadius:6,padding:"2px 10px",fontSize:11,fontWeight:700,whiteSpace:"nowrap"}}>{s.label}</span>;
}
function Sec({ children, style={} }) {
  return <div style={{fontSize:10,fontWeight:800,color:C.muted,letterSpacing:".08em",textTransform:"uppercase",marginBottom:10,...style}}>{children}</div>;
}
function ModalShell({ title, onClose, children, maxW=520 }) {
  return (
    <div style={{position:"fixed",inset:0,background:"#000d",zIndex:300,display:"flex",alignItems:"center",justifyContent:"center",padding:16}}>
      <div style={{background:C.panel,border:`1px solid ${C.border}`,borderRadius:18,padding:"24px",width:"100%",maxWidth:maxW,maxHeight:"92vh",overflowY:"auto"}}>
        <div style={{display:"flex",justifyContent:"space-between",alignItems:"center",marginBottom:18}}>
          <h2 style={{margin:0,color:"#f0f6ff",fontSize:17,fontWeight:900}}>{title}</h2>
          <button onClick={onClose} title="Close" style={btn(C.dim,C.muted)}>✕</button>
        </div>
        {children}
      </div>
    </div>
  );
}
function NoteInput({ onSave }) {
  const [text, setText] = useState("");
  const submit = () => { const v = text.trim(); if (!v) return; onSave(v); setText(""); };
  return (
    <div style={{display:"flex",gap:8,marginBottom:11}}>
      <textarea
        value={text}
        onChange={e=>setText(e.target.value)}
        onKeyDown={e=>{ if(e.key==="Enter" && (e.metaKey||e.ctrlKey)) { e.preventDefault(); submit(); }}}
        placeholder="Add a note… (⌘/Ctrl+Enter to save)"
        rows={2}
        style={{...inp({flex:1}),resize:"vertical",fontFamily:"inherit"}}
      />
      <button onClick={submit} style={{...btn("#091c09","#4ade80"),alignSelf:"flex-end"}}>Save</button>
    </div>
  );
}
function ScoreRing({ score, size=44 }) {
  const pct=Math.max(0,Math.min(100,score||0));
  const color=pct>=75?"#4ade80":pct>=50?"#facc15":pct>=30?"#fb923c":"#f87171";
  const r=14,circ=2*Math.PI*r,dash=circ*(pct/100);
  return (
    <div style={{position:"relative",width:size,height:size,flexShrink:0}}>
      <svg width={size} height={size} style={{transform:"rotate(-90deg)"}}>
        <circle cx={size/2} cy={size/2} r={r} fill="none" stroke={C.dim} strokeWidth={3}/>
        <circle cx={size/2} cy={size/2} r={r} fill="none" stroke={color} strokeWidth={3} strokeDasharray={`${dash} ${circ-dash}`} strokeLinecap="round"/>
      </svg>
      <div style={{position:"absolute",inset:0,display:"flex",alignItems:"center",justifyContent:"center",fontSize:size*.26,fontWeight:900,color}}>{pct}</div>
    </div>
  );
}


// ═══════════════════════════════════════════════════════════
//  STABLE EDITORS (template / automation / broker / network / compose)
//  Defined at module level so React doesn't unmount-remount on parent re-renders.
// ═══════════════════════════════════════════════════════════
// ---- Block defaults — used when adding a new block to a template.
const BLOCK_DEFAULTS = {
  banner:  { type:"banner",  title:"Big Announcement", subtitle:"Subhead supporting your headline", padding:20 },
  header:  { type:"header",  text:"Your Headline Here", fontSize:28, align:"left", padding:16 },
  para:    { type:"para",    text:"Your paragraph copy here. Use this space to tell your story.", fontSize:15, align:"left", padding:16 },
  btn:     { type:"btn",     text:"Call to Action", url:"https://", align:"center", fontSize:15, padding:24 },
  divider: { type:"divider", padding:28, style:"line" },
  spacer:  { type:"spacer",  height:24 },
  logo:    { type:"logo",    src:"", alt:"Logo", width:96, align:"center", padding:16 },
  footer:  { type:"footer",  companyName:"{COMPANY_NAME|Your Company}", companyAddress:"{COMPANY_ADDRESS|123 Main St, City, ST 00000}", showUnsubscribe:true, color:"#767676", fontSize:12, align:"center" },
  image:   { type:"image",   src:"", alt:"", padding:16, width:"fit", align:"center" },
  // Image Banner — same content as an image but rendered in locked "bleed" mode (edge-to-edge).
  // Exists as a separate library entry so users who just want to drop in their own pre-designed
  // banner image don't have to know about the bleed size option.
  imageBanner: { type:"imageBanner", src:"", alt:"", padding:16 },
  // A row is a horizontal container with 2 or 3 columns; each column holds its own list of
  // child blocks (header/para/image/etc.). Rendered as a CSS table for email-client compat.
  row:     { type:"row",     cols:2, children:[[],[]], padding:16, gap:12 },
};
const uid8 = () => Math.random().toString(36).slice(2,10);
const newBlock = (type, opts) => {
  const base = { ...(BLOCK_DEFAULTS[type] || {type}), id: uid8() };
  if (type === "row") {
    const cols = (opts && opts.cols) || base.cols || 2;
    return { ...base, cols, children: Array.from({length:cols}, () => []) };
  }
  return base;
};

// Recursive helpers for the nested (row+columns) block tree.
function findBlockById(blocks, id) {
  for (const b of blocks) {
    if (b.id === id) return b;
    if (b.type === "row") {
      for (const col of (b.children||[])) {
        for (const child of col) { if (child.id === id) return child; }
      }
    }
  }
  return null;
}
function updateBlockTreeById(blocks, id, patch) {
  return blocks.map(b => {
    if (b.id === id) return { ...b, ...patch };
    if (b.type === "row") {
      return { ...b, children: (b.children||[]).map(col => col.map(c => c.id === id ? { ...c, ...patch } : c)) };
    }
    return b;
  });
}
function removeBlockTreeById(blocks, id) {
  const out = [];
  for (const b of blocks) {
    if (b.id === id) continue;
    if (b.type === "row") {
      out.push({ ...b, children: (b.children||[]).map(col => col.filter(c => c.id !== id)) });
    } else {
      out.push(b);
    }
  }
  return out;
}
// Deep-clone a block and assign fresh ids to it + every nested child. Used when duplicating.
function cloneBlockWithFreshIds(b) {
  const next = { ...b, id: Math.random().toString(36).slice(2,10) };
  if (b.type === "row" && Array.isArray(b.children)) {
    next.children = b.children.map(col => (col||[]).map(c => cloneBlockWithFreshIds(c)));
  }
  return next;
}
// Insert a duplicate of the block immediately after the original — works at top level OR
// inside a row column. Returns the new block's id so the caller can re-select it.
function duplicateBlockInTree(blocks, id) {
  const top = blocks.findIndex(b => b.id === id);
  if (top >= 0) {
    const dup = cloneBlockWithFreshIds(blocks[top]);
    const next = [...blocks]; next.splice(top + 1, 0, dup);
    return { next, newId: dup.id };
  }
  let newId = null;
  const next = blocks.map(b => {
    if (b.type !== "row" || !Array.isArray(b.children)) return b;
    const children = b.children.map(col => {
      const i = (col||[]).findIndex(c => c.id === id);
      if (i < 0) return col;
      const dup = cloneBlockWithFreshIds(col[i]);
      newId = dup.id;
      const n = [...col]; n.splice(i + 1, 0, dup);
      return n;
    });
    return { ...b, children };
  });
  return { next, newId };
}

function TemplateEditor({ initial, brand, onSave, onCancel, onDelete, folders }) {
  // Normalize initial to a block-tree if not already. Legacy templates with a body string and no
  // blocks become a single "html" block so they round-trip without losing content.
  const seed = initial || { name:"", category:"marketing", mode:"rich", subject:"", body:"", color:"#3b82f6", blocks:[], versions:[] };
  if (!seed.blocks || !seed.blocks.length) {
    seed.blocks = seed.body && /<\w/.test(seed.body) ? [{ id:Math.random().toString(36).slice(2,10), type:"html", html: seed.body }] : [];
  }
  const [form, setForm] = useState({ ...seed, versions: seed.versions || [] });
  const [device, setDevice] = useState("desktop"); // desktop | mobile (preview modal)
  const [hoveredDraftId, setHoveredDraftId] = useState(null); // {draftId, rect:{top,left,right,width}} | null
  const [showPreview, setShowPreview] = useState(false);
  const [editorTab, setEditorTab] = useState("design"); // design | settings | history
  const [selectedBlockId, setSelectedBlockId] = useState(null);
  // Click anywhere outside a block deselects the current block — same as the X minimize
  // button. We listen on mousedown so it fires before any per-block onClick (which
  // stopPropagation's). We skip clicks inside the canvas (which already manages selection)
  // and inside data-no-drag (form controls, popovers) so interacting with the property
  // form doesn't deselect itself.
  useEffect(() => {
    if (!selectedBlockId) return;
    const onDocDown = (e) => {
      const t = e.target;
      if (!(t && t.closest)) return;
      if (t.closest("[data-block-form-for]")) return;     // inside the open property form
      if (t.closest("[data-block-card]")) return;          // clicking another block selects it
      if (t.closest("[data-no-drag]")) return;             // form controls / popovers
      if (t.closest("[data-template-canvas]")) return;     // canvas already manages its own deselect
      setSelectedBlockId(null);
    };
    document.addEventListener("mousedown", onDocDown, true);
    return () => document.removeEventListener("mousedown", onDocDown, true);
  }, [selectedBlockId]);
  // When the user selects a block, scroll its property form into view. Tall bleed banners
  // can push the form off the bottom of the canvas — this brings the controls back to where
  // the user can see them without manual scrolling.
  useEffect(() => {
    if (!selectedBlockId) return;
    const t = setTimeout(() => {
      const el = document.querySelector(`[data-block-form-for="${selectedBlockId}"]`);
      if (el && typeof el.scrollIntoView === "function") {
        el.scrollIntoView({ behavior: "smooth", block: "nearest" });
      }
    }, 60);
    return () => clearTimeout(t);
  }, [selectedBlockId]);
  const [showMergePicker, setShowMergePicker] = useState(false);
  const [dragging, setDragging] = useState(null);
  const [dragOverId, setDragOverId] = useState(null);
  // Track which block the cursor is hovering, so the drag handle only appears for that block
  // (Notion/Apple pattern). Avoids both clutter and the previous left:-34 clipping problem.
  const [hoveredBlockId, setHoveredBlockId] = useState(null);
  // Undo/redo: linear history stack of form snapshots. Push on every change (debounced 700ms
  // so a burst of keystrokes coalesces into one history entry). Cmd/Ctrl+Z undoes, Cmd+Shift+Z
  // or Cmd+Y redoes. Cap stack at 60 entries.
  const historyRef = useRef([]);
  const historyIndexRef = useRef(-1);
  const isRestoringRef = useRef(false);
  const [historyTick, setHistoryTick] = useState(0); // re-render trigger only
  // Seed history with the initial form on mount so the first user edit creates a real undo step.
  useEffect(() => {
    if (historyRef.current.length === 0) {
      historyRef.current = [JSON.parse(JSON.stringify(initial || { name:"", subject:"", body:"", blocks:[], mode:"rich", color:"#3b82f6", category:"marketing" }))];
      historyIndexRef.current = 0;
      setHistoryTick(t => t + 1);
    }
  }, []); // eslint-disable-line
  // Pre-create a 1×1 transparent GIF to replace the browser's default drag ghost — the chunky
  // duplicated screenshot. Cleaner UX: source block dims in place, drop indicator line shows
  // where the block will land, no floating ghost.
  const ghostImgRef = useRef(null);
  useEffect(() => {
    const img = new Image();
    img.src = "data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7";
    ghostImgRef.current = img;
  }, []);
  const set = (k,v) => setForm(f => ({...f, [k]: v}));
  const setBlocks = (updater) => setForm(f => ({...f, blocks: typeof updater === "function" ? updater(f.blocks||[]) : updater}));
  // Recursive updaters so we can edit/delete nested blocks inside row columns.
  const updateBlock = (id, patch) => setBlocks(bs => updateBlockTreeById(bs, id, patch));
  const removeBlock = (id) => { setBlocks(bs => removeBlockTreeById(bs, id)); if (selectedBlockId === id) setSelectedBlockId(null); };
  // Duplicate a block (top-level OR nested in a column) with all properties + a new id. Selects
  // the new copy so the user sees the action's result.
  const duplicateBlock = (id) => {
    setBlocks(bs => {
      const { next, newId } = duplicateBlockInTree(bs, id);
      if (newId) setTimeout(() => setSelectedBlockId(newId), 0);
      return next;
    });
  };
  // Move within the same level only. Looks up where the block lives (top-level or a row column)
  // and swaps with its sibling at index +/- 1. No cross-column moves in v1.
  const moveBlock = (id, dir) => setBlocks(bs => {
    const i = bs.findIndex(b => b.id === id);
    if (i >= 0) {
      const j = i + dir; if (j < 0 || j >= bs.length) return bs;
      const next = [...bs]; [next[i], next[j]] = [next[j], next[i]]; return next;
    }
    return bs.map(b => {
      if (b.type !== "row") return b;
      const children = (b.children||[]).map(col => {
        const idx = col.findIndex(c => c.id === id);
        if (idx < 0) return col;
        const j = idx + dir; if (j < 0 || j >= col.length) return col;
        const next = [...col]; [next[idx], next[j]] = [next[j], next[idx]]; return next;
      });
      return { ...b, children };
    });
  });
  const insertBlock = (type, opts) => {
    const b = newBlock(type, opts);
    setBlocks(bs => [...(bs||[]), b]);
    setSelectedBlockId(b.id);
  };
  // Add a child block into a specific column. Each column holds at most ONE block; inserting
  // when the column already has content prompts to confirm replacement.
  const insertIntoColumn = (rowId, colIndex, type) => {
    const b = newBlock(type);
    setBlocks(bs => bs.map(r => {
      if (r.id !== rowId || r.type !== "row") return r;
      const children = (r.children||[]).map((col, i) => {
        if (i !== colIndex) return col;
        if (col.length > 0 && !window.confirm("This column already has a block. Replace it?")) return col;
        return [b];
      });
      return { ...r, children };
    }));
    setSelectedBlockId(b.id);
  };
  // Change number of columns on a row (2 ↔ 3). Preserves children, dropping extras when
  // shrinking (with a confirm prompt) and appending an empty column when growing.
  const changeRowCols = (rowId, newCols) => {
    setBlocks(bs => bs.map(r => {
      if (r.id !== rowId || r.type !== "row") return r;
      const current = r.children || [];
      if (newCols < current.length) {
        const dropped = current.slice(newCols).flat();
        if (dropped.length && !window.confirm(`Reduce to ${newCols} columns? This will delete ${dropped.length} block${dropped.length===1?"":"s"} in removed columns.`)) return r;
        return { ...r, cols: newCols, children: current.slice(0, newCols), colSpans: Array.from({length:newCols}, () => 1) };
      }
      const padded = [...current];
      while (padded.length < newCols) padded.push([]);
      return { ...r, cols: newCols, children: padded, colSpans: Array.from({length:newCols}, () => 1) };
    }));
  };
  // Merge two adjacent cells in a row (cellIndex and cellIndex+1) into a single wider cell.
  // Combined cell's span sums the two. Single-block-per-column limit honored — if both have a
  // block, prompt to keep one.
  const mergeRowCells = (rowId, cellIndex) => {
    setBlocks(bs => bs.map(r => {
      if (r.id !== rowId || r.type !== "row") return r;
      const children = r.children || [];
      const spans = (r.colSpans && r.colSpans.length === children.length) ? r.colSpans : children.map(() => 1);
      if (cellIndex < 0 || cellIndex >= children.length - 1) return r;
      // Hard cap: a merged cell can span at most 2 of 3 slots — we don't allow collapsing
      // the entire row into a single column.
      if ((spans[cellIndex] + spans[cellIndex+1]) > 2) return r;
      const a = children[cellIndex] || [];
      const b = children[cellIndex+1] || [];
      let merged = [...a, ...b];
      if (merged.length > 1) {
        if (!window.confirm("Both cells contain a block. Only the first will be kept after merging. Continue?")) return r;
        merged = merged.slice(0, 1);
      }
      const newChildren = [
        ...children.slice(0, cellIndex),
        merged,
        ...children.slice(cellIndex + 2),
      ];
      const newSpans = [
        ...spans.slice(0, cellIndex),
        spans[cellIndex] + spans[cellIndex+1],
        ...spans.slice(cellIndex + 2),
      ];
      return { ...r, children: newChildren, colSpans: newSpans };
    }));
  };
  // Split a previously-merged cell back into its original slot count. Original block (if any)
  // stays in the first sub-cell.
  const splitRowCell = (rowId, cellIndex) => {
    setBlocks(bs => bs.map(r => {
      if (r.id !== rowId || r.type !== "row") return r;
      const children = r.children || [];
      const spans = (r.colSpans && r.colSpans.length === children.length) ? r.colSpans : children.map(() => 1);
      const span = spans[cellIndex] || 1;
      if (span <= 1) return r;
      const newChildren = [
        ...children.slice(0, cellIndex),
        children[cellIndex] || [],
        ...Array.from({length: span - 1}, () => []),
        ...children.slice(cellIndex + 1),
      ];
      const newSpans = [
        ...spans.slice(0, cellIndex),
        ...Array.from({length: span}, () => 1),
        ...spans.slice(cellIndex + 1),
      ];
      return { ...r, children: newChildren, colSpans: newSpans };
    }));
  };
  const sampleCtx = templateContext(
    { firstName:"Sarah", lastName:"Lee", email:"sarah@example.com", phone:"555-0142", company:"Acme Holdings", stage:"Qualified", territory:"Austin TX", investmentLevel:"$500k–$1M", source:"Referral", assignedTo:"You", createdAt:new Date(Date.now()-14*86400000).toISOString(),
      partners:[{firstName:"Joe"},{firstName:"Levy"}] },
    brand,
    { repName:"You", companyEmail:"you@example.com", companyPhone:"555-0100", repTitle:"Franchise Development", companyName:brand?.name }
  );
  const fill = s => fillMergeTags(s, sampleCtx);
  // 10 most-recently used merge tags. Persisted globally so it carries across editor sessions.
  const [recentTags, setRecentTags] = useState(() => {
    try { return JSON.parse(localStorage.getItem("ff_merge_tag_recents") || "[]").slice(0, 10); } catch { return []; }
  });
  const bumpRecent = (key) => {
    setRecentTags(prev => {
      const next = [key, ...prev.filter(k => k !== key)].slice(0, 10);
      try { localStorage.setItem("ff_merge_tag_recents", JSON.stringify(next)); } catch {}
      return next;
    });
  };
  const insertVar = (key) => {
    // Insert merge tag into the currently focused field. If a block is selected, into its main text.
    const tag = `{${key}}`;
    bumpRecent(key);
    if (!selectedBlockId) {
      // Default: append to subject (most common use)
      set("subject", (form.subject || "") + tag);
      return;
    }
    const b = form.blocks.find(x => x.id === selectedBlockId);
    if (!b) return;
    if (b.type === "header" || b.type === "para" || b.type === "btn") updateBlock(b.id, { text: (b.text||"") + tag });
    else if (b.type === "banner") updateBlock(b.id, { title: (b.title||"") + tag });
  };
  const previewHtml = (device) => {
    if (form.mode === "html") return fill(form.body || "");
    // Device-aware preview: desktop renders the fixed-wallpaper + card pair; mobile drops
    // the wallpaper entirely (matches what recipients see on phones) and tightens padding.
    // Container queries / @media on viewport-width don't fire inside the preview modal
    // because the host page is wide, so we toggle the wrap directly off `device`.
    const inner = fill(blocksToHtml(form.blocks || [], form.color));
    const cardCss = contentCardCss(form);
    const bgCss = canvasBgCss(form);
    const cardBg = cardCss || (bgCss ? "background:#ffffff;" : "");
    const fontLinks = collectFontLinks(form.blocks || []);
    const mobileStyles = collectMobileFontStyles(form.blocks || []);
    if (device === "mobile") {
      const css = `<style>h1,h2{overflow-wrap:break-word;word-wrap:break-word;}</style>`;
      return fontLinks + mobileStyles + css + `<div class="email-card" style="${cardBg}width:100%;margin:0 auto;overflow:hidden">${inner}</div>`;
    }
    const css = `<style>
      .email-wallpaper{background-attachment:fixed;min-height:100%;}
      h1,h2{overflow-wrap:break-word;word-wrap:break-word;}
    </style>`;
    const card = `<div class="email-card" style="${cardBg}max-width:600px;width:100%;margin:0 auto;position:relative;z-index:1;overflow:hidden">${inner}</div>`;
    return fontLinks + mobileStyles + css + (bgCss ? `<div class="email-wallpaper" style="${bgCss};padding:0;min-height:100%">${card}</div>` : card);
  };
  const valid = form.name.trim();
  // ---- Drafts: global list of 10 most-recent in-progress templates that haven't been saved.
  // Stored at `ff_template_drafts` so it survives across editor sessions and templates. Each
  // editor session gets a stable `draftId` so autosave overwrites the same slot.
  const draftIdRef = useRef(initial?.draftId || initial?.id || Math.random().toString(36).slice(2,10));
  const [drafts, setDrafts] = useState(() => { try { return JSON.parse(localStorage.getItem("ff_template_drafts") || "[]"); } catch { return []; } });
  const writeDrafts = (list) => { try { localStorage.setItem("ff_template_drafts", JSON.stringify(list)); } catch {} setDrafts(list); };
  useEffect(() => {
    // Push to undo history (debounced) on every form change — but skip when restoring from
    // history (otherwise undo/redo would themselves create new history entries).
    if (isRestoringRef.current) { isRestoringRef.current = false; return; }
    const t = setTimeout(() => {
      const cur = historyRef.current;
      const idx = historyIndexRef.current;
      const last = cur[idx];
      const snap = JSON.stringify(form);
      if (last && JSON.stringify(last) === snap) return;
      const trimmed = cur.slice(0, idx + 1);
      const next = [...trimmed, JSON.parse(snap)].slice(-60);
      historyRef.current = next;
      historyIndexRef.current = next.length - 1;
      setHistoryTick(t => t + 1);
    }, 700);
    return () => clearTimeout(t);
  }, [form]); // eslint-disable-line
  const [draftSavedFlash, setDraftSavedFlash] = useState(false);
  const draftFlashTimeoutRef = useRef(null);
  useEffect(() => {
    if (!form.name?.trim() && !(form.blocks||[]).length && !form.body?.trim() && !form.subject?.trim()) return;
    const t = setTimeout(() => {
      const snap = { ...form, draftId: draftIdRef.current, at: new Date().toISOString(), label: form.name?.trim() || "(Untitled draft)" };
      const list = [snap, ...drafts.filter(d => d.draftId !== draftIdRef.current)].slice(0, 10);
      writeDrafts(list);
      // Flash the "Draft saved" badge for ~2.2s; setTimeout re-renders to clear.
      setDraftSavedFlash(true);
      if (draftFlashTimeoutRef.current) clearTimeout(draftFlashTimeoutRef.current);
      draftFlashTimeoutRef.current = setTimeout(() => setDraftSavedFlash(false), 2200);
    }, 1200);
    return () => clearTimeout(t);
  }, [form.name, form.subject, form.body, form.blocks, form.mode, form.color, form.category, form.canvasBg, form.canvasBgGradient, form.bgPatternType, form.bgPatternEmoji, form.bgPatternImage, form.bgPatternWord, form.bgPatternSize, form.bgPatternOpacity, form.contentBg, form.contentBgOpacity]); // eslint-disable-line
  // Undo / redo handlers + keyboard shortcut listener.
  const canUndo = historyIndexRef.current > 0;
  const canRedo = historyIndexRef.current >= 0 && historyIndexRef.current < historyRef.current.length - 1;
  const doUndo = () => {
    if (historyIndexRef.current <= 0) return;
    isRestoringRef.current = true;
    historyIndexRef.current -= 1;
    setForm(historyRef.current[historyIndexRef.current]);
    setHistoryTick(t => t + 1);
  };
  const doRedo = () => {
    if (historyIndexRef.current >= historyRef.current.length - 1) return;
    isRestoringRef.current = true;
    historyIndexRef.current += 1;
    setForm(historyRef.current[historyIndexRef.current]);
    setHistoryTick(t => t + 1);
  };
  useEffect(() => {
    const onKey = (e) => {
      // Skip shortcut when typing inside an input/textarea so native undo still works there.
      const tag = (e.target?.tagName||"").toLowerCase();
      if (tag === "input" || tag === "textarea" || (e.target?.isContentEditable)) return;
      const mod = e.ctrlKey || e.metaKey;
      if (!mod) return;
      if (e.key.toLowerCase() === "z" && !e.shiftKey) { e.preventDefault(); doUndo(); }
      else if ((e.key.toLowerCase() === "y") || (e.key.toLowerCase() === "z" && e.shiftKey)) { e.preventDefault(); doRedo(); }
    };
    document.addEventListener("keydown", onKey);
    return () => document.removeEventListener("keydown", onKey);
  }, []); // eslint-disable-line
  const saveSnapshot = () => {
    if (!valid) { alert("Template name is required."); return; }
    const inner = blocksToHtml(form.blocks || [], form.color);
    const mobileStyles = collectMobileFontStyles(form.blocks || []);
    const fontLinks = collectFontLinks(form.blocks || []);
    // Layered wrap: content card is capped at 600px (industry-standard email body width —
    // Mailchimp, HubSpot, Klaviyo all default here). The wallpaper extends to viewport
    // edges so on desktop you get the marketing-email look of a centered card framed by
    // wallpaper bars. Mobile drops the wallpaper entirely so the card fills the screen.
    const bgCss = canvasBgCss(form);
    const cardCss = contentCardCss(form);
    const cardBg = cardCss || (bgCss ? "background:#ffffff;" : "");
    const layered = `<div class="email-card" style="${cardBg}max-width:600px;width:100%;margin:0 auto;position:relative;z-index:1;overflow:hidden">${inner}</div>`;
    const responsiveCss = `<style>
      .email-wallpaper{background-attachment:fixed;min-height:100vh;}
      h1,h2{overflow-wrap:break-word;word-wrap:break-word;}
      @media (max-width:600px){
        .email-wallpaper{background:none !important;background-image:none !important;padding:0 !important;}
      }
    </style>`;
    const persisted = (fontLinks + mobileStyles + responsiveCss) + (bgCss ? `<div class="email-wallpaper" style="${bgCss};padding:0;min-height:100vh">${layered}</div>` : layered);
    // On a real save, drop the autosaved draft for this session.
    writeDrafts(drafts.filter(d => d.draftId !== draftIdRef.current));
    onSave({ ...form, body: form.mode === "html" ? form.body : persisted });
  };
  const restoreDraft = (d) => {
    if (!confirm(`Restore "${d.label}"? Current unsaved changes will be lost.`)) return;
    draftIdRef.current = d.draftId;
    const { draftId, at, label, ...rest } = d;
    setForm(f => ({...f, ...rest, name: label === "(Untitled draft)" ? "" : (rest.name || label) }));
    setSelectedBlockId(null);
  };
  const deleteDraft = (draftId) => {
    if (!confirm("Delete this draft?")) return;
    writeDrafts(drafts.filter(d => d.draftId !== draftId));
  };
  const onUploadImage = (blockId) => {
    const inp = document.createElement("input");
    inp.type = "file"; inp.accept = "image/*";
    inp.onchange = (e) => {
      const file = e.target.files?.[0]; if (!file) return;
      if (file.size > 500*1024) { if (!confirm(`This image is ${(file.size/1024).toFixed(0)}KB. Embedding large images inflates email size and may cause clipping in Gmail (>102KB clip threshold). Continue?`)) return; }
      const reader = new FileReader();
      reader.onload = ev => updateBlock(blockId, { src: ev.target.result, alt: file.name });
      reader.readAsDataURL(file);
    };
    inp.click();
  };
  const selectedBlock = (form.blocks || []).find(b => b.id === selectedBlockId);
  return (
    <div style={{display:"flex",flexDirection:"column",gap:10,flex:1,minHeight:0,minWidth:0}}>
      {/* Top bar — name + actions */}
      <div style={{display:"flex",alignItems:"center",gap:10,background:C.panel,border:`1px solid ${C.border}`,borderRadius:10,padding:"10px 14px"}}>
        <button onClick={onCancel} style={btn(C.dim,C.muted)} title="Back to templates">← Templates</button>
        <input value={form.name} onChange={e=>set("name",e.target.value)} placeholder="Untitled template…" style={inp({flex:1,fontSize:14,fontWeight:800})}/>
        <div style={{display:"flex",gap:4,background:"#090f1c",borderRadius:7,padding:3}}>
          {[["design","🎨 Design"],["settings","⚙ Settings"],["drafts",`📝 Drafts (${drafts.length})`]].map(([k,l])=>(
            <button key={k} onClick={()=>setEditorTab(k)} style={{background:editorTab===k?"#162035":"transparent",border:"none",borderRadius:5,padding:"5px 10px",color:editorTab===k?C.accent:C.muted,fontSize:11,fontWeight:editorTab===k?800:600,cursor:"pointer",fontFamily:"inherit"}}>{l}</button>
          ))}
        </div>
        {initial?.id && onDelete && <button onClick={()=>{ if(confirm(`Delete "${initial.name}"?`)) onDelete(initial.id); }} style={btn("#1a0808","#f87171")} title="Delete template">🗑</button>}
      </div>

      <div style={{display:"flex",gap:10,flex:1,minHeight:0,minWidth:0}}>
        {/* LEFT rail — blocks library / settings / history (scroll area) + persistent action buttons */}
        <div style={{width:200,flexShrink:0,background:C.panel,border:`1px solid ${C.border}`,borderRadius:10,padding:"10px 11px",display:"flex",flexDirection:"column",minHeight:0}}>
          <div style={{flex:1,minHeight:0,overflowY:"auto",paddingRight:2}}>
            {editorTab === "design" && <>
              {form.mode==="rich" && <>
                <div style={{fontSize:9,fontWeight:800,color:C.dim,letterSpacing:".08em",marginBottom:7}}>CONTENT BLOCKS</div>
                <div style={{display:"flex",flexDirection:"column",gap:5,marginBottom:14}}>
                  {[["banner","Hero Banner"],["imageBanner","Image Banner"],["logo","Logo"],["header","Heading"],["para","Paragraph"],["btn","Button"],["divider","Divider"],["spacer","Spacer"],["image","Image"],["footer","Footer"]].map(([k,lbl])=>(
                    <button key={k} onClick={()=>insertBlock(k)} style={{background:"#090f1c",border:`1px solid ${C.border}`,borderRadius:7,padding:"7px 10px",display:"flex",alignItems:"center",justifyContent:"center",cursor:"pointer",fontFamily:"inherit",textAlign:"center"}} onMouseEnter={e=>e.currentTarget.style.borderColor=C.accent} onMouseLeave={e=>e.currentTarget.style.borderColor=C.border}>
                      <span style={{fontSize:11,color:C.text,fontWeight:600}}>{lbl}</span>
                    </button>
                  ))}
                  {/* Row block — splits into 2 or 3 side-by-side columns of nested blocks. */}
                  <div style={{background:"#090f1c",border:`1px solid ${C.border}`,borderRadius:7,padding:"6px 8px"}}>
                    <div style={{display:"flex",alignItems:"center",gap:8,marginBottom:5}}>
                      <span style={{fontSize:11,color:C.text,fontWeight:600,flex:1,textAlign:"center"}}>Columns</span>
                    </div>
                    <div style={{display:"flex",gap:4}}>
                      <button onClick={()=>insertBlock("row",{cols:2})} title="Insert a 2-column row" style={{flex:1,background:"#162035",border:`1px solid ${C.border}`,borderRadius:5,padding:"4px 0",fontSize:10,color:C.accent,cursor:"pointer",fontFamily:"inherit",fontWeight:700}}>2 cols</button>
                      <button onClick={()=>insertBlock("row",{cols:3})} title="Insert a 3-column row" style={{flex:1,background:"#162035",border:`1px solid ${C.border}`,borderRadius:5,padding:"4px 0",fontSize:10,color:C.accent,cursor:"pointer",fontFamily:"inherit",fontWeight:700}}>3 cols</button>
                    </div>
                  </div>
                </div>
              </>}
              <div style={{display:"flex",alignItems:"center",justifyContent:"space-between",marginBottom:7}}>
                <span style={{fontSize:9,fontWeight:800,color:C.dim,letterSpacing:".08em"}}>RECENT MERGE TAGS</span>
                <button onClick={()=>setShowMergePicker(true)} style={{background:"transparent",border:"none",color:C.accent,fontSize:10,cursor:"pointer",fontFamily:"inherit"}} title="Browse the full library of merge tags">All →</button>
              </div>
              {recentTags.length > 0 ? (
                <div style={{display:"flex",flexDirection:"column",gap:4,marginBottom:10}}>
                  {recentTags.map(v=>(
                    <button key={v} onClick={()=>insertVar(v)} style={{background:"#1a1908",border:"1px solid #facc1533",borderRadius:7,padding:"5px 9px",color:"#facc15",fontSize:10,fontWeight:700,cursor:"pointer",fontFamily:"ui-monospace,monospace",textAlign:"left"}}>{`{${v}}`}</button>
                  ))}
                </div>
              ) : (
                <div style={{background:"#090f1c",border:`1px dashed ${C.border}`,borderRadius:7,padding:"9px 11px",fontSize:10,color:C.muted,lineHeight:1.5,marginBottom:10}}>No recents yet — click <strong style={{color:C.accent}}>All →</strong> to browse the full library.</div>
              )}
              <div style={{fontSize:10,color:C.dim,lineHeight:1.5,padding:"8px 4px 0",borderTop:`1px solid ${C.border}`}}>
                {form.mode==="rich" ? `Click a block to add. Click a block in the canvas to edit it. Use {KEY|fallback} for default values (e.g. {FIRST_NAME|there}).` : "Plain-text mode — type freely."}
              </div>
            </>}
            {editorTab === "settings" && <>
              <div style={{fontSize:9,fontWeight:800,color:C.dim,letterSpacing:".08em",marginBottom:7}}>TEMPLATE TYPE</div>
              <div style={{display:"flex",flexDirection:"column",gap:5,marginBottom:14}}>
                <button onClick={()=>set("mode","rich")} style={{background:form.mode==="rich"?"#091420":"#090f1c",border:`1.5px solid ${form.mode==="rich"?"#60a5fa":C.border}`,borderRadius:7,padding:"8px 10px",cursor:"pointer",fontFamily:"inherit",textAlign:"left"}}>
                  <div style={{fontSize:12,fontWeight:800,color:form.mode==="rich"?"#60a5fa":C.text}}>📧 Marketing</div>
                  <div style={{fontSize:10,color:C.muted,marginTop:2}}>Visual block builder</div>
                </button>
                <button onClick={()=>set("mode","html")} style={{background:form.mode==="html"?"#091420":"#090f1c",border:`1.5px solid ${form.mode==="html"?"#60a5fa":C.border}`,borderRadius:7,padding:"8px 10px",cursor:"pointer",fontFamily:"inherit",textAlign:"left"}}>
                  <div style={{fontSize:12,fontWeight:800,color:form.mode==="html"?"#60a5fa":C.text}}>{"<>"} Plain / HTML</div>
                  <div style={{fontSize:10,color:C.muted,marginTop:2}}>Raw text or HTML paste</div>
                </button>
              </div>
              <div style={{fontSize:9,fontWeight:800,color:C.dim,letterSpacing:".08em",marginBottom:7}}>FOLDER</div>
              <select value={form.category} onChange={e=>set("category",e.target.value)} style={inp({width:"100%",fontSize:11,padding:"7px 10px",boxSizing:"border-box",marginBottom:14})}>
                {((folders && folders.length) ? folders : TEMPLATE_FOLDERS.map(f => ({...f, name: f.label}))).map(c => {
                  // Walk parentId chain so the dropdown shows "Parent / Child" for nested folders
                  const folderMap = new Map((folders||[]).map(f => [f.id, f]));
                  const labelChain = [];
                  let cur = c;
                  while (cur) { labelChain.unshift(cur.name || cur.label); cur = cur.parentId ? folderMap.get(cur.parentId) : null; if (labelChain.length > 5) break; }
                  return <option key={c.id} value={c.id}>{c.icon} {labelChain.join(" / ")}</option>;
                })}
              </select>
              {/* Content background color — solid color behind every block. */}
              <div style={{fontSize:10,fontWeight:800,color:C.dim,letterSpacing:".08em",marginBottom:9}}>CONTENT BACKGROUND COLOR</div>
              <div style={{display:"flex",alignItems:"center",gap:10,marginBottom:10,background:"#090f1c",border:`1px solid ${C.border}`,borderRadius:9,padding:"10px 12px"}}>
                <input type="color" value={form.contentBg||"#ffffff"} onChange={e=>{ set("contentBg", e.target.value); set("contentBgOpacity", 1); }} style={{width:40,height:32,border:"none",borderRadius:6,cursor:"pointer",flexShrink:0}} title="Content background color"/>
                <div style={{flex:1,minWidth:0}}>
                  <div style={{fontSize:12,color:C.text,fontWeight:700,fontFamily:"ui-monospace,monospace"}}>{form.contentBg || "#ffffff (default)"}</div>
                  <div style={{fontSize:10,color:C.dim,marginTop:2,lineHeight:1.4}}>Solid color behind every block. Defaults to white when a wallpaper is set.</div>
                </div>
                {form.contentBg && <button onClick={()=>set("contentBg","")} title="Reset to default" style={{...btn(C.dim,C.muted),padding:"4px 8px",fontSize:10}}>Reset</button>}
              </div>
              {/* Default block color — drives the default bg for newly-inserted banners,
                  buttons, links, etc. Separate from content background so a colored card
                  + colored block defaults don't collide visually. */}
              <div style={{fontSize:10,fontWeight:800,color:C.dim,letterSpacing:".08em",marginBottom:9}}>DEFAULT BLOCK COLOR</div>
              <div style={{display:"flex",alignItems:"center",gap:10,marginBottom:7,background:"#090f1c",border:`1px solid ${C.border}`,borderRadius:9,padding:"10px 12px"}}>
                <input type="color" value={form.color||"#3b82f6"} onChange={e=>set("color", e.target.value)} style={{width:40,height:32,border:"none",borderRadius:6,cursor:"pointer",flexShrink:0}} title="Default block color"/>
                <div style={{flex:1,minWidth:0}}>
                  <div style={{fontSize:12,color:C.text,fontWeight:700,fontFamily:"ui-monospace,monospace"}}>{form.color || "#3b82f6"}</div>
                  <div style={{fontSize:10,color:C.dim,marginTop:2,lineHeight:1.4}}>Used for new banners, buttons, and accents. Each block can override its own color.</div>
                </div>
              </div>
              {/* Desktop wallpaper — visible around the email column on wide screens; auto-hidden
                  on mobile via a media query so the email fills the device viewport cleanly. */}
              <div style={{borderTop:`1px solid ${C.border}`,paddingTop:14,marginTop:14,marginBottom:7}}>
                <div style={{display:"flex",alignItems:"baseline",gap:10,marginBottom:5}}>
                  <div style={{fontSize:10,fontWeight:800,color:C.dim,letterSpacing:".08em"}}>DESKTOP WALLPAPER</div>
                  <span style={{fontSize:10,color:C.accent,fontWeight:700,letterSpacing:".02em"}}>DESKTOP ONLY</span>
                </div>
                <div style={{fontSize:10,color:C.dim,lineHeight:1.5,marginBottom:9}}>Sits behind the email column. Automatically hidden on mobile so the email fills the screen.</div>
                <div style={{display:"flex",alignItems:"center",gap:8,marginBottom:10,background:"#090f1c",border:`1px solid ${C.border}`,borderRadius:9,padding:"8px 10px"}}>
                  <input type="color" value={form.canvasBg||"#f8fafc"} onChange={e=>{ set("canvasBg",e.target.value); set("canvasBgGradient",""); }} style={{width:34,height:28,border:"none",borderRadius:6,cursor:"pointer",flexShrink:0}} title="Solid background color"/>
                  <GradientPickerButton value={form.canvasBgGradient} onPick={v=>set("canvasBgGradient",v)}/>
                  <span style={{fontSize:10,color:C.muted,fontFamily:"ui-monospace,monospace",flex:1,minWidth:0,overflow:"hidden",textOverflow:"ellipsis",whiteSpace:"nowrap"}}>{form.canvasBgGradient ? "gradient" : (form.canvasBg || "default")}</span>
                  <button onClick={()=>{ set("canvasBg",""); set("canvasBgGradient",""); }} title="Reset to default white" style={{...btn(C.dim,C.muted),padding:"4px 8px",fontSize:10}}>Reset</button>
                </div>
                <div style={{fontSize:9,fontWeight:800,color:C.dim,letterSpacing:".08em",marginBottom:6}}>PATTERN</div>
                <div style={{display:"flex",gap:3,background:"#090f1c",border:`1px solid ${C.border}`,borderRadius:7,padding:3,marginBottom:9}}>
                  {[["none","None"],["emoji","Emoji"],["word","Words"],["image","Logo"]].map(([k,l])=>{
                    const sel = (form.bgPatternType||"none") === k;
                    return <button key={k} onClick={()=>set("bgPatternType",k)} style={{flex:1,background:sel?"#162035":"transparent",border:"none",borderRadius:5,padding:"5px 0",fontSize:10,color:sel?C.accent:C.muted,cursor:"pointer",fontFamily:"inherit",fontWeight:sel?800:600}}>{l}</button>;
                  })}
                </div>
                {form.bgPatternType === "emoji" && (
                  <EmojiPatternPicker value={form.bgPatternEmoji||""} onChange={v=>set("bgPatternEmoji",v)}/>
                )}
                {form.bgPatternType === "word" && (
                  <input value={form.bgPatternWord||""} onChange={e=>set("bgPatternWord",e.target.value)} maxLength={20} placeholder="e.g. SOLD, OPEN, BRAND" style={inp({width:"100%",boxSizing:"border-box",fontSize:13,padding:"7px 10px",marginBottom:9,textAlign:"center",fontWeight:700,letterSpacing:".05em"})}/>
                )}
                {form.bgPatternType === "image" && (
                  <div style={{display:"flex",gap:5,marginBottom:9}}>
                    <input value={form.bgPatternImage||""} onChange={e=>set("bgPatternImage",e.target.value)} placeholder="Logo URL…" style={inp({flex:1,minWidth:0,fontSize:11,padding:"6px 9px"})}/>
                    <button onClick={()=>{
                      const fi = document.createElement("input");
                      fi.type = "file"; fi.accept = "image/*";
                      fi.onchange = (e) => {
                        const f = e.target.files?.[0]; if (!f) return;
                        if (f.size > 200*1024 && !confirm(`Logo is ${(f.size/1024).toFixed(0)}KB — large patterns bloat the email. Continue?`)) return;
                        const r = new FileReader();
                        r.onload = (ev) => set("bgPatternImage", ev.target.result);
                        r.readAsDataURL(f);
                      };
                      fi.click();
                    }} style={{...btn(C.dim,C.accent),fontSize:11,padding:"6px 10px",whiteSpace:"nowrap"}}>Upload</button>
                  </div>
                )}
                {form.bgPatternType && form.bgPatternType !== "none" && (
                  <>
                    <div style={{display:"flex",justifyContent:"space-between",alignItems:"baseline",marginBottom:3}}>
                      <div style={{fontSize:9,color:C.dim,fontWeight:700,letterSpacing:".04em"}}>TILE SIZE</div>
                      <div style={{fontSize:10,color:C.muted}}>{form.bgPatternSize||60}px</div>
                    </div>
                    <input type="range" min="24" max="160" step="4" value={form.bgPatternSize||60} onChange={e=>set("bgPatternSize",+e.target.value)} style={{width:"100%",marginBottom:9,accentColor:C.accent}}/>
                    <div style={{display:"flex",justifyContent:"space-between",alignItems:"baseline",marginBottom:3}}>
                      <div style={{fontSize:9,color:C.dim,fontWeight:700,letterSpacing:".04em"}}>OPACITY</div>
                      <div style={{fontSize:10,color:C.muted}}>{Math.round((form.bgPatternOpacity ?? 0.15)*100)}%</div>
                    </div>
                    <input type="range" min="0.05" max="1" step="0.05" value={form.bgPatternOpacity ?? 0.15} onChange={e=>set("bgPatternOpacity",+e.target.value)} style={{width:"100%",marginBottom:6,accentColor:C.accent}}/>
                  </>
                )}
              </div>
            </>}
            {editorTab === "drafts" && <>
              <div style={{fontSize:9,fontWeight:800,color:C.dim,letterSpacing:".08em",marginBottom:7}}>UNSAVED DRAFTS</div>
              <div style={{fontSize:10,color:C.dim,lineHeight:1.5,marginBottom:9}}>Autosaved as you edit. The 10 most recent unsaved templates appear here — restore any to pick up where you left off.</div>
              {drafts.length === 0 ? (
                <div style={{fontSize:11,color:C.muted,lineHeight:1.5}}>No drafts yet. Anything you type that isn't saved will appear here automatically.</div>
              ) : (
                <div style={{display:"flex",flexDirection:"column",gap:6}}>
                  {drafts.map((d)=>{
                    const isCurrent = d.draftId === draftIdRef.current;
                    const isHovered = hoveredDraftId?.draftId === d.draftId && !isCurrent;
                    return (
                      <div key={d.draftId}
                        onMouseEnter={(e)=>{
                          if (isCurrent) return;
                          const rect = e.currentTarget.getBoundingClientRect();
                          setHoveredDraftId({ draftId: d.draftId, rect: { top: rect.top, left: rect.left, right: rect.right, width: rect.width } });
                        }}
                        onMouseLeave={()=>setHoveredDraftId(p=>p?.draftId===d.draftId?null:p)}
                        style={{background:"#090f1c",border:`1px solid ${isHovered?C.accent:isCurrent?C.accent+"55":C.border}`,borderRadius:7,padding:"8px 10px",transition:"border-color .12s"}}>
                        <div style={{fontSize:11,fontWeight:700,color:C.text,marginBottom:2,overflow:"hidden",textOverflow:"ellipsis",whiteSpace:"nowrap"}}>{d.label}{isCurrent && <span style={{fontSize:9,color:C.accent,marginLeft:6,fontWeight:600}}>· current</span>}</div>
                        <div style={{fontSize:10,color:C.dim,marginBottom:6}}>{(d.blocks||[]).length} block{(d.blocks||[]).length===1?"":"s"} · {new Date(d.at).toLocaleString("en-US",{month:"short",day:"numeric",hour:"numeric",minute:"2-digit"})}</div>
                        <div style={{display:"flex",gap:5}}>
                          <button onClick={()=>restoreDraft(d)} disabled={isCurrent} style={{...btn(C.dim,isCurrent?C.muted:C.accent),padding:"4px 10px",fontSize:10,flex:1,opacity:isCurrent?0.5:1,cursor:isCurrent?"default":"pointer"}}>↺ Restore</button>
                          <button onClick={()=>deleteDraft(d.draftId)} style={{...btn("#1a0808","#f87171"),padding:"4px 8px",fontSize:10}} title="Delete draft">🗑</button>
                        </div>
                      </div>
                    );
                  })}
                </div>
              )}
            </>}
          </div>
        </div>

        {/* CENTER — canvas with block list */}
        <div style={{flex:1,display:"flex",flexDirection:"column",gap:8,minWidth:0,background:C.panel,border:`1px solid ${C.border}`,borderRadius:10,padding:"12px",overflowY:"auto"}}>
          <div>
            <div style={{fontSize:10,fontWeight:800,color:C.dim,letterSpacing:".05em",marginBottom:4}}>SUBJECT LINE</div>
            <div style={{display:"flex",gap:7,alignItems:"stretch"}}>
              <div style={{flex:1,minWidth:0}}>
                <MergeTagInput value={form.subject} onChange={v=>set("subject",v)} onInsertTag={bumpRecent} placeholder="Subject — type # to insert a merge tag. e.g. Welcome to {BRAND}, {FIRST_NAME|there}!" style={inp({width:"100%",boxSizing:"border-box"})}/>
              </div>
              {/* Undo / Redo — compact pair, disabled when stack empty. */}
              <div style={{display:"flex",gap:0,border:`1px solid ${C.border}`,borderRadius:8,overflow:"hidden"}}>
                <button onClick={doUndo} disabled={!canUndo} title="Undo (⌘Z)" style={{background:"#090f1c",border:"none",borderRight:`1px solid ${C.border}`,padding:"9px 12px",color:canUndo?C.text:C.dim,cursor:canUndo?"pointer":"not-allowed",fontSize:14,fontFamily:"inherit",opacity:canUndo?1:0.5}}>↶</button>
                <button onClick={doRedo} disabled={!canRedo} title="Redo (⌘⇧Z)" style={{background:"#090f1c",border:"none",padding:"9px 12px",color:canRedo?C.text:C.dim,cursor:canRedo?"pointer":"not-allowed",fontSize:14,fontFamily:"inherit",opacity:canRedo?1:0.5}}>↷</button>
              </div>
              <button onClick={()=>setShowPreview(true)} style={{...btn("#091420","#60a5fa",true),padding:"9px 14px",whiteSpace:"nowrap"}} title="Preview the email in desktop and mobile">👁 Preview</button>
              <button onClick={saveSnapshot} style={{...btn("#091c09","#4ade80",true),padding:"9px 16px",whiteSpace:"nowrap"}}>{initial?.id?"💾 Save":"✨ Create"}</button>
            </div>
          </div>

          {form.mode === "html" ? (
            <div style={{display:"flex",flexDirection:"column",flex:1,minHeight:0}}>
              <div style={{fontSize:10,fontWeight:800,color:C.dim,letterSpacing:".05em",marginBottom:4}}>BODY (plain / HTML)</div>
              <textarea value={form.body} onChange={e=>set("body",e.target.value)} placeholder="Paste HTML or type plain text. Merge tags + fallbacks work here too." style={{...inp({width:"100%",flex:1,resize:"none",fontFamily:"ui-monospace,monospace",fontSize:12,lineHeight:1.6,boxSizing:"border-box",minHeight:300})}}/>
            </div>
          ) : (
            <div style={{flex:1,minHeight:0,display:"flex",flexDirection:"column",gap:6}}>
              <div style={{display:"flex",alignItems:"center",gap:9}}>
                <div style={{fontSize:10,fontWeight:800,color:C.dim,letterSpacing:".05em",flex:1}}>EMAIL CANVAS — click a block to edit, hover a block to reveal the ✥ drag handle</div>
                {draftSavedFlash && (
                  <span style={{fontSize:10,fontWeight:700,color:"#4ade80",background:"#0a1c10",border:"1px solid #4ade8033",borderRadius:6,padding:"3px 8px",animation:"draftSaveFade 2.2s ease forwards",whiteSpace:"nowrap"}}>✓ Draft saved</span>
                )}
              </div>
              <style>{`@keyframes draftSaveFade{0%{opacity:0;transform:translateY(-2px)}10%{opacity:1;transform:translateY(0)}70%{opacity:1}100%{opacity:0}}`}</style>
              <div data-template-canvas onClick={()=>setSelectedBlockId(null)} style={{...canvasBgStyle(form),border:`1px solid #cbd5e1`,borderRadius:8,padding:0,flex:1,minHeight:240,overflowY:"auto",color:"#1e293b"}}>
                <div style={{...(contentCardStyle(form) || {}), padding:0, overflow:"hidden", maxWidth:600, width:"100%", margin:"0 auto"}}>
                {(form.blocks||[]).length === 0 && (
                  <div onClick={(e)=>e.stopPropagation()} style={{textAlign:"center",padding:"32px 16px",color:"#94a3b8"}}>
                    <div style={{fontSize:34,marginBottom:10}}>✨</div>
                    <div style={{fontSize:14,fontWeight:700,color:"#475569",marginBottom:5}}>Start building your email</div>
                    <div style={{fontSize:12,marginBottom:14}}>Click a block on the left rail to add it.</div>
                  </div>
                )}
                {(() => {
                  // Render each top-level block. Row blocks render their columns inline; the
                  // selected nested child's property form is rendered FULL-WIDTH below the row
                  // (not inside a cramped column) to fix the clipping problem.
                  const renderBlockCard = (b, options) => {
                    const { indexInSiblings, totalSiblings, draggable } = options;
                    const isSel = selectedBlockId === b.id;
                    const isRow = b.type === "row";
                    // Find the selected nested child (if this is a row and a child is selected).
                    const nestedSel = isRow ? (b.children||[]).flat().find(c => c.id === selectedBlockId) : null;
                    // Position of nested selected child within its column (for move ↑/↓ buttons).
                    let nestedColIdx = -1, nestedIdxInCol = -1, nestedColLen = 0;
                    if (nestedSel) {
                      (b.children||[]).forEach((col, ci) => {
                        const idx = col.findIndex(c => c.id === nestedSel.id);
                        if (idx >= 0) { nestedColIdx = ci; nestedIdxInCol = idx; nestedColLen = col.length; }
                      });
                    }
                    const showForm = isSel || !!nestedSel;
                    const formTarget = nestedSel || b;
                    const isDraggingMe = dragging === b.id;
                    const isDropTarget = draggable && dragOverId === b.id && dragging && dragging !== b.id;
                    return (
                      <div key={b.id}
                        {...(draggable ? {
                          // Card itself is NOT draggable by default. The ✥ drag handle flips
                          // draggable="true" on mousedown so a click anywhere else on the card
                          // (sliders, color pickers, text) NEVER initiates a drag. The handle
                          // restores draggable="false" on mouseup.
                          draggable: false,
                          onDragStart: (e)=>{
                            setDragging(b.id);
                            e.dataTransfer.effectAllowed="move";
                            e.dataTransfer.setData("text/plain", b.id);
                            if (ghostImgRef.current) e.dataTransfer.setDragImage(ghostImgRef.current, 0, 0);
                            e.stopPropagation();
                          },
                          onDragEnter: (e)=>{ e.preventDefault(); if (dragging && dragging !== b.id) setDragOverId(b.id); },
                          onDragOver: (e)=>{ e.preventDefault(); e.dataTransfer.dropEffect="move"; },
                          onDragLeave: (e)=>{ /* keep target sticky until enter elsewhere */ },
                          onDrop: (e)=>{ e.preventDefault(); e.stopPropagation(); if (!dragging || dragging===b.id) { setDragOverId(null); return; } setBlocks(bs=>{ const from=bs.findIndex(x=>x.id===dragging); const to=bs.findIndex(x=>x.id===b.id); if (from<0||to<0) return bs; const next=[...bs]; const [m]=next.splice(from,1); next.splice(to,0,m); return next; }); setDragging(null); setDragOverId(null); },
                          onDragEnd: ()=>{ setDragging(null); setDragOverId(null); },
                        } : {})}
                        data-block-card
                        onClick={(e)=>{ e.stopPropagation(); setSelectedBlockId(b.id); }}
                        onMouseEnter={()=>draggable && setHoveredBlockId(b.id)}
                        onMouseLeave={()=>setHoveredBlockId(p => p === b.id ? null : p)}
                        style={{position:"relative",cursor:"pointer",opacity:isDraggingMe?0.32:1,transform:isDraggingMe?"scale(0.985)":"scale(1)",transition:"opacity .14s ease, transform .14s ease, box-shadow .12s ease",boxShadow: isSel ? `inset 0 0 0 2px ${form.color}, 0 0 0 2px ${form.color}` : "none",borderRadius:isSel?4:0}}>
                        {/* Drop indicator line above the hovered target — accent-colored with a soft glow. */}
                        {isDropTarget && <div style={{position:"absolute",top:-4,left:0,right:0,height:3,background:form.color,borderRadius:2,boxShadow:`0 0 10px ${form.color}aa`,pointerEvents:"none",zIndex:2}}/>}
                        {draggable && (hoveredBlockId === b.id || isDraggingMe) && (
                          <div
                            title="Drag to move this block"
                            onMouseDown={(e)=>{
                              // Flip the card's draggable attribute to true on press so a
                              // drag can start; restore to false on release so accidental
                              // clicks elsewhere on the card never start a drag.
                              const card = e.currentTarget.closest("[data-block-card]");
                              if (card) {
                                card.setAttribute("draggable", "true");
                                const restore = () => { card.setAttribute("draggable", "false"); document.removeEventListener("mouseup", restore); };
                                document.addEventListener("mouseup", restore);
                              }
                            }}
                            style={{position:"absolute",top:"50%",transform:"translateY(-50%)",left:6,display:"flex",alignItems:"center",justifyContent:"center",width:24,height:24,borderRadius:6,background:isDraggingMe?form.color:"rgba(15,23,42,0.86)",border:`1px solid ${isDraggingMe?form.color:"rgba(255,255,255,0.14)"}`,cursor:"grab",userSelect:"none",boxShadow:"0 2px 6px rgba(0,0,0,0.18)",transition:"background .14s, opacity .14s",zIndex:3,opacity:1}}>
                            <DragHandleIcon size={14} color="#ffffff"/>
                          </div>
                        )}
                        {isRow ? (
                          // Row block: each column is a small square placeholder when empty, or shows
                          // its single block when filled. Only one block per column. Cells with
                          // colSpan > 1 take proportionally more width. Merge / split buttons live
                          // between adjacent cells when the row is selected (3-slot rows only).
                          (() => {
                            const cells = (b.children || []).slice(0, b.cols || 2);
                            const spans = (b.colSpans && b.colSpans.length === cells.length) ? b.colSpans : cells.map(() => 1);
                            return (
                              <div style={{display:"flex",gap:0,padding:"4px 2px",alignItems:"center",position:"relative"}}>
                                {cells.map((col, ci) => {
                                  const cellBlock = col[0];
                                  const cellSel = cellBlock && cellBlock.id === selectedBlockId;
                                  const span = spans[ci] || 1;
                                  const isLastCell = ci === cells.length - 1;
                                  const totalSpan = spans.reduce((a,s)=>a+s,0) || cells.length;
                                  return (
                                    <React.Fragment key={ci}>
                                      <div onClick={(e)=>e.stopPropagation()} style={{flex:span,minWidth:0,marginLeft: ci === 0 ? 0 : (b.gap||12)/2, marginRight: isLastCell ? 0 : (b.gap||12)/2,background:"rgba(99,102,241,0.04)",border:`1px dashed ${C.border}`,borderRadius:6,padding:"6px 6px",display:"flex",flexDirection:"column",alignItems:"stretch",justifyContent:"center"}}>
                                        <div style={{fontSize:9,fontWeight:700,color:"#94a3b8",letterSpacing:".05em",marginBottom:4,textAlign:"center"}}>{span > 1 ? `MERGED · ${span}/${totalSpan}` : `COL ${ci+1}`}{span > 1 && <button onClick={()=>splitRowCell(b.id, ci)} title="Split this merged cell back into separate columns" style={{marginLeft:6,background:"transparent",border:"none",color:C.accent,fontSize:10,cursor:"pointer",fontWeight:700}}>⤢ Split</button>}</div>
                                        {cellBlock ? (
                                          <div onClick={(e)=>{ e.stopPropagation(); setSelectedBlockId(cellBlock.id); }} style={{position:"relative",border:`2px solid ${cellSel?form.color:"transparent"}`,borderRadius:6,padding:"2px 4px",cursor:"pointer",background:cellSel?"#eef4ff":"transparent"}}>
                                            <EditableBlockHtml block={cellBlock} html={fill(blocksToHtml([cellBlock], form.color))} onChange={(patch)=>updateBlock(cellBlock.id, patch)}/>
                                          </div>
                                        ) : (
                                          <div style={{aspectRatio: span > 1 ? "2 / 1" : "1 / 1",display:"flex",alignItems:"center",justifyContent:"center",background:"#f1f5f9",border:`1px dashed #cbd5e1`,borderRadius:6,minHeight:100}}>
                                            <RowColumnAddMenu onPick={(t)=>insertIntoColumn(b.id, ci, t)}/>
                                          </div>
                                        )}
                                      </div>
                                      {/* Merge button between this cell and the next — only when the row is selected,
                                          total slots = 3, AND no merges yet (cells.length === 3). Once one merge
                                          happens we have 2 cells and merging again would create a span-3 = single
                                          column, which we explicitly disallow. */}
                                      {!isLastCell && isSel && b.cols === 3 && cells.length === 3 && (
                                        <button onClick={(e)=>{ e.stopPropagation(); mergeRowCells(b.id, ci); }} title={`Merge column ${ci+1} with column ${ci+2}`} style={{position:"absolute",top:"50%",left:`calc(${((ci+1) * (100 / cells.length))}% - 14px)`,transform:"translateY(-50%)",zIndex:5,width:28,height:28,borderRadius:"50%",background:form.color,border:"2px solid #fff",color:"#fff",fontSize:16,fontWeight:800,cursor:"pointer",display:"flex",alignItems:"center",justifyContent:"center",boxShadow:"0 3px 10px rgba(0,0,0,0.25)",fontFamily:"inherit"}}>⇆</button>
                                      )}
                                    </React.Fragment>
                                  );
                                })}
                              </div>
                            );
                          })()
                        ) : (
                          <EditableBlockHtml
                            block={b}
                            html={fill(blocksToHtml([b], form.color, { isFirst: indexInSiblings === 0, isLast: indexInSiblings === (totalSiblings - 1), prevType: indexInSiblings > 0 ? form.blocks[indexInSiblings - 1]?.type : null, nextHasBg: (() => { const n = form.blocks[indexInSiblings + 1]; return !!(n && (n.type === "banner" || n.type === "imageBanner" || n.blockBg || n.blockBgGradient || n.blockBgImage || (n.type === "image" && n.width === "bleed") || (n.type === "spacer" && n.bgColor))); })(), wrapperPadH: form.contentBg ? 22 : 24, wrapperPadV: form.contentBg ? 20 : 24 }))}
                            onChange={(patch)=>updateBlock(b.id, patch)}
                          />
                        )}
                        {showForm && (
                          <div data-block-form-for={formTarget.id} data-no-drag draggable={false} onClick={(e)=>e.stopPropagation()} onMouseDown={(e)=>e.stopPropagation()} onDragStart={(e)=>{ e.preventDefault(); e.stopPropagation(); }} style={{background:"#0c1422",border:`1px solid ${form.color}55`,borderRadius:8,padding:10,marginTop:6,color:C.text}}>
                            <div style={{display:"flex",alignItems:"center",gap:6,marginBottom:8}}>
                              <span style={{fontSize:10,fontWeight:800,color:form.color,letterSpacing:".05em",textTransform:"uppercase",flex:1}}>
                                {formTarget.type} block{formTarget.type==="row"?` · ${formTarget.cols||2} cols`:""}{nestedSel?` · in col ${nestedColIdx+1}`:""}
                              </span>
                              {nestedSel ? <>
                                <button onClick={()=>moveBlock(nestedSel.id,-1)} disabled={nestedIdxInCol<=0} style={{...btn(C.dim,C.muted),padding:"2px 7px",fontSize:11,opacity:nestedIdxInCol<=0?0.4:1}} title="Move up">↑</button>
                                <button onClick={()=>moveBlock(nestedSel.id,1)} disabled={nestedIdxInCol>=(nestedColLen-1)} style={{...btn(C.dim,C.muted),padding:"2px 7px",fontSize:11,opacity:nestedIdxInCol>=(nestedColLen-1)?0.4:1}} title="Move down">↓</button>
                                <button onClick={()=>duplicateBlock(nestedSel.id)} style={{...btn(C.dim,"#60a5fa"),padding:"2px 7px",fontSize:11}} title="Duplicate block">⎘</button>
                                <button onClick={()=>removeBlock(nestedSel.id)} style={{...btn("#1a0808","#f87171"),padding:"2px 7px",fontSize:11}} title="Delete block">🗑</button>
                                <button onClick={()=>setSelectedBlockId(null)} style={{...btn(C.dim,C.muted),padding:"2px 7px",fontSize:11}} title="Close (deselect block)">✕</button>
                              </> : <>
                                <button onClick={()=>moveBlock(b.id,-1)} disabled={indexInSiblings===0} style={{...btn(C.dim,C.muted),padding:"2px 7px",fontSize:11,opacity:indexInSiblings===0?0.4:1}} title="Move up">↑</button>
                                <button onClick={()=>moveBlock(b.id,1)} disabled={indexInSiblings===(totalSiblings-1)} style={{...btn(C.dim,C.muted),padding:"2px 7px",fontSize:11,opacity:indexInSiblings===(totalSiblings-1)?0.4:1}} title="Move down">↓</button>
                                <button onClick={()=>duplicateBlock(b.id)} style={{...btn(C.dim,"#60a5fa"),padding:"2px 7px",fontSize:11}} title="Duplicate block">⎘</button>
                                <button onClick={()=>removeBlock(b.id)} style={{...btn("#1a0808","#f87171"),padding:"2px 7px",fontSize:11}} title="Delete block">🗑</button>
                                <button onClick={()=>setSelectedBlockId(null)} style={{...btn(C.dim,C.muted),padding:"2px 7px",fontSize:11}} title="Close (deselect block)">✕</button>
                              </>}
                            </div>
                            {nestedSel ? (
                              <BlockPropertyForm block={nestedSel} onChange={(patch)=>updateBlock(nestedSel.id,patch)} onUploadImage={()=>onUploadImage(nestedSel.id)} onInsertTag={bumpRecent}/>
                            ) : isRow ? (
                              <RowPropertyForm block={b} onChangeCols={(n)=>changeRowCols(b.id, n)} onChange={(patch)=>updateBlock(b.id, patch)}/>
                            ) : (
                              <BlockPropertyForm block={b} onChange={(patch)=>updateBlock(b.id,patch)} onUploadImage={()=>onUploadImage(b.id)} onInsertTag={bumpRecent}/>
                            )}
                          </div>
                        )}
                      </div>
                    );
                  };
                  return (form.blocks||[]).map((b, i) => renderBlockCard(b, { isTopLevel:true, indexInSiblings:i, totalSiblings:(form.blocks||[]).length, draggable:true }));
                })()}
                </div>
              </div>
            </div>
          )}
        </div>

      </div>

      {/* Preview modal */}
      {showPreview && (
        <div onClick={()=>setShowPreview(false)} style={{position:"fixed",inset:0,background:"#000c",zIndex:1001,display:"flex",alignItems:"center",justifyContent:"center",padding:24}}>
          <div onClick={e=>e.stopPropagation()} style={{background:C.panel,border:`1px solid ${C.border}`,borderRadius:14,padding:18,width:"100%",maxWidth:device==="mobile"?440:1100,maxHeight:"92vh",display:"flex",flexDirection:"column",gap:10}}>
            <div style={{display:"flex",alignItems:"center",gap:10}}>
              <span style={{fontSize:18}}>👁</span>
              <h3 style={{flex:1,margin:0,color:"#f0f6ff",fontWeight:900,fontSize:14}}>Preview</h3>
              <div style={{display:"flex",gap:3,background:"#090f1c",borderRadius:6,padding:2}}>
                <button onClick={()=>setDevice("desktop")} style={{background:device==="desktop"?"#162035":"transparent",border:"none",borderRadius:4,padding:"5px 12px",fontSize:11,color:device==="desktop"?C.accent:C.muted,cursor:"pointer",fontFamily:"inherit"}}>🖥 Desktop</button>
                <button onClick={()=>setDevice("mobile")} style={{background:device==="mobile"?"#162035":"transparent",border:"none",borderRadius:4,padding:"5px 12px",fontSize:11,color:device==="mobile"?C.accent:C.muted,cursor:"pointer",fontFamily:"inherit"}}>📱 Mobile</button>
              </div>
              <button onClick={()=>setShowPreview(false)} title="Close" style={btn(C.dim,C.muted)}>✕</button>
            </div>
            <div style={{background:"#1a1a1a",borderRadius:12,padding:device==="mobile"?"16px 12px":"16px",flex:1,minHeight:0,display:"flex",justifyContent:"center",alignItems:"flex-start",border:`1px solid ${C.border}`,overflow:"hidden"}}>
              {/* The previewHtml already produces its own wallpaper+card wrapping, so this
                  outer frame just provides the phone/desktop shell + the sender header. */}
              <div style={{borderRadius:device==="mobile"?22:8,width:device==="mobile"?340:"100%",maxHeight:"100%",overflowY:"auto",border:device==="mobile"?"7px solid #0a0a0a":"1px solid #cbd5e1",color:"#1e293b",background:"#fff"}}>
                <div style={{background:"#fff",borderBottom:"1px solid #e2e8f0",padding:device==="mobile"?"10px 14px":"14px 20px",fontSize:device==="mobile"?11:13,color:"#475569"}}>
                  <div style={{fontSize:device==="mobile"?10:12,color:"#94a3b8",marginBottom:3,fontFamily:"system-ui"}}>From: {brand?.name||"Your Brand"} &lt;rep@example.com&gt;</div>
                  <div style={{fontFamily:"system-ui"}}><strong>Subject:</strong> {fill(form.subject)||<em style={{color:"#94a3b8"}}>(no subject)</em>}</div>
                </div>
                <div style={{fontFamily:"Helvetica,Arial,sans-serif",fontSize:device==="mobile"?14:15}}>
                  {form.mode === "html"
                    ? (/<\w/.test(form.body) ? <div dangerouslySetInnerHTML={{__html: previewHtml(device) || "<p style='color:#94a3b8;font-style:italic'>(empty)</p>"}}/> : <pre style={{whiteSpace:"pre-wrap",margin:0,fontSize:device==="mobile"?12:14,fontFamily:"ui-monospace,monospace",lineHeight:1.6,padding:"18px 14px 26px"}}>{previewHtml(device) || "(empty)"}</pre>)
                    : <div dangerouslySetInnerHTML={{__html: previewHtml(device) || "<p style='color:#94a3b8;font-style:italic;padding:18px'>(empty — add a block to start)</p>"}}/>
                  }
                </div>
              </div>
            </div>
          </div>
        </div>
      )}

      {/* Merge tag picker overlay */}
      {/* Floating draft hover preview — fixed-position so it sits above the entire UI and
          doesn't require scrolling the drafts column to see. Positioned next to the hovered
          row using its viewport-relative rect captured on mouseenter. */}
      {hoveredDraftId && (() => {
        const d = drafts.find(x => x.draftId === hoveredDraftId.draftId);
        if (!d) return null;
        // Position to the right of the hovered row; clamp to viewport so it never spills off.
        const popupWidth = 380;
        const margin = 12;
        let left = hoveredDraftId.rect.right + margin;
        if (left + popupWidth > window.innerWidth - 12) left = Math.max(12, hoveredDraftId.rect.left - popupWidth - margin);
        const top = Math.max(12, Math.min(hoveredDraftId.rect.top, window.innerHeight - 500));
        return (
          <div style={{position:"fixed",top,left,zIndex:10000,width:popupWidth,maxHeight:480,background:"#fff",border:`2px solid ${C.accent}`,borderRadius:12,boxShadow:"0 18px 48px rgba(0,0,0,0.7)",overflow:"hidden",pointerEvents:"none"}}>
            <div style={{padding:"9px 14px",background:"#0c1422",borderBottom:`1px solid ${C.border}`,color:C.text,fontSize:12,fontWeight:700,fontFamily:"system-ui"}}>{d.label}</div>
            <div style={{maxHeight:434,overflow:"hidden",background:hasWallpaper(d)?(d.canvasBg||"#f8fafc"):"#ffffff"}}>
              <div style={{transform:"scale(0.6)",transformOrigin:"top left",width:`${100/0.6}%`,fontFamily:"Helvetica,Arial,sans-serif",fontSize:14,color:"#1e293b"}}>
                <div dangerouslySetInnerHTML={{__html: blocksToHtml(d.blocks||[], d.color||"#3b82f6") || "<p style='color:#94a3b8;font-style:italic;padding:14px'>(empty draft)</p>"}}/>
              </div>
            </div>
          </div>
        );
      })()}
      {showMergePicker && (
        <div onClick={()=>setShowMergePicker(false)} style={{position:"fixed",inset:0,background:"#000c",zIndex:1001,display:"flex",alignItems:"center",justifyContent:"center",padding:16}}>
          <div onClick={e=>e.stopPropagation()} style={{background:C.panel,border:`1px solid ${C.border}`,borderRadius:14,padding:20,width:"100%",maxWidth:560,maxHeight:"82vh",overflowY:"auto"}}>
            <div style={{display:"flex",alignItems:"center",gap:10,marginBottom:14}}>
              <span style={{fontSize:22}}>🏷️</span>
              <h3 style={{flex:1,margin:0,color:"#f0f6ff",fontWeight:900,fontSize:15}}>Merge Tags</h3>
              <button onClick={()=>setShowMergePicker(false)} title="Close" style={btn(C.dim,C.muted)}>✕</button>
            </div>
            <div style={{fontSize:11,color:C.muted,marginBottom:14,lineHeight:1.5}}>Click any tag to insert it into the selected block (or the subject line if nothing is selected). Use <strong style={{color:"#facc15",fontFamily:"ui-monospace,monospace"}}>{`{KEY|fallback}`}</strong> for default values when the field is empty.</div>
            {MERGE_TAG_GROUPS.map(g => (
              <div key={g.label} style={{marginBottom:14}}>
                <div style={{fontSize:10,fontWeight:800,color:C.dim,letterSpacing:".06em",marginBottom:7}}>{g.label.toUpperCase()}</div>
                <div style={{display:"grid",gridTemplateColumns:"repeat(auto-fill,minmax(160px,1fr))",gap:6}}>
                  {g.tags.map(([k,desc,sample])=>(
                    <button key={k} onClick={()=>{ insertVar(k); setShowMergePicker(false); }} style={{background:"#090f1c",border:"1px solid #facc1533",borderRadius:7,padding:"7px 10px",cursor:"pointer",fontFamily:"inherit",textAlign:"left"}} onMouseEnter={e=>e.currentTarget.style.borderColor="#facc15"} onMouseLeave={e=>e.currentTarget.style.borderColor="#facc1533"}>
                      <div style={{fontSize:11,color:"#facc15",fontFamily:"ui-monospace,monospace",fontWeight:700}}>{`{${k}}`}</div>
                      <div style={{fontSize:10,color:C.muted,marginTop:2}}>{desc}</div>
                    </button>
                  ))}
                </div>
              </div>
            ))}
          </div>
        </div>
      )}
    </div>
  );
}

// Property form for a single selected block — used inline in the canvas.
// Input/textarea wrapper that pops a merge-tag autocomplete when the user types "#".
// Filters by the text typed after "#". Arrow keys + Enter/Tab to select, Esc to dismiss.
// On insert, replaces `#filter` with `{TAG_KEY}` and bumps the global recents.
// Portal-based popover that always renders above everything else (z-index 10000), positioned
// relative to an anchor element via getBoundingClientRect. Closes on outside click + Escape +
// scroll/resize. Use this for any dropdown/popover so it escapes clipping parents (overflow:auto
// containers, modal stacks, etc.) and the user can dismiss it by clicking anywhere outside.
function Popover({ anchorRef, onClose, align="bottom-left", offset=6, minWidth, children }) {
  const popRef = useRef(null);
  // Two-phase placement: render off-screen first to measure the popover, then commit the
  // final position with vertical flip (open upward if no room below) + horizontal clamp so
  // the popover never overflows the viewport.
  const [pos, setPos] = useState(null);
  useEffect(() => {
    const place = () => {
      const a = anchorRef.current;
      const p = popRef.current;
      if (!a) return;
      const r = a.getBoundingClientRect();
      const popH = p ? p.offsetHeight : 0;
      const popW = p ? p.offsetWidth  : 0;
      const vh = window.innerHeight, vw = window.innerWidth;
      const margin = 8;
      // Vertical: prefer below; flip above if it'd overflow the viewport AND there's more room above.
      const spaceBelow = vh - r.bottom - offset;
      const spaceAbove = r.top - offset;
      const flipUp = popH > 0 && spaceBelow < popH && spaceAbove > spaceBelow;
      let top = flipUp ? Math.max(margin, r.top - offset - popH) : r.bottom + offset;
      // If still overflowing (popover larger than entire viewport), pin to top with margin.
      if (popH > 0 && top + popH > vh - margin) top = Math.max(margin, vh - popH - margin);
      // Horizontal: anchor edge based on alignment, then clamp into viewport.
      let left = align === "bottom-right" ? r.right - popW : r.left;
      if (popW > 0) {
        if (left + popW > vw - margin) left = Math.max(margin, vw - popW - margin);
        if (left < margin) left = margin;
      }
      setPos({ top, left, measured: popH > 0 && popW > 0 });
    };
    // First pass renders off-screen so we can measure.
    place();
    // Second pass once the popover has mounted with real content (measure + reposition).
    const t = requestAnimationFrame(place);
    window.addEventListener("resize", place);
    window.addEventListener("scroll", place, true);
    return () => { cancelAnimationFrame(t); window.removeEventListener("resize", place); window.removeEventListener("scroll", place, true); };
  }, [anchorRef, align, offset]);
  useEffect(() => {
    const onDocDown = (e) => {
      if (popRef.current?.contains(e.target)) return;
      if (anchorRef.current?.contains(e.target)) return;
      onClose();
    };
    const onKey = (e) => { if (e.key === "Escape") onClose(); };
    document.addEventListener("mousedown", onDocDown);
    document.addEventListener("keydown", onKey);
    return () => { document.removeEventListener("mousedown", onDocDown); document.removeEventListener("keydown", onKey); };
  }, [anchorRef, onClose]);
  // Render off-screen on first paint (so measurement works), then snap into the real spot.
  const style = pos
    ? { position:"fixed", top:pos.top, left:pos.left, zIndex:10000, ...(minWidth?{minWidth}:{}), visibility: pos.measured ? "visible" : "hidden" }
    : { position:"fixed", top:-9999, left:-9999, zIndex:10000, visibility:"hidden" };
  return ReactDOM.createPortal(<div ref={popRef} style={style}>{children}</div>, document.body);
}

function MergeTagInput({ value, onChange, onInsertTag, multiline, style, placeholder, ...rest }) {
  const ref = useRef(null);
  const [pop, setPop] = useState(null); // { triggerStart, filter } | null
  const [hi, setHi] = useState(0);
  const all = ALL_MERGE_TAGS;
  const filtered = pop ? all.filter(t => t.key.toLowerCase().includes(pop.filter.toLowerCase()) || t.desc.toLowerCase().includes(pop.filter.toLowerCase())).slice(0, 8) : [];
  const checkTrigger = (text, caret) => {
    // Look back from caret for the most recent "#" not preceded by a letter/digit. If we find
    // one with only word chars + underscore between it and the caret, we're in autocomplete mode.
    const before = text.slice(0, caret);
    const m = before.match(/(^|[\s\n])(#[A-Za-z0-9_]*)$/);
    if (!m) return null;
    return { triggerStart: caret - m[2].length, filter: m[2].slice(1) };
  };
  const onChg = (e) => {
    const v = e.target.value;
    onChange(v);
    const caret = e.target.selectionStart || v.length;
    const trig = checkTrigger(v, caret);
    setPop(trig);
    setHi(0);
  };
  const pick = (tag) => {
    if (!pop) return;
    const v = value || "";
    const before = v.slice(0, pop.triggerStart);
    const after = v.slice((pop.triggerStart + 1 + pop.filter.length));
    const next = `${before}{${tag.key}}${after}`;
    onChange(next);
    setPop(null);
    if (onInsertTag) onInsertTag(tag.key);
    setTimeout(() => { if (ref.current) ref.current.focus(); }, 0);
  };
  const onKey = (e) => {
    if (!pop || filtered.length === 0) return;
    if (e.key === "ArrowDown") { e.preventDefault(); setHi(h => Math.min(h+1, filtered.length-1)); }
    else if (e.key === "ArrowUp") { e.preventDefault(); setHi(h => Math.max(h-1, 0)); }
    else if (e.key === "Enter" || e.key === "Tab") { e.preventDefault(); pick(filtered[hi]); }
    else if (e.key === "Escape") { e.preventDefault(); setPop(null); }
  };
  const Comp = multiline ? "textarea" : "input";
  return (
    <div style={{position:"relative",width:"100%"}}>
      <Comp ref={ref} value={value||""} onChange={onChg} onKeyDown={onKey} placeholder={placeholder} style={style} {...rest}/>
      {pop && filtered.length > 0 && (
        <Popover anchorRef={ref} onClose={()=>setPop(null)} align="bottom-left" minWidth={260}>
          <div style={{background:C.panel,border:`1px solid ${C.accent}55`,borderRadius:8,padding:5,boxShadow:"0 8px 26px #000a",minWidth:260,maxWidth:360}}>
            <div style={{fontSize:9,fontWeight:800,color:C.dim,letterSpacing:".06em",padding:"4px 7px 6px"}}>MERGE TAGS · ESC to close</div>
            {filtered.map((t, i) => (
              <div key={t.key} onClick={()=>pick(t)} onMouseEnter={()=>setHi(i)} style={{display:"flex",alignItems:"center",gap:7,padding:"6px 8px",borderRadius:5,cursor:"pointer",background:hi===i?"#162035":"transparent"}}>
                <span style={{fontSize:11,color:"#facc15",fontFamily:"ui-monospace,monospace",fontWeight:700,flexShrink:0}}>{`{${t.key}}`}</span>
                <span style={{fontSize:10,color:C.muted,overflow:"hidden",textOverflow:"ellipsis",whiteSpace:"nowrap"}}>{t.desc}</span>
              </div>
            ))}
          </div>
        </Popover>
      )}
    </div>
  );
}

// All supported gradient directions/styles. Each maps to the CSS used at build time and
// shows a directional arrow icon so users can pick by visual intuition.
const GRADIENT_STYLES = [
  // 3×3 grid: corners are diagonals, edges are cardinals, center is radial.
  { id:"tlbr",   icon:"↘", label:"Top-left → bottom-right" },
  { id:"bt",     icon:"↑", label:"Bottom → top" },
  { id:"bltr",   icon:"↗", label:"Bottom-left → top-right" },
  { id:"lr",     icon:"→", label:"Left → right" },
  { id:"radial", icon:"⊙", label:"Radial (center outward)" },
  { id:"rl",     icon:"←", label:"Right → left" },
  { id:"brtl",   icon:"↖", label:"Bottom-right → top-left" },
  { id:"tb",     icon:"↓", label:"Top → bottom" },
  { id:"trbl",   icon:"↙", label:"Top-right → bottom-left" },
];
const STYLE_TO_ANGLE = { lr:90, rl:270, tb:180, bt:0, tlbr:135, trbl:225, bltr:45, brtl:315 };
function buildGradientCss(style, a, b) {
  if (style === "radial") return `radial-gradient(circle, ${a} 0%, ${b} 100%)`;
  return `linear-gradient(${STYLE_TO_ANGLE[style] ?? 90}deg, ${a} 0%, ${b} 100%)`;
}
// Build a tile-able pattern data URL. Three modes:
//   • "emoji" — text glyph centered in an SVG tile
//   • "word"  — short word/phrase rendered as text in an SVG tile
//   • "image" (logo) — uploaded image embedded inside an SVG tile, sized to ~55% of the tile
//      so there's visible spacing around each logo (matches the emoji behavior).
function buildPatternDataUrl({ type, emoji, image, word, size = 60, opacity = 0.15 }) {
  const safe = (s) => String(s||"").replace(/&/g,"&amp;").replace(/</g,"&lt;").replace(/>/g,"&gt;");
  if (type === "image" && image) {
    const inner = Math.round(size * 0.55);
    const offset = Math.round((size - inner) / 2);
    // preserveAspectRatio:"xMidYMid meet" keeps the logo proportional and centered in the slot.
    const svg = `<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="${size}" height="${size}"><image href="${image}" xlink:href="${image}" x="${offset}" y="${offset}" width="${inner}" height="${inner}" opacity="${opacity}" preserveAspectRatio="xMidYMid meet"/></svg>`;
    return `data:image/svg+xml;utf8,${encodeURIComponent(svg)}`;
  }
  if (type === "emoji" && emoji) {
    const fs = Math.round(size * 0.55);
    const svg = `<svg xmlns="http://www.w3.org/2000/svg" width="${size}" height="${size}"><text x="50%" y="55%" text-anchor="middle" dominant-baseline="middle" font-size="${fs}" opacity="${opacity}">${safe(emoji)}</text></svg>`;
    return `data:image/svg+xml;utf8,${encodeURIComponent(svg)}`;
  }
  if (type === "word" && word) {
    // Pick a font-size that fits the word within ~80% of tile width. Shorter strings get larger
    // text. Bold sans-serif looks cleanest at small sizes. Uppercase glyphs are wider than
    // lowercase so we use a higher per-char width factor for all-caps strings → smaller font
    // → more horizontal margin between tiled instances.
    const trimmed = String(word).trim();
    const len = Math.max(1, trimmed.length);
    const isAllCaps = /[A-Z]/.test(trimmed) && trimmed === trimmed.toUpperCase();
    const charW = isAllCaps ? 0.78 : 0.55;
    const fs = Math.max(8, Math.min(Math.round(size * 0.42), Math.round((size * 0.8) / (len * charW))));
    const svg = `<svg xmlns="http://www.w3.org/2000/svg" width="${size}" height="${size}"><text x="50%" y="55%" text-anchor="middle" dominant-baseline="middle" font-size="${fs}" opacity="${opacity}" font-family="Helvetica,Arial,sans-serif" font-weight="700" fill="#1e293b">${safe(word)}</text></svg>`;
    return `data:image/svg+xml;utf8,${encodeURIComponent(svg)}`;
  }
  return "";
}

// Universal emoji set — restricted to glyphs available in Unicode 11 (2018) or earlier so they
// render correctly on every reasonably current OS, browser, and phone. Deduped by character.
const EMOJI_PICKER_RAW = [
  // Smileys & faces
  ["😀","grinning happy face"],["😃","smiling face"],["😄","beaming happy"],["😁","grinning teeth"],["😆","laughing"],["😅","sweat smile"],["🤣","rolling laughing rofl"],["😂","tears of joy laughing"],["🙂","slight smile"],["🙃","upside down face"],
  ["😉","wink"],["😊","smiling blush"],["😇","angel halo innocent"],["🥰","smiling hearts love"],["😍","heart eyes love"],["🤩","star eyes excited"],["😘","kiss face"],["😗","kissing"],["😚","kiss closed eyes"],["😙","kiss smile"],
  ["🥲","smiling tear"],["😋","yummy savor food"],["😛","tongue stuck out"],["😜","wink tongue"],["🤪","zany crazy"],["😝","squint tongue"],["🤑","money mouth"],["🤗","hugging"],["🤭","hand over mouth oops"],["🤫","shush quiet"],
  ["🤔","thinking"],["🤐","zipper mouth quiet"],["🤨","raised eyebrow"],["😐","neutral face"],["😑","expressionless"],["😶","no mouth speechless"],["😏","smirk"],["😒","unamused"],["🙄","eye roll"],["😬","grimace"],
  ["🤥","lying"],["😌","relieved"],["😔","pensive sad"],["😪","sleepy"],["🤤","drooling"],["😴","sleeping"],["😷","mask sick"],["🤒","thermometer sick"],["🤕","bandage hurt"],["🤢","nauseated"],
  ["🤮","vomit"],["🤧","sneezing"],["🥵","hot heat"],["🥶","cold freezing"],["🥴","woozy"],["😵","dizzy"],["🤯","mind blown explode"],["🤠","cowboy"],["🥳","party face celebrate"],["🥸","disguised"],
  ["😎","cool sunglasses"],["🤓","nerd"],["🧐","monocle inspect"],["😕","confused"],["😟","worried"],["🙁","slight frown"],["☹️","frown"],["😮","open mouth surprised"],["😯","hushed"],["😲","astonished"],
  ["😳","flushed"],["🥺","pleading"],["😦","frown open"],["😧","anguished"],["😨","fearful"],["😰","anxious sweat"],["😥","sad relief"],["😢","crying tear"],["😭","sobbing"],["😱","scream fear"],
  ["😖","confounded"],["😣","persevering"],["😞","disappointed"],["😓","downcast sweat"],["😩","weary"],["😫","tired"],["🥱","yawn"],["😤","triumph proud"],["😡","red angry"],["😠","angry"],
  ["🤬","cursing"],["😈","devil smile"],["👿","angry devil"],["💀","skull"],["☠️","skull crossbones"],["💩","poop"],["🤡","clown"],["👹","ogre"],["👺","goblin"],["👻","ghost"],
  ["👽","alien"],["👾","monster"],["🤖","robot"],
  // Hearts & symbols
  ["❤️","red heart love"],["🧡","orange heart"],["💛","yellow heart"],["💚","green heart"],["💙","blue heart"],["💜","purple heart"],["🖤","black heart"],["🤍","white heart"],["🤎","brown heart"],["💔","broken heart"],
  ["❣️","heart exclamation"],["💕","two hearts"],["💞","revolving hearts"],["💓","beating heart"],["💗","growing heart"],["💖","sparkling heart"],["💘","heart arrow cupid"],["💝","heart ribbon gift"],["💟","heart decoration"],
  ["☮️","peace"],["✝️","cross"],["☪️","star crescent"],["🕉️","om"],["☸️","wheel dharma"],["✡️","star david"],["🔯","six pointed star"],["🕎","menorah"],["☯️","yin yang"],
  ["♈","aries"],["♉","taurus"],["♊","gemini"],["♋","cancer"],["♌","leo"],["♍","virgo"],["♎","libra"],["♏","scorpio"],["♐","sagittarius"],["♑","capricorn"],["♒","aquarius"],["♓","pisces"],
  ["⛎","ophiuchus"],["🆔","id"],["⚛️","atom"],["🉑","accept"],["☢️","radioactive"],["☣️","biohazard"],["📴","mobile off"],["📳","vibration"],["🈶","not free"],["🈚","free"],["🈸","application"],["🈺","open business"],["🈷️","monthly"],
  ["✴️","sparkle"],["🆚","versus vs"],["💮","white flower"],["🉐","bargain"],["㊙️","secret"],["㊗️","congratulations"],["🈴","passing"],["🈵","no vacancy"],["🈹","discount"],["🈲","prohibited"],["🅰️","a button blood"],["🅱️","b button blood"],
  ["🆎","ab button blood"],["🆑","cl button"],["🅾️","o button blood"],["🆘","sos"],["❌","cross mark no"],["⭕","big circle"],["🛑","stop sign"],["⛔","no entry"],["📛","name badge"],["🚫","prohibited"],["💯","100 hundred"],
  ["💢","anger"],["♨️","hot springs"],["🚷","no pedestrians"],["🚯","no littering"],["🚳","no bicycles"],["🚱","not potable"],["🔞","no under 18"],["📵","no phones"],["🚭","no smoking"],["❗","exclamation"],["❕","white exclamation"],
  ["❓","question"],["❔","white question"],["‼️","double exclamation"],["⁉️","interrobang"],["🔅","dim"],["🔆","bright"],["〽️","part alternation"],["⚠️","warning"],["🚸","children crossing"],["🔱","trident"],["⚜️","fleur de lis"],
  ["🔰","japanese symbol beginner"],["♻️","recycle"],["✅","check mark"],["🈯","reserved"],["💹","chart yen"],["❇️","sparkle"],["✳️","eight spoked"],["❎","cross button"],["🌐","globe meridians"],["💠","diamond dot"],["Ⓜ️","circled m"],
  ["🌀","cyclone spiral"],["💤","zzz sleep"],["🏧","atm"],["🚾","wc"],["♿","wheelchair access"],["🅿️","p parking"],["🈳","vacancy"],["🛂","passport control"],["🛃","customs"],["🛄","baggage claim"],["🛅","luggage locker"],
  ["🚹","mens"],["🚺","womens"],["🚼","baby symbol"],["🚻","restroom"],["🚮","litter bin"],["🎦","cinema"],["📶","signal bars"],["🈁","here japanese"],["🔣","input symbols"],["ℹ️","information"],["🔤","input latin"],["🔡","abcd lowercase"],
  ["🔠","abcd uppercase"],["🆖","ng no good"],["🆗","ok button"],["🆙","up button"],["🆒","cool button"],["🆕","new button"],["🆓","free button"],
  ["0️⃣","zero"],["1️⃣","one"],["2️⃣","two"],["3️⃣","three"],["4️⃣","four"],["5️⃣","five"],["6️⃣","six"],["7️⃣","seven"],["8️⃣","eight"],["9️⃣","nine"],["🔟","ten"],
  ["🔢","numbers"],["#️⃣","hash"],["*️⃣","asterisk"],["⏏️","eject"],["▶️","play"],["⏸️","pause"],["⏯️","play pause"],["⏹️","stop"],["⏺️","record"],["⏭️","next track"],["⏮️","last track"],["⏩","fast forward"],["⏪","rewind"],
  ["⏫","fast up"],["⏬","fast down"],["◀️","play reverse"],["🔼","up arrow"],["🔽","down arrow"],["➡️","right arrow"],["⬅️","left arrow"],["⬆️","up arrow"],["⬇️","down arrow"],["↗️","up right arrow"],["↘️","down right arrow"],
  ["↙️","down left arrow"],["↖️","up left arrow"],["↕️","up down arrow"],["↔️","left right arrow"],["↪️","left arrow curving right"],["↩️","right arrow curving left"],["⤴️","up arrow curve"],["⤵️","down arrow curve"],["🔀","shuffle"],
  ["🔁","repeat"],["🔂","repeat single"],["🔄","arrows counterclockwise"],["🔃","clockwise"],["🎵","music note"],["🎶","musical notes"],["➕","plus"],["➖","minus"],["➗","divide"],["✖️","multiply"],["♾️","infinity"],
  ["💲","heavy dollar"],["💱","currency exchange"],["™️","trademark"],["©️","copyright"],["®️","registered"],["〰️","wavy dash"],["➰","curly loop"],["➿","double curly loop"],["🔚","end"],["🔙","back"],["🔛","on"],["🔝","top"],["🔜","soon"],
  ["✔️","check mark"],["☑️","check box"],["🔘","radio button"],["🔴","red circle"],["🟠","orange circle"],["🟡","yellow circle"],["🟢","green circle"],["🔵","blue circle"],["🟣","purple circle"],["⚫","black circle"],["⚪","white circle"],
  ["🟤","brown circle"],["🟥","red square"],["🟧","orange square"],["🟨","yellow square"],["🟩","green square"],["🟦","blue square"],["🟪","purple square"],["⬛","black square"],["⬜","white square"],["🟫","brown square"],["◼️","black medium"],
  ["◻️","white medium"],["◾","black small"],["◽","white small"],["▪️","black tiny"],["▫️","white tiny"],["🔶","large orange diamond"],["🔷","large blue diamond"],["🔸","small orange diamond"],["🔹","small blue diamond"],
  ["🔺","red triangle up"],["🔻","red triangle down"],["💠","diamond dot blue"],["🔲","black square button"],["🔳","white square button"],
  // Sparkles & celebration
  ["⭐","star"],["🌟","glowing star"],["✨","sparkles"],["💫","dizzy"],["🎉","party tada"],["🎊","confetti"],["🎁","gift present"],["🎈","balloon"],["🎂","birthday cake"],["🎀","ribbon bow"],["🎄","christmas tree"],["🎃","jack o lantern"],
  ["🎆","fireworks"],["🎇","sparkler"],["🧨","firecracker"],["🪅","piñata"],["🎏","carp streamer"],["🎐","wind chime"],["🎑","moon viewing"],["🎒","backpack"],["🎓","graduation"],["🎗️","reminder ribbon"],["🎟️","admission ticket"],
  ["🎫","ticket"],["🎠","carousel horse"],["🎡","ferris wheel"],["🎢","roller coaster"],
  // Awards & status
  ["🏆","trophy"],["🥇","gold medal"],["🥈","silver medal"],["🥉","bronze medal"],["🎖️","military medal"],["🏅","sports medal"],["🎽","running shirt"],["🎯","target bullseye"],["🎳","bowling"],["⛳","golf flag"],
  // Tools & business
  ["🚀","rocket launch"],["⚡","lightning bolt"],["💡","idea bulb"],["🔥","fire trending"],["💎","diamond gem"],["📈","chart up"],["📉","chart down"],["📊","bar chart"],["💰","money bag"],["💵","dollar bills"],["💴","yen bills"],
  ["💶","euro bills"],["💷","pound bills"],["💸","money flying"],["💳","credit card"],["💼","briefcase"],["🗝️","old key"],["🔑","key"],["📅","calendar date"],["📆","calendar"],["⏰","alarm clock"],["⏱️","stopwatch"],
  ["⏲️","timer"],["⌛","hourglass done"],["⏳","hourglass"],["🕰️","mantelpiece clock"],["⌚","watch"],
  // Communication
  ["📞","phone"],["☎️","telephone"],["📟","pager"],["📠","fax"],["📡","satellite"],["📺","tv"],["📻","radio"],["🔊","speaker loud"],["🔉","speaker medium"],["🔈","speaker low"],["🔇","muted"],["🔕","bell off"],["🔔","bell"],
  ["📢","loudspeaker"],["📣","megaphone"],["📯","postal horn"],["🎙️","studio mic"],["🎚️","level slider"],["🎛️","control knobs"],["🎤","microphone"],["🎧","headphones"],["📱","phone mobile"],["📲","calling"],
  ["✉️","envelope mail"],["📧","email mail at"],["📨","incoming email mail"],["📩","outgoing email mail"],["📤","outbox tray"],["📥","inbox tray"],["📬","mailbox open mail"],["📭","mailbox closed mail"],["📮","postbox mail"],["📫","mailbox flag mail"],["📪","mailbox no flag mail"],
  ["💬","speech bubble"],["🗨️","left speech"],["🗯️","angry speech"],["💭","thought bubble"],
  // Tech
  ["💻","laptop"],["🖥️","desktop"],["🖨️","printer"],["⌨️","keyboard"],["🖱️","mouse"],["🖲️","trackball"],["💽","minidisc"],["💾","floppy disk"],["💿","cd"],["📀","dvd"],["🧮","abacus"],["🎥","movie camera"],
  ["🎞️","film frames"],["📽️","projector"],["📷","camera"],["📸","camera flash"],["📹","video camera"],["📼","videocassette"],["🔍","magnifying right"],["🔎","magnifying left"],["🕯️","candle"],["💡","light bulb"],
  ["🔦","flashlight"],["🏮","red lantern"],["🪔","diya lamp"],
  // Books & writing
  ["📓","notebook"],["📔","decorative notebook"],["📕","red book"],["📖","open book"],["📗","green book"],["📘","blue book"],["📙","orange book"],["📚","books"],["📒","ledger"],["📃","page curl"],["📜","scroll"],["📄","page document"],
  ["📰","newspaper"],["🗞️","rolled newspaper"],["📑","tabbed pages"],["🔖","bookmark"],["🏷️","label tag"],["🪧","placard sign"],["📝","memo"],["✏️","pencil"],["✒️","pen nib"],["🖋️","fountain pen"],["🖊️","pen"],["🖌️","paintbrush"],["🖍️","crayon"],
  // Containers & shopping
  ["🛍️","shopping bags"],["🛒","shopping cart"],["🎒","bag pack"],["🧳","luggage"],["👛","clutch"],["👜","handbag"],["👝","pouch"],["💼","briefcase"],
  // People & gestures
  ["👋","wave hand"],["🤚","raised back"],["🖐️","hand fingers spread"],["✋","raised hand"],["🖖","vulcan"],["👌","ok"],["🤌","pinch"],["🤏","pinch small"],["✌️","peace fingers"],["🤞","crossed fingers"],["🤟","love you gesture"],
  ["🤘","sign of horns"],["🤙","call me"],["👈","point left"],["👉","point right"],["👆","point up index"],["🖕","middle finger"],["👇","point down"],["☝️","index up"],["👍","thumbs up"],["👎","thumbs down"],
  ["✊","raised fist"],["👊","oncoming fist"],["🤛","left fist"],["🤜","right fist"],["👏","clap"],["🙌","raising hands"],["👐","open hands"],["🤲","palms up"],["🤝","handshake"],["🙏","pray thank"],["✍️","writing hand"],
  ["💅","nails"],["🤳","selfie"],["💪","muscle bicep"],["🦾","mechanical arm"],["🦿","mechanical leg"],["🦵","leg"],["🦶","foot"],["👂","ear"],["🦻","hearing aid"],["👃","nose"],["🧠","brain"],["🫀","heart anatomy"],
  ["🫁","lungs"],["🦷","tooth"],["🦴","bone"],["👀","eyes"],["👁️","eye"],["👅","tongue"],["👄","lips"],
  // Animals
  ["🐶","dog face"],["🐱","cat face"],["🐭","mouse face"],["🐹","hamster"],["🐰","rabbit"],["🦊","fox"],["🐻","bear"],["🐼","panda"],["🐨","koala"],["🐯","tiger"],["🦁","lion"],["🐮","cow face"],["🐷","pig"],
  ["🐽","pig nose"],["🐸","frog"],["🐵","monkey face"],["🐔","chicken"],["🐧","penguin"],["🐦","bird"],["🐤","baby chick"],["🐣","hatching chick"],["🐥","front chick"],["🦆","duck"],["🦅","eagle"],["🦉","owl"],["🦇","bat"],
  ["🐺","wolf"],["🐗","boar"],["🐴","horse face"],["🦄","unicorn"],["🐝","bee"],["🐛","bug"],["🦋","butterfly"],["🐌","snail"],["🐞","lady beetle"],["🐜","ant"],["🦟","mosquito"],["🦗","cricket"],["🕷️","spider"],
  ["🕸️","spider web"],["🦂","scorpion"],["🐢","turtle"],["🐍","snake"],["🦎","lizard"],["🦖","t rex"],["🦕","sauropod"],["🐙","octopus"],["🦑","squid"],["🦐","shrimp"],["🦞","lobster"],["🦀","crab"],["🐡","blowfish"],
  ["🐠","tropical fish"],["🐟","fish"],["🐬","dolphin"],["🐳","spouting whale"],["🐋","whale"],["🦈","shark"],["🐊","crocodile"],["🐅","tiger"],["🐆","leopard"],["🦓","zebra"],["🦍","gorilla"],["🐘","elephant"],
  ["🦛","hippo"],["🦏","rhino"],["🐪","camel"],["🐫","two hump camel"],["🦒","giraffe"],["🦘","kangaroo"],["🐃","water buffalo"],["🐂","ox"],["🐄","cow"],["🐎","horse"],["🐖","pig"],["🐏","ram"],["🐑","sheep"],
  ["🦙","llama"],["🐐","goat"],["🦌","deer"],["🐕","dog"],["🐩","poodle"],["🦮","guide dog"],["🐕‍🦺","service dog"],["🐈","cat"],["🐓","rooster"],["🦃","turkey"],["🦚","peacock"],["🦜","parrot"],["🦢","swan"],
  ["🦩","flamingo"],["🕊️","dove"],["🐇","rabbit"],["🦝","raccoon"],["🦨","skunk"],["🦡","badger"],["🦫","beaver"],["🦦","otter"],["🦥","sloth"],["🐁","mouse"],["🐀","rat"],["🐿️","chipmunk"],["🐾","paw prints"],
  // Plants & nature
  ["💐","bouquet"],["🌸","cherry blossom"],["💮","white flower"],["🏵️","rosette"],["🌹","rose"],["🥀","wilted rose"],["🌺","hibiscus"],["🌻","sunflower"],["🌷","tulip"],["🌱","seedling"],["🌲","evergreen"],["🌳","deciduous"],
  ["🌴","palm tree"],["🌵","cactus"],["🌾","wheat"],["🌿","herb"],["☘️","shamrock"],["🍀","four leaf"],["🍁","maple leaf"],["🍂","fallen leaf"],["🍃","leaf wind"],
  // Sun & sky
  ["🌍","earth africa"],["🌎","earth americas"],["🌏","earth asia"],["🌐","globe meridians"],["🌑","new moon"],["🌒","waxing crescent"],["🌓","first quarter moon"],["🌔","waxing gibbous"],["🌕","full moon"],["🌖","waning gibbous"],
  ["🌗","last quarter"],["🌘","waning crescent"],["🌙","crescent moon"],["🌚","new moon face"],["🌛","first quarter face"],["🌜","last quarter face"],["☀️","sun"],["🌝","full moon face"],["🌞","sun with face"],
  ["🪐","ringed planet"],["⭐","star"],["🌟","glowing star"],["🌠","shooting star"],["🌌","milky way"],["☁️","cloud"],["⛅","partly sunny"],["⛈️","cloud lightning rain"],["🌤️","sun behind small cloud"],
  ["🌥️","sun behind large cloud"],["🌦️","sun behind rain"],["🌧️","cloud rain"],["🌨️","cloud snow"],["🌩️","cloud lightning"],["🌪️","tornado"],["🌫️","fog"],["🌬️","wind face"],["🌀","cyclone"],["🌈","rainbow"],
  ["🌂","umbrella"],["☂️","umbrella open"],["☔","umbrella rain"],["⚡","high voltage"],["❄️","snowflake"],["☃️","snowman snow"],["⛄","snowman"],["☄️","comet"],["🔥","fire"],["💧","droplet"],["🌊","wave"],
  // Food
  ["🍎","red apple"],["🍐","pear"],["🍊","orange"],["🍋","lemon"],["🍌","banana"],["🍉","watermelon"],["🍇","grapes"],["🍓","strawberry"],["🫐","blueberries"],["🍈","melon"],["🍒","cherries"],["🍑","peach"],["🥭","mango"],
  ["🍍","pineapple"],["🥥","coconut"],["🥝","kiwi"],["🍅","tomato"],["🍆","eggplant"],["🥑","avocado"],["🥦","broccoli"],["🥬","leafy green"],["🥒","cucumber"],["🌶️","hot pepper"],["🫑","bell pepper"],["🌽","corn"],
  ["🥕","carrot"],["🫒","olive"],["🧄","garlic"],["🧅","onion"],["🥔","potato"],["🍠","sweet potato"],["🥐","croissant"],["🥯","bagel"],["🍞","bread"],["🥖","baguette"],["🫓","flatbread"],["🥨","pretzel"],["🥞","pancakes"],
  ["🧇","waffle"],["🧀","cheese"],["🍖","meat on bone"],["🍗","poultry leg chicken"],["🥩","cut of meat steak"],["🥓","bacon"],["🍔","hamburger burger food"],["🍟","fries french potato food"],["🍕","pizza food"],["🌭","hot dog hotdog food"],["🥪","sandwich food"],["🌮","taco food"],
  ["🌯","burrito food"],["🫔","tamale food"],["🥙","stuffed flatbread"],["🧆","falafel"],["🥚","egg"],["🍳","cooking egg"],["🥘","shallow pan"],["🍲","pot of food"],["🫕","fondue"],["🥣","bowl spoon"],["🥗","green salad"],["🍿","popcorn corn"],
  ["🧈","butter"],["🧂","salt"],["🥫","canned food"],["🍱","bento"],["🍘","rice cracker"],["🍙","rice ball"],["🍚","cooked rice"],["🍛","curry rice"],["🍜","ramen noodles"],["🍝","spaghetti"],["🍠","roasted sweet potato"],
  ["🍢","oden"],["🍣","sushi"],["🍤","fried shrimp"],["🍥","fish cake"],["🥮","mooncake"],["🍡","dango"],["🥟","dumpling"],["🥠","fortune cookie"],["🥡","takeout box"],["🦀","crab"],["🦞","lobster food"],["🦐","shrimp food"],
  ["🦑","squid food"],["🦪","oyster"],["🍦","soft serve icecream"],["🍧","shaved ice"],["🍨","ice cream"],["🍩","donut doughnut"],["🍪","cookie"],["🎂","birthday cake"],["🍰","shortcake cake"],["🧁","cupcake cake"],["🥧","pie"],["🍫","chocolate bar"],
  ["🍬","candy"],["🍭","lollipop"],["🍮","custard"],["🍯","honey pot"],["🍼","baby bottle"],["🥛","milk"],["☕","coffee"],["🫖","teapot"],["🍵","tea"],["🍶","sake"],["🍾","bottle pop"],["🍷","wine"],["🍸","cocktail"],
  ["🍹","tropical drink"],["🍺","beer"],["🍻","clinking beers"],["🥂","clinking glasses"],["🥃","tumbler"],["🥤","cup straw"],["🧋","bubble tea"],["🧃","beverage box"],["🧉","mate"],["🧊","ice cube"],["🥢","chopsticks"],["🍽️","fork knife plate"],
  ["🍴","fork knife"],["🥄","spoon"],["🔪","kitchen knife"],["🏺","amphora"],
  // Travel & places
  ["🚗","car"],["🚕","taxi"],["🚙","suv"],["🚌","bus"],["🚎","trolleybus"],["🏎️","racing car"],["🚓","police car"],["🚑","ambulance"],["🚒","fire engine"],["🚐","minibus"],["🛻","pickup"],["🚚","delivery truck"],
  ["🚛","articulated truck"],["🚜","tractor"],["🦯","white cane"],["🦽","manual wheelchair"],["🦼","motorized wheelchair"],["🛴","kick scooter"],["🚲","bicycle"],["🛵","motor scooter"],["🏍️","motorcycle"],["🛺","auto rickshaw"],
  ["🚨","police light"],["🚔","oncoming police"],["🚍","oncoming bus"],["🚘","oncoming car"],["🚖","oncoming taxi"],["🚡","aerial tramway"],["🚠","mountain cableway"],["🚟","suspension railway"],["🚃","railway car"],
  ["🚋","tram"],["🚞","mountain railway"],["🚝","monorail"],["🚄","high speed train"],["🚅","bullet train"],["🚈","light rail"],["🚂","locomotive"],["🚆","train"],["🚇","metro"],["🚊","tram car"],["🚉","station"],
  ["✈️","airplane"],["🛫","airplane departure"],["🛬","airplane arrival"],["🛩️","small airplane"],["💺","seat"],["🛰️","satellite"],["🚀","rocket"],["🛸","ufo flying saucer"],["🚁","helicopter"],["🛶","canoe"],["⛵","sailboat"],
  ["🚤","speedboat"],["🛥️","motor boat"],["🛳️","passenger ship"],["⛴️","ferry"],["🚢","ship"],["⚓","anchor"],["⛽","fuel pump"],["🚧","construction"],["🚦","vertical traffic light"],["🚥","horizontal traffic light"],
  ["🗺️","world map"],["🗿","moai"],["🗽","statue of liberty"],["🗼","tokyo tower"],["🏰","castle"],["🏯","japanese castle"],["🏟️","stadium"],["🎡","ferris wheel"],["🎢","roller coaster"],["🎠","carousel"],["⛲","fountain"],
  ["⛱️","beach umbrella"],["🏖️","beach"],["🏝️","desert island"],["🏜️","desert"],["🌋","volcano"],["⛰️","mountain"],["🏔️","snow mountain"],["🗻","mount fuji"],["🏕️","camping"],["⛺","tent"],["🛖","hut"],["🏠","house"],
  ["🏡","house garden"],["🏘️","houses"],["🏚️","derelict house"],["🏗️","construction site"],["🏭","factory"],["🏢","office building"],["🏬","department store"],["🏣","japanese post office"],["🏤","european post office"],
  ["🏥","hospital"],["🏦","bank"],["🏨","hotel"],["🏪","convenience store"],["🏫","school"],["🏩","love hotel"],["💒","wedding chapel"],["🏛️","classical building"],["⛪","church"],["🕌","mosque"],["🛕","hindu temple"],
  ["🕍","synagogue"],["⛩️","shinto shrine"],["🕋","kaaba"],["⛲","fountain place"],["⛺","tent place"],
  // Activities & objects
  ["⚽","soccer"],["🏀","basketball"],["🏈","american football"],["⚾","baseball"],["🥎","softball"],["🎾","tennis"],["🏐","volleyball"],["🏉","rugby"],["🥏","frisbee"],["🎱","8 ball"],["🪀","yo yo"],["🏓","ping pong"],
  ["🏸","badminton"],["🥅","goal net"],["🏒","ice hockey"],["🏑","field hockey"],["🥍","lacrosse"],["🏏","cricket"],["🥌","curling stone"],["🛷","sled"],["🎿","skis"],["⛷️","skier"],["🏂","snowboarder"],["🏋️","weight lifting"],
  ["🤼","wrestling"],["🤸","cartwheel"],["⛹️","bouncing ball"],["🤺","fencing"],["🤾","handball"],["🏌️","golfing"],["🏇","horse racing"],["🧘","lotus position"],["🏄","surfing"],["🏊","swimming"],["🤽","water polo"],
  ["🚣","rowing boat"],["🧗","climbing"],["🚵","mountain biking"],["🚴","biking"],["🎬","clapper"],["🎨","artist palette"],["🎭","performing arts"],["🎪","circus tent"],["🎤","mic"],["🎧","headphones"],["🎼","musical score"],
  ["🎹","piano"],["🥁","drum"],["🎷","saxophone"],["🎺","trumpet"],["🎸","guitar"],["🪕","banjo"],["🎻","violin"],["🎲","game die"],["♟️","chess pawn"],["🎯","direct hit"],["🎳","bowling pin"],["🎮","video game"],
  ["🕹️","joystick"],["🎰","slot machine"],["🧩","puzzle"],
  // Symbols & misc
  ["🛎️","bellhop bell"],["🧳","luggage"],["⏳","hourglass"],["⏰","clock"],["🛍️","shopping bags"],["🧰","toolbox"],["🪛","screwdriver"],["🔧","wrench"],["🔨","hammer"],["⚒️","hammer pick"],["🛠️","hammer wrench"],
  ["⛏️","pick"],["🪓","axe"],["🪚","saw"],["🔩","nut bolt"],["⚙️","gear"],["🪝","hook"],["⛓️","chains"],["🪤","mouse trap"],["🧲","magnet"],["🔫","water gun"],["💣","bomb"],["🧨","firecracker"],["🪃","boomerang"],
  ["🏹","bow arrow"],["🛡️","shield"],["⚔️","crossed swords"],["🗡️","dagger"],["🚬","cigarette"],["⚰️","coffin"],["⚱️","funeral urn"],["🪦","headstone"],["🏺","amphora pot"],["💉","syringe"],["🩸","drop blood"],
  ["💊","pill"],["🩹","bandage"],["🩺","stethoscope"],["🩻","x-ray"],["🚪","door"],["🪟","window"],["🛏️","bed"],["🛋️","couch"],["🪑","chair"],["🚽","toilet"],["🪠","plunger"],["🚿","shower"],["🛁","bathtub"],
  ["🧴","lotion"],["🧷","safety pin"],["🧹","broom"],["🧺","basket"],["🧻","roll of paper"],["🪣","bucket"],["🧼","soap"],["🪥","toothbrush"],["🪒","razor"],["🧽","sponge"],["🪣","bucket pail"],["🧯","fire extinguisher"],
  ["🛒","shopping cart trolley"],
];
// Defensive dedupe by character — even if a glyph is listed twice with different keywords, it
// only renders once in the picker (and search merges its keywords).
const EMOJI_PICKER = (() => {
  const byChar = new Map();
  for (const [c, k] of EMOJI_PICKER_RAW) {
    if (byChar.has(c)) byChar.set(c, { c, k: `${byChar.get(c).k} ${k}` });
    else byChar.set(c, { c, k });
  }
  return [...byChar.values()];
})();

// React style object for the email canvas backdrop (solid color OR gradient, plus optional
// tiled pattern). Used directly in the editor canvas + preview modal.
function canvasBgStyle(f) {
  const bg = f.canvasBgGradient || f.canvasBg || "#f8fafc";
  const pattern = buildPatternDataUrl({ type: f.bgPatternType, emoji: f.bgPatternEmoji, image: f.bgPatternImage, word: f.bgPatternWord, size: f.bgPatternSize, opacity: f.bgPatternOpacity });
  const style = { background: bg };
  if (pattern) {
    // Use single-quoted url() so the value can be safely inlined into a double-quoted style
    // attribute when the body is persisted as an HTML string.
    style.backgroundImage = `url('${pattern}')`;
    style.backgroundRepeat = "repeat";
    // All three pattern types now use SVG tiles, so always honor the tile-size slider.
    if (f.bgPatternType === "emoji" || f.bgPatternType === "image" || f.bgPatternType === "word") {
      style.backgroundSize = `${f.bgPatternSize||60}px ${f.bgPatternSize||60}px`;
    }
  }
  return style;
}

// Convert hex (#rrggbb / #rgb) to rgba() with the given alpha so the content backdrop's
// opacity can sit semi-transparently over the wallpaper without affecting nested text.
function hexToRgba(hex, alpha) {
  if (!hex) return "";
  let s = String(hex).trim();
  if (/^#?[a-f\d]{3}$/i.test(s)) { s = s.replace("#",""); s = `${s[0]}${s[0]}${s[1]}${s[1]}${s[2]}${s[2]}`; }
  const m = s.match(/^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i);
  if (!m) return hex;
  const a = alpha == null ? 1 : Math.max(0, Math.min(1, alpha));
  return `rgba(${parseInt(m[1],16)},${parseInt(m[2],16)},${parseInt(m[3],16)},${a})`;
}

// Middle layer: sits between the wallpaper and the blocks. Solid color with adjustable
// opacity to improve text readability over busy wallpapers. Renders only when `contentBg`
// is set (otherwise blocks sit directly on the wallpaper).
// Has the user picked any wallpaper layer? Used to decide the email-card's default bg —
// when a wallpaper is set, the card needs an opaque background or the wallpaper bleeds
// through (the ring artifact). When there's no wallpaper, the card stays transparent.
function hasWallpaper(f) {
  return !!(f.canvasBg || f.canvasBgGradient || (f.bgPatternType && f.bgPatternType !== "none"));
}
function contentCardStyle(f) {
  if (!f.contentBg && !hasWallpaper(f)) return null;
  // Sharp corners — content card reads as a continuous column with the wallpaper, not
  // an inset card. First/last block sits flush against the top/bottom edge.
  return {
    background: f.contentBg ? hexToRgba(f.contentBg, f.contentBgOpacity ?? 1) : "#ffffff",
    boxSizing: "border-box",
    width: "100%",
  };
}
function contentCardCss(f) {
  if (!f.contentBg && !hasWallpaper(f)) return "";
  const bg = f.contentBg ? hexToRgba(f.contentBg, f.contentBgOpacity ?? 1) : "#ffffff";
  // Sharp corners + trailing semicolon (cascades correctly when concatenated with more
  // inline styles in the layered wrap).
  return `background:${bg};box-sizing:border-box;`;
}

// Same backdrop as a CSS string — used when wrapping the final email body HTML for persistence.
function canvasBgCss(f) {
  const s = canvasBgStyle(f);
  const out = [];
  if (s.background) out.push(`background:${s.background}`);
  if (s.backgroundImage) out.push(`background-image:${s.backgroundImage}`);
  if (s.backgroundRepeat) out.push(`background-repeat:${s.backgroundRepeat}`);
  if (s.backgroundSize) out.push(`background-size:${s.backgroundSize}`);
  return out.join(";");
}

function parseGradient(value) {
  if (!value || typeof value !== "string") return null;
  const linear = value.match(/linear-gradient\(\s*(\d+(?:\.\d+)?)deg\s*,\s*([^,\s]+)(?:\s+\d+%)?\s*,\s*([^,\s)]+)/i);
  if (linear) {
    const angle = Math.round(+linear[1]);
    const map = { 90:"lr", 270:"rl", 180:"tb", 0:"bt", 135:"tlbr", 225:"trbl", 45:"bltr", 315:"brtl" };
    return { colorA: linear[2], colorB: linear[3], style: map[angle] || "lr" };
  }
  const radial = value.match(/radial-gradient\([^,]*,\s*([^,\s]+)(?:\s+\d+%)?\s*,\s*([^,\s)]+)/i);
  if (radial) return { colorA: radial[1], colorB: radial[2], style: "radial" };
  return null;
}

// Gradient picker — opens a custom builder (two colors + 9 directions + preset shortcuts).
// Calls onPick(value) with the CSS gradient string (or "" to clear back to solid color).
function GradientPickerButton({ value, onPick }) {
  const [open, setOpen] = useState(false);
  const btnRef = useRef(null);
  const active = value && (value.startsWith("linear-gradient") || value.startsWith("radial-gradient"));
  return (
    <>
      <button ref={btnRef} onClick={(e)=>{ e.stopPropagation(); setOpen(o=>!o); }} title="Build a gradient" style={{width:48,height:24,borderRadius:5,border:`1px solid ${C.border}`,cursor:"pointer",background:active?value:"linear-gradient(90deg,#3b82f6,#7c3aed,#ec4899)",padding:0}}>{!active && <span style={{fontSize:9,color:"#fff",fontWeight:800,letterSpacing:".05em",textShadow:"0 1px 2px #000a"}}>GRAD</span>}</button>
      {open && (
        <Popover anchorRef={btnRef} onClose={()=>setOpen(false)} align="bottom-right">
          <GradientBuilder value={value} onPick={onPick} onClose={()=>setOpen(false)}/>
        </Popover>
      )}
    </>
  );
}

// Emoji wallpaper picker: searchable scrollable grid pulled from the (deduped) EMOJI_PICKER.
// Click any emoji to set. No manual text input — only search + select.
function EmojiPatternPicker({ value, onChange }) {
  const [q, setQ] = useState("");
  // Word-boundary match: typing "star" matches "star", "starlight", "stars" but not "custard".
  // Compared against each space-separated keyword via startsWith for clean prefix UX.
  const filtered = q.trim()
    ? EMOJI_PICKER.filter(e => {
        const ql = q.trim().toLowerCase();
        return e.k.toLowerCase().split(/\s+/).some(w => w.startsWith(ql));
      })
    : EMOJI_PICKER;
  return (
    <div style={{marginBottom:8}}>
      <input value={q} onChange={e=>setQ(e.target.value)} placeholder="🔍 Search emojis (star, gift, heart, money, food…)" style={inp({width:"100%",boxSizing:"border-box",fontSize:11,padding:"6px 10px",marginBottom:6})}/>
      <div style={{maxHeight:180,overflowY:"auto",background:"#090f1c",border:`1px solid ${C.border}`,borderRadius:7,padding:4,display:"grid",gridTemplateColumns:"repeat(8,1fr)",gap:2}}>
        {filtered.length === 0 && <div style={{gridColumn:"1/-1",fontSize:10,color:C.muted,textAlign:"center",padding:"12px 4px"}}>No matches.</div>}
        {filtered.map(e => {
          const sel = value === e.c;
          return (
            <button key={e.c} onClick={()=>onChange(e.c)} title={e.k} style={{background:sel?"#162035":"transparent",border:`1px solid ${sel?C.accent:"transparent"}`,borderRadius:5,padding:"5px 0",cursor:"pointer",fontSize:18,fontFamily:"Apple Color Emoji,Segoe UI Emoji,Noto Color Emoji,sans-serif",lineHeight:1}}>{e.c}</button>
          );
        })}
      </div>
      {value && <div style={{fontSize:10,color:C.muted,textAlign:"center",marginTop:5}}>Selected: <span style={{fontSize:16,fontFamily:"Apple Color Emoji,Segoe UI Emoji,sans-serif"}}>{value}</span></div>}
    </div>
  );
}

function GradientBuilder({ value, onPick, onClose }) {
  // Initialize from the existing gradient if it parses, else sensible defaults.
  const parsed = parseGradient(value);
  const [colorA, setColorA] = useState(parsed?.colorA || "#3b82f6");
  const [colorB, setColorB] = useState(parsed?.colorB || "#7c3aed");
  const [style,  setStyle]  = useState(parsed?.style  || "lr");
  // Apply helper — keeps state and the parent's `bgGradient` in sync on every change.
  const apply = (s, a, b) => onPick(buildGradientCss(s, a, b));
  const setA = (v) => { setColorA(v); apply(style, v, colorB); };
  const setB = (v) => { setColorB(v); apply(style, colorA, v); };
  const setS = (s) => { setStyle(s);  apply(s, colorA, colorB); };
  const swap = () => { setColorA(colorB); setColorB(colorA); apply(style, colorB, colorA); };
  const usePreset = (preset) => {
    const p = parseGradient(preset.value);
    if (p) { setColorA(p.colorA); setColorB(p.colorB); setStyle(p.style); }
    onPick(preset.value);
  };
  const clear = () => { onPick(""); onClose(); };
  const previewCss = buildGradientCss(style, colorA, colorB);
  return (
    <div style={{background:C.panel,border:`1px solid ${C.border}`,borderRadius:10,padding:11,boxShadow:"0 8px 26px #000a",width:280}}>
      {/* Live preview swatch */}
      <div style={{background:previewCss,height:54,borderRadius:8,border:`1px solid ${C.border}`,marginBottom:10}}/>

      {/* Two color pickers + swap */}
      <div style={{fontSize:9,fontWeight:800,color:C.dim,letterSpacing:".06em",marginBottom:5}}>COLORS</div>
      <div style={{display:"flex",alignItems:"center",gap:7,marginBottom:11}}>
        <div style={{flex:1,display:"flex",alignItems:"center",gap:6,background:"#090f1c",border:`1px solid ${C.border}`,borderRadius:7,padding:"5px 7px"}}>
          <input type="color" value={colorA} onChange={e=>setA(e.target.value)} style={{width:24,height:22,border:"none",borderRadius:4,cursor:"pointer",padding:0,background:"transparent"}}/>
          <span style={{fontSize:10,color:C.muted,fontFamily:"ui-monospace,monospace"}}>{colorA}</span>
        </div>
        <button onClick={swap} title="Swap colors" style={{...btn(C.dim,C.accent),padding:"4px 9px",fontSize:13}}>⇄</button>
        <div style={{flex:1,display:"flex",alignItems:"center",gap:6,background:"#090f1c",border:`1px solid ${C.border}`,borderRadius:7,padding:"5px 7px"}}>
          <input type="color" value={colorB} onChange={e=>setB(e.target.value)} style={{width:24,height:22,border:"none",borderRadius:4,cursor:"pointer",padding:0,background:"transparent"}}/>
          <span style={{fontSize:10,color:C.muted,fontFamily:"ui-monospace,monospace"}}>{colorB}</span>
        </div>
      </div>

      {/* Direction grid */}
      <div style={{fontSize:9,fontWeight:800,color:C.dim,letterSpacing:".06em",marginBottom:5}}>DIRECTION</div>
      <div style={{display:"grid",gridTemplateColumns:"repeat(3,1fr)",gap:5,marginBottom:11}}>
        {GRADIENT_STYLES.map(s => (
          <button key={s.id} onClick={()=>setS(s.id)} title={s.label} style={{height:38,borderRadius:6,border:style===s.id?`2px solid ${C.accent}`:`1px solid ${C.border}`,background:buildGradientCss(s.id, colorA, colorB),cursor:"pointer",padding:0,display:"flex",alignItems:"center",justifyContent:"center",position:"relative",overflow:"hidden"}}>
            <span style={{fontSize:18,color:"#fff",textShadow:"0 1px 3px #000a",fontWeight:700}}>{s.icon}</span>
          </button>
        ))}
      </div>

      {/* Presets row */}
      <div style={{fontSize:9,fontWeight:800,color:C.dim,letterSpacing:".06em",marginBottom:5}}>PRESETS</div>
      <div style={{display:"grid",gridTemplateColumns:"repeat(6,1fr)",gap:4,marginBottom:10}}>
        {GRADIENT_PRESETS.filter(p => p.value).map(g => (
          <button key={g.label} onClick={()=>usePreset(g)} title={g.label} style={{height:22,borderRadius:5,border:value===g.value?`2px solid ${C.accent}`:`1px solid ${C.border}`,background:g.value,cursor:"pointer",padding:0}}/>
        ))}
      </div>

      <div style={{display:"flex",alignItems:"center",gap:6}}>
        <div style={{flex:1,fontSize:9,color:C.dim,lineHeight:1.4}}>Some email clients fall back to a solid color.</div>
        <button onClick={clear} style={{...btn("#1a0808","#f87171"),fontSize:10,padding:"5px 10px"}} title="Clear gradient (use solid color instead)">Clear</button>
      </div>
    </div>
  );
}

// Territory list edit/delete menu — owns its own ref so the Popover can anchor to the pencil
// button. Replaces an absolute-positioned div that got clipped by the rail's scroll container.
function TerritoryRowMenu({ isOpen, onToggle, onClose, onEdit, onDelete }) {
  const btnRef = useRef(null);
  return (
    <>
      <button ref={btnRef} onClick={e=>{ e.stopPropagation(); onToggle(); }} title="Edit / delete" style={{...btn(C.dim,C.muted),padding:"2px 7px",fontSize:11}}>✏️</button>
      {isOpen && (
        <Popover anchorRef={btnRef} onClose={onClose} align="bottom-right">
          <div style={{background:C.panel,border:`1px solid ${C.border}`,borderRadius:8,padding:5,boxShadow:"0 8px 24px #000a",minWidth:140}}>
            <button onClick={onEdit}   style={{display:"block",width:"100%",textAlign:"left",background:"transparent",border:"none",color:C.text,padding:"7px 10px",borderRadius:5,cursor:"pointer",fontSize:12,fontFamily:"inherit"}} onMouseEnter={e=>e.currentTarget.style.background="#162035"} onMouseLeave={e=>e.currentTarget.style.background="transparent"}>✏️ Edit</button>
            <button onClick={onDelete} style={{display:"block",width:"100%",textAlign:"left",background:"transparent",border:"none",color:"#f87171",padding:"7px 10px",borderRadius:5,cursor:"pointer",fontSize:12,fontFamily:"inherit"}} onMouseEnter={e=>e.currentTarget.style.background="#1a0808"} onMouseLeave={e=>e.currentTarget.style.background="transparent"}>🗑 Delete</button>
          </div>
        </Popover>
      )}
    </>
  );
}

// "+" menu inside a row column — picks a block type to insert into that column.
function RowColumnAddMenu({ onPick }) {
  const [open, setOpen] = useState(false);
  const btnRef = useRef(null);
  return (
    <>
      <button ref={btnRef} onClick={(e)=>{ e.stopPropagation(); setOpen(o=>!o); }} title="Add a block to this column" style={{background:"transparent",border:`1.5px dashed ${C.accent}66`,borderRadius:"50%",width:40,height:40,display:"flex",alignItems:"center",justifyContent:"center",cursor:"pointer",color:C.accent,fontSize:22,fontWeight:500,fontFamily:"inherit",lineHeight:1,padding:0,transition:"all .12s"}} onMouseEnter={e=>{ e.currentTarget.style.borderColor = C.accent; e.currentTarget.style.background = "#162035"; }} onMouseLeave={e=>{ e.currentTarget.style.borderColor = C.accent + "66"; e.currentTarget.style.background = "transparent"; }}>+</button>
      {open && (
        <Popover anchorRef={btnRef} onClose={()=>setOpen(false)} align="bottom-left">
          <div style={{background:C.panel,border:`1px solid ${C.border}`,borderRadius:8,padding:5,boxShadow:"0 8px 26px #000a",minWidth:140}}>
            {/* Column-cell picker — every block except banner types and Columns itself
                (no nested rows). Logo + Footer included so reps can drop them inline. */}
            {[["logo","Logo"],["header","Heading"],["para","Paragraph"],["btn","Button"],["divider","Divider"],["spacer","Spacer"],["image","Image"],["footer","Footer"]].map(([k,lbl])=>(
              <button key={k} onClick={()=>{ onPick(k); setOpen(false); }} style={{display:"flex",alignItems:"center",width:"100%",background:"transparent",border:"none",borderRadius:5,padding:"6px 9px",cursor:"pointer",color:C.text,fontFamily:"inherit",textAlign:"left"}} onMouseEnter={e=>e.currentTarget.style.background="#162035"} onMouseLeave={e=>e.currentTarget.style.background="transparent"}>
                <span style={{fontSize:11,fontWeight:600}}>{lbl}</span>
              </button>
            ))}
          </div>
        </Popover>
      )}
    </>
  );
}

// Property form for a row block — choose between 2 and 3 columns.
function RowPropertyForm({ block, onChangeCols, onChange }) {
  const gap = block.gap ?? 12;
  return (
    <div>
      <div style={{fontSize:9,color:C.dim,fontWeight:700,marginBottom:5,letterSpacing:".04em"}}>COLUMNS</div>
      <div style={{display:"flex",gap:6}}>
        {[2,3].map(n => {
          const sel = (block.cols||2) === n;
          return <button key={n} onClick={()=>onChangeCols(n)} style={{flex:1,background:sel?"#162035":"transparent",border:`1.5px solid ${sel?C.accent:C.border}`,borderRadius:7,padding:"7px 0",fontSize:12,color:sel?C.accent:C.muted,cursor:"pointer",fontFamily:"inherit",fontWeight:sel?800:600}}>{n} columns</button>;
        })}
      </div>
      <div style={{marginTop:10}}>
        <div style={{fontSize:9,color:C.dim,fontWeight:700,marginBottom:5,letterSpacing:".04em",display:"flex",justifyContent:"space-between"}}>
          <span>COLUMN GAP</span><span style={{color:C.muted,fontWeight:600,letterSpacing:0,textTransform:"none"}}>{gap}px</span>
        </div>
        <input type="range" min={0} max={48} value={gap} onChange={e=>onChange&&onChange({gap: +e.target.value})} style={{width:"100%",accentColor:C.accent}}/>
      </div>
      <label style={{display:"flex",alignItems:"center",gap:7,cursor:"pointer",marginTop:9}}>
        <input type="checkbox" checked={!!block.noStack} onChange={e=>onChange&&onChange({noStack:e.target.checked})}/>
        <span style={{fontSize:11,color:C.text}}>Keep horizontal on mobile <span style={{color:C.muted,fontWeight:500}}>· don't stack columns under 480px</span></span>
      </label>
      <div style={{fontSize:10,color:C.dim,lineHeight:1.5,marginTop:7}}>Each column holds one block (image, paragraph, button…). Click the <strong style={{color:C.accent}}>+</strong> inside a column to add one.{block.cols === 3 ? ` Use the ⇆ merge buttons between cells (visible when the row is selected) to make one column span two — e.g. image on the left + a wider paragraph on the right.` : ""}</div>
    </div>
  );
}

// 4-directional arrow cross — used as the drag handle on template blocks so users immediately
// recognize it as a "grab and move in any direction" affordance instead of vertical dots.
function DragHandleIcon({ size = 22, color = "#475569" }) {
  return (
    <svg width={size} height={size} viewBox="0 0 24 24" fill="none" stroke={color} strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" style={{display:"block"}}>
      <line x1="12" y1="3" x2="12" y2="21"/>
      <line x1="3" y1="12" x2="21" y2="12"/>
      <polyline points="9,6 12,3 15,6"/>
      <polyline points="9,18 12,21 15,18"/>
      <polyline points="6,9 3,12 6,15"/>
      <polyline points="18,9 21,12 18,15"/>
    </svg>
  );
}

// Editable preview wrapper — renders block HTML via blocksToHtml (so styling stays in lockstep
// with the exported email), then walks the DOM after mount and makes the text-bearing element
// contentEditable. On user input, emits a patch back to the block. This is the "type directly
// inside the block builder" experience the rep sees on the canvas.
// Sanitize HTML emitted from contentEditable. Allows only inline formatting tags +
// span/br so a paste can't smuggle in scripts/styles, while preserving the user's
// per-selection B/I/U/strikethrough markup.
const ALLOWED_INLINE_TAGS = new Set(["B","STRONG","I","EM","U","S","STRIKE","BR","SPAN","FONT","A"]);
function sanitizeRichHtml(html) {
  if (!html) return "";
  const tmp = document.createElement("div");
  tmp.innerHTML = html;
  const walk = (node) => {
    [...node.childNodes].forEach(child => {
      if (child.nodeType === 1) {
        if (!ALLOWED_INLINE_TAGS.has(child.tagName)) {
          // Replace disallowed element with its text contents.
          const text = document.createTextNode(child.textContent || "");
          node.replaceChild(text, child);
        } else {
          // Strip all attributes except style for color/decoration and href on <a>.
          [...child.attributes].forEach(a => {
            if (child.tagName === "A" && a.name === "href") return;
            if (a.name === "style") return;
            child.removeAttribute(a.name);
          });
          walk(child);
        }
      } else if (child.nodeType !== 3) {
        node.removeChild(child);
      }
    });
  };
  walk(tmp);
  return tmp.innerHTML;
}

function EditableBlockHtml({ html, block, onChange }) {
  const ref = useRef(null);
  // Re-entry guard: when the user types, we trigger onChange which re-renders the parent
  // with a new `html` prop. Without this flag, we'd reset innerHTML on every keystroke and
  // the cursor would jump to the start. Setting `editingRef` true on input tells the next
  // effect run to skip the innerHTML refresh — the user's typed DOM stays put.
  const editingRef = useRef(false);
  const editableMap = {
    header: [["h1", "text"]],
    para:   [["p",  "text"]],
    btn:    [["a",  "text"]],
    banner: [["h2", "title"], ["p", "subtitle"]],
  };
  useEffect(() => {
    if (!ref.current) return;
    if (editingRef.current) { editingRef.current = false; return; }
    ref.current.innerHTML = html;
    const targets = editableMap[block.type] || [];
    targets.forEach(([sel, key]) => {
      const el = ref.current.querySelector(sel);
      if (!el) return;
      el.setAttribute("contenteditable", "true");
      el.setAttribute("spellcheck", "true");
      el.style.outline = "none";
      el.style.cursor = "text";
      // innerHTML so per-selection B/I/U/strikethrough (applied via execCommand) is captured.
      el.oninput = () => { editingRef.current = true; onChange({ [key]: sanitizeRichHtml(el.innerHTML) }); };
      el.onkeydown = (e) => { if (e.key === "Escape") el.blur(); };
      el.onpaste = (e) => {
        // Plain-text paste so foreign fonts/styles don't leak in.
        e.preventDefault();
        const text = (e.clipboardData || window.clipboardData).getData("text");
        document.execCommand("insertText", false, text);
      };
    });
  }, [html, block.type, block.id]); // eslint-disable-line
  return <div ref={ref}/>;
}

// Pad vertical / horizontal sliders — control inner padding around the block's content.
// For text blocks with a row background, these adjust the row-bg cell's padding. For other
// blocks, they're stored as block.padV/padH (consumers of those values use them as needed).
function PadControls({ block, onChange, defaults = { v: 18, h: 18 }, label = "INNER PADDING", lockH = false, lockHReason = "" }) {
  const pv = block.padV != null ? block.padV : defaults.v;
  const ph = block.padH != null ? block.padH : defaults.h;
  return (
    <div style={{marginTop:8,paddingTop:7,borderTop:`1px solid ${C.border}`}}>
      <div style={{fontSize:9,color:C.dim,fontWeight:700,marginBottom:5,letterSpacing:".04em"}}>{label}</div>
      <LabeledSlider label="VERTICAL"   val={pv} onChg={v=>onChange({padV:v})} max={80}/>
      <LabeledSlider label={lockH ? `HORIZONTAL · LOCKED${lockHReason?` · ${lockHReason}`:""}` : "HORIZONTAL"} val={ph} onChg={v=>onChange({padH:v})} max={80} disabled={lockH}/>
    </div>
  );
}

// Single-row labeled slider — defined at module scope so React doesn't unmount/remount
// it on every parent render (which was breaking the drag interaction in InsetControls).
function LabeledSlider({ label, val, onChg, max = 64, min = 0, disabled = false }) {
  return (
    <div style={{marginBottom:5,opacity:disabled?0.45:1}}>
      <div style={{display:"flex",justifyContent:"space-between",alignItems:"baseline",marginBottom:2}}>
        <span style={{fontSize:9,color:C.dim,fontWeight:700,letterSpacing:".04em"}}>{label}</span>
        <span style={{fontSize:10,color:C.muted}}>{val}px</span>
      </div>
      <input type="range" min={min} max={max} value={val} disabled={disabled} onChange={e=>onChg(+e.target.value)} style={{width:"100%",accentColor:C.accent,cursor:disabled?"not-allowed":"pointer"}}/>
    </div>
  );
}

// Gap-from-edges controls. Universal slider sets all four sides at once; the disclosure
// reveals individual top / sides / bottom sliders for fine-tuning. `lockSides` hides the
// side controls when the host context only allows vertical adjustment (e.g. square banner
// that already bleeds edge-to-edge).
function InsetControls({ baseKey, sidesKey, topKey, bottomKey, values, onChange, lockSides = false }) {
  const [showSides, setShowSides] = useState(values.top != null || values.bot != null || values.sides != null);
  const base = values.base != null ? values.base : 15;
  const top = values.top != null ? values.top : base;
  const bot = values.bot != null ? values.bot : base;
  const sides = values.sides != null ? values.sides : base;
  return (
    <div style={{marginTop:8,paddingTop:7,borderTop:`1px solid ${C.border}`}}>
      <LabeledSlider label={lockSides ? "VERTICAL GAP FROM EDGES" : "GAP FROM EDGES (ALL)"} val={base} onChg={v=>onChange({ [baseKey]: v, [topKey]: undefined, [bottomKey]: undefined, [sidesKey]: undefined })}/>
      <button onClick={()=>setShowSides(s=>!s)} style={{background:"transparent",border:"none",color:C.muted,fontSize:10,fontWeight:700,cursor:"pointer",fontFamily:"inherit",padding:"2px 0",letterSpacing:".04em"}}>{showSides ? "▼ FINE-TUNE PER SIDE" : "▶ FINE-TUNE PER SIDE"}</button>
      {showSides && (
        <div style={{paddingLeft:6,paddingTop:4,borderLeft:`2px solid ${C.border}`,marginLeft:1}}>
          <LabeledSlider label="TOP"    val={top}   onChg={v=>onChange({ [topKey]: v })}/>
          {!lockSides && <LabeledSlider label="SIDES" val={sides} onChg={v=>onChange({ [sidesKey]: v })}/>}
          <LabeledSlider label="BOTTOM" val={bot}   onChg={v=>onChange({ [bottomKey]: v })}/>
        </div>
      )}
    </div>
  );
}

function BlockPropertyForm({ block, onChange, onUploadImage, onInsertTag }) {
  const fld = (lbl, child) => <div style={{marginBottom:7}}><div style={{fontSize:9,color:C.dim,fontWeight:700,marginBottom:3,letterSpacing:".04em"}}>{lbl.toUpperCase()}</div>{child}</div>;
  const numInp = (val, onChg, range=[8,72]) => <input type="number" min={range[0]} max={range[1]} value={val||""} onChange={e=>onChg(+e.target.value)} style={inp({width:80,fontSize:11,padding:"5px 8px"})}/>;
  // Text inputs with merge-tag autocomplete (# trigger). Non-text fields still use a plain <input>.
  const txt = (val, onChg, placeholder, multiline) =>
    <MergeTagInput value={val} onChange={onChg} onInsertTag={onInsertTag} multiline={multiline} placeholder={placeholder} rows={multiline?2:undefined} style={inp({width:"100%",boxSizing:"border-box",fontSize:12,padding:multiline?undefined:"6px 10px",fontFamily:"inherit",resize:multiline?"vertical":undefined})}/>;
  const plainTxt = (val, onChg, placeholder) =>
    <input value={val||""} onChange={e=>onChg(e.target.value)} placeholder={placeholder} style={inp({width:"100%",boxSizing:"border-box",fontSize:12,padding:"6px 10px"})}/>;
  const alignPicker = (val, onChg) => <div style={{display:"flex",gap:3,background:"#090f1c",border:`1px solid ${C.border}`,borderRadius:6,padding:2}}>
    {[["left","⬅"],["center","↔"],["right","➡"]].map(([k,ic])=>(
      <button key={k} onClick={()=>onChg(k)} style={{flex:1,background:val===k?"#162035":"transparent",border:"none",borderRadius:4,padding:"4px 0",fontSize:11,color:val===k?C.accent:C.muted,cursor:"pointer",fontFamily:"inherit"}}>{ic}</button>
    ))}
  </div>;
  const colorInp = (val, onChg, def) => <input type="color" value={val||def||"#000000"} onChange={e=>onChg(e.target.value)} style={{width:32,height:24,border:"none",borderRadius:5,cursor:"pointer"}}/>;
  // Per-block row background — wraps the block in a colored "band". Rounded = inner card with
  // soft corners; Full bleed = sharp corners that extend horizontally to the email's edges.
  const rowBgControl = (
    <div style={{borderTop:`1px solid ${C.border}`,paddingTop:8,marginTop:9}}>
      <div style={{fontSize:9,color:C.dim,fontWeight:700,marginBottom:5,letterSpacing:".04em"}}>ROW BACKGROUND <span style={{color:C.muted,fontWeight:500,textTransform:"none",letterSpacing:0}}>· optional, fills left-to-right</span></div>
      <div style={{display:"flex",alignItems:"center",gap:8,marginBottom:6}}>
        <input type="color" value={block.blockBg||"#ffffff"} onChange={e=>{ onChange({blockBg:e.target.value, blockBgGradient:""}); }} style={{width:32,height:24,border:"none",borderRadius:5,cursor:"pointer"}} title="Solid row color"/>
        <GradientPickerButton value={block.blockBgGradient} onPick={v=>onChange({blockBgGradient:v})}/>
        <span style={{fontSize:10,color:C.muted,fontFamily:"ui-monospace,monospace",flex:1}}>{block.blockBgGradient ? "gradient" : (block.blockBg || "none")}</span>
        <button onClick={()=>onChange({blockBg:"",blockBgGradient:"",blockBgImage:""})} title="No row background" style={{...btn(C.dim,C.muted),padding:"3px 7px",fontSize:10}}>Clear</button>
      </div>
      <div style={{display:"flex",alignItems:"center",gap:6,marginBottom:6}}>
        <input type="text" value={block.blockBgImage||""} onChange={e=>onChange({blockBgImage:e.target.value})} placeholder="Background image URL (https://…)" style={inp({flex:1,fontSize:11,padding:"5px 8px"})}/>
        {block.blockBgImage && <button onClick={()=>onChange({blockBgImage:""})} title="Remove image" style={{...btn(C.dim,C.muted),padding:"3px 7px",fontSize:10}}>×</button>}
      </div>
      {block.blockBgImage && (
        <div style={{display:"flex",gap:6,marginBottom:6}}>
          <select value={block.blockBgPosition||"center center"} onChange={e=>onChange({blockBgPosition:e.target.value})} style={inp({flex:1,fontSize:11,padding:"5px 7px"})} title="Image position">
            <option value="center top">Top</option>
            <option value="center center">Center</option>
            <option value="center bottom">Bottom</option>
            <option value="left top">Top-left</option>
            <option value="right top">Top-right</option>
            <option value="left bottom">Bottom-left</option>
            <option value="right bottom">Bottom-right</option>
          </select>
          <select value={block.blockBgSize||"cover"} onChange={e=>onChange({blockBgSize:e.target.value})} style={inp({width:90,fontSize:11,padding:"5px 7px"})} title="Image size">
            <option value="cover">Cover</option>
            <option value="contain">Contain</option>
            <option value="auto">Original</option>
            <option value="100% 100%">Stretch</option>
          </select>
          <select value={block.blockBgRepeat||"no-repeat"} onChange={e=>onChange({blockBgRepeat:e.target.value})} style={inp({width:90,fontSize:11,padding:"5px 7px"})} title="Repeat">
            <option value="no-repeat">No repeat</option>
            <option value="repeat">Tile</option>
            <option value="repeat-x">Tile horiz</option>
            <option value="repeat-y">Tile vert</option>
          </select>
        </div>
      )}
      {(block.blockBg || block.blockBgGradient || block.blockBgImage) && (
        <>
          <div style={{display:"flex",gap:3,background:"#090f1c",border:`1px solid ${C.border}`,borderRadius:6,padding:2}}>
            {[["square","▭ Full bleed","Sharp corners — fills horizontally to the backdrop edges"],["rounded","◐ Rounded","Soft corners with adjustable gap from the content background edges"]].map(([k,l,tip])=>{
              const sel = (block.blockBgCorner||"square") === k;
              return <button key={k} onClick={()=>onChange({blockBgCorner:k})} title={tip} style={{flex:1,background:sel?"#162035":"transparent",border:"none",borderRadius:4,padding:"4px 0",fontSize:10,color:sel?C.accent:C.muted,cursor:"pointer",fontFamily:"inherit",fontWeight:sel?700:500}}>{l}</button>;
            })}
          </div>
          {(block.blockBgCorner||"square") === "rounded" && (
            <InsetControls
              baseKey="blockBgInset" sidesKey="blockBgInsetSides" topKey="blockBgInsetTop" bottomKey="blockBgInsetBottom"
              values={{ base: block.blockBgInset, top: block.blockBgInsetTop, bot: block.blockBgInsetBottom, sides: block.blockBgInsetSides }}
              onChange={(patch)=>onChange(patch)}
            />
          )}
        </>
      )}
    </div>
  );
  const bgPicker = (solidVal, gradientVal, onSolid, onGradient) => (
    <div style={{display:"flex",alignItems:"center",gap:6}}>
      {colorInp(solidVal, v=>{ onGradient(""); onSolid(v); })}
      <GradientPickerButton value={gradientVal} onPick={onGradient}/>
    </div>
  );
  // Style toggle button used by the B/I/U toolbar — onMouseDown.preventDefault keeps the
  // canvas contentEditable focused (otherwise focus jumps to the button and the cursor moves).
  const styleBtn = (active, onClick, label, title, extraStyle = {}) => (
    <button onMouseDown={e=>e.preventDefault()} onClick={onClick} title={title} style={{background:active?"#162035":"#090f1c",border:`1px solid ${active?C.accent+"88":C.border}`,borderRadius:6,padding:"5px 10px",color:active?C.accent:C.text,fontFamily:"inherit",cursor:"pointer",fontSize:13,minWidth:30,...extraStyle}}>{label}</button>
  );
  const fontPicker = (block) => (
    <div style={{display:"flex",flexDirection:"column",gap:6}}>
      <div style={{display:"flex",gap:6}}>
        <select value={block.fontFamily||""} onChange={e=>onChange({fontFamily:e.target.value})} style={inp({flex:1,fontSize:11,padding:"5px 7px",minWidth:0,fontFamily:block.fontFamily||"inherit"})} title="Font family">
          <option value="">Default font</option>
          <optgroup label="Email-safe">
            {FONT_FAMILIES.filter(f=>f.category==="safe").map(f=><option key={f.label} value={f.value} style={{fontFamily:f.value}}>{f.label}</option>)}
          </optgroup>
          <optgroup label="Modern (may fall back)">
            {FONT_FAMILIES.filter(f=>f.category==="modern").map(f=><option key={f.label} value={f.value} style={{fontFamily:f.value}}>{f.label}</option>)}
          </optgroup>
        </select>
        <select value={block.fontWeight||""} onChange={e=>onChange({fontWeight:e.target.value?+e.target.value:undefined})} style={inp({width:110,fontSize:11,padding:"5px 7px"})} title="Font weight">
          <option value="">Weight</option>
          {FONT_WEIGHTS.map(w=><option key={w.value} value={w.value}>{w.label}</option>)}
        </select>
      </div>
      <div style={{display:"flex",gap:5,alignItems:"center"}}>
        {styleBtn(false, ()=>document.execCommand("bold"),          "B", "Bold (selection)",          {fontWeight:900})}
        {styleBtn(false, ()=>document.execCommand("italic"),        "I", "Italic (selection)",        {fontStyle:"italic",fontWeight:700})}
        {styleBtn(false, ()=>document.execCommand("underline"),     "U", "Underline (selection)",     {textDecoration:"underline",fontWeight:700})}
        {styleBtn(false, ()=>document.execCommand("strikeThrough"), "S", "Strikethrough (selection)", {textDecoration:"line-through",fontWeight:700})}
        <span style={{fontSize:10,color:C.dim,marginLeft:6}}>Highlight text on the canvas, then click</span>
      </div>
    </div>
  );
  switch (block.type) {
    case "banner": return <>
      {fld("Title", txt(block.title, v=>onChange({title:v}), "Big Announcement"))}
      {fld("Subtitle", txt(block.subtitle, v=>onChange({subtitle:v}), "Supporting subhead"))}
      {fld("Font", fontPicker(block))}
      {fld("Shape", (
        <div style={{display:"flex",gap:3,background:"#090f1c",border:`1px solid ${C.border}`,borderRadius:6,padding:2}}>
          {[["square","▭ Square","Sharp corners — full-bleed rectangle that fills the edges of the content backdrop (and top/bottom when first/last block)"],["rounded","◐ Rounded","Soft 12px corners — inset card with adjustable gap from the content background edges"]].map(([k,l,tip])=>{
            const sel = (block.cornerStyle||"square") === k;
            return <button key={k} onClick={()=>onChange({cornerStyle:k})} title={tip} style={{flex:1,background:sel?"#162035":"transparent",border:"none",borderRadius:4,padding:"5px 0",fontSize:11,color:sel?C.accent:C.muted,cursor:"pointer",fontFamily:"inherit",fontWeight:sel?700:500}}>{l}</button>;
          })}
        </div>
      ))}
      {/* Square mode → vertical-only gap-from-edges (sides always bleed to email edges).
          Rounded → all four sides adjustable. */}
      <InsetControls
        baseKey="inset" sidesKey="insetSides" topKey="insetTop" bottomKey="insetBottom"
        values={{ base: block.inset, top: block.insetTop, bot: block.insetBottom, sides: block.insetSides }}
        lockSides={(block.cornerStyle||"square") !== "rounded"}
        onChange={(patch)=>onChange(patch)}
      />
      <div style={{display:"flex",gap:8}}>
        <div style={{flex:1.2}}>{fld("Background", bgPicker(block.bgColor, block.bgGradient, v=>onChange({bgColor:v}), v=>onChange({bgGradient:v})))}</div>
        <div style={{flex:1}}>{fld("Text color", colorInp(block.color, v=>onChange({color:v}), "#ffffff"))}</div>
      </div>
      <div style={{display:"flex",gap:8}}>
        <div style={{flex:1}}>{fld("Title size", numInp(block.fontSize||24, v=>onChange({fontSize:v}), [12,48]))}</div>
        <div style={{flex:1}}>{fld("Subtitle size", numInp(block.subtitleFontSize||14, v=>onChange({subtitleFontSize:v}), [10,28]))}</div>
      </div>
      <PadControls block={block} onChange={onChange} defaults={{v:32, h:20}} label="INNER PADDING" lockH={(block.cornerStyle||"square") !== "rounded"} lockHReason="bleeds to edges"/>
    </>;
    case "header": {
      // Horizontal padding only makes sense when:
      //  - the block has a rounded row background (acts like a card with its own inner H pad), or
      //  - the text is left- or right-aligned (then H padding shifts the visible text).
      // For centered text on a square/no-bg block, H padding has no visual effect and
      // would be confusing, so we lock it.
      const hasRowBg_h = block.blockBg || block.blockBgGradient || block.blockBgImage;
      const isRounded = (block.blockBgCorner || "square") === "rounded";
      const lockH = (block.align === "center") && !(hasRowBg_h && isRounded);
      const onAlignChange = (v) => onChange({ align: v, padV: undefined, padH: undefined });
      return <>
        {fld("Heading text", txt(block.text, v=>onChange({text:v}), "Your headline"))}
        {fld("Font", fontPicker(block))}
        <div style={{display:"flex",gap:8}}>
          <div style={{flex:1}}>{fld("Align", alignPicker(block.align, onAlignChange))}</div>
          <div style={{flex:1}}>{fld("Size", numInp(block.fontSize||28, v=>onChange({fontSize:v}), [14,80]))}</div>
          <div style={{flex:1}}>{fld("Color", colorInp(block.color, v=>onChange({color:v}), "#1e293b"))}</div>
        </div>
        <div style={{display:"flex",gap:8}}>
          <div style={{flex:1}}>{fld("Line height", <input type="number" step="0.05" min="0.8" max="3" value={block.lineHeight||""} placeholder="auto" onChange={e=>onChange({lineHeight: e.target.value ? +e.target.value : null})} style={inp({width:"100%",boxSizing:"border-box",fontSize:11,padding:"5px 8px"})}/>)}</div>
          <div style={{flex:1}}>{fld("Mobile size", <input type="number" min="10" max="80" value={block.fontSizeMobile||""} placeholder="auto" onChange={e=>onChange({fontSizeMobile: e.target.value ? +e.target.value : null})} style={inp({width:"100%",boxSizing:"border-box",fontSize:11,padding:"5px 8px"})}/>)}</div>
        </div>
        <PadControls block={block} onChange={onChange} defaults={{v:18, h:18}} lockH={lockH} lockHReason="text centered"/>
        {rowBgControl}
      </>;
    }
    case "para": {
      const hasRowBg_p = block.blockBg || block.blockBgGradient || block.blockBgImage;
      const isRounded_p = (block.blockBgCorner || "square") === "rounded";
      const lockH_p = (block.align === "center") && !(hasRowBg_p && isRounded_p);
      const onAlignChange_p = (v) => onChange({ align: v, padV: undefined, padH: undefined });
      return <>
        {fld("Paragraph text", txt(block.text, v=>onChange({text:v}), "Body copy", true))}
        {fld("Font", fontPicker(block))}
        <div style={{display:"flex",gap:8}}>
          <div style={{flex:1}}>{fld("Align", alignPicker(block.align, onAlignChange_p))}</div>
          <div style={{flex:1}}>{fld("Size", numInp(block.fontSize||15, v=>onChange({fontSize:v}), [10,48]))}</div>
          <div style={{flex:1}}>{fld("Color", colorInp(block.color, v=>onChange({color:v}), "#475569"))}</div>
        </div>
        <div style={{display:"flex",gap:8}}>
          <div style={{flex:1}}>{fld("Line height", <input type="number" step="0.05" min="0.8" max="3" value={block.lineHeight||""} placeholder="auto" onChange={e=>onChange({lineHeight: e.target.value ? +e.target.value : null})} style={inp({width:"100%",boxSizing:"border-box",fontSize:11,padding:"5px 8px"})}/>)}</div>
          <div style={{flex:1}}>{fld("Mobile size", <input type="number" min="10" max="48" value={block.fontSizeMobile||""} placeholder="auto" onChange={e=>onChange({fontSizeMobile: e.target.value ? +e.target.value : null})} style={inp({width:"100%",boxSizing:"border-box",fontSize:11,padding:"5px 8px"})}/>)}</div>
        </div>
        <PadControls block={block} onChange={onChange} defaults={{v:18, h:18}} lockH={lockH_p} lockHReason="text centered"/>
        {rowBgControl}
      </>;
    }
    case "btn": {
      const cornerMap = { sharp: 2, soft: 8, pill: 99 };
      const curRadius = block.btnRadius != null ? block.btnRadius : (cornerMap[block.btnCornerStyle] != null ? cornerMap[block.btnCornerStyle] : 8);
      return <>
        {fld("Button label", txt(block.text, v=>onChange({text:v}), "Call to Action"))}
        {fld("URL", plainTxt(block.url, v=>onChange({url:v}), "https://"))}
        {fld("Font", fontPicker(block))}
        <div style={{display:"flex",gap:8}}>
          <div style={{flex:1}}>{fld("Align", alignPicker(block.align, v=>onChange({align:v})))}</div>
          <div style={{flex:1.2}}>{fld("Background", bgPicker(block.bgColor, block.bgGradient, v=>onChange({bgColor:v}), v=>onChange({bgGradient:v})))}</div>
          <div style={{flex:1}}>{fld("Text color", colorInp(block.color, v=>onChange({color:v}), "#ffffff"))}</div>
        </div>
        <div style={{fontSize:9,color:C.dim,fontWeight:700,marginBottom:5,letterSpacing:".04em",display:"flex",justifyContent:"space-between"}}>
          <span>CORNER RADIUS</span><span style={{color:C.muted,fontWeight:600,letterSpacing:0,textTransform:"none"}}>{curRadius}px</span>
        </div>
        <div style={{display:"flex",gap:5,marginBottom:5}}>
          {[["sharp","Sharp",2],["soft","Soft",8],["pill","Pill",99]].map(([k,l,v])=>{
            const sel = curRadius === v;
            return <button key={k} onClick={()=>onChange({btnCornerStyle:k, btnRadius:v})} style={{flex:1,background:sel?"#162035":"#090f1c",border:`1px solid ${sel?C.accent+"66":C.border}`,borderRadius:6,padding:"5px 0",fontSize:11,color:sel?C.accent:C.muted,cursor:"pointer",fontFamily:"inherit",fontWeight:sel?800:600}}>{l} ({v}px)</button>;
          })}
        </div>
        <input type="range" min={0} max={99} value={curRadius} onChange={e=>onChange({btnRadius:+e.target.value, btnCornerStyle:undefined})} style={{width:"100%",accentColor:C.accent,marginBottom:8}}/>
        <div style={{display:"flex",gap:8,marginBottom:5}}>
          <div style={{flex:1}}>{fld("Pad vertical", numInp(block.btnPadV||13, v=>onChange({btnPadV:v}), [4,40]))}</div>
          <div style={{flex:1}}>{fld("Pad horizontal", numInp(block.btnPadH||30, v=>onChange({btnPadH:v}), [4,80]))}</div>
        </div>
        {rowBgControl}
      </>;
    }
    case "divider": {
      const style = block.style || "line";
      return <>
        <div style={{fontSize:9,color:C.dim,fontWeight:700,marginBottom:5,letterSpacing:".04em"}}>STYLE</div>
        <div style={{display:"flex",gap:5,marginBottom:8,flexWrap:"wrap"}}>
          {[["line","Solid"],["dashed","Dashed"],["dotted","Dotted"],["double","Double"],["image","Image"]].map(([k,l])=>{
            const sel = style === k;
            return <button key={k} onClick={()=>onChange({style:k})} style={{flex:"1 1 60px",background:sel?"#162035":"#090f1c",border:`1px solid ${sel?C.accent+"66":C.border}`,borderRadius:6,padding:"5px 0",fontSize:11,color:sel?C.accent:C.muted,cursor:"pointer",fontFamily:"inherit",fontWeight:sel?800:600}}>{l}</button>;
          })}
        </div>
        {style === "image" && (
          <div style={{marginBottom:7}}>
            {fld("Image URL", plainTxt(block.imageSrc, v=>onChange({imageSrc:v}), "https://… (decorative border PNG)"))}
          </div>
        )}
        <div style={{display:"flex",gap:8}}>
          {style !== "image" && <div style={{flex:1}}>{fld("Line color", colorInp(block.color, v=>onChange({color:v}), "#e2e8f0"))}</div>}
          {style !== "image" && style !== "double" && <div style={{flex:1}}>{fld("Thickness", numInp(block.thickness||1, v=>onChange({thickness:v}), [1,12]))}</div>}
          <div style={{flex:1}}>{fld("Spacing", numInp(block.padding||28, v=>onChange({padding:v}), [4,80]))}</div>
        </div>
      </>;
    }
    case "logo": return <>
      {fld("Logo URL", plainTxt(block.src, v=>onChange({src:v}), "https://… your logo image"))}
      {fld("Alt text", plainTxt(block.alt, v=>onChange({alt:v}), "Logo"))}
      <div style={{fontSize:9,color:C.dim,fontWeight:700,marginBottom:5,letterSpacing:".04em",display:"flex",justifyContent:"space-between"}}>
        <span>WIDTH</span><span style={{color:C.muted,fontWeight:600,letterSpacing:0,textTransform:"none"}}>{block.width||96}px</span>
      </div>
      <input type="range" min={24} max={400} value={block.width||96} onChange={e=>onChange({width:+e.target.value})} style={{width:"100%",accentColor:C.accent,marginBottom:5}}/>
      <div style={{display:"flex",gap:5,marginBottom:8}}>
        {[24,48,96,120,200].map(w => (
          <button key={w} onClick={()=>onChange({width:w})} style={{flex:1,background:(block.width||96)===w?"#162035":"#090f1c",border:`1px solid ${(block.width||96)===w?C.accent+"66":C.border}`,borderRadius:6,padding:"4px 0",fontSize:11,color:(block.width||96)===w?C.accent:C.muted,cursor:"pointer",fontFamily:"inherit",fontWeight:(block.width||96)===w?800:600}}>{w}</button>
        ))}
      </div>
      <div style={{display:"flex",gap:8}}>
        <div style={{flex:1}}>{fld("Align", alignPicker(block.align, v=>onChange({align:v})))}</div>
        <div style={{flex:1}}>{fld("Bottom space", numInp(block.padding||16, v=>onChange({padding:v}), [0,60]))}</div>
      </div>
    </>;
    case "footer": return <>
      {fld("Company name", txt(block.companyName, v=>onChange({companyName:v}), "Your Company"))}
      {fld("Company address", txt(block.companyAddress, v=>onChange({companyAddress:v}), "Street, City, ST"))}
      <label style={{display:"flex",alignItems:"center",gap:7,cursor:"pointer",marginBottom:7}}>
        <input type="checkbox" checked={block.showUnsubscribe !== false} onChange={e=>onChange({showUnsubscribe:e.target.checked})}/>
        <span style={{fontSize:11,color:C.text}}>Show Unsubscribe / Report-abuse links</span>
      </label>
      <div style={{display:"flex",gap:8}}>
        <div style={{flex:1}}>{fld("Align", alignPicker(block.align, v=>onChange({align:v})))}</div>
        <div style={{flex:1}}>{fld("Size", numInp(block.fontSize||12, v=>onChange({fontSize:v}), [9,16]))}</div>
        <div style={{flex:1}}>{fld("Color", colorInp(block.color, v=>onChange({color:v}), "#767676"))}</div>
      </div>
      {rowBgControl}
    </>;
    case "spacer": {
      const h = block.height ?? 24;
      return <>
        <div style={{fontSize:9,color:C.dim,fontWeight:700,marginBottom:5,letterSpacing:".04em",display:"flex",justifyContent:"space-between"}}>
          <span>HEIGHT</span><span style={{color:C.muted,fontWeight:600,letterSpacing:0,textTransform:"none"}}>{h}px</span>
        </div>
        <input type="range" min={4} max={120} value={h} onChange={e=>onChange({height: +e.target.value})} style={{width:"100%",accentColor:C.accent}}/>
        <div style={{display:"flex",gap:5,marginTop:6}}>
          {[8,16,24,40,64].map(preset => (
            <button key={preset} onClick={()=>onChange({height: preset})} style={{flex:1,background:h===preset?"#162035":"#090f1c",border:`1px solid ${h===preset?C.accent+"66":C.border}`,borderRadius:6,padding:"4px 0",fontSize:11,color:h===preset?C.accent:C.muted,cursor:"pointer",fontFamily:"inherit",fontWeight:h===preset?800:600}}>{preset}</button>
          ))}
        </div>
        <div style={{marginTop:11,paddingTop:9,borderTop:`1px solid ${C.border}`}}>
          <div style={{fontSize:9,color:C.dim,fontWeight:700,marginBottom:5,letterSpacing:".04em"}}>BACKGROUND COLOR <span style={{color:C.muted,fontWeight:500,textTransform:"none",letterSpacing:0}}>· optional</span></div>
          <div style={{display:"flex",alignItems:"center",gap:8}}>
            <input type="color" value={block.bgColor||"#ffffff"} onChange={e=>onChange({bgColor:e.target.value})} style={{width:32,height:24,border:"none",borderRadius:5,cursor:"pointer"}}/>
            <span style={{fontSize:11,color:C.muted,fontFamily:"ui-monospace,monospace",flex:1}}>{block.bgColor || "transparent"}</span>
            <button onClick={()=>onChange({bgColor:""})} title="No background" style={{...btn(C.dim,C.muted),padding:"3px 7px",fontSize:10}}>Clear</button>
          </div>
        </div>
      </>;
    }
    case "image": {
      // Align does nothing when the image fills the container (Fit) or extends past it
      // (Bleed), so we hide the Align row for those modes to reduce noise.
      const w = block.width || "fit";
      const hideAlign = w === "fit" || w === "bleed";
      return <>
        {fld("Image source", <div style={{display:"flex",gap:6,alignItems:"center"}}>
          <input value={block.src||""} onChange={e=>onChange({src:e.target.value})} placeholder="Paste URL or upload →" style={inp({flex:1,minWidth:0,fontSize:11,padding:"6px 10px"})}/>
          <button onClick={onUploadImage} style={{...btn(C.dim,C.accent),fontSize:10,padding:"5px 10px",whiteSpace:"nowrap"}}>📤 Upload</button>
        </div>)}
        {fld("Size", (
          <div style={{display:"flex",flexDirection:"column",gap:5}}>
            <div style={{display:"flex",gap:3,background:"#090f1c",border:`1px solid ${C.border}`,borderRadius:6,padding:2}}>
              {[["small","S"],["medium","M"],["large","L"],["fit","Fit"],["bleed","Bleed"],["custom","Custom"]].map(([k,l])=>{
                const sel = (block.width||"fit") === k;
                return <button key={k} onClick={()=>onChange({width:k})} style={{flex:1,background:sel?"#162035":"transparent",border:"none",borderRadius:4,padding:"4px 0",fontSize:10,color:sel?C.accent:C.muted,cursor:"pointer",fontFamily:"inherit",fontWeight:sel?700:500}} title={({small:"25% of container",medium:"50% of container",large:"75% of container",fit:"100% of container",bleed:"Full bleed — extends past the email padding to the edges (great for branded banner uploads)",custom:"Custom pixel width"}[k])}>{l}</button>;
              })}
            </div>
            {block.width === "custom" && (
              <div style={{display:"flex",alignItems:"center",gap:6}}>
                <input type="number" min="40" max="800" value={block.widthCustom||300} onChange={e=>onChange({widthCustom:+e.target.value||300})} style={inp({width:90,fontSize:11,padding:"5px 8px"})}/>
                <span style={{fontSize:10,color:C.muted}}>px (max-width 100% on mobile)</span>
              </div>
            )}
          </div>
        ))}
        {!hideAlign && fld("Align", alignPicker(block.align||"center", v=>onChange({align:v})))}
        {fld("Link URL (clickable)", txt(block.url, v=>onChange({url:v}), "https:// (leave blank for non-clickable)"))}
        {fld("Alt text", txt(block.alt, v=>onChange({alt:v}), "Image description"))}
        {rowBgControl}
      </>;
    }
    case "imageBanner": return <>
      {fld("Banner image", <div style={{display:"flex",gap:6,alignItems:"center"}}>
        <input value={block.src||""} onChange={e=>onChange({src:e.target.value})} placeholder="Paste URL or upload →" style={inp({flex:1,minWidth:0,fontSize:11,padding:"6px 10px"})}/>
        <button onClick={onUploadImage} style={{...btn(C.dim,C.accent),fontSize:10,padding:"5px 10px",whiteSpace:"nowrap"}}>📤 Upload</button>
      </div>)}
      {fld("Link URL (clickable)", txt(block.url, v=>onChange({url:v}), "https:// (leave blank for non-clickable)"))}
      {fld("Alt text", txt(block.alt, v=>onChange({alt:v}), "Image description"))}
      <div style={{fontSize:10,color:C.dim,lineHeight:1.5,padding:"7px 4px 0",borderTop:`1px solid ${C.border}`,marginTop:4}}>This block fills the email edge-to-edge. Upload a pre-designed banner image and it'll fit the email width automatically. If it's the first or last block, it also extends to the top/bottom edges.</div>
    </>;
    default: return <div style={{fontSize:11,color:C.muted}}>This block type has no editable properties.</div>;
  }
}

function AutomationRuleBuilder({ initial, stages, leadStages, templates, brokers, onSave, onCancel }) {
  const [form, setForm] = useState(initial || { name:"", enabled:true, trigger:{type:"opp_stage_change",params:{}}, conditions:[], actions:[] });
  const set = (k,v) => setForm(f => ({...f, [k]: v}));
  const updateCond = (i,patch) => setForm(f => ({...f, conditions: f.conditions.map((c,ix)=>ix===i?{...c,...patch}:c)}));
  const addCond    = () => setForm(f => ({...f, conditions:[...f.conditions, {field:"stage",operator:"equals",value:""}]}));
  const removeCond = (i) => setForm(f => ({...f, conditions: f.conditions.filter((_,ix)=>ix!==i)}));
  const updateAct  = (i,patch) => setForm(f => ({...f, actions: f.actions.map((a,ix)=>ix===i?{...a,...patch}:a)}));
  const addAct     = () => setForm(f => ({...f, actions:[...f.actions, {type:"send_template",params:{}}]}));
  const removeAct  = (i) => setForm(f => ({...f, actions: f.actions.filter((_,ix)=>ix!==i)}));
  const isStageTrigger = form.trigger.type === "opp_stage_change" || form.trigger.type === "lead_stage_change";

  return (
    <div style={{display:"flex",flexDirection:"column",gap:14,maxWidth:760}}>
      <input value={form.name} onChange={e=>set("name",e.target.value)} placeholder="Automation name… (e.g. Notify broker on Discovery Day)" style={inp({fontSize:15,fontWeight:800})}/>

      <div style={{background:C.panel,border:`1px solid ${C.border}`,borderRadius:12,padding:"14px 16px"}}>
        <div style={{display:"flex",alignItems:"center",gap:10,marginBottom:10}}>
          <span style={{background:"#091420",color:"#60a5fa",border:"1px solid #60a5fa44",borderRadius:6,padding:"3px 10px",fontSize:11,fontWeight:800,letterSpacing:".08em"}}>WHEN</span>
          <select value={form.trigger.type} onChange={e=>set("trigger",{type:e.target.value,params:{}})} style={inp({flex:1})}>
            {AUTOMATION_TRIGGERS.map(t=><option key={t.id} value={t.id}>{t.label}</option>)}
          </select>
        </div>
        {form.trigger.type==="score_drops" && (
          <div style={{display:"flex",alignItems:"center",gap:8,fontSize:12,color:C.muted}}>
            Threshold: <input type="number" min="0" max="100" value={form.trigger.params?.threshold||50} onChange={e=>set("trigger",{...form.trigger,params:{...form.trigger.params,threshold:+e.target.value}})} style={inp({width:70})}/>
          </div>
        )}
        {form.trigger.type==="no_activity" && (
          <div style={{display:"flex",alignItems:"center",gap:8,fontSize:12,color:C.muted}}>
            Days: <input type="number" min="1" value={form.trigger.params?.days||7} onChange={e=>set("trigger",{...form.trigger,params:{...form.trigger.params,days:+e.target.value}})} style={inp({width:70})}/>
          </div>
        )}
        {isStageTrigger && (
          <div style={{display:"flex",alignItems:"center",gap:8,fontSize:12,color:C.muted,flexWrap:"wrap"}}>
            Stage equals:
            <select value={form.trigger.params?.stage||""} onChange={e=>set("trigger",{...form.trigger,params:{...form.trigger.params,stage:e.target.value}})} style={inp({flex:1,minWidth:160})}>
              <option value="">(any stage)</option>
              {(form.trigger.type==="opp_stage_change"?stages:leadStages).map(s=><option key={s.id} value={s.id}>{s.label}</option>)}
            </select>
          </div>
        )}
      </div>

      <div style={{background:C.panel,border:`1px solid ${C.border}`,borderRadius:12,padding:"14px 16px"}}>
        <div style={{display:"flex",alignItems:"center",gap:10,marginBottom:10}}>
          <span style={{background:"#1a1908",color:"#facc15",border:"1px solid #facc1544",borderRadius:6,padding:"3px 10px",fontSize:11,fontWeight:800,letterSpacing:".08em"}}>IF (optional)</span>
          <span style={{fontSize:11,color:C.muted,flex:1}}>{form.conditions.length===0?"No conditions — always fires when triggered.":`${form.conditions.length} condition${form.conditions.length===1?"":"s"} (all must match)`}</span>
          <button onClick={addCond} style={btn(C.dim,C.muted)}>+ Add Condition</button>
        </div>
        {form.conditions.map((c,i)=>(
          <div key={i} style={{display:"flex",gap:6,marginBottom:6}}>
            <input value={c.field} onChange={e=>updateCond(i,{field:e.target.value})} placeholder="field (e.g. score)" style={inp({flex:1})}/>
            <select value={c.operator} onChange={e=>updateCond(i,{operator:e.target.value})} style={inp({width:140})}>
              {CONDITION_OPS.map(op=><option key={op} value={op}>{op}</option>)}
            </select>
            <input value={c.value} onChange={e=>updateCond(i,{value:e.target.value})} placeholder="value" style={inp({flex:1})}/>
            <button onClick={()=>removeCond(i)} title="Remove condition" style={btn("#1a0808","#f87171")}>×</button>
          </div>
        ))}
      </div>

      <div style={{background:C.panel,border:`1px solid ${C.border}`,borderRadius:12,padding:"14px 16px"}}>
        <div style={{display:"flex",alignItems:"center",gap:10,marginBottom:10}}>
          <span style={{background:"#091c09",color:"#4ade80",border:"1px solid #4ade8044",borderRadius:6,padding:"3px 10px",fontSize:11,fontWeight:800,letterSpacing:".08em"}}>THEN</span>
          <span style={{fontSize:11,color:C.muted,flex:1}}>{form.actions.length===0?"No actions defined yet.":`${form.actions.length} action${form.actions.length===1?"":"s"}`}</span>
          <button onClick={addAct} style={btn(C.dim,C.muted)}>+ Add Action</button>
        </div>
        {form.actions.map((a,i)=>(
          <div key={i} style={{display:"flex",gap:6,marginBottom:6,flexWrap:"wrap",alignItems:"center"}}>
            <select value={a.type} onChange={e=>updateAct(i,{type:e.target.value,params:{}})} style={inp({minWidth:200})}>
              {AUTOMATION_ACTIONS.map(act=><option key={act.id} value={act.id}>{act.label}</option>)}
            </select>
            {a.type==="send_template" && (
              <select value={a.params?.templateId||""} onChange={e=>updateAct(i,{params:{...a.params,templateId:e.target.value}})} style={inp({flex:1,minWidth:180})}>
                <option value="">— pick a template —</option>
                {templates.map(t=><option key={t.id} value={t.id}>{t.name||t.label}</option>)}
              </select>
            )}
            {a.type==="change_stage" && (
              <select value={a.params?.stage||""} onChange={e=>updateAct(i,{params:{...a.params,stage:e.target.value}})} style={inp({flex:1,minWidth:180})}>
                <option value="">— pick a stage —</option>
                {stages.map(s=><option key={s.id} value={s.id}>{s.label}</option>)}
              </select>
            )}
            {a.type==="assign_broker" && (
              <select value={a.params?.brokerId||""} onChange={e=>updateAct(i,{params:{...a.params,brokerId:e.target.value}})} style={inp({flex:1,minWidth:180})}>
                <option value="">— pick a broker —</option>
                {brokers.map(b=><option key={b.id} value={b.id}>{b.firstName} {b.lastName}</option>)}
              </select>
            )}
            {a.type==="notify" && (
              <input value={a.params?.message||""} onChange={e=>updateAct(i,{params:{...a.params,message:e.target.value}})} placeholder="Notification message" style={inp({flex:1,minWidth:180})}/>
            )}
            {a.type==="flag" && (
              <input value={a.params?.flag||""} onChange={e=>updateAct(i,{params:{...a.params,flag:e.target.value}})} placeholder="Flag label (e.g. 'Needs attention')" style={inp({flex:1,minWidth:180})}/>
            )}
            <button onClick={()=>removeAct(i)} title="Remove action" style={btn("#1a0808","#f87171")}>×</button>
          </div>
        ))}
      </div>

      <div style={{display:"flex",alignItems:"center",gap:10}}>
        <label style={{fontSize:12,color:C.muted,display:"flex",alignItems:"center",gap:7,cursor:"pointer"}}>
          <input type="checkbox" checked={form.enabled} onChange={e=>set("enabled",e.target.checked)}/>
          Enable this rule
        </label>
        <div style={{marginLeft:"auto",display:"flex",gap:10}}>
          <button onClick={onCancel} style={btn(C.dim,C.muted)}>Cancel</button>
          <button onClick={()=>{ if(!form.name.trim()){ alert("Name is required."); return; } onSave(form); }} style={btn("#091c09","#4ade80",true)}>{initial?.id?"Save Changes":"Create Automation"}</button>
        </div>
      </div>
    </div>
  );
}

function NetworkForm({ initial, onSave, onCancel }) {
  const [form, setForm] = useState(initial || { name:"", website:"", phone:"", email:"", notes:"", color:"#a78bfa" });
  const set = (k,v) => setForm(f => ({...f, [k]: v}));
  return (
    <div style={{position:"fixed",inset:0,background:"#000c",zIndex:300,display:"flex",alignItems:"center",justifyContent:"center",padding:16}}>
      <div style={{background:C.panel,border:`1px solid ${C.border}`,borderRadius:18,padding:24,width:"100%",maxWidth:460}}>
        <h2 style={{margin:"0 0 16px",color:"#f0f6ff",fontSize:17,fontWeight:900}}>{initial?.id?"Edit Network":"Add Broker Network"}</h2>
        {[["name","Network Name (e.g. FranNet, IFPG)"],["website","Website"],["phone","Phone"],["email","Email"]].map(([k,l])=>(
          <div key={k} style={{marginBottom:10}}>
            <label style={{display:"block",fontSize:11,color:C.dim,marginBottom:3,fontWeight:700}}>{l}</label>
            <input value={form[k]||""} onChange={e=>set(k,e.target.value)} style={inp({width:"100%",boxSizing:"border-box"})}/>
          </div>
        ))}
        <div style={{display:"flex",gap:10,marginBottom:10,alignItems:"center"}}>
          <label style={{fontSize:11,color:C.dim,fontWeight:700}}>Color</label>
          <input type="color" value={form.color} onChange={e=>set("color",e.target.value)} style={{width:36,height:32,border:"none",borderRadius:6,cursor:"pointer"}}/>
        </div>
        <div style={{marginBottom:14}}>
          <label style={{display:"block",fontSize:11,color:C.dim,marginBottom:3,fontWeight:700}}>Notes</label>
          <textarea value={form.notes||""} onChange={e=>set("notes",e.target.value)} rows={2} style={{...inp({width:"100%",boxSizing:"border-box",resize:"vertical",fontFamily:"inherit"})}}/>
        </div>
        <div style={{display:"flex",gap:10,justifyContent:"flex-end"}}>
          <button onClick={onCancel} style={btn(C.dim,C.muted)}>Cancel</button>
          <button onClick={()=>{ if(!form.name.trim()){alert("Name required.");return;} onSave(form); }} style={btn("#091c09","#4ade80",true)}>{initial?.id?"Save Changes":"Add Network"}</button>
        </div>
      </div>
    </div>
  );
}

function BrokerForm({ initial, networks, onSave, onCancel }) {
  const [form, setForm] = useState(initial || { firstName:"", lastName:"", email:"", phone:"", networkId:"", specialty:"", commission:"", notes:"" });
  const set = (k,v) => setForm(f => ({...f, [k]: v}));
  return (
    <div style={{position:"fixed",inset:0,background:"#000c",zIndex:300,display:"flex",alignItems:"center",justifyContent:"center",padding:16}}>
      <div style={{background:C.panel,border:`1px solid ${C.border}`,borderRadius:18,padding:24,width:"100%",maxWidth:480}}>
        <h2 style={{margin:"0 0 16px",color:"#f0f6ff",fontSize:17,fontWeight:900}}>{initial?.id?"Edit Broker":"Add Broker / Consultant"}</h2>
        <div style={{display:"grid",gridTemplateColumns:"1fr 1fr",gap:9,marginBottom:9}}>
          {[["firstName","First Name"],["lastName","Last Name"],["email","Email"],["phone","Phone"]].map(([k,l])=>(
            <div key={k}>
              <label style={{display:"block",fontSize:11,color:C.dim,marginBottom:3,fontWeight:700}}>{l}</label>
              <input value={form[k]||""} onChange={e=>set(k,e.target.value)} style={inp({width:"100%",boxSizing:"border-box"})}/>
            </div>
          ))}
        </div>
        <div style={{marginBottom:9}}>
          <label style={{display:"block",fontSize:11,color:C.dim,marginBottom:3,fontWeight:700}}>Network</label>
          <select value={form.networkId||""} onChange={e=>set("networkId",e.target.value||null)} style={inp({width:"100%",boxSizing:"border-box"})}>
            <option value="">— Independent (no network) —</option>
            {networks.map(n=><option key={n.id} value={n.id}>{n.name}</option>)}
          </select>
        </div>
        <div style={{display:"grid",gridTemplateColumns:"1fr 1fr",gap:9,marginBottom:9}}>
          <div>
            <label style={{display:"block",fontSize:11,color:C.dim,marginBottom:3,fontWeight:700}}>Specialty</label>
            <input value={form.specialty||""} onChange={e=>set("specialty",e.target.value)} placeholder="e.g. QSR, fitness, services" style={inp({width:"100%",boxSizing:"border-box"})}/>
          </div>
          <div>
            <label style={{display:"block",fontSize:11,color:C.dim,marginBottom:3,fontWeight:700}}>Commission</label>
            <input value={form.commission||""} onChange={e=>set("commission",e.target.value)} placeholder="e.g. 50% of franchise fee" style={inp({width:"100%",boxSizing:"border-box"})}/>
          </div>
        </div>
        <div style={{marginBottom:14}}>
          <label style={{display:"block",fontSize:11,color:C.dim,marginBottom:3,fontWeight:700}}>Notes</label>
          <textarea value={form.notes||""} onChange={e=>set("notes",e.target.value)} rows={2} style={{...inp({width:"100%",boxSizing:"border-box",resize:"vertical",fontFamily:"inherit"})}}/>
        </div>
        <div style={{display:"flex",gap:10,justifyContent:"flex-end"}}>
          <button onClick={onCancel} style={btn(C.dim,C.muted)}>Cancel</button>
          <button onClick={()=>{ if(!form.firstName.trim() && !form.lastName.trim()){alert("Name required.");return;} onSave(form); }} style={btn("#091c09","#4ade80",true)}>{initial?.id?"Save Changes":"Add Broker"}</button>
        </div>
      </div>
    </div>
  );
}

// Inline clickable record name. Stops propagation so it doesn't double-fire parent row clicks.
// Format a YYYY-MM-DD due date relative to today. Returns { label, color }.
function formatDueDate(ymd) {
  if (!ymd) return { label: "no date", color: "#94a3b8" };
  const today = todayYMD();
  const todayMs = new Date(today).getTime(); const dueMs = new Date(ymd).getTime();
  const diff = Math.round((dueMs - todayMs) / 86400000);
  if (diff === 0) return { label: "Today", color: "#facc15" };
  if (diff === 1) return { label: "Tomorrow", color: "#a78bfa" };
  if (diff === -1) return { label: "Yesterday · overdue", color: "#f87171" };
  if (diff < 0)  return { label: `${Math.abs(diff)}d overdue`, color: "#f87171" };
  if (diff <= 7) return { label: `in ${diff}d`, color: "#94a3b8" };
  return { label: new Date(ymd).toLocaleDateString("en-US",{month:"short",day:"numeric"}), color: "#64748b" };
}

// Tasks panel — every task carries a required dueDate. Manual add takes text + date picker.
// AI auto-creates with parsed-from-note dates. Open tasks first, completed collapsed below.
function TasksPanel({ type, rec, onAdd, onToggle, onDelete }) {
  const [draft, setDraft] = useState("");
  const [draftDue, setDraftDue] = useState(() => offsetToYMD(1));
  const [showDone, setShowDone] = useState(false);
  const tasks = rec.tasks || [];
  const open = tasks.filter(t => !t.completed).sort((a,b) => cmpYMD(a.dueDate||"9999", b.dueDate||"9999"));
  const done = tasks.filter(t => t.completed).sort((a,b) => new Date(b.completedAt||0) - new Date(a.completedAt||0));
  const submit = () => { if (!draft.trim()) return; onAdd(type, rec.id, draft.trim(), draftDue, "manual"); setDraft(""); setDraftDue(offsetToYMD(1)); };
  return (
    <div style={{background:C.panel,border:`1px solid ${C.border}`,borderRadius:14,padding:"14px 18px"}}>
      <div style={{display:"flex",alignItems:"center",gap:10,marginBottom:11}}>
        <Sec style={{margin:0}}>✓ Tasks {open.length>0 && <span style={{fontWeight:600,color:C.muted,marginLeft:5}}>· {open.length} open</span>}</Sec>
        <div style={{marginLeft:"auto",fontSize:11,color:C.dim}}>✨ AI suggests + completes tasks when you save a note</div>
      </div>
      {open.length === 0 && done.length === 0 && (
        <div style={{fontSize:12,color:C.muted,padding:"10px 12px",background:"#090f1c",borderRadius:8,marginBottom:10,fontStyle:"italic"}}>No tasks yet. Add one below, or save a note and let AI extract next-actions.</div>
      )}
      {open.map(t => {
        const due = formatDueDate(t.dueDate);
        return (
          <div key={t.id} style={{display:"flex",alignItems:"center",gap:9,background:"#090f1c",borderRadius:8,padding:"8px 11px",marginBottom:5}}>
            <button onClick={()=>onToggle(type, rec.id, t.id)} title="Mark complete" style={{width:18,height:18,borderRadius:5,border:`1.5px solid ${C.border}`,background:"transparent",cursor:"pointer",flexShrink:0,fontFamily:"inherit",padding:0}}/>
            <div style={{flex:1,fontSize:13,color:C.text}}>{t.text}</div>
            <span style={{fontSize:10,fontWeight:700,color:due.color,background:due.color+"15",border:`1px solid ${due.color}33`,borderRadius:5,padding:"2px 7px",whiteSpace:"nowrap"}}>{due.label}</span>
            {t.source === "ai" && <span title="AI-suggested from a note" style={{fontSize:9,fontWeight:800,color:"#38bdf8",background:"#091420",border:"1px solid #38bdf833",borderRadius:5,padding:"2px 6px",letterSpacing:".04em"}}>✨ AI</span>}
            <button onClick={()=>onDelete(type, rec.id, t.id)} title="Delete task" style={{background:"transparent",border:"none",color:C.dim,fontSize:13,cursor:"pointer",padding:"2px 6px",fontFamily:"inherit"}}>×</button>
          </div>
        );
      })}
      <div style={{display:"flex",gap:7,marginTop:7,flexWrap:"wrap"}}>
        <input value={draft} onChange={e=>setDraft(e.target.value)} onKeyDown={e=>{if(e.key==="Enter"){e.preventDefault();submit();}}} placeholder="+ Add a task…" style={inp({flex:1,minWidth:180,fontSize:12})}/>
        <input type="date" value={draftDue} onChange={e=>setDraftDue(e.target.value)} title="Due date — required" style={{...inp({fontSize:12}),width:140,colorScheme:"dark"}}/>
        <button onClick={submit} disabled={!draft.trim()||!draftDue} style={{...btn("#091c09","#4ade80",true),opacity:(draft.trim()&&draftDue)?1:0.4,cursor:(draft.trim()&&draftDue)?"pointer":"not-allowed"}}>Add</button>
      </div>
      {done.length > 0 && (
        <div style={{marginTop:11,paddingTop:9,borderTop:`1px solid ${C.border}`}}>
          <button onClick={()=>setShowDone(s=>!s)} style={{background:"transparent",border:"none",color:C.muted,fontSize:11,fontWeight:700,cursor:"pointer",fontFamily:"inherit",padding:0,letterSpacing:".04em"}}>{showDone?"▼":"▶"} {done.length} completed</button>
          {showDone && done.map(t => (
            <div key={t.id} style={{display:"flex",alignItems:"center",gap:9,padding:"6px 4px",marginTop:5,opacity:.6}}>
              <button onClick={()=>onToggle(type, rec.id, t.id)} title="Reopen" style={{width:16,height:16,borderRadius:4,border:"none",background:"#4ade8055",cursor:"pointer",flexShrink:0,color:"#4ade80",fontSize:11,fontWeight:900,fontFamily:"inherit",padding:0}}>✓</button>
              <div style={{flex:1,fontSize:12,color:C.muted,textDecoration:"line-through"}}>{t.text}</div>
              {t.completedBy === "ai" && <span title="AI auto-completed via note" style={{fontSize:9,color:"#4ade80",fontWeight:700}}>✓ AI</span>}
              {t.source === "ai" && <span style={{fontSize:9,color:C.dim,fontWeight:700}}>✨</span>}
              <button onClick={()=>onDelete(type, rec.id, t.id)} title="Delete task" style={{background:"transparent",border:"none",color:C.dim,fontSize:12,cursor:"pointer",padding:"2px 6px",fontFamily:"inherit"}}>×</button>
            </div>
          ))}
        </div>
      )}
    </div>
  );
}

function RecordLink({ children, onClick, color = "inherit", weight = "inherit" }) {
  return (
    <button
      onClick={(e) => { e.stopPropagation(); onClick && onClick(); }}
      onMouseEnter={(e) => { e.currentTarget.style.textDecorationStyle = "solid"; e.currentTarget.style.opacity = "0.85"; }}
      onMouseLeave={(e) => { e.currentTarget.style.textDecorationStyle = "dotted"; e.currentTarget.style.opacity = "1"; }}
      style={{
        background: "transparent", border: "none", padding: 0,
        color, fontWeight: weight, fontFamily: "inherit", fontSize: "inherit",
        textDecoration: "underline", textDecorationStyle: "dotted", textUnderlineOffset: "3px",
        cursor: "pointer",
      }}
    >{children}</button>
  );
}

// Render a notification title with the candidate's name wrapped as a RecordLink when navigation is possible.
// Falls back to plain text if there's no candidate name or no record reference.
function LinkedTitle({ notification, onNameClick }) {
  const title = notification.title;
  const name = notification.data?.candidateName;
  const ref = notification.recordRef;
  if (!name || !ref || !title.includes(name)) return <>{title}</>;
  const idx = title.indexOf(name);
  return (
    <>
      {title.slice(0, idx)}
      <RecordLink onClick={() => onNameClick(notification)} color="inherit" weight="inherit">{name}</RecordLink>
      {title.slice(idx + name.length)}
    </>
  );
}

function NotificationBanner({ notification, topOffset, onDismiss, onView }) {
  // Auto-dismiss timeline: 12s fully visible, then 3s opacity fade to 0, then dismiss.
  // Hovering pauses the countdown and fades color back in over 3s; mouseleave restarts the 15s clock.
  const FULL_MS = 12000, FADE_MS = 3000, TOTAL_MS = FULL_MS + FADE_MS;
  const [fading, setFading] = useState(false);
  const fadeTimerRef = useRef(null);
  const dismissTimerRef = useRef(null);
  const startCountdown = useCallback(() => {
    setFading(false);
    fadeTimerRef.current = setTimeout(() => setFading(true), FULL_MS);
    dismissTimerRef.current = setTimeout(() => notification && onDismiss(notification.id), TOTAL_MS);
  }, [notification?.id, onDismiss]); // eslint-disable-line
  const clearTimers = () => { clearTimeout(fadeTimerRef.current); clearTimeout(dismissTimerRef.current); };
  useEffect(() => {
    if (!notification) return;
    startCountdown();
    return clearTimers;
  }, [notification?.id, startCountdown]);
  if (!notification) return null;
  const isCritical = notification.severity === "critical";
  return (
    <div
      onMouseEnter={() => { clearTimers(); setFading(false); }}
      onMouseLeave={() => { startCountdown(); }}
      style={{
        position: "fixed", top: topOffset, left: 0, right: 0, zIndex: 700,
        padding: "10px 20px",
        background: isCritical ? "#000" : "#0a3018",
        color:      isCritical ? "#ff4d6b" : "#86efac",
        borderBottom: `1px solid ${isCritical ? "#ef4444" : "#4ade8055"}`,
        boxShadow:  isCritical ? "0 0 18px #ef444466" : "0 4px 16px #00000099",
        display: "flex", alignItems: "center", gap: 12,
        fontSize: 13, fontWeight: isCritical ? 900 : 700, letterSpacing: isCritical ? ".02em" : 0,
        fontFamily: "inherit",
        opacity: fading ? 0 : 1,
        transition: "opacity 3s ease",
        pointerEvents: fading ? "auto" : "auto",
      }}
    >
      <span style={{fontSize: 17, flexShrink: 0}}>✨</span>
      <span style={{flex: 1, lineHeight: 1.4}}><LinkedTitle notification={notification} onNameClick={onView}/></span>
      {notification.recordRef && <button onClick={()=>onView(notification)} style={{background:"transparent",border:`1px solid ${isCritical?"#ef4444":"#4ade8055"}`,color:"inherit",fontSize:11,fontWeight:700,padding:"3px 10px",borderRadius:6,cursor:"pointer",fontFamily:"inherit"}}>View →</button>}
      <button onClick={()=>onDismiss(notification.id)} title="Dismiss" style={{background:"transparent",border:"none",color:"inherit",fontSize:14,cursor:"pointer",padding:"2px 6px",opacity:.7,fontFamily:"inherit"}}>✕</button>
    </div>
  );
}

// Lists every AI-driven automation in the app with description + a toggle/dropdown bound
// to the existing setting key. Pure presentation — all wiring done by the parent.
function AiAutomationsModal({ settings, setSetting, hasApiKey, onClose }) {
  const items = [
    { id:"autoScoreOnLoad",  type:"toggle", icon:"🎯", title:"Auto-Score Opportunities on Load",
      desc:"When the app opens, AI scores every opportunity (1–100 quality score + 1-line summary). Lets you sort the pipeline by AI-ranked priority immediately. Uses Claude Haiku — ~$0.60/user/mo at 600 scores." },
    { id:"autoScoreOnNote",  type:"toggle", icon:"🔁", title:"Re-Score When Note Added",
      desc:"After you save a note on an opportunity, AI re-scores it so the new context is reflected. Important: high-signal notes (validation calls, FDD reviews) can move a score by 10–20 points." },
    { id:"aiClassifyNotes",  type:"toggle", icon:"🏷️", title:"AI Note Classification",
      desc:"On every saved note, a Haiku call infers two flags: isTouchpoint (two-way exchange) and isCallAttempt (you tried to phone them). Used by the call tracker (1→Contacted, 3→Nurturing) and the stale-lead detector. Switching this off falls back to pure-keyword heuristics." },
    { id:"aiSummaryAutoOpen",type:"toggle", icon:"✨", title:"Auto-Open AI Summary on Opp",
      desc:"When you open an opportunity profile, the per-candidate AI summary modal opens automatically. Good for daily-driver mode, noisy if you cruise the kanban." },
    { id:"aiOrganizeFrequency", type:"select", icon:"📊", title:"AI Organize — Pipeline Audit Cadence",
      desc:"Runs the full-pipeline audit (likely wrong stage, missing materials, candidates to disqualify, habits to improve). Manual = only when you click the button. Weekly/Monthly = surfaced as a banner reminder.",
      options:[["manual","Manual only"],["weekly","Weekly reminder"],["monthly","Monthly reminder"]] },
  ];
  // Read-only informational entries — these always run.
  const builtins = [
    { icon:"🚨", title:"Hot Lead Going Dark", desc:"Scan-on-load notification. Surfaces high-signal leads with 7+ days of silence. Heuristic (no API call)." },
    { icon:"📦", title:"Promised Material Tracker", desc:"Scans recent notes for phrases like 'I'll send' or 'will provide' and flags items that look promised-but-not-delivered. Regex-based, no API call." },
    { icon:"🎯", title:"What's Next Prioritization", desc:"On the What's Next tab, AI ranks the single most urgent action across the pipeline and explains why. Triggered manually by visiting the tab." },
  ];
  return (
    <div style={{position:"fixed",inset:0,background:"#000c",zIndex:1001,display:"flex",alignItems:"center",justifyContent:"center",padding:16}}>
      <div style={{background:C.panel,border:`1px solid ${C.border}`,borderRadius:16,padding:24,width:"100%",maxWidth:680,maxHeight:"92vh",overflowY:"auto"}}>
        <div style={{display:"flex",alignItems:"center",gap:11,marginBottom:6}}>
          <span style={{fontSize:24}}>🤖</span>
          <h3 style={{flex:1,margin:0,color:"#f0f6ff",fontWeight:900,fontSize:16}}>AI Automations</h3>
          <button onClick={onClose} title="Close" style={btn(C.dim,C.muted)}>✕</button>
        </div>
        <div style={{fontSize:12,color:C.muted,marginBottom:14,lineHeight:1.55}}>
          These run automatically in the background — separate from the WHEN/IF/THEN rules you build above. All use Claude Haiku by default (configurable in <strong style={{color:C.text}}>Settings → AI</strong>).
        </div>
        {!hasApiKey && (
          <div style={{background:"#1a1908",border:"1px solid #facc1533",borderRadius:8,padding:"9px 13px",fontSize:11,color:"#facc15",marginBottom:14,lineHeight:1.5}}>
            🧪 <strong>Mock AI mode is active.</strong> These toggles still control behavior, but the AI calls return hand-tuned mock data. Add your Anthropic key in Settings → AI to switch to real responses.
          </div>
        )}
        {items.map(it => (
          <div key={it.id} style={{background:"#090f1c",border:`1px solid ${C.border}`,borderRadius:10,padding:"12px 14px",marginBottom:9,display:"flex",gap:12,alignItems:"flex-start"}}>
            <span style={{fontSize:22,lineHeight:1,marginTop:2}}>{it.icon}</span>
            <div style={{flex:1,minWidth:0}}>
              <div style={{fontSize:13,fontWeight:800,color:C.text,marginBottom:3}}>{it.title}</div>
              <div style={{fontSize:11,color:C.muted,lineHeight:1.5}}>{it.desc}</div>
            </div>
            <div style={{flexShrink:0,marginTop:2}}>
              {it.type === "toggle" && (
                <ToggleSwitch checked={!!settings[it.id]} onChange={v=>setSetting(it.id, v)}/>
              )}
              {it.type === "select" && (
                <select value={settings[it.id]||it.options[0][0]} onChange={e=>setSetting(it.id, e.target.value)} style={inp({fontSize:11,padding:"6px 9px"})}>
                  {it.options.map(([v,l])=><option key={v} value={v}>{l}</option>)}
                </select>
              )}
            </div>
          </div>
        ))}
        <div style={{fontSize:10,fontWeight:800,color:C.dim,letterSpacing:".08em",margin:"14px 0 8px"}}>ALWAYS-ON BACKGROUND HELPERS</div>
        {builtins.map((it,i) => (
          <div key={i} style={{background:"transparent",border:`1px dashed ${C.border}`,borderRadius:10,padding:"10px 13px",marginBottom:7,display:"flex",gap:11,alignItems:"flex-start"}}>
            <span style={{fontSize:18,lineHeight:1,marginTop:1}}>{it.icon}</span>
            <div style={{flex:1,minWidth:0}}>
              <div style={{fontSize:12,fontWeight:700,color:C.text,marginBottom:2}}>{it.title}</div>
              <div style={{fontSize:11,color:C.muted,lineHeight:1.45}}>{it.desc}</div>
            </div>
          </div>
        ))}
        <div style={{display:"flex",justifyContent:"flex-end",marginTop:14}}>
          <button onClick={onClose} style={btn("#091c09","#4ade80",true)}>Done</button>
        </div>
      </div>
    </div>
  );
}

// Build a manual analytics report — pick a date range + optional stage filter, then generate.
function GenerateReportModal({ onClose, onGenerate, stages }) {
  const today = new Date();
  const monthAgo = new Date(today.getTime() - 30*86400000);
  const fmt = (d) => d.toISOString().slice(0,10);
  const [title, setTitle] = useState("");
  const [from, setFrom] = useState(fmt(monthAgo));
  const [to,   setTo]   = useState(fmt(today));
  const [stageFilter, setStageFilter] = useState([]); // empty = all stages
  const toggleStage = (id) => setStageFilter(prev => prev.includes(id) ? prev.filter(x => x !== id) : [...prev, id]);
  const run = () => {
    onGenerate({
      type: "manual",
      title: title.trim() || `Manual · ${new Date(from).toLocaleDateString("en-US",{month:"short",day:"numeric"})}–${new Date(to).toLocaleDateString("en-US",{month:"short",day:"numeric"})}`,
      periodStart: from ? new Date(from).toISOString() : null,
      periodEnd:   to   ? new Date(to+"T23:59:59").toISOString() : null,
      filters: { stages: stageFilter },
    });
  };
  return (
    <div onClick={onClose} style={{position:"fixed",inset:0,background:"#000c",zIndex:1001,display:"flex",alignItems:"center",justifyContent:"center",padding:16}}>
      <div onClick={e=>e.stopPropagation()} style={{background:C.panel,border:`1px solid ${C.border}`,borderRadius:14,padding:20,width:"100%",maxWidth:520,maxHeight:"92vh",overflowY:"auto"}}>
        <div style={{display:"flex",alignItems:"center",gap:10,marginBottom:14}}>
          <span style={{fontSize:22}}>📋</span>
          <h3 style={{flex:1,margin:0,color:"#f0f6ff",fontWeight:900,fontSize:15}}>Generate Report</h3>
          <button onClick={onClose} title="Close" style={btn(C.dim,C.muted)}>✕</button>
        </div>
        <div style={{marginBottom:10}}>
          <div style={{fontSize:10,fontWeight:800,color:C.dim,letterSpacing:".05em",marginBottom:4}}>TITLE <span style={{color:C.muted,fontWeight:500}}>· optional</span></div>
          <input value={title} onChange={e=>setTitle(e.target.value)} placeholder="e.g. Q2 board update" style={inp({width:"100%",boxSizing:"border-box"})}/>
        </div>
        <div style={{display:"flex",gap:10,marginBottom:12}}>
          <div style={{flex:1}}>
            <div style={{fontSize:10,fontWeight:800,color:C.dim,letterSpacing:".05em",marginBottom:4}}>FROM</div>
            <input type="date" value={from} onChange={e=>setFrom(e.target.value)} style={inp({width:"100%",boxSizing:"border-box"})}/>
          </div>
          <div style={{flex:1}}>
            <div style={{fontSize:10,fontWeight:800,color:C.dim,letterSpacing:".05em",marginBottom:4}}>TO</div>
            <input type="date" value={to} onChange={e=>setTo(e.target.value)} style={inp({width:"100%",boxSizing:"border-box"})}/>
          </div>
        </div>
        <div style={{marginBottom:10}}>
          <div style={{fontSize:10,fontWeight:800,color:C.dim,letterSpacing:".05em",marginBottom:5}}>STAGES <span style={{color:C.muted,fontWeight:500}}>· empty = all</span></div>
          <div style={{display:"flex",flexWrap:"wrap",gap:5}}>
            {(stages||[]).map(s => {
              const sel = stageFilter.includes(s.id);
              return <button key={s.id} onClick={()=>toggleStage(s.id)} style={{background:sel?s.color+"33":"#090f1c",border:`1px solid ${sel?s.color:C.border}`,borderRadius:6,padding:"4px 10px",fontSize:11,color:sel?s.color:C.muted,cursor:"pointer",fontFamily:"inherit",fontWeight:sel?700:500}}>{s.label}</button>;
            })}
          </div>
        </div>
        <div style={{fontSize:11,color:C.dim,lineHeight:1.55,marginBottom:14,padding:"7px 10px",background:"#091420",border:"1px solid #60a5fa33",borderRadius:7}}>
          The report captures a snapshot of pipeline KPIs (leads, opps, conv rate, close rate, etc.) plus stage distribution, top opps, stale items, and broker activity. View it in-app, download as CSV (Excel-friendly), or print to PDF.
        </div>
        <div style={{display:"flex",gap:8,justifyContent:"flex-end"}}>
          <button onClick={onClose} style={btn(C.dim,C.muted)}>Cancel</button>
          <button onClick={run} style={btn("#091c09","#4ade80",true)}>Generate</button>
        </div>
      </div>
    </div>
  );
}

// View a stored report — KPIs, pipeline bars, top lists, broker activity, AI advice if any.
function ViewReportModal({ report, onClose, onPrint, onCsv, stages, onOpenRecord, onOpenBroker, onOpenNetwork }) {
  const k = report.data?.kpis || {};
  const stageEntries = Object.entries(report.data?.stages || {});
  const maxStage = Math.max(...stageEntries.map(([,v]) => v.count), 1);
  const topOpps = report.data?.topOpps || [];
  const staleList = report.data?.staleList || [];
  const brokerActivity = report.data?.brokerActivity || [];
  const sourcePerformance = report.data?.sourcePerformance || [];
  const maxSourceOpps = Math.max(...sourcePerformance.map(s => s.opps + s.leads), 1);
  return (
    <div onClick={onClose} style={{position:"fixed",inset:0,background:"#000c",zIndex:1001,display:"flex",alignItems:"center",justifyContent:"center",padding:16}}>
      <div onClick={e=>e.stopPropagation()} style={{background:C.panel,border:`1px solid ${C.border}`,borderRadius:14,padding:0,width:"100%",maxWidth:680,maxHeight:"92vh",display:"flex",flexDirection:"column"}}>
        <div style={{display:"flex",alignItems:"center",gap:10,padding:"16px 20px",borderBottom:`1px solid ${C.border}`}}>
          <div style={{flex:1,minWidth:0}}>
            <h3 style={{margin:0,color:"#f0f6ff",fontWeight:900,fontSize:15,overflow:"hidden",textOverflow:"ellipsis",whiteSpace:"nowrap"}}>{report.title}</h3>
            <div style={{fontSize:10,color:C.muted,marginTop:3}}>{new Date(report.generatedAt).toLocaleString()}</div>
          </div>
          <button onClick={onCsv} style={btn("#091c09","#4ade80")} title="Download CSV">⬇ CSV</button>
          <button onClick={onPrint} style={btn("#091420","#60a5fa")} title="Print or save as PDF">🖨 PDF</button>
          <button onClick={onClose} title="Close" style={btn(C.dim,C.muted)}>✕</button>
        </div>
        <div style={{padding:"16px 20px",overflowY:"auto",flex:1}}>
          {/* Territory-unlock report — surfaced when a restricted/off-limits territory is removed.
              Lists every active (non-disqualified, non-closed-lost) lead/opp that previously hit
              the AI territory-conflict flag for that area. Each name links to the candidate's
              profile so the rep can immediately call/email "we're now able to talk again." */}
          {report.data?.unlock && (() => {
            const u = report.data.unlock;
            const list = u.unlockedRecords || [];
            return (
              <div style={{marginBottom:18}}>
                <div style={{background:"#091c14",border:"1px solid #4ade8033",borderRadius:10,padding:"12px 14px",marginBottom:14,display:"flex",gap:11,alignItems:"flex-start"}}>
                  <span style={{fontSize:22,lineHeight:1}}>🔓</span>
                  <div style={{flex:1,minWidth:0}}>
                    <div style={{fontSize:13,fontWeight:800,color:"#86efac",marginBottom:3}}>{u.territoryLabel} is no longer restricted</div>
                    <div style={{fontSize:11,color:C.muted,lineHeight:1.55}}>Previously this area was marked <strong style={{color:"#fca5a5"}}>{u.restrictionLabel}</strong>. The {list.length} {list.length===1?"candidate":"candidates"} below were flagged as territory conflicts and are now reachable. Disqualified leads and closed-lost opps are excluded.</div>
                  </div>
                </div>
                {list.length === 0 ? (
                  <div style={{background:"#090f1c",border:`1px dashed ${C.border}`,borderRadius:9,padding:"22px 16px",color:C.muted,fontSize:12,textAlign:"center"}}>No previously blocked candidates — nothing to revisit for this area.</div>
                ) : (
                  <>
                    <div style={{fontSize:10,fontWeight:800,color:C.dim,letterSpacing:".06em",marginBottom:7}}>CANDIDATES TO REVISIT ({list.length})</div>
                    <div style={{display:"flex",flexDirection:"column",gap:6,marginBottom:6}}>
                      {list.map(r => (
                        <div key={r.id} style={{display:"flex",alignItems:"center",gap:10,background:"#090f1c",border:`1px solid ${C.border}`,borderRadius:8,padding:"9px 12px"}}>
                          <div style={{flex:1,minWidth:0}}>
                            <div style={{display:"flex",alignItems:"center",gap:8,marginBottom:3}}>
                              <span style={{fontSize:9,fontWeight:800,color:r.type==="opp"?"#a78bfa":"#60a5fa",background:r.type==="opp"?"#1a1429":"#091420",border:`1px solid ${r.type==="opp"?"#a78bfa44":"#60a5fa44"}`,borderRadius:4,padding:"1px 5px",letterSpacing:".05em"}}>{r.type==="opp"?"OPP":"LEAD"}</span>
                              <span style={{fontSize:13,fontWeight:700,color:C.text,overflow:"hidden",textOverflow:"ellipsis",whiteSpace:"nowrap"}}>{onOpenRecord ? <RecordLink onClick={()=>onOpenRecord(r.id)} color={C.text} weight={700}>{r.name}</RecordLink> : r.name}</span>
                              <span style={{fontSize:10,color:C.muted}}>· {r.stageLabel}</span>
                            </div>
                            <div style={{fontSize:11,color:C.muted,overflow:"hidden",textOverflow:"ellipsis",whiteSpace:"nowrap"}}>
                              {r.email ? <span>{r.email}</span> : null}
                              {r.email && r.phone ? <span style={{margin:"0 8px",color:C.dim}}>·</span> : null}
                              {r.phone ? <span>{r.phone}</span> : null}
                              {r.territory ? <span style={{margin:"0 8px",color:C.dim}}>·</span> : null}
                              {r.territory ? <span style={{color:C.dim}}>{r.territory}</span> : null}
                            </div>
                          </div>
                          {r.score>0 && <div style={{fontSize:11,fontWeight:700,color:"#fb923c",width:30,textAlign:"right"}}>{r.score}</div>}
                        </div>
                      ))}
                    </div>
                  </>
                )}
              </div>
            );
          })()}
          {(k.leadCount != null || k.oppCount != null) && <>
          <div style={{fontSize:10,fontWeight:800,color:C.dim,letterSpacing:".06em",marginBottom:9}}>KEY METRICS</div>
          <div style={{display:"grid",gridTemplateColumns:"repeat(4,1fr)",gap:8,marginBottom:18}}>
            {[["Leads",k.leadCount||0,"#60a5fa"],["Opps",k.oppCount||0,"#a78bfa"],["Signed",k.signed||0,"#4ade80"],["Lost",k.lost||0,"#94a3b8"],["Conv Rate",(k.convRate||0)+"%","#facc15"],["Close Rate",(k.closeRate||0)+"%","#38bdf8"],["Avg Score",k.avgScore||0,"#fb923c"],["Avg Close",(k.avgCloseDays||0)+"d","#34d399"]].map(([l,v,c])=>(
              <div key={l} style={{background:"#090f1c",border:`1px solid ${C.border}`,borderRadius:8,padding:"9px 11px"}}>
                <div style={{fontSize:18,fontWeight:900,color:c}}>{v}</div>
                <div style={{fontSize:9,color:C.muted,letterSpacing:".04em",textTransform:"uppercase",marginTop:1}}>{l}</div>
              </div>
            ))}
          </div>
          </>}
          {stageEntries.length > 0 && <>
            <div style={{fontSize:10,fontWeight:800,color:C.dim,letterSpacing:".06em",marginBottom:7}}>PIPELINE STAGES</div>
            <div style={{marginBottom:18}}>
              {stageEntries.map(([id,v]) => (
                <div key={id} style={{display:"flex",alignItems:"center",gap:8,marginBottom:5}}>
                  <div style={{width:140,fontSize:11,color:C.muted,textAlign:"right",overflow:"hidden",textOverflow:"ellipsis",whiteSpace:"nowrap"}}>{v.label}</div>
                  <div style={{flex:1,height:14,background:"#090f1c",borderRadius:4,overflow:"hidden"}}>
                    <div style={{width:`${(v.count/maxStage)*100}%`,height:"100%",background:v.color,borderRadius:4}}/>
                  </div>
                  <div style={{width:24,fontSize:11,fontWeight:700,color:v.color}}>{v.count}</div>
                </div>
              ))}
            </div>
          </>}
          {topOpps.length > 0 && <>
            <div style={{fontSize:10,fontWeight:800,color:C.dim,letterSpacing:".06em",marginBottom:7}}>TOP OPPORTUNITIES</div>
            <div style={{marginBottom:18}}>
              {topOpps.map(o => (
                <div key={o.id} style={{display:"flex",alignItems:"center",gap:9,background:"#090f1c",borderRadius:7,padding:"6px 11px",marginBottom:4}}>
                  <div style={{flex:1,fontSize:12,color:C.text}}>{onOpenRecord ? <RecordLink onClick={()=>onOpenRecord(o.id)} color={C.text}>{o.name}</RecordLink> : o.name}</div>
                  <div style={{fontSize:10,color:C.muted}}>{o.stage}</div>
                  <div style={{fontSize:12,fontWeight:700,color:"#fb923c",width:32,textAlign:"right"}}>{o.score}</div>
                </div>
              ))}
            </div>
          </>}
          {staleList.length > 0 && <>
            <div style={{fontSize:10,fontWeight:800,color:"#f87171",letterSpacing:".06em",marginBottom:7}}>STALE OPPORTUNITIES</div>
            <div style={{marginBottom:18}}>
              {staleList.map(o => (
                <div key={o.id} style={{display:"flex",alignItems:"center",gap:9,background:"#1a0808",borderRadius:7,padding:"6px 11px",marginBottom:4,border:"1px solid #f8717122"}}>
                  <div style={{flex:1,fontSize:12,color:C.text}}>{onOpenRecord ? <RecordLink onClick={()=>onOpenRecord(o.id)} color={C.text}>{o.name}</RecordLink> : o.name}</div>
                  <div style={{fontSize:10,color:C.muted}}>{o.stage}</div>
                  <div style={{fontSize:11,fontWeight:700,color:"#fca5a5"}}>{o.days}d stale</div>
                </div>
              ))}
            </div>
          </>}
          {sourcePerformance.length > 0 && <>
            <div style={{fontSize:10,fontWeight:800,color:C.dim,letterSpacing:".06em",marginBottom:7}}>LEAD SOURCE PERFORMANCE</div>
            <div style={{marginBottom:18,background:"#090f1c",border:`1px solid ${C.border}`,borderRadius:8,padding:"8px 11px"}}>
              <div style={{display:"grid",gridTemplateColumns:"1.4fr 50px 50px 60px 60px 60px",gap:6,fontSize:9,color:C.dim,fontWeight:800,letterSpacing:".05em",padding:"4px 0",borderBottom:`1px solid ${C.border}`,marginBottom:5}}>
                <div>SOURCE</div><div style={{textAlign:"right"}}>LEADS</div><div style={{textAlign:"right"}}>OPPS</div><div style={{textAlign:"right"}}>SIGNED</div><div style={{textAlign:"right"}}>CONV</div><div style={{textAlign:"right"}}>CLOSE</div>
              </div>
              {sourcePerformance.map(s => (
                <div key={s.name} style={{display:"grid",gridTemplateColumns:"1.4fr 50px 50px 60px 60px 60px",gap:6,fontSize:11,padding:"5px 0",alignItems:"center"}}>
                  <div style={{color:C.text,fontWeight:600,minWidth:0}}>
                    <div style={{overflow:"hidden",textOverflow:"ellipsis",whiteSpace:"nowrap"}}>{s.name}</div>
                    <div style={{height:3,background:"#0a1525",borderRadius:2,marginTop:3,overflow:"hidden"}}>
                      <div style={{width:`${((s.opps+s.leads)/maxSourceOpps)*100}%`,height:"100%",background:s.signed>0?"#4ade80":"#60a5fa",borderRadius:2}}/>
                    </div>
                  </div>
                  <div style={{textAlign:"right",color:"#60a5fa"}}>{s.leads}</div>
                  <div style={{textAlign:"right",color:"#a78bfa"}}>{s.opps}</div>
                  <div style={{textAlign:"right",color:"#4ade80",fontWeight:700}}>{s.signed}</div>
                  <div style={{textAlign:"right",color:"#facc15"}}>{s.convRate}%</div>
                  <div style={{textAlign:"right",color:"#38bdf8"}}>{s.closeRate}%</div>
                </div>
              ))}
            </div>
          </>}
          {brokerActivity.length > 0 && <>
            <div style={{fontSize:10,fontWeight:800,color:C.dim,letterSpacing:".06em",marginBottom:7}}>BROKER ACTIVITY</div>
            <div style={{marginBottom:18}}>
              {brokerActivity.map(b => (
                <div key={b.id} style={{display:"flex",alignItems:"center",gap:9,background:"#090f1c",borderRadius:7,padding:"6px 11px",marginBottom:4}}>
                  <div style={{flex:1,fontSize:12,color:C.text}}>{onOpenBroker ? <RecordLink onClick={()=>onOpenBroker(b.id)} color={C.text} weight={700}>{b.name}</RecordLink> : b.name}</div>
                  <div style={{fontSize:10,color:C.muted}}>{b.assignedOpps} opps · {b.comms} comms</div>
                </div>
              ))}
            </div>
          </>}
          {report.aiAdvice && <>
            <div style={{fontSize:10,fontWeight:800,color:"#38bdf8",letterSpacing:".06em",marginBottom:7}}>🤖 AI STRATEGY ADVICE</div>
            <div style={{background:"#091420",border:"1px solid #38bdf833",borderRadius:9,padding:"14px 16px",fontSize:12,color:"#c8d8ef",lineHeight:1.65,whiteSpace:"pre-wrap",fontFamily:"Georgia,serif"}}>
              {(() => {
                // Wrap any candidate name from topOpps/staleList/dropouts in the advice text with a RecordLink.
                if (!onOpenRecord) return report.aiAdvice;
                const allNamed = [...topOpps, ...staleList, ...(report.data?.dropouts||[])].filter(r => r.name && r.id);
                // Dedupe by name (prefer the first id encountered), build a regex.
                const byName = new Map();
                allNamed.forEach(r => { if (!byName.has(r.name)) byName.set(r.name, r.id); });
                if (!byName.size) return report.aiAdvice;
                const names = [...byName.keys()].sort((a,b) => b.length - a.length); // longest first to avoid partial matches
                const escaped = names.map(n => n.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"));
                const re = new RegExp(`(${escaped.join("|")})`, "g");
                const parts = report.aiAdvice.split(re);
                return parts.map((p, i) => byName.has(p) ? <RecordLink key={i} onClick={()=>onOpenRecord(byName.get(p))} color="inherit" weight="inherit">{p}</RecordLink> : p);
              })()}
            </div>
          </>}
        </div>
      </div>
    </div>
  );
}

function LostReasonModal({ candidateName, onCancel, onConfirm }) {
  const [reason, setReason] = useState(null);
  const [notes, setNotes] = useState("");
  return (
    <div style={{position:"fixed",inset:0,background:"#000c",zIndex:400,display:"flex",alignItems:"center",justifyContent:"center",padding:16}}>
      <div style={{background:C.panel,border:`1px solid ${C.border}`,borderRadius:16,padding:24,width:"100%",maxWidth:460}}>
        <div style={{display:"flex",alignItems:"center",gap:12,marginBottom:14}}>
          <div style={{fontSize:28}}>❌</div>
          <div style={{flex:1}}>
            <h3 style={{margin:0,color:"#f0f6ff",fontSize:17,fontWeight:900}}>Close as Lost</h3>
            <div style={{fontSize:12,color:C.muted,marginTop:2}}>{candidateName}</div>
          </div>
        </div>
        <div style={{fontSize:12,color:C.muted,marginBottom:11}}>Why are you closing this opportunity?</div>
        <div style={{display:"flex",flexDirection:"column",gap:8,marginBottom:14}}>
          {LOST_REASONS.map(r => (
            <button key={r.id} onClick={()=>setReason(r.id)} style={{
              background: reason===r.id ? r.color+"22" : "#090f1c",
              border: `1.5px solid ${reason===r.id ? r.color : C.border}`,
              borderRadius: 10, padding: "11px 14px",
              cursor: "pointer", fontFamily: "inherit", textAlign: "left",
              display: "flex", alignItems: "center", gap: 11,
            }}>
              <span style={{fontSize:20,width:22,textAlign:"center"}}>{r.icon}</span>
              <div style={{flex:1}}>
                <div style={{fontWeight:800,color:reason===r.id?r.color:C.text,fontSize:13}}>{r.label}</div>
                <div style={{fontSize:11,color:C.muted,marginTop:2}}>{r.desc}</div>
              </div>
              {reason===r.id && <span style={{fontSize:14,color:r.color,fontWeight:900}}>✓</span>}
            </button>
          ))}
        </div>
        <div style={{marginBottom:14}}>
          <label style={{display:"block",fontSize:11,color:C.dim,fontWeight:700,marginBottom:4}}>Additional notes (optional)</label>
          <textarea value={notes} onChange={e=>setNotes(e.target.value)} placeholder="Any context worth preserving — e.g. 'wants to reconnect in Q3'…" rows={2} style={{...inp({width:"100%",boxSizing:"border-box",resize:"vertical",fontFamily:"inherit"})}}/>
        </div>
        <div style={{display:"flex",gap:10,justifyContent:"flex-end"}}>
          <button onClick={onCancel} style={btn(C.dim,C.muted)}>Cancel</button>
          <button onClick={()=>{ if(!reason){alert("Pick a reason first.");return;} onConfirm({reason, notes:notes.trim()}); }} style={btn("#1a0808","#f87171",true)}>Close as Lost</button>
        </div>
      </div>
    </div>
  );
}

// ═══════════════════════════════════════════════════════════
//  TEMPLATE FOLDER MODAL
// ═══════════════════════════════════════════════════════════
// ═══════════════════════════════════════════════════════════
//  DISCOVERY DAY — form + detail
// ═══════════════════════════════════════════════════════════
function DiscoveryDayFormModal({ initial, onSave, onCancel, settings, bOpps, bLeads }) {
  const isEdit = !!initial.id;
  const [form, setForm] = useState(isEdit ? initial : {
    name: "",
    date: "",
    startTime: "",
    endTime: "",
    timeZone: settings?.timezone === "AUTO" ? Intl.DateTimeFormat().resolvedOptions().timeZone : (settings?.timezone || "America/New_York"),
    format: "in_person", // in_person | virtual | hybrid
    location: "",
    videoLink: "",
    status: "draft",
    candidateIds: [],
    corporateAttendees: [],
    checklist: DEFAULT_DDAY_CHECKLIST.map(it => ({ ...it, id: Math.random().toString(36).slice(2,10), done: false })),
    prepNotes: "",
    recapNotes: "",
  });
  const set = (k, v) => setForm(f => ({ ...f, [k]: v }));
  const [attDraft, setAttDraft] = useState({ name:"", role:"", email:"", confirmed:false });
  const toggleCandidate = (recRef) => {
    const cur = form.candidateIds || [];
    const exists = cur.find(c => c.type === recRef.type && c.id === recRef.id);
    set("candidateIds", exists ? cur.filter(c => !(c.type === recRef.type && c.id === recRef.id)) : [...cur, recRef]);
  };
  const addAttendee = () => {
    if (!attDraft.name.trim()) return;
    set("corporateAttendees", [...(form.corporateAttendees||[]), { id: Math.random().toString(36).slice(2,10), ...attDraft }]);
    setAttDraft({ name:"", role:"", email:"", confirmed:false });
  };
  const removeAttendee = (id) => set("corporateAttendees", (form.corporateAttendees||[]).filter(a => a.id !== id));
  const toggleAttendeeConfirmed = (id) => set("corporateAttendees", (form.corporateAttendees||[]).map(a => a.id === id ? {...a, confirmed: !a.confirmed} : a));
  const valid = (form.name||"").trim().length > 0;
  // Active pipeline candidates the rep can attach (opps + qualified leads)
  const candidatePool = [
    ...bOpps.filter(o => o.stage !== "closed_lost").map(o => ({ type:"opp", id:o.id, name:`${o.firstName||""} ${o.lastName||""}`.trim(), stage:o.stage, email:o.email })),
    ...bLeads.filter(l => l.stage !== "disqualified").map(l => ({ type:"lead", id:l.id, name:`${l.firstName||""} ${l.lastName||""}`.trim(), stage:l.stage, email:l.email })),
  ];
  return (
    <div onClick={onCancel} style={{position:"fixed",inset:0,background:"#000c",zIndex:1100,display:"flex",alignItems:"center",justifyContent:"center",padding:16}}>
      <div onClick={e=>e.stopPropagation()} style={{background:C.panel,border:`1px solid ${C.border}`,borderRadius:14,width:"100%",maxWidth:680,maxHeight:"92vh",display:"flex",flexDirection:"column"}}>
        <div style={{display:"flex",alignItems:"center",gap:11,padding:"16px 20px",borderBottom:`1px solid ${C.border}`}}>
          <span style={{fontSize:22}}>🎓</span>
          <div style={{flex:1}}>
            <h3 style={{margin:0,fontSize:15,fontWeight:900,color:"#f0f6ff"}}>{isEdit ? "Edit Discovery Day" : "New Discovery Day"}</h3>
            <div style={{fontSize:11,color:C.muted,marginTop:2}}>Plan logistics, format, attendees, and an end-to-end checklist.</div>
          </div>
          <button onClick={onCancel} title="Close" style={btn(C.dim,C.muted)}>✕</button>
        </div>
        <div style={{padding:"14px 20px",overflowY:"auto",flex:1,display:"flex",flexDirection:"column",gap:13}}>
          <div>
            <div style={{fontSize:9,fontWeight:800,color:C.dim,letterSpacing:".08em",marginBottom:5}}>EVENT NAME</div>
            <input value={form.name||""} onChange={e=>set("name", e.target.value)} placeholder="e.g. October Discovery Day — Atlanta HQ" style={inp({width:"100%",boxSizing:"border-box"})} autoFocus/>
          </div>
          <div style={{display:"grid",gridTemplateColumns:"1.4fr 1fr 1fr",gap:9}}>
            <div>
              <div style={{fontSize:9,fontWeight:800,color:C.dim,letterSpacing:".08em",marginBottom:5}}>DATE</div>
              <input type="date" value={form.date||""} onChange={e=>set("date", e.target.value)} style={inp({width:"100%",boxSizing:"border-box"})}/>
            </div>
            <div>
              <div style={{fontSize:9,fontWeight:800,color:C.dim,letterSpacing:".08em",marginBottom:5}}>START</div>
              <input type="time" value={form.startTime||""} onChange={e=>set("startTime", e.target.value)} style={inp({width:"100%",boxSizing:"border-box"})}/>
            </div>
            <div>
              <div style={{fontSize:9,fontWeight:800,color:C.dim,letterSpacing:".08em",marginBottom:5}}>END</div>
              <input type="time" value={form.endTime||""} onChange={e=>set("endTime", e.target.value)} style={inp({width:"100%",boxSizing:"border-box"})}/>
            </div>
          </div>
          <div>
            <div style={{fontSize:9,fontWeight:800,color:C.dim,letterSpacing:".08em",marginBottom:5}}>FORMAT</div>
            <div style={{display:"flex",gap:7}}>
              {[
                {k:"in_person", icon:"🏢", label:"In-Person", desc:"Candidates fly in to HQ for the day."},
                {k:"virtual",   icon:"💻", label:"Virtual",   desc:"Run the whole day over video."},
                {k:"hybrid",    icon:"🔀", label:"Hybrid",    desc:"Some attendees in-person, others remote."},
              ].map(o => {
                const sel = form.format === o.k;
                return (
                  <button key={o.k} onClick={()=>set("format", o.k)} style={{flex:1,background:sel?"#162035":"#090f1c",border:`1.5px solid ${sel?C.accent+"88":C.border}`,borderRadius:9,padding:"10px 12px",textAlign:"left",cursor:"pointer",fontFamily:"inherit",color:C.text}}>
                    <div style={{fontSize:12,fontWeight:800,color:sel?C.accent:C.text,display:"flex",alignItems:"center",gap:6}}><span>{o.icon}</span><span>{o.label}</span></div>
                    <div style={{fontSize:10,color:C.muted,marginTop:3,lineHeight:1.4}}>{o.desc}</div>
                  </button>
                );
              })}
            </div>
          </div>
          {(form.format === "in_person" || form.format === "hybrid") && (
            <div>
              <div style={{fontSize:9,fontWeight:800,color:C.dim,letterSpacing:".08em",marginBottom:5}}>LOCATION</div>
              <input value={form.location||""} onChange={e=>set("location", e.target.value)} placeholder="e.g. Brand HQ — 123 Main St, Atlanta, GA" style={inp({width:"100%",boxSizing:"border-box"})}/>
            </div>
          )}
          {(form.format === "virtual" || form.format === "hybrid") && (
            <div>
              <div style={{fontSize:9,fontWeight:800,color:C.dim,letterSpacing:".08em",marginBottom:5}}>VIDEO LINK</div>
              <input value={form.videoLink||""} onChange={e=>set("videoLink", e.target.value)} placeholder="e.g. https://zoom.us/j/…" style={inp({width:"100%",boxSizing:"border-box"})}/>
            </div>
          )}
          <div>
            <div style={{fontSize:9,fontWeight:800,color:C.dim,letterSpacing:".08em",marginBottom:5}}>STATUS</div>
            <select value={form.status||"draft"} onChange={e=>set("status", e.target.value)} style={inp({width:"100%",boxSizing:"border-box"})}>
              {Object.entries(DDAY_STATUSES).map(([k,s]) => <option key={k} value={k}>{s.icon} {s.label}</option>)}
            </select>
          </div>
          {/* Candidate roster */}
          <div>
            <div style={{fontSize:9,fontWeight:800,color:C.dim,letterSpacing:".08em",marginBottom:5}}>CANDIDATES ({(form.candidateIds||[]).length})</div>
            <div style={{fontSize:10,color:C.muted,marginBottom:6,lineHeight:1.4}}>Pick which candidates are attending. Pulled from your active opportunities and qualified leads.</div>
            {candidatePool.length === 0 ? (
              <div style={{fontSize:11,color:C.muted,fontStyle:"italic",padding:"8px 0"}}>No active candidates in this brand yet.</div>
            ) : (
              <div style={{maxHeight:170,overflowY:"auto",background:"#090f1c",border:`1px solid ${C.border}`,borderRadius:8,padding:6}}>
                {candidatePool.map(r => {
                  const sel = (form.candidateIds||[]).some(c => c.type===r.type && c.id===r.id);
                  return (
                    <label key={`${r.type}_${r.id}`} style={{display:"flex",alignItems:"center",gap:9,padding:"6px 8px",borderRadius:6,cursor:"pointer",background:sel?"#162035":"transparent"}}>
                      <input type="checkbox" checked={sel} onChange={()=>toggleCandidate(r)}/>
                      <span style={{fontSize:9,fontWeight:800,color:r.type==="opp"?"#a78bfa":"#60a5fa",background:r.type==="opp"?"#1a1429":"#091420",border:`1px solid ${r.type==="opp"?"#a78bfa44":"#60a5fa44"}`,borderRadius:4,padding:"1px 5px",letterSpacing:".05em"}}>{r.type.toUpperCase()}</span>
                      <span style={{fontSize:12,color:C.text,flex:1,minWidth:0,overflow:"hidden",textOverflow:"ellipsis",whiteSpace:"nowrap"}}>{r.name||"(unnamed)"}</span>
                      <span style={{fontSize:10,color:C.muted}}>{r.stage}</span>
                    </label>
                  );
                })}
              </div>
            )}
          </div>
          {/* Corporate attendees */}
          <div>
            <div style={{fontSize:9,fontWeight:800,color:C.dim,letterSpacing:".08em",marginBottom:5}}>CORPORATE ATTENDEES ({(form.corporateAttendees||[]).length})</div>
            <div style={{fontSize:10,color:C.muted,marginBottom:6,lineHeight:1.4}}>Who on the corporate team will be there. Mark confirmed once their calendar is locked.</div>
            <div style={{display:"flex",flexDirection:"column",gap:5,marginBottom:7}}>
              {(form.corporateAttendees||[]).map(a => (
                <div key={a.id} style={{display:"flex",alignItems:"center",gap:9,background:"#090f1c",border:`1px solid ${a.confirmed?"#4ade8055":C.border}`,borderRadius:7,padding:"7px 10px"}}>
                  <input type="checkbox" checked={!!a.confirmed} onChange={()=>toggleAttendeeConfirmed(a.id)} title="Toggle confirmed"/>
                  <div style={{flex:1,minWidth:0}}>
                    <div style={{fontSize:12,fontWeight:700,color:C.text}}>{a.name}{a.role?<span style={{color:C.muted,fontWeight:500,marginLeft:6}}>· {a.role}</span>:null}</div>
                    {a.email && <div style={{fontSize:10,color:C.muted,marginTop:1}}>{a.email}</div>}
                  </div>
                  {a.confirmed && <span style={{fontSize:9,fontWeight:800,color:"#4ade80",background:"#091c09",border:"1px solid #4ade8055",borderRadius:4,padding:"1px 5px",letterSpacing:".05em"}}>CONFIRMED</span>}
                  <button onClick={()=>removeAttendee(a.id)} title="Remove" style={{background:"transparent",border:"none",color:C.dim,fontSize:14,cursor:"pointer",fontFamily:"inherit",padding:"2px 6px"}}>✕</button>
                </div>
              ))}
            </div>
            <div style={{display:"grid",gridTemplateColumns:"1.3fr 1fr 1.3fr auto",gap:6}}>
              <input value={attDraft.name} onChange={e=>setAttDraft({...attDraft, name: e.target.value})} placeholder="Name" style={inp({fontSize:11,padding:"7px 9px",boxSizing:"border-box"})} onKeyDown={e=>{ if (e.key==="Enter") addAttendee(); }}/>
              <input value={attDraft.role} onChange={e=>setAttDraft({...attDraft, role: e.target.value})} placeholder="Role (CEO, Ops VP…)" style={inp({fontSize:11,padding:"7px 9px",boxSizing:"border-box"})}/>
              <input value={attDraft.email} onChange={e=>setAttDraft({...attDraft, email: e.target.value})} placeholder="Email (optional)" style={inp({fontSize:11,padding:"7px 9px",boxSizing:"border-box"})}/>
              <button onClick={addAttendee} disabled={!attDraft.name.trim()} style={{...btn("#091420","#60a5fa",!!attDraft.name.trim()),opacity:attDraft.name.trim()?1:0.5,cursor:attDraft.name.trim()?"pointer":"not-allowed",fontSize:11}}>+ Add</button>
            </div>
          </div>
          <div>
            <div style={{fontSize:9,fontWeight:800,color:C.dim,letterSpacing:".08em",marginBottom:5}}>PREP NOTES (visible only to internal team)</div>
            <textarea value={form.prepNotes||""} onChange={e=>set("prepNotes", e.target.value)} placeholder="Topics to emphasize, candidate-specific objections to handle, special accommodations…" rows={3} style={inp({width:"100%",boxSizing:"border-box",resize:"vertical",fontFamily:"inherit"})}/>
          </div>
        </div>
        <div style={{display:"flex",justifyContent:"flex-end",gap:8,padding:"12px 20px",borderTop:`1px solid ${C.border}`}}>
          <button onClick={onCancel} style={btn(C.dim,C.muted)}>Cancel</button>
          <button onClick={()=>valid && onSave(form)} disabled={!valid} style={{...btn("#091c09","#4ade80",valid),opacity:valid?1:0.5,cursor:valid?"pointer":"not-allowed"}}>{isEdit?"Save Changes":"Create Discovery Day"}</button>
        </div>
      </div>
    </div>
  );
}

function DiscoveryDayDetail({ dday, onBack, onEdit, onDelete, bOpps, bLeads, settings, onOpenRecord, toggleItem, addItem, deleteItem, saveDday }) {
  const [newItemDraft, setNewItemDraft] = useState({ pre:"", day_of:"", post:"" });
  const stat = DDAY_STATUSES[dday.status||"draft"];
  const candidates = (dday.candidateIds||[]).map(ref => {
    const pool = ref.type === "opp" ? bOpps : bLeads;
    const rec = pool.find(r => r.id === ref.id);
    return rec ? { ...rec, type: ref.type } : null;
  }).filter(Boolean);
  const checklist = dday.checklist || [];
  const done = checklist.filter(it => it.done).length;
  const pct = checklist.length ? Math.round((done/checklist.length)*100) : 0;
  const grouped = DDAY_CHECKLIST_CATEGORIES.map(cat => ({ ...cat, items: checklist.filter(it => (it.category||"day_of") === cat.id) }));
  return (
    <div>
      <div style={{display:"flex",alignItems:"center",gap:9,marginBottom:14}}>
        <button onClick={onBack} style={btn(C.dim,C.muted)} title="Back to Discovery Days">← Discovery Days</button>
        <div style={{marginLeft:"auto",display:"flex",gap:7}}>
          <button onClick={onEdit} style={btn(C.dim,C.muted)} title="Edit Discovery Day details">✏️ Edit</button>
          <button onClick={onDelete} style={btn("#1a0808","#f87171")} title="Delete Discovery Day">🗑 Delete</button>
        </div>
      </div>
      <div style={{background:C.panel,border:`1px solid ${stat.color}55`,borderRadius:14,padding:"20px 24px",marginBottom:14,boxShadow:`0 0 18px ${stat.color}22`}}>
        <div style={{display:"flex",alignItems:"flex-start",gap:18}}>
          <div style={{width:64,height:64,borderRadius:14,background:stat.color+"22",border:`1.5px solid ${stat.color}66`,display:"flex",alignItems:"center",justifyContent:"center",fontSize:28,flexShrink:0}}>{dday.format==="virtual"?"💻":dday.format==="hybrid"?"🔀":"🏢"}</div>
          <div style={{flex:1,minWidth:0}}>
            <div style={{display:"flex",alignItems:"center",gap:8,flexWrap:"wrap",marginBottom:5}}>
              <div style={{fontSize:22,fontWeight:900,color:"#f0f6ff"}}>{dday.name||"Untitled Discovery Day"}</div>
              <span style={{fontSize:10,fontWeight:800,color:stat.color,background:stat.color+"15",border:`1px solid ${stat.color}44`,borderRadius:5,padding:"2px 8px",letterSpacing:".05em"}}>{stat.icon} {stat.label.toUpperCase()}</span>
            </div>
            <div style={{fontSize:12,color:C.muted,marginBottom:8}}>
              📅 {dday.date ? new Date(dday.date).toLocaleDateString("en-US",{weekday:"long",month:"long",day:"numeric",year:"numeric"}) : "No date set"}{dday.startTime?` · ${dday.startTime}`:""}{dday.endTime?`–${dday.endTime}`:""}
            </div>
            {dday.location && <div style={{fontSize:12,color:C.muted,marginBottom:3}}>📍 {dday.location}</div>}
            {dday.videoLink && <div style={{fontSize:12,marginBottom:3}}>💻 <a href={dday.videoLink} target="_blank" rel="noreferrer" style={{color:"#60a5fa",textDecoration:"none"}}>{dday.videoLink}</a></div>}
            {/* Status quick-change */}
            <div style={{display:"flex",alignItems:"center",gap:9,marginTop:11}}>
              <span style={{fontSize:10,color:C.dim,fontWeight:700,letterSpacing:".05em"}}>QUICK STATUS:</span>
              {Object.entries(DDAY_STATUSES).map(([k,s]) => (
                <button key={k} onClick={()=>saveDday({...dday, status: k})} style={{background:(dday.status||"draft")===k?s.color+"22":"transparent",border:`1px solid ${(dday.status||"draft")===k?s.color+"66":C.border}`,borderRadius:6,padding:"3px 9px",color:(dday.status||"draft")===k?s.color:C.muted,fontSize:10,fontWeight:(dday.status||"draft")===k?800:600,cursor:"pointer",fontFamily:"inherit"}}>{s.icon} {s.label}</button>
              ))}
            </div>
          </div>
        </div>
        {/* Progress bar */}
        {checklist.length > 0 && (
          <div style={{marginTop:16}}>
            <div style={{display:"flex",alignItems:"center",justifyContent:"space-between",marginBottom:4}}>
              <div style={{fontSize:10,fontWeight:800,color:C.dim,letterSpacing:".05em"}}>OVERALL PROGRESS</div>
              <div style={{fontSize:11,fontWeight:800,color:pct===100?"#4ade80":stat.color}}>{done} / {checklist.length} · {pct}%</div>
            </div>
            <div style={{height:7,background:"#090f1c",borderRadius:4,overflow:"hidden"}}>
              <div style={{width:`${pct}%`,height:"100%",background:pct===100?"#4ade80":stat.color,borderRadius:4,transition:"width .25s"}}/>
            </div>
          </div>
        )}
      </div>
      {/* Candidates */}
      <div style={{background:C.panel,border:`1px solid ${C.border}`,borderRadius:12,padding:"14px 16px",marginBottom:14}}>
        <div style={{fontSize:10,fontWeight:800,color:C.dim,letterSpacing:".06em",marginBottom:9}}>CANDIDATES ATTENDING ({candidates.length})</div>
        {candidates.length === 0 ? (
          <div style={{fontSize:12,color:C.muted,fontStyle:"italic"}}>No candidates attached yet. Click ✏️ Edit above to add them.</div>
        ) : (
          <div style={{display:"flex",flexDirection:"column",gap:5}}>
            {candidates.map(c => (
              <div key={`${c.type}_${c.id}`} style={{display:"flex",alignItems:"center",gap:10,background:"#090f1c",borderRadius:8,padding:"8px 12px"}}>
                <Ava name={`${c.firstName||""} ${c.lastName||""}`} size={32}/>
                <div style={{flex:1,minWidth:0}}>
                  <div style={{fontSize:13,fontWeight:700}}>
                    <RecordLink onClick={()=>onOpenRecord(c.type, c.id)} color={C.text} weight={700}>{c.firstName} {c.lastName}</RecordLink>
                    <span style={{fontSize:9,fontWeight:800,color:c.type==="opp"?"#a78bfa":"#60a5fa",background:c.type==="opp"?"#1a1429":"#091420",border:`1px solid ${c.type==="opp"?"#a78bfa44":"#60a5fa44"}`,borderRadius:4,padding:"1px 5px",letterSpacing:".05em",marginLeft:7}}>{c.type.toUpperCase()}</span>
                  </div>
                  <div style={{fontSize:10,color:C.muted,marginTop:1}}>{c.email||"no email"}{c.phone?` · ${c.phone}`:""}{c.territory?` · 📍 ${c.territory}`:""}</div>
                </div>
              </div>
            ))}
          </div>
        )}
      </div>
      {/* Corporate attendees */}
      <div style={{background:C.panel,border:`1px solid ${C.border}`,borderRadius:12,padding:"14px 16px",marginBottom:14}}>
        <div style={{display:"flex",alignItems:"center",gap:9,marginBottom:9}}>
          <div style={{fontSize:10,fontWeight:800,color:C.dim,letterSpacing:".06em"}}>CORPORATE ATTENDEES ({(dday.corporateAttendees||[]).length})</div>
          <span style={{fontSize:10,color:"#4ade80",marginLeft:"auto"}}>{(dday.corporateAttendees||[]).filter(a => a.confirmed).length} confirmed · {(dday.corporateAttendees||[]).filter(a => !a.confirmed).length} pending</span>
        </div>
        {(dday.corporateAttendees||[]).length === 0 ? (
          <div style={{fontSize:12,color:C.muted,fontStyle:"italic"}}>No corporate attendees yet.</div>
        ) : (
          <div style={{display:"flex",flexDirection:"column",gap:5}}>
            {(dday.corporateAttendees||[]).map(a => (
              <div key={a.id} style={{display:"flex",alignItems:"center",gap:10,background:"#090f1c",border:`1px solid ${a.confirmed?"#4ade8055":C.border}`,borderRadius:8,padding:"8px 12px"}}>
                <Ava name={a.name||"?"} size={28}/>
                <div style={{flex:1,minWidth:0}}>
                  <div style={{fontSize:12,fontWeight:700,color:C.text}}>{a.name}{a.role?<span style={{color:C.muted,fontWeight:500,marginLeft:6}}>· {a.role}</span>:null}</div>
                  {a.email && <div style={{fontSize:10,color:C.muted,marginTop:1}}>{a.email}</div>}
                </div>
                <span style={{fontSize:9,fontWeight:800,color:a.confirmed?"#4ade80":"#facc15",background:a.confirmed?"#091c09":"#1a1908",border:`1px solid ${a.confirmed?"#4ade8055":"#facc1555"}`,borderRadius:4,padding:"2px 7px",letterSpacing:".05em"}}>{a.confirmed?"✓ CONFIRMED":"⏳ PENDING"}</span>
              </div>
            ))}
          </div>
        )}
      </div>
      {/* Checklist by category */}
      <div style={{background:C.panel,border:`1px solid ${C.border}`,borderRadius:12,padding:"14px 16px",marginBottom:14}}>
        <div style={{fontSize:10,fontWeight:800,color:C.dim,letterSpacing:".06em",marginBottom:11}}>FULL CHECKLIST</div>
        <div style={{display:"flex",flexDirection:"column",gap:10}}>
          {grouped.map(cat => {
            const catDone = cat.items.filter(it => it.done).length;
            return (
              <div key={cat.id} style={{background:"#090f1c",border:`1px solid ${C.border}`,borderRadius:9,padding:"11px 13px"}}>
                <div style={{display:"flex",alignItems:"center",gap:8,marginBottom:9}}>
                  <span style={{fontSize:16}}>{cat.icon}</span>
                  <div style={{fontSize:12,fontWeight:800,color:cat.color,letterSpacing:".04em"}}>{cat.label}</div>
                  <div style={{fontSize:10,color:C.muted,marginLeft:"auto"}}>{catDone}/{cat.items.length}</div>
                </div>
                <div style={{display:"flex",flexDirection:"column",gap:3}}>
                  {cat.items.map(it => (
                    <label key={it.id} style={{display:"flex",alignItems:"center",gap:9,padding:"5px 7px",borderRadius:5,cursor:"pointer"}}>
                      <input type="checkbox" checked={!!it.done} onChange={()=>toggleItem(it.id)}/>
                      <span style={{flex:1,fontSize:12,color:it.done?C.muted:C.text,textDecoration:it.done?"line-through":"none"}}>{it.label}</span>
                      <button onClick={(e)=>{e.preventDefault(); deleteItem(it.id);}} title="Delete checklist item" style={{background:"transparent",border:"none",color:C.dim,fontSize:13,cursor:"pointer",padding:"1px 5px",fontFamily:"inherit"}}>✕</button>
                    </label>
                  ))}
                </div>
                <div style={{display:"flex",gap:5,marginTop:7}}>
                  <input value={newItemDraft[cat.id]||""} onChange={e=>setNewItemDraft({...newItemDraft, [cat.id]: e.target.value})} placeholder="+ Add an item…" style={inp({flex:1,boxSizing:"border-box",fontSize:11,padding:"5px 9px"})} onKeyDown={e=>{ if (e.key==="Enter" && newItemDraft[cat.id]?.trim()){ addItem(newItemDraft[cat.id], cat.id); setNewItemDraft({...newItemDraft, [cat.id]:""}); } }}/>
                  <button onClick={()=>{ if (newItemDraft[cat.id]?.trim()){ addItem(newItemDraft[cat.id], cat.id); setNewItemDraft({...newItemDraft, [cat.id]:""}); } }} disabled={!newItemDraft[cat.id]?.trim()} style={{...btn(C.dim,"#60a5fa",!!newItemDraft[cat.id]?.trim()),opacity:newItemDraft[cat.id]?.trim()?1:0.4,cursor:newItemDraft[cat.id]?.trim()?"pointer":"not-allowed",padding:"3px 9px",fontSize:11}}>Add</button>
                </div>
              </div>
            );
          })}
        </div>
      </div>
      {/* Prep + recap notes */}
      <div style={{display:"grid",gridTemplateColumns:"1fr 1fr",gap:11}}>
        <div style={{background:C.panel,border:`1px solid ${C.border}`,borderRadius:12,padding:"14px 16px"}}>
          <div style={{fontSize:10,fontWeight:800,color:C.dim,letterSpacing:".06em",marginBottom:7}}>PREP NOTES</div>
          <textarea value={dday.prepNotes||""} onChange={e=>saveDday({...dday, prepNotes: e.target.value})} placeholder="Internal notes — candidate-specific objections, topics to emphasize, accommodations…" rows={4} style={inp({width:"100%",boxSizing:"border-box",resize:"vertical",fontFamily:"inherit",fontSize:12})}/>
        </div>
        <div style={{background:C.panel,border:`1px solid ${C.border}`,borderRadius:12,padding:"14px 16px"}}>
          <div style={{fontSize:10,fontWeight:800,color:C.dim,letterSpacing:".06em",marginBottom:7}}>RECAP / DEBRIEF</div>
          <textarea value={dday.recapNotes||""} onChange={e=>saveDday({...dday, recapNotes: e.target.value})} placeholder="How did the day go? Standout candidates, things to improve next time…" rows={4} style={inp({width:"100%",boxSizing:"border-box",resize:"vertical",fontFamily:"inherit",fontSize:12})}/>
        </div>
      </div>
    </div>
  );
}

// ═══════════════════════════════════════════════════════════
//  FRANCHISEE — form + detail
// ═══════════════════════════════════════════════════════════
function FranchiseeFormModal({ initial, territories, onSave, onCancel, onDelete }) {
  const isEdit = !!initial.id;
  const [form, setForm] = useState(isEdit ? initial : {
    firstName: "", lastName: "", businessName: "", email: "", phone: "",
    status: "awarded",
    territoryIds: [],
    awardedAt: "",
    plannedOpenAt: "",
    actualOpenAt: "",
    unitsOpen: 1,
    notes: "",
    notesLog: [],
  });
  const set = (k,v) => setForm(f => ({...f, [k]: v}));
  const emailIssue = validateEmail(form.email);
  const phoneIssue = validatePhone(form.phone);
  const toggleTerritory = (tid) => {
    const cur = form.territoryIds || [];
    set("territoryIds", cur.includes(tid) ? cur.filter(x => x !== tid) : [...cur, tid]);
  };
  const valid = (form.firstName||"").trim() && (form.lastName||"").trim() && !emailIssue && !phoneIssue;
  return (
    <div onClick={onCancel} style={{position:"fixed",inset:0,background:"#000c",zIndex:1100,display:"flex",alignItems:"center",justifyContent:"center",padding:16}}>
      <div onClick={e=>e.stopPropagation()} style={{background:C.panel,border:`1px solid ${C.border}`,borderRadius:14,width:"100%",maxWidth:580,maxHeight:"92vh",display:"flex",flexDirection:"column"}}>
        <div style={{display:"flex",alignItems:"center",gap:11,padding:"16px 20px",borderBottom:`1px solid ${C.border}`}}>
          <span style={{fontSize:22}}>🏪</span>
          <div style={{flex:1}}>
            <h3 style={{margin:0,fontSize:15,fontWeight:900,color:"#f0f6ff"}}>{isEdit ? "Edit Franchisee" : "New Franchisee"}</h3>
            <div style={{fontSize:11,color:C.muted,marginTop:2}}>Profile for an awarded franchisee + their territory assignments.</div>
          </div>
          <button onClick={onCancel} title="Close" style={btn(C.dim,C.muted)}>✕</button>
        </div>
        <div style={{padding:"14px 20px",overflowY:"auto",flex:1,display:"flex",flexDirection:"column",gap:11}}>
          <div style={{display:"grid",gridTemplateColumns:"1fr 1fr",gap:9}}>
            <div>
              <div style={{fontSize:9,fontWeight:800,color:C.dim,letterSpacing:".08em",marginBottom:5}}>FIRST NAME <span style={{color:"#f87171"}}>*</span></div>
              <input value={form.firstName||""} onChange={e=>set("firstName", e.target.value)} style={inp({width:"100%",boxSizing:"border-box"})} autoFocus/>
            </div>
            <div>
              <div style={{fontSize:9,fontWeight:800,color:C.dim,letterSpacing:".08em",marginBottom:5}}>LAST NAME <span style={{color:"#f87171"}}>*</span></div>
              <input value={form.lastName||""} onChange={e=>set("lastName", e.target.value)} style={inp({width:"100%",boxSizing:"border-box"})}/>
            </div>
          </div>
          <div>
            <div style={{fontSize:9,fontWeight:800,color:C.dim,letterSpacing:".08em",marginBottom:5}}>BUSINESS NAME / LLC</div>
            <input value={form.businessName||""} onChange={e=>set("businessName", e.target.value)} placeholder="e.g. Reyes Holdings LLC" style={inp({width:"100%",boxSizing:"border-box"})}/>
          </div>
          <div style={{display:"grid",gridTemplateColumns:"1fr 1fr",gap:9}}>
            <div>
              <div style={{fontSize:9,fontWeight:800,color:C.dim,letterSpacing:".08em",marginBottom:5}}>EMAIL</div>
              <div style={{position:"relative"}}>
                <input value={form.email||""} onChange={e=>set("email", e.target.value)} placeholder="jane@example.com" style={inp({width:"100%",boxSizing:"border-box",paddingRight: emailIssue?32:undefined, borderColor: emailIssue ? "#fb923c66" : undefined})}/>
                {emailIssue && <span title={emailIssue} style={{position:"absolute",right:9,top:"50%",transform:"translateY(-50%)",fontSize:13,color:"#fb923c",cursor:"help"}}>⚠️</span>}
              </div>
              {emailIssue && <div style={{fontSize:10,color:"#fb923c",marginTop:3}}>{emailIssue}</div>}
            </div>
            <div>
              <div style={{fontSize:9,fontWeight:800,color:C.dim,letterSpacing:".08em",marginBottom:5}}>PHONE</div>
              <div style={{position:"relative"}}>
                <input value={form.phone||""} onChange={e=>set("phone", e.target.value)} placeholder="555-123-4567" style={inp({width:"100%",boxSizing:"border-box",paddingRight: phoneIssue?32:undefined, borderColor: phoneIssue ? "#fb923c66" : undefined})}/>
                {phoneIssue && <span title={phoneIssue} style={{position:"absolute",right:9,top:"50%",transform:"translateY(-50%)",fontSize:13,color:"#fb923c",cursor:"help"}}>⚠️</span>}
              </div>
              {phoneIssue && <div style={{fontSize:10,color:"#fb923c",marginTop:3}}>{phoneIssue}</div>}
            </div>
          </div>
          <div>
            <div style={{fontSize:9,fontWeight:800,color:C.dim,letterSpacing:".08em",marginBottom:5}}>STATUS</div>
            <div style={{display:"flex",flexWrap:"wrap",gap:5}}>
              {Object.entries(FRANCHISEE_STATUSES).map(([k,s]) => {
                const sel = form.status === k;
                return (
                  <button key={k} onClick={()=>set("status", k)} style={{background:sel?s.color+"22":"#090f1c",border:`1.5px solid ${sel?s.color+"77":C.border}`,borderRadius:8,padding:"7px 11px",color:sel?s.color:C.text,fontSize:11,fontWeight:sel?800:600,cursor:"pointer",fontFamily:"inherit",display:"flex",alignItems:"center",gap:5}} title={s.desc}>
                    <span>{s.icon}</span><span>{s.label}</span>
                  </button>
                );
              })}
            </div>
          </div>
          <div style={{display:"grid",gridTemplateColumns:"1fr 1fr 1fr",gap:9}}>
            <div>
              <div style={{fontSize:9,fontWeight:800,color:C.dim,letterSpacing:".08em",marginBottom:5}}>AWARDED</div>
              <input type="date" value={form.awardedAt||""} onChange={e=>set("awardedAt", e.target.value)} style={inp({width:"100%",boxSizing:"border-box"})}/>
            </div>
            <div>
              <div style={{fontSize:9,fontWeight:800,color:C.dim,letterSpacing:".08em",marginBottom:5}}>PLANNED OPEN</div>
              <input type="date" value={form.plannedOpenAt||""} onChange={e=>set("plannedOpenAt", e.target.value)} style={inp({width:"100%",boxSizing:"border-box"})}/>
            </div>
            <div>
              <div style={{fontSize:9,fontWeight:800,color:C.dim,letterSpacing:".08em",marginBottom:5}}>ACTUAL OPEN</div>
              <input type="date" value={form.actualOpenAt||""} onChange={e=>set("actualOpenAt", e.target.value)} style={inp({width:"100%",boxSizing:"border-box"})}/>
            </div>
          </div>
          <div>
            <div style={{fontSize:9,fontWeight:800,color:C.dim,letterSpacing:".08em",marginBottom:5}}>UNITS OPEN</div>
            <input type="number" min="0" value={form.unitsOpen||0} onChange={e=>set("unitsOpen", Number(e.target.value))} style={inp({width:120,boxSizing:"border-box"})}/>
          </div>
          {/* Territory multi-assign */}
          <div>
            <div style={{fontSize:9,fontWeight:800,color:C.dim,letterSpacing:".08em",marginBottom:5}}>TERRITORY ASSIGNMENTS ({(form.territoryIds||[]).length})</div>
            <div style={{fontSize:10,color:C.muted,marginBottom:6,lineHeight:1.4}}>Pick one or more territories from your Territory Map. Multiple franchisees can share a territory.</div>
            {territories.length === 0 ? (
              <div style={{fontSize:11,color:C.muted,fontStyle:"italic",padding:"8px 0"}}>No territories created yet — add them from the Territories tab first.</div>
            ) : (
              <div style={{maxHeight:200,overflowY:"auto",background:"#090f1c",border:`1px solid ${C.border}`,borderRadius:8,padding:6}}>
                {territories.map(t => {
                  const sel = (form.territoryIds||[]).includes(t.id);
                  const status = territoryStatus(t);
                  const sDef = TERRITORY_STATUSES[status];
                  // Available + restricted territories aren't assignable to a franchisee.
                  // Available specifically: it's unsold, so a franchisee holding it would be a logical contradiction.
                  const disabled = sDef && sDef.assignable === false;
                  const reason = status === "available"
                    ? "Available territories are unsold — change status to Active or Resale first."
                    : sDef?.restricted ? "Restricted (compliance) territories can't be owned." : "";
                  return (
                    <label key={t.id} title={disabled?reason:undefined} style={{display:"flex",alignItems:"center",gap:9,padding:"6px 8px",borderRadius:5,cursor:disabled?"not-allowed":"pointer",background:sel?"#162035":"transparent",opacity:disabled?0.5:1}}>
                      <input type="checkbox" checked={sel} disabled={disabled} onChange={()=>!disabled && toggleTerritory(t.id)}/>
                      <span style={{fontSize:13,flexShrink:0}}>{sDef?.icon||"📍"}</span>
                      <span style={{fontSize:12,color:C.text,flex:1,minWidth:0,overflow:"hidden",textOverflow:"ellipsis",whiteSpace:"nowrap"}}>{t.label}</span>
                      <span style={{fontSize:9,fontWeight:700,color:sDef?.color||C.muted,background:(sDef?.color||C.muted)+"15",border:`1px solid ${(sDef?.color||C.muted)}44`,borderRadius:4,padding:"1px 5px",letterSpacing:".05em"}}>{sDef?.label||"Active"}</span>
                    </label>
                  );
                })}
              </div>
            )}
          </div>
          <div>
            <div style={{fontSize:9,fontWeight:800,color:C.dim,letterSpacing:".08em",marginBottom:5}}>QUICK NOTES</div>
            <textarea value={form.notes||""} onChange={e=>set("notes", e.target.value)} placeholder="Anything important about this franchisee — relationships, expansion plans, history…" rows={3} style={inp({width:"100%",boxSizing:"border-box",resize:"vertical",fontFamily:"inherit"})}/>
          </div>
        </div>
        <div style={{display:"flex",justifyContent:"space-between",gap:8,padding:"12px 20px",borderTop:`1px solid ${C.border}`}}>
          {onDelete ? <button onClick={onDelete} style={btn("#1a0808","#f87171")}>🗑 Remove</button> : <span/>}
          <div style={{display:"flex",gap:8}}>
            <button onClick={onCancel} style={btn(C.dim,C.muted)}>Cancel</button>
            <button onClick={()=>valid && onSave(form)} disabled={!valid} style={{...btn("#091c09","#4ade80",valid),opacity:valid?1:0.5,cursor:valid?"pointer":"not-allowed"}}>{isEdit?"Save Changes":"Add Franchisee"}</button>
          </div>
        </div>
      </div>
    </div>
  );
}

function FranchiseeDetail({ fr, onBack, onEdit, onDelete, territories, bFranchisees, openFranchisee, addNote, deleteNote }) {
  const [noteDraft, setNoteDraft] = useState("");
  const stat = FRANCHISEE_STATUSES[fr.status||"awarded"];
  const myTerritories = (fr.territoryIds||[]).map(tid => territories.find(t => t.id === tid)).filter(Boolean);
  // Co-franchisees: anyone else assigned to any of my territories
  const coMap = new Map();
  myTerritories.forEach(t => {
    bFranchisees.filter(f => f.id !== fr.id && (f.territoryIds||[]).includes(t.id)).forEach(f => {
      if (!coMap.has(f.id)) coMap.set(f.id, { fr: f, sharedTerritories: [] });
      coMap.get(f.id).sharedTerritories.push(t.label);
    });
  });
  const coFranchisees = [...coMap.values()];
  const notesLog = (fr.notesLog||[]).slice().sort((a,b) => new Date(b.at) - new Date(a.at));
  return (
    <div>
      <div style={{display:"flex",alignItems:"center",gap:9,marginBottom:14}}>
        <button onClick={onBack} style={btn(C.dim,C.muted)} title="Back to Franchisee Hub">← Franchisee Hub</button>
        <div style={{marginLeft:"auto",display:"flex",gap:7}}>
          <button onClick={onEdit} style={btn(C.dim,C.muted)} title="Edit franchisee details">✏️ Edit</button>
          <button onClick={onDelete} style={btn("#1a0808","#f87171")} title="Remove franchisee">🗑 Remove</button>
        </div>
      </div>
      <div style={{background:C.panel,border:`1px solid ${stat.color}55`,borderRadius:14,padding:"20px 24px",marginBottom:14,boxShadow:`0 0 18px ${stat.color}22`,display:"flex",gap:18,alignItems:"flex-start"}}>
        <Ava name={`${fr.firstName||""} ${fr.lastName||""}`} size={64}/>
        <div style={{flex:1,minWidth:0}}>
          <div style={{display:"flex",alignItems:"center",gap:9,flexWrap:"wrap",marginBottom:5}}>
            <div style={{fontSize:22,fontWeight:900,color:"#f0f6ff"}}>{fr.firstName} {fr.lastName}</div>
            <span style={{fontSize:10,fontWeight:800,color:stat.color,background:stat.color+"15",border:`1px solid ${stat.color}44`,borderRadius:5,padding:"2px 8px",letterSpacing:".05em"}}>{stat.icon} {stat.label.toUpperCase()}</span>
          </div>
          {fr.businessName && <div style={{fontSize:13,color:C.muted,marginBottom:7}}>{fr.businessName}</div>}
          <div style={{fontSize:12,color:C.muted}}>{[fr.email,fr.phone].filter(Boolean).join(" · ") || <em>no contact info</em>}</div>
          {(fr.awardedAt || fr.plannedOpenAt || fr.actualOpenAt) && (
            <div style={{display:"flex",gap:14,marginTop:9,fontSize:11,color:C.muted,flexWrap:"wrap"}}>
              {fr.awardedAt && <span>🎉 Awarded: <strong style={{color:C.text}}>{new Date(fr.awardedAt).toLocaleDateString()}</strong></span>}
              {fr.plannedOpenAt && <span>📅 Planned open: <strong style={{color:C.text}}>{new Date(fr.plannedOpenAt).toLocaleDateString()}</strong></span>}
              {fr.actualOpenAt && <span>🟢 Opened: <strong style={{color:C.text}}>{new Date(fr.actualOpenAt).toLocaleDateString()}</strong></span>}
              {fr.unitsOpen > 0 && <span>🏪 Units open: <strong style={{color:C.text}}>{fr.unitsOpen}</strong></span>}
            </div>
          )}
          {fr.notes && <div style={{fontSize:12,color:C.muted,marginTop:10,background:"#090f1c",border:`1px solid ${C.border}`,borderRadius:7,padding:"9px 12px",lineHeight:1.55,whiteSpace:"pre-wrap"}}>{fr.notes}</div>}
        </div>
      </div>
      {/* Territories */}
      <div style={{background:C.panel,border:`1px solid ${C.border}`,borderRadius:12,padding:"14px 16px",marginBottom:14}}>
        <div style={{fontSize:10,fontWeight:800,color:C.dim,letterSpacing:".06em",marginBottom:9}}>TERRITORY ASSIGNMENTS ({myTerritories.length})</div>
        {myTerritories.length === 0 ? (
          <div style={{fontSize:12,color:C.muted,fontStyle:"italic"}}>No territories assigned. Edit this franchisee to attach territories from the Territory Map.</div>
        ) : (
          <div style={{display:"flex",flexWrap:"wrap",gap:7}}>
            {myTerritories.map(t => (
              <div key={t.id} style={{background:"#090f1c",border:`1px solid ${t.restricted?"#fb923c55":"#a78bfa55"}`,borderRadius:8,padding:"7px 11px",fontSize:12,color:C.text,display:"flex",alignItems:"center",gap:7}}>
                <span>{t.restricted?"🚫":"📍"}</span>
                <span style={{fontWeight:700}}>{t.label}</span>
                {t.restricted && <span style={{fontSize:9,fontWeight:700,color:"#fb923c",letterSpacing:".05em"}}>OFF-LIMITS</span>}
              </div>
            ))}
          </div>
        )}
      </div>
      {/* Co-franchisees sharing my territories */}
      {coFranchisees.length > 0 && (
        <div style={{background:C.panel,border:`1px solid ${C.border}`,borderRadius:12,padding:"14px 16px",marginBottom:14}}>
          <div style={{fontSize:10,fontWeight:800,color:C.dim,letterSpacing:".06em",marginBottom:9}}>OTHER FRANCHISEES IN THESE TERRITORIES ({coFranchisees.length})</div>
          <div style={{display:"flex",flexDirection:"column",gap:6}}>
            {coFranchisees.map(({fr: co, sharedTerritories}) => {
              const cs = FRANCHISEE_STATUSES[co.status||"awarded"];
              return (
                <div key={co.id} style={{display:"flex",alignItems:"center",gap:10,background:"#090f1c",border:`1px solid ${C.border}`,borderRadius:8,padding:"8px 12px",cursor:"pointer"}} onClick={()=>openFranchisee(co.id)}>
                  <Ava name={`${co.firstName||""} ${co.lastName||""}`} size={28}/>
                  <div style={{flex:1,minWidth:0}}>
                    <div style={{fontSize:12,fontWeight:700,color:C.text}}>{co.firstName} {co.lastName} <span style={{fontSize:9,fontWeight:800,color:cs.color,marginLeft:6}}>{cs.icon} {cs.label}</span></div>
                    <div style={{fontSize:10,color:C.muted,marginTop:1}}>Shared: {sharedTerritories.join(", ")}</div>
                  </div>
                </div>
              );
            })}
          </div>
        </div>
      )}
      {/* Notes log */}
      <div style={{background:C.panel,border:`1px solid ${C.border}`,borderRadius:12,padding:"14px 16px"}}>
        <div style={{fontSize:10,fontWeight:800,color:C.dim,letterSpacing:".06em",marginBottom:9}}>NOTES LOG ({notesLog.length})</div>
        <div style={{display:"flex",gap:8,marginBottom:11}}>
          <textarea value={noteDraft} onChange={e=>setNoteDraft(e.target.value)} placeholder="Add a note — recent conversation, opening update, milestone, issue…" rows={2} style={inp({flex:1,boxSizing:"border-box",resize:"vertical",fontFamily:"inherit"})}/>
          <button onClick={()=>{ addNote(noteDraft); setNoteDraft(""); }} disabled={!noteDraft.trim()} style={{...btn("#091c09","#4ade80",!!noteDraft.trim()),opacity:noteDraft.trim()?1:0.5,cursor:noteDraft.trim()?"pointer":"not-allowed",alignSelf:"flex-start"}}>+ Add Note</button>
        </div>
        {notesLog.length === 0 ? (
          <div style={{fontSize:12,color:C.muted,fontStyle:"italic"}}>No notes yet — log conversations, opening updates, milestones, etc.</div>
        ) : (
          <div style={{display:"flex",flexDirection:"column",gap:7}}>
            {notesLog.map(n => (
              <div key={n.id} style={{background:"#090f1c",border:`1px solid ${C.border}`,borderRadius:8,padding:"9px 12px"}}>
                <div style={{display:"flex",alignItems:"center",justifyContent:"space-between",marginBottom:5}}>
                  <div style={{fontSize:10,color:C.dim,fontWeight:700}}>{new Date(n.at).toLocaleString("en-US",{month:"short",day:"numeric",year:"numeric",hour:"numeric",minute:"2-digit"})}</div>
                  <button onClick={()=>{ if(confirm("Delete this note?")) deleteNote(n.id); }} title="Delete note" style={{background:"transparent",border:"none",color:C.dim,fontSize:11,cursor:"pointer",fontFamily:"inherit",padding:"2px 6px"}}>✕</button>
                </div>
                <div style={{fontSize:12,color:C.text,lineHeight:1.55,whiteSpace:"pre-wrap"}}>{n.body}</div>
              </div>
            ))}
          </div>
        )}
      </div>
    </div>
  );
}

function FolderModal({ initial, folders, onSave, onCancel, onDelete }) {
  const [form, setForm] = useState(initial);
  const set = (k,v) => setForm(p => ({...p, [k]:v}));
  // Build parent picker options — exclude self + all descendants of self to prevent cycles
  const banned = new Set([form.id]);
  if (form.id) {
    let changed = true;
    while (changed) {
      changed = false;
      folders.forEach(f => { if (banned.has(f.parentId) && !banned.has(f.id)) { banned.add(f.id); changed = true; } });
    }
  }
  const parentOptions = folders.filter(f => !banned.has(f.id));
  const valid = (form.name||"").trim().length > 0;
  return (
    <div onClick={onCancel} style={{position:"fixed",inset:0,background:"#000c",zIndex:1100,display:"flex",alignItems:"center",justifyContent:"center",padding:16}}>
      <div onClick={e=>e.stopPropagation()} style={{background:C.panel,border:`1px solid ${C.border}`,borderRadius:14,padding:22,width:"100%",maxWidth:460}}>
        <div style={{display:"flex",alignItems:"center",gap:11,marginBottom:14}}>
          <span style={{fontSize:24}}>{form.icon||"📁"}</span>
          <div style={{flex:1}}>
            <h3 style={{margin:0,color:"#f0f6ff",fontWeight:900,fontSize:16}}>{initial.id?"Edit folder":"New folder"}</h3>
            <div style={{fontSize:11,color:C.muted,marginTop:3}}>Organize templates and (in SaaS) share across seats.</div>
          </div>
          <button onClick={onCancel} title="Close" style={btn(C.dim,C.muted)}>✕</button>
        </div>

        <div style={{fontSize:9,fontWeight:800,color:C.dim,letterSpacing:".08em",marginBottom:6}}>NAME</div>
        <input value={form.name||""} onChange={e=>set("name", e.target.value)} placeholder="e.g. Validation Calls" style={inp({width:"100%",boxSizing:"border-box",marginBottom:12})} autoFocus/>

        <div style={{fontSize:9,fontWeight:800,color:C.dim,letterSpacing:".08em",marginBottom:6}}>ICON</div>
        <div style={{display:"flex",flexWrap:"wrap",gap:6,marginBottom:14,maxHeight:96,overflowY:"auto",background:"#090f1c",border:`1px solid ${C.border}`,borderRadius:8,padding:8}}>
          {FOLDER_ICONS.map(ic => (
            <button key={ic} onClick={()=>set("icon", ic)} title={ic} style={{width:30,height:30,background:form.icon===ic?"#162035":"transparent",border:`1px solid ${form.icon===ic?C.accent+"88":"transparent"}`,borderRadius:6,fontSize:16,cursor:"pointer",fontFamily:"inherit",display:"flex",alignItems:"center",justifyContent:"center"}}>{ic}</button>
          ))}
        </div>

        <div style={{fontSize:9,fontWeight:800,color:C.dim,letterSpacing:".08em",marginBottom:6}}>PARENT FOLDER</div>
        <select value={form.parentId||""} onChange={e=>set("parentId", e.target.value||null)} style={inp({width:"100%",boxSizing:"border-box",marginBottom:14,fontSize:12,padding:"9px 10px"})}>
          <option value="">— None (top-level) —</option>
          {parentOptions.map(f => <option key={f.id} value={f.id}>{f.icon||"📁"} {f.name}</option>)}
        </select>

        <div style={{fontSize:9,fontWeight:800,color:C.dim,letterSpacing:".08em",marginBottom:6}}>SCOPE</div>
        <div style={{display:"flex",gap:8,marginBottom:6}}>
          {[
            {k:"private",     label:"🔒 Private",     desc:"Visible only to you."},
            {k:"group",       label:"🌐 Group",       desc:"Shared across seats on this brand."},
            {k:"multi_brand", label:"🏢 Multi-Brand", desc:"Shared across every brand in your org."}
          ].map(opt => {
            const sel = (form.scope||"private") === opt.k;
            return (
              <button key={opt.k} onClick={()=>set("scope", opt.k)} style={{flex:1,background:sel?"#162035":"#090f1c",border:`1.5px solid ${sel?C.accent+"88":C.border}`,borderRadius:9,padding:"10px 12px",textAlign:"left",cursor:"pointer",fontFamily:"inherit",color:C.text}}>
                <div style={{fontSize:12,fontWeight:800,color:sel?C.accent:C.text}}>{opt.label}</div>
                <div style={{fontSize:10,color:C.muted,marginTop:3,lineHeight:1.4}}>{opt.desc}</div>
              </button>
            );
          })}
        </div>
        {(form.scope||"private")==="group" && <div style={{fontSize:10,color:"#facc15",background:"#1a1908",border:"1px solid #facc1533",borderRadius:6,padding:"7px 10px",marginBottom:14,marginTop:8,lineHeight:1.5}}>⚠️ Sharing activates once SaaS multi-seat is live. For now, group folders behave like private but are tagged for migration.</div>}
        {(form.scope||"private")==="multi_brand" && <div style={{fontSize:10,color:"#a78bfa",background:"#1a1429",border:"1px solid #a78bfa33",borderRadius:6,padding:"7px 10px",marginBottom:14,marginTop:8,lineHeight:1.5}}>🏢 This folder will be visible from every brand's Templates view. Useful for org-wide assets (legal disclaimers, master email templates, cross-brand promos). Behavior is tagged now and activated when SaaS multi-seat is live.</div>}
        {(form.scope||"private")==="private" && <div style={{marginBottom:14}}/>}

        <div style={{display:"flex",justifyContent:"space-between",gap:8}}>
          {onDelete ? (
            <button onClick={onDelete} style={btn("#1a0808","#f87171")} title="Delete folder">🗑 Delete</button>
          ) : <span/>}
          <div style={{display:"flex",gap:8}}>
            <button onClick={onCancel} style={btn(C.dim,C.muted)}>Cancel</button>
            <button onClick={()=>valid && onSave(form)} disabled={!valid} style={{...btn("#091c09","#4ade80",valid),opacity:valid?1:0.5,cursor:valid?"pointer":"not-allowed"}}>{initial.id?"Save":"Create folder"}</button>
          </div>
        </div>
      </div>
    </div>
  );
}

// ═══════════════════════════════════════════════════════════
//  SCHEDULING MODALS
// ═══════════════════════════════════════════════════════════
function EventTypeFormModal({ initial, onSave, onCancel }) {
  const [form, setForm] = useState(initial || {
    name: "", durationMin: 30, description: "", location: "phone", color: "#60a5fa", icon: "📞",
    confirmationSubject: "", confirmationBody: "", linkedStageHint: "", bufferBeforeMin: 0, bufferAfterMin: 0,
    active: true,
  });
  const set = (k,v) => setForm(f => ({...f, [k]: v}));
  return (
    <div style={{position:"fixed",inset:0,background:"#000c",zIndex:1001,display:"flex",alignItems:"center",justifyContent:"center",padding:16}}>
      <div style={{background:C.panel,border:`1px solid ${C.border}`,borderRadius:16,padding:24,width:"100%",maxWidth:520,maxHeight:"92vh",overflowY:"auto"}}>
        <div style={{display:"flex",alignItems:"center",gap:11,marginBottom:14}}>
          <span style={{fontSize:24}}>{form.icon||"📅"}</span>
          <h3 style={{flex:1,margin:0,color:"#f0f6ff",fontWeight:900,fontSize:16}}>{initial?.id ? "Edit Event Type" : "New Event Type"}</h3>
          <button onClick={onCancel} title="Close" style={btn(C.dim,C.muted)}>✕</button>
        </div>
        <div style={{marginBottom:10}}>
          <label style={{display:"block",fontSize:11,color:C.dim,fontWeight:700,marginBottom:3}}>Name</label>
          <input autoFocus value={form.name} onChange={e=>set("name",e.target.value)} placeholder="e.g. 30-min Intro Call" style={inp({width:"100%",boxSizing:"border-box"})}/>
        </div>
        <div style={{display:"flex",gap:8,marginBottom:10}}>
          <div style={{flex:1}}>
            <label style={{display:"block",fontSize:11,color:C.dim,fontWeight:700,marginBottom:3}}>Duration (min)</label>
            <select value={form.durationMin} onChange={e=>set("durationMin",+e.target.value)} style={inp({width:"100%",boxSizing:"border-box"})}>
              {[15,30,45,60,90,120].map(d=><option key={d} value={d}>{d} minutes</option>)}
            </select>
          </div>
          <div style={{flex:1}}>
            <label style={{display:"block",fontSize:11,color:C.dim,fontWeight:700,marginBottom:3}}>Location</label>
            <select value={form.location} onChange={e=>set("location",e.target.value)} style={inp({width:"100%",boxSizing:"border-box"})}>
              <option value="phone">📞 Phone</option>
              <option value="video">💻 Video Call</option>
              <option value="in_person">🏢 In Person</option>
              <option value="custom">🔗 Custom</option>
            </select>
          </div>
        </div>
        <div style={{display:"flex",gap:8,alignItems:"center",marginBottom:10}}>
          <label style={{fontSize:11,color:C.dim,fontWeight:700}}>Color</label>
          <input type="color" value={form.color} onChange={e=>set("color",e.target.value)} style={{width:36,height:32,border:"none",borderRadius:6,cursor:"pointer"}}/>
          <label style={{fontSize:11,color:C.dim,fontWeight:700,marginLeft:8}}>Icon</label>
          <input value={form.icon} onChange={e=>set("icon",e.target.value)} maxLength={2} style={{...inp({width:54,textAlign:"center",fontSize:18})}}/>
        </div>
        <div style={{marginBottom:10}}>
          <label style={{display:"block",fontSize:11,color:C.dim,fontWeight:700,marginBottom:3}}>Description</label>
          <textarea value={form.description} onChange={e=>set("description",e.target.value)} rows={2} placeholder="What is this meeting for?" style={{...inp({width:"100%",boxSizing:"border-box",resize:"vertical",fontFamily:"inherit"})}}/>
        </div>
        <div style={{marginBottom:10}}>
          <label style={{display:"block",fontSize:11,color:C.dim,fontWeight:700,marginBottom:3}}>Confirmation Subject Line</label>
          <input value={form.confirmationSubject} onChange={e=>set("confirmationSubject",e.target.value)} placeholder="Use {FIRST_NAME}, {BRAND}, {DATE}, {TIME}, {REP_NAME}" style={inp({width:"100%",boxSizing:"border-box"})}/>
        </div>
        <div style={{marginBottom:14}}>
          <label style={{display:"block",fontSize:11,color:C.dim,fontWeight:700,marginBottom:3}}>Confirmation Email Body (template)</label>
          <textarea value={form.confirmationBody} onChange={e=>set("confirmationBody",e.target.value)} rows={5} placeholder={`Hi {FIRST_NAME},\n\nWe're confirmed for {DATE} at {TIME}.\n\n{REP_NAME}`} style={{...inp({width:"100%",boxSizing:"border-box",resize:"vertical",fontFamily:"inherit",fontSize:12})}}/>
        </div>
        <div style={{display:"flex",gap:10,justifyContent:"flex-end"}}>
          <button onClick={onCancel} style={btn(C.dim,C.muted)}>Cancel</button>
          <button onClick={()=>{ if(!form.name.trim()){alert("Name is required.");return;} onSave(form); }} style={btn("#091c09","#4ade80",true)}>{initial?.id?"Save Changes":"Create Event Type"}</button>
        </div>
      </div>
    </div>
  );
}

function BookMeetingModal({ eventTypes, availability, existingBookings, assignableRecords, initialRecord, onSave, onCancel }) {
  const [step, setStep] = useState("pick"); // pick | slot | confirm
  const [eventTypeId, setEventTypeId] = useState(eventTypes[0]?.id || "");
  const [recordRef, setRecordRef] = useState(initialRecord ? { type: initialRecord.recType || "lead", id: initialRecord.id, name: `${initialRecord.firstName} ${initialRecord.lastName}` } : null);
  const [recordPartners, setRecordPartners] = useState(initialRecord?.partners || []);
  const [attendeeName, setAttendeeName] = useState(initialRecord ? `${initialRecord.firstName} ${initialRecord.lastName}` : "");
  const [attendeeEmail, setAttendeeEmail] = useState(initialRecord?.email || "");
  const [searchA, setSearchA] = useState("");
  const [dayISO, setDayISO] = useState(() => {
    // Default to tomorrow
    const d = new Date(); d.setDate(d.getDate()+1);
    return d.toISOString().slice(0,10);
  });
  const [slotISO, setSlotISO] = useState(null);
  const [notes, setNotes] = useState("");
  const [addAttendees, setAddAttendees] = useState(false);
  const [extraAttendees, setExtraAttendees] = useState([]); // [{name, email}]

  const et = eventTypes.find(e => e.id === eventTypeId);
  const slots = et ? generateAvailableSlots({ availability, existingBookings, durationMin: et.durationMin, dayISO }) : [];

  // Day picker — next 14 days starting from today
  const days = Array.from({length:14}, (_, i) => {
    const d = new Date(); d.setDate(d.getDate()+i);
    return d.toISOString().slice(0,10);
  });
  const formatDayShort = (iso) => {
    const d = new Date(iso+"T12:00:00");
    return d.toLocaleDateString("en-US", { weekday:"short", month:"short", day:"numeric" });
  };
  const formatTime = (iso) => new Date(iso).toLocaleTimeString("en-US", { hour:"numeric", minute:"2-digit" });

  const isVideo = et?.location === "video";
  const cleanExtras = extraAttendees.map(a => ({ name: (a.name||"").trim(), email: (a.email||"").trim() })).filter(a => a.email);

  const canSubmit = eventTypeId && slotISO && attendeeEmail.trim();
  const submit = () => {
    if (!canSubmit) return;
    const et2 = eventTypes.find(e => e.id === eventTypeId);
    const slot = slots.find(s => s.startISO === slotISO);
    if (!et2 || !slot) return;
    onSave({
      eventTypeId, recordRef, attendeeName: attendeeName.trim(), attendeeEmail: attendeeEmail.trim(),
      additionalAttendees: (et2.location === "video" && addAttendees) ? cleanExtras : [],
      startISO: slot.startISO, endISO: slot.endISO, timeZone: resolveTz(availability.timeZone),
      location: et2.location, notes: notes.trim(),
    });
  };

  const addBusinessPartners = () => {
    if (!recordPartners?.length) return;
    setExtraAttendees(prev => {
      const existing = new Set(prev.map(a => (a.email||"").toLowerCase()));
      const additions = recordPartners
        .filter(p => p.email && !existing.has(p.email.toLowerCase()))
        .map(p => ({ name: `${p.firstName||""} ${p.lastName||""}`.trim(), email: p.email }));
      return [...prev, ...additions];
    });
  };

  const filteredRecords = assignableRecords.filter(r => !searchA.trim() || `${r.firstName} ${r.lastName}`.toLowerCase().includes(searchA.toLowerCase()));

  return (
    <div style={{position:"fixed",inset:0,background:"#000c",zIndex:1001,display:"flex",alignItems:"center",justifyContent:"center",padding:16}}>
      <div style={{background:C.panel,border:`1px solid ${C.border}`,borderRadius:16,padding:24,width:"100%",maxWidth:620,maxHeight:"92vh",overflowY:"auto"}}>
        <div style={{display:"flex",alignItems:"center",gap:11,marginBottom:14}}>
          <span style={{fontSize:24}}>📅</span>
          <h3 style={{flex:1,margin:0,color:"#f0f6ff",fontWeight:900,fontSize:16}}>Schedule Meeting</h3>
          <button onClick={onCancel} title="Close" style={btn(C.dim,C.muted)}>✕</button>
        </div>

        {/* Record picker */}
        <div style={{marginBottom:12}}>
          <label style={{display:"block",fontSize:11,color:C.dim,fontWeight:700,marginBottom:4}}>Candidate</label>
          {recordRef ? (
            <div style={{display:"flex",alignItems:"center",gap:8,background:"#091420",border:"1px solid #60a5fa44",borderRadius:8,padding:"8px 12px"}}>
              <span style={{fontSize:14}}>👤</span>
              <div style={{flex:1,fontSize:13,color:"#60a5fa",fontWeight:700}}>{recordRef.name}</div>
              <button onClick={()=>{ setRecordRef(null); setRecordPartners([]); setAttendeeName(""); setAttendeeEmail(""); }} style={{...btn(C.dim,C.muted),fontSize:10}}>Change</button>
            </div>
          ) : (
            <div style={{background:"#090f1c",border:`1px solid ${C.border}`,borderRadius:8,padding:8}}>
              <input value={searchA} onChange={e=>setSearchA(e.target.value)} placeholder="Search leads & opps…" style={{...inp({width:"100%",boxSizing:"border-box"}),marginBottom:6}}/>
              <div style={{maxHeight:160,overflowY:"auto"}}>
                {filteredRecords.length === 0 && <div style={{padding:8,fontSize:11,color:C.dim,textAlign:"center"}}>No matches.</div>}
                {filteredRecords.slice(0,30).map(r => (
                  <div key={r.id} onClick={()=>{ setRecordRef({type:r.recType,id:r.id,name:`${r.firstName} ${r.lastName}`}); setRecordPartners(r.partners||[]); setAttendeeName(`${r.firstName} ${r.lastName}`); setAttendeeEmail(r.email||""); }} style={{display:"flex",alignItems:"center",gap:8,padding:"6px 8px",borderRadius:6,cursor:"pointer"}} onMouseEnter={e=>e.currentTarget.style.background="#162035"} onMouseLeave={e=>e.currentTarget.style.background="transparent"}>
                    <span style={{fontSize:12,color:C.text,flex:1}}>{r.firstName} {r.lastName}</span>
                    <span style={{fontSize:9,color:r.recType==="opp"?"#4ade80":"#60a5fa",background:r.recType==="opp"?"#091c09":"#091420",border:`1px solid ${r.recType==="opp"?"#4ade8033":"#60a5fa33"}`,borderRadius:4,padding:"1px 6px",fontWeight:700,textTransform:"uppercase"}}>{r.recType}</span>
                  </div>
                ))}
              </div>
            </div>
          )}
        </div>

        {/* Event type picker */}
        <div style={{marginBottom:12}}>
          <label style={{display:"block",fontSize:11,color:C.dim,fontWeight:700,marginBottom:4}}>Event Type</label>
          <div style={{display:"grid",gridTemplateColumns:"repeat(auto-fill,minmax(140px,1fr))",gap:6}}>
            {eventTypes.map(e => (
              <button key={e.id} onClick={()=>setEventTypeId(e.id)} style={{background:eventTypeId===e.id?e.color+"22":"#090f1c",border:`1.5px solid ${eventTypeId===e.id?e.color:C.border}`,borderRadius:8,padding:"8px 10px",cursor:"pointer",fontFamily:"inherit",textAlign:"left"}}>
                <div style={{fontSize:16,marginBottom:2}}>{e.icon}</div>
                <div style={{fontSize:11,fontWeight:700,color:eventTypeId===e.id?e.color:C.text}}>{e.name}</div>
                <div style={{fontSize:10,color:C.muted,marginTop:2}}>{e.durationMin}m · {e.location}</div>
              </button>
            ))}
          </div>
        </div>

        {/* Day picker */}
        <div style={{marginBottom:12}}>
          <label style={{display:"block",fontSize:11,color:C.dim,fontWeight:700,marginBottom:4}}>Day</label>
          <div style={{display:"flex",gap:5,overflowX:"auto",paddingBottom:4}}>
            {days.map(d => (
              <button key={d} onClick={()=>{ setDayISO(d); setSlotISO(null); }} style={{flexShrink:0,background:dayISO===d?"#162035":"#090f1c",border:`1px solid ${dayISO===d?C.accent:C.border}`,borderRadius:6,padding:"6px 10px",cursor:"pointer",fontFamily:"inherit",color:dayISO===d?C.accent:C.text,fontSize:11,fontWeight:dayISO===d?700:500,minWidth:78}}>
                {formatDayShort(d)}
              </button>
            ))}
          </div>
        </div>

        {/* Slot picker */}
        <div style={{marginBottom:12}}>
          <label style={{display:"block",fontSize:11,color:C.dim,fontWeight:700,marginBottom:4}}>Available Time Slots</label>
          {slots.length === 0 ? (
            <div style={{padding:14,background:"#090f1c",border:`1px solid ${C.border}`,borderRadius:8,fontSize:11,color:C.muted,textAlign:"center"}}>
              No availability on {formatDayShort(dayISO)}. Try another day or expand your weekly hours in <strong style={{color:C.text}}>Scheduling → Availability</strong>.
            </div>
          ) : (
            <div style={{display:"grid",gridTemplateColumns:"repeat(auto-fill,minmax(100px,1fr))",gap:5,maxHeight:200,overflowY:"auto",background:"#090f1c",border:`1px solid ${C.border}`,borderRadius:8,padding:8}}>
              {slots.map(s => (
                <button key={s.startISO} onClick={()=>setSlotISO(s.startISO)} style={{background:slotISO===s.startISO?"#091c09":"transparent",border:`1px solid ${slotISO===s.startISO?"#4ade80":C.border}`,borderRadius:5,padding:"6px 8px",fontSize:11,color:slotISO===s.startISO?"#4ade80":C.text,fontWeight:slotISO===s.startISO?700:500,cursor:"pointer",fontFamily:"inherit"}}>
                  {formatTime(s.startISO)}
                </button>
              ))}
            </div>
          )}
        </div>

        {/* Attendee info */}
        <div style={{display:"flex",gap:8,marginBottom:10}}>
          <div style={{flex:1}}>
            <label style={{display:"block",fontSize:11,color:C.dim,fontWeight:700,marginBottom:3}}>Attendee Name</label>
            <input value={attendeeName} onChange={e=>setAttendeeName(e.target.value)} style={inp({width:"100%",boxSizing:"border-box"})}/>
          </div>
          <div style={{flex:1}}>
            <label style={{display:"block",fontSize:11,color:C.dim,fontWeight:700,marginBottom:3}}>Email</label>
            <input value={attendeeEmail} onChange={e=>setAttendeeEmail(e.target.value)} placeholder="attendee@example.com" style={inp({width:"100%",boxSizing:"border-box"})}/>
          </div>
        </div>

        {isVideo && (
          <div style={{marginBottom:12,background:"#090f1c",border:`1px solid ${C.border}`,borderRadius:8,padding:"10px 12px"}}>
            <label style={{display:"flex",alignItems:"center",gap:8,cursor:"pointer",fontSize:12,color:C.text,fontWeight:700}}>
              <input type="checkbox" checked={addAttendees} onChange={e=>setAddAttendees(e.target.checked)} style={{accentColor:"#4ade80"}}/>
              <span>Add more attendees to the invite</span>
            </label>
            {addAttendees && (
              <div style={{marginTop:10}}>
                {extraAttendees.length === 0 && (
                  <div style={{fontSize:11,color:C.dim,marginBottom:8}}>No additional attendees yet. Add manually below or pull from this candidate's business partners.</div>
                )}
                {extraAttendees.map((a, i) => (
                  <div key={i} style={{display:"flex",gap:6,marginBottom:6}}>
                    <input value={a.name} onChange={e=>setExtraAttendees(prev=>prev.map((x,j)=>j===i?{...x,name:e.target.value}:x))} placeholder="Name" style={inp({flex:1,minWidth:0,boxSizing:"border-box"})}/>
                    <input value={a.email} onChange={e=>setExtraAttendees(prev=>prev.map((x,j)=>j===i?{...x,email:e.target.value}:x))} placeholder="email@example.com" style={inp({flex:1.3,minWidth:0,boxSizing:"border-box"})}/>
                    <button onClick={()=>setExtraAttendees(prev=>prev.filter((_,j)=>j!==i))} style={btn("#1a0808","#f87171")} title="Remove">✕</button>
                  </div>
                ))}
                <div style={{display:"flex",gap:7,marginTop:8,flexWrap:"wrap"}}>
                  <button onClick={()=>setExtraAttendees(prev=>[...prev,{name:"",email:""}])} style={btn(C.dim,C.accent)}>+ Add Attendee</button>
                  <button onClick={addBusinessPartners} disabled={!recordPartners?.length} style={{...btn("#091420","#60a5fa",true),opacity:recordPartners?.length?1:.5,cursor:recordPartners?.length?"pointer":"not-allowed"}} title={recordPartners?.length?`Add ${recordPartners.length} partner(s) from this candidate`:"This candidate has no business partners on file"}>👥 Add Business Partners</button>
                </div>
              </div>
            )}
          </div>
        )}

        <div style={{marginBottom:14}}>
          <label style={{display:"block",fontSize:11,color:C.dim,fontWeight:700,marginBottom:3}}>Notes (optional)</label>
          <textarea value={notes} onChange={e=>setNotes(e.target.value)} rows={2} placeholder="Anything to include in the invite…" style={{...inp({width:"100%",boxSizing:"border-box",resize:"vertical",fontFamily:"inherit"})}}/>
        </div>

        <div style={{display:"flex",gap:10,justifyContent:"flex-end"}}>
          <button onClick={onCancel} style={btn(C.dim,C.muted)}>Cancel</button>
          <button onClick={submit} disabled={!canSubmit} style={btn("#091c09","#4ade80",true)}>📅 Book Meeting</button>
        </div>
      </div>
    </div>
  );
}

function BookingDetailModal({ booking, eventType, recordName, brand, settings, onCancel, onMarkCompleted, onCancelBooking, onOpenRecord }) {
  const start = new Date(booking.startISO);
  const end   = new Date(booking.endISO);
  const status = BOOKING_STATUSES[booking.status] || {label:booking.status, color:"#94a3b8"};
  const title = `${eventType?.name || "Meeting"} — ${recordName} (${brand?.name||BRAND.name})`;
  const extras = booking.additionalAttendees || [];
  const attendeeLine = [
    booking.attendeeName && booking.attendeeEmail ? `${booking.attendeeName} <${booking.attendeeEmail}>` : booking.attendeeEmail,
    ...extras.filter(a => a.email).map(a => a.name ? `${a.name} <${a.email}>` : a.email),
  ].filter(Boolean).join(", ");
  const description = [
    `Meeting: ${eventType?.name || ""}`,
    eventType?.description ? `\n${eventType.description}` : "",
    attendeeLine ? `\n\nAttendees: ${attendeeLine}` : "",
    booking.notes ? `\n\nNotes:\n${booking.notes}` : "",
    `\n\nScheduled via ${BRAND.name}.`,
  ].join("");
  const location = booking.location === "phone" ? "Phone call" : booking.location === "video" ? "Video call" : booking.location === "in_person" ? "In person" : (booking.customLocationText||"");

  const onGoogle = () => { window.open(buildGoogleCalUrl({title,startISO:booking.startISO,endISO:booking.endISO,description,location}), "_blank"); };
  const onOutlook = () => { window.open(buildOutlookCalUrl({title,startISO:booking.startISO,endISO:booking.endISO,description,location}), "_blank"); };
  const onIcs = () => { const ics = buildIcs({uid: booking.id, title, startISO: booking.startISO, endISO: booking.endISO, description, location}); downloadIcs(`${(eventType?.slug||"meeting")}-${booking.startISO.slice(0,10)}.ics`, ics); };

  return (
    <div style={{position:"fixed",inset:0,background:"#000c",zIndex:1001,display:"flex",alignItems:"center",justifyContent:"center",padding:16}}>
      <div style={{background:C.panel,border:`1px solid ${C.border}`,borderRadius:16,padding:24,width:"100%",maxWidth:520,maxHeight:"92vh",overflowY:"auto"}}>
        <div style={{display:"flex",alignItems:"center",gap:11,marginBottom:14}}>
          <span style={{fontSize:24}}>{eventType?.icon || "📅"}</span>
          <div style={{flex:1,minWidth:0}}>
            <h3 style={{margin:0,color:"#f0f6ff",fontWeight:900,fontSize:16}}>{eventType?.name || "Meeting"}</h3>
            <div style={{fontSize:11,color:C.muted,marginTop:2}}>with {recordName}</div>
          </div>
          <span style={{background:status.color+"22",color:status.color,border:`1px solid ${status.color}55`,borderRadius:5,padding:"3px 10px",fontSize:11,fontWeight:700}}>{status.label}</span>
          <button onClick={onCancel} title="Close" style={btn(C.dim,C.muted)}>✕</button>
        </div>

        <div style={{background:"#090f1c",borderRadius:10,padding:"12px 14px",marginBottom:14}}>
          <div style={{fontSize:13,color:C.text,fontWeight:700,marginBottom:4}}>{start.toLocaleDateString("en-US",{weekday:"long",month:"long",day:"numeric",year:"numeric"})}</div>
          <div style={{fontSize:12,color:C.muted}}>{start.toLocaleTimeString("en-US",{hour:"numeric",minute:"2-digit"})} – {end.toLocaleTimeString("en-US",{hour:"numeric",minute:"2-digit"})} ({booking.timeZone||"local"})</div>
          <div style={{fontSize:12,color:C.muted,marginTop:4}}>📍 {location}</div>
          {booking.attendeeEmail && <div style={{fontSize:12,color:C.muted,marginTop:2}}>✉️ {booking.attendeeEmail}</div>}
          {extras.length > 0 && (
            <div style={{fontSize:12,color:C.muted,marginTop:4}}>
              👥 +{extras.length} additional: {extras.map(a => a.name || a.email).join(", ")}
            </div>
          )}
          {booking.notes && <div style={{fontSize:12,color:"#c8d8ef",marginTop:8,whiteSpace:"pre-wrap",lineHeight:1.5}}>{booking.notes}</div>}
        </div>

        {booking.status === "scheduled" && (
          <>
            <div style={{fontSize:11,color:C.dim,fontWeight:700,marginBottom:6,letterSpacing:".04em"}}>ADD TO YOUR CALENDAR</div>
            <div style={{display:"flex",gap:8,marginBottom:14}}>
              <button onClick={onGoogle}  style={{...btn("#091c09","#4ade80"),flex:1}}>📆 Google</button>
              <button onClick={onOutlook} style={{...btn("#091420","#60a5fa"),flex:1}}>📧 Outlook</button>
              <button onClick={onIcs}     style={{...btn(C.dim,C.muted),flex:1}}>⬇️ .ics</button>
            </div>
          </>
        )}

        <div style={{display:"flex",gap:8,justifyContent:"flex-end",flexWrap:"wrap"}}>
          {booking.recordRef && <button onClick={onOpenRecord} style={btn(C.dim,"#60a5fa")}>→ Open profile</button>}
          {booking.status === "scheduled" && <button onClick={onCancelBooking} style={btn("#1a0808","#f87171")}>Cancel Booking</button>}
          {booking.status === "scheduled" && <button onClick={onMarkCompleted} style={btn("#091c09","#4ade80",true)}>✓ Mark Completed</button>}
        </div>
      </div>
    </div>
  );
}

function BrokerComposeBox({ broker, onSend }) {
  const [type, setType] = useState("email");
  const [subject, setSubject] = useState("");
  const [body, setBody] = useState("");
  const submit = () => {
    if (!body.trim()) return;
    onSend({ type, subject: type==="email" ? subject : undefined, body, direction:"outbound" });
    setSubject(""); setBody("");
  };
  return (
    <div style={{background:"#090f1c",borderRadius:10,padding:"12px 14px",border:`1px solid ${C.border}`,marginTop:10}}>
      <div style={{display:"flex",gap:6,marginBottom:9,alignItems:"center",flexWrap:"wrap"}}>
        <Sec style={{margin:0}}>Send / Log</Sec>
        {[["email","✉️ Email"],["sms","💬 SMS"],["call","📞 Call"],["note","📝 Note"]].map(([k,l])=>(
          <button key={k} onClick={()=>setType(k)} style={btn(type===k?"#162035":C.bg,type===k?C.accent:C.muted)}>{l}</button>
        ))}
      </div>
      {type==="email" && <input value={subject} onChange={e=>setSubject(e.target.value)} placeholder="Subject…" style={{...inp({width:"100%",boxSizing:"border-box"}),marginBottom:7}}/>}
      <textarea value={body} onChange={e=>setBody(e.target.value)} placeholder={type==="call"?"Call notes (what was discussed, next steps)…":type==="note"?"Internal note about this broker (private)…":`Message to ${broker.firstName}…`} rows={3} style={{...inp({width:"100%",boxSizing:"border-box",resize:"vertical",fontFamily:"inherit"}),marginBottom:7}}/>
      <div style={{display:"flex",gap:7,justifyContent:"flex-end",alignItems:"center"}}>
        {type==="email" && broker.email && <a href={`mailto:${broker.email}?subject=${encodeURIComponent(subject)}&body=${encodeURIComponent(body)}`} style={{...btn("#091420","#60a5fa"),textDecoration:"none",fontSize:11}}>📤 Open in Mail</a>}
        {type==="sms"   && broker.phone && <a href={`sms:${broker.phone}&body=${encodeURIComponent(body)}`} style={{...btn("#091c09","#4ade80"),textDecoration:"none",fontSize:11}}>📱 Open SMS</a>}
        {type==="call"  && broker.phone && <a href={`tel:${broker.phone}`} style={{...btn("#091c09","#4ade80"),textDecoration:"none",fontSize:11}}>📞 Call Now</a>}
        <button onClick={submit} style={btn("#091c09","#4ade80",true)}>Log & Save</button>
      </div>
    </div>
  );
}

// ═══════════════════════════════════════════════════════════
//  DocuSign integration components
// ═══════════════════════════════════════════════════════════
function SendDocusignModal({ rec, onSend, onCancel }) {
  const [docType, setDocType] = useState("fdd");
  const [recipient, setRecipient] = useState(rec.email || "");
  const [message, setMessage] = useState("");
  const [sending, setSending] = useState(false);
  const docLabel = docType==="fdd" ? "Franchise Disclosure Document" : "Franchise Agreement";
  const subject = `Please sign: ${docLabel}`;
  const defaultMessage = `Hi ${rec.firstName||"there"},\n\nPlease find the ${docLabel} attached for your review and signature. Don't hesitate to reach out with any questions.\n\nBest regards`;
  const handleSend = async () => {
    if (!recipient.trim()) { alert("Recipient email is required."); return; }
    setSending(true);
    await new Promise(r => setTimeout(r, 900)); // simulate DocuSign API call
    onSend({ docType, recipient: recipient.trim(), subject, message: message.trim() || defaultMessage });
    setSending(false);
  };
  return (
    <div style={{position:"fixed",inset:0,background:"#000d",zIndex:300,display:"flex",alignItems:"center",justifyContent:"center",padding:16}}>
      <div style={{background:C.panel,border:`1px solid ${C.border}`,borderRadius:18,padding:24,width:"100%",maxWidth:520}}>
        <div style={{display:"flex",alignItems:"center",gap:10,marginBottom:14}}>
          <div style={{width:36,height:36,borderRadius:8,background:"#facc1522",display:"flex",alignItems:"center",justifyContent:"center",fontSize:18}}>✍️</div>
          <div style={{flex:1}}>
            <h2 style={{margin:0,color:"#f0f6ff",fontSize:16,fontWeight:900}}>Send via DocuSign</h2>
            <div style={{fontSize:11,color:C.muted}}>Recipient will receive an email with the signing link</div>
          </div>
          <button onClick={onCancel} title="Close" style={btn(C.dim,C.muted)}>✕</button>
        </div>
        <div style={{marginBottom:12}}>
          <label style={{display:"block",fontSize:11,color:C.dim,fontWeight:700,marginBottom:5}}>Document</label>
          <div style={{display:"flex",gap:7}}>
            {[["fdd","📄","Franchise Disclosure Document (FDD)"],["agreement","📝","Franchise Agreement"]].map(([k,emoji,label])=>(
              <button key={k} onClick={()=>setDocType(k)} style={{flex:1,background:docType===k?"#1a1908":"#090f1c",border:`1.5px solid ${docType===k?"#facc15":C.border}`,borderRadius:9,padding:"10px 13px",color:docType===k?"#facc15":C.text,cursor:"pointer",fontFamily:"inherit",textAlign:"left"}}>
                <div style={{fontSize:18,marginBottom:2}}>{emoji}</div>
                <div style={{fontSize:11,fontWeight:700}}>{label}</div>
              </button>
            ))}
          </div>
        </div>
        <div style={{marginBottom:11}}>
          <label style={{display:"block",fontSize:11,color:C.dim,fontWeight:700,marginBottom:3}}>Recipient Email</label>
          <input value={recipient} onChange={e=>setRecipient(e.target.value)} style={inp({width:"100%",boxSizing:"border-box"})}/>
        </div>
        <div style={{marginBottom:11}}>
          <label style={{display:"block",fontSize:11,color:C.dim,fontWeight:700,marginBottom:3}}>Subject</label>
          <div style={{background:"#070c14",border:`1px solid ${C.border}`,borderRadius:8,padding:"9px 13px",fontSize:13,color:C.muted}}>{subject}</div>
        </div>
        <div style={{marginBottom:13}}>
          <label style={{display:"block",fontSize:11,color:C.dim,fontWeight:700,marginBottom:3}}>Personal Message</label>
          <textarea value={message} onChange={e=>setMessage(e.target.value)} rows={5} placeholder={defaultMessage} style={{...inp({width:"100%",boxSizing:"border-box",resize:"vertical",fontFamily:"inherit",fontSize:12,lineHeight:1.5})}}/>
        </div>
        <div style={{background:"#1a1908",border:"1px solid #facc1533",borderRadius:8,padding:"8px 12px",fontSize:11,color:"#facc15",marginBottom:14,display:"flex",alignItems:"center",gap:7}}>
          <span>ⓘ</span><span><strong>Sandbox simulation.</strong> Real DocuSign envelope sending requires a server-side OAuth integration (the client-side API key alone isn't enough). Status updates are manual here; in production they'd come from DocuSign webhooks.</span>
        </div>
        <div style={{display:"flex",gap:10,justifyContent:"flex-end"}}>
          <button onClick={onCancel} style={btn(C.dim,C.muted)}>Cancel</button>
          <button onClick={handleSend} disabled={sending||!recipient.trim()} style={btn("#1a1908","#facc15",true)}>{sending?"⟳ Sending…":`📤 Send ${docType==="fdd"?"FDD":"Agreement"}`}</button>
        </div>
      </div>
    </div>
  );
}

function DocuSignSection({ rec, isConnected, onSend, onUpdateEnvelope, onVoidEnvelope, onOpenSettings }) {
  const envelopes = rec.docusignEnvelopes || [];
  const STATUS_COLORS = { sent:"#60a5fa", viewed:"#facc15", completed:"#4ade80", declined:"#f87171", voided:"#64748b" };
  const STATUS_LABELS = { sent:"Sent", viewed:"Viewed", completed:"Signed", declined:"Declined", voided:"Voided" };
  const fmtAgo = (iso) => {
    if (!iso) return "";
    const d = Math.floor((Date.now()-new Date(iso))/86400000);
    if (d === 0) return "today";
    if (d === 1) return "yesterday";
    return `${d} days ago`;
  };
  const fddEnv = envelopes.find(e => e.docType === "fdd" && e.status !== "voided");
  const agreementEnv = envelopes.find(e => e.docType === "agreement" && e.status !== "voided");
  const EnvCard = ({ env, docLabel, docType }) => {
    if (!env) {
      return (
        <div style={{background:"#090f1c",border:`1px dashed ${C.border}`,borderRadius:10,padding:"14px 16px",display:"flex",alignItems:"center",gap:12}}>
          <span style={{fontSize:22,opacity:.5}}>{docType==="fdd"?"📄":"📝"}</span>
          <div style={{flex:1}}>
            <div style={{fontSize:13,fontWeight:700,color:C.text}}>{docLabel}</div>
            <div style={{fontSize:11,color:C.muted}}>Not sent yet</div>
          </div>
          {isConnected && <button onClick={()=>onSend(docType)} style={btn("#1a1908","#facc15",true)}>📤 Send</button>}
        </div>
      );
    }
    const color = STATUS_COLORS[env.status] || "#94a3b8";
    return (
      <div style={{background:"#090f1c",border:`1px solid ${color}33`,borderRadius:10,padding:"12px 16px"}}>
        <div style={{display:"flex",alignItems:"center",gap:10,marginBottom:8}}>
          <span style={{fontSize:20}}>{docType==="fdd"?"📄":"📝"}</span>
          <div style={{flex:1,minWidth:0}}>
            <div style={{fontSize:13,fontWeight:700,color:C.text}}>{docLabel}</div>
            <div style={{fontSize:11,color:C.muted,fontFamily:"ui-monospace,monospace"}}>{env.envelopeId}</div>
          </div>
          <span style={{background:color+"22",color,border:`1px solid ${color}55`,borderRadius:6,padding:"3px 10px",fontSize:11,fontWeight:800,letterSpacing:".04em"}}>● {STATUS_LABELS[env.status]||env.status}</span>
        </div>
        <div style={{fontSize:11,color:C.muted,marginBottom:9,lineHeight:1.5}}>
          <div>Sent <strong style={{color:C.text}}>{fmtAgo(env.sentAt)}</strong> to <strong style={{color:C.text}}>{env.recipient}</strong></div>
          {env.viewedAt    && <div>Viewed {fmtAgo(env.viewedAt)}</div>}
          {env.completedAt && <div style={{color:"#4ade80"}}>✓ Signed {fmtAgo(env.completedAt)}</div>}
          {env.declinedAt  && <div style={{color:"#f87171"}}>Declined {fmtAgo(env.declinedAt)}</div>}
        </div>
        <div style={{display:"flex",gap:6,flexWrap:"wrap"}}>
          {env.status === "sent" && (
            <>
              <button onClick={()=>onUpdateEnvelope(env.id,{status:"viewed"})} style={{...btn(C.dim,C.muted),fontSize:10}}>Mark Viewed</button>
              <button onClick={()=>onUpdateEnvelope(env.id,{status:"completed"})} style={{...btn("#091c09","#4ade80"),fontSize:10}}>Mark Signed</button>
              <button onClick={()=>onUpdateEnvelope(env.id,{status:"declined"})} style={{...btn("#1a0808","#f87171"),fontSize:10}}>Mark Declined</button>
              <button onClick={()=>onSend(docType)} style={{...btn(C.dim,"#60a5fa"),fontSize:10}}>↻ Resend</button>
              <button onClick={()=>onVoidEnvelope(env.id)} style={{...btn(C.dim,C.muted),fontSize:10,marginLeft:"auto"}}>🚫 Void</button>
            </>
          )}
          {env.status === "viewed" && (
            <>
              <button onClick={()=>onUpdateEnvelope(env.id,{status:"completed"})} style={{...btn("#091c09","#4ade80"),fontSize:10}}>Mark Signed</button>
              <button onClick={()=>onUpdateEnvelope(env.id,{status:"declined"})} style={{...btn("#1a0808","#f87171"),fontSize:10}}>Mark Declined</button>
              <button onClick={()=>onSend(docType)} style={{...btn(C.dim,"#60a5fa"),fontSize:10}}>↻ Resend</button>
              <button onClick={()=>onVoidEnvelope(env.id)} style={{...btn(C.dim,C.muted),fontSize:10,marginLeft:"auto"}}>🚫 Void</button>
            </>
          )}
          {env.status === "completed" && (
            <div style={{fontSize:11,color:"#4ade80",fontWeight:700}}>✓ Fully executed</div>
          )}
          {env.status === "declined" && (
            <button onClick={()=>onSend(docType)} style={{...btn("#091c09","#60a5fa"),fontSize:10}}>↻ Send Again</button>
          )}
        </div>
      </div>
    );
  };
  return (
    <div style={{background:C.panel,border:`1px solid ${C.border}`,borderRadius:14,padding:"14px 16px"}}>
      <div style={{display:"flex",alignItems:"center",gap:10,marginBottom:12}}>
        <Sec style={{margin:0}}>DocuSign</Sec>
        {isConnected
          ? <span style={{background:"#091c09",color:"#4ade80",border:"1px solid #4ade8044",borderRadius:14,padding:"2px 9px",fontSize:10,fontWeight:800}}>● Connected</span>
          : <span style={{background:"#1a0808",color:"#f87171",border:"1px solid #f8717144",borderRadius:14,padding:"2px 9px",fontSize:10,fontWeight:800}}>○ Not Connected</span>}
      </div>
      {!isConnected ? (
        <div style={{background:"#090f1c",borderRadius:10,padding:"14px 16px",fontSize:12,color:C.muted,lineHeight:1.6}}>
          Connect your DocuSign account in <strong style={{color:C.text}}>Settings → Integrations</strong> to send the FDD and Franchise Agreement directly from this opportunity, with sent/viewed/signed status synced automatically.
          <div style={{marginTop:10}}>
            <button onClick={onOpenSettings} style={btn(C.dim,C.accent)}>⚙ Open DocuSign Settings →</button>
          </div>
        </div>
      ) : (
        <div style={{display:"flex",flexDirection:"column",gap:10}}>
          <EnvCard env={fddEnv}       docLabel="Franchise Disclosure Document (FDD)" docType="fdd"/>
          <EnvCard env={agreementEnv} docLabel="Franchise Agreement"                 docType="agreement"/>
          <div style={{fontSize:10,color:C.dim,marginTop:2,display:"flex",alignItems:"center",gap:6}}>
            <span>🔒</span><span>Powered by DocuSign Connect · Sandbox mode for tutorial</span>
          </div>
        </div>
      )}
    </div>
  );
}

// ═══════════════════════════════════════════════════════════
//  TERRITORY FORM (stable module-level — used for create AND edit)
// ═══════════════════════════════════════════════════════════
// ═══════════════════════════════════════════════════════════
//  CREATE TERRITORY MODAL — entry point for every territory-creation flow.
//  Submodes:
//    "pick"      — the option picker
//    "zipCity"   — search a single zip/city → polygon bounding box
//    "radius"    — search an address + enter radius → circle
//    "offLimits" — sub-picker for restricted-region kinds
//    "states"    — multi-select US states + per-state status, fetches actual state polygons
// ═══════════════════════════════════════════════════════════
function CreateTerritoryModal({ onCancel, onModeSelected, onPendingCreated, onCreateRestricted }) {
  const [step, setStep] = useState("pick");
  const [zipQ, setZipQ] = useState("");
  const [zipBusy, setZipBusy] = useState(false);
  const [radQ, setRadQ] = useState("");
  const [radMi, setRadMi] = useState("10");
  const [radBusy, setRadBusy] = useState(false);
  const [selectedStates, setSelectedStates] = useState({}); // {stateName: "not_registered"|"pending_registration"}
  const [creatingStates, setCreatingStates] = useState(false);
  const [progress, setProgress] = useState("");

  const doZip = async () => {
    if (!zipQ.trim()) return;
    setZipBusy(true);
    try {
      const res = await fetch(`https://nominatim.openstreetmap.org/search?q=${encodeURIComponent(zipQ)}&format=json&limit=1`);
      const data = await res.json();
      if (!data[0]?.boundingbox) { alert("No boundary for that location."); setZipBusy(false); return; }
      const { boundingbox, display_name } = data[0];
      const s=+boundingbox[0], n=+boundingbox[1], w=+boundingbox[2], e=+boundingbox[3];
      onPendingCreated({ kind:"polygon", latlngs:[[s,w],[s,e],[n,e],[n,w]] }, display_name.split(",")[0].trim());
    } catch { alert("Search failed."); }
    setZipBusy(false);
  };

  const doRadius = async () => {
    const r = parseFloat(radMi);
    if (!radQ.trim() || isNaN(r) || r <= 0) { alert("Enter an address and a radius in miles."); return; }
    setRadBusy(true);
    try {
      const res = await fetch(`https://nominatim.openstreetmap.org/search?q=${encodeURIComponent(radQ)}&format=json&limit=1`);
      const data = await res.json();
      if (!data[0]) { alert("Address not found."); setRadBusy(false); return; }
      const { lat, lon, display_name } = data[0];
      onPendingCreated({ kind:"circle", center:[+lat,+lon], radiusMeters: r*1609.34 }, `${display_name.split(",")[0].trim()} (${r}mi)`);
    } catch { alert("Search failed."); }
    setRadBusy(false);
  };

  const toggleState = (name) => setSelectedStates(prev => {
    const next = {...prev};
    if (next[name]) delete next[name];
    else next[name] = "not_registered";
    return next;
  });
  const setStateStatus = (name, status) => setSelectedStates(prev => ({...prev, [name]: status}));

  const createRestrictedStates = async () => {
    const entries = Object.entries(selectedStates);
    if (entries.length === 0) { alert("Pick at least one state."); return; }
    setCreatingStates(true);
    const out = [];
    for (let i = 0; i < entries.length; i++) {
      const [name, status] = entries[i];
      setProgress(`Fetching ${name} (${i+1}/${entries.length})…`);
      const geo = await fetchStatePolygon(name);
      if (geo) {
        out.push({
          id: Math.random().toString(36).slice(2,10),
          label: `${name} · ${RESTRICTION_TYPES[status].label}`,
          color: RESTRICTION_TYPES[status].color,
          opacity: 0.35,
          ...geo,
          restricted: true,
          restrictionType: status,
          stateName: name,
          assignees: [],
          createdAt: new Date().toISOString(),
        });
      }
      if (i < entries.length - 1) await new Promise(r => setTimeout(r, 650));
    }
    setProgress(`Created ${out.length} restricted area${out.length===1?"":"s"}.`);
    onCreateRestricted(out);
    setCreatingStates(false);
  };

  const headerTitle = {
    pick:      "Create Territory",
    zipCity:   "Single ZIP / City",
    radius:    "Radius around Address",
    offLimits: "Off-Limits Area",
    states:    "Restrict U.S. State(s)",
  }[step];

  return (
    <div style={{position:"fixed",inset:0,background:"#000c",zIndex:1005,display:"flex",alignItems:"center",justifyContent:"center",padding:16}}>
      <div style={{background:C.panel,border:`1px solid ${C.border}`,borderRadius:16,padding:24,width:"100%",maxWidth: step === "states" ? 640 : 480}}>
        <div style={{display:"flex",alignItems:"center",gap:11,marginBottom:14}}>
          {step !== "pick" && !creatingStates && <button onClick={()=>setStep(step==="states"?"offLimits":"pick")} style={btn(C.dim,C.muted)}>← Back</button>}
          <h3 style={{margin:0,color:"#f0f6ff",fontWeight:900,fontSize:16,flex:1}}>{headerTitle}</h3>
          <button onClick={onCancel} title="Close" style={btn(C.dim,C.muted)} disabled={creatingStates}>✕</button>
        </div>

        {step === "pick" && (
          <div style={{display:"flex",flexDirection:"column",gap:8}}>
            {[
              { id:"zipCity",  icon:"📍", label:"Single ZIP / City",      desc:"Find one zip or city; create a polygon from its bounding box." },
              { id:"multiZip", icon:"🔢", label:"Multi-ZIP Combination",  desc:"Stack multiple zip codes into one combined territory." },
              { id:"radius",   icon:"⊙",  label:"Radius around Address",  desc:"Pin an address and create a circle with a custom radius." },
              { id:"draw",     icon:"✏️", label:"Freehand Polygon",       desc:"Click points on the map to outline a custom shape." },
              { id:"offLimits",icon:"🚫", label:"Off-Limits Area",        desc:"Create a locked, restricted region (e.g. registration-pending states)." },
            ].map(opt => (
              <button key={opt.id} onClick={()=>{
                if (opt.id === "multiZip") { onModeSelected("zip"); return; }
                if (opt.id === "draw")     { onModeSelected("draw"); return; }
                setStep(opt.id);
              }} style={{background:"#090f1c",border:`1px solid ${C.border}`,borderRadius:10,padding:"13px 16px",cursor:"pointer",textAlign:"left",fontFamily:"inherit",display:"flex",alignItems:"flex-start",gap:13}}>
                <span style={{fontSize:24,lineHeight:1,flexShrink:0}}>{opt.icon}</span>
                <div style={{flex:1,minWidth:0}}>
                  <div style={{fontSize:14,fontWeight:800,color:C.text}}>{opt.label}</div>
                  <div style={{fontSize:11,color:C.muted,marginTop:3,lineHeight:1.4}}>{opt.desc}</div>
                </div>
                <span style={{color:C.dim,alignSelf:"center"}}>→</span>
              </button>
            ))}
          </div>
        )}

        {step === "zipCity" && (
          <div>
            <label style={{display:"block",fontSize:11,color:C.dim,fontWeight:700,marginBottom:4}}>Search for a ZIP code or city</label>
            <input autoFocus value={zipQ} onChange={e=>setZipQ(e.target.value)} onKeyDown={e=>e.key==="Enter"&&doZip()} placeholder="e.g. 78701 or Austin, TX" style={{...inp({width:"100%",boxSizing:"border-box"}),marginBottom:14}}/>
            <div style={{display:"flex",gap:10,justifyContent:"flex-end"}}>
              <button onClick={onCancel} style={btn(C.dim,C.muted)}>Cancel</button>
              <button onClick={doZip} disabled={zipBusy} style={btn("#091c09","#4ade80",true)}>{zipBusy?"⟳ Searching…":"🔍 Find & Save"}</button>
            </div>
          </div>
        )}

        {step === "radius" && (
          <div>
            <label style={{display:"block",fontSize:11,color:C.dim,fontWeight:700,marginBottom:4}}>Address</label>
            <input autoFocus value={radQ} onChange={e=>setRadQ(e.target.value)} placeholder="e.g. 1600 Pennsylvania Ave, Washington DC" style={{...inp({width:"100%",boxSizing:"border-box"}),marginBottom:10}}/>
            <label style={{display:"block",fontSize:11,color:C.dim,fontWeight:700,marginBottom:4}}>Radius (miles)</label>
            <input value={radMi} onChange={e=>setRadMi(e.target.value)} type="number" min="0.1" step="0.5" style={{...inp({width:120}),marginBottom:14}}/>
            <div style={{display:"flex",gap:10,justifyContent:"flex-end"}}>
              <button onClick={onCancel} style={btn(C.dim,C.muted)}>Cancel</button>
              <button onClick={doRadius} disabled={radBusy} style={btn("#091c09","#4ade80",true)}>{radBusy?"⟳ Searching…":"🔍 Find & Save"}</button>
            </div>
          </div>
        )}

        {step === "offLimits" && (
          <div style={{display:"flex",flexDirection:"column",gap:8}}>
            <div style={{fontSize:12,color:C.muted,marginBottom:6,lineHeight:1.5}}>Off-limits territories are locked, color-coded red/orange, and visible at a glance — useful for compliance, registration status, or other restricted regions.</div>
            <button onClick={()=>setStep("states")} style={{background:"#090f1c",border:`1px solid ${C.border}`,borderRadius:10,padding:"13px 16px",cursor:"pointer",textAlign:"left",fontFamily:"inherit",display:"flex",alignItems:"flex-start",gap:13}}>
              <span style={{fontSize:24,lineHeight:1,flexShrink:0}}>🏛️</span>
              <div style={{flex:1,minWidth:0}}>
                <div style={{fontSize:14,fontWeight:800,color:C.text}}>Restrict U.S. State(s)</div>
                <div style={{fontSize:11,color:C.muted,marginTop:3,lineHeight:1.4}}>Mark whole states as Not Registered, Pending Registration, or Sold Out. Generates one locked territory per state using actual state borders.</div>
              </div>
              <span style={{color:C.dim,alignSelf:"center"}}>→</span>
            </button>
            <div style={{fontSize:11,color:C.dim,padding:"8px 4px",fontStyle:"italic"}}>More off-limits types coming soon (zip exclusions, custom polygons, etc.)</div>
          </div>
        )}

        {step === "states" && (
          <div>
            <div style={{fontSize:12,color:C.muted,marginBottom:12,lineHeight:1.5}}>
              Pick states to mark as off-limits. Each selected state becomes its own locked territory using its real outline. Defaults to <strong style={{color:"#fca5a5"}}>Not Registered</strong> — toggle to <strong style={{color:"#fb923c"}}>Pending Registration</strong> or <strong style={{color:"#facc15"}}>Sold Out</strong> per state.
            </div>
            {!creatingStates && (
              <div style={{maxHeight:340,overflowY:"auto",background:"#090f1c",border:`1px solid ${C.border}`,borderRadius:8,padding:8,marginBottom:14}}>
                {US_STATES.map(state => {
                  const checked = !!selectedStates[state];
                  const status = selectedStates[state] || "not_registered";
                  return (
                    <div key={state} style={{display:"flex",alignItems:"center",gap:8,padding:"6px 8px",borderRadius:6,background:checked?"#162035":"transparent"}}>
                      <div onClick={()=>toggleState(state)} style={{width:15,height:15,borderRadius:3,border:`1.5px solid ${checked?"#4ade80":C.border}`,background:checked?"#4ade80":"transparent",display:"flex",alignItems:"center",justifyContent:"center",color:"#052e16",fontSize:11,fontWeight:900,cursor:"pointer",flexShrink:0}}>{checked?"✓":""}</div>
                      <div onClick={()=>toggleState(state)} style={{flex:1,fontSize:12,color:checked?C.text:C.muted,cursor:"pointer"}}>{state}</div>
                      {checked && (
                        <div style={{display:"flex",gap:4}}>
                          {Object.entries(RESTRICTION_TYPES).map(([k,v])=>(
                            <button key={k} onClick={()=>setStateStatus(state,k)} style={{background:status===k?v.color+"22":"transparent",border:`1px solid ${status===k?v.color:C.border}`,color:status===k?v.color:C.muted,borderRadius:4,padding:"2px 8px",fontSize:10,fontWeight:700,cursor:"pointer",fontFamily:"inherit"}}>{v.label}</button>
                          ))}
                        </div>
                      )}
                    </div>
                  );
                })}
              </div>
            )}
            {creatingStates && (
              <div style={{textAlign:"center",padding:24,color:"#facc15"}}>
                <div style={{fontSize:28,marginBottom:8,animation:"spin 1s linear infinite"}}>⟳</div>
                <div style={{fontSize:13,fontWeight:700,marginBottom:4}}>{progress}</div>
                <div style={{fontSize:11,color:C.dim}}>Fetching state polygons from OpenStreetMap (~0.6s per state).</div>
                <style>{`@keyframes spin{from{transform:rotate(0)}to{transform:rotate(360deg)}}`}</style>
              </div>
            )}
            <div style={{display:"flex",alignItems:"center",gap:10}}>
              <div style={{flex:1,fontSize:11,color:C.muted}}>{Object.keys(selectedStates).length} state{Object.keys(selectedStates).length===1?"":"s"} selected</div>
              <button onClick={onCancel} style={btn(C.dim,C.muted)} disabled={creatingStates}>Cancel</button>
              <button onClick={createRestrictedStates} disabled={creatingStates || Object.keys(selectedStates).length===0} style={btn("#1a0808","#dc2626",true)}>{creatingStates?"⟳ Creating…":"🔒 Create Restricted Areas"}</button>
            </div>
          </div>
        )}
      </div>
    </div>
  );
}

function TerritoryForm({ initial, pending, assignableRecords = [], defaults = {}, onSave, onCancel }) {
  const isEdit = !!initial?.id;
  const [name, setName]       = useState(initial?.label || defaults.label || "");
  const [color, setColor]     = useState(initial?.color || defaults.color || "#3b82f6");
  const [opacity, setOpacity] = useState(initial?.opacity ?? defaults.opacity ?? 0.35);
  const [assignees, setAssignees] = useState(initial?.assignees || []);
  const [searchA, setSearchA] = useState("");
  const [showPicker, setShowPicker] = useState(false);

  const kind = initial?.kind || pending?.kind || "polygon";
  const kindInfo = kind === "circle"      ? { icon:"⊙",  label:"Circle (radius)" }
                : kind === "multipolygon" ? { icon:"🔢", label:`Multi-ZIP (${pending?.components?.length || initial?.components?.length || 0} zips)` }
                :                            { icon:"🗺️", label:"Polygon" };

  const toggleAssignee = (rec) => setAssignees(prev => {
    const ex = prev.find(a => a.id === rec.id);
    if (ex) return prev.filter(a => a.id !== rec.id);
    return [...prev, { type: rec.recType, id: rec.id, name: `${rec.firstName} ${rec.lastName}` }];
  });
  const filtered = assignableRecords.filter(r => !searchA.trim() || `${r.firstName} ${r.lastName}`.toLowerCase().includes(searchA.toLowerCase()));
  const handleSave = () => {
    if (!name.trim()) { alert("Territory name is required."); return; }
    onSave({ label: name.trim(), color, opacity, assignees });
  };

  return (
    <div style={{position:"fixed",inset:0,background:"#000c",zIndex:1001,display:"flex",alignItems:"center",justifyContent:"center",padding:16}}>
      <div style={{background:C.panel,border:`1px solid ${C.border}`,borderRadius:16,padding:24,width:"100%",maxWidth:480}}>
        <div style={{display:"flex",alignItems:"center",gap:11,marginBottom:14}}>
          <div style={{fontSize:26}}>{kindInfo.icon}</div>
          <div style={{flex:1}}>
            <h3 style={{margin:0,color:"#f0f6ff",fontWeight:900,fontSize:16}}>{isEdit ? "Edit Territory" : "Save Territory"}</h3>
            <div style={{fontSize:11,color:C.muted,marginTop:2}}>{kindInfo.label}</div>
          </div>
          <button onClick={onCancel} title="Close" style={btn(C.dim,C.muted)}>✕</button>
        </div>

        <div style={{marginBottom:12}}>
          <label style={{display:"block",fontSize:11,color:C.dim,fontWeight:700,marginBottom:4}}>Territory Name</label>
          <input autoFocus value={name} onChange={e=>setName(e.target.value)} placeholder="e.g. Austin Metro" style={inp({width:"100%",boxSizing:"border-box"})}/>
        </div>

        <div style={{display:"flex",alignItems:"center",gap:10,marginBottom:14}}>
          <label style={{fontSize:11,color:C.dim,fontWeight:700}}>Color</label>
          <input type="color" value={color} onChange={e=>setColor(e.target.value)} style={{width:36,height:32,border:"none",borderRadius:6,cursor:"pointer"}}/>
          <label style={{fontSize:11,color:C.dim,fontWeight:700}}>Opacity</label>
          <input type="range" min="0.1" max="0.8" step="0.05" value={opacity} onChange={e=>setOpacity(+e.target.value)} style={{flex:1}}/>
          <span style={{fontSize:11,color:C.muted,minWidth:32,textAlign:"right"}}>{Math.round(opacity*100)}%</span>
        </div>

        <div style={{marginBottom:14}}>
          <div style={{display:"flex",alignItems:"center",gap:8,marginBottom:6}}>
            <label style={{fontSize:11,color:C.dim,fontWeight:700}}>Assigned Franchisees / Candidates ({assignees.length})</label>
            <button onClick={()=>setShowPicker(!showPicker)} style={{...btn(C.dim,C.muted),fontSize:10,padding:"3px 8px",marginLeft:"auto"}}>{showPicker?"✕ Close":"+ Add / remove"}</button>
          </div>
          <div style={{display:"flex",flexWrap:"wrap",gap:5,marginBottom:6,minHeight:22}}>
            {assignees.length === 0 && <span style={{fontSize:11,color:C.dim,fontStyle:"italic"}}>None assigned</span>}
            {assignees.map(a => (
              <span key={a.id} style={{background:"#091420",border:"1px solid #60a5fa44",color:"#60a5fa",borderRadius:5,padding:"2px 9px",fontSize:11,fontWeight:700,display:"inline-flex",alignItems:"center",gap:4}}>
                {a.name}
                <button onClick={()=>setAssignees(p=>p.filter(x=>x.id!==a.id))} title="Remove assignee" style={{background:"transparent",border:"none",color:"#60a5fa",fontSize:13,cursor:"pointer",padding:0,marginLeft:2,fontFamily:"inherit",lineHeight:1}}>×</button>
              </span>
            ))}
          </div>
          {showPicker && (
            <div style={{marginTop:8,background:"#090f1c",border:`1px solid ${C.border}`,borderRadius:8,padding:10}}>
              <input value={searchA} onChange={e=>setSearchA(e.target.value)} placeholder="Search candidates…" style={{...inp({width:"100%",boxSizing:"border-box"}),marginBottom:8}}/>
              <div style={{maxHeight:210,overflowY:"auto",display:"flex",flexDirection:"column",gap:2}}>
                {filtered.length === 0 && <div style={{fontSize:11,color:C.dim,padding:"6px 0",textAlign:"center"}}>No candidates match.</div>}
                {filtered.map(r => {
                  const checked = !!assignees.find(a => a.id === r.id);
                  return (
                    <div key={r.id} onClick={()=>toggleAssignee(r)} style={{display:"flex",alignItems:"center",gap:8,padding:"6px 8px",borderRadius:6,cursor:"pointer",background:checked?"#162035":"transparent"}}>
                      <div style={{width:15,height:15,borderRadius:3,border:`1.5px solid ${checked?"#4ade80":C.border}`,background:checked?"#4ade80":"transparent",display:"flex",alignItems:"center",justifyContent:"center",color:"#052e16",fontSize:11,fontWeight:900,flexShrink:0}}>{checked?"✓":""}</div>
                      <div style={{flex:1,minWidth:0}}>
                        <div style={{fontSize:12,color:C.text,fontWeight:600}}>{r.firstName} {r.lastName}</div>
                        {r.territory && <div style={{fontSize:10,color:C.muted}}>📍 {r.territory}</div>}
                      </div>
                      <span style={{background:r.recType==="opp"?"#091c09":"#091420",color:r.recType==="opp"?"#4ade80":"#60a5fa",border:`1px solid ${r.recType==="opp"?"#4ade8033":"#60a5fa33"}`,borderRadius:4,padding:"1px 6px",fontSize:9,fontWeight:700,textTransform:"uppercase"}}>{r.recType}</span>
                    </div>
                  );
                })}
              </div>
            </div>
          )}
        </div>

        <div style={{display:"flex",gap:10,justifyContent:"flex-end"}}>
          <button onClick={onCancel} style={btn(C.dim,C.muted)}>Cancel</button>
          <button onClick={handleSave} style={btn("#091c09","#4ade80",true)}>{isEdit ? "Save Changes" : "Save Territory"}</button>
        </div>
      </div>
    </div>
  );
}

// ═══════════════════════════════════════════════════════════
//  TERRITORY MAP (Leaflet + OpenStreetMap)
// ═══════════════════════════════════════════════════════════
function TerritoryMap({ territories, onSave, onDelete, assignableRecords = [], onNavigate }) {
  const mapElRef = useRef(null);
  const mapRef = useRef(null);
  const polyLayersRef = useRef({});
  const drawLayerRef = useRef(null);
  const zipPreviewRef = useRef([]);
  const [ready, setReady] = useState(typeof window !== "undefined" && !!window.L);
  const [mode, setMode] = useState("pan"); // pan | draw | zip
  const [drawPts, setDrawPts] = useState([]);
  const [zipDraft, setZipDraft] = useState([]); // [{name, latlngs}] for multi-zip
  const [searchQ, setSearchQ] = useState("");
  const [searching, setSearching] = useState(false);
  const [showCreate, setShowCreate] = useState(false);
  const [color, setColor] = useState("#3b82f6");
  const [opacity, setOpacity] = useState(0.35);
  const [listTab, setListTab] = useState("active"); // "active" | "offlimits" — legacy two-tab toggle
  const [listStatusFilter, setListStatusFilter] = useState("all"); // "all" | any TERRITORY_STATUSES key
  const [listSearch, setListSearch] = useState("");
  const [listSort, setListSort] = useState("name"); // "name" | "status" | "assignees" | "newest"
  const [editMenuFor, setEditMenuFor] = useState(null); // territory id whose ✏️ menu is open
  const [selected, setSelected] = useState(null);
  const [pending, setPending] = useState(null);   // new-territory geometry being saved
  const [defaultLabel, setDefaultLabel] = useState("");
  const [editingTerr, setEditingTerr] = useState(null);

  useEffect(() => {
    if (window.L) { setReady(true); return; }
    const id = setInterval(() => { if (window.L) { setReady(true); clearInterval(id); }}, 100);
    return () => clearInterval(id);
  }, []);

  useEffect(() => {
    if (!ready || !mapElRef.current || mapRef.current) return;
    const L = window.L;
    const map = L.map(mapElRef.current, { zoomControl: true, worldCopyJump: true }).setView([39.5, -98.5], 4);
    L.tileLayer("https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png", {
      attribution: '© <a href="https://www.openstreetmap.org/copyright" target="_blank">OpenStreetMap</a>',
      maxZoom: 19,
    }).addTo(map);
    mapRef.current = map;
    setTimeout(() => map.invalidateSize(), 50);
    return () => { map.remove(); mapRef.current = null; };
  }, [ready]);

  // Render territories — polygon / multipolygon / circle
  useEffect(() => {
    if (!mapRef.current || !window.L) return;
    const L = window.L, map = mapRef.current;
    Object.values(polyLayersRef.current).forEach(layer => { try { map.removeLayer(layer); } catch {} });
    polyLayersRef.current = {};
    territories.forEach(t => {
      const sel = selected === t.id;
      const styleOpts = t.restricted
        ? { color: t.color || "#dc2626", fillColor: t.color || "#dc2626", fillOpacity: sel ? 0.5 : 0.42, weight: sel ? 3 : 2, dashArray: "8,4" }
        : { color: t.color || "#3b82f6", fillColor: t.color || "#3b82f6", fillOpacity: t.opacity ?? 0.35, weight: sel ? 3 : 1.5 };
      let layer = null;
      if (t.kind === "circle" && t.center && t.radiusMeters) {
        layer = L.circle(t.center, { ...styleOpts, radius: t.radiusMeters });
      } else if (t.kind === "multipolygon" && t.components?.length) {
        const rings = t.components.map(c => c.latlngs).filter(ll => ll?.length >= 3);
        if (rings.length) layer = L.polygon(rings, styleOpts);
      } else if (t.latlngs?.length >= 3) {
        layer = L.polygon(t.latlngs, styleOpts);
      }
      if (!layer) return;
      const assignNames = (t.assignees||[]).map(a=>a.name).join(", ");
      layer.bindTooltip(assignNames ? `${t.label||"Territory"} — ${assignNames}` : (t.label||"Territory"), { sticky: true });
      layer.on("click", () => setSelected(s => s === t.id ? null : t.id));
      polyLayersRef.current[t.id] = layer;
      layer.addTo(map);
    });
  }, [territories, selected]);

  useEffect(() => {
    if (!mapRef.current) return;
    const map = mapRef.current;
    const onClick = (e) => {
      if (mode !== "draw") return;
      const pt = [e.latlng.lat, e.latlng.lng];
      setDrawPts(prev => {
        const next = [...prev, pt];
        if (drawLayerRef.current) { try { map.removeLayer(drawLayerRef.current); } catch {} }
        if (next.length >= 2) {
          drawLayerRef.current = window.L.polygon(next, { color: "#3b82f6", fillOpacity: 0.15, weight: 2, dashArray: "5,5" }).addTo(map);
        }
        return next;
      });
    };
    map.on("click", onClick);
    return () => map.off("click", onClick);
  }, [mode]);

  useEffect(() => {
    if (!mapRef.current) return;
    mapRef.current.getContainer().style.cursor = mode === "draw" ? "crosshair" : "";
  }, [mode]);

  // Multi-ZIP draft preview overlay
  useEffect(() => {
    if (!mapRef.current || !window.L) return;
    const map = mapRef.current;
    zipPreviewRef.current.forEach(l => { try { map.removeLayer(l); } catch {} });
    zipPreviewRef.current = zipDraft.map(c => window.L.polygon(c.latlngs, { color: "#facc15", fillColor: "#facc15", fillOpacity: 0.25, weight: 2, dashArray: "5,5" }).addTo(map));
  }, [zipDraft]);

  const finishDraw = () => {
    if (drawPts.length < 3) return;
    if (drawLayerRef.current) { try { mapRef.current?.removeLayer(drawLayerRef.current); } catch {} drawLayerRef.current = null; }
    setPending({ kind: "polygon", latlngs: [...drawPts] });
    setDefaultLabel(""); setDrawPts([]); setMode("pan");
  };
  const cancelDraw = () => {
    if (drawLayerRef.current) { try { mapRef.current?.removeLayer(drawLayerRef.current); } catch {} drawLayerRef.current = null; }
    setDrawPts([]); setMode("pan");
  };
  const finishMultiZip = () => {
    if (zipDraft.length === 0) return;
    setPending({ kind: "multipolygon", components: [...zipDraft] });
    setDefaultLabel(zipDraft.map(c => c.name).join(" + "));
    setZipDraft([]); setMode("pan");
  };
  const cancelMultiZip = () => { setZipDraft([]); setMode("pan"); };

  const doSearch = async () => {
    // Search is purely a navigation tool (and a Multi-ZIP collector when in zip mode).
    // To CREATE a territory, the user opens the "+ Create Territory" picker.
    if (!searchQ.trim() || !mapRef.current) return;
    setSearching(true);
    try {
      const res = await fetch(`https://nominatim.openstreetmap.org/search?q=${encodeURIComponent(searchQ)}&format=json&limit=1`);
      const data = await res.json();
      if (data.length === 0) { alert(`No location found for "${searchQ}".`); setSearching(false); return; }
      const { lat, lon, boundingbox, display_name } = data[0];
      const placeName = display_name.split(",")[0].trim();

      // Multi-ZIP mode: add to the staged draft
      if (mode === "zip") {
        if (!boundingbox) { alert("This location doesn't have a usable boundary."); setSearching(false); return; }
        const s=+boundingbox[0], n=+boundingbox[1], w=+boundingbox[2], e=+boundingbox[3];
        setZipDraft(prev => [...prev, { name: placeName, latlngs: [[s,w],[s,e],[n,e],[n,w]] }]);
        setSearchQ("");
        mapRef.current.fitBounds([[s,w],[n,e]]);
        setSearching(false);
        return;
      }
      // Default mode: navigate the map only.
      if (boundingbox) mapRef.current.fitBounds([[+boundingbox[0],+boundingbox[2]],[+boundingbox[1],+boundingbox[3]]]);
      else mapRef.current.setView([+lat, +lon], 12);
    } catch { alert("Search failed. Check connection."); }
    setSearching(false);
  };

  const saveNewTerritory = ({label, color: c, opacity: o, assignees}) => {
    if (!pending) return;
    const base = { id: uid(), label, color: c, opacity: o, assignees, kind: pending.kind, createdAt: nowIso() };
    if (pending.kind === "polygon")       onSave({ ...base, latlngs: pending.latlngs });
    if (pending.kind === "multipolygon")  onSave({ ...base, components: pending.components });
    if (pending.kind === "circle")        onSave({ ...base, center: pending.center, radiusMeters: pending.radiusMeters });
    setPending(null); setDefaultLabel("");
  };

  const fitToTerritory = (t) => {
    if (!mapRef.current || !window.L) return;
    const L = window.L;
    let bounds = null;
    if (t.kind === "circle" && t.center && t.radiusMeters) bounds = L.circle(t.center, {radius: t.radiusMeters}).getBounds();
    else if (t.kind === "multipolygon" && t.components?.length) bounds = L.polygon(t.components.map(c => c.latlngs)).getBounds();
    else if (t.latlngs?.length >= 3) bounds = L.polygon(t.latlngs).getBounds();
    if (bounds) mapRef.current.fitBounds(bounds, { padding: [40, 40] });
  };

  const selTerr = territories.find(t => t.id === selected);

  if (!ready) {
    return (
      <div style={{display:"flex",alignItems:"center",justifyContent:"center",height:400,color:C.muted,flexDirection:"column",gap:12}}>
        <div style={{fontSize:24,animation:"spin 1s linear infinite"}}>⟳</div>
        <div>Loading world map…</div>
        <style>{`@keyframes spin{from{transform:rotate(0)}to{transform:rotate(360deg)}}`}</style>
      </div>
    );
  }

  return (
    <div style={{display:"flex",flexDirection:"column",height:"100%",gap:12}}>
      <div style={{display:"flex",gap:8,alignItems:"center",flexWrap:"wrap",background:C.panel,borderRadius:12,padding:"10px 14px",border:`1px solid ${C.border}`}}>
        <input value={searchQ} onChange={e=>setSearchQ(e.target.value)} onKeyDown={e=>e.key==="Enter"&&doSearch()} placeholder={mode==="zip"?"Search a zip / city to add to draft…":"Search city, zip, or address to navigate…"} style={inp({width:280})}/>
        <button onClick={doSearch} disabled={searching} style={btn("#091c09","#4ade80")}>{searching?"⟳":"🔍 Search"}</button>
        <span style={{color:C.muted,fontSize:11}}>|</span>
        {mode === "zip" ? (
          <>
            <span style={{fontSize:11,color:"#facc15",fontWeight:800,letterSpacing:".04em"}}>Multi-ZIP: {zipDraft.length} zip{zipDraft.length===1?"":"s"} staged</span>
            {zipDraft.length >= 1 && <button onClick={finishMultiZip} style={btn("#091c09","#4ade80",true)}>✓ Finish & Name</button>}
            <button onClick={cancelMultiZip} style={btn("#1a0808","#f87171")}>✕ Cancel</button>
          </>
        ) : mode === "draw" ? (
          <>
            <span style={{fontSize:11,color:"#60a5fa",fontWeight:800,letterSpacing:".04em"}}>Drawing: {drawPts.length} pt{drawPts.length===1?"":"s"}</span>
            {drawPts.length>=3 && <button onClick={finishDraw} style={btn("#091c09","#4ade80",true)}>✓ Finish</button>}
            <button onClick={cancelDraw} style={btn("#1a0808","#f87171")}>✕ Cancel</button>
          </>
        ) : (
          <button onClick={()=>setShowCreate(true)} style={btn("#091c09","#4ade80",true)}>+ Create Territory</button>
        )}
        <span style={{color:C.muted,fontSize:11,marginLeft:"auto"}}>Default</span>
        <input type="color" value={color} onChange={e=>setColor(e.target.value)} style={{width:28,height:28,border:"none",borderRadius:6,cursor:"pointer"}}/>
        <input type="range" min="0.1" max="0.8" step="0.05" value={opacity} onChange={e=>setOpacity(+e.target.value)} style={{width:70}} title={`Opacity ${Math.round(opacity*100)}%`}/>
      </div>

      <div style={{display:"flex",gap:12,flex:1,minHeight:0}}>
        <div style={{flex:1,position:"relative",borderRadius:12,overflow:"hidden",border:`1px solid ${C.border}`,minHeight:480}}>
          <div ref={mapElRef} style={{position:"absolute",inset:0}}/>
          {mode==="draw" && (
            <div style={{position:"absolute",bottom:12,left:"50%",transform:"translateX(-50%)",background:"#000d",borderRadius:8,padding:"6px 14px",fontSize:11,color:"#60a5fa",pointerEvents:"none",zIndex:1000}}>
              {drawPts.length===0?"Click on the map to start adding territory corners":drawPts.length<3?`${drawPts.length} point${drawPts.length===1?"":"s"} — need 3+ to finish`:"Click ✓ Finish to save"}
            </div>
          )}
          {mode==="zip" && (
            <div style={{position:"absolute",bottom:12,left:"50%",transform:"translateX(-50%)",background:"#000d",borderRadius:8,padding:"6px 14px",fontSize:11,color:"#facc15",pointerEvents:"none",zIndex:1000}}>
              Search a zip / city above to add it. Combine multiple, then click ✓ Finish & Name.
            </div>
          )}
        </div>
        <div style={{width:240,display:"flex",flexDirection:"column",gap:10}}>
          <div style={{background:C.panel,border:`1px solid ${C.border}`,borderRadius:12,padding:"12px 14px",flex:1,overflowY:"auto"}}>
            {(() => {
              // Bucket by status, then apply the active filter + search + sort.
              const withStatus = territories.map(t => ({ t, status: territoryStatus(t), statusDef: TERRITORY_STATUSES[territoryStatus(t)] }));
              const counts = Object.fromEntries(Object.keys(TERRITORY_STATUSES).map(k => [k, withStatus.filter(x => x.status === k).length]));
              const q = listSearch.trim().toLowerCase();
              let shown = withStatus.filter(({ status, t }) => {
                if (listStatusFilter !== "all" && status !== listStatusFilter) return false;
                if (!q) return true;
                return [t.label, t.stateName].filter(Boolean).some(v => v.toLowerCase().includes(q));
              });
              // Sort
              const sortFns = {
                name:      (a,b) => (a.t.label||"").localeCompare(b.t.label||""),
                status:    (a,b) => a.status.localeCompare(b.status) || (a.t.label||"").localeCompare(b.t.label||""),
                assignees: (a,b) => ((b.t.assignees?.length||0) - (a.t.assignees?.length||0)) || (a.t.label||"").localeCompare(b.t.label||""),
                newest:    (a,b) => (b.t.createdAt||"").localeCompare(a.t.createdAt||""),
              };
              shown = shown.sort(sortFns[listSort] || sortFns.name);
              const sel = listStatusFilter === "all" ? null : TERRITORY_STATUSES[listStatusFilter];
              const selColor = sel?.color || C.accent;
              return (
                <>
                  {/* Stacked single-column controls: search → status filter → sort. Each row
                      is full-width so the dropdown values are never clipped on the 240px rail. */}
                  <input value={listSearch} onChange={e=>setListSearch(e.target.value)} placeholder="🔍 Search territories…" style={inp({width:"100%",boxSizing:"border-box",fontSize:11,padding:"6px 9px",marginBottom:6})}/>
                  <select
                    value={listStatusFilter}
                    onChange={e=>{ setListStatusFilter(e.target.value); setEditMenuFor(null); }}
                    title="Filter by status"
                    style={{...inp({width:"100%",boxSizing:"border-box",fontSize:11,padding:"6px 8px",cursor:"pointer",marginBottom:6}), color:selColor, borderColor: sel ? selColor+"66" : C.border, fontWeight: sel?700:500}}
                  >
                    <option value="all">● All ({territories.length})</option>
                    {Object.entries(TERRITORY_STATUSES).map(([k,s]) => (
                      <option key={k} value={k}>{s.icon} {s.label} ({counts[k]||0})</option>
                    ))}
                  </select>
                  <select value={listSort} onChange={e=>setListSort(e.target.value)} title="Sort by" style={inp({width:"100%",boxSizing:"border-box",fontSize:11,padding:"6px 8px",cursor:"pointer",marginBottom:9})}>
                    <option value="name">Sort: A–Z</option>
                    <option value="status">Sort: By status</option>
                    <option value="assignees">Sort: By assigned</option>
                    <option value="newest">Sort: Newest first</option>
                  </select>
                  {/* Active-filter pill — clear, single line showing what's filtered with one-click clear */}
                  {sel && (
                    <div style={{display:"flex",alignItems:"center",gap:6,marginBottom:9,padding:"5px 9px",background:selColor+"15",border:`1px solid ${selColor}44`,borderRadius:7}}>
                      <span style={{fontSize:11}}>{sel.icon}</span>
                      <span style={{flex:1,fontSize:11,fontWeight:700,color:selColor}}>{sel.label}</span>
                      <span style={{fontSize:10,color:selColor,opacity:0.75}}>{counts[listStatusFilter]||0}</span>
                      <button onClick={()=>setListStatusFilter("all")} title="Clear status filter" style={{background:"transparent",border:"none",color:selColor,fontSize:13,cursor:"pointer",padding:"0 2px",lineHeight:1,fontFamily:"inherit"}}>✕</button>
                    </div>
                  )}
                  {shown.length===0&&<div style={{color:C.dim,fontSize:11,lineHeight:1.5,padding:"8px 2px"}}>
                    {territories.length===0 ? "No territories yet. Search a location, build a multi-zip, or use Draw to outline an area." : "No territories match your filter."}
                  </div>}
                  {shown.map(({t, statusDef})=>{
                    const isRestricted = statusDef?.restricted;
                    const kindIcon = isRestricted ? (statusDef.icon || "🔒") : (t.kind === "circle" ? "⊙" : t.kind === "multipolygon" ? "🔢" : "🗺️");
                    const subLabel = isRestricted ? "🔒 locked" : (t.assignees?.length>0?`${t.assignees.length} assigned`:(statusDef?.assignable===false?"unassignable":"unassigned"));
                    const menuOpen = editMenuFor === t.id;
                    return (
                      <div key={t.id} onClick={()=>{ setSelected(t.id===selected?null:t.id); fitToTerritory(t); setEditMenuFor(null); }} style={{display:"flex",alignItems:"center",gap:8,padding:"7px 10px",borderRadius:8,cursor:"pointer",background:selected===t.id?"#162035":"transparent",marginBottom:4,position:"relative"}}>
                        <div style={{width:12,height:12,borderRadius:3,background:t.color,flexShrink:0}}/>
                        <div style={{flex:1,minWidth:0}}>
                          <div style={{fontSize:12,color:C.text,overflow:"hidden",textOverflow:"ellipsis",whiteSpace:"nowrap"}}>{t.label}</div>
                          <div style={{display:"flex",alignItems:"center",gap:5,marginTop:2}}>
                            <span style={{fontSize:9,fontWeight:800,color:statusDef?.color||C.dim,background:(statusDef?.color||C.dim)+"15",border:`1px solid ${(statusDef?.color||C.dim)}44`,borderRadius:4,padding:"1px 5px",letterSpacing:".05em",display:"flex",alignItems:"center",gap:3}}>
                              <span>{statusDef?.icon||"•"}</span><span>{statusDef?.label||"Active"}</span>
                            </span>
                            <span style={{fontSize:9,color:C.dim,display:"flex",alignItems:"center",gap:3}}><span>{kindIcon}</span><span>{subLabel}</span></span>
                          </div>
                        </div>
                        {!isRestricted && (
                          <TerritoryRowMenu
                            isOpen={menuOpen}
                            onToggle={()=>setEditMenuFor(menuOpen?null:t.id)}
                            onClose={()=>setEditMenuFor(null)}
                            onEdit={()=>{ setSelected(t.id); fitToTerritory(t); setEditMenuFor(null); }}
                            onDelete={()=>{ if(confirm(`Delete "${t.label}"? This cannot be undone.`)) { onDelete(t.id); setEditMenuFor(null); } }}
                          />
                        )}
                      </div>
                    );
                  })}
                </>
              );
            })()}
          </div>
          {selTerr&&(selTerr.restricted ? (
            <div style={{background:C.panel,border:`1.5px solid ${(RESTRICTION_TYPES[selTerr.restrictionType]?.color||"#dc2626")}55`,borderRadius:12,padding:"12px 14px"}}>
              <div style={{display:"flex",alignItems:"center",gap:7,marginBottom:8}}>
                <span style={{fontSize:14}}>{RESTRICTION_TYPES[selTerr.restrictionType]?.icon || "🔒"}</span>
                <span style={{fontSize:10,color:RESTRICTION_TYPES[selTerr.restrictionType]?.color||"#dc2626",textTransform:"uppercase",letterSpacing:".05em",fontWeight:800}}>
                  {RESTRICTION_TYPES[selTerr.restrictionType]?.label || "Off-Limits"}
                </span>
                <span style={{marginLeft:"auto",background:"#000",color:"#ff4d6b",border:"1px solid #ef4444",borderRadius:4,padding:"1px 6px",fontSize:9,fontWeight:800,letterSpacing:".05em"}}>🔒 LOCKED</span>
              </div>
              <div style={{fontSize:13,fontWeight:800,color:C.text,marginBottom:4}}>{selTerr.label}</div>
              {selTerr.stateName && <div style={{fontSize:11,color:C.muted,marginBottom:10}}>State: {selTerr.stateName}</div>}
              <div style={{fontSize:11,color:C.dim,marginBottom:10,lineHeight:1.5}}>This is a compliance / off-limits area. Assignees and edits are disabled. To remove the restriction, delete this territory.</div>
              <button onClick={()=>{ if(confirm(`Remove the "${selTerr.label}" restriction? You can re-create it anytime.`)) onDelete(selTerr.id); }} style={{...btn("#1a0808","#f87171"),width:"100%",fontSize:11}}>🗑 Remove Restriction</button>
            </div>
          ) : (
            <div style={{background:C.panel,border:`1px solid ${C.border}`,borderRadius:12,padding:"12px 14px"}}>
              <div style={{display:"flex",alignItems:"center",gap:7,marginBottom:8}}>
                <span style={{fontSize:14}}>{selTerr.kind === "circle" ? "⊙" : selTerr.kind === "multipolygon" ? "🔢" : "🗺️"}</span>
                <span style={{fontSize:10,color:C.muted,textTransform:"uppercase",letterSpacing:".05em",fontWeight:800}}>
                  {selTerr.kind === "circle" ? `Circle · ${Math.round((selTerr.radiusMeters||0)/1609.34)} mi` : selTerr.kind === "multipolygon" ? `Multi-ZIP (${selTerr.components?.length||0})` : "Polygon"}
                </span>
              </div>
              <input defaultValue={selTerr.label} onBlur={e=>onSave({...selTerr,label:e.target.value})} style={{...inp({width:"100%",boxSizing:"border-box",padding:"6px 9px",fontSize:12}),marginBottom:8}}/>
              <div style={{display:"flex",alignItems:"center",gap:6,marginBottom:10}}>
                <input type="color" defaultValue={selTerr.color} onBlur={e=>onSave({...selTerr,color:e.target.value})} style={{width:32,height:26,border:"none",borderRadius:5,cursor:"pointer"}}/>
                <span style={{fontSize:10,color:C.dim}}>color</span>
              </div>
              {/* Status picker — set Active / Available / Resale. Restricted statuses are
                  set via the "Create Territory → Off-Limits" flow which builds the polygon
                  + flags it as restricted, so we don't surface them here. */}
              <div style={{fontSize:10,color:C.dim,fontWeight:700,marginBottom:5,letterSpacing:".04em"}}>STATUS</div>
              <div style={{display:"flex",gap:4,marginBottom:10}}>
                {["active","available","resale"].map(k => {
                  const s = TERRITORY_STATUSES[k];
                  const sel = territoryStatus(selTerr) === k;
                  return (
                    <button key={k} onClick={()=>onSave({...selTerr, status: k, restricted: false})} title={s.desc} style={{flex:1,background:sel?s.color+"22":"#090f1c",border:`1.5px solid ${sel?s.color+"77":C.border}`,borderRadius:7,padding:"6px 4px",color:sel?s.color:C.muted,fontSize:10,fontWeight:sel?800:600,cursor:"pointer",fontFamily:"inherit",display:"flex",alignItems:"center",justifyContent:"center",gap:3}}>
                      <span>{s.icon}</span><span>{s.label}</span>
                    </button>
                  );
                })}
              </div>
              {territoryStatus(selTerr)==="available" && <div style={{fontSize:10,color:"#60a5fa",background:"#091420",border:"1px solid #60a5fa33",borderRadius:6,padding:"6px 9px",marginBottom:10,lineHeight:1.4}}>📦 Open for awarding. Inbound leads pointing here won't be flagged as conflicts. Franchisees can't be assigned until status changes to Active or Resale.</div>}
              {territoryStatus(selTerr)==="resale" && <div style={{fontSize:10,color:"#a78bfa",background:"#1a1429",border:"1px solid #a78bfa33",borderRadius:6,padding:"6px 9px",marginBottom:10,lineHeight:1.4}}>🔄 Marked for resale. Inbound leads still flag as conflicts, but the conflict reason will surface as "resale possible" so reps can start that conversation.</div>}
              <div style={{fontSize:10,color:C.dim,fontWeight:700,marginTop:4,marginBottom:5,letterSpacing:".04em"}}>ASSIGNEES ({selTerr.assignees?.length||0})</div>
              {(!selTerr.assignees || selTerr.assignees.length===0) && <div style={{fontSize:11,color:C.dim,fontStyle:"italic",marginBottom:8}}>None assigned</div>}
              <div style={{display:"flex",flexWrap:"wrap",gap:4,marginBottom:10}}>
                {(selTerr.assignees||[]).map(a => (
                  <span key={a.id} onClick={()=>onNavigate?.(a)} style={{background:"#091420",color:"#60a5fa",border:"1px solid #60a5fa44",borderRadius:5,padding:"2px 8px",fontSize:11,fontWeight:700,cursor:onNavigate?"pointer":"default"}} title={onNavigate?"Click to open profile":""}>
                    {a.name}
                  </span>
                ))}
              </div>
              <div style={{display:"flex",gap:6}}>
                <button onClick={()=>setEditingTerr(selTerr)} style={{...btn(C.dim,C.muted),flex:1,fontSize:11}}>✏️ Edit</button>
                <button onClick={()=>onDelete(selTerr.id)} style={{...btn("#1a0808","#f87171"),flex:1,fontSize:11}}>🗑 Delete</button>
              </div>
            </div>
          ))}
        </div>
      </div>

      {pending && (
        <TerritoryForm
          initial={null}
          pending={pending}
          assignableRecords={assignableRecords}
          defaults={{ label: defaultLabel, color, opacity }}
          onSave={saveNewTerritory}
          onCancel={()=>{ setPending(null); setDefaultLabel(""); }}
        />
      )}
      {editingTerr && (
        <TerritoryForm
          initial={editingTerr}
          assignableRecords={assignableRecords}
          onSave={(data)=>{ onSave({...editingTerr, ...data}); setEditingTerr(null); }}
          onCancel={()=>setEditingTerr(null)}
        />
      )}
      {showCreate && (
        <CreateTerritoryModal
          onCancel={()=>setShowCreate(false)}
          onModeSelected={(m)=>{ setMode(m); setShowCreate(false); }}
          onPendingCreated={(p, label)=>{ setPending(p); setDefaultLabel(label||""); setShowCreate(false); }}
          onCreateRestricted={(territories)=>{ territories.forEach(t => onSave(t)); setShowCreate(false); setListTab("offlimits"); }}
        />
      )}
    </div>
  );
}

// ═══════════════════════════════════════════════════════════
//  MAIN APP
// ═══════════════════════════════════════════════════════════
// Note: this top-level component is intentionally brand-agnostic. The product
// name shown to users comes from `BRAND.name` (see src/branding.js).
function App() {
  const [brands,        setBrands]        = useState([]);
  const [activeBrand,   setActiveBrand]   = useState(null);
  const [leads,         setLeads]         = useState({});
  const [opps,          setOpps]          = useState({});
  const [dupLeads,      setDupLeads]      = useState({}); // {[brandId]: [duplicateEntries]}
  const [reports,       setReports]       = useState({}); // {[brandId]: ReportEntry[]}
  const [analyticsTab,  setAnalyticsTab]  = useState("overview"); // overview | reports
  const [overviewMode,  setOverviewMode]  = useState("month"); // week | month | quarter | year | custom
  const [overviewAnchor,setOverviewAnchor]= useState(() => new Date().toISOString());
  const [overviewCustom,setOverviewCustom]= useState(() => { const t = new Date(); const a = new Date(t.getTime() - 30*86400000); return { from: a.toISOString().slice(0,10), to: t.toISOString().slice(0,10) }; });
  const [brokerAnalyticsTab, setBrokerAnalyticsTab] = useState("brokers"); // brokers | networks
  const [territories,   setTerritories]   = useState({});
  const [leadStages,    setLeadStages]    = useState({});
  const [oppStages,     setOppStages]     = useState({});
  const [scores,        setScores]        = useState({});
  const [scoreFeedback, setScoreFeedback] = useState({});
  const [nav,           setNav]           = useState("whats_next");
  const [subView,       setSubView]       = useState("list");
  const [selected,      setSelected]      = useState(null);
  const [modal,         setModal]         = useState(null);
  const [modalData,     setModalData]     = useState({});
  const [toast,         setToast]         = useState(null);
  const [loading,       setLoading]       = useState(true);
  const [whatsNext,     setWhatsNext]     = useState(null);
  const [wnLoading,     setWnLoading]     = useState(false);
  const [aiSummary,     setAiSummary]     = useState("");
  const [aiOrganize,    setAiOrganize]    = useState(null);
  const [aiOrganizeLoading, setAiOrganizeLoading] = useState(false);
  const [docusignKey,   setDocusignKey]   = useState(()=>localStorage.getItem("ff_docusign_key")||"");
  const saveDocusignKey = (k)=>{
    const v = k||"";
    const wasConnected = !!docusignKey;
    setDocusignKey(v);
    if(v) localStorage.setItem("ff_docusign_key",v); else localStorage.removeItem("ff_docusign_key");
    // If a previously-connected integration just got disconnected, fire a notification.
    if (wasConnected && !v) {
      // Defer to next tick so pushNotification is in scope from useCallback closure.
      setTimeout(()=>pushNotification("integration_off", {integration:"DocuSign"}, {dedupeKey:"integration_off_DocuSign"}), 50);
    }
  };
  const hasDocusignKey = !!docusignKey;
  const [settings,      setSettings]      = useState(DEFAULT_SETTINGS);
  const setSetting = (key, value) => { const next = {...settings, [key]: value}; p("ff4_settings", setSettings, next); };
  const [aiLoading,     setAiLoading]     = useState(false);
  const [scoringIds,    setScoringIds]    = useState(new Set());
  const [fddParsing,    setFddParsing]    = useState(false);
  const [sidebarOpen,   setSidebarOpen]   = useState(true);
  // Templates view state lives at the App scope (instead of inside TemplatesView)
  // so parent re-renders — like a sidebar toggle — don't unmount the view and kick the
  // rep out of the template editor mid-edit.
  const [templatesEditing,    setTemplatesEditing]    = useState(null);
  const [templatesChooseType, setTemplatesChooseType] = useState(null);
  const [templatesTab,        setTemplatesTab]        = useState("mine");
  const [templatesFilter,     setTemplatesFilter]     = useState("all");
  const [templatesSearch,     setTemplatesSearch]     = useState("");
  const [templatePreview,     setTemplatePreview]     = useState(null);
  const [chooseFolderId,      setChooseFolderId]      = useState(null); // folder the new template will be created into; null = first available
  const [brandPickerOpen,     setBrandPickerOpen]     = useState(false); // sidebar brand-switcher popover
  // SettingsView's active category lives at App scope so editing a value (which calls
  // setSetting → setSettings → App re-render → SettingsView remount, since SettingsView
  // is defined inside App) doesn't kick the user back to the default "profile" tab.
  const [settingsCat,         setSettingsCat]         = useState("profile");
  const [reportPopup,         setReportPopup]         = useState(null); // report obj — surface a fresh report immediately after generation
  const [brokerDetailId,      setBrokerDetailId]      = useState(null); // when set, BrokerHubView renders the broker profile
  const [networkDetailId,     setNetworkDetailId]     = useState(null); // when set, BrokerHubView renders the network profile (with searchable broker list)
  // Stage change confirmation
  const [stageConfirm,  setStageConfirm]  = useState(null); // {type,id,stageId,label}
  const [closedLostFor, setClosedLostFor] = useState(null); // {type,id}
  const [notifications, setNotifications] = useState([]); // top-of-screen + history
  // New feature state: templates, automations, brokers, broker comms
  const [customTemplates, setCustomTemplates] = useState({}); // {[brandId]:[...]}
  const [automations,     setAutomations]     = useState({}); // {[brandId]:[...]}
  const [brokers,         setBrokers]         = useState({}); // {[brandId]:{networks:[],agents:[]}}
  const [brokerComms,     setBrokerComms]     = useState({}); // {[oppId]:[...messages]} (opp-scoped)
  const [eventTypes,      setEventTypes]      = useState({}); // {[brandId]: EventType[]}
  const [bookings,        setBookings]        = useState({}); // {[brandId]: Booking[]}
  const [availability,    setAvailability]    = useState({}); // {[brandId]: Availability}
  const [templateFolders, setTemplateFolders] = useState({}); // {[brandId]: Folder[]} — flat array with parentId for nesting
  const [discoveryDays,   setDiscoveryDays]   = useState({}); // {[brandId]: DiscoveryDay[]}
  const [franchisees,     setFranchisees]     = useState({}); // {[brandId]: Franchisee[]}
  const [ddayDetailId,    setDdayDetailId]    = useState(null); // open Discovery Day detail
  const [franchiseeDetailId, setFranchiseeDetailId] = useState(null); // open Franchisee profile
  const [folderModal,     setFolderModal]     = useState(null); // null | {mode:"new"|"edit", folder?:Folder, parentId?:string}

  // ── Load ──────────────────────────────────────────────────
  useEffect(() => {
    Promise.all([
      S.get("ff4_brands"),S.get("ff4_leads"),S.get("ff4_opps"),
      S.get("ff4_terr"),S.get("ff4_lstages"),S.get("ff4_ostages"),
      S.get("ff4_scores"),S.get("ff4_sfeedback"),S.get("ff4_activeBrand"),
      S.get("ff4_seeded"),S.get("ff4_ctemplates"),S.get("ff4_autos"),
      S.get("ff4_brokers"),S.get("ff4_bcomms"),S.get("ff4_settings"),
      S.get("ff4_notifications"),
      S.get("ff4_event_types"),S.get("ff4_bookings"),S.get("ff4_availability"),
      S.get("ff4_dup_leads"),
      S.get("ff4_reports"),
      S.get("ff4_template_folders"),
      S.get("ff4_dday"),
      S.get("ff4_franchisees"),
    ]).then(async ([b,l,o,t,ls,os,sc,sf,ab,seeded,ct,autos,brk,bc,settingsLoaded,notifs,evTypesLoaded,bookingsLoaded,availLoaded,dupLoaded,reportsLoaded,foldersLoaded,ddaysLoaded,franchiseesLoaded])=>{
      let bl = b||[];
      let leadsMap = l||{};
      let oppsMap = o||{};
      let lStagesMap = ls||{};
      let oStagesMap = os||{};
      let active = ab;

      // ── Schema migrations ─────────────────────────────────
      const schemaV = (await S.get("ff4_schemav")) || 0;
      if (schemaV < 1) {
        // v1: move "agreement_sent" to AFTER "discovery_day" for any existing brand stages
        const next = {...oStagesMap};
        let changed = false;
        Object.keys(next).forEach(bid => {
          const stages = next[bid];
          if (!Array.isArray(stages)) return;
          const a = stages.findIndex(s => s.id === "agreement_sent");
          const d = stages.findIndex(s => s.id === "discovery_day");
          if (a > -1 && d > -1 && a < d) {
            const moved = [...stages];
            [moved[a], moved[d]] = [moved[d], moved[a]];
            next[bid] = moved;
            changed = true;
          }
        });
        if (changed) { oStagesMap = next; await S.set("ff4_ostages", oStagesMap); }
        await S.set("ff4_schemav", 1);
      }
      if (schemaV < 2) {
        // v2: ensure every brand has a "Closed Lost" stage at the end of its opp pipeline
        const next = {...oStagesMap};
        let changed = false;
        Object.keys(next).forEach(bid => {
          const stages = next[bid];
          if (!Array.isArray(stages)) return;
          if (!stages.some(s => s.id === "closed_lost")) {
            next[bid] = [...stages, { id:"closed_lost", label:"Closed Lost", color:"#64748b", icon:"❌" }];
            changed = true;
          }
        });
        if (changed) { oStagesMap = next; await S.set("ff4_ostages", oStagesMap); }
        await S.set("ff4_schemav", 2);
      }
      if (schemaV < 3) {
        // v3: classify every untagged note as a touchpoint (heuristic backfill).
        // Free + instant; users can click the 📞 icon on any note to flip the tag.
        const tagAll = (recsMap) => {
          let touched = false;
          const next = {...recsMap};
          Object.keys(next).forEach(bid => {
            const arr = next[bid];
            if (!Array.isArray(arr)) return;
            next[bid] = arr.map(rec => {
              const notes = rec.notes || [];
              if (!notes.length || notes.every(n => n.isTouchpoint !== undefined)) return rec;
              touched = true;
              return {...rec, notes: notes.map(n => n.isTouchpoint !== undefined ? n : {...n, isTouchpoint: heuristicClassifyTouchpoint(n.text)})};
            });
          });
          return [next, touched];
        };
        const [newLeads, lt] = tagAll(leadsMap);
        const [newOpps,  ot] = tagAll(oppsMap);
        if (lt) { leadsMap = newLeads; await S.set("ff4_leads", leadsMap); }
        if (ot) { oppsMap  = newOpps;  await S.set("ff4_opps",  oppsMap);  }
        await S.set("ff4_schemav", 3);
      }
      if (schemaV < 5) {
        // v5: backfill isCallAttempt on existing notes via the call heuristic. (v4 was the notif tab seed.)
        const tag = (recsMap) => {
          let touched = false;
          const next = {...recsMap};
          Object.keys(next).forEach(bid => {
            const arr = next[bid];
            if (!Array.isArray(arr)) return;
            next[bid] = arr.map(rec => {
              const notes = rec.notes || [];
              if (!notes.length || notes.every(nt => nt.isCallAttempt !== undefined)) return rec;
              touched = true;
              return {...rec, notes: notes.map(nt => nt.isCallAttempt !== undefined ? nt : {...nt, isCallAttempt: heuristicClassifyCall(nt.text)})};
            });
          });
          return [next, touched];
        };
        const [newLeads2, lt2] = tag(leadsMap);
        const [newOpps2,  ot2] = tag(oppsMap);
        if (lt2) { leadsMap = newLeads2; await S.set("ff4_leads", leadsMap); }
        if (ot2) { oppsMap  = newOpps2;  await S.set("ff4_opps",  oppsMap);  }
        await S.set("ff4_schemav", 5);
      }
      if (schemaV < 6) {
        // v6: seed default event types + availability per brand. Idempotent — skips brands that already have data.
        const evTypesNext = {...(evTypesLoaded||{})};
        const bookingsNext = {...(bookingsLoaded||{})};
        const availNext = {...(availLoaded||{})};
        let touched = false;
        (bl||[]).forEach(brand => {
          if (!evTypesNext[brand.id] || !evTypesNext[brand.id].length) {
            evTypesNext[brand.id] = DEFAULT_EVENT_TYPES.map(et => ({ ...et, id: uid(), isDefault: true, active: true, bufferBeforeMin: 0, bufferAfterMin: 0, createdAt: nowIso() }));
            touched = true;
          }
          if (!bookingsNext[brand.id]) { bookingsNext[brand.id] = []; touched = true; }
          if (!availNext[brand.id])    { availNext[brand.id]    = JSON.parse(JSON.stringify(DEFAULT_AVAILABILITY)); touched = true; }
        });
        if (touched) {
          await S.set("ff4_event_types", evTypesNext);
          await S.set("ff4_bookings",    bookingsNext);
          await S.set("ff4_availability", availNext);
          evTypesLoaded = evTypesNext;
          bookingsLoaded = bookingsNext;
          availLoaded = availNext;
        }
        await S.set("ff4_schemav", 6);
      }
      if (schemaV < 7) {
        // v7: reshape default event types — drop Validation Call Prep / Discovery Day Prep / Award Call,
        // shrink Intro Call to 15min, add Franchise Overview Call. Preserves user-created entries.
        const REMOVED_DEFAULT_SLUGS = new Set(["validation-prep", "discovery-day-prep", "award-call"]);
        const FRESH_DEFAULTS = DEFAULT_EVENT_TYPES.map(et => ({
          ...et, id: uid(), isDefault: true, active: true, bufferBeforeMin: 0, bufferAfterMin: 0, createdAt: nowIso(),
        }));
        const evTypesNext = {...(evTypesLoaded||{})};
        Object.keys(evTypesNext).forEach(brandId => {
          const existing = evTypesNext[brandId] || [];
          // Drop removed defaults; keep user-created entries and any defaults that survived.
          const kept = existing.filter(et => !(et.isDefault && REMOVED_DEFAULT_SLUGS.has(et.slug)));
          const haveSlugs = new Set(kept.map(et => et.slug));
          // Update Intro Call duration if still a default
          const reshaped = kept.map(et => {
            if (et.isDefault && et.slug === "intro-call" && et.durationMin !== 15) {
              return { ...et, durationMin: 15, description: "Quick qualifying call to learn about the candidate's goals and franchise interest." };
            }
            return et;
          });
          // Add any new defaults missing (Franchise Overview Call)
          const additions = FRESH_DEFAULTS.filter(et => !haveSlugs.has(et.slug)).map(et => ({...et, id: uid()}));
          evTypesNext[brandId] = [...reshaped, ...additions];
        });
        await S.set("ff4_event_types", evTypesNext);
        evTypesLoaded = evTypesNext;
        await S.set("ff4_schemav", 7);
      }
      if (schemaV < 8) {
        // v8: backfill `originatedAsLead` on every existing opp so funnel math (conversion rate)
        // counts them as having been a lead at some point. Synthetic flag marks opps whose
        // original lead record never existed (created directly), real flag is set when an opp
        // is converted from an actual lead in `convertToOpp`. Also inject a tutorial
        // `agreement_signed` opp (Devon Mitchell) into tutorial brands so close rate is non-zero.
        const tagOpps = {...oppsMap};
        let touchedOpps = false;
        const nowMs = Date.now();
        const daysAgoIso = (d) => new Date(nowMs - d*86400000).toISOString();
        Object.keys(tagOpps).forEach(bid => {
          const arr = tagOpps[bid];
          if (!Array.isArray(arr)) return;
          let next = arr.map(o => {
            if (o.originatedAsLead) return o;
            touchedOpps = true;
            return { ...o, originatedAsLead: true, syntheticLeadOrigin: !o.originalLeadId, leadOriginAt: o.createdAt };
          });
          // If this looks like the tutorial brand (Maya Chen present) and no agreement_signed opp exists yet, add Devon.
          const isTutorial = next.some(o => /Maya Chen \(Tutorial\)/.test(`${o.firstName} ${o.lastName}`));
          const hasSigned  = next.some(o => o.stage === "agreement_signed");
          if (isTutorial && !hasSigned) {
            const noteAt = (d, text) => ({ id: uid(), text, at: daysAgoIso(d), isTouchpoint: true, isCallAttempt: false });
            next = [...next, {
              id: uid(),
              firstName: "Devon", lastName: "Mitchell (Tutorial)", email: "devon.mitchell@example.com", phone: "704-555-0188",
              company: "Mitchell Hospitality Group", location: "Charlotte, NC", territory: "Charlotte Metro",
              investmentLevel: "$500k–$1M", source: "Referral", stage: "agreement_signed", assignedTo: "You",
              createdAt: daysAgoIso(62), updatedAt: daysAgoIso(3), stageEnteredAt: daysAgoIso(3),
              originatedAsLead: true, syntheticLeadOrigin: true, leadOriginAt: daysAgoIso(62),
              partners: [], activities: [],
              notes: [
                noteAt(62, "Referred by David Thompson. Owns three quick-serve concepts in Charlotte already — knows what he's doing."),
                noteAt(48, "FDD signed within a week. Validation calls all glowing — peers love him."),
                noteAt(20, "Discovery Day was a slam dunk. Devon was clearly ready to commit."),
                noteAt(10, "Agreement sent."),
                noteAt(3,  "🎉 Agreement signed! Devon is officially in. Award call scheduled, opening timeline being drafted."),
              ],
            }];
            touchedOpps = true;
          }
          tagOpps[bid] = next;
        });
        if (touchedOpps) { oppsMap = tagOpps; await S.set("ff4_opps", oppsMap); }
        await S.set("ff4_schemav", 8);
      }
      if (schemaV < 9) {
        // v9: Expand the tutorial brand with ~90 procedurally-generated leads/opps spread across
        // every stage and aged up to 2 years. Adds extra broker networks + agents, a handful of
        // duplicate-lead entries, and a few historical reports so the app feels lived-in.
        // Only runs against a brand that still looks like the original tutorial (Maya Chen present).
        const FIRST_NAMES = ["James","Olivia","Noah","Emma","Liam","Ava","Lucas","Mia","Mason","Sophia","Ethan","Isabella","Logan","Charlotte","Aiden","Amelia","Caleb","Harper","Ryan","Evelyn","Jack","Abigail","Owen","Emily","Daniel","Elizabeth","Henry","Sofia","Sebastian","Aria","Matthew","Ella","Joseph","Scarlett","Levi","Grace","Wyatt","Chloe","Julian","Lily","Asher","Hannah","Grayson","Layla","Leo","Riley","Lincoln","Aurora","Hudson","Nora","Theodore","Zoey","Eli","Mila","Jaxon","Ellie","Cameron","Lucy","Connor","Avery","Diego","Stella","Maya","Hazel","Adrian","Brooklyn","Ezra","Naomi","Roman","Aaliyah","Xavier","Madison","Sawyer","Camila","Jonah","Penelope","Felix","Eliana","Beckett","Aubrey","Tobias","Anna","Silas","Maria","Bennett","Iris","Ronan","Violet","Hugo","Genesis","Knox","Rose","Quinn","Skylar"];
        const LAST_NAMES = ["Garcia","Williams","Smith","Johnson","Brown","Davis","Miller","Wilson","Moore","Taylor","Anderson","Thomas","Jackson","White","Harris","Martin","Thompson","Robinson","Clark","Rodriguez","Lewis","Lee","Walker","Hall","Allen","Young","King","Wright","Lopez","Hill","Scott","Green","Adams","Baker","Gonzalez","Nelson","Carter","Mitchell","Perez","Roberts","Turner","Phillips","Campbell","Parker","Evans","Edwards","Collins","Stewart","Sanchez","Morris","Rogers","Reed","Cook","Morgan","Bell","Murphy","Bailey","Rivera","Cooper","Richardson","Cox","Howard","Ward","Torres","Peterson","Gray","Ramirez","Brooks","Foster","Bryant","Long","Hughes","Flores","Washington","Butler","Simmons","Patterson","Diaz","Hamilton","Graham","Sullivan","Wallace","Woods","Cole","Reynolds","Hunter","Holmes","Black","Sanders","Pierce","Henderson","Coleman","Jenkins","Perry","Powell","Gibson","Russell","Stone","Curtis"];
        const COMPANIES = ["Holdings LLC","Capital Group","Ventures","Hospitality","Investments","Properties","Brands","Enterprise","Group","Partners","Equity","Restaurants","Wellness","Co","Brands LLC","Industries"];
        const CITIES = [["Dallas","TX","DFW"],["Houston","TX","Houston Metro"],["Austin","TX","Austin"],["San Antonio","TX","South Texas"],["Phoenix","AZ","Phoenix Metro"],["Tucson","AZ","Southern AZ"],["Denver","CO","Front Range"],["Boulder","CO","Boulder"],["Las Vegas","NV","Las Vegas"],["Reno","NV","Northern NV"],["Salt Lake City","UT","Wasatch Front"],["Portland","OR","Pacific Northwest"],["Seattle","WA","Puget Sound"],["Spokane","WA","Eastern WA"],["Sacramento","CA","Sacramento Valley"],["San Diego","CA","Southern California"],["San Jose","CA","Bay Area"],["Fresno","CA","Central Valley"],["Boise","ID","Treasure Valley"],["Albuquerque","NM","Central NM"],["Oklahoma City","OK","OKC Metro"],["Tulsa","OK","Eastern OK"],["Wichita","KS","Central KS"],["Kansas City","MO","KC Metro"],["St. Louis","MO","St. Louis Metro"],["Memphis","TN","Mid-South"],["Nashville","TN","Middle Tennessee"],["Knoxville","TN","East TN"],["Atlanta","GA","Metro Atlanta"],["Savannah","GA","Coastal GA"],["Jacksonville","FL","Northeast FL"],["Orlando","FL","Central FL"],["Tampa","FL","Tampa Bay"],["Miami","FL","South Florida"],["Charlotte","NC","Charlotte Metro"],["Raleigh","NC","Triangle"],["Charleston","SC","Lowcountry"],["Columbia","SC","Midlands"],["Louisville","KY","Louisville Metro"],["Lexington","KY","Bluegrass"],["Indianapolis","IN","Central IN"],["Columbus","OH","Central OH"],["Cleveland","OH","Northeast OH"],["Cincinnati","OH","Greater Cincinnati"],["Detroit","MI","Metro Detroit"],["Grand Rapids","MI","West MI"],["Milwaukee","WI","Milwaukee Metro"],["Madison","WI","South-Central WI"],["Minneapolis","MN","Twin Cities"],["Buffalo","NY","Western NY"],["Rochester","NY","Finger Lakes"],["Pittsburgh","PA","Pittsburgh Metro"],["Philadelphia","PA","Greater Philly"],["Richmond","VA","Central VA"],["Norfolk","VA","Hampton Roads"],["Baltimore","MD","Baltimore Metro"]];
        const SOURCES = ["Referral","Website","Trade Show","LinkedIn","Google Ads","Facebook Ads","Franchise Expo","Direct Inquiry","Broker","Partner","Conference","Webinar"];
        const INVESTMENT_LEVELS = ["$100k–$250k","$250k–$500k","$500k–$1M","$1M+"];
        const oppsTouched = {...(oppsMap||{})}, leadsTouched = {...(leadsMap||{})}, brokersTouched = {...(brk||{})}, dupTouched = {...(dupLoaded||{})}, reportsTouched = {...(reportsLoaded||{})};
        let didExpand = false;
        Object.keys(oppsTouched).forEach(brandId => {
          const oppList = oppsTouched[brandId] || [];
          const leadList = leadsTouched[brandId] || [];
          const isTutorial = oppList.some(o => /Maya Chen \(Tutorial\)/.test(`${o.firstName} ${o.lastName}`));
          const brandRecord = (bl||[]).find(b => b.id === brandId);
          if (!isTutorial || brandRecord?.tutorialExpanded) return;
          didExpand = true;
          // Seeded pseudo-random — order-stable across reloads, but varied.
          let _seed = 0xa1b2;
          const rand = () => { _seed = (_seed * 9301 + 49297) % 233280; return _seed / 233280; };
          const pick = (arr) => arr[Math.floor(rand() * arr.length)];
          const daysAgoIso = (d) => new Date(Date.now() - d*86400000).toISOString();
          // Stage targets — every stage gets >=4 records. Plus aged signed/lost so close rate + history exist.
          const LEAD_STAGE_TARGETS = { new_lead: 12, contacted: 8, nurturing: 7, disqualified: 4 };
          const OPP_STAGE_TARGETS = { qualified: 6, intro_call: 6, fdd_sent: 5, fdd_signed: 5, fdd_review_call: 4, application: 4, validation: 4, ceo_qa: 4, intake_form: 4, discovery_day: 4, agreement_sent: 4, agreement_signed: 7, closed_lost: 8 };
          const mkName = () => `${pick(FIRST_NAMES)} ${pick(LAST_NAMES)} (Tutorial)`;
          const mkCity = () => pick(CITIES);
          const mkLead = (stage) => {
            const [first, ...rest] = mkName().split(" ");
            const last = rest.join(" ");
            const [city, st, terr] = mkCity();
            const ageDays = Math.floor(rand() * (stage === "nurturing" ? 730 : stage === "disqualified" ? 540 : 180)) + 3;
            const lastTouch = Math.max(1, Math.floor(rand() * Math.min(ageDays, 45)));
            const noteCount = 1 + Math.floor(rand() * 4);
            const notes = [];
            for (let i = 0; i < noteCount; i++) {
              const dAgo = Math.max(1, Math.floor(ageDays - (i * ageDays / noteCount)));
              const isTouch = rand() > 0.5;
              notes.push({ id: uid(), text: pick([`Initial inquiry through ${pick(SOURCES).toLowerCase()}.`,"Sent territory availability list.","Asked about ROI expectations and timeline.","Forwarded FDD link.","Left voicemail — no callback yet.","Spoke briefly, wants to think it over.","Lead mentioned competitor brands they're also considering.","Confirmed financing pre-qualification."]), at: daysAgoIso(dAgo), isTouchpoint: isTouch, isCallAttempt: !isTouch });
            }
            return { id: uid(), firstName: first, lastName: last, email: `${first.toLowerCase()}.${last.split(" ")[0].toLowerCase()}${Math.floor(rand()*99)}@example.com`, phone: `${Math.floor(200+rand()*700)}-555-${String(Math.floor(rand()*10000)).padStart(4,"0")}`, company: `${last.split(" ")[0]} ${pick(COMPANIES)}`, location: `${city}, ${st}`, territory: terr, investmentLevel: pick(INVESTMENT_LEVELS), source: pick(SOURCES), stage, assignedTo: "You", createdAt: daysAgoIso(ageDays), updatedAt: daysAgoIso(lastTouch), stageEnteredAt: daysAgoIso(Math.max(1, Math.floor(rand()*Math.min(ageDays,30)))), notes, activities:[], partners:[] };
          };
          const mkOpp = (stage) => {
            const l = mkLead(stage);
            const isSigned = stage === "agreement_signed";
            const isLost   = stage === "closed_lost";
            const baseAge = isSigned ? 90 + Math.floor(rand()*640) : isLost ? 60 + Math.floor(rand()*550) : 14 + Math.floor(rand()*260);
            const stageAge = stage === "closed_lost" ? Math.max(2, Math.floor(rand()*120)) : stage === "agreement_signed" ? Math.max(2, Math.floor(rand()*60)) : Math.max(1, Math.floor(rand()*30));
            return { ...l, stage, createdAt: daysAgoIso(baseAge), stageEnteredAt: daysAgoIso(stageAge), updatedAt: daysAgoIso(Math.max(1, Math.floor(stageAge / 2))), originatedAsLead: true, syntheticLeadOrigin: true, leadOriginAt: daysAgoIso(baseAge), lostReason: isLost ? pick(["confirmed_lost","ghosted","not_ready"]) : undefined };
          };
          // Generate
          const moreLeads = [];
          Object.entries(LEAD_STAGE_TARGETS).forEach(([stg, n]) => { for (let i=0;i<n;i++) moreLeads.push(mkLead(stg)); });
          const moreOpps = [];
          Object.entries(OPP_STAGE_TARGETS).forEach(([stg, n]) => { for (let i=0;i<n;i++) moreOpps.push(mkOpp(stg)); });
          // Extra broker networks + agents
          const existingBroker = brokersTouched[brandId] || { networks:[], agents:[] };
          const newNetworks = [
            { id: uid(), name: "FranServe Northeast", website: "https://franserve.example", phone: "800-555-0211", email: "regional@franserve.example", color: "#34d399", notes: "Strong placement velocity in NY/NJ/CT. Specializes in food & service concepts.", createdAt: daysAgoIso(420) },
            { id: uid(), name: "Pivot Franchise Group", website: "https://pivotfranchise.example", phone: "800-555-0319", email: "consultants@pivotfranchise.example", color: "#fb923c", notes: "Mid-size network covering Southeast + Midwest. Heavy fitness vertical.", createdAt: daysAgoIso(300) },
          ];
          const newAgents = [
            { id: uid(), firstName:"Marcus",  lastName:"Reyes (Tutorial)",      email:"marcus.reyes@franserve.example",    phone:"212-555-0388", networkId:newNetworks[0].id, specialty:"QSR & casual dining", commission:"45% of franchise fee", notes:"NYC-based. Top performer in 2024. Sends 3-4 candidates/mo.", createdAt: daysAgoIso(380) },
            { id: uid(), firstName:"Joanna",  lastName:"Klein (Tutorial)",      email:"joanna.klein@franserve.example",    phone:"617-555-0412", networkId:newNetworks[0].id, specialty:"Service & home-based",   commission:"$12k flat",         notes:"Boston territory. Slower volume but high close rate.",   createdAt: daysAgoIso(290) },
            { id: uid(), firstName:"Devin",   lastName:"Okoro (Tutorial)",      email:"devin.okoro@pivotfranchise.example",phone:"404-555-0511", networkId:newNetworks[1].id, specialty:"Fitness & wellness",   commission:"40% of franchise fee", notes:"Atlanta hub. Strong fitness pipeline.",                  createdAt: daysAgoIso(260) },
            { id: uid(), firstName:"Tara",    lastName:"Wexler (Tutorial)",     email:"tara.wexler@pivotfranchise.example",phone:"312-555-0623", networkId:newNetworks[1].id, specialty:"Retail & beauty",      commission:"35% of franchise fee", notes:"Chicago-based. Beauty/retail specialist.",               createdAt: daysAgoIso(200) },
            { id: uid(), firstName:"Hassan",  lastName:"Mahmoud (Tutorial)",    email:"hassan.mahmoud@pivotfranchise.example",phone:"832-555-0744", networkId:newNetworks[1].id, specialty:"Health & nutrition", commission:"40% of franchise fee", notes:"Houston. Strong in QSR-adjacent health brands.",         createdAt: daysAgoIso(150) },
            { id: uid(), firstName:"Priya",   lastName:"Nair (Tutorial)",       email:"priya.nair@franserve.example",      phone:"609-555-0859", networkId:newNetworks[0].id, specialty:"Education & tutoring",  commission:"$10k flat",         notes:"NJ-based. Education brands are her sweet spot.",         createdAt: daysAgoIso(120) },
          ];
          brokersTouched[brandId] = { networks: [...existingBroker.networks, ...newNetworks], agents: [...existingBroker.agents, ...newAgents] };
          // Assign brokers to some opps (every 3rd opp gets a broker, deterministic-ish)
          const allBrokerIds = [...existingBroker.agents.map(a=>a.id), ...newAgents.map(a=>a.id)];
          moreOpps.forEach((o, idx) => { if (idx % 3 === 0 && allBrokerIds.length) o.brokerId = allBrokerIds[idx % allBrokerIds.length]; });
          // Persist
          leadsTouched[brandId] = [...leadList, ...moreLeads];
          oppsTouched[brandId]  = [...oppList,  ...moreOpps];
          // Duplicate leads — 5 entries that look like dup attempts
          const dupList = dupTouched[brandId] || [];
          const oppsForDup = oppsTouched[brandId].slice(0, 5);
          const moreDups = oppsForDup.map((o,i) => ({ ...mkLead("new_lead"), id: uid(), email: o.email, dupOf: o.id, dupOfType: "opp", dupReason: i%2===0?"email":"phone", flaggedAt: daysAgoIso(2 + i*5), createdAt: daysAgoIso(2 + i*5) }));
          dupTouched[brandId] = [...dupList, ...moreDups];
          // Historical reports — 3 entries (weekly, monthly, AI strategy from 2mo ago)
          const reportList = reportsTouched[brandId] || [];
          const histSnap = () => {
            const allOpps = oppsTouched[brandId];
            const allLeads = leadsTouched[brandId];
            const signed = allOpps.filter(o=>o.stage==="agreement_signed").length;
            const lost = allOpps.filter(o=>o.stage==="closed_lost").length;
            const stages = {};
            DEFAULT_OPP_STAGES.forEach(s => { stages[s.id] = { label: s.label, color: s.color, count: allOpps.filter(o=>o.stage===s.id).length }; });
            return {
              kpis: { leadCount: Math.floor(allLeads.length*0.7), oppCount: Math.floor(allOpps.length*0.8), signed: Math.max(1,signed-2), lost: Math.max(0,lost-3), convRate: 62, closeRate: 14, avgScore: 71, avgCloseDays: 88 },
              stages,
              topOpps: allOpps.slice(0,5).map(o => ({ id:o.id, name:`${o.firstName} ${o.lastName}`, stage:o.stage, score: 70+Math.floor(rand()*25) })),
              staleList: allOpps.filter(o=>{const stg=DEFAULT_OPP_STAGES.find(s=>s.id===o.stage); return stg?.staleDays && daysSince(o.stageEnteredAt) >= stg.staleDays;}).slice(0,5).map(o => ({ id:o.id, name:`${o.firstName} ${o.lastName}`, stage:o.stage, days: daysSince(o.stageEnteredAt) })),
              dropouts: allOpps.filter(o=>o.stage==="closed_lost").slice(0,4).map(o => ({ id:o.id, name:`${o.firstName} ${o.lastName}`, reason:o.lostReason||"unknown" })),
              brokerActivity: brokersTouched[brandId].agents.slice(0,5).map(a => ({ id:a.id, name:`${a.firstName} ${a.lastName}`, assignedOpps: allOpps.filter(o=>o.brokerId===a.id).length, comms: Math.floor(rand()*6) })),
              sourcePerformance: Object.entries(allOpps.reduce((acc,o)=>{ const s=o.source||"Unspecified"; if(!acc[s])acc[s]={name:s,leads:0,opps:0,signed:0,lost:0}; acc[s].opps++; if(o.stage==="agreement_signed") acc[s].signed++; if(o.stage==="closed_lost") acc[s].lost++; return acc; },{})).map(([,v])=>({...v, total: v.opps+v.leads, convRate: 60+Math.floor(rand()*25), closeRate: v.opps?Math.round(v.signed/v.opps*100):0})).slice(0,6),
            };
          };
          const wkStart = new Date(Date.now() - 14*86400000);
          const wkEnd   = new Date(Date.now() - 7*86400000);
          const moStart = new Date(Date.now() - 60*86400000);
          const moEnd   = new Date(Date.now() - 30*86400000);
          const aiAt    = new Date(Date.now() - 56*86400000);
          const histReports = [
            { id: uid(), type:"weekly",  title:`Weekly · ${wkStart.toLocaleDateString("en-US",{month:"short",day:"numeric"})}–${wkEnd.toLocaleDateString("en-US",{month:"short",day:"numeric"})}`,    generatedAt: daysAgoIso(7),  period:{start:wkStart.toISOString(), end:wkEnd.toISOString()}, filters:{}, data: histSnap() },
            { id: uid(), type:"monthly", title:`Monthly · ${moStart.toLocaleDateString("en-US",{month:"long",year:"numeric"})}`, generatedAt: daysAgoIso(30), period:{start:moStart.toISOString(), end:moEnd.toISOString()}, filters:{}, data: histSnap() },
            { id: uid(), type:"ai_strategy", title:`🤖 AI Strategy · ${aiAt.toLocaleDateString("en-US",{month:"short",day:"numeric",year:"numeric"})}`, generatedAt: aiAt.toISOString(), period:{start:null, end:null}, filters:{}, data: histSnap(), aiAdvice: `## What's working\n- Strong velocity on QSR-adjacent referrals — close rate trending above 14%.\n- Broker network coverage now spans 5 specialists across 3 networks; placement quality climbing.\n\n## Biggest risk\n- ~30% of opps sit in FDD-Sent / FDD-Signed without forward motion in 14d+. The middle of the funnel is the leak.\n\n## Next actions\n- **Force a weekly FDD review cadence** on every Mon — every record older than 10d gets a call or a documented "park" decision.\n- **Double down on Referral** as the top performing source — it converts at ~2x other channels.\n- **Cut spend on Google Ads** unless attribution data improves; the quality is well below referrals.\n- **Rebalance broker workload** — Sarah Chen is at capacity, Marcus Reyes has spare bandwidth.\n\n## Bold bet (next 30 days)\n- Set a hard rule: any opp inactive 21d gets explicit disqualify-or-revive treatment from the rep, no soft holding. Measure pipeline cleanliness weekly.` },
          ];
          reportsTouched[brandId] = [...reportList, ...histReports];
          // Mark brand as expanded
          const brandsCurrent = bl || [];
          bl = brandsCurrent.map(b => b.id === brandId ? { ...b, tutorialExpanded: true } : b);
        });
        if (didExpand) {
          leadsMap = leadsTouched; oppsMap = oppsTouched;
          await Promise.all([
            S.set("ff4_leads", leadsMap),
            S.set("ff4_opps", oppsMap),
            S.set("ff4_brokers", brokersTouched),
            S.set("ff4_dup_leads", dupTouched),
            S.set("ff4_reports", reportsTouched),
            S.set("ff4_brands", bl),
          ]);
          brk = brokersTouched; dupLoaded = dupTouched; reportsLoaded = reportsTouched;
        }
        await S.set("ff4_schemav", 9);
      }
      if (schemaV < 10) {
        // v10: normalize closed_lost lostReason values to the canonical LOST_REASONS ids
        // (ghosted | confirmed_lost | not_ready). Earlier v9 expansion seeded freeform reason
        // strings that didn't match the LOST_REASONS lookup so the chip rendered as "unknown".
        const VALID = new Set(["ghosted","confirmed_lost","not_ready"]);
        const REMAP = { financing_fell_through:"not_ready", price:"confirmed_lost", timing:"not_ready", competitor:"confirmed_lost", poor_fit:"confirmed_lost", lost_interest:"ghosted" };
        const nextOpps = {...(oppsMap||{})};
        let touched = false;
        Object.keys(nextOpps).forEach(bid => {
          nextOpps[bid] = (nextOpps[bid]||[]).map(o => {
            if (o.stage !== "closed_lost" || !o.lostReason || VALID.has(o.lostReason)) return o;
            const fixed = REMAP[o.lostReason] || "confirmed_lost";
            touched = true;
            return { ...o, lostReason: fixed };
          });
        });
        if (touched) { oppsMap = nextOpps; await S.set("ff4_opps", oppsMap); }
        await S.set("ff4_schemav", 10);
      }
      if (schemaV < 11) {
        // v11: backfill `dueDate` on every existing task. Tasks created before this migration
        // had no due date; now they're required. Default = today, so the rep sees them and
        // can re-schedule manually if needed.
        const today = new Date().toISOString().slice(0,10);
        const backfill = (rec) => rec.tasks ? { ...rec, tasks: rec.tasks.map(t => t.dueDate ? t : { ...t, dueDate: today }) } : rec;
        const nLeads = {...(leadsMap||{})};
        const nOpps = {...(oppsMap||{})};
        Object.keys(nLeads).forEach(bid => { nLeads[bid] = (nLeads[bid]||[]).map(backfill); });
        Object.keys(nOpps).forEach(bid => { nOpps[bid] = (nOpps[bid]||[]).map(backfill); });
        leadsMap = nLeads; oppsMap = nOpps;
        await Promise.all([S.set("ff4_leads", leadsMap), S.set("ff4_opps", oppsMap)]);
        await S.set("ff4_schemav", 11);
      }
      if (schemaV < 12) {
        // v12: seed tasks across tutorial records so the morning digest + tasks panel have
        // real data on first reload. Mix of today / overdue / future / completed. Uses
        // local-time YMD so "due today" stays aligned with the user's calendar (not UTC).
        const today = new Date();
        const ymd = (d) => `${d.getFullYear()}-${String(d.getMonth()+1).padStart(2,"0")}-${String(d.getDate()).padStart(2,"0")}`;
        const dayOffset = (n) => { const d = new Date(today.getTime() + n*86400000); return ymd(d); };
        const nLeads = {...(leadsMap||{})}, nOpps = {...(oppsMap||{})};
        let touched = false;
        Object.keys(nOpps).forEach(brandId => {
          const isTutorial = (nOpps[brandId]||[]).some(o => /Maya Chen \(Tutorial\)/.test(`${o.firstName||""} ${o.lastName||""}`));
          if (!isTutorial) return;
          // Tutorial task plan — stable per-record. Indexes into the per-stage opps so the
          // seeding is deterministic across reloads/wipes.
          const opps = nOpps[brandId];
          const byStage = {};
          opps.forEach(o => { (byStage[o.stage] = byStage[o.stage]||[]).push(o); });
          const plans = [
            // [stage, indexInStage, [{text, daysFromToday, source, completed?}]]
            ["agreement_sent",   0, [{ text:"Confirm signature ETA", days:0, source:"manual" }, { text:"Send onboarding packet", days:2, source:"ai" }]],
            ["agreement_sent",   1, [{ text:"Loop in legal for final review", days:0, source:"ai" }]],
            ["discovery_day",    0, [{ text:"Send Discovery Day agenda", days:0, source:"ai" }, { text:"Book travel confirmation", days:1, source:"manual" }]],
            ["discovery_day",    1, [{ text:"Follow up on hotel block", days:-2, source:"ai" }]],
            ["validation",       0, [{ text:"Schedule validation call w/ existing operator", days:0, source:"ai" }]],
            ["validation",       1, [{ text:"Share validation contact list", days:-1, source:"manual" }]],
            ["fdd_review_call",  0, [{ text:"Send FDD review agenda", days:0, source:"ai" }, { text:"Confirm time zone for call", days:3, source:"ai" }]],
            ["fdd_signed",       0, [{ text:"Move to validation stage prep", days:1, source:"manual" }]],
            ["fdd_sent",         0, [{ text:"Retry call — no response in 5d", days:0, source:"ai" }]],
            ["fdd_sent",         1, [{ text:"Share franchise team intro deck", days:5, source:"manual" }]],
            ["intro_call",       0, [{ text:"Send intro deck", days:-3, source:"ai" }]],
            ["intro_call",       1, [{ text:"Schedule follow-up qualifying call", days:2, source:"ai" }]],
            ["qualified",        0, [{ text:"Send territory map", days:0, source:"manual" }]],
            ["qualified",        1, [{ text:"Confirm financing readiness", days:4, source:"ai" }]],
            ["application",      0, [{ text:"Review submitted application", days:0, source:"ai", completed:true }]],
            ["ceo_qa",           0, [{ text:"Prep CEO with candidate background", days:1, source:"ai" }]],
            ["intake_form",      0, [{ text:"Chase missing intake fields", days:0, source:"ai" }]],
          ];
          plans.forEach(([stg, idx, taskList]) => {
            const target = byStage[stg]?.[idx];
            if (!target) return;
            const existing = target.tasks || [];
            const newTasks = taskList
              .filter(t => !existing.some(et => et.text === t.text))
              .map(t => ({
                id: uid(), text: t.text, dueDate: dayOffset(t.days),
                completed: !!t.completed,
                completedAt: t.completed ? new Date(today.getTime() - 86400000).toISOString() : null,
                completedBy: t.completed && t.source === "ai" ? "ai" : undefined,
                source: t.source, createdAt: new Date(today.getTime() - 86400000*2).toISOString(),
              }));
            if (newTasks.length) {
              const ti = opps.indexOf(target);
              opps[ti] = { ...target, tasks: [...existing, ...newTasks] };
              touched = true;
            }
          });
          // Add a couple of lead-side tasks too
          const leads = nLeads[brandId] || [];
          const leadByStage = {};
          leads.forEach(l => { (leadByStage[l.stage] = leadByStage[l.stage]||[]).push(l); });
          const leadPlans = [
            ["contacted", 0, [{ text:"Retry call — went to voicemail", days:0, source:"ai" }]],
            ["nurturing", 0, [{ text:"Send quarterly check-in note", days:0, source:"manual" }]],
            ["new_lead",  0, [{ text:"Initial qualification call", days:0, source:"ai" }]],
            ["new_lead",  1, [{ text:"Send brand overview deck", days:1, source:"ai" }]],
          ];
          leadPlans.forEach(([stg, idx, taskList]) => {
            const target = leadByStage[stg]?.[idx];
            if (!target) return;
            const existing = target.tasks || [];
            const newTasks = taskList
              .filter(t => !existing.some(et => et.text === t.text))
              .map(t => ({ id: uid(), text: t.text, dueDate: dayOffset(t.days), completed: !!t.completed, source: t.source, createdAt: new Date(today.getTime() - 86400000*2).toISOString() }));
            if (newTasks.length) {
              const li = leads.indexOf(target);
              leads[li] = { ...target, tasks: [...existing, ...newTasks] };
              touched = true;
            }
          });
          nLeads[brandId] = leads;
        });
        if (touched) {
          leadsMap = nLeads; oppsMap = nOpps;
          await Promise.all([S.set("ff4_leads", leadsMap), S.set("ff4_opps", oppsMap)]);
        }
        await S.set("ff4_schemav", 12);
      }
      if (schemaV < 13) {
        // v13: v12 stored dueDate as UTC ISO YYYY-MM-DD which drifts a day vs the user's
        // local clock once the TZ is west of UTC. Wipe + re-seed all tutorial tasks using
        // local-time YMD so "today" lines up everywhere. Idempotent: detects tutorial brand,
        // removes tasks on every record, then re-runs the v12 plan with the corrected ymd.
        const tz_ymd = (d) => `${d.getFullYear()}-${String(d.getMonth()+1).padStart(2,"0")}-${String(d.getDate()).padStart(2,"0")}`;
        const now2 = new Date();
        const dayOffsetTz = (n) => tz_ymd(new Date(now2.getTime() + n*86400000));
        const nLeads2 = {...(leadsMap||{})}, nOpps2 = {...(oppsMap||{})};
        let touched2 = false;
        Object.keys(nOpps2).forEach(brandId => {
          const opps = nOpps2[brandId] || [];
          const isTutorial = opps.some(o => /Maya Chen \(Tutorial\)/.test(`${o.firstName||""} ${o.lastName||""}`));
          if (!isTutorial) return;
          // Wipe ALL tasks on tutorial records (they were all seeded by v12)
          nOpps2[brandId] = opps.map(o => o.tasks ? { ...o, tasks: [] } : o);
          const leads = (nLeads2[brandId] || []);
          nLeads2[brandId] = leads.map(l => l.tasks ? { ...l, tasks: [] } : l);
          // Re-seed the plans (same content as v12 but local-time dates)
          const oppsFresh = nOpps2[brandId];
          const byStage = {};
          oppsFresh.forEach(o => { (byStage[o.stage] = byStage[o.stage]||[]).push(o); });
          const plans = [
            ["agreement_sent", 0, [{ text:"Confirm signature ETA", days:0, source:"manual" }, { text:"Send onboarding packet", days:2, source:"ai" }]],
            ["agreement_sent", 1, [{ text:"Loop in legal for final review", days:0, source:"ai" }]],
            ["discovery_day",  0, [{ text:"Send Discovery Day agenda", days:0, source:"ai" }, { text:"Book travel confirmation", days:1, source:"manual" }]],
            ["discovery_day",  1, [{ text:"Follow up on hotel block", days:-2, source:"ai" }]],
            ["validation",     0, [{ text:"Schedule validation call w/ existing operator", days:0, source:"ai" }]],
            ["validation",     1, [{ text:"Share validation contact list", days:-1, source:"manual" }]],
            ["fdd_review_call",0, [{ text:"Send FDD review agenda", days:0, source:"ai" }, { text:"Confirm time zone for call", days:3, source:"ai" }]],
            ["fdd_signed",     0, [{ text:"Move to validation stage prep", days:1, source:"manual" }]],
            ["fdd_sent",       0, [{ text:"Retry call — no response in 5d", days:0, source:"ai" }]],
            ["fdd_sent",       1, [{ text:"Share franchise team intro deck", days:5, source:"manual" }]],
            ["intro_call",     0, [{ text:"Send intro deck", days:-3, source:"ai" }]],
            ["intro_call",     1, [{ text:"Schedule follow-up qualifying call", days:2, source:"ai" }]],
            ["qualified",      0, [{ text:"Send territory map", days:0, source:"manual" }]],
            ["qualified",      1, [{ text:"Confirm financing readiness", days:4, source:"ai" }]],
            ["application",    0, [{ text:"Review submitted application", days:0, source:"ai", completed:true }]],
            ["ceo_qa",         0, [{ text:"Prep CEO with candidate background", days:1, source:"ai" }]],
            ["intake_form",    0, [{ text:"Chase missing intake fields", days:0, source:"ai" }]],
          ];
          plans.forEach(([stg, idx, taskList]) => {
            const target = byStage[stg]?.[idx];
            if (!target) return;
            const newTasks = taskList.map(t => ({
              id: uid(), text: t.text, dueDate: dayOffsetTz(t.days),
              completed: !!t.completed,
              completedAt: t.completed ? new Date(now2.getTime() - 86400000).toISOString() : null,
              completedBy: t.completed && t.source === "ai" ? "ai" : undefined,
              source: t.source, createdAt: new Date(now2.getTime() - 86400000*2).toISOString(),
            }));
            const ti = oppsFresh.indexOf(target);
            oppsFresh[ti] = { ...target, tasks: newTasks };
            touched2 = true;
          });
          const leadsFresh = nLeads2[brandId];
          const leadByStage = {};
          leadsFresh.forEach(l => { (leadByStage[l.stage] = leadByStage[l.stage]||[]).push(l); });
          const leadPlans = [
            ["contacted", 0, [{ text:"Retry call — went to voicemail", days:0, source:"ai" }]],
            ["nurturing", 0, [{ text:"Send quarterly check-in note", days:0, source:"manual" }]],
            ["new_lead",  0, [{ text:"Initial qualification call", days:0, source:"ai" }]],
            ["new_lead",  1, [{ text:"Send brand overview deck", days:1, source:"ai" }]],
          ];
          leadPlans.forEach(([stg, idx, taskList]) => {
            const target = leadByStage[stg]?.[idx];
            if (!target) return;
            const newTasks = taskList.map(t => ({ id: uid(), text: t.text, dueDate: dayOffsetTz(t.days), completed: !!t.completed, source: t.source, createdAt: new Date(now2.getTime() - 86400000*2).toISOString() }));
            const li = leadsFresh.indexOf(target);
            leadsFresh[li] = { ...target, tasks: newTasks };
            touched2 = true;
          });
        });
        if (touched2) {
          leadsMap = nLeads2; oppsMap = nOpps2;
          await Promise.all([S.set("ff4_leads", leadsMap), S.set("ff4_opps", oppsMap)]);
        }
        await S.set("ff4_schemav", 13);
      }
      if (schemaV < 14) {
        // v14: remove every `promised_material` notification — the scan that produced them
        // has been retired in favor of AI-extracted tasks (which cover the same intent without
        // spamming the notification list once per record per day).
        const filtered = (notifs||[]).filter(n => n.type !== "promised_material");
        if (filtered.length !== (notifs||[]).length) {
          notifs = filtered;
          await S.set("ff4_notifications", notifs);
        }
        await S.set("ff4_schemav", 14);
      }
      if (schemaV < 15) {
        // v15: spread brokerIds across tutorial leads so the Broker Performance LEADS column
        // has real signal. Deterministic round-robin across the broker roster — every 2nd lead
        // on a tutorial brand gets attributed to a broker.
        const nLeads3 = {...(leadsMap||{})};
        let touched3 = false;
        Object.keys(nLeads3).forEach(brandId => {
          const leads = nLeads3[brandId] || [];
          const agents = (brk?.[brandId]?.agents || []);
          const isTutorial = leads.some(l => /\(Tutorial\)/.test(`${l.firstName||""} ${l.lastName||""}`));
          if (!isTutorial || !agents.length) return;
          const updated = leads.map((l, idx) => {
            if (l.brokerId) return l;
            if (idx % 2 !== 0) return l;
            return { ...l, brokerId: agents[idx % agents.length].id };
          });
          if (updated.some((l, i) => l !== leads[i])) {
            nLeads3[brandId] = updated;
            touched3 = true;
          }
        });
        if (touched3) {
          leadsMap = nLeads3;
          await S.set("ff4_leads", leadsMap);
        }
        await S.set("ff4_schemav", 15);
      }
      if (schemaV < 16) {
        // v16: seed default template folders (Marketing + Follow-up) per existing brand
        // into the new ff4_template_folders store. Folders become fully editable per-brand
        // (rename / delete / nest / scope toggle). Defaults are seeded with isDefault:true
        // so the UI can mark them but the user can still edit or remove them.
        const foldersNext = {...(foldersLoaded||{})};
        let touchedFolders = false;
        (bl||[]).forEach(brand => {
          if (!foldersNext[brand.id] || !foldersNext[brand.id].length) {
            foldersNext[brand.id] = TEMPLATE_FOLDERS.map(f => ({
              id: f.id, name: f.label, icon: f.icon, color: f.color,
              parentId: null, scope: "private", isDefault: true, createdAt: nowIso()
            }));
            touchedFolders = true;
          }
        });
        if (touchedFolders) {
          await S.set("ff4_template_folders", foldersNext);
          foldersLoaded = foldersNext;
        }
        await S.set("ff4_schemav", 16);
      }
      if (schemaV < 17) {
        // v17: split legacy `investmentLevel` into `netWorth` and `liquidity`.
        // For existing records, copy `investmentLevel` → `netWorth` so historical data still
        // surfaces in the new tile. Liquidity stays blank; reps backfill as they re-touch records.
        const migrate = (rec) => (!rec.investmentLevel || rec.netWorth || rec.liquidity) ? rec : { ...rec, netWorth: rec.investmentLevel };
        const lNext = {...(leadsMap||{})};
        const oNext = {...(oppsMap||{})};
        let touchedLeads17 = false, touchedOpps17 = false;
        Object.keys(lNext).forEach(bid => { const before = lNext[bid]||[]; const after = before.map(migrate); if (after.some((r,i)=>r!==before[i])) { lNext[bid]=after; touchedLeads17 = true; } });
        Object.keys(oNext).forEach(bid => { const before = oNext[bid]||[]; const after = before.map(migrate); if (after.some((r,i)=>r!==before[i])) { oNext[bid]=after; touchedOpps17 = true; } });
        if (touchedLeads17) { leadsMap = lNext; await S.set("ff4_leads", leadsMap); }
        if (touchedOpps17)  { oppsMap  = oNext; await S.set("ff4_opps",  oppsMap);  }
        await S.set("ff4_schemav", 17);
      }

      // First-run seed: sample brand + leads + opps + brokers + comms.
      // Notes are populated with realistic content (including some "promised but not delivered" items
      // and a clear disqualify candidate) so AI Organize and AI Scoring have meaty data to analyze.
      // Every fake record carries a "(Tutorial)" last-name suffix.
      if (!seeded && bl.length===0) {
        const now = nowIso();
        const daysAgo = (d) => new Date(Date.now() - d*86400000).toISOString();
        const brandId = uid();
        const sampleBrand = { id:brandId, name:"Best Brand", industry:"Quick Service Restaurant", website:"https://bestbrand.example", emoji:"⭐", color:"#f59e0b", createdAt:now };
        bl = [sampleBrand];
        lStagesMap = { [brandId]: DEFAULT_LEAD_STAGES };
        oStagesMap = { [brandId]: DEFAULT_OPP_STAGES };

        // Pre-generate IDs for brokers and opps that need cross-referencing
        const sarahId = uid(), davidId = uid(), aishaId = uid();
        const networkId = uid();
        const mayaOppId = uid(), nadiaOppId = uid();
        // Pre-generate the rest so territory assignees can reference them
        const jordanLeadId = uid(), priyaLeadId = uid(), marcusLeadId = uid();
        const elenaOppId = uid(), bradOppId = uid(), lindaOppId = uid(), carlosOppId = uid(),
              rachelOppId = uid(), tomOppId = uid(), gregOppId = uid();

        // Helper for building records with backdated stage entry
        const mkRec = (over) => ({ id:uid(), createdAt:over.createdAt||now, updatedAt:now, stageEnteredAt:over.stageEnteredAt||now, notes:[], activities:[], partners:[], ...over });
        const mkOpp = (over) => ({ ...mkRec(over), originatedAsLead: true, syntheticLeadOrigin: true, leadOriginAt: over.createdAt || now });
        // Pre-classify so the call-tracker badge + touchpoint indicator are accurate on fresh seed
        // (schema migrations have already run by the time the seed block executes, so the v5 backfill won't catch these).
        const note = (daysBack, text) => ({ id:uid(), text, at: daysAgo(daysBack), isTouchpoint: heuristicClassifyTouchpoint(text), isCallAttempt: heuristicClassifyCall(text) });

        leadsMap = { [brandId]: [
          mkRec({ id: jordanLeadId, firstName:"Jordan", lastName:"Reyes (Tutorial)", email:"jordan.reyes@example.com", phone:"512-555-0142", company:"Reyes Holdings", location:"Austin, TX", territory:"Austin TX", investmentLevel:"$250k–$500k", source:"Website", stage:"new_lead",
            createdAt: daysAgo(2), stageEnteredAt: daysAgo(2),
            notes:[ note(2, "Came in through the website form. Real estate background — owns 4 rental properties. Marked the $250k–$500k box. Need to schedule a first call this week.") ]
          }),
          mkRec({ id: priyaLeadId, firstName:"Priya", lastName:"Shah (Tutorial)", email:"priya.shah@example.com", phone:"617-555-0188", company:"Shah Ventures", location:"Boston, MA", territory:"Boston Metro", investmentLevel:"$500k–$1M", source:"Referral", stage:"contacted",
            createdAt: daysAgo(10), stageEnteredAt: daysAgo(7),
            notes:[
              note(8, "Had a great 20-min intro call. CFO background, looking to open 2 units in year 1."),
              note(5, "Priya asked for a list of existing Boston-area franchisees she could chat with informally. Promised to send by end of week."),
            ]
          }),
          mkRec({ id: marcusLeadId, firstName:"Marcus", lastName:"Okafor (Tutorial)", email:"marcus.okafor@example.com", phone:"305-555-0173", company:"Okafor Group", location:"Miami, FL", territory:"South Florida", investmentLevel:"$1M+", source:"Trade Show", stage:"nurturing",
            createdAt: daysAgo(45), stageEnteredAt: daysAgo(30),
            notes:[
              note(45, "Met at the Black Franchise Forum. $2M+ in liquid capital. Looking for a brand with a multi-unit track record."),
              note(10, "Quarterly check-in. Still evaluating 2–3 other concepts. Not ready to commit but staying engaged."),
            ]
          }),
        ]};

        oppsMap = { [brandId]: [
          // 1. QUALIFIED (3d in stage — fresh)
          mkOpp({ id: elenaOppId, firstName:"Elena", lastName:"Vasquez (Tutorial)", email:"elena.vasquez@example.com", phone:"206-555-0119", company:"Vasquez Enterprises", location:"Seattle, WA", territory:"Pacific Northwest", investmentLevel:"$500k–$1M", source:"LinkedIn", stage:"qualified", assignedTo:"You",
            createdAt: daysAgo(7), stageEnteredAt: daysAgo(3),
            notes:[
              note(7, "Connected via LinkedIn. 12-year restaurant ops background, currently owns 2 units of another QSR brand."),
              note(3, "Qualified on intro call. Strong financials, ready to move fast. Wants to open 1 unit in year 1, 3 by year 3."),
              note(2, "Elena asked for an investment range breakdown by Pacific NW territory before her next call. I told her I'd put one together."),
            ],
            activities:[{ id:uid(), type:"stage", text:"Moved to Qualified Lead", at: daysAgo(3) }]
          }),
          // 2. INTRO_CALL (9d in stage — STALE @ 7d threshold)
          mkOpp({ id: bradOppId, firstName:"Brad", lastName:"Henderson (Tutorial)", email:"brad.henderson@example.com", phone:"404-555-0162", company:"Henderson Auto Group", location:"Atlanta, GA", territory:"Greater Atlanta", investmentLevel:"$250k–$500k", source:"Website", stage:"intro_call", assignedTo:"You",
            createdAt: daysAgo(10), stageEnteredAt: daysAgo(9),
            notes:[
              note(10, "Brad inquired via the website. Owns a successful auto repair shop, looking for a less hands-on second business."),
              note(9, "30-min intro call. Liked the concept but said he wants to discuss with his wife before next steps. Said he'd circle back this week."),
            ]
          }),
          // 3. FDD_SENT (8d in stage — STALE @ 7d) + promised Item 19 follow-up never sent
          mkOpp({ id: lindaOppId, firstName:"Linda", lastName:"Park (Tutorial)", email:"linda.park@example.com", phone:"415-555-0193", company:"Park Capital", location:"San Francisco, CA", territory:"Bay Area", investmentLevel:"$500k–$1M", source:"LinkedIn", stage:"fdd_sent", assignedTo:"You",
            createdAt: daysAgo(14), stageEnteredAt: daysAgo(8),
            notes:[
              note(14, "Connected on LinkedIn. CFO at a Series-B SaaS company, looking for a stable cash-flow side business."),
              note(10, "Great intro call. Asked detailed questions about Item 19 unit economics. Promised to send her our supplemental Item 19 breakdown deck by EOW."),
              note(8, "FDD sent via DocuSign. Waiting on review."),
            ]
          }),
          // 4. FDD_SIGNED (3d — fresh, engaged candidate)
          mkOpp({ firstName:"Carlos", lastName:"Mendoza (Tutorial)", email:"carlos.mendoza@example.com", phone:"713-555-0148", company:"Mendoza Holdings LLC", location:"Houston, TX", territory:"Houston Metro", investmentLevel:"$500k–$1M", source:"Trade Show", stage:"fdd_signed", assignedTo:"You",
            createdAt: daysAgo(12), stageEnteredAt: daysAgo(3),
            notes:[
              note(12, "Met at IFA Show. Owns 3 unrelated franchises, deep operator experience."),
              note(6, "FDD sent."),
              note(3, "FDD signed within 48 hours. Very engaged. Scheduled FDD review call for Wednesday."),
            ]
          }),
          // 5. VALIDATION (11d — STALE @ 10d) — risky signals in notes
          mkOpp({ firstName:"Rachel", lastName:"Kim (Tutorial)", email:"rachel.kim@example.com", phone:"619-555-0156", company:"Kim Wellness Group", location:"San Diego, CA", territory:"Southern California", investmentLevel:"$1M+", source:"Referral", stage:"validation", assignedTo:"You",
            createdAt: daysAgo(35), stageEnteredAt: daysAgo(11),
            notes:[
              note(35, "Referred by an existing franchisee. Strong financial profile, $1.5M liquid."),
              note(14, "Got through FDD review and submitted application."),
              note(11, "Started validation calls. Reached 3 of 5 franchisees on her list."),
              note(5,  "Two of three validation calls came back lukewarm — operators complaining about supply-chain headaches and labor costs. Rachel sounded concerned but said she'd think it over. Haven't heard from her since."),
            ]
          }),
          // 6. APPLICATION (6d — STALE @ 5d) + weak financials + promised PFS template not sent
          mkOpp({ firstName:"Tom", lastName:"Wright (Tutorial)", email:"tom.wright@example.com", phone:"503-555-0184", company:"Wright Construction", location:"Portland, OR", territory:"Pacific Northwest", investmentLevel:"$250k–$500k", source:"Website", stage:"application", assignedTo:"You",
            createdAt: daysAgo(28), stageEnteredAt: daysAgo(6),
            notes:[
              note(28, "Application started but Tom keeps missing follow-up calls."),
              note(6,  "Application finally submitted. Looks thin on liquid capital — only $80k cash on a $400k startup. May be a stretch."),
              note(6,  "Promised to send him a Personal Financial Statement template so he can detail his net worth properly. Need to do that."),
            ]
          }),
          // 7. INTAKE_FORM (6d — STALE @ 5d) — CLEAR DISQUALIFY candidate
          mkOpp({ firstName:"Greg", lastName:"Foster (Tutorial)", email:"greg.foster@example.com", phone:"716-555-0178", company:"Foster Investments", location:"Buffalo, NY", territory:"Western NY", investmentLevel:"$250k–$500k", source:"Trade Show", stage:"intake_form", assignedTo:"You",
            createdAt: daysAgo(22), stageEnteredAt: daysAgo(6),
            notes:[
              note(22, "Met at IFA Show. Claimed he had cash ready to deploy."),
              note(14, "Going through application. Asked the same questions about royalties three separate times — clearly not retaining the info."),
              note(9,  "Background check came back. Credit score: 580. Our minimum is 680."),
              note(6,  "Pushed Greg to provide updated financials. He's been evasive. He keeps pushing to stay in the pipeline despite the credit issue."),
            ]
          }),
          // 8. DISCOVERY_DAY (4d — fresh, ready to convert)
          mkOpp({ id: nadiaOppId, firstName:"Nadia", lastName:"Hassan (Tutorial)", email:"nadia.hassan@example.com", phone:"480-555-0241", company:"Hassan Hospitality", location:"Phoenix, AZ", territory:"Phoenix Metro", investmentLevel:"$500k–$1M", source:"Referral", stage:"discovery_day", assignedTo:"You", brokerId: aishaId,
            createdAt: daysAgo(25), stageEnteredAt: daysAgo(4),
            notes:[
              note(25, "Referred by Aisha Patel (broker, FranNet). Hospitality background — owns a 28-room boutique hotel."),
              note(12, "Solid intro call. Asked about multi-unit development from the jump."),
              note(8,  "FDD review call went very well. Mentioned she might bring a co-investor."),
              note(4,  "Confirmed for Discovery Day on the 28th. Bringing her husband and her business partner."),
            ]
          }),
          // 9. AGREEMENT_SENT (8d — STALE @ 7d) — promised territory map + brand guidelines never sent
          mkOpp({ id: mayaOppId, firstName:"Maya", lastName:"Chen (Tutorial)", email:"maya.chen@example.com", phone:"312-555-0223", company:"Chen Group LLC", location:"Chicago, IL", territory:"Greater Chicago", investmentLevel:"$1M+", source:"LinkedIn", stage:"agreement_sent", assignedTo:"You", brokerId: sarahId,
            createdAt: daysAgo(35), stageEnteredAt: daysAgo(8),
            notes:[
              note(35, "Sarah Chen (broker) brought her in. Real estate developer, ran multi-unit Subway franchises a decade ago."),
              note(20, "Strong throughout the process — qualified, FDD signed quickly, all validation calls positive."),
              note(15, "Attended Discovery Day with her ops manager. Very impressed."),
              note(8,  "Agreement sent for signature."),
              note(8,  "Maya also asked for the territory exclusivity map and our brand guidelines PDF for her marketing planning. I haven't sent either yet."),
            ]
          }),
          // 10. AGREEMENT_SIGNED — closed-won, drives a non-zero close rate
          mkOpp({ firstName:"Devon", lastName:"Mitchell (Tutorial)", email:"devon.mitchell@example.com", phone:"704-555-0188", company:"Mitchell Hospitality Group", location:"Charlotte, NC", territory:"Charlotte Metro", investmentLevel:"$500k–$1M", source:"Referral", stage:"agreement_signed", assignedTo:"You", brokerId: davidId,
            createdAt: daysAgo(62), stageEnteredAt: daysAgo(3),
            notes:[
              note(62, "Referred by David Thompson. Owns three quick-serve concepts in Charlotte already — knows what he's doing."),
              note(48, "FDD signed within a week. Validation calls all glowing — peers love him."),
              note(20, "Discovery Day was a slam dunk. Devon was clearly ready to commit."),
              note(10, "Agreement sent."),
              note(3,  "🎉 Agreement signed! Devon is officially in. Award call scheduled, opening timeline being drafted."),
            ]
          }),
        ]};

        const brokerData = {
          networks: [
            { id:networkId, name:"FranNet (Tutorial)", website:"https://frannet.example", phone:"800-555-0100", email:"contact@frannet.example", color:"#a78bfa", notes:"National network of franchise consultants — placeholder data for the tutorial.", createdAt:now },
          ],
          agents: [
            { id:sarahId, firstName:"Sarah", lastName:"Chen (Tutorial)",     email:"sarah.chen@frannet.example",    phone:"212-555-0231", networkId, specialty:"QSR & food service",     commission:"50% of franchise fee", notes:"15+ years placing QSR concepts. Strong in Northeast. Brought us Maya Chen — one of our hottest candidates this quarter.", createdAt:now },
            { id:davidId, firstName:"David", lastName:"Thompson (Tutorial)", email:"david.thompson@frannet.example", phone:"312-555-0184", networkId, specialty:"Service & home-based",   commission:"$10k flat",            notes:"Focuses on lower-investment concepts. Midwest territory. Slower placement velocity but candidates are usually well-qualified.", createdAt:now },
            { id:aishaId, firstName:"Aisha", lastName:"Patel (Tutorial)",    email:"aisha.patel@frannet.example",   phone:"415-555-0277", networkId, specialty:"Fitness & wellness",     commission:"40% of franchise fee", notes:"West Coast specialist, fitness/health brands. Recently sent Nadia Hassan our way — looks like a winner.", createdAt:now },
          ],
        };
        const brokersMapToSave = { [brandId]: brokerData };

        // Seed broker communications scoped to specific opps so the Broker Hub Communications tab isn't empty.
        const seededComms = {
          [mayaOppId]: [
            { id:uid(), brokerId:sarahId, type:"call",  direction:"outbound", body:"15-min call w/ Sarah to debrief on Maya. Sarah confirms Maya is committed — just busy closing on a building. Suggested I follow up with her on Friday.", at: daysAgo(12) },
            { id:uid(), brokerId:sarahId, type:"email", direction:"inbound",  subject:"Re: Chen agreement", body:"Hey — has Maya gotten the agreement back to you yet? She told me she's planning to sign this week. Let me know if I should nudge her.", at: daysAgo(3) },
          ],
          [nadiaOppId]: [
            { id:uid(), brokerId:aishaId, type:"email", direction:"outbound", subject:"Discovery Day plans for Nadia", body:"Aisha — Nadia is confirmed for Discovery Day on the 28th. She's bringing her husband and business partner. Anything we should know in advance?", at: daysAgo(7) },
            { id:uid(), brokerId:aishaId, type:"note",  direction:"outbound", body:"Aisha mentioned Nadia has been tire-kicking 2 other concepts but is leaning heavily toward us. Worth pushing for fast close after Discovery Day.", at: daysAgo(5) },
          ],
        };

        // Seed 10 sample territories mixing polygons, multi-zips, and circles,
        // with assignees linked to the seeded leads/opps so the feature shows up live on first run.
        const territoriesToSeed = [
          // 1) Circle around Austin — 25 mi
          { id: uid(), label: "Austin TX Metro (Tutorial)", kind: "circle", color: "#3b82f6", opacity: 0.35,
            center: [30.2672, -97.7431], radiusMeters: 40234,
            assignees: [{type:"lead", id: jordanLeadId, name: "Jordan Reyes (Tutorial)"}], createdAt: now },
          // 2) Polygon around Boston metro
          { id: uid(), label: "Boston Metro (Tutorial)", kind: "polygon", color: "#a78bfa", opacity: 0.35,
            latlngs: [[42.20,-71.30],[42.20,-70.85],[42.50,-70.85],[42.50,-71.30]],
            assignees: [{type:"lead", id: priyaLeadId, name: "Priya Shah (Tutorial)"}], createdAt: now },
          // 3) Polygon around Miami-Dade
          { id: uid(), label: "South Florida (Tutorial)", kind: "polygon", color: "#f97316", opacity: 0.35,
            latlngs: [[25.50,-80.45],[25.50,-80.10],[26.05,-80.10],[26.05,-80.45]],
            assignees: [{type:"lead", id: marcusLeadId, name: "Marcus Okafor (Tutorial)"}], createdAt: now },
          // 4) Multi-ZIP around Manhattan
          { id: uid(), label: "Manhattan Cluster (Tutorial)", kind: "multipolygon", color: "#ec4899", opacity: 0.35,
            components: [
              { name: "10001", latlngs: [[40.745,-74.005],[40.745,-73.985],[40.760,-73.985],[40.760,-74.005]] },
              { name: "10002", latlngs: [[40.710,-74.000],[40.710,-73.975],[40.725,-73.975],[40.725,-74.000]] },
              { name: "10003", latlngs: [[40.725,-74.000],[40.725,-73.980],[40.740,-73.980],[40.740,-74.000]] },
            ],
            assignees: [], createdAt: now },
          // 5) Polygon around the SF Bay Area
          { id: uid(), label: "Bay Area (Tutorial)", kind: "polygon", color: "#10b981", opacity: 0.35,
            latlngs: [[37.40,-122.55],[37.40,-122.10],[37.90,-122.10],[37.90,-122.55]],
            assignees: [{type:"opp", id: lindaOppId, name: "Linda Park (Tutorial)"}], createdAt: now },
          // 6) Circle around Seattle — 30 mi
          { id: uid(), label: "Seattle Metro (Tutorial)", kind: "circle", color: "#0ea5e9", opacity: 0.35,
            center: [47.6062, -122.3321], radiusMeters: 48280,
            assignees: [{type:"opp", id: elenaOppId, name: "Elena Vasquez (Tutorial)"}], createdAt: now },
          // 7) Multi-ZIP around Chicago Loop
          { id: uid(), label: "Chicago Loop (Tutorial)", kind: "multipolygon", color: "#facc15", opacity: 0.35,
            components: [
              { name: "60601", latlngs: [[41.882,-87.625],[41.882,-87.615],[41.892,-87.615],[41.892,-87.625]] },
              { name: "60602", latlngs: [[41.880,-87.640],[41.880,-87.625],[41.890,-87.625],[41.890,-87.640]] },
              { name: "60604", latlngs: [[41.876,-87.640],[41.876,-87.625],[41.882,-87.625],[41.882,-87.640]] },
            ],
            assignees: [{type:"opp", id: mayaOppId, name: "Maya Chen (Tutorial)"}], createdAt: now },
          // 8) Polygon around Phoenix Valley
          { id: uid(), label: "Phoenix Valley (Tutorial)", kind: "polygon", color: "#f59e0b", opacity: 0.35,
            latlngs: [[33.20,-112.40],[33.20,-111.70],[33.70,-111.70],[33.70,-112.40]],
            assignees: [{type:"opp", id: nadiaOppId, name: "Nadia Hassan (Tutorial)"}], createdAt: now },
          // 9) Circle around Atlanta — 35 mi
          { id: uid(), label: "Greater Atlanta (Tutorial)", kind: "circle", color: "#dc2626", opacity: 0.35,
            center: [33.7490, -84.3880], radiusMeters: 56327,
            assignees: [{type:"opp", id: bradOppId, name: "Brad Henderson (Tutorial)"}], createdAt: now },
          // 10) Polygon around Denver metro
          { id: uid(), label: "Denver Metro (Tutorial)", kind: "polygon", color: "#8b5cf6", opacity: 0.35,
            latlngs: [[39.50,-105.20],[39.50,-104.70],[39.90,-104.70],[39.90,-105.20]],
            assignees: [], createdAt: now },
        ];
        const territoriesMapToSave = { [brandId]: territoriesToSeed };

        // Seed scheduling defaults: 6 event types + base availability + empty bookings.
        // Schema v6 also seeds these via migration, but baking them here means fresh installs
        // get them without needing the migration to run after seed.
        const seededEventTypes = { [brandId]: DEFAULT_EVENT_TYPES.map(et => ({
          ...et, id: uid(), isDefault: true, active: true, bufferBeforeMin: 0, bufferAfterMin: 0, createdAt: now,
        })) };
        const seededBookings   = { [brandId]: [] };
        const seededAvailability = { [brandId]: JSON.parse(JSON.stringify(DEFAULT_AVAILABILITY)) };
        const seededFolders = { [brandId]: TEMPLATE_FOLDERS.map(f => ({
          id: f.id, name: f.label, icon: f.icon, color: f.color,
          parentId: null, scope: "private", isDefault: true, createdAt: now
        })) };

        active = brandId;
        await Promise.all([
          S.set("ff4_brands", bl),
          S.set("ff4_leads", leadsMap),
          S.set("ff4_opps", oppsMap),
          S.set("ff4_lstages", lStagesMap),
          S.set("ff4_ostages", oStagesMap),
          S.set("ff4_brokers", brokersMapToSave),
          S.set("ff4_bcomms", seededComms),
          S.set("ff4_terr", territoriesMapToSave),
          S.set("ff4_event_types", seededEventTypes),
          S.set("ff4_bookings", seededBookings),
          S.set("ff4_availability", seededAvailability),
          S.set("ff4_template_folders", seededFolders),
          S.set("ff4_activeBrand", active),
          S.set("ff4_seeded", true),
        ]);
        // Make the seeded brokers + comms + territories + scheduling available immediately on first render
        brk = brokersMapToSave;
        bc = seededComms;
        t = territoriesMapToSave;
        evTypesLoaded = seededEventTypes;
        bookingsLoaded = seededBookings;
        availLoaded = seededAvailability;
        foldersLoaded = seededFolders;
      }

      setBrands(bl); setLeads(leadsMap); setOpps(oppsMap); setTerritories(t||{});
      setLeadStages(lStagesMap); setOppStages(oStagesMap);
      setScores(sc||{}); setScoreFeedback(sf||{});
      setCustomTemplates(ct||{}); setAutomations(autos||{});
      setBrokers(brk||{}); setBrokerComms(bc||{});
      setEventTypes(evTypesLoaded||{}); setBookings(bookingsLoaded||{}); setAvailability(availLoaded||{});
      setDupLeads(dupLoaded||{});
      setReports(reportsLoaded||{});
      setTemplateFolders(foldersLoaded||{});
      setDiscoveryDays(ddaysLoaded||{});
      setFranchisees(franchiseesLoaded||{});
      let notifsList = Array.isArray(notifs) ? notifs : [];
      // Schema v4 — first time bumping creates an "App updated" notification so the user sees the feature work.
      if ((await S.get("ff4_schemav") || 0) < 4) {
        notifsList = [{ id: uid(), type: "app_updated", category: "system", severity: "normal", isBanner: false,
          title: `${BRAND.name} updated to v5-dev — Notifications tab is now live.`, data: {version:"5-dev"},
          recordRef: null, dedupeKey: "app_updated_v5-dev", createdAt: nowIso(), readAt: null, bannerDismissedAt: null },
          ...notifsList];
        await S.set("ff4_schemav", 4);
        await S.set("ff4_notifications", notifsList);
      }
      setNotifications(notifsList);
      const mergedSettings = {...DEFAULT_SETTINGS, ...(settingsLoaded||{})};
      setSettings(mergedSettings);
      if (mergedSettings.sidebarDefault === "collapsed") setSidebarOpen(false);
      if (mergedSettings.defaultView === "kanban")       setSubView("kanban");
      const firstId=bl[0]?.id||null;
      setActiveBrand(active||firstId);
      setLoading(false);
    });
  },[]);

  const p = useCallback(async(key,setter,val)=>{ setter(val); await S.set(key,val); },[]);
  const toast$ = (msg,type="ok") => { if (settings && settings.inAppToasts === false) return; setToast({msg,type}); const dur = { short:1800, medium:3200, long:5500 }[settings?.toastDuration||"medium"] || 3200; setTimeout(()=>setToast(null), dur); };

  // ── Data management helpers ───────────────────────────────
  const exportAllData = () => {
    const data = {};
    Object.keys(localStorage).filter(k => k.startsWith("ff4_") || k === "ff_docusign_key" || k === "ff_api_key").forEach(k => { data[k] = localStorage.getItem(k); });
    const blob = new Blob([JSON.stringify(data, null, 2)], {type:"application/json"});
    const url = URL.createObjectURL(blob);
    const a = document.createElement("a");
    a.href = url; a.download = `${BRAND.nameLower}-export-${new Date().toISOString().slice(0,10)}.json`;
    document.body.appendChild(a); a.click(); a.remove();
    URL.revokeObjectURL(url);
    toast$("Data exported!");
    pushNotification("export_ready", {});
  };
  const importAllData = (file) => {
    const reader = new FileReader();
    reader.onload = (e) => {
      try {
        const data = JSON.parse(e.target.result);
        Object.entries(data).forEach(([k, v]) => { if (typeof v === "string") localStorage.setItem(k, v); });
        toast$("Imported — reloading…");
        setTimeout(()=>location.reload(), 800);
      } catch { toast$("Import failed: invalid file.", "err"); }
    };
    reader.readAsText(file);
  };
  const resetToDemoData = () => {
    if (!confirm("⚠️ This will delete your current data (brands, leads, opportunities, brokers, templates, automations, territories) and restore the tutorial pipeline. This cannot be undone. Continue?")) return;
    if (!confirm("Are you absolutely sure? Your current data will be replaced with the tutorial dataset. This is your last chance to cancel.")) return;
    Object.keys(localStorage).filter(k => k.startsWith("ff4_")).forEach(k => localStorage.removeItem(k));
    location.reload();
  };
  const wipeAllData = () => {
    if (!confirm("⚠️ This will permanently delete ALL data (brands, leads, opportunities, brokers, templates, settings, API keys). This cannot be undone. Continue?")) return;
    if (!confirm("Are you absolutely sure? This is your last chance to cancel.")) return;
    Object.keys(localStorage).filter(k => k.startsWith("ff4_") || k === "ff_docusign_key" || k === "ff_api_key").forEach(k => localStorage.removeItem(k));
    location.reload();
  };

  // ── Brand helpers ─────────────────────────────────────────
  const brand    = brands.find(b=>b.id===activeBrand)||null;
  const bLeads   = (activeBrand?leads[activeBrand]  :[])||[];
  const bOpps    = (activeBrand?opps[activeBrand]   :[])||[];
  const bDupLeads= (activeBrand?dupLeads[activeBrand]:[])||[];
  const bReports = (activeBrand?reports[activeBrand]:[])||[];
  const bTerr    = (activeBrand?territories[activeBrand]:[])||[];
  const bLStages = (activeBrand?leadStages[activeBrand]:DEFAULT_LEAD_STAGES)||DEFAULT_LEAD_STAGES;
  const bOStages = (activeBrand?oppStages[activeBrand] :DEFAULT_OPP_STAGES)||DEFAULT_OPP_STAGES;
  const bTemplates = (activeBrand?customTemplates[activeBrand]:[])||[];
  const bFolders   = (activeBrand?templateFolders[activeBrand]:[])||[];
  const bDdays     = (activeBrand?discoveryDays[activeBrand]:[])||[];
  const bFranchisees = (activeBrand?franchisees[activeBrand]:[])||[];
  const bAutos     = (activeBrand?automations[activeBrand]:[])||[];
  const bBrokerData= (activeBrand?brokers[activeBrand]:null)||{networks:[],agents:[]};
  const bEventTypes= (activeBrand?eventTypes[activeBrand]:[])||[];
  const bBookings  = (activeBrand?bookings[activeBrand]:[])||[];
  const bAvailability = (activeBrand?availability[activeBrand]:null)||DEFAULT_AVAILABILITY;
  const bNetworks  = bBrokerData.networks||[];
  const bBrokers   = bBrokerData.agents||[];

  const saveBrand = (br) => {
    const n = br.id ? brands.map(b=>b.id===br.id?br:b) : [...brands,{...br,id:uid(),createdAt:nowIso()}];
    p("ff4_brands",setBrands,n);
    if (!br.id) {
      const newId = n[n.length-1]?.id;
      if (newId) {
        p("ff4_lstages",setLeadStages,{...leadStages,[newId]:DEFAULT_LEAD_STAGES});
        p("ff4_ostages",setOppStages, {...oppStages, [newId]:DEFAULT_OPP_STAGES});
        setActiveBrand(newId); S.set("ff4_activeBrand",newId);
      }
    }
    setModal(null); toast$("Brand saved!");
  };
  const deleteBrand = (id) => {
    const n=brands.filter(b=>b.id!==id);
    p("ff4_brands",setBrands,n);
    const newA=n[0]?.id||null; setActiveBrand(newA); S.set("ff4_activeBrand",newA);
    setModal(null); toast$("Brand deleted.","err");
  };
  const switchBrand = (id) => { setActiveBrand(id); S.set("ff4_activeBrand",id); setSubView("list"); setSelected(null); };

  // ── Territory ─────────────────────────────────────────────
  const saveTerr = (t) => {
    if (!activeBrand) return;
    // Functional update — concurrent saves (e.g. bulk-creating multiple restricted states)
    // must each read the latest array, not the stale closure snapshot.
    setTerritories(prev => {
      const cur = (prev[activeBrand]||[]);
      const exists = cur.some(x => x.id === t.id);
      const nextBrandList = exists ? cur.map(x => x.id === t.id ? t : x) : [...cur, t];
      const next = {...prev, [activeBrand]: nextBrandList};
      S.set("ff4_terr", next);
      return next;
    });
  };
  const deleteTerr = (id) => {
    if (!activeBrand) return;
    const territory = bTerr.find(t => t.id === id);
    p("ff4_terr",setTerritories,{...territories,[activeBrand]:bTerr.filter(t=>t.id!==id)});
    // If the removed territory was restricted (off-limits / registration-pending / sold-out),
    // generate a "newly callable" report so the rep knows which leads were previously locked
    // out and can now be revisited (e.g. registration just completed in a state). Includes
    // every active lead/opp whose territory text matched the deleted territory's label.
    // Excludes disqualified leads + closed-lost opps — not worth revisiting.
    if (territory?.restricted && territory.label) {
      const needle = territory.label.toLowerCase().replace(/territory/gi, "").trim();
      const matches = (rec) => (rec.territory || "").toLowerCase().includes(needle);
      const affectedLeads = bLeads.filter(l => l.stage !== "disqualified" && matches(l)).map(l => ({
        id: l.id, type: "lead", name: `${l.firstName||""} ${l.lastName||""}`.trim() || "(unnamed)",
        territory: l.territory, stage: l.stage,
        stageLabel: (bLStages.find(s => s.id === l.stage)?.label) || l.stage,
        email: l.email, phone: l.phone, score: l.score || 0, source: l.source,
      }));
      const affectedOpps = bOpps.filter(o => o.stage !== "closed_lost" && matches(o)).map(o => ({
        id: o.id, type: "opp", name: `${o.firstName||""} ${o.lastName||""}`.trim() || "(unnamed)",
        territory: o.territory, stage: o.stage,
        stageLabel: (bOStages.find(s => s.id === o.stage)?.label) || o.stage,
        email: o.email, phone: o.phone, score: o.score || 0, source: o.source,
      }));
      const unlocked = [...affectedOpps, ...affectedLeads];
      const report = {
        id: uid(),
        type: "territory_unlock",
        title: `🔓 Territory Unlocked · ${territory.label}`,
        generatedAt: nowIso(),
        period: { start: null, end: null },
        filters: {},
        data: {
          unlock: {
            territoryLabel: territory.label,
            restrictionLabel: territory.restrictionLabel || "Off-Limits",
            unlockedCount: unlocked.length,
            unlockedRecords: unlocked,
          },
          // Keep these empty so ViewReportModal's KPI/stage sections cleanly skip.
          kpis: {}, stages: {}, topOpps: [], staleList: [], brokerActivity: [], sourcePerformance: [],
        },
      };
      saveReport(report);
      setReportPopup(report);
      if (unlocked.length === 0) toast$(`Removed "${territory.label}" — no previously blocked leads to revisit.`);
      else toast$(`Removed "${territory.label}" — ${unlocked.length} previously blocked ${unlocked.length===1?"lead":"leads"} to revisit.`);
    }
  };

  // ── AI conflict check for leads + opps ────────────────────
  // Returns false (no conflict) when:
  //   • the record has no territory text
  //   • no territory in the brand matches the text
  //   • every matching territory is non-conflicting (e.g. `available` — that's good news,
  //     not a conflict). A territory's `conflict` flag lives in TERRITORY_STATUSES.
  // Returns a "conflict descriptor" when there's a real conflict so the UI can show the
  // appropriate reason (off-limits, resale possible, owned-active, etc.). For backward
  // compatibility this still resolves truthy where needed (boolean checks pass).
  const checkConflict = useCallback((rec) => {
    if (!bTerr.length || !rec.territory) return false;
    const needle = rec.territory.toLowerCase();
    const matching = bTerr.filter(t => t.label && needle.includes(t.label.toLowerCase().replace(/territory/gi,"").trim()));
    if (!matching.length) return false;
    // If any matched territory is conflicting, we flag. Otherwise it's available/clear.
    const conflicting = matching.find(t => TERRITORY_STATUSES[territoryStatus(t)]?.conflict);
    if (!conflicting) return false;
    const status = territoryStatus(conflicting);
    return { territory: conflicting, status, label: TERRITORY_STATUSES[status]?.label || "Conflict" };
  },[bTerr]);

  // ── Record helpers ────────────────────────────────────────
  // Normalize a phone number for duplicate matching: strip everything but digits, drop any
  // leading 1 (US country code) so "555-555-1234" and "+1 (555) 555-1234" match.
  const normalizePhone = (p) => {
    const d = String(p||"").replace(/\D/g, "");
    return d.length === 11 && d.startsWith("1") ? d.slice(1) : d;
  };
  // Find an existing lead/opp whose email or phone matches the candidate's. Returns
  // { match, matchType, reason } or null. Matches are: email (case-insensitive) OR phone
  // (digit-normalized). Both is reported when both match.
  const findDuplicate = (candidate) => {
    const email = (candidate.email||"").toLowerCase().trim();
    const phone = normalizePhone(candidate.phone);
    if (!email && !phone) return null;
    const all = [
      ...bLeads.map(l => ({rec: l, type: "lead"})),
      ...bOpps .map(o => ({rec: o, type: "opp"})),
    ];
    for (const { rec, type: t } of all) {
      const e = (rec.email||"").toLowerCase().trim();
      const p = normalizePhone(rec.phone);
      const emailMatch = email && e && e === email;
      const phoneMatch = phone && p && p === phone;
      if (emailMatch || phoneMatch) {
        const reason = emailMatch && phoneMatch ? "both" : emailMatch ? "email" : "phone";
        return { match: rec, matchType: t, reason };
      }
    }
    return null;
  };
  const saveRecord = (type,rec) => {
    if (!activeBrand) return;
    const n=nowIso(); const isNew=!rec.id;

    // ── Duplicate detection for new leads ─────────────────
    // Catch when a rep types in (or imports) a lead whose email or phone already exists in
    // the pipeline. Instead of creating a duplicate record, we merge any new fields into the
    // original, append a note describing what was merged, and stash the duplicate attempt in
    // the Duplicate Leads bucket (Settings → Duplicate Leads, restorable from there).
    if (type === "lead" && isNew) {
      const dup = findDuplicate(rec);
      if (dup) {
        const reasonLabel = dup.reason === "both" ? "email + phone"
                          : dup.reason === "email" ? "email"
                          : "phone";
        // Merge new fields into original where original is missing them. Original values win.
        const mergeFields = ["firstName","lastName","email","phone","company","location","territory","netWorth","liquidity","source","assignedTo"];
        const labelFor = { firstName:"First name", lastName:"Last name", email:"Email", phone:"Phone", company:"Company", location:"Location", territory:"Territory", netWorth:"Net worth", liquidity:"Liquidity", source:"Source", assignedTo:"Assigned to" };
        const added = {};
        const merged = { ...dup.match };
        for (const f of mergeFields) {
          const cur = (merged[f]||"").toString().trim();
          const inc = (rec[f]||"").toString().trim();
          if (!cur && inc) { merged[f] = rec[f]; added[f] = rec[f]; }
        }
        const addedLines = Object.entries(added).map(([k,v]) => `• ${labelFor[k]||k}: ${v}`).join("\n");
        const noteText = `🔁 Duplicate lead attempted — matched on ${reasonLabel}.\n` + (addedLines ? `New data merged into this record:\n${addedLines}\n(Existing field values were kept.)` : "All fields matched the existing record — nothing new to merge.");
        const noteId = uid();
        const note = { id: noteId, text: noteText, at: n, isTouchpoint: false, isCallAttempt: false, isDupFlag: true };
        const updated = { ...merged, notes: [...(merged.notes||[]), note], activities: [...(merged.activities||[]), { id: uid(), type:"duplicate", text:`🔁 Duplicate detected — merged into this record (matched on ${reasonLabel})`, at: n }], updatedAt: n };
        if (dup.matchType === "lead") {
          p("ff4_leads", setLeads, { ...leads, [activeBrand]: bLeads.map(l => l.id === merged.id ? updated : l) });
        } else {
          p("ff4_opps", setOpps, { ...opps, [activeBrand]: bOpps.map(o => o.id === merged.id ? updated : o) });
        }
        // Stash the duplicate attempt itself
        const dupEntry = { ...rec, id: uid(), createdAt: n, dupOf: merged.id, dupOfType: dup.matchType, dupReason: dup.reason, flaggedAt: n };
        const nextDup = { ...dupLeads, [activeBrand]: [...bDupLeads, dupEntry] };
        p("ff4_dup_leads", setDupLeads, nextDup);
        setModal(null);
        toast$(`🔁 Duplicate detected — merged into ${merged.firstName||"existing"} ${merged.lastName||"record"}'s file.`);
        return;
      }
    }

    // Every opp implicitly originated as a lead — even when created directly via the form —
    // so funnel analytics (conversion rate) stay accurate. Hidden from the UI; surfaced only
    // in the analytics math + as data on the record for future reporting.
    const oppExtras = (type === "opp" && isNew) ? { originatedAsLead: true, syntheticLeadOrigin: true, leadOriginAt: n } : {};
    const built=isNew?{...rec,id:uid(),createdAt:n,updatedAt:n,stageEnteredAt:n,notes:[],activities:[],partners:[],...oppExtras}:{...rec,updatedAt:n};
    if (type==="lead") { const next={...leads,[activeBrand]:isNew?[...bLeads,built]:bLeads.map(l=>l.id===built.id?built:l)}; p("ff4_leads",setLeads,next); }
    else { const next={...opps,[activeBrand]:isNew?[...bOpps,built]:bOpps.map(o=>o.id===built.id?built:o)}; p("ff4_opps",setOpps,next); }
    setModal(null); toast$(isNew?"Created!":"Saved!");
    if (isNew && type === "lead") {
      pushNotification("new_lead", { candidateName: `${built.firstName} ${built.lastName}`.trim() }, { recordRef: {type:"lead", id: built.id}, dedupeKey: `new_lead_${built.id}` });
    }
  };
  // Restore a previously-flagged duplicate back into the leads list.
  const restoreDuplicate = (dupId) => {
    if (!activeBrand) return;
    const dup = bDupLeads.find(d => d.id === dupId);
    if (!dup) return;
    const { dupOf, dupOfType, dupReason, flaggedAt, ...lead } = dup;
    const restored = { ...lead, id: uid(), createdAt: nowIso(), updatedAt: nowIso(), stageEnteredAt: nowIso(), notes: lead.notes||[], activities:[...(lead.activities||[]), { id: uid(), type:"restored", text:`↺ Restored from Duplicate Leads`, at: nowIso() }], partners:[], stage: lead.stage||"new_lead", restoredFromDuplicate: true };
    p("ff4_leads", setLeads, { ...leads, [activeBrand]: [...bLeads, restored] });
    const nextDup = { ...dupLeads, [activeBrand]: bDupLeads.filter(d => d.id !== dupId) };
    p("ff4_dup_leads", setDupLeads, nextDup);
    toast$("Duplicate restored to leads.");
  };
  const deleteDuplicate = (dupId) => {
    if (!activeBrand) return;
    if (!confirm("Permanently delete this duplicate entry? The original lead/opp it was merged into is not affected.")) return;
    const nextDup = { ...dupLeads, [activeBrand]: bDupLeads.filter(d => d.id !== dupId) };
    p("ff4_dup_leads", setDupLeads, nextDup);
    toast$("Duplicate deleted.","err");
  };
  const deleteRecord = (type,id) => {
    if (!activeBrand) return;
    if (type==="lead") p("ff4_leads",setLeads,{...leads,[activeBrand]:bLeads.filter(l=>l.id!==id)});
    else p("ff4_opps",setOpps,{...opps,[activeBrand]:bOpps.filter(o=>o.id!==id)});
    setSelected(null); setSubView("list"); toast$("Deleted.","err");
  };
  const convertToOpp = (lead) => {
    if (!activeBrand) return;
    const n=nowIso();
    const opp={...lead,id:uid(),createdAt:n,updatedAt:n,stageEnteredAt:n,stage:bOStages[0]?.id||"qualified",originalLeadId:lead.id,originatedAsLead:true,syntheticLeadOrigin:false,leadOriginAt:lead.createdAt||n,activities:[...(lead.activities||[]),{id:uid(),type:"convert",text:"Converted from Lead",at:n}]};
    p("ff4_opps",setOpps,{...opps,[activeBrand]:[...bOpps,opp]});
    p("ff4_leads",setLeads,{...leads,[activeBrand]:bLeads.map(l=>l.id===lead.id?{...l,convertedToOpp:true,convertedAt:n}:l)});
    toast$("Converted! 🎉"); setNav("opps"); setSelected({type:"opp",id:opp.id}); setSubView("detail");
  };

  // Stage change → confirm first
  const requestStageChange = (type,id,stageId) => {
    // Closed Lost on an opportunity is its own flow — needs a reason, not yes/no.
    if (type === "opp" && stageId === "closed_lost") { setClosedLostFor({type, id}); return; }
    const stages=type==="lead"?bLStages:bOStages;
    const label=stages.find(s=>s.id===stageId)?.label||stageId;
    setStageConfirm({type,id,stageId,label});
  };
  const confirmCloseLost = ({reason, notes}) => {
    if (!closedLostFor || !activeBrand) return;
    const { type, id } = closedLostFor;
    const reasonLabel = LOST_REASONS.find(r=>r.id===reason)?.label || reason;
    const n = nowIso();
    const text = `Closed Lost: ${reasonLabel}${notes?` — ${notes}`:""}`;
    // Auto-block communications when the candidate explicitly said no ("confirmed_lost"). The
    // other two reasons (ghosted / not_ready) leave the door open for future outreach.
    const autoBlock = reason === "confirmed_lost";
    const upd = (r) => ({
      ...r,
      stage:"closed_lost", stageEnteredAt:n, updatedAt:n, lostReason:reason, lostNotes:notes,
      ...(autoBlock ? { commsBlocked: true, commsBlockedAt: n, commsBlockedReason: "Closed Lost — confirmed lost" } : {}),
      activities:[...(r.activities||[]), { id:uid(), type:"stage", text, at:n },
        ...(autoBlock && !r.commsBlocked ? [{ id:uid(), type:"comms", text:"🚫 Communications blocked automatically (confirmed lost)", at:n }] : [])],
    });
    p("ff4_opps", setOpps, {...opps, [activeBrand]: bOpps.map(o => o.id===id ? upd(o) : o)});
    setScores(sc => { const nx = {...sc}; delete nx[id]; S.set("ff4_scores", nx); return nx; });
    toast$(autoBlock ? `Closed Lost · ${reasonLabel} — comms blocked` : `Closed Lost · ${reasonLabel}`, "err");
    setClosedLostFor(null);
  };
  // ── Notification helpers ──────────────────────────────────
  const pushNotification = useCallback((type, data = {}, opts = {}) => {
    const cfg = NOTIFICATION_TYPES[type];
    if (!cfg) return;
    const dedupeKey = opts.dedupeKey;
    setNotifications(prev => {
      // Skip if a dedupe-keyed notification with the same key exists in the last 24h.
      if (dedupeKey && prev.some(n => n.dedupeKey === dedupeKey && (Date.now() - new Date(n.createdAt)) < 86400000)) return prev;
      const notif = {
        id: uid(), type, category: cfg.category, severity: cfg.severity, isBanner: cfg.isBanner,
        title: cfg.format(data), data, recordRef: opts.recordRef || null,
        dedupeKey: dedupeKey || null, createdAt: nowIso(),
        readAt: null, bannerDismissedAt: null,
      };
      const next = [notif, ...prev].slice(0, 200); // cap history at 200
      S.set("ff4_notifications", next);
      return next;
    });
  }, []);
  const dismissBannerNotification = useCallback((id) => {
    setNotifications(prev => {
      const n = nowIso();
      const next = prev.map(nt => nt.id === id ? {...nt, bannerDismissedAt: n} : nt);
      S.set("ff4_notifications", next);
      return next;
    });
  }, []);
  const markNotificationRead = useCallback((id) => {
    setNotifications(prev => {
      const n = nowIso();
      const next = prev.map(nt => nt.id === id && !nt.readAt ? {...nt, readAt: n} : nt);
      S.set("ff4_notifications", next);
      return next;
    });
  }, []);
  const markAllNotificationsRead = useCallback(() => {
    setNotifications(prev => {
      const n = nowIso();
      const next = prev.map(nt => nt.readAt ? nt : {...nt, readAt: n});
      S.set("ff4_notifications", next);
      return next;
    });
  }, []);
  const clearAllNotifications = useCallback(() => {
    if (!confirm("Clear all notifications? This can't be undone.")) return;
    setNotifications([]); S.set("ff4_notifications", []);
  }, []);
  const openNotificationRecord = useCallback((notif) => {
    const ref = notif.recordRef;
    markNotificationRead(notif.id);
    dismissBannerNotification(notif.id);
    if (!ref) return;
    if (ref.type === "opp")  { setNav("opps");  setSelected({type:"opp",  id: ref.id}); setSubView("detail"); }
    if (ref.type === "lead") { setNav("leads"); setSelected({type:"lead", id: ref.id}); setSubView("detail"); }
  }, [markNotificationRead, dismissBannerNotification]);

  // ── Scheduling CRUD helpers ───────────────────────────────
  const saveEventType = (et) => {
    if (!activeBrand) return;
    setEventTypes(prev => {
      const cur = prev[activeBrand] || [];
      const exists = cur.find(x => x.id === et.id);
      const built = exists ? {...exists, ...et, updatedAt: nowIso()} : {...et, id: et.id||uid(), createdAt: nowIso(), active: et.active !== false};
      const nextList = exists ? cur.map(x => x.id === et.id ? built : x) : [...cur, built];
      const next = {...prev, [activeBrand]: nextList};
      S.set("ff4_event_types", next);
      return next;
    });
    toast$("Event type saved.");
  };
  const toggleEventTypeActive = (id) => {
    if (!activeBrand) return;
    setEventTypes(prev => {
      const next = {...prev, [activeBrand]: (prev[activeBrand]||[]).map(et => et.id===id ? {...et, active: !et.active} : et)};
      S.set("ff4_event_types", next);
      return next;
    });
  };
  const deleteEventType = (id) => {
    if (!activeBrand) return;
    setEventTypes(prev => {
      const next = {...prev, [activeBrand]: (prev[activeBrand]||[]).filter(et => et.id !== id)};
      S.set("ff4_event_types", next);
      return next;
    });
    toast$("Event type deleted.", "err");
  };
  const saveAvailability = (avail) => {
    if (!activeBrand) return;
    setAvailability(prev => {
      const next = {...prev, [activeBrand]: avail};
      S.set("ff4_availability", next);
      return next;
    });
  };
  const saveBooking = (payload) => {
    if (!activeBrand) return;
    const id = uid();
    const n = nowIso();
    const booking = { ...payload, id, status: "scheduled", createdAt: n, addedToGoogle: false, addedToOutlook: false, icsDownloaded: false };
    setBookings(prev => {
      const next = {...prev, [activeBrand]: [...(prev[activeBrand]||[]), booking]};
      S.set("ff4_bookings", next);
      return next;
    });
    // Activity log on the linked record
    const et = bEventTypes.find(e => e.id === booking.eventTypeId);
    const start = new Date(booking.startISO);
    const whenText = `${start.toLocaleDateString("en-US",{month:"short",day:"numeric"})} at ${start.toLocaleTimeString("en-US",{hour:"numeric",minute:"2-digit"})}`;
    const actText = `📅 ${et?.name||"Meeting"} scheduled for ${whenText}`;
    let addedPartnerCount = 0;
    if (booking.recordRef) {
      const ref = booking.recordRef;
      const setter = ref.type === "lead" ? setLeads : setOpps;
      const storageKey = ref.type === "lead" ? "ff4_leads" : "ff4_opps";
      setter(prev => {
        const cur = prev[activeBrand]||[];
        const next = {...prev, [activeBrand]: cur.map(r => {
          if (r.id !== ref.id) return r;
          const existingEmails = new Set((r.partners||[]).map(p => (p.email||"").toLowerCase()).filter(Boolean));
          // Promote any additional attendee with a new email to a partner on the record.
          const newPartners = (booking.additionalAttendees||[])
            .filter(a => a.email && !existingEmails.has(a.email.toLowerCase()))
            .map(a => {
              const parts = (a.name||"").trim().split(/\s+/);
              const firstName = parts[0] || "";
              const lastName  = parts.slice(1).join(" ") || "";
              return { id: uid(), firstName, lastName, email: a.email, role: "Business Partner", source: "scheduling", createdAt: n };
            });
          addedPartnerCount = newPartners.length;
          const activities = [...(r.activities||[]), {id: uid(), type:"booking", text: actText, at: n, bookingId: id}];
          if (newPartners.length) {
            activities.push({ id: uid(), type:"partner", text: `👥 Added ${newPartners.length} business partner${newPartners.length===1?"":"s"} from booking attendees: ${newPartners.map(p=>p.email).join(", ")}`, at: n });
          }
          return { ...r, updatedAt: n, partners: [...(r.partners||[]), ...newPartners], activities };
        })};
        S.set(storageKey, next);
        return next;
      });
    }
    pushNotification("booking_created", { candidateName: payload.attendeeName, eventTypeName: et?.name||"Meeting", when: whenText }, { recordRef: booking.recordRef, dedupeKey: `booking_created_${id}` });
    toast$(addedPartnerCount ? `Meeting scheduled! +${addedPartnerCount} new business partner${addedPartnerCount===1?"":"s"} added.` : "Meeting scheduled!");
  };
  const markBookingCompleted = (bookingId) => {
    if (!activeBrand) return;
    const booking = bBookings.find(b => b.id === bookingId);
    if (!booking || booking.status === "completed") return;
    const et = bEventTypes.find(e => e.id === booking.eventTypeId);
    const n = nowIso();
    // Create a synthetic note for the call tracker — every completed booking counts.
    const noteId = uid();
    const noteText = `Completed: ${et?.name||"Meeting"} (${et?.durationMin||0}min)${booking.notes?`\n\nNotes: ${booking.notes}`:""}`;
    setBookings(prev => {
      const next = {...prev, [activeBrand]: (prev[activeBrand]||[]).map(b => b.id===bookingId ? {...b, status:"completed", completedAt:n, syntheticNoteId: noteId} : b)};
      S.set("ff4_bookings", next);
      return next;
    });
    // Append the synthetic note + activity on the linked record (functional setState to avoid races)
    if (booking.recordRef) {
      const ref = booking.recordRef;
      const setter = ref.type === "lead" ? setLeads : setOpps;
      const storageKey = ref.type === "lead" ? "ff4_leads" : "ff4_opps";
      setter(prev => {
        const cur = prev[activeBrand]||[];
        const next = {...prev, [activeBrand]: cur.map(r => r.id !== ref.id ? r : {
          ...r,
          updatedAt: n,
          notes: [...(r.notes||[]), { id: noteId, text: noteText, at: n, isTouchpoint: true, isCallAttempt: true, fromBookingId: bookingId }],
          activities: [...(r.activities||[]), { id: uid(), type:"booking", text: `✅ ${et?.name||"Meeting"} completed`, at: n, bookingId }],
        })};
        S.set(storageKey, next);
        return next;
      });
      // For leads, check auto-advance now that call count bumped
      if (ref.type === "lead") setTimeout(()=>maybeAutoAdvanceLeadByCalls(ref.id), 60);
    }
    pushNotification("booking_completed", { candidateName: booking.attendeeName, eventTypeName: et?.name||"Meeting" }, { recordRef: booking.recordRef, dedupeKey: `booking_completed_${bookingId}` });
    toast$("Meeting marked completed.");
  };
  const cancelBooking = (bookingId) => {
    if (!activeBrand) return;
    const booking = bBookings.find(b => b.id === bookingId);
    if (!booking) return;
    const et = bEventTypes.find(e => e.id === booking.eventTypeId);
    const n = nowIso();
    setBookings(prev => {
      const next = {...prev, [activeBrand]: (prev[activeBrand]||[]).map(b => b.id===bookingId ? {...b, status:"cancelled", cancelledAt:n} : b)};
      S.set("ff4_bookings", next);
      return next;
    });
    if (booking.recordRef) {
      const ref = booking.recordRef;
      const setter = ref.type === "lead" ? setLeads : setOpps;
      const storageKey = ref.type === "lead" ? "ff4_leads" : "ff4_opps";
      setter(prev => {
        const next = {...prev, [activeBrand]: (prev[activeBrand]||[]).map(r => r.id !== ref.id ? r : {...r, updatedAt: n, activities:[...(r.activities||[]), {id: uid(), type:"booking", text: `❌ ${et?.name||"Meeting"} cancelled`, at: n, bookingId}]})};
        S.set(storageKey, next);
        return next;
      });
    }
    toast$("Booking cancelled.", "err");
  };
  const markBookingPatch = (bookingId, patch) => {
    if (!activeBrand) return;
    setBookings(prev => {
      const next = {...prev, [activeBrand]: (prev[activeBrand]||[]).map(b => b.id===bookingId ? {...b, ...patch} : b)};
      S.set("ff4_bookings", next);
      return next;
    });
  };

  const dismissConflict = (type, recId) => {
    if (!activeBrand) return;
    const n = nowIso();
    const upd = (r) => ({...r, conflictDismissedAt:n, updatedAt:n});
    if (type === "lead") p("ff4_leads", setLeads, {...leads, [activeBrand]: bLeads.map(l => l.id===recId ? upd(l) : l)});
    else                 p("ff4_opps",  setOpps,  {...opps,  [activeBrand]: bOpps.map(o  => o.id===recId ? upd(o) : o)});
    toast$("Territory conflict dismissed.");
  };
  const confirmStageChange = () => {
    if (!stageConfirm||!activeBrand) return;
    const {type,id,stageId} = stageConfirm;
    const stages=type==="lead"?bLStages:bOStages;
    const label=stages.find(s=>s.id===stageId)?.label||stageId;
    const n=nowIso();
    // Auto-block communications when a lead is disqualified — those candidates shouldn't be
    // contacted again. Manual override is always available via the checkbox in the profile.
    const autoBlock = type === "lead" && stageId === "disqualified";
    const upd=r=>({
      ...r,
      stage:stageId, stageEnteredAt:n, updatedAt:n,
      ...(autoBlock && !r.commsBlocked ? { commsBlocked: true, commsBlockedAt: n, commsBlockedReason: "Disqualified" } : {}),
      activities:[...(r.activities||[]),{id:uid(),type:"stage",text:`Moved to ${label}`,at:n},
        ...(autoBlock && !r.commsBlocked ? [{ id:uid(), type:"comms", text:"🚫 Communications blocked automatically (disqualified)", at:n }] : [])],
    });
    if (type==="lead") p("ff4_leads",setLeads,{...leads,[activeBrand]:bLeads.map(l=>l.id===id?upd(l):l)});
    else {
      p("ff4_opps",setOpps,{...opps,[activeBrand]:bOpps.map(o=>o.id===id?upd(o):o)});
      setScores(sc=>{const n2={...sc};delete n2[id]; S.set("ff4_scores",n2); return n2;});
    }
    toast$(autoBlock ? `→ ${label} · comms blocked` : `→ ${label}`); setStageConfirm(null);
  };
  // Manually toggle the comms-block flag from the profile checkbox.
  const toggleBlockComms = (type, id) => {
    if (!activeBrand) return;
    const n = nowIso();
    const upd = (r) => {
      const now = !r.commsBlocked;
      return {
        ...r,
        commsBlocked: now,
        commsBlockedAt: now ? n : null,
        commsBlockedReason: now ? "Manually blocked from profile" : null,
        updatedAt: n,
        activities: [...(r.activities||[]), { id: uid(), type:"comms", text: now ? "🚫 Communications blocked (manual)" : "✓ Communications unblocked (manual)", at: n }],
      };
    };
    if (type === "lead") p("ff4_leads", setLeads, { ...leads, [activeBrand]: bLeads.map(l => l.id === id ? upd(l) : l) });
    else                 p("ff4_opps",  setOpps,  { ...opps,  [activeBrand]: bOpps .map(o => o.id === id ? upd(o) : o) });
  };

  const addNote = (type,id,text) => {
    if (!text.trim()||!activeBrand) return;
    const n=nowIso();
    const noteId = uid();
    const initialTouchpoint = heuristicClassifyTouchpoint(text);
    const initialCall       = heuristicClassifyCall(text);
    const upd=r=>({...r,updatedAt:n,notes:[...(r.notes||[]),{id:noteId,text,at:n,isTouchpoint:initialTouchpoint,isCallAttempt:initialCall}],activities:[...(r.activities||[]),{id:uid(),type:"note",text:"Note added",at:n}]});
    if (type==="lead") p("ff4_leads",setLeads,{...leads,[activeBrand]:bLeads.map(l=>l.id===id?upd(l):l)});
    else { p("ff4_opps",setOpps,{...opps,[activeBrand]:bOpps.map(o=>o.id===id?upd(o):o)}); setScores(sc=>{const n2={...sc};delete n2[id];S.set("ff4_scores",n2);return n2;}); }
    toast$("Note saved!");
    // For leads, immediately check if the call count triggers a stage auto-advance
    if (type === "lead") setTimeout(() => maybeAutoAdvanceLeadByCalls(id), 50);
    // Refine in background with AI — if it disagrees with heuristic, silently update the note + recheck.
    if (settings.aiClassifyNotes === false) return;
    classifyNote(text).then(ai => {
      if (ai.isTouchpoint === initialTouchpoint && ai.isCallAttempt === initialCall) return;
      const patchNote = (r) => ({...r, notes: (r.notes||[]).map(nt => nt.id === noteId ? {...nt, isTouchpoint: ai.isTouchpoint, isCallAttempt: ai.isCallAttempt} : nt)});
      const setter = type === "lead" ? setLeads : setOpps;
      const storageKey = type === "lead" ? "ff4_leads" : "ff4_opps";
      setter(prev => {
        const next = {...prev, [activeBrand]: (prev[activeBrand]||[]).map(r => r.id === id ? patchNote(r) : r)};
        S.set(storageKey, next);
        return next;
      });
      if (type === "lead" && ai.isCallAttempt !== initialCall) {
        setTimeout(() => maybeAutoAdvanceLeadByCalls(id), 60);
      }
    }).catch(()=>{}); // silent fail
    // AI task extraction + auto-completion in one call. New tasks get a due date (parsed
    // from natural-language hints in the note, defaulting to 3 days out). Existing open
    // tasks get auto-completed if the note describes them as done.
    if (settings.aiTasksFromNotes === false) return;
    const list = type === "lead" ? bLeads : bOpps;
    const rec  = list.find(r => r.id === id);
    if (!rec) return;
    const recName  = `${rec.firstName||""} ${rec.lastName||""}`.trim();
    const stage    = rec.stage || "-";
    const allTasks = rec.tasks || [];
    const openTasksOrdered = allTasks.filter(t => !t.completed);
    extractTaskOpsAi(text, recName, stage, allTasks).then(({ newTasks, completedTaskIndices }) => {
      const completedIds = (completedTaskIndices||[]).map(i => openTasksOrdered[i]?.id).filter(Boolean);
      const fresh = (newTasks||[]).filter(c => !allTasks.some(t => taskSimilarity(c.text, t.text) >= 0.55));
      if (!fresh.length && !completedIds.length) return;
      const now = nowIso();
      const newTaskRecords = fresh.map(t => ({ id: uid(), text: t.text, dueDate: offsetToYMD(t.dueDays), completed: false, source: "ai", aiNoteId: noteId, createdAt: now }));
      const setter = type === "lead" ? setLeads : setOpps;
      const storageKey = type === "lead" ? "ff4_leads" : "ff4_opps";
      setter(prev => {
        const next = { ...prev, [activeBrand]: (prev[activeBrand]||[]).map(r => {
          if (r.id !== id) return r;
          const completedSet = new Set(completedIds);
          const patched = (r.tasks||[]).map(t => completedSet.has(t.id) ? { ...t, completed: true, completedAt: now, completedBy: "ai" } : t);
          return { ...r, tasks: [...patched, ...newTaskRecords] };
        }) };
        S.set(storageKey, next);
        return next;
      });
      const parts = [];
      if (newTaskRecords.length) parts.push(`✨ ${newTaskRecords.length} task${newTaskRecords.length===1?"":"s"} added`);
      if (completedIds.length)   parts.push(`✅ ${completedIds.length} task${completedIds.length===1?"":"s"} completed`);
      if (parts.length) toast$(parts.join(" · "));
    }).catch(()=>{}); // silent fail
  };
  // Task CRUD — tasks live on the record (rec.tasks). Manual + AI both flow through here.
  // Every task carries a required dueDate (YYYY-MM-DD). UI enforces it on manual entry;
  // AI defaults to +3 days or whatever the note parsed to.
  const addTask = (type, id, text, dueDate, source = "manual") => {
    if (!text.trim() || !activeBrand) return;
    const due = dueDate || offsetToYMD(1);
    const task = { id: uid(), text: text.trim(), dueDate: due, completed: false, source, createdAt: nowIso() };
    const list = type === "lead" ? leads : opps;
    const setter = type === "lead" ? setLeads : setOpps;
    const key = type === "lead" ? "ff4_leads" : "ff4_opps";
    const bList = list[activeBrand] || [];
    const next = { ...list, [activeBrand]: bList.map(r => r.id === id ? { ...r, updatedAt: nowIso(), tasks: [...(r.tasks||[]), task] } : r) };
    p(key, setter, next);
  };
  const toggleTask = (type, id, taskId) => {
    if (!activeBrand) return;
    const list = type === "lead" ? leads : opps;
    const setter = type === "lead" ? setLeads : setOpps;
    const key = type === "lead" ? "ff4_leads" : "ff4_opps";
    const bList = list[activeBrand] || [];
    const now = nowIso();
    const next = { ...list, [activeBrand]: bList.map(r => r.id === id ? { ...r, tasks: (r.tasks||[]).map(t => t.id === taskId ? { ...t, completed: !t.completed, completedAt: t.completed ? null : now } : t) } : r) };
    p(key, setter, next);
  };
  const deleteTask = (type, id, taskId) => {
    if (!activeBrand) return;
    const list = type === "lead" ? leads : opps;
    const setter = type === "lead" ? setLeads : setOpps;
    const key = type === "lead" ? "ff4_leads" : "ff4_opps";
    const bList = list[activeBrand] || [];
    const next = { ...list, [activeBrand]: bList.map(r => r.id === id ? { ...r, tasks: (r.tasks||[]).filter(t => t.id !== taskId) } : r) };
    p(key, setter, next);
  };

  // Auto-advance a lead's stage based on its accumulated call count.
  // 1st call: new_lead → contacted. 3rd call: contacted → nurturing. Other stages untouched.
  const maybeAutoAdvanceLeadByCalls = (leadId) => {
    if (!activeBrand) return;
    let advanced = null;
    setLeads(prev => {
      const list = prev[activeBrand] || [];
      const lead = list.find(l => l.id === leadId);
      if (!lead) return prev;
      const callCount = (lead.notes||[]).filter(nt => nt.isCallAttempt === true).length;
      let newStage = null, ctx = null;
      if (lead.stage === "new_lead"  && callCount >= 1) { newStage = "contacted";  ctx = "1st call logged"; }
      else if (lead.stage === "contacted" && callCount >= 3) { newStage = "nurturing"; ctx = "3rd call logged"; }
      if (!newStage) return prev;
      const n = nowIso();
      const stageDisplay = bLStages.find(s => s.id === newStage)?.label || newStage;
      advanced = { name: `${lead.firstName} ${lead.lastName}`, to: stageDisplay, ctx };
      const updated = list.map(l => l.id === leadId ? {
        ...l, stage: newStage, stageEnteredAt: n, updatedAt: n,
        activities: [...(l.activities||[]), { id:uid(), type:"stage", text:`Auto-moved to ${stageDisplay} — ${ctx}`, at:n }],
      } : l);
      const next = {...prev, [activeBrand]: updated};
      S.set("ff4_leads", next);
      return next;
    });
    if (advanced) toast$(`${advanced.name} → ${advanced.to} (${advanced.ctx})`);
  };
  // Manual override — user can click the 📞 icon on a note to flip its touchpoint tag.
  const toggleNoteTouchpoint = (type, recId, noteId) => {
    if (!activeBrand) return;
    const setter = type === "lead" ? setLeads : setOpps;
    const storageKey = type === "lead" ? "ff4_leads" : "ff4_opps";
    setter(prev => {
      const next = {...prev, [activeBrand]: (prev[activeBrand]||[]).map(r => r.id !== recId ? r : {...r, notes:(r.notes||[]).map(nt => nt.id !== noteId ? nt : {...nt, isTouchpoint: !nt.isTouchpoint})})};
      S.set(storageKey, next);
      return next;
    });
  };
  const addPartner = (type,id,partner) => {
    if (!activeBrand) return;
    const upd=r=>({...r,partners:[...(r.partners||[]),{...partner,id:uid()}],updatedAt:nowIso()});
    if (type==="lead") p("ff4_leads",setLeads,{...leads,[activeBrand]:bLeads.map(l=>l.id===id?upd(l):l)});
    else p("ff4_opps",setOpps,{...opps,[activeBrand]:bOpps.map(o=>o.id===id?upd(o):o)});
    setModal(null); toast$("Partner added!");
  };
  const removePartner = (type,recId,partnerId) => {
    const upd=r=>({...r,partners:(r.partners||[]).filter(p=>p.id!==partnerId),updatedAt:nowIso()});
    if (type==="lead") p("ff4_leads",setLeads,{...leads,[activeBrand]:bLeads.map(l=>l.id===recId?upd(l):l)});
    else p("ff4_opps",setOpps,{...opps,[activeBrand]:bOpps.map(o=>o.id===recId?upd(o):o)});
  };

  // ── Template helpers ──────────────────────────────────────
  const saveTemplate = (tpl) => {
    if (!activeBrand) return;
    const cur = bTemplates;
    const exists = cur.find(x=>x.id===tpl.id);
    const next = exists ? cur.map(x=>x.id===tpl.id?{...tpl,updatedAt:nowIso()}:x) : [...cur,{...tpl,id:tpl.id||uid(),createdAt:nowIso(),updatedAt:nowIso()}];
    p("ff4_ctemplates",setCustomTemplates,{...customTemplates,[activeBrand]:next});
    toast$(exists?"Template saved.":"Template created!");
  };
  const deleteTemplate = (id) => {
    if (!activeBrand) return;
    p("ff4_ctemplates",setCustomTemplates,{...customTemplates,[activeBrand]:bTemplates.filter(t=>t.id!==id)});
    toast$("Template deleted.","err");
  };

  // ── Template folder helpers (rename / nest / scope) ─────────
  const saveFolder = (folder) => {
    if (!activeBrand) return;
    const cur = bFolders;
    const exists = cur.find(f => f.id === folder.id);
    const next = exists
      ? cur.map(f => f.id === folder.id ? {...f, ...folder, updatedAt: nowIso()} : f)
      : [...cur, {id: folder.id||uid(), parentId:null, scope:"private", isDefault:false, createdAt: nowIso(), ...folder}];
    p("ff4_template_folders", setTemplateFolders, {...templateFolders, [activeBrand]: next});
    toast$(exists ? "Folder updated." : "Folder created!");
  };
  const deleteFolder = (id) => {
    if (!activeBrand) return;
    // Collect descendants so we can re-parent or wipe the templates under them
    const descendants = new Set();
    const stack = [id];
    while (stack.length) {
      const cur = stack.pop();
      descendants.add(cur);
      bFolders.filter(f => f.parentId === cur).forEach(f => stack.push(f.id));
    }
    const remaining = bFolders.filter(f => !descendants.has(f.id));
    p("ff4_template_folders", setTemplateFolders, {...templateFolders, [activeBrand]: remaining});
    // Reset any templates in the deleted folders to the first remaining root folder (or null)
    const fallback = remaining.find(f => !f.parentId)?.id || null;
    const updatedTemplates = bTemplates.map(t => descendants.has(t.category) ? {...t, category: fallback} : t);
    if (updatedTemplates.some((t, i) => t !== bTemplates[i])) {
      p("ff4_ctemplates", setCustomTemplates, {...customTemplates, [activeBrand]: updatedTemplates});
    }
    toast$("Folder deleted.","err");
  };

  // ── Discovery Day helpers ─────────────────────────────────────
  const saveDday = (dday) => {
    if (!activeBrand) return;
    const cur = bDdays;
    const exists = cur.find(d => d.id === dday.id);
    const next = exists
      ? cur.map(d => d.id === dday.id ? {...d, ...dday, updatedAt: nowIso()} : d)
      : [...cur, {id: dday.id||uid(), createdAt: nowIso(), ...dday}];
    p("ff4_dday", setDiscoveryDays, {...discoveryDays, [activeBrand]: next});
    toast$(exists ? "Discovery Day updated." : "Discovery Day created!");
  };
  const deleteDday = (id) => {
    if (!activeBrand) return;
    p("ff4_dday", setDiscoveryDays, {...discoveryDays, [activeBrand]: bDdays.filter(d => d.id !== id)});
    toast$("Discovery Day deleted.","err");
  };
  const toggleDdayChecklistItem = (ddayId, itemId) => {
    const dday = bDdays.find(d => d.id === ddayId);
    if (!dday) return;
    const items = (dday.checklist||[]).map(it => it.id === itemId ? {...it, done: !it.done, doneAt: !it.done ? nowIso() : null} : it);
    saveDday({...dday, checklist: items});
  };
  const addDdayChecklistItem = (ddayId, label, category) => {
    const dday = bDdays.find(d => d.id === ddayId);
    if (!dday || !label.trim()) return;
    saveDday({...dday, checklist: [...(dday.checklist||[]), { id: uid(), label: label.trim(), category: category||"day_of", done: false }]});
  };
  const deleteDdayChecklistItem = (ddayId, itemId) => {
    const dday = bDdays.find(d => d.id === ddayId);
    if (!dday) return;
    saveDday({...dday, checklist: (dday.checklist||[]).filter(it => it.id !== itemId)});
  };

  // ── Franchisee helpers ────────────────────────────────────────
  const saveFranchisee = (fr) => {
    if (!activeBrand) return;
    const cur = bFranchisees;
    const exists = cur.find(f => f.id === fr.id);
    const next = exists
      ? cur.map(f => f.id === fr.id ? {...f, ...fr, updatedAt: nowIso()} : f)
      : [...cur, {id: fr.id||uid(), createdAt: nowIso(), notesLog: [], ...fr}];
    p("ff4_franchisees", setFranchisees, {...franchisees, [activeBrand]: next});
    toast$(exists ? "Franchisee updated." : "Franchisee created!");
  };
  const deleteFranchisee = (id) => {
    if (!activeBrand) return;
    p("ff4_franchisees", setFranchisees, {...franchisees, [activeBrand]: bFranchisees.filter(f => f.id !== id)});
    toast$("Franchisee removed.","err");
  };
  const addFranchiseeNote = (fid, body) => {
    const trimmed = (body||"").trim();
    if (!trimmed) return;
    const fr = bFranchisees.find(f => f.id === fid);
    if (!fr) return;
    saveFranchisee({...fr, notesLog: [...(fr.notesLog||[]), { id: uid(), body: trimmed, at: nowIso() }]});
  };
  const deleteFranchiseeNote = (fid, noteId) => {
    const fr = bFranchisees.find(f => f.id === fid);
    if (!fr) return;
    saveFranchisee({...fr, notesLog: (fr.notesLog||[]).filter(n => n.id !== noteId)});
  };

  // ── Automation helpers ────────────────────────────────────
  const saveAutomation = (rule) => {
    if (!activeBrand) return;
    const cur = bAutos;
    const exists = cur.find(x=>x.id===rule.id);
    const next = exists ? cur.map(x=>x.id===rule.id?{...rule,updatedAt:nowIso()}:x) : [...cur,{...rule,id:rule.id||uid(),createdAt:nowIso(),runCount:0,enabled:rule.enabled??true}];
    p("ff4_autos",setAutomations,{...automations,[activeBrand]:next});
    toast$(exists?"Automation saved.":"Automation created!");
  };
  const deleteAutomation = (id) => {
    if (!activeBrand) return;
    p("ff4_autos",setAutomations,{...automations,[activeBrand]:bAutos.filter(a=>a.id!==id)});
    toast$("Automation deleted.","err");
  };
  const toggleAutomation = (id) => {
    if (!activeBrand) return;
    p("ff4_autos",setAutomations,{...automations,[activeBrand]:bAutos.map(a=>a.id===id?{...a,enabled:!a.enabled}:a)});
  };

  // ── Broker helpers ────────────────────────────────────────
  const saveBrokerData = (newData) => {
    if (!activeBrand) return;
    p("ff4_brokers",setBrokers,{...brokers,[activeBrand]:newData});
  };
  const saveNetwork = (net) => {
    const cur = bNetworks;
    const exists = cur.find(x=>x.id===net.id);
    const nextNet = exists ? cur.map(x=>x.id===net.id?net:x) : [...cur,{...net,id:net.id||uid(),createdAt:nowIso()}];
    saveBrokerData({...bBrokerData,networks:nextNet});
    toast$(exists?"Network saved.":"Network added!");
  };
  const deleteNetwork = (id) => {
    saveBrokerData({...bBrokerData,networks:bNetworks.filter(n=>n.id!==id),agents:bBrokers.map(a=>a.networkId===id?{...a,networkId:null}:a)});
    toast$("Network deleted.","err");
  };
  const saveBroker = (brk) => {
    const cur = bBrokers;
    const exists = cur.find(x=>x.id===brk.id);
    const nextAg = exists ? cur.map(x=>x.id===brk.id?brk:x) : [...cur,{...brk,id:brk.id||uid(),createdAt:nowIso()}];
    saveBrokerData({...bBrokerData,agents:nextAg});
    toast$(exists?"Broker saved.":"Broker added!");
  };
  const deleteBroker = (id) => {
    saveBrokerData({...bBrokerData,agents:bBrokers.filter(b=>b.id!==id)});
    // Unassign from any opportunity
    p("ff4_opps",setOpps,{...opps,[activeBrand]:bOpps.map(o=>o.brokerId===id?{...o,brokerId:null}:o)});
    toast$("Broker deleted.","err");
  };
  // Notes log on broker / network profiles. Stored as a timestamped array under
  // `notesLog`. The legacy `notes` string (single bio blurb) is preserved separately.
  const addBrokerNote = (brokerId, body) => {
    const trimmed = (body||"").trim();
    if (!trimmed) return;
    const next = bBrokers.map(b => b.id === brokerId ? {...b, notesLog: [...(b.notesLog||[]), { id: uid(), body: trimmed, at: nowIso() }]} : b);
    saveBrokerData({...bBrokerData, agents: next});
  };
  const deleteBrokerNote = (brokerId, noteId) => {
    const next = bBrokers.map(b => b.id === brokerId ? {...b, notesLog: (b.notesLog||[]).filter(n => n.id !== noteId)} : b);
    saveBrokerData({...bBrokerData, agents: next});
  };
  const addNetworkNote = (networkId, body) => {
    const trimmed = (body||"").trim();
    if (!trimmed) return;
    const next = bNetworks.map(n => n.id === networkId ? {...n, notesLog: [...(n.notesLog||[]), { id: uid(), body: trimmed, at: nowIso() }]} : n);
    saveBrokerData({...bBrokerData, networks: next});
  };
  const deleteNetworkNote = (networkId, noteId) => {
    const next = bNetworks.map(n => n.id === networkId ? {...n, notesLog: (n.notesLog||[]).filter(x => x.id !== noteId)} : n);
    saveBrokerData({...bBrokerData, networks: next});
  };
  // Cross-app navigation: jump to a broker or network profile. Anywhere a broker/network
  // name is rendered (reports, notifications, broker hub, lead/opp detail), it can be
  // wrapped in <RecordLink onClick={()=>openBroker(id)}>… so the user lands on the profile.
  const openBroker = (id) => {
    if (!id) return;
    setNetworkDetailId(null);
    setBrokerDetailId(id);
    setNav("brokers");
  };
  const openNetwork = (id) => {
    if (!id) return;
    setBrokerDetailId(null);
    setNetworkDetailId(id);
    setNav("brokers");
  };
  const assignBrokerToOpp = (oppId,brokerId) => {
    if (!activeBrand) return;
    const broker = bBrokers.find(b=>b.id===brokerId);
    const n=nowIso();
    p("ff4_opps",setOpps,{...opps,[activeBrand]:bOpps.map(o=>o.id===oppId?{...o,brokerId,updatedAt:n,activities:[...(o.activities||[]),{id:uid(),type:"broker",text:`Broker assigned: ${broker?.firstName||""} ${broker?.lastName||""}`.trim(),at:n}]}:o)});
    toast$("Broker assigned!");
  };
  const unassignBrokerFromOpp = (oppId) => {
    if (!activeBrand) return;
    const n=nowIso();
    p("ff4_opps",setOpps,{...opps,[activeBrand]:bOpps.map(o=>o.id===oppId?{...o,brokerId:null,updatedAt:n,activities:[...(o.activities||[]),{id:uid(),type:"broker",text:"Broker unassigned",at:n}]}:o)});
    toast$("Broker unassigned.","err");
  };
  const addBrokerComm = (oppId,msg) => {
    const list = brokerComms[oppId]||[];
    const next = {...brokerComms,[oppId]:[...list,{...msg,id:uid(),at:nowIso()}]};
    p("ff4_bcomms",setBrokerComms,next);
  };

  // ── DocuSign envelope helpers ─────────────────────────────
  const sendDocusignEnvelope = (oppId, { docType, recipient, subject, message }) => {
    if (!activeBrand) return;
    const n = nowIso();
    const envelope = {
      id: uid(),
      docType, // "fdd" | "agreement"
      status: "sent",
      envelopeId: `ENV-${Math.random().toString(36).slice(2,10).toUpperCase()}`,
      recipient, subject, message,
      sentAt: n, viewedAt: null, completedAt: null, declinedAt: null,
    };
    const docLabel = docType==="fdd" ? "FDD" : "Franchise Agreement";
    const targetStage = docType === "fdd" ? "fdd_sent" : docType === "agreement" ? "agreement_sent" : null;
    // Single batched update so the envelope add AND stage auto-advance both land
    p("ff4_opps", setOpps, {...opps, [activeBrand]: bOpps.map(o => {
      if (o.id !== oppId) return o;
      const acts = [...(o.activities||[]), { id:uid(), type:"docusign", text:`${docLabel} sent via DocuSign to ${recipient}`, at:n }];
      let next = {...o, docusignEnvelopes:[...(o.docusignEnvelopes||[]), envelope], updatedAt:n, activities:acts};
      if (targetStage && bOStages.find(s=>s.id===targetStage) && o.stage !== targetStage) {
        const tLabel = bOStages.find(s=>s.id===targetStage).label;
        next = {...next, stage:targetStage, stageEnteredAt:n, activities:[...next.activities, { id:uid(), type:"stage", text:`Auto-moved to ${tLabel} (DocuSign)`, at:n }]};
      }
      return next;
    })});
    // Drop any cached score since the opp materially changed
    setScores(sc => { const n2 = {...sc}; delete n2[oppId]; S.set("ff4_scores", n2); return n2; });
    toast$(`${docLabel} sent via DocuSign!`);
  };
  const updateDocusignEnvelope = (oppId, envId, patch) => {
    if (!activeBrand) return;
    const n = nowIso();
    const ts = (s) => s==="viewed" ? {viewedAt:n} : s==="completed" ? {completedAt:n} : s==="declined" ? {declinedAt:n} : {};
    p("ff4_opps", setOpps, {...opps, [activeBrand]: bOpps.map(o => o.id === oppId
      ? {...o, updatedAt:n, docusignEnvelopes: (o.docusignEnvelopes||[]).map(e => e.id===envId ? {...e, ...patch, ...(patch.status?ts(patch.status):{})} : e)}
      : o)});
    // Fire notification on status transitions
    if (patch.status) {
      const opp = bOpps.find(o => o.id === oppId);
      const env = (opp?.docusignEnvelopes||[]).find(e => e.id === envId);
      if (opp && env) {
        const docLabel = env.docType === "fdd" ? "FDD" : "Agreement";
        const data = { candidateName: `${opp.firstName} ${opp.lastName}`, docLabel };
        const ref = { recordRef: {type:"opp", id:oppId}, dedupeKey: `env_${patch.status}_${envId}` };
        if (patch.status === "viewed")    pushNotification("envelope_viewed",   data, ref);
        if (patch.status === "completed") pushNotification("envelope_signed",   data, ref);
        if (patch.status === "declined")  pushNotification("envelope_declined", data, ref);
      }
    }
  };
  const voidDocusignEnvelope = (oppId, envId) => updateDocusignEnvelope(oppId, envId, { status: "voided" });

  // ── AI Scoring ────────────────────────────────────────────
  const scoreOpp = useCallback(async(opp)=>{
    if (!opp||scoringIds.has(opp.id)) return;
    setScoringIds(s=>new Set([...s,opp.id]));
    // Trim notes to the most recent 4 to bound the input. For kanban sort the model only
    // needs a quick "is this opp hot?" signal — not a full case review.
    const recentNotes = (opp.notes||[]).slice(-4).map(n=>`[${fmtDate(n.at)}] ${n.text}`).join(" | ")||"(none)";
    const prompt = `Rate this franchise candidate 0-100. Higher = hotter (more engaged, more likely to close).\n\n${opp.firstName} ${opp.lastName} | Stage ${opp.stage} (${daysSince(opp.stageEnteredAt)}d) | Notes: ${recentNotes}\n\nReturn ONLY JSON: {"total":<0-100>,"summary":"<≤15 words>","flag":<null or short warning>}`;
    try {
      const raw = await callClaude([{role:"user",content:prompt}], "", 120, settings.aiModelScore || "claude-haiku-4-5");
      const parsed = JSON.parse(raw.replace(/```json|```/g,"").trim());
      // Use functional setState so concurrent score calls don't overwrite each other.
      setScores(prev => {
        const next = {...prev, [opp.id]: {total:parsed.total, summary:parsed.summary, flag:parsed.flag, scoredAt:nowIso()}};
        S.set("ff4_scores", next);
        return next;
      });
    } catch{}
    setScoringIds(s=>{const n=new Set(s);n.delete(opp.id);return n;});
  },[scoringIds,settings.aiModelScore]);


  useEffect(()=>{ if(loading)return; if(!settings.autoScoreOnLoad) return; bOpps.forEach(o=>{ if(!scores[o.id]&&!scoringIds.has(o.id)) scoreOpp(o); }); },[loading,activeBrand,settings.autoScoreOnLoad]);// eslint-disable-line

  // ── Notification scans (fire once per app load) ───────────
  // - Hot lead going dark: high-scoring lead with 7+ days silent
  // - Envelope idle: DocuSign envelope sent 7+ days ago with no signature
  // - Promised material: lead/opp with a "promised" / "will send" / "I'll send" phrase in recent notes
  // - Digests: morning / weekly / monthly when due (dedupe-keyed by date)
  const scanFiredRef = useRef(false);
  useEffect(() => {
    if (loading || !activeBrand || scanFiredRef.current) return;
    scanFiredRef.current = true;

    const today = new Date();
    const ymd = today.toISOString().slice(0,10);
    const isMorning = today.getHours() < 12;
    const isMonday  = today.getDay() === 1;
    const dayOfMonth = today.getDate();

    // Hot lead going dark
    bLeads.forEach(l => {
      const lvl = leadStaleLevel(l);
      if (!lvl) return;
      const days = daysSince(lastContactDate(l));
      // Use the score on the lead's converted-to-opp if any, else use length of notes as a proxy (no scoring on leads)
      const sc = scores[l.id]?.total || (l.notes||[]).length * 10; // rough proxy
      if (sc >= 60 && days >= 7) {
        pushNotification("hot_lead_dark",
          { candidateName: `${l.firstName} ${l.lastName}`, score: sc, days },
          { recordRef: {type:"lead", id:l.id}, dedupeKey: `hot_lead_dark_${l.id}_${ymd}` });
      }
    });

    // Envelope idle
    bOpps.forEach(o => {
      (o.docusignEnvelopes||[]).forEach(env => {
        if (env.status !== "sent") return;
        const d = daysSince(env.sentAt);
        if (d < 7) return;
        pushNotification("envelope_idle",
          { candidateName: `${o.firstName} ${o.lastName}`, docLabel: env.docType==="fdd"?"FDD":"Agreement", days: d },
          { recordRef: {type:"opp", id:o.id}, dedupeKey: `envelope_idle_${env.id}_${ymd}` });
      });
    });

    // Tasks-due-today notification — fires once per day after 9am local. Counts open
    // tasks across every lead + opp whose dueDate is today. No per-record badges anywhere
    // else in the app, on purpose — this is the one and only nag.
    const hour = today.getHours();
    if (hour >= 9) {
      const today_ymd = today.toISOString().slice(0,10);
      let dueCount = 0;
      [...bLeads, ...bOpps].forEach(r => (r.tasks||[]).forEach(t => { if (!t.completed && t.dueDate === today_ymd) dueCount++; }));
      if (dueCount > 0) {
        pushNotification("tasks_due_today",
          { count: dueCount },
          { dedupeKey: `tasks_due_today_${ymd}` });
      }
    }
    // Morning digest — once per day before noon
    if (isMorning) {
      const topOppName = bOpps.map(o => ({...o, sc: scores[o.id]?.total||0})).sort((a,b)=>b.sc-a.sc)[0];
      if (topOppName) {
        const name = `${topOppName.firstName} ${topOppName.lastName}`;
        pushNotification("digest_morning",
          { topPriority: `Focus on ${name} (score ${topOppName.sc}) and dismiss stale leads.`, candidateName: name },
          { dedupeKey: `digest_morning_${ymd}`, recordRef: { type:"opp", id: topOppName.id } });
      }
    }

    // Weekly digest — Monday morning
    if (isMonday && isMorning) {
      const wk = `${today.getFullYear()}-W${Math.ceil((today.getDate() + 6 - today.getDay())/7)}`;
      const signed = bOpps.filter(o => o.stage === "agreement_signed").length;
      const lost   = bOpps.filter(o => o.stage === "closed_lost").length;
      const added  = bLeads.filter(l => daysSince(l.createdAt) <= 7).length;
      pushNotification("digest_weekly", { signed, lost, added }, { dedupeKey: `digest_weekly_${wk}` });
      // Auto-generate a weekly snapshot report for the past 7 days. Dedupe via title check so we
      // don't double-create when the user refreshes multiple times in the same week.
      const weekTitle = `📅 Weekly · ${new Date(today.getTime() - 7*86400000).toLocaleDateString("en-US",{month:"short",day:"numeric"})}–${today.toLocaleDateString("en-US",{month:"short",day:"numeric"})}`;
      if (!bReports.some(r => r.type === "weekly" && r.title === weekTitle)) {
        setTimeout(() => generateReport({ type:"weekly", title: weekTitle, periodStart: new Date(today.getTime() - 7*86400000).toISOString(), periodEnd: today.toISOString() }), 120);
      }
    }

    // Bookings — upcoming 24h, starting within 1h, and auto-complete past end time
    const nowMs = Date.now();
    bBookings.forEach(bk => {
      if (bk.status !== "scheduled") return;
      const startMs = new Date(bk.startISO).getTime();
      const endMs   = new Date(bk.endISO).getTime();
      const et = bEventTypes.find(e => e.id === bk.eventTypeId);
      const whenText = new Date(bk.startISO).toLocaleString("en-US", {month:"short",day:"numeric",hour:"numeric",minute:"2-digit"});
      // Auto-flip to completed
      if (endMs < nowMs) {
        setTimeout(() => markBookingCompleted(bk.id), 80);
        return;
      }
      // Starting within 1 hour
      if (startMs - nowMs > 0 && startMs - nowMs <= 3600000) {
        pushNotification("booking_starting_1h",
          { candidateName: bk.attendeeName, eventTypeName: et?.name||"Meeting", when: whenText },
          { recordRef: bk.recordRef, dedupeKey: `booking_starting_1h_${bk.id}_${ymd}` });
      }
      // Upcoming in next 24h (but more than 1h away)
      else if (startMs - nowMs > 3600000 && startMs - nowMs <= 86400000) {
        pushNotification("booking_upcoming_24h",
          { candidateName: bk.attendeeName, eventTypeName: et?.name||"Meeting", when: whenText },
          { recordRef: bk.recordRef, dedupeKey: `booking_upcoming_24h_${bk.id}_${ymd}` });
      }
    });

    // Monthly digest — first 3 days of the month
    if (dayOfMonth <= 3 && isMorning) {
      const ym = ymd.slice(0,7);
      const signed = bOpps.filter(o => o.stage === "agreement_signed").length;
      const lost   = bOpps.filter(o => o.stage === "closed_lost").length;
      const opsCountedAsLeadsD = bOpps.filter(o => !bLeads.find(l => l.id === o.originalLeadId)).length;
      const totalFunnelD = bLeads.length + opsCountedAsLeadsD;
      const convRate = totalFunnelD ? Math.min(100, Math.round((bOpps.length / totalFunnelD) * 100)) : 0;
      pushNotification("digest_monthly", { signed, lost, convRate }, { dedupeKey: `digest_monthly_${ym}` });
      // Auto-generate a monthly snapshot report covering the prior month.
      const prevMonth = new Date(today.getFullYear(), today.getMonth() - 1, 1);
      const prevMonthEnd = new Date(today.getFullYear(), today.getMonth(), 0, 23, 59, 59);
      const monthTitle = `📆 Monthly · ${prevMonth.toLocaleDateString("en-US",{month:"long",year:"numeric"})}`;
      if (!bReports.some(r => r.type === "monthly" && r.title === monthTitle)) {
        setTimeout(() => generateReport({ type:"monthly", title: monthTitle, periodStart: prevMonth.toISOString(), periodEnd: prevMonthEnd.toISOString() }), 180);
      }
    }
  }, [loading, activeBrand]);// eslint-disable-line

  const sortedOppsByStage=useMemo(()=>{
    const map={};
    bOStages.forEach(s=>{ map[s.id]=bOpps.filter(o=>o.stage===s.id).sort((a,b)=>(scores[b.id]?.total||0)-(scores[a.id]?.total||0)); });
    return map;
  },[bOpps,bOStages,scores]);

  // ── What's Next (auto-loads) ──────────────────────────────
  const fetchWhatsNext=useCallback(async()=>{
    if(!activeBrand) return;
    setWnLoading(true); setWhatsNext(null);
    // Bound input: top 12 opps + top 5 unconverted leads. Trim each line.
    const oppsSummary = bOpps.slice(0,12).map(o=>{
      const sc=scores[o.id]; const s=bOStages.find(st=>st.id===o.stage);
      const isStale = s?.staleDays && daysSince(o.stageEnteredAt) >= s.staleDays;
      const fddSent = (o.docusignEnvelopes||[]).some(e=>e.docType==="fdd"&&e.status!=="voided");
      return `${o.firstName} ${o.lastName} | ${o.stage} ${daysSince(o.stageEnteredAt)}d${isStale?"⚠":""} | score ${sc?.total||"-"} | fdd ${fddSent?"y":"n"}`;
    }).join("\n")||"none";
    const leadsSummary = bLeads.filter(l=>!l.convertedToOpp).slice(0,5).map(l=>`${l.firstName} ${l.lastName} | ${l.stage} | notes ${(l.notes||[]).length}`).join("\n")||"none";
    try {
      const raw = await callClaude([{role:"user",content:`Pick the #1 most urgent action right now.\n\nBRAND: ${brand?.name||"-"}\nOPPS:\n${oppsSummary}\nLEADS:\n${leadsSummary}\n\nReturn ONLY JSON: {"task":"<≤12 words>","person":"<First Last or null>","reason":"<1 sentence>","action_type":"<call|email|send_fdd|schedule|review|disqualify|other>","urgency":"<high|medium|low>"}`}], "", 200, settings.aiModelWhatsNext || "claude-haiku-4-5");
      setWhatsNext(JSON.parse(raw.replace(/```json|```/g,"").trim()));
    } catch { setWhatsNext({task:"Review your pipeline and follow up with your most stale opportunity.",person:null,reason:"Could not parse AI response.",action_type:"review",urgency:"medium"}); }
    setWnLoading(false);
  },[activeBrand,bOpps,bLeads,bOStages,scores,brand,settings.aiModelWhatsNext]);

  // Auto-trigger when navigating to What's Next
  useEffect(()=>{ if(nav==="whats_next"&&!whatsNext&&!wnLoading&&!loading&&activeBrand) fetchWhatsNext(); },[nav,activeBrand,loading]);// eslint-disable-line

  // ── Jump-to-next ─────────────────────────────────────────
  // One-tap action that fetches/uses the current What's Next recommendation and navigates
  // straight to the recommended person — skipping the summary tab. Used by:
  //   • the new "🎯 Next" button on Leads + Opportunities list pages
  //   • the same button inside Quick Actions on a record profile
  //   • the right-side button on the "What's Next?" sidebar tab
  // If the cached recommendation already exists and resolves to a real record, navigate
  // immediately. Otherwise fetch a fresh one and navigate when it resolves.
  const [jumpingToNext, setJumpingToNext] = useState(false);
  const findPersonByName = useCallback((name)=>{
    if(!name) return null;
    const nl=name.toLowerCase().trim();
    const opp=bOpps.find(r=>`${r.firstName} ${r.lastName}`.toLowerCase()===nl);
    if(opp) return {type:"opp",id:opp.id};
    const lead=bLeads.find(r=>`${r.firstName} ${r.lastName}`.toLowerCase()===nl);
    if(lead) return {type:"lead",id:lead.id};
    return null;
  },[bOpps,bLeads]);
  const jumpToNext = useCallback(async () => {
    if (!activeBrand || jumpingToNext) return;
    // 1) If we already have a recommendation and it resolves, jump now.
    const navTo = (match) => {
      setNav(match.type==="opp"?"opps":"leads");
      setSelected(match);
      setSubView("detail");
    };
    if (whatsNext) {
      const m = findPersonByName(whatsNext.person);
      if (m) { navTo(m); return; }
    }
    // 2) Otherwise fetch and navigate to whatever the fresh AI pick is. Wraps a clone of
    //    fetchWhatsNext that doesn't clear state on entry (so the loading spinner can show
    //    on the originating button instead of the summary tab).
    setJumpingToNext(true);
    try {
      const oppsSummary = bOpps.slice(0,12).map(o=>{
        const sc=scores[o.id]; const s=bOStages.find(st=>st.id===o.stage);
        const isStale = s?.staleDays && daysSince(o.stageEnteredAt) >= s.staleDays;
        const fddSent = (o.docusignEnvelopes||[]).some(e=>e.docType==="fdd"&&e.status!=="voided");
        return `${o.firstName} ${o.lastName} | ${o.stage} ${daysSince(o.stageEnteredAt)}d${isStale?"⚠":""} | score ${sc?.total||"-"} | fdd ${fddSent?"y":"n"}`;
      }).join("\n")||"none";
      const leadsSummary = bLeads.filter(l=>!l.convertedToOpp).slice(0,5).map(l=>`${l.firstName} ${l.lastName} | ${l.stage} | notes ${(l.notes||[]).length}`).join("\n")||"none";
      const raw = await callClaude([{role:"user",content:`Pick the #1 most urgent action right now.\n\nBRAND: ${brand?.name||"-"}\nOPPS:\n${oppsSummary}\nLEADS:\n${leadsSummary}\n\nReturn ONLY JSON: {"task":"<≤12 words>","person":"<First Last or null>","reason":"<1 sentence>","action_type":"<call|email|send_fdd|schedule|review|disqualify|other>","urgency":"<high|medium|low>"}`}], "", 200, settings.aiModelWhatsNext || "claude-haiku-4-5");
      const next = JSON.parse(raw.replace(/```json|```/g,"").trim());
      setWhatsNext(next);
      const m = findPersonByName(next.person);
      if (m) {
        navTo(m);
      } else {
        // No exact name match — fall back to the summary tab so the user can see context.
        setNav("whats_next");
        toast$("Couldn't match the AI-suggested name; showing the summary instead.","err");
      }
    } catch (e) {
      toast$("Could not load the next recommendation. Try again.","err");
      setNav("whats_next");
    } finally {
      setJumpingToNext(false);
    }
  }, [activeBrand, jumpingToNext, whatsNext, findPersonByName, bOpps, bLeads, bOStages, scores, brand, settings.aiModelWhatsNext]);

  // Today's tasks — pure list lookup, no AI. Cheap enough to recompute every render.
  // Returns rows like { task, recId, recType, recName, stage } sorted by stage importance.
  const tasksDueToday = (() => {
    if (!activeBrand) return [];
    const today_ymd = todayYMD();
    const STAGE_WEIGHT = { agreement_sent: 0, agreement_signed: 1, discovery_day: 2, validation: 3, ceo_qa: 4, intake_form: 5, application: 6, fdd_review_call: 7, fdd_signed: 8, fdd_sent: 9, intro_call: 10, qualified: 11, new_lead: 12, contacted: 13, nurturing: 14, disqualified: 15, closed_lost: 16 };
    const rows = [];
    [...bLeads, ...bOpps].forEach(r => {
      const recType = bLeads.includes(r) ? "lead" : "opp";
      (r.tasks||[]).forEach(t => { if (!t.completed && t.dueDate === today_ymd) rows.push({ task: t, recId: r.id, recType, recName: `${r.firstName} ${r.lastName}`, stage: r.stage }); });
    });
    return rows.sort((a,b) => (STAGE_WEIGHT[a.stage]??99) - (STAGE_WEIGHT[b.stage]??99));
  })();

  // ── AI Summary ────────────────────────────────────────────
  const runAiSummary=async(rec)=>{
    setAiLoading(true); setAiSummary(""); setModal("aiSummary"); setModalData({rec});
    // Trim notes to most recent 6 to keep input tight.
    const notes = (rec.notes||[]).slice(-6).map(n=>`[${fmtDate(n.at)}] ${n.text}`).join("\n")||"(none)";
    const sc = scores[rec.id];
    const fddSent = (rec.docusignEnvelopes||[]).some(e=>e.docType==="fdd"&&e.status!=="voided");
    const agSent  = (rec.docusignEnvelopes||[]).some(e=>e.docType==="agreement"&&e.status!=="voided");
    const finance = [rec.netWorth ? `Net worth ${rec.netWorth}` : null, rec.liquidity ? `Liquidity ${rec.liquidity}` : null].filter(Boolean).join(" / ") || (rec.investmentLevel || "-");
    const prompt = `Review ${rec.firstName} ${rec.lastName}.\nStage ${rec.stage} (${daysSince(rec.stageEnteredAt)}d) | Territory ${rec.territory||"-"} | ${finance} | Score ${sc?.total||"-"} | FDD ${fddSent?"sent":"no"} | Agreement ${agSent?"sent":"no"}\n\nNotes:\n${notes}\n\nReturn markdown:\n## Summary\n<2 sentences>\n\n## Missing / Risks\n<2-3 bullets>\n\n## Next Steps\n<top 3 bullets>`;
    try { const text = await callClaude([{role:"user",content:prompt}], "", 500, settings.aiModelSummary || "claude-haiku-4-5"); setAiSummary(text); }
    catch { setAiSummary("Error."); }
    setAiLoading(false);
  };

  // ── AI Organize (pipeline-wide audit) ─────────────────────
  const runAiOrganize = async () => {
    if (!activeBrand || bOpps.length === 0) { toast$("No opportunities to organize.","err"); return; }
    setAiOrganizeLoading(true); setAiOrganize(null); setModal("aiOrganize");
    const stageList = bOStages.map(s=>`${s.id}=${s.label}`).join(", ");
    const oppLines = bOpps.map(o => {
      const sc = scores[o.id];
      const st = bOStages.find(s=>s.id===o.stage);
      const isStale = st?.staleDays && daysSince(o.stageEnteredAt) >= st.staleDays;
      const recentNotes = (o.notes||[]).slice(-4).map(n=>`[${fmtDate(n.at)}] ${n.text}`).join(" | ");
      return `id:${o.id} | ${o.firstName} ${o.lastName} | stage:${o.stage} (${daysSince(o.stageEnteredAt)}d${isStale?", STALE":""}) | score:${sc?.total||"unscored"} | notes:${recentNotes||"(none)"}`;
    }).join("\n");
    const prompt = `You are a franchise development advisor doing a pipeline audit.

BRAND: ${brand?.name}
AVAILABLE STAGES: ${stageList}

OPPORTUNITIES:
${oppLines}

Identify issues across the whole pipeline. Be specific — reference opportunities by their id. Don't be redundant; only include items that are clear from the data. If a section has no issues, return an empty array.

Return STRICTLY valid JSON. No prose, no markdown:
{
  "disqualify":        [{"id":"<opp id>","reason":"<1 sentence why to drop>"}],
  "wrong_stage":       [{"id":"<opp id>","suggestedStage":"<stage id from list>","reason":"<1 sentence why>"}],
  "missing_materials": [{"id":"<opp id>","item":"<what was promised>","reason":"<short context>"}],
  "habits":            [{"title":"<short pattern name>","detail":"<2 sentence advice>"}]
}`;
    try {
      const raw = await callClaude([{role:"user",content:prompt}], "", 1200, settings.aiModelOrganize || "claude-haiku-4-5");
      const parsed = JSON.parse(raw.replace(/```json|```/g,"").trim());
      setAiOrganize(parsed);
    } catch (e) {
      setAiOrganize({ error: "AI didn't return valid JSON. Try again, or check your API key." });
    }
    setAiOrganizeLoading(false);
  };

  // ── FDD Upload ────────────────────────────────────────────
  const parseFDD=async(file,brandId)=>{
    setFddParsing(true);
    const reader=new FileReader();
    reader.onload=async(e)=>{
      const b64=e.target.result.split(",")[1];
      try{const raw=await callClaudeWithPDF(b64,`Extract FDD financial data from Items 5, 6, 7, and 19 ONLY. Return ONLY JSON, no markdown:\n{"franchiseFee":"<Item 5 single territory fee>","royaltyRate":"<Item 6 royalty %>","adFundRate":"<Item 6 ad fund %>","investmentMin":"<Item 7 min total>","investmentMax":"<Item 7 max total>","avgGrossSales":"<Item 19 avg gross sales or null>","avgEbitda":"<Item 19 EBITDA or null>","avgCOGS":"<Item 19 COGS % or null>","avgLaborPct":"<Item 19 labor % or null>","avgNetProfit":"<Item 19 net profit or null>","item19Notes":"<Item 19 key caveats 1-2 sentences>"}`, 700, settings.aiModelFDD || "claude-haiku-4-5");
        const parsed=JSON.parse(raw.replace(/```json|```/g,"").trim());
        const updated=brands.map(b=>b.id===brandId?{...b,fddData:parsed,fddUploadedAt:nowIso()}:b);
        p("ff4_brands",setBrands,updated); toast$("FDD parsed!"); setModal("fddResult"); setModalData({brandId,parsed});
      }catch{toast$("Parse error.","err");}
      setFddParsing(false);
    };
    reader.readAsDataURL(file);
  };

  const saveStages=(type,stages)=>{
    if(!activeBrand) return;
    if(type==="lead") p("ff4_lstages",setLeadStages,{...leadStages,[activeBrand]:stages});
    else p("ff4_ostages",setOppStages,{...oppStages,[activeBrand]:stages});
    setModal(null); toast$("Stages saved!");
  };

  const liveRec=selected?(selected.type==="lead"?bLeads.find(l=>l.id===selected.id):bOpps.find(o=>o.id===selected.id)):null;
  const staleOpps=bOpps.filter(o=>{const s=bOStages.find(st=>st.id===o.stage);return s?.staleDays&&daysSince(o.stageEnteredAt)>=s.staleDays;});

  // ════════════════════════════════════════════════════════
  //  INLINE VIEWS
  // ════════════════════════════════════════════════════════

  // What's Next
  const WhatsNextView=()=>{
    const urgColor=whatsNext?.urgency==="high"?"#f87171":whatsNext?.urgency==="medium"?"#facc15":"#4ade80";
    const actionIcon={call:"📞",email:"✉️",send_fdd:"📄",schedule:"📅",review:"🔍",disqualify:"🚩",other:"⚡"}[whatsNext?.action_type]||"⚡";

    // Find the named person across opps and leads for profile navigation
    const findPerson=(name)=>{
      if(!name) return null;
      const nl=name.toLowerCase().trim();
      const opp=bOpps.find(r=>`${r.firstName} ${r.lastName}`.toLowerCase()===nl);
      if(opp) return {type:"opp",id:opp.id};
      const lead=bLeads.find(r=>`${r.firstName} ${r.lastName}`.toLowerCase()===nl);
      if(lead) return {type:"lead",id:lead.id};
      return null;
    };
    const personMatch=whatsNext?.person?findPerson(whatsNext.person):null;
    const goToPerson=()=>{
      if(!personMatch) return;
      setNav(personMatch.type==="opp"?"opps":"leads");
      setSelected(personMatch);
      setSubView("detail");
    };

    return(
      <div style={{display:"flex",flexDirection:"column",alignItems:"center",justifyContent:"center",minHeight:420,gap:24}}>
        <div style={{textAlign:"center"}}>
          <div style={{fontSize:11,fontWeight:800,color:C.muted,letterSpacing:".1em",marginBottom:6}}>WHAT'S NEXT?</div>
          <div style={{fontSize:13,color:C.muted}}>Your single most important action right now</div>
        </div>
        {wnLoading&&<div style={{textAlign:"center",color:"#38bdf8",fontSize:14}}><div style={{fontSize:32,animation:"spin 1s linear infinite"}}>⟳</div><div style={{marginTop:8}}>Analyzing pipeline…</div></div>}
        {!wnLoading&&whatsNext&&(
          <div style={{background:C.panel,border:`2px solid ${urgColor}44`,borderRadius:20,padding:"32px 36px",maxWidth:520,width:"100%",textAlign:"center",boxShadow:`0 0 40px ${urgColor}11`}}>
            <div style={{fontSize:36,marginBottom:14}}>{actionIcon}</div>
            <div style={{fontSize:20,fontWeight:900,color:"#f0f6ff",lineHeight:1.4,marginBottom:12}}>{whatsNext.task}</div>
            {whatsNext.person&&(
              <div
                onClick={personMatch?goToPerson:undefined}
                title={personMatch?"Open profile":whatsNext.person}
                style={{display:"inline-flex",alignItems:"center",gap:7,background:personMatch?"#0f1f38":C.dim+"88",border:`1px solid ${personMatch?C.accent+"55":"transparent"}`,borderRadius:20,padding:"5px 16px",fontSize:13,color:personMatch?"#60a5fa":"#94a3b8",marginBottom:14,cursor:personMatch?"pointer":"default",transition:"background .15s, border-color .15s"}}
                onMouseEnter={personMatch?e=>{e.currentTarget.style.background="#162e50";e.currentTarget.style.borderColor=C.accent+"88";}:undefined}
                onMouseLeave={personMatch?e=>{e.currentTarget.style.background="#0f1f38";e.currentTarget.style.borderColor=C.accent+"55";}:undefined}
              >
                👤 {whatsNext.person}
                {personMatch&&<span style={{fontSize:11,opacity:.75,fontWeight:700}}>→ Open Profile</span>}
              </div>
            )}
            <div style={{fontSize:13,color:C.muted,lineHeight:1.6,marginBottom:20}}>{whatsNext.reason}</div>
            <div style={{display:"flex",gap:12,justifyContent:"center"}}>
              <button onClick={()=>setWhatsNext(null)} style={btn(C.dim,C.muted)}>Skip</button>
              <button onClick={fetchWhatsNext} style={btn("#0a1a30","#60a5fa")}>↻ Refresh</button>
            </div>
          </div>
        )}
        {!wnLoading&&!whatsNext&&<button onClick={fetchWhatsNext} style={{...btn("#091c09","#4ade80",true),padding:"14px 32px",fontSize:14}}>✨ What should I do right now?</button>}
        {/* Today's tasks — plain list, no AI. Always renders when there are tasks due today. */}
        {tasksDueToday.length > 0 && (
          <div style={{background:"#091420",border:"1px solid #38bdf833",borderRadius:14,padding:"16px 18px",maxWidth:520,width:"100%"}}>
            <div style={{display:"flex",alignItems:"center",gap:8,marginBottom:10}}>
              <span style={{fontSize:14}}>☀️</span>
              <div style={{fontSize:10,fontWeight:800,color:"#38bdf8",letterSpacing:".06em"}}>TODAY'S TASKS · {tasksDueToday.length} DUE</div>
            </div>
            <div style={{display:"flex",flexDirection:"column",gap:5}}>
              {tasksDueToday.slice(0,8).map(row => (
                <div key={row.task.id} style={{display:"flex",alignItems:"center",gap:8,background:"#0a1525",borderRadius:7,padding:"7px 10px"}}>
                  <button onClick={()=>toggleTask(row.recType, row.recId, row.task.id)} title="Mark complete" style={{width:15,height:15,borderRadius:4,border:`1.5px solid ${C.border}`,background:"transparent",cursor:"pointer",flexShrink:0,padding:0}}/>
                  <div style={{flex:1,fontSize:12,color:C.text,minWidth:0,overflow:"hidden",textOverflow:"ellipsis",whiteSpace:"nowrap"}}>
                    <RecordLink onClick={()=>{setNav(row.recType==="opp"?"opps":"leads"); setSelected({type:row.recType,id:row.recId}); setSubView("detail");}} color="#60a5fa" weight={700}>{row.recName}</RecordLink>
                    <span style={{color:C.muted,margin:"0 6px"}}>·</span>
                    <span style={{color:C.text}}>{row.task.text}</span>
                  </div>
                  {row.task.source === "ai" && <span title="AI-suggested" style={{fontSize:9,color:"#38bdf8",fontWeight:700}}>✨</span>}
                </div>
              ))}
              {tasksDueToday.length > 8 && <div style={{fontSize:11,color:C.dim,paddingLeft:10,marginTop:3}}>+ {tasksDueToday.length - 8} more</div>}
            </div>
          </div>
        )}
        <style>{`@keyframes spin{from{transform:rotate(0)}to{transform:rotate(360deg)}}`}</style>
      </div>
    );
  };

  // List
  const ListView=({type})=>{
    const [search,setSearch]=useState("");
    const [filterStage,setFilterStage]=useState("all");
    const recs=type==="lead"?bLeads:bOpps;
    const stages=type==="lead"?bLStages:bOStages;
    const filtered=recs.filter(r=>{
      const q=search.toLowerCase();
      return(filterStage==="all"||r.stage===filterStage)&&(!q||`${r.firstName} ${r.lastName} ${r.email} ${r.territory} ${r.company}`.toLowerCase().includes(q));
    });
    return(
      <div>
        <div style={{display:"flex",gap:10,marginBottom:16,flexWrap:"wrap",alignItems:"center"}}>
          <input value={search} onChange={e=>setSearch(e.target.value)} placeholder="Search…" style={inp({width:180})}/>
          <select value={filterStage} onChange={e=>setFilterStage(e.target.value)} style={inp({minWidth:160})}>
            <option value="all">All Stages</option>
            {stages.map(s=><option key={s.id} value={s.id}>{s.label} ({recs.filter(r=>r.stage===s.id).length})</option>)}
          </select>
          {/* "Next Up" jump button — skips the What's Next? summary tab and navigates straight
              to the AI-recommended record. Mirrors the rocket button on the sidebar and the
              Quick Action on a record profile. */}
          <button onClick={jumpToNext} disabled={jumpingToNext} title="Jump straight to the next AI-recommended lead/opp" style={{...btn("#091420","#60a5fa",true),marginLeft:"auto",opacity:jumpingToNext?0.6:1,cursor:jumpingToNext?"wait":"pointer"}}>{jumpingToNext?"⏳ Finding…":"🚀 Next Up"}</button>
        </div>
        {filtered.length===0&&<div style={{textAlign:"center",color:C.muted,padding:48}}>No records.</div>}
        <div style={{display:"flex",flexDirection:"column",gap:7}}>
          {filtered.map(rec=>{
            const stage=stages.find(s=>s.id===rec.stage)||{color:"#64748b"};
            const leadLvl = type==="lead" ? leadStaleLevel(rec) : null;
            const isStale = type==="opp" ? (stage.staleDays && daysSince(rec.stageEnteredAt)>=stage.staleDays) : (leadLvl !== null);
            const isUrgent = leadLvl === "urgent";
            const sc=type==="opp"?scores[rec.id]:null;
            const hasConflict=checkConflict(rec) && !rec.conflictDismissedAt;
            const callCount = type==="lead" ? (rec.notes||[]).filter(n => n.isCallAttempt === true).length : 0;
            const callBadgeColor = callCount === 0 ? "#64748b" : callCount >= 3 ? "#4ade80" : "#facc15";
            return(
              <div key={rec.id} onClick={()=>{setSelected({type,id:rec.id});setSubView("detail");}}
                style={{background:C.panel,border:`1px solid ${isStale?"#f8717144":C.border}`,borderLeft:`4px solid ${stage.color}`,borderRadius:12,padding:"12px 17px",display:"flex",alignItems:"center",gap:13,cursor:"pointer",transition:"background .15s"}}
                onMouseEnter={e=>e.currentTarget.style.background="#101c2e"} onMouseLeave={e=>e.currentTarget.style.background=C.panel}>
                <Ava name={`${rec.firstName} ${rec.lastName}`}/>
                {sc&&settings.showScoreRings&&<ScoreRing score={sc.total} size={40}/>}
                <div style={{flex:1,minWidth:0}}>
                  <div style={{fontWeight:700,color:C.text,fontSize:14,display:"flex",alignItems:"center",gap:7}}>
                    {rec.firstName} {rec.lastName}
                    {hasConflict&&<span style={{fontSize:10,background:"#1a0808",color:"#f87171",border:"1px solid #f8717133",borderRadius:20,padding:"1px 7px"}}>⚠️ Territory Conflict</span>}
                  </div>
                  <div style={{fontSize:12,color:C.muted}}>{[rec.email,rec.territory?`📍${rec.territory}`:null].filter(Boolean).join(" · ")}</div>
                </div>
                <div style={{display:"flex",flexDirection:"column",alignItems:"flex-end",gap:4}}>
                  <SBadge stageId={rec.stage} stages={stages}/>
                  {type==="lead" && <span title={`${callCount} call${callCount===1?"":"s"} logged · auto-advances at 1 (Contacted) and 3 (Nurturing)`} style={{fontSize:10,color:callBadgeColor,fontWeight:700,letterSpacing:".02em"}}>📞 {Math.min(callCount,3)}/3{callCount>3?` (+${callCount-3})`:""}</span>}
                  {isStale&&settings.showStaleBadges&&(isUrgent
                    ? <span style={{fontSize:10,background:"#000",color:"#ff4d6b",border:"1px solid #ef4444",borderRadius:5,padding:"1px 7px",fontWeight:800,boxShadow:"0 0 8px #ef444466"}}>🚨 {daysSince(lastContactDate(rec))}d silent</span>
                    : <span style={{fontSize:10,color:"#fca5a5"}}>⚠️ {daysSince(type==="opp"?rec.stageEnteredAt:lastContactDate(rec))}d stale</span>)}
                </div>
              </div>
            );
          })}
        </div>
      </div>
    );
  };

  // Kanban
  const KanbanView=({type})=>{
    const stages=type==="lead"?bLStages:bOStages;
    const recs=type==="lead"?bLeads:bOpps;
    const [draggingId, setDraggingId] = useState(null);
    const [hoverStageId, setHoverStageId] = useState(null);
    const handleColumnDragOver = (e, stageId) => {
      e.preventDefault();
      e.dataTransfer.dropEffect = "move";
      if (hoverStageId !== stageId) setHoverStageId(stageId);
    };
    const handleDrop = (e, stageId) => {
      e.preventDefault();
      const recId = e.dataTransfer.getData("text/plain");
      setHoverStageId(null);
      setDraggingId(null);
      if (!recId) return;
      const rec = recs.find(r => r.id === recId);
      if (!rec || rec.stage === stageId) return;
      requestStageChange(type, recId, stageId);
    };
    return(
      <div style={{overflowX:"auto",paddingBottom:16}}>
        <div style={{display:"flex",gap:11,minWidth:"max-content"}}>
          {stages.map(stage=>{
            const cards=type==="opp"?(sortedOppsByStage[stage.id]||[]):recs.filter(r=>r.stage===stage.id);
            const isHover = hoverStageId === stage.id;
            const isDragging = !!draggingId;
            const isValidTarget = isHover && draggingId && recs.find(r=>r.id===draggingId)?.stage !== stage.id;
            return(
              <div key={stage.id}
                onDragOver={(e)=>handleColumnDragOver(e, stage.id)}
                onDragLeave={(e)=>{ if (e.currentTarget === e.target) setHoverStageId(null); }}
                onDrop={(e)=>handleDrop(e, stage.id)}
                style={{width:210,background:isValidTarget?"#162035":C.panel,border:`${isValidTarget?2:1}px ${isDragging?"dashed":"solid"} ${isValidTarget?stage.color:stage.color+"33"}`,borderRadius:14,padding:"12px 9px",flexShrink:0,transition:"background .15s, border-color .15s"}}>
                <div style={{display:"flex",alignItems:"center",gap:7,marginBottom:10}}>
                  <div style={{width:6,height:24,borderRadius:3,background:stage.color}}/>
                  <div>
                    <div style={{fontSize:10,fontWeight:800,color:stage.color,letterSpacing:".06em",textTransform:"uppercase"}}>{stage.label}</div>
                    <div style={{fontSize:10,color:C.muted}}>{cards.length}</div>
                  </div>
                </div>
                <div style={{display:"flex",flexDirection:"column",gap:7,minHeight:isDragging?60:"auto"}}>
                  {cards.map((rec,idx)=>{
                    const sc=type==="opp"?scores[rec.id]:null;
                    const isScoring=scoringIds.has(rec.id);
                    const leadLvl = type==="lead" ? leadStaleLevel(rec) : null;
                    const isStale = type==="opp" ? (stage.staleDays && daysSince(rec.stageEnteredAt)>=stage.staleDays) : (leadLvl !== null);
                    const isUrgent = leadLvl === "urgent";
                    const hasConflict = checkConflict(rec) && !rec.conflictDismissedAt;
                    const isDragged = draggingId === rec.id;
                    const lostReasonObj = rec.lostReason ? LOST_REASONS.find(r=>r.id===rec.lostReason) : null;
                    const callCount = type==="lead" ? (rec.notes||[]).filter(n => n.isCallAttempt === true).length : 0;
                    const callBadgeColor = callCount === 0 ? "#64748b" : callCount >= 3 ? "#4ade80" : "#facc15";
                    return(
                      <div key={rec.id}
                        draggable
                        onDragStart={(e)=>{ e.dataTransfer.setData("text/plain", rec.id); e.dataTransfer.effectAllowed = "move"; setDraggingId(rec.id); }}
                        onDragEnd={()=>{ setDraggingId(null); setHoverStageId(null); }}
                        onClick={()=>{ if(!draggingId) { setSelected({type,id:rec.id}); setSubView("detail"); } }}
                        title="Drag to move · click to open"
                        style={{background:isStale?"#1a0a08":"#090f1c",border:`1px solid ${isStale?"#f8717144":C.border}`,borderLeft:`3px solid ${stage.color}`,borderRadius:9,padding:"9px 11px",cursor:isDragged?"grabbing":"grab",opacity:isDragged?0.4:1,transition:"opacity .12s"}}>
                        <div style={{display:"flex",alignItems:"center",gap:7}}>
                          {type==="opp"&&settings.showScoreRings&&(isScoring?<div style={{width:36,height:36,borderRadius:"50%",border:`2px solid ${C.dim}`,display:"flex",alignItems:"center",justifyContent:"center",fontSize:11,color:C.muted}}>⟳</div>:<ScoreRing score={sc?.total} size={36}/>)}
                          <div>
                            <div style={{fontSize:12,fontWeight:700,color:C.text}}>{rec.firstName} {rec.lastName}</div>
                            {rec.territory&&<div style={{fontSize:10,color:C.muted}}>📍{rec.territory}</div>}
                            {type==="lead" && <div title={`${callCount} call${callCount===1?"":"s"} logged`} style={{fontSize:10,color:callBadgeColor,fontWeight:700,marginTop:1}}>📞 {Math.min(callCount,3)}/3{callCount>3?` (+${callCount-3})`:""}</div>}
                            {isStale&&settings.showStaleBadges&&(isUrgent
                              ? <div style={{display:"inline-block",marginTop:3,fontSize:10,background:"#000",color:"#ff4d6b",border:"1px solid #ef4444",borderRadius:5,padding:"1px 6px",fontWeight:800,boxShadow:"0 0 8px #ef444466"}}>🚨 {daysSince(lastContactDate(rec))}d silent</div>
                              : <div style={{fontSize:10,color:"#fca5a5"}}>⚠️ {daysSince(type==="opp"?rec.stageEnteredAt:lastContactDate(rec))}d</div>)}
                            {hasConflict&&<div style={{fontSize:10,color:"#f87171"}}>⚠️ Territory conflict</div>}
                            {sc?.flag&&<div style={{fontSize:10,color:"#fb923c"}}>🚩{sc.flag.slice(0,28)}</div>}
                            {lostReasonObj&&<div style={{display:"inline-block",marginTop:3,fontSize:9,background:lostReasonObj.color+"22",color:lostReasonObj.color,border:`1px solid ${lostReasonObj.color}44`,borderRadius:5,padding:"1px 6px",fontWeight:700}}>{lostReasonObj.icon} {lostReasonObj.label}</div>}
                          </div>
                        </div>
                        {idx===0&&sc&&<div style={{marginTop:5,fontSize:9,color:C.muted,fontWeight:700}}>▲ HIGHEST PRIORITY</div>}
                      </div>
                    );
                  })}
                  {cards.length===0&&<div style={{textAlign:"center",color:isValidTarget?stage.color:C.dim,fontSize:11,padding:"14px 0",fontWeight:isValidTarget?800:400}}>{isValidTarget?"Drop here":"Empty"}</div>}
                </div>
              </div>
            );
          })}
        </div>
        <style>{`@keyframes spin{from{transform:rotate(0)}to{transform:rotate(360deg)}}`}</style>
      </div>
    );
  };

  // Detail
  const DetailView=()=>{
    if(!liveRec) return null;
    const rec=liveRec; const type=selected.type;
    const stages=type==="lead"?bLStages:bOStages;
    const sc=type==="opp"?scores[rec.id]:null;
    const isScoring=scoringIds.has(rec.id);
    const allEmails=[rec.email,...(rec.partners||[]).map(p=>p.email)].filter(Boolean);
    const allPhones=[rec.phone,...(rec.partners||[]).map(p=>p.phone)].filter(Boolean);
    const stage=stages.find(s=>s.id===rec.stage)||{};
    const isStale=stage.staleDays&&daysSince(rec.stageEnteredAt)>=stage.staleDays;
    const hasConflict=checkConflict(rec);
    return(
      <div style={{display:"flex",gap:18,flexWrap:"wrap"}}>
        <div style={{flex:"2 1 340px",display:"flex",flexDirection:"column",gap:13}}>
          {/* Header */}
          <div style={{background:C.panel,border:`1px solid ${C.border}`,borderRadius:14,padding:"18px 20px"}}>
            <div style={{display:"flex",alignItems:"center",gap:13,marginBottom:13}}>
              <Ava name={`${rec.firstName} ${rec.lastName}`} size={48}/>
              {type==="opp"&&(isScoring?<div style={{fontSize:11,color:C.muted}}>Scoring…</div>:sc?<ScoreRing score={sc.total} size={52}/>:<button onClick={()=>scoreOpp(rec)} style={btn(C.dim,C.muted)}>Score</button>)}
              <div style={{flex:1}}>
                <h2 style={{margin:0,color:"#f0f6ff",fontSize:20,fontWeight:900}}>{rec.firstName} {rec.lastName}</h2>
                {rec.company&&<div style={{color:C.muted,fontSize:12}}>{rec.company}</div>}
                {sc?.summary&&<div style={{fontSize:12,color:"#94a3b8",marginTop:4,lineHeight:1.4}}>{sc.summary}</div>}
              </div>
              <div style={{display:"flex",gap:8}}>
                {type==="lead"&&!rec.convertedToOpp&&<button onClick={()=>convertToOpp(rec)} style={btn("#091c09","#4ade80",true)}>⬆ Opp</button>}
                {type==="opp"&&hasDocusignKey&&(rec.commsBlocked
                  ? <button disabled title="Communications are blocked for this contact." style={{...btn(C.dim,C.muted),opacity:0.55,cursor:"not-allowed"}}>✍️ Send via DocuSign</button>
                  : <button onClick={()=>setModal("sendDocusign")} style={btn("#1a1908","#facc15",true)} title="Send FDD or Agreement via DocuSign">✍️ Send via DocuSign</button>)}
                <button onClick={()=>{setModal("editRecord");setModalData({type,rec:{...rec}});}} title="Edit record" style={btn(C.dim,C.muted)}>✏️</button>
                <button onClick={()=>{setModal("confirmDelete");setModalData({type,rec});}} style={btn("#1a0808","#f87171")} title={`Delete this ${type==="opp"?"opportunity":"lead"}`}>🗑</button>
              </div>
            </div>
            {hasConflict&&!rec.conflictDismissedAt&&(
              <div style={{background:"#1a0808",border:"1px solid #f8717133",borderRadius:8,padding:"8px 8px 8px 12px",fontSize:12,color:"#fca5a5",marginBottom:12,display:"flex",alignItems:"center",gap:8}}>
                <span style={{flex:1}}>⚠️ <strong>Territory Conflict:</strong> "{rec.territory}" overlaps with an existing territory in your map.</span>
                <button onClick={()=>dismissConflict(type, rec.id)} title="Dismiss" style={{background:"transparent",border:"1px solid #f8717133",color:"#fca5a5",fontSize:12,cursor:"pointer",padding:"2px 8px",borderRadius:5,fontFamily:"inherit"}}>✕ Dismiss</button>
              </div>
            )}
            {type==="opp"&&isStale&&rec.stage!=="closed_lost"&&<div style={{background:"#1a0808",border:"1px solid #f8717133",borderRadius:8,padding:"8px 12px",fontSize:12,color:"#fca5a5",marginBottom:12}}>⚠️ Stale: {daysSince(rec.stageEnteredAt)} days in {stage.label}</div>}
            {type==="opp"&&rec.stage==="closed_lost"&&(()=>{
              const lostObj = LOST_REASONS.find(r => r.id === rec.lostReason);
              return (
                <div style={{background:"#0f0c14",border:`1px solid ${(lostObj?.color||"#64748b")}55`,borderRadius:10,padding:"11px 14px",marginBottom:12}}>
                  <div style={{display:"flex",alignItems:"center",gap:11,flexWrap:"wrap"}}>
                    <span style={{fontSize:22}}>{lostObj?.icon||"❌"}</span>
                    <div style={{flex:1,minWidth:0}}>
                      <div style={{fontSize:9,fontWeight:800,color:C.dim,letterSpacing:".06em"}}>CLOSED LOST · STATUS</div>
                      <div style={{display:"flex",alignItems:"center",gap:8,marginTop:3}}>
                        <select value={rec.lostReason||""} onChange={(e)=>{const v=e.target.value||null; const next=bOpps.map(o=>o.id===rec.id?{...o,lostReason:v,updatedAt:nowIso()}:o); p("ff4_opps",setOpps,{...opps,[activeBrand]:next});}} style={{background:(lostObj?.color||"#64748b")+"22",border:`1.5px solid ${(lostObj?.color||"#64748b")}66`,borderRadius:6,padding:"4px 9px",color:lostObj?.color||C.text,fontFamily:"inherit",fontSize:13,fontWeight:800,cursor:"pointer"}}>
                          <option value="">— pick status —</option>
                          {LOST_REASONS.map(r => <option key={r.id} value={r.id}>{r.label}</option>)}
                        </select>
                        {lostObj && <span style={{fontSize:11,color:C.muted,fontStyle:"italic"}}>{lostObj.desc}</span>}
                      </div>
                      {rec.lostNotes && <div style={{fontSize:12,color:C.muted,marginTop:7,padding:"6px 9px",background:"#090f1c",borderRadius:6,lineHeight:1.45}}>{rec.lostNotes}</div>}
                    </div>
                  </div>
                </div>
              );
            })()}
            {type==="lead"&&(()=>{
              const lvl = leadStaleLevel(rec);
              if (!lvl) return null;
              const days = daysSince(lastContactDate(rec));
              if (lvl === "urgent") return (
                <div style={{background:"#000",border:"1.5px solid #ef4444",borderRadius:8,padding:"11px 14px",marginBottom:12,boxShadow:"0 0 18px #ef444466",display:"flex",alignItems:"center",gap:11}}>
                  <span style={{fontSize:22}}>🚨</span>
                  <div style={{flex:1}}>
                    <div style={{fontSize:13,fontWeight:900,color:"#ff4d6b",letterSpacing:".03em"}}>RED FLAG — {days} DAYS SINCE LAST COMMUNICATION</div>
                    <div style={{fontSize:11,color:"#fca5a5",marginTop:2}}>This lead has gone dark. Reach out today or formally drop them — letting it sit further hurts your conversion rate.</div>
                  </div>
                </div>
              );
              return (
                <div style={{background:"#1a0808",border:"1px solid #f8717133",borderRadius:8,padding:"8px 12px",fontSize:12,color:"#fca5a5",marginBottom:12}}>⚠️ Stale: {days} days since last communication with this lead.</div>
              );
            })()}
            <div style={{display:"grid",gridTemplateColumns:"1fr 1fr",gap:8}}>
              {/* Tile row. Email + phone show an inline ⚠️ when the value looks malformed.
                  Net worth and liquidity replaced the legacy single "Investment" tile;
                  legacy records still show `investmentLevel` as a fallback so existing
                  data isn't hidden until the rep edits the record. */}
              {[
                ["✉️","Email",     rec.email,                                 `mailto:${rec.email}`,                  validateEmail(rec.email)],
                ["📱","Phone",     rec.phone,                                 `tel:${rec.phone}`,                     validatePhone(rec.phone)],
                ["📍","Territory", rec.territory,                             null,                                    null],
                ["💵","Net Worth", rec.netWorth || (!rec.liquidity && rec.investmentLevel ? rec.investmentLevel : ""), null, null],
                ["💧","Liquidity", rec.liquidity,                             null,                                    null],
                ["🔗","Source",    rec.source,                                null,                                    null],
                ["👤","Assigned",  rec.assignedTo,                            null,                                    null],
              ].filter(([,,v])=>v).map(([icon,lbl,val,href,issue])=>(
                <div key={lbl} style={{background:"#090f1c",borderRadius:7,padding:"7px 11px",border: issue?`1px solid #fb923c55`:`1px solid transparent`}}>
                  <div style={{fontSize:10,color:C.dim,marginBottom:2,fontWeight:700,display:"flex",alignItems:"center",gap:5}}>
                    <span>{icon} {lbl}</span>
                    {issue && <span title={`This ${lbl.toLowerCase()} looks invalid: ${issue}`} style={{fontSize:11,color:"#fb923c",cursor:"help",marginLeft:"auto"}}>⚠️</span>}
                  </div>
                  {href?<a href={href} style={{fontSize:12,color:issue?"#fb923c":"#60a5fa",textDecoration:"none"}}>{val}</a>:<div style={{fontSize:12,color:C.text}}>{val}</div>}
                </div>
              ))}
            </div>
          </div>

          {/* Tasks — manual + AI-suggested next-actions for this record */}
          <TasksPanel type={type} rec={rec} onAdd={addTask} onToggle={toggleTask} onDelete={deleteTask}/>

          {/* Call Tracker — leads only */}
          {type==="lead"&&(()=>{
            const callCount = (rec.notes||[]).filter(n => n.isCallAttempt === true).length;
            const cap = Math.min(callCount, 3);
            const overflow = Math.max(0, callCount - 3);
            const status = callCount === 0
              ? { color: C.muted, msg: "No calls logged yet — the first call moves them to Contacted." }
              : callCount === 1
                ? { color: "#facc15", msg: "1st call done. 2 more recommended before nurturing." }
                : callCount === 2
                  ? { color: "#facc15", msg: "2 calls in. One more recommended before nurturing." }
                  : { color: "#4ade80", msg: callCount > 3 ? `${callCount} calls logged — well past the 3-call mark.` : "3 calls completed — ready for Nurturing." };
            return (
              <div style={{background:C.panel,border:`1px solid ${C.border}`,borderRadius:14,padding:"14px 18px"}}>
                <div style={{display:"flex",alignItems:"center",gap:10,marginBottom:11}}>
                  <Sec style={{margin:0}}>📞 Call Tracker</Sec>
                  <div style={{marginLeft:"auto",fontSize:11,color:C.dim}}>Detected automatically from your notes</div>
                </div>
                <div style={{display:"flex",alignItems:"center",gap:10,marginBottom:8}}>
                  {[0,1,2].map(i => {
                    const filled = i < cap;
                    return (
                      <div key={i} style={{
                        width: 38, height: 38, borderRadius: "50%",
                        background: filled ? "#4ade8022" : "#0a1422",
                        border: `2px solid ${filled ? "#4ade80" : C.border}`,
                        display: "flex", alignItems: "center", justifyContent: "center",
                        fontSize: 16, color: filled ? "#4ade80" : C.dim,
                      }}>📞</div>
                    );
                  })}
                  <div style={{marginLeft:8}}>
                    <div style={{fontSize:22,fontWeight:900,color:status.color,lineHeight:1}}>{Math.min(callCount,3)}<span style={{fontSize:14,fontWeight:700,color:C.muted}}> / 3</span>{overflow>0 && <span style={{fontSize:11,color:C.muted,marginLeft:5,fontWeight:600}}>(+{overflow})</span>}</div>
                    <div style={{fontSize:11,color:C.muted,marginTop:3}}>calls logged</div>
                  </div>
                </div>
                <div style={{fontSize:12,color:status.color,lineHeight:1.5}}>{status.msg}</div>
              </div>
            );
          })()}

          {/* AI Score */}
          {type==="opp"&&sc&&(()=>{
            const total = sc.total||0;
            const color = total>=(settings.highScoreThreshold||75)?"#4ade80":total>=50?"#facc15":total>=(settings.lowScoreThreshold||30)?"#fb923c":"#f87171";
            return (
              <div style={{background:C.panel,border:`1px solid ${C.border}`,borderRadius:14,padding:"14px 18px"}}>
                <div style={{display:"flex",alignItems:"center",gap:12}}>
                  <Sec style={{margin:0}}>AI Priority Score</Sec>
                  <div style={{marginLeft:"auto",fontSize:10,color:C.dim}}>Scored {fmtDate(sc.scoredAt)}</div>
                  <button onClick={()=>scoreOpp(rec)} title="Re-score opportunity" style={{...btn(C.dim,C.muted),padding:"3px 9px",fontSize:10}}>↻</button>
                </div>
                <div style={{display:"flex",alignItems:"center",gap:13,marginTop:11}}>
                  <ScoreRing score={total} size={56}/>
                  <div style={{flex:1,minWidth:0}}>
                    <div style={{fontSize:24,fontWeight:900,color}}>{total}/100</div>
                    {sc.summary&&<div style={{fontSize:12,color:"#94a3b8",lineHeight:1.5,marginTop:2}}>{sc.summary}</div>}
                  </div>
                </div>
                {sc.flag&&<div style={{marginTop:11,background:"#1a0d08",border:"1px solid #fb923c44",borderRadius:8,padding:"7px 12px",fontSize:12,color:"#fb923c"}}>🚩 {sc.flag}</div>}
              </div>
            );
          })()}

          {/* Stage mover */}
          <div style={{background:C.panel,border:`1px solid ${C.border}`,borderRadius:14,padding:"14px 16px"}}>
            <Sec>Pipeline Stage</Sec>
            <div style={{display:"flex",gap:5,flexWrap:"wrap"}}>
              {(type==="lead"?bLStages:bOStages).map(s=>(
                <button key={s.id} onClick={()=>requestStageChange(type,rec.id,s.id)} style={{background:rec.stage===s.id?s.color+"33":"#090f1c",border:`1.5px solid ${rec.stage===s.id?s.color:C.border}`,borderRadius:7,padding:"4px 9px",cursor:"pointer",color:rec.stage===s.id?s.color:C.muted,fontSize:11,fontWeight:700}}>{s.label}</button>
              ))}
            </div>
          </div>

          {/* Partners */}
          <div style={{background:C.panel,border:`1px solid ${C.border}`,borderRadius:14,padding:"14px 16px"}}>
            <div style={{display:"flex",alignItems:"center",gap:10,marginBottom:11}}>
              <Sec style={{margin:0}}>Business Partners</Sec>
              <button onClick={()=>{setModal("newPartner");setModalData({type,id:rec.id});}} style={btn("#090f1c","#60a5fa")}>+ Add</button>
            </div>
            {(rec.partners||[]).length===0&&<div style={{color:C.dim,fontSize:12}}>No partners.</div>}
            {(rec.partners||[]).map(p=>(
              <div key={p.id} style={{display:"flex",alignItems:"center",gap:9,background:"#090f1c",borderRadius:9,padding:"9px 12px",marginBottom:7}}>
                <Ava name={`${p.firstName} ${p.lastName}`} size={30}/>
                <div style={{flex:1}}><div style={{fontSize:13,fontWeight:700,color:C.text}}>{p.firstName} {p.lastName}{p.role&&<span style={{marginLeft:7,fontSize:10,background:C.dim,borderRadius:20,padding:"1px 8px",color:"#94a3b8"}}>{p.role}</span>}</div><div style={{fontSize:11,color:C.muted}}>{[p.email,p.phone].filter(Boolean).join(" · ")}</div></div>
                <button onClick={()=>removePartner(type,rec.id,p.id)} title="Remove partner" style={btn("#1a0808","#f87171")}>×</button>
              </div>
            ))}
          </div>

          {/* Broker (opps only, hidden unless assigned OR brokers exist to assign) */}
          {type==="opp" && (() => {
            const assignedBroker = rec.brokerId ? bBrokers.find(b => b.id === rec.brokerId) : null;
            if (!assignedBroker && bBrokers.length === 0) return null; // no brokers exist anywhere — hide entirely
            if (!assignedBroker) {
              // Show only the Assign control inline
              return (
                <div style={{background:C.panel,border:`1px dashed ${C.border}`,borderRadius:14,padding:"12px 16px",display:"flex",alignItems:"center",gap:10}}>
                  <span style={{fontSize:18,opacity:.6}}>🤝</span>
                  <span style={{fontSize:12,color:C.muted,flex:1}}>No broker assigned to this opportunity.</span>
                  <select defaultValue="" onChange={e=>{ if(e.target.value) assignBrokerToOpp(rec.id, e.target.value); }} style={inp({fontSize:11,padding:"5px 9px"})}>
                    <option value="">Assign broker…</option>
                    {bBrokers.map(b=>{
                      const n = bNetworks.find(x=>x.id===b.networkId);
                      return <option key={b.id} value={b.id}>{b.firstName} {b.lastName}{n?` (${n.name})`:""}</option>;
                    })}
                  </select>
                </div>
              );
            }
            // Full broker panel — only mounted when broker is assigned
            const net = bNetworks.find(n => n.id === assignedBroker.networkId);
            const oppComms = (brokerComms[rec.id]||[]).filter(m => m.brokerId === assignedBroker.id);
            return (
              <div style={{background:C.panel,border:`1px solid ${C.border}`,borderRadius:14,padding:"14px 16px"}}>
                <div style={{display:"flex",alignItems:"center",gap:10,marginBottom:12}}>
                  <Sec style={{margin:0}}>Assigned Broker</Sec>
                  <button onClick={()=>{ if(confirm(`Unassign ${assignedBroker.firstName} ${assignedBroker.lastName} from this opportunity?`)) unassignBrokerFromOpp(rec.id); }} style={{...btn(C.dim,C.muted),fontSize:10,padding:"3px 9px",marginLeft:"auto"}}>Unassign</button>
                </div>
                <div style={{display:"flex",alignItems:"center",gap:11,background:"#090f1c",borderRadius:10,padding:"10px 13px",marginBottom:10}}>
                  <Ava name={`${assignedBroker.firstName} ${assignedBroker.lastName}`} size={40}/>
                  <div style={{flex:1,minWidth:0}}>
                    <div style={{display:"flex",alignItems:"center",gap:7,marginBottom:3,flexWrap:"wrap"}}>
                      <div style={{fontSize:14,fontWeight:800,color:C.text}}><RecordLink onClick={()=>openBroker(assignedBroker.id)} color={C.text} weight={800}>{assignedBroker.firstName} {assignedBroker.lastName}</RecordLink></div>
                      {net && <RecordLink onClick={()=>openNetwork(net.id)} color={net.color||"#94a3b8"} weight={700}><span style={{background:(net.color||"#1e3a5f")+"22",border:`1px solid ${(net.color||"#1e3a5f")}44`,borderRadius:5,padding:"1px 7px",fontSize:10,fontWeight:700}}>{net.name}</span></RecordLink>}
                      {assignedBroker.specialty && <span style={{background:"#1a1908",color:"#facc15",border:"1px solid #facc1533",borderRadius:5,padding:"1px 7px",fontSize:10,fontWeight:700}}>{assignedBroker.specialty}</span>}
                    </div>
                    <div style={{fontSize:11,color:C.muted}}>{[assignedBroker.email,assignedBroker.phone].filter(Boolean).join(" · ")||<em>no contact info</em>}</div>
                    {assignedBroker.commission && <div style={{fontSize:11,color:C.dim,marginTop:2}}>💰 {assignedBroker.commission}</div>}
                  </div>
                  <div style={{display:"flex",gap:6}}>
                    {assignedBroker.email && <a href={`mailto:${assignedBroker.email}`} style={{...btn("#091420","#60a5fa"),textDecoration:"none"}}>✉️</a>}
                    {assignedBroker.phone && <a href={`tel:${assignedBroker.phone}`}   style={{...btn("#091c09","#4ade80"),textDecoration:"none"}}>📞</a>}
                  </div>
                </div>
                {/* Conversation feed scoped to this opp + broker */}
                <Sec>Communications ({oppComms.length})</Sec>
                <div style={{maxHeight:240,overflowY:"auto",marginBottom:4}}>
                  {oppComms.length===0 && <div style={{color:C.dim,fontSize:11,padding:"6px 0"}}>No messages yet — start the conversation below.</div>}
                  {oppComms.slice().sort((a,b)=>new Date(a.at)-new Date(b.at)).map(m=>(
                    <div key={m.id} style={{background:"#090f1c",border:`1px solid ${C.border}`,borderRadius:8,padding:"8px 12px",marginBottom:6}}>
                      <div style={{display:"flex",gap:7,alignItems:"center",marginBottom:3}}>
                        <span style={{fontSize:13}}>{m.type==="email"?"✉️":m.type==="sms"?"💬":m.type==="call"?"📞":"📝"}</span>
                        <strong style={{fontSize:11,color:C.text,textTransform:"uppercase",letterSpacing:".05em"}}>{m.type}</strong>
                        <span style={{fontSize:10,color:C.dim,marginLeft:"auto"}}>{fmtDate(m.at)} · {fmtTime(m.at)}</span>
                      </div>
                      {m.subject && <div style={{fontSize:12,color:C.text,marginBottom:2}}><strong style={{color:C.dim,fontWeight:600}}>Subject:</strong> {m.subject}</div>}
                      <div style={{fontSize:12,color:"#c8d8ef",whiteSpace:"pre-wrap",lineHeight:1.5}}>{m.body}</div>
                    </div>
                  ))}
                </div>
                <BrokerComposeBox broker={assignedBroker} onSend={(msg)=>addBrokerComm(rec.id, {...msg, brokerId: assignedBroker.id})}/>
              </div>
            );
          })()}

          {/* Communications blocked banner — shown when this person should not be contacted. */}
          {rec.commsBlocked && (
            <div style={{background:"#1a0808",border:"1.5px solid #ef4444",borderRadius:12,padding:"11px 14px",display:"flex",alignItems:"center",gap:11}}>
              <span style={{fontSize:20,lineHeight:1}}>🚫</span>
              <div style={{flex:1}}>
                <div style={{fontSize:13,fontWeight:800,color:"#ff4d6b",letterSpacing:".02em"}}>Communications blocked</div>
                <div style={{fontSize:11,color:"#fca5a5",marginTop:3,lineHeight:1.5}}>This contact should not receive emails, texts, or calls.{rec.commsBlockedReason ? ` Reason: ${rec.commsBlockedReason}.` : ""}{rec.commsBlockedAt ? ` (${new Date(rec.commsBlockedAt).toLocaleDateString("en-US",{month:"short",day:"numeric",year:"numeric"})})` : ""}</div>
              </div>
            </div>
          )}

          {/* Actions */}
          <div style={{background:C.panel,border:`1px solid ${C.border}`,borderRadius:14,padding:"14px 16px"}}>
            <Sec>Quick Actions</Sec>
            <div style={{display:"flex",gap:8,flexWrap:"wrap"}}>
              {(() => {
                // When comms are blocked, outbound channels render as disabled buttons (gray, not-allowed cursor)
                // so the rep can SEE they exist but can't accidentally fire one off. AI / scoring / scheduling
                // tools that don't reach the contact stay enabled.
                const blocked = !!rec.commsBlocked;
                const blockedTitle = "Communications are blocked for this contact. Uncheck the box below to re-enable.";
                const disabledStyle = { ...btn(C.dim, C.muted), opacity: 0.55, cursor: "not-allowed", textDecoration: "none" };
                return <>
                  {allPhones.length>0 && (
                    blocked
                      ? <>
                          <button disabled title={blockedTitle} style={disabledStyle}>📞 Call</button>
                          <button disabled title={blockedTitle} style={disabledStyle}>💬 Text All</button>
                        </>
                      : <>
                          <a href={`tel:${allPhones[0]}`} style={{...btn("#091c09","#4ade80"),textDecoration:"none"}}>📞 Call</a>
                          <a href={`sms:${allPhones.join(",")}`} style={{...btn("#091420","#60a5fa"),textDecoration:"none"}}>💬 Text All</a>
                        </>
                  )}
                  {allEmails.length>0 && (
                    blocked
                      ? <button disabled title={blockedTitle} style={disabledStyle}>✉️ Email All</button>
                      : <a href={`mailto:${allEmails.join(",")}`} style={{...btn("#160f30","#a78bfa"),textDecoration:"none"}}>✉️ Email All</a>
                  )}
                  {blocked
                    ? <button disabled title={blockedTitle} style={disabledStyle}>📋 Templates</button>
                    : <button onClick={()=>{setModal("template");setModalData({rec});}} style={btn("#1a1908","#facc15")}>📋 Templates</button>}
                  {blocked
                    ? <button disabled title={blockedTitle} style={disabledStyle}>📅 Schedule Meeting</button>
                    : <button onClick={()=>{setModal("bookMeeting");setModalData({rec, type});}} style={btn("#0f1a14","#34d399",true)} title="Schedule a meeting with this candidate">📅 Schedule Meeting</button>}
                  <button onClick={()=>runAiSummary(rec)} style={btn("#091420","#38bdf8",true)}>✨ AI Summary</button>
                  {type==="opp"&&<button onClick={()=>scoreOpp(rec)} style={btn(C.dim,C.muted)}>↻ Re-score</button>}
                  {/* Jump straight to the AI's next recommended record — same as the sidebar
                      rocket button and the list-page Next button. Lets a rep "next, please"
                      their way through the day's most urgent records. */}
                  <button onClick={jumpToNext} disabled={jumpingToNext} title="Jump straight to the next AI-recommended lead/opp" style={{...btn("#091420","#60a5fa",true),opacity:jumpingToNext?0.6:1,cursor:jumpingToNext?"wait":"pointer"}}>{jumpingToNext?"⏳ Finding…":"🚀 Next Up"}</button>
                </>;
              })()}
            </div>
            {/* Manual block-comms toggle. Auto-fires on lead→Disqualified and opp→Closed Lost (confirmed lost). */}
            <label style={{display:"flex",alignItems:"center",gap:9,marginTop:12,padding:"9px 11px",background:rec.commsBlocked?"#1a0808":"#090f1c",border:`1px solid ${rec.commsBlocked?"#ef4444":C.border}`,borderRadius:9,cursor:"pointer"}}>
              <input type="checkbox" checked={!!rec.commsBlocked} onChange={()=>toggleBlockComms(type, rec.id)} style={{accentColor:"#ef4444",width:16,height:16,cursor:"pointer"}}/>
              <span style={{flex:1,fontSize:12,fontWeight:700,color:rec.commsBlocked?"#fca5a5":C.text}}>Block all communications</span>
              <span style={{fontSize:10,color:C.muted,fontWeight:500}}>{rec.commsBlocked ? "Disables email, SMS, call, templates, and scheduling for this contact." : "Mark this person as do-not-contact."}</span>
            </label>
          </div>

          {/* Notes */}
          <div style={{background:C.panel,border:`1px solid ${C.border}`,borderRadius:14,padding:"14px 16px"}}>
            <Sec>Notes</Sec>
            <NoteInput onSave={text=>addNote(type,rec.id,text)}/>
            {(rec.notes||[]).slice().reverse().map(n=>{
              const isTouch = n.isTouchpoint === true;
              return (
                <div key={n.id} style={{background:"#090f1c",border:`1px solid ${isTouch?"#4ade8033":C.border}`,borderRadius:8,padding:"9px 13px",marginBottom:7}}>
                  <div style={{fontSize:13,color:"#c8d8ef",lineHeight:1.6,whiteSpace:"pre-wrap"}}>{n.text}</div>
                  <div style={{fontSize:10,color:C.dim,marginTop:4,display:"flex",alignItems:"center",gap:8}}>
                    <span>{fmtDate(n.at)} · {fmtTime(n.at)}</span>
                    <button
                      onClick={()=>toggleNoteTouchpoint(type, rec.id, n.id)}
                      title={isTouch ? "Tagged as a real touchpoint — counts toward last-contact date. Click to mark as internal." : "Internal note — does not count toward last-contact date. Click to mark as a touchpoint."}
                      style={{background:isTouch?"#091c09":"transparent",border:`1px solid ${isTouch?"#4ade8055":C.border}`,borderRadius:5,padding:"1px 7px",fontSize:10,cursor:"pointer",color:isTouch?"#4ade80":C.muted,fontWeight:isTouch?700:500,fontFamily:"inherit",marginLeft:"auto"}}>
                      {isTouch ? "📞 Touchpoint" : "🗒 Internal"}
                    </button>
                  </div>
                </div>
              );
            })}
            {(rec.notes||[]).length===0&&<div style={{color:C.dim,fontSize:12}}>No notes yet.</div>}
          </div>

          {/* DocuSign — opps only, anchored at the bottom of the profile */}
          {type==="opp" && (
            <DocuSignSection
              rec={rec}
              isConnected={hasDocusignKey}
              onSend={(docType)=>{ setModalData({...modalData, prefillDocType: docType}); setModal("sendDocusign"); }}
              onUpdateEnvelope={(envId, patch)=>updateDocusignEnvelope(rec.id, envId, patch)}
              onVoidEnvelope={(envId)=>voidDocusignEnvelope(rec.id, envId)}
              onOpenSettings={()=>{ setNav("settings"); }}
            />
          )}
        </div>
        <div style={{flex:"1 1 210px"}}>
          <div style={{background:C.panel,border:`1px solid ${C.border}`,borderRadius:14,padding:"14px 16px",position:"sticky",top:0}}>
            <Sec>Activity</Sec>
            {(rec.activities||[]).slice().reverse().map(a=>(
              <div key={a.id} style={{display:"flex",gap:9,alignItems:"flex-start",marginBottom:9}}>
                <div style={{width:7,height:7,borderRadius:"50%",background:a.type==="stage"?"#3b82f6":a.type==="note"?"#a78bfa":"#4ade80",marginTop:5,flexShrink:0}}/>
                <div><div style={{fontSize:12,color:"#94a3b8"}}>{a.text}</div><div style={{fontSize:10,color:C.dim}}>{fmtDate(a.at)}</div></div>
              </div>
            ))}
            <div style={{display:"flex",gap:9,alignItems:"flex-start"}}>
              <div style={{width:7,height:7,borderRadius:"50%",background:C.dim,marginTop:5,flexShrink:0}}/>
              <div><div style={{fontSize:12,color:"#94a3b8"}}>Record created</div><div style={{fontSize:10,color:C.dim}}>{fmtDate(rec.createdAt)}</div></div>
            </div>
          </div>
        </div>
      </div>
    );
  };

  // Analytics
  // ── Reports ───────────────────────────────────────────────
  // Build a compact report snapshot of the brand's pipeline. We persist only KPIs + small
  // top-N lists + (optionally) AI advice text — never the full lead/opp records — so storage
  // stays tiny no matter how big the brand grows.
  const buildReportSnapshot = (filters = {}) => {
    const fromMs = filters.from ? new Date(filters.from).getTime() : 0;
    const toMs   = filters.to   ? new Date(filters.to  ).getTime() : Date.now();
    const inRange = (iso) => { if (!iso) return false; const t = new Date(iso).getTime(); return t >= fromMs && t <= toMs; };
    const allowedStages = (filters.stages && filters.stages.length) ? new Set(filters.stages) : null;
    const includeOpp = (o) => (!allowedStages || allowedStages.has(o.stage));
    const includeLead = (l) => (!allowedStages || allowedStages.has(l.stage));
    const periodLeads = filters.from || filters.to ? bLeads.filter(l => inRange(l.createdAt)) : bLeads;
    const periodOpps  = filters.from || filters.to ? bOpps.filter(o => inRange(o.createdAt) || inRange(o.stageEnteredAt)) : bOpps;
    const leads = periodLeads.filter(includeLead);
    const opps  = periodOpps.filter(includeOpp);
    const signed = opps.filter(o => o.stage === "agreement_signed").length;
    const lost   = opps.filter(o => o.stage === "closed_lost").length;
    const opsCountedAsLeads = opps.filter(o => !leads.find(l => l.id === o.originalLeadId)).length;
    const totalFunnel = leads.length + opsCountedAsLeads;
    const convRate = totalFunnel ? Math.min(100, Math.round((opps.length / totalFunnel) * 100)) : 0;
    const closeRate = opps.length ? Math.round((signed / opps.length) * 100) : 0;
    const avgScore = opps.length ? Math.round(opps.reduce((a, o) => a + (scores[o.id]?.total || 0), 0) / opps.length) : 0;
    const stageBuckets = {};
    bOStages.forEach(s => { stageBuckets[s.id] = { label: s.label, color: s.color, count: opps.filter(o => o.stage === s.id).length }; });
    const closedOps = opps.filter(o => o.stage === "agreement_signed");
    const closeDeltas = closedOps.map(o => Math.max(0, new Date(o.stageEnteredAt || o.updatedAt || Date.now()).getTime() - new Date(o.leadOriginAt || o.createdAt).getTime()));
    const avgCloseDays = closeDeltas.length ? Math.round(closeDeltas.reduce((a,b)=>a+b,0) / closeDeltas.length / 86400000) : 0;
    const topOpps = [...opps].sort((a,b) => (scores[b.id]?.total||0) - (scores[a.id]?.total||0)).slice(0, 8).map(o => ({ id: o.id, name: `${o.firstName||""} ${o.lastName||""}`.trim(), stage: o.stage, score: scores[o.id]?.total || 0 }));
    const staleList = opps.filter(o => { const s = bOStages.find(st => st.id === o.stage); return s?.staleDays && daysSince(o.stageEnteredAt) >= s.staleDays; }).slice(0, 8).map(o => ({ id: o.id, name: `${o.firstName||""} ${o.lastName||""}`.trim(), stage: o.stage, days: daysSince(o.stageEnteredAt) }));
    const dropouts = opps.filter(o => o.stage === "closed_lost").map(o => ({ id: o.id, name: `${o.firstName||""} ${o.lastName||""}`.trim(), reason: o.lostReason || "unknown" }));
    const bAgents = bBrokerData?.agents || [];
    // brokerComms is keyed by oppId — flatten across all opps then filter by brokerId.
    const allBrokerMsgs = Object.values(brokerComms || {}).flat();
    const brokerActivity = bAgents.map(a => ({
      id: a.id,
      name: `${a.firstName||""} ${a.lastName||""}`.trim(),
      assignedOpps: opps.filter(o => o.brokerId === a.id).length,
      comms: allBrokerMsgs.filter(m => m.brokerId === a.id).length,
    })).sort((a,b) => b.assignedOpps - a.assignedOpps).slice(0, 8);
    // Lead source performance — group every lead + opp by their `source` field and compute
    // per-source funnel metrics so the rep can see which channels actually convert + close.
    const sourceMap = {};
    const bumpSource = (rec, bucket) => {
      const src = (rec.source && rec.source.trim()) || "Unspecified";
      if (!sourceMap[src]) sourceMap[src] = { name: src, leads: 0, opps: 0, signed: 0, lost: 0 };
      sourceMap[src][bucket] += 1;
    };
    leads.forEach(l => bumpSource(l, "leads"));
    opps.forEach(o => {
      bumpSource(o, "opps");
      if (o.stage === "agreement_signed") bumpSource(o, "signed");
      if (o.stage === "closed_lost") bumpSource(o, "lost");
    });
    const sourcePerformance = Object.values(sourceMap).map(s => {
      const total = s.leads + s.opps;
      const convRateSrc = total ? Math.round((s.opps / total) * 100) : 0;
      const closeRateSrc = s.opps ? Math.round((s.signed / s.opps) * 100) : 0;
      return { ...s, total, convRate: convRateSrc, closeRate: closeRateSrc };
    }).sort((a,b) => b.signed - a.signed || b.opps - a.opps || b.leads - a.leads);
    return {
      kpis: { leadCount: leads.length, oppCount: opps.length, signed, lost, convRate, closeRate, avgScore, avgCloseDays },
      stages: stageBuckets,
      topOpps, staleList, dropouts, brokerActivity, sourcePerformance,
    };
  };
  const saveReport = (report) => {
    if (!activeBrand) return;
    // Cap at the 24 most-recent reports per brand. Older ones get pruned automatically so
    // storage doesn't grow unbounded. Each report is < ~3KB so 24 reports ≈ 70KB total.
    const list = [report, ...bReports].slice(0, 24);
    const next = { ...reports, [activeBrand]: list };
    p("ff4_reports", setReports, next);
  };
  const deleteReport = (id) => {
    if (!activeBrand) return;
    const next = { ...reports, [activeBrand]: bReports.filter(r => r.id !== id) };
    p("ff4_reports", setReports, next);
  };
  // CSV export — flat KPIs + opportunity list + lead list. Excel opens this natively. Lightweight.
  const downloadReportCsv = (report) => {
    const rows = [];
    rows.push(["Report", report.title]);
    rows.push(["Generated", new Date(report.generatedAt).toLocaleString()]);
    if (report.period?.start) rows.push(["Period start", new Date(report.period.start).toLocaleDateString()]);
    if (report.period?.end)   rows.push(["Period end",   new Date(report.period.end).toLocaleDateString()]);
    rows.push([]);
    // Territory-unlock report — list every newly-callable lead/opp with contact info so the rep
    // can pull it straight into a dialer / call sheet.
    if (report.data?.unlock) {
      const u = report.data.unlock;
      rows.push(["Territory unlocked", u.territoryLabel]);
      rows.push(["Previous restriction", u.restrictionLabel]);
      rows.push(["Candidates to revisit", u.unlockedCount]);
      rows.push([]);
      rows.push(["Type","Name","Stage","Email","Phone","Territory","Score","Source"]);
      (u.unlockedRecords||[]).forEach(r => rows.push([
        r.type === "opp" ? "Opportunity" : "Lead",
        r.name, r.stageLabel||r.stage, r.email||"", r.phone||"", r.territory||"", r.score||0, r.source||"",
      ]));
      rows.push([]);
    }
    rows.push(["KPI", "Value"]);
    const k = report.data?.kpis || {};
    rows.push(["Leads", k.leadCount]);
    rows.push(["Opportunities", k.oppCount]);
    rows.push(["Signed", k.signed]);
    rows.push(["Lost", k.lost]);
    rows.push(["Conversion rate %", k.convRate]);
    rows.push(["Close rate %", k.closeRate]);
    rows.push(["Avg score", k.avgScore]);
    rows.push(["Avg close (days)", k.avgCloseDays]);
    rows.push([]);
    rows.push(["Pipeline stage", "Count"]);
    Object.entries(report.data?.stages||{}).forEach(([k,v]) => rows.push([v.label, v.count]));
    rows.push([]);
    rows.push(["Top opportunities (name, stage, score)"]);
    (report.data?.topOpps||[]).forEach(o => rows.push([o.name, o.stage, o.score]));
    rows.push([]);
    rows.push(["Stale opportunities (name, stage, days stale)"]);
    (report.data?.staleList||[]).forEach(o => rows.push([o.name, o.stage, o.days]));
    rows.push([]);
    rows.push(["Broker activity (name, assigned opps, comms count)"]);
    (report.data?.brokerActivity||[]).forEach(b => rows.push([b.name, b.assignedOpps, b.comms]));
    rows.push([]);
    rows.push(["Lead source performance"]);
    rows.push(["Source", "Leads", "Opps", "Signed", "Lost", "Conv rate %", "Close rate %"]);
    (report.data?.sourcePerformance||[]).forEach(s => rows.push([s.name, s.leads, s.opps, s.signed, s.lost, s.convRate, s.closeRate]));
    const csv = rows.map(r => r.map(c => {
      const s = String(c == null ? "" : c);
      return /[,"\n]/.test(s) ? `"${s.replace(/"/g,'""')}"` : s;
    }).join(",")).join("\r\n");
    const blob = new Blob([csv], { type: "text/csv;charset=utf-8;" });
    const url = URL.createObjectURL(blob);
    const a = document.createElement("a");
    a.href = url; a.download = `${(report.title||"report").replace(/[^a-z0-9]+/gi,"-").toLowerCase()}.csv`;
    document.body.appendChild(a); a.click(); document.body.removeChild(a);
    URL.revokeObjectURL(url);
  };
  // Open the report in a print-friendly window so the user can save as PDF via the browser's
  // native print dialog. No PDF library needed — minimal bundle cost.
  const printReport = (report) => {
    const w = window.open("", "_blank", "width=900,height=1100");
    if (!w) { toast$("Pop-up blocked — allow pop-ups to print/export PDF.", "err"); return; }
    const k = report.data?.kpis || {};
    const stages = Object.entries(report.data?.stages || {});
    const maxStage = Math.max(...stages.map(([,v]) => v.count), 1);
    const topOpps = report.data?.topOpps || [];
    const staleList = report.data?.staleList || [];
    const brokerActivity = report.data?.brokerActivity || [];
    const sourcePerformance = report.data?.sourcePerformance || [];
    const aiBlock = report.aiAdvice ? `<section><h2>AI Strategy Advice</h2><div class="ai-md">${report.aiAdvice.replace(/\n\n/g,"</p><p>").replace(/\n/g,"<br/>").replace(/^/,"<p>").replace(/$/,"</p>").replace(/\*\*([^*]+)\*\*/g,"<strong>$1</strong>").replace(/^### (.+)$/gm,"<h3>$1</h3>").replace(/^## (.+)$/gm,"<h2>$1</h2>")}</div></section>` : "";
    const html = `<!doctype html><html><head><meta charset="utf-8"><title>${report.title} — ${brand?.name||BRAND.name}</title><style>
      body{font-family:Helvetica,Arial,sans-serif;color:#1e293b;margin:32px;background:#fff;font-size:13px;line-height:1.55}
      h1{font-size:22px;margin:0 0 4px;font-weight:800}
      h2{font-size:14px;color:#475569;letterspacing:.04em;text-transform:uppercase;margin:22px 0 9px;font-weight:800;border-bottom:1px solid #e2e8f0;padding-bottom:5px}
      h3{font-size:13px;margin:14px 0 6px;font-weight:700}
      .kpis{display:grid;grid-template-columns:repeat(4,1fr);gap:10px;margin:12px 0 18px}
      .kpi{background:#f8fafc;border:1px solid #e2e8f0;border-radius:8px;padding:10px 12px}
      .kpi .v{font-size:20px;font-weight:800;color:#0f172a}
      .kpi .l{font-size:10px;color:#64748b;text-transform:uppercase;letter-spacing:.05em;margin-top:2px}
      table{width:100%;border-collapse:collapse;font-size:12px}
      th,td{text-align:left;padding:6px 8px;border-bottom:1px solid #e2e8f0}
      th{color:#64748b;font-weight:700;font-size:10px;text-transform:uppercase;letter-spacing:.04em}
      .bar-row{display:flex;align-items:center;gap:8px;margin-bottom:5px;font-size:11px}
      .bar-row .name{width:160px;text-align:right;color:#64748b}
      .bar-row .bar-track{flex:1;height:14px;background:#f1f5f9;border-radius:4px;overflow:hidden}
      .bar-row .bar-fill{height:100%;border-radius:4px}
      .bar-row .count{width:30px;text-align:right;font-weight:700}
      .ai-md{background:#f8fafc;border-left:3px solid #3b82f6;padding:14px 16px;border-radius:0 8px 8px 0;font-size:12px;line-height:1.65;color:#1e293b}
      .ai-md p{margin:0 0 9px}
      .ai-md h2{border:0;padding:0;margin:14px 0 6px;font-size:13px;color:#0f172a;text-transform:none;letter-spacing:0}
      .ai-md h3{margin:11px 0 5px;font-size:12px}
      header{border-bottom:2px solid #0f172a;padding-bottom:8px;margin-bottom:14px}
      .meta{color:#64748b;font-size:11px;margin-top:3px}
      @media print{body{margin:18px}}
    </style></head><body>
      <header><h1>${report.title}</h1><div class="meta">${brand?.name||BRAND.name} · Generated ${new Date(report.generatedAt).toLocaleString()}${report.period?.start ? ` · ${new Date(report.period.start).toLocaleDateString()} – ${new Date(report.period.end).toLocaleDateString()}` : ""}</div></header>
      <section><h2>Key Metrics</h2>
        <div class="kpis">
          <div class="kpi"><div class="v">${k.leadCount||0}</div><div class="l">Leads</div></div>
          <div class="kpi"><div class="v">${k.oppCount||0}</div><div class="l">Opportunities</div></div>
          <div class="kpi"><div class="v">${k.signed||0}</div><div class="l">Signed</div></div>
          <div class="kpi"><div class="v">${k.lost||0}</div><div class="l">Closed Lost</div></div>
          <div class="kpi"><div class="v">${k.convRate||0}%</div><div class="l">Conv Rate</div></div>
          <div class="kpi"><div class="v">${k.closeRate||0}%</div><div class="l">Close Rate</div></div>
          <div class="kpi"><div class="v">${k.avgScore||0}</div><div class="l">Avg Score</div></div>
          <div class="kpi"><div class="v">${k.avgCloseDays||0}d</div><div class="l">Avg Close Time</div></div>
        </div>
      </section>
      <section><h2>Pipeline Stages</h2>
        ${stages.map(([id, v]) => `<div class="bar-row"><div class="name">${v.label}</div><div class="bar-track"><div class="bar-fill" style="width:${(v.count/maxStage)*100}%;background:${v.color}"></div></div><div class="count">${v.count}</div></div>`).join("")}
      </section>
      ${topOpps.length ? `<section><h2>Top Opportunities</h2><table><tr><th>Name</th><th>Stage</th><th>Score</th></tr>${topOpps.map(o => `<tr><td>${o.name}</td><td>${o.stage}</td><td>${o.score}</td></tr>`).join("")}</table></section>` : ""}
      ${staleList.length ? `<section><h2>Stale Opportunities</h2><table><tr><th>Name</th><th>Stage</th><th>Days stale</th></tr>${staleList.map(o => `<tr><td>${o.name}</td><td>${o.stage}</td><td>${o.days}</td></tr>`).join("")}</table></section>` : ""}
      ${sourcePerformance.length ? `<section><h2>Lead Source Performance</h2><table><tr><th>Source</th><th>Leads</th><th>Opps</th><th>Signed</th><th>Lost</th><th>Conv</th><th>Close</th></tr>${sourcePerformance.map(s => `<tr><td>${s.name}</td><td>${s.leads}</td><td>${s.opps}</td><td>${s.signed}</td><td>${s.lost}</td><td>${s.convRate}%</td><td>${s.closeRate}%</td></tr>`).join("")}</table></section>` : ""}
      ${brokerActivity.length ? `<section><h2>Broker Activity</h2><table><tr><th>Broker</th><th>Assigned Opps</th><th>Comms</th></tr>${brokerActivity.map(b => `<tr><td>${b.name}</td><td>${b.assignedOpps}</td><td>${b.comms}</td></tr>`).join("")}</table></section>` : ""}
      ${aiBlock}
      <footer style="margin-top:24px;padding-top:10px;border-top:1px solid #e2e8f0;color:#94a3b8;font-size:10px">Generated by ${BRAND.name} · ${new Date(report.generatedAt).toLocaleString()}</footer>
    </body></html>`;
    w.document.write(html); w.document.close();
    setTimeout(() => { try { w.focus(); w.print(); } catch {} }, 250);
  };
  // AI Strategy Report — one Claude call summarizing the pipeline + returning markdown advice.
  // Capped to 1 per 7 days per brand to keep token spend bounded. Mock-mode parity: returns
  // hand-tuned advice when no API key is set.
  const runAiStrategyReport = async () => {
    if (!activeBrand) return;
    // Enforce the cap
    const lastAi = bReports.find(r => r.type === "ai_strategy");
    if (lastAi && (Date.now() - new Date(lastAi.generatedAt).getTime()) < 7*86400000) {
      const daysLeft = Math.ceil((7 - (Date.now() - new Date(lastAi.generatedAt).getTime())/86400000));
      toast$(`⏳ AI strategy report can run once per week — ${daysLeft}d until next available.`, "err");
      return;
    }
    const snap = buildReportSnapshot();
    // Build a compact summary string to send to Claude. Stays well under 500 tokens of input.
    const k = snap.kpis;
    const stageLine = Object.entries(snap.stages).map(([id, v]) => `${v.label}=${v.count}`).join(" | ");
    const topOppLine = snap.topOpps.slice(0,5).map(o => `${o.name}(${o.stage}, score ${o.score})`).join("; ");
    const staleLine = snap.staleList.slice(0,5).map(o => `${o.name}(${o.stage}, ${o.days}d stale)`).join("; ");
    const brokerLine = snap.brokerActivity.slice(0,5).map(b => `${b.name}(${b.assignedOpps} opps)`).join("; ");
    const sourceLine = (snap.sourcePerformance||[]).slice(0,6).map(s => `${s.name}(${s.leads}L/${s.opps}O/${s.signed}S, ${s.closeRate}% close)`).join("; ");
    const userMsg = `Frandev pipeline snapshot for "${brand?.name||"this brand"}":
KPIs: ${k.leadCount} leads, ${k.oppCount} opps, ${k.signed} signed, ${k.lost} lost. Conv rate ${k.convRate}%. Close rate ${k.closeRate}%. Avg score ${k.avgScore}. Avg close ${k.avgCloseDays}d.
Stages: ${stageLine}
Top opps: ${topOppLine||"none"}
Stale opps: ${staleLine||"none"}
Brokers: ${brokerLine||"no brokers"}
Lead sources: ${sourceLine||"none"}

Generate a strategic advisory report (max 2 PDF pages of markdown). Use ## section headings. Cover: (1) what's going well, (2) the biggest risk you see in the pipeline, (3) 3-5 specific next-action recommendations including which lead sources to double down on vs. cut, (4) one bold bet for the next 30 days. Frame as actionable advice for the franchise development rep, not a description of the data.`;
    toast$("✨ Generating AI strategy report…");
    let advice = "";
    try {
      advice = await callClaude([{role:"user", content: userMsg}], "You are a senior franchise development advisor. Be terse, specific, and confident. Use markdown headings and bullet lists. No fluff.", 900, settings.aiModelOrganize || "claude-haiku-4-5");
    } catch (e) {
      advice = "## AI request failed\nThe Claude API call did not complete. Check your API key in Settings → AI, or try again later.";
    }
    // Mock-mode fallback: callClaude returns a JSON error blob when no key is set or prompt is unrecognized.
    const isMockError = !advice || !advice.trim() || /^\{[^}]*"error"/.test(advice.trim()) || /Mock AI mode/i.test(advice);
    if (isMockError) {
      const srcs = snap.sourcePerformance || [];
      const competing = srcs.filter(s => s.opps >= 2);
      const bestSrc  = competing.length ? competing.reduce((a,b) => b.closeRate > a.closeRate ? b : a) : null;
      const worstSrc = competing.length ? competing.reduce((a,b) => b.closeRate < a.closeRate ? b : a) : null;
      const srcInsight = (bestSrc && worstSrc && bestSrc.name !== worstSrc.name)
        ? `- **Double down on ${bestSrc.name}** (${bestSrc.closeRate}% close, ${bestSrc.signed}/${bestSrc.opps} signed) — it's outperforming every other channel.\n- **Cut or rework ${worstSrc.name}** (${worstSrc.closeRate}% close on ${worstSrc.opps} opps) — the volume isn't translating into deals.`
        : (bestSrc ? `- **${bestSrc.name} is your strongest channel** (${bestSrc.closeRate}% close) — invest more time there.` : `- Lead source attribution is thin — start tagging every new lead with a source so this section gets real signal.`);
      advice = `## What's working\n- ${k.signed} signed deals, ${k.closeRate}% close rate, ${k.convRate}% lead→opp conversion.\n- Top of pipeline: ${snap.topOpps[0]?.name||"(none)"} (score ${snap.topOpps[0]?.score||0}) is your strongest active opp — protect that momentum.\n\n## Biggest risk\n- ${snap.staleList.length} stale opps including ${snap.staleList[0]?.name||"(none)"} — momentum loss compounds quickly. Every week of silence drops the close-probability meaningfully.\n\n## Next actions\n- **Force-pick a daily top-3** from the stale list and call/text within 24h. Treat as non-negotiable.\n- **Block 30 min/day for FDD follow-ups** — the ${snap.stages.fdd_sent?.count||0} in FDD Sent are the biggest single recoverable cohort.\n${srcInsight}\n- **Re-rank brokers** by recent comms × signed ratio. Over-index time on the top performer; have a candid conversation with the bottom one.\n- **Tighten qualification at intake** — disqualify faster to keep the pipeline clean. The cost of carrying a dead lead is higher than the cost of saying no.\n\n## Bold bet (next 30 days)\n- Get every opp past FDD Review within 14 days of receipt, or formally disqualify. No "soft middle." Pick one rep behavior, measure it weekly.`;
    }
    const report = {
      id: uid(), type: "ai_strategy",
      title: `🤖 AI Strategy · ${new Date().toLocaleDateString("en-US", { month:"short", day:"numeric", year:"numeric" })}`,
      generatedAt: nowIso(),
      data: snap, aiAdvice: advice.trim(),
    };
    saveReport(report);
    toast$("AI strategy report ready.");
  };
  // Generate a non-AI report (manual or auto-fired).
  const generateReport = ({ type, title, periodStart, periodEnd, filters }) => {
    const snap = buildReportSnapshot({ from: periodStart, to: periodEnd, ...(filters||{}) });
    const report = {
      id: uid(), type,
      title: title || `${type.charAt(0).toUpperCase() + type.slice(1)} · ${new Date().toLocaleDateString("en-US",{month:"short",day:"numeric"})}`,
      generatedAt: nowIso(),
      period: { start: periodStart || null, end: periodEnd || null },
      filters: filters || {},
      data: snap,
    };
    saveReport(report);
    return report;
  };

  const AnalyticsView=()=>{
    const maxC=Math.max(...bOStages.map(s=>bOpps.filter(o=>o.stage===s.id).length),1);
    // Period (MTD default) + period-filtered KPIs
    const period = getPeriodRange(overviewMode, overviewAnchor, overviewCustom);
    const inRange = (iso, s, e) => { if (!iso) return false; const t = new Date(iso).getTime(); return t >= s.getTime() && t <= e.getTime(); };
    // Period-filtered (with delta vs matching prior window)
    const leadsInPeriodList = bLeads.filter(l => inRange(l.createdAt, period.start, period.end))
                              .concat(bOpps.filter(o => inRange(o.leadOriginAt || o.createdAt, period.start, period.end)));
    const leadsPrevList = bLeads.filter(l => inRange(l.createdAt, period.prevStart, period.prevEnd))
                          .concat(bOpps.filter(o => inRange(o.leadOriginAt || o.createdAt, period.prevStart, period.prevEnd)));
    const leadsInPeriod = leadsInPeriodList.length;
    const leadsPrev = leadsPrevList.length;
    const oppsInPeriodList = bOpps.filter(o => inRange(o.createdAt, period.start, period.end));
    const oppsPrevList = bOpps.filter(o => inRange(o.createdAt, period.prevStart, period.prevEnd));
    const oppsInPeriod = oppsInPeriodList.length;
    const oppsPrev = oppsPrevList.length;
    const signedInPeriod = bOpps.filter(o => o.stage === "agreement_signed" && inRange(o.stageEnteredAt, period.start, period.end)).length;
    const signedPrev = bOpps.filter(o => o.stage === "agreement_signed" && inRange(o.stageEnteredAt, period.prevStart, period.prevEnd)).length;
    // Conversion rate (period) — % of new leads-in-period that became opps in the same window.
    const convRatePeriod = leadsInPeriod ? Math.min(100, Math.round((oppsInPeriod / leadsInPeriod) * 100)) : 0;
    const convRatePrev   = leadsPrev    ? Math.min(100, Math.round((oppsPrev    / leadsPrev   ) * 100)) : 0;
    // Stale (period) — opps that crossed their staleDays threshold within this window.
    const becameStaleIn = (o, s, e) => {
      const stg = bOStages.find(st => st.id === o.stage);
      if (!stg?.staleDays || !o.stageEnteredAt) return false;
      const t = new Date(o.stageEnteredAt).getTime() + stg.staleDays * 86400000;
      return t >= s.getTime() && t <= e.getTime();
    };
    const staleInPeriod = bOpps.filter(o => becameStaleIn(o, period.start, period.end)).length;
    const stalePrev     = bOpps.filter(o => becameStaleIn(o, period.prevStart, period.prevEnd)).length;
    // Territory conflicts (period) — current conflicts on records created in this window.
    const conflictsInPeriod = bOpps.filter(o => inRange(o.createdAt, period.start, period.end) && checkConflict(o)).length
                            + bLeads.filter(l => inRange(l.createdAt, period.start, period.end) && checkConflict(l)).length;
    const conflictsPrev = bOpps.filter(o => inRange(o.createdAt, period.prevStart, period.prevEnd) && checkConflict(o)).length
                        + bLeads.filter(l => inRange(l.createdAt, period.prevStart, period.prevEnd) && checkConflict(l)).length;
    // Avg lead score (period) — mean of computeLeadScore across leads/opps originated in window.
    const leadScorePool      = leadsInPeriodList.length;
    const avgLeadScore       = leadScorePool ? Math.round(leadsInPeriodList.reduce((a,l)=>a+computeLeadScore(l),0) / leadScorePool) : 0;
    const leadScorePrevPool  = leadsPrevList.length;
    const avgLeadScorePrev   = leadScorePrevPool ? Math.round(leadsPrevList.reduce((a,l)=>a+computeLeadScore(l),0) / leadScorePrevPool) : 0;
    // Avg opp score (period) — mean of AI scores across opps created in window.
    const avgOppScore      = oppsInPeriodList.length ? Math.round(oppsInPeriodList.reduce((a,o)=>a+(scores[o.id]?.total||0),0) / oppsInPeriodList.length) : 0;
    const avgOppScorePrev  = oppsPrevList.length    ? Math.round(oppsPrevList.reduce((a,o)=>a+(scores[o.id]?.total||0),0) / oppsPrevList.length) : 0;
    // Trailing-12-months metrics — frandev cycles are 3–4 months. Close rate + avg close time
    // can't honestly be MTD, so they stay TTM regardless of selected period.
    const ttmStart = new Date(Date.now() - 365*86400000);
    const ttmOpps = bOpps.filter(o => new Date(o.createdAt).getTime() >= ttmStart.getTime());
    const ttmSigned = ttmOpps.filter(o => o.stage === "agreement_signed");
    const closeRateTTM = ttmOpps.length ? Math.round((ttmSigned.length / ttmOpps.length) * 100) : 0;
    const closeDeltas = ttmSigned.map(o => Math.max(0, new Date(o.stageEnteredAt || o.updatedAt || Date.now()).getTime() - new Date(o.leadOriginAt || o.createdAt).getTime()));
    const avgCloseMs = closeDeltas.length ? closeDeltas.reduce((a,b)=>a+b,0)/closeDeltas.length : 0;
    const avgCloseLabel = closeDeltas.length === 0 ? "—" : `${(avgCloseMs / (30.44*86400000)).toFixed(1)}mo`;
    const periodModes = [["week","Week"],["month","Month"],["quarter","Quarter"],["year","Year"],["custom","Custom"]];
    return(
      <div style={{display:"flex",flexDirection:"column",gap:14}}>
        {/* Sub-tab nav: Overview vs Reports */}
        <div style={{display:"flex",gap:4,background:"#090f1c",border:`1px solid ${C.border}`,borderRadius:10,padding:4,maxWidth:420}}>
          {[["overview",`📊 Overview`],["reports",`📋 Reports (${bReports.length})`]].map(([k,l])=>(
            <button key={k} onClick={()=>setAnalyticsTab(k)} style={{flex:1,background:analyticsTab===k?"#162035":"transparent",border:"none",borderRadius:7,padding:"8px 14px",color:analyticsTab===k?C.accent:C.muted,fontSize:12,fontWeight:analyticsTab===k?800:600,cursor:"pointer",fontFamily:"inherit"}}>{l}</button>
          ))}
        </div>

        {analyticsTab === "overview" && <>
          {/* Period selector */}
          <div style={{display:"flex",alignItems:"center",gap:12,flexWrap:"wrap"}}>
            <div style={{display:"flex",gap:3,background:"#090f1c",border:`1px solid ${C.border}`,borderRadius:9,padding:3}}>
              {periodModes.map(([k,l]) => (
                <button key={k} onClick={()=>{ setOverviewMode(k); if(k!=="custom") setOverviewAnchor(new Date().toISOString()); }} style={{background:overviewMode===k?"#162035":"transparent",border:"none",borderRadius:6,padding:"6px 12px",color:overviewMode===k?C.accent:C.muted,fontSize:11,fontWeight:overviewMode===k?800:600,cursor:"pointer",fontFamily:"inherit"}}>{l}</button>
              ))}
            </div>
            {overviewMode !== "custom" ? (
              <div style={{display:"flex",alignItems:"center",gap:8}}>
                <button onClick={()=>setOverviewAnchor(shiftPeriod(overviewMode, overviewAnchor, -1))} style={{background:C.panel,border:`1px solid ${C.border}`,borderRadius:7,padding:"6px 10px",color:C.muted,fontSize:11,cursor:"pointer",fontFamily:"inherit"}} title="Previous period">◀</button>
                <div style={{minWidth:140,textAlign:"center",fontSize:13,fontWeight:800,color:C.text}}>{period.label}{period.isCurrent && <span style={{fontSize:10,color:C.muted,marginLeft:6,fontWeight:600}}>· to date</span>}</div>
                <button onClick={()=>setOverviewAnchor(shiftPeriod(overviewMode, overviewAnchor, +1))} disabled={period.isCurrent} style={{background:C.panel,border:`1px solid ${C.border}`,borderRadius:7,padding:"6px 10px",color:period.isCurrent?C.dim:C.muted,fontSize:11,cursor:period.isCurrent?"not-allowed":"pointer",fontFamily:"inherit",opacity:period.isCurrent?0.4:1}} title="Next period">▶</button>
                {!period.isCurrent && <button onClick={()=>setOverviewAnchor(new Date().toISOString())} style={{background:"#091420",border:`1px solid ${C.border}`,borderRadius:7,padding:"6px 10px",color:"#60a5fa",fontSize:11,fontWeight:700,cursor:"pointer",fontFamily:"inherit"}}>Today</button>}
              </div>
            ) : (
              <div style={{display:"flex",alignItems:"center",gap:6}}>
                <input type="date" value={overviewCustom.from} onChange={e=>setOverviewCustom({...overviewCustom, from:e.target.value})} style={{background:C.panel,border:`1px solid ${C.border}`,borderRadius:7,padding:"6px 9px",color:C.text,fontSize:11,fontFamily:"inherit",colorScheme:"dark"}}/>
                <span style={{color:C.muted,fontSize:11}}>→</span>
                <input type="date" value={overviewCustom.to} onChange={e=>setOverviewCustom({...overviewCustom, to:e.target.value})} style={{background:C.panel,border:`1px solid ${C.border}`,borderRadius:7,padding:"6px 9px",color:C.text,fontSize:11,fontFamily:"inherit",colorScheme:"dark"}}/>
              </div>
            )}
          </div>
          {/* KPI grid — period-filtered (with delta) + TTM-only metrics that need a longer window */}
          <div style={{display:"flex",gap:11,flexWrap:"wrap"}}>
            {[
              ["Leads",           leadsInPeriod,           "#60a5fa", "period",    leadsPrev,           leadsInPeriod],
              ["Opportunities",   oppsInPeriod,            "#a78bfa", "period",    oppsPrev,            oppsInPeriod],
              ["Signed",          signedInPeriod,          "#4ade80", "period",    signedPrev,          signedInPeriod],
              ["Conv Rate",       `${convRatePeriod}%`,    "#facc15", "period",    convRatePrev,        convRatePeriod],
              ["Stale",           staleInPeriod,           "#f87171", "period",    stalePrev,           staleInPeriod],
              ["Terr. Conflicts", conflictsInPeriod,       "#f87171", "period",    conflictsPrev,       conflictsInPeriod],
              ["Avg Lead Score",  avgLeadScore,            "#fb923c", "period",    avgLeadScorePrev,    avgLeadScore],
              ["Avg Opp Score",   avgOppScore,             "#fb923c", "period",    avgOppScorePrev,     avgOppScore],
              ["Close Rate",      `${closeRateTTM}%`,      "#38bdf8", "ttm"],
              ["Avg Close Time",  avgCloseLabel,           "#34d399", "ttm"],
            ].map(([l,v,c,kind,prev,curNum]) => {
              let subtitle = null;
              if (kind === "period") {
                const ch = pctChange(curNum||0, prev||0);
                subtitle = <div style={{fontSize:10,color:ch.color,fontWeight:700,marginTop:3,letterSpacing:".02em"}}>{ch.label}<span style={{color:C.dim,fontWeight:500,marginLeft:4}}>vs {period.prevLabel}</span></div>;
              } else {
                subtitle = <div style={{fontSize:10,color:C.dim,fontWeight:600,marginTop:3,letterSpacing:".02em"}}>trailing 12mo</div>;
              }
              return (
                <div key={l} style={{background:C.panel,border:`1px solid ${C.border}`,borderRadius:12,padding:"14px 16px",flex:"1 1 130px",minWidth:130}}>
                  <div style={{fontSize:24,fontWeight:900,color:c,lineHeight:1.1}}>{v}</div>
                  <div style={{fontSize:11,color:C.muted,marginTop:3}}>{l}</div>
                  {subtitle}
                </div>
              );
            })}
          </div>
          <div style={{background:C.panel,border:`1px solid ${C.border}`,borderRadius:14,padding:"18px 20px"}}>
            <Sec>Opportunity Pipeline</Sec>
            {bOStages.map(s=>{const cnt=bOpps.filter(o=>o.stage===s.id).length;return(
              <div key={s.id} style={{display:"flex",alignItems:"center",gap:10,marginBottom:7}}>
                <div style={{width:155,fontSize:11,color:C.muted,textAlign:"right"}}>{s.label}</div>
                <div style={{flex:1,height:20,background:"#090f1c",borderRadius:5,overflow:"hidden"}}>
                  <div style={{width:`${(cnt/maxC)*100}%`,height:"100%",background:s.color+"cc",borderRadius:5,display:"flex",alignItems:"center",paddingLeft:7,fontSize:11,fontWeight:700,color:"#fff",minWidth:cnt>0?20:0}}>{cnt>0?cnt:""}</div>
                </div>
                <div style={{width:22,fontSize:12,fontWeight:700,color:s.color}}>{cnt}</div>
              </div>
            );})}
          </div>
          {(() => {
            // Lead source performance — same computation as buildReportSnapshot.sourcePerformance
            // but reactive to live data instead of a frozen snapshot.
            const map = {};
            const bump = (rec, bucket) => { const src = (rec.source||"").trim() || "Unspecified"; if (!map[src]) map[src] = { name: src, leads:0, opps:0, signed:0, lost:0 }; map[src][bucket] += 1; };
            bLeads.forEach(l => bump(l, "leads"));
            bOpps.forEach(o => { bump(o, "opps"); if (o.stage==="agreement_signed") bump(o,"signed"); if (o.stage==="closed_lost") bump(o,"lost"); });
            const rows = Object.values(map).map(s => { const total = s.leads + s.opps; return { ...s, total, convRate: total ? Math.round((s.opps/total)*100) : 0, closeRate: s.opps ? Math.round((s.signed/s.opps)*100) : 0 }; }).sort((a,b) => b.signed - a.signed || b.opps - a.opps || b.leads - a.leads);
            if (!rows.length) return null;
            const maxTotal = Math.max(...rows.map(r => r.total), 1);
            // Highlight best + worst performers (by close rate among sources with >= 2 opps)
            const competing = rows.filter(r => r.opps >= 2);
            const best  = competing.length ? competing.reduce((a,b) => b.closeRate > a.closeRate ? b : a) : null;
            const worst = competing.length ? competing.reduce((a,b) => b.closeRate < a.closeRate ? b : a) : null;
            return (
              <div style={{background:C.panel,border:`1px solid ${C.border}`,borderRadius:14,padding:"18px 20px"}}>
                <Sec>🔗 Lead Source Performance</Sec>
                {best && worst && best.name !== worst.name && (
                  <div style={{display:"flex",gap:10,marginBottom:14,flexWrap:"wrap"}}>
                    <div style={{flex:"1 1 200px",background:"#0a1c10",border:"1px solid #4ade8033",borderRadius:9,padding:"9px 13px"}}>
                      <div style={{fontSize:9,fontWeight:800,color:"#4ade80",letterSpacing:".06em"}}>✓ TOP PERFORMER</div>
                      <div style={{fontSize:14,fontWeight:800,color:C.text,marginTop:3}}>{best.name}</div>
                      <div style={{fontSize:11,color:C.muted,marginTop:1}}>{best.closeRate}% close · {best.signed}/{best.opps} signed</div>
                    </div>
                    <div style={{flex:"1 1 200px",background:"#1c0a0a",border:"1px solid #f8717133",borderRadius:9,padding:"9px 13px"}}>
                      <div style={{fontSize:9,fontWeight:800,color:"#f87171",letterSpacing:".06em"}}>⚠ UNDERPERFORMER</div>
                      <div style={{fontSize:14,fontWeight:800,color:C.text,marginTop:3}}>{worst.name}</div>
                      <div style={{fontSize:11,color:C.muted,marginTop:1}}>{worst.closeRate}% close · {worst.signed}/{worst.opps} signed</div>
                    </div>
                  </div>
                )}
                <div style={{display:"grid",gridTemplateColumns:"1.4fr 60px 60px 60px 65px 65px",gap:8,fontSize:9,color:C.dim,fontWeight:800,letterSpacing:".05em",padding:"6px 4px",borderBottom:`1px solid ${C.border}`,marginBottom:4}}>
                  <div>SOURCE</div><div style={{textAlign:"right"}}>LEADS</div><div style={{textAlign:"right"}}>OPPS</div><div style={{textAlign:"right"}}>SIGNED</div><div style={{textAlign:"right"}}>CONV</div><div style={{textAlign:"right"}}>CLOSE</div>
                </div>
                {rows.map(s => (
                  <div key={s.name} style={{display:"grid",gridTemplateColumns:"1.4fr 60px 60px 60px 65px 65px",gap:8,fontSize:12,padding:"7px 4px",alignItems:"center",borderBottom:`1px solid ${C.border}`}}>
                    <div style={{minWidth:0}}>
                      <div style={{color:C.text,fontWeight:600,overflow:"hidden",textOverflow:"ellipsis",whiteSpace:"nowrap"}}>{s.name}</div>
                      <div style={{height:4,background:"#0a1525",borderRadius:2,marginTop:4,overflow:"hidden"}}>
                        <div style={{width:`${(s.total/maxTotal)*100}%`,height:"100%",background:s.signed>0?"#4ade80":s.opps>0?"#60a5fa":"#475569",borderRadius:2}}/>
                      </div>
                    </div>
                    <div style={{textAlign:"right",color:"#60a5fa",fontWeight:700}}>{s.leads}</div>
                    <div style={{textAlign:"right",color:"#a78bfa",fontWeight:700}}>{s.opps}</div>
                    <div style={{textAlign:"right",color:"#4ade80",fontWeight:800}}>{s.signed}</div>
                    <div style={{textAlign:"right",color:"#facc15",fontWeight:700}}>{s.convRate}%</div>
                    <div style={{textAlign:"right",color:"#38bdf8",fontWeight:700}}>{s.closeRate}%</div>
                  </div>
                ))}
              </div>
            );
          })()}
          {(() => {
            // Broker performance — every broker network and individual broker is effectively a
            // lead source. Same shape as Lead Source Performance but split by Networks vs Brokers.
            if (!bBrokers.length && !bNetworks.length) return null;
            // Per-broker / per-network: leads & opps & signed flow, mirroring the LEAD SOURCE
            // PERFORMANCE shape (LEADS / OPPS / SIGNED / CONV / CLOSE). Leads carry brokerId
            // when assigned; opps carry brokerId. Conv rate = opps / (leads + opps).
            const perBroker = bBrokers.map(b => {
              const myLeads = bLeads.filter(l => l.brokerId === b.id);
              const myOpps  = bOpps .filter(o => o.brokerId === b.id);
              const signedB = myOpps.filter(o => o.stage === "agreement_signed").length;
              const totalFunnel = myLeads.length + myOpps.length;
              return {
                id: b.id,
                name: `${b.firstName||""} ${b.lastName||""}`.trim() || "Unnamed broker",
                networkName: bNetworks.find(n => n.id === b.networkId)?.name || "—",
                leads: myLeads.length,
                opps: myOpps.length,
                signed: signedB,
                convRate: totalFunnel ? Math.round((myOpps.length / totalFunnel) * 100) : 0,
                closeRate: myOpps.length ? Math.round((signedB / myOpps.length) * 100) : 0,
              };
            });
            const perNetwork = bNetworks.map(n => {
              const memberIds = bBrokers.filter(b => b.networkId === n.id).map(b => b.id);
              const myLeads = bLeads.filter(l => memberIds.includes(l.brokerId));
              const myOpps  = bOpps .filter(o => memberIds.includes(o.brokerId));
              const signedN = myOpps.filter(o => o.stage === "agreement_signed").length;
              const totalFunnel = myLeads.length + myOpps.length;
              return {
                id: n.id,
                name: n.name || "Unnamed network",
                memberCount: memberIds.length,
                leads: myLeads.length,
                opps: myOpps.length,
                signed: signedN,
                convRate: totalFunnel ? Math.round((myOpps.length / totalFunnel) * 100) : 0,
                closeRate: myOpps.length ? Math.round((signedN / myOpps.length) * 100) : 0,
              };
            });
            // Default to whichever has data; user can toggle.
            const tab = brokerAnalyticsTab;
            const rows = (tab === "networks" ? perNetwork : perBroker).slice().sort((a,b) => b.signed - a.signed || b.opps - a.opps || b.leads - a.leads);
            if (!rows.length) return null;
            const maxOpps = Math.max(...rows.map(r => r.leads + r.opps), 1);
            const competing = rows.filter(r => r.opps >= 2);
            const best  = competing.length ? competing.reduce((a,b) => b.closeRate > a.closeRate ? b : a) : null;
            const worst = competing.length ? competing.reduce((a,b) => b.closeRate < a.closeRate ? b : a) : null;
            return (
              <div style={{background:C.panel,border:`1px solid ${C.border}`,borderRadius:14,padding:"18px 20px"}}>
                <div style={{display:"flex",alignItems:"center",gap:12,marginBottom:12,flexWrap:"wrap"}}>
                  <Sec style={{margin:0}}>🤝 Broker Performance</Sec>
                  <div style={{display:"flex",gap:3,background:"#090f1c",border:`1px solid ${C.border}`,borderRadius:8,padding:3,marginLeft:"auto"}}>
                    {[["brokers","Individual Brokers"],["networks","Networks"]].map(([k,l]) => (
                      <button key={k} onClick={()=>setBrokerAnalyticsTab(k)} style={{background:tab===k?"#162035":"transparent",border:"none",borderRadius:6,padding:"5px 11px",color:tab===k?C.accent:C.muted,fontSize:11,fontWeight:tab===k?800:600,cursor:"pointer",fontFamily:"inherit"}}>{l}</button>
                    ))}
                  </div>
                </div>
                {best && worst && best.name !== worst.name && (
                  <div style={{display:"flex",gap:10,marginBottom:14,flexWrap:"wrap"}}>
                    <div style={{flex:"1 1 200px",background:"#0a1c10",border:"1px solid #4ade8033",borderRadius:9,padding:"9px 13px"}}>
                      <div style={{fontSize:9,fontWeight:800,color:"#4ade80",letterSpacing:".06em"}}>✓ TOP PERFORMER</div>
                      <div style={{fontSize:14,fontWeight:800,color:C.text,marginTop:3}}>{best.name}</div>
                      <div style={{fontSize:11,color:C.muted,marginTop:1}}>{best.closeRate}% close · {best.signed}/{best.opps} signed</div>
                    </div>
                    <div style={{flex:"1 1 200px",background:"#1c0a0a",border:"1px solid #f8717133",borderRadius:9,padding:"9px 13px"}}>
                      <div style={{fontSize:9,fontWeight:800,color:"#f87171",letterSpacing:".06em"}}>⚠ UNDERPERFORMER</div>
                      <div style={{fontSize:14,fontWeight:800,color:C.text,marginTop:3}}>{worst.name}</div>
                      <div style={{fontSize:11,color:C.muted,marginTop:1}}>{worst.closeRate}% close · {worst.signed}/{worst.opps} signed</div>
                    </div>
                  </div>
                )}
                <div style={{display:"grid",gridTemplateColumns:"1.4fr 60px 60px 60px 65px 65px",gap:8,fontSize:9,color:C.dim,fontWeight:800,letterSpacing:".05em",padding:"6px 4px",borderBottom:`1px solid ${C.border}`,marginBottom:4}}>
                  <div>{tab==="networks"?"NETWORK":"BROKER"}</div>
                  <div style={{textAlign:"right"}}>LEADS</div>
                  <div style={{textAlign:"right"}}>OPPS</div>
                  <div style={{textAlign:"right"}}>SIGNED</div>
                  <div style={{textAlign:"right"}}>CONV</div>
                  <div style={{textAlign:"right"}}>CLOSE</div>
                </div>
                {rows.map(r => (
                  <div key={r.id} style={{display:"grid",gridTemplateColumns:"1.4fr 60px 60px 60px 65px 65px",gap:8,fontSize:12,padding:"7px 4px",alignItems:"center",borderBottom:`1px solid ${C.border}`}}>
                    <div style={{minWidth:0}}>
                      <div style={{color:C.text,fontWeight:600,overflow:"hidden",textOverflow:"ellipsis",whiteSpace:"nowrap"}}>{r.name}</div>
                      {tab==="brokers" && <div style={{fontSize:10,color:C.dim,marginTop:1,overflow:"hidden",textOverflow:"ellipsis",whiteSpace:"nowrap"}}>{r.networkName}</div>}
                      {tab==="networks" && <div style={{fontSize:10,color:C.dim,marginTop:1}}>{r.memberCount} broker{r.memberCount===1?"":"s"}</div>}
                      <div style={{height:4,background:"#0a1525",borderRadius:2,marginTop:4,overflow:"hidden"}}>
                        <div style={{width:`${((r.leads+r.opps)/maxOpps)*100}%`,height:"100%",background:r.signed>0?"#4ade80":r.opps>0?"#a78bfa":"#475569",borderRadius:2}}/>
                      </div>
                    </div>
                    <div style={{textAlign:"right",color:"#60a5fa",fontWeight:700}}>{r.leads}</div>
                    <div style={{textAlign:"right",color:"#a78bfa",fontWeight:700}}>{r.opps}</div>
                    <div style={{textAlign:"right",color:"#4ade80",fontWeight:800}}>{r.signed}</div>
                    <div style={{textAlign:"right",color:"#facc15",fontWeight:700}}>{r.convRate}%</div>
                    <div style={{textAlign:"right",color:"#38bdf8",fontWeight:700}}>{r.closeRate}%</div>
                  </div>
                ))}
              </div>
            );
          })()}
          {staleOpps.length>0&&(
            <div style={{background:C.panel,border:"1px solid #f8717133",borderRadius:14,padding:"18px 20px"}}>
              <Sec style={{color:"#f87171"}}>⚠️ Stale Opportunities</Sec>
              {staleOpps.map(o=>{const s=bOStages.find(st=>st.id===o.stage)||{label:o.stage,icon:"◎"};const sc=scores[o.id];return(
                <div key={o.id} style={{display:"flex",alignItems:"center",gap:11,background:"#090f1c",borderRadius:9,padding:"9px 13px",marginBottom:7,cursor:"pointer",transition:"background .15s"}} onMouseEnter={e=>e.currentTarget.style.background="#101c2e"} onMouseLeave={e=>e.currentTarget.style.background="#090f1c"} onClick={()=>{setNav("opps");setSelected({type:"opp",id:o.id});setSubView("detail");}}>
                  <Ava name={`${o.firstName} ${o.lastName}`} size={30}/>{sc&&<ScoreRing score={sc.total} size={34}/>}
                  <div style={{flex:1}}><div style={{fontSize:13,fontWeight:700,color:C.text}}>{o.firstName} {o.lastName}</div><div style={{fontSize:11,color:C.muted}}>{s.label}</div></div>
                  <div style={{fontSize:12,color:"#fca5a5",fontWeight:700}}>{daysSince(o.stageEnteredAt)}d stale</div>
                </div>
              );})}
            </div>
          )}
        </>}

        {analyticsTab === "reports" && <ReportsView/>}
      </div>
    );
  };

  // Reports — auto weekly/monthly snapshots, manual builder, and AI strategy reports.
  const ReportsView = () => {
    const [showGen, setShowGen] = useState(false);
    const [viewing, setViewing] = useState(null); // report being shown
    const lastAi = bReports.find(r => r.type === "ai_strategy");
    const aiCooldownMs = lastAi ? Math.max(0, 7*86400000 - (Date.now() - new Date(lastAi.generatedAt).getTime())) : 0;
    const aiCooldownDays = aiCooldownMs ? Math.ceil(aiCooldownMs / 86400000) : 0;
    const typeIcon = { weekly:"📅", monthly:"📆", manual:"📋", ai_strategy:"🤖" };
    const typeLabel = { weekly:"Weekly", monthly:"Monthly", manual:"Manual", ai_strategy:"AI Strategy" };
    return (
      <div style={{display:"flex",flexDirection:"column",gap:14}}>
        <div style={{background:C.panel,border:`1px solid ${C.border}`,borderRadius:12,padding:"14px 16px",display:"flex",alignItems:"center",gap:11,flexWrap:"wrap"}}>
          <div style={{flex:1,minWidth:200}}>
            <div style={{fontSize:13,fontWeight:800,color:C.text}}>Reports & Analytics Exports</div>
            <div style={{fontSize:11,color:C.muted,marginTop:3,lineHeight:1.5}}>Weekly + monthly reports generate automatically. Build custom ones with filters, or have AI summarize your pipeline (1×/week).</div>
          </div>
          <button onClick={()=>setShowGen(true)} style={btn("#091c09","#4ade80",true)}>+ Generate Report</button>
          <button onClick={runAiStrategyReport} disabled={aiCooldownMs > 0} title={aiCooldownMs > 0 ? `Available again in ${aiCooldownDays}d` : "Run an AI strategy advisory report"} style={{...btn("#0a1a30","#38bdf8",true),opacity: aiCooldownMs > 0 ? 0.5 : 1, cursor: aiCooldownMs > 0 ? "not-allowed":"pointer"}}>🤖 AI Strategy {aiCooldownMs > 0 ? `· ${aiCooldownDays}d` : ""}</button>
        </div>

        {bReports.length === 0 ? (
          <div style={{background:C.panel,border:`1px solid ${C.border}`,borderRadius:12,padding:"30px 22px",textAlign:"center",color:C.muted}}>
            <div style={{fontSize:30,marginBottom:9}}>📋</div>
            <div style={{fontSize:13,fontWeight:700,color:C.text,marginBottom:5}}>No reports yet</div>
            <div style={{fontSize:12,maxWidth:420,margin:"0 auto",lineHeight:1.5}}>Weekly reports are auto-generated on Mondays. Click <strong style={{color:C.text}}>+ Generate Report</strong> above to build one now with custom filters.</div>
          </div>
        ) : (
          <div style={{display:"flex",flexDirection:"column",gap:8}}>
            {bReports.map(r => (
              <div key={r.id} style={{background:C.panel,border:`1px solid ${C.border}`,borderRadius:11,padding:"11px 14px",display:"flex",alignItems:"center",gap:11}}>
                <span style={{fontSize:22,lineHeight:1}}>{typeIcon[r.type]||"📄"}</span>
                <div style={{flex:1,minWidth:0}}>
                  <div style={{fontSize:13,fontWeight:800,color:C.text,marginBottom:2}}>{r.title}</div>
                  <div style={{fontSize:11,color:C.muted}}>
                    <span>{typeLabel[r.type]||r.type}</span>
                    <span style={{margin:"0 8px"}}>·</span>
                    <span>{new Date(r.generatedAt).toLocaleString("en-US",{month:"short",day:"numeric",year:"numeric",hour:"numeric",minute:"2-digit"})}</span>
                    {r.data?.kpis && <><span style={{margin:"0 8px"}}>·</span><span>{r.data.kpis.oppCount} opps · {r.data.kpis.signed} signed · {r.data.kpis.closeRate}% close</span></>}
                  </div>
                </div>
                <button onClick={()=>setViewing(r)} style={btn(C.dim,C.accent)}>View</button>
                <button onClick={()=>printReport(r)} style={btn("#091420","#60a5fa")} title="Open print/PDF view">🖨 PDF</button>
                <button onClick={()=>downloadReportCsv(r)} style={btn("#091c09","#4ade80")} title="Download CSV (opens in Excel)">⬇ CSV</button>
                <button onClick={()=>{ if(confirm(`Delete this report?`)) deleteReport(r.id); }} title="Delete report" style={btn("#1a0808","#f87171")}>🗑</button>
              </div>
            ))}
          </div>
        )}

        {showGen && <GenerateReportModal onClose={()=>setShowGen(false)} onGenerate={(opts)=>{ generateReport(opts); setShowGen(false); }} stages={bOStages}/>}
        {viewing && <ViewReportModal report={viewing} onClose={()=>setViewing(null)} onPrint={()=>printReport(viewing)} onCsv={()=>downloadReportCsv(viewing)} stages={bOStages} onOpenRecord={(id)=>{ const isOpp = bOpps.find(o=>o.id===id); setNav(isOpp?"opps":"leads"); setSelected({type:isOpp?"opp":"lead",id}); setSubView("detail"); setViewing(null); }} onOpenBroker={(id)=>{ openBroker(id); setViewing(null); }} onOpenNetwork={(id)=>{ openNetwork(id); setViewing(null); }}/>}
      </div>
    );
  };

  // Templates
  const TemplatesView = () => {
    // State lives in App-scope refs (declared above) so a parent re-render — e.g.
    // toggling the sidebar — doesn't unmount this view and reset its editing state.
    const editing = templatesEditing;
    const setEditing = setTemplatesEditing;
    const chooseTypeFor = templatesChooseType;
    const setChooseTypeFor = setTemplatesChooseType;
    const tab = templatesTab;
    const setTab = setTemplatesTab;
    const filter = templatesFilter;
    const setFilter = setTemplatesFilter;
    const search = templatesSearch;
    const setSearch = setTemplatesSearch;
    if (editing !== null) {
      return (
        <TemplateEditor
          initial={editing === "new" ? null : editing}
          brand={brand}
          folders={bFolders}
          onSave={t => { saveTemplate(editing === "new" || editing?.fromStarter ? {...t, id: undefined} : {...editing, ...t}); setEditing(null); }}
          onCancel={() => setEditing(null)}
          onDelete={(editing && editing.id && !editing.fromStarter) ? (id) => { deleteTemplate(id); setEditing(null); } : null}
        />
      );
    }
    // Multi-brand folders: any folder with scope === "multi_brand" is visible from every brand
    // and (in SaaS) shares its template contents across brands. We merge folders from other
    // brands into bFoldersMerged so they appear in the rail, and merge their templates so the
    // grid shows cross-brand content when a multi-brand folder is selected. Folders owned by
    // *this* brand always come first; we tag merged folders with `ownerBrandId` (when it isn't
    // activeBrand) so the UI can hide edit/delete and show a brand-attribution chip.
    const multiBrandFromOtherBrands = Object.entries(templateFolders || {})
      .filter(([bId]) => bId !== activeBrand)
      .flatMap(([bId, list]) => (list||[]).filter(f => f.scope === "multi_brand").map(f => ({...f, ownerBrandId: bId})));
    const bFoldersMerged = [...bFolders, ...multiBrandFromOtherBrands];
    const multiBrandFolderIds = new Set(bFoldersMerged.filter(f => f.scope === "multi_brand").map(f => f.id));
    const crossBrandTemplates = Object.entries(customTemplates || {})
      .filter(([bId]) => bId !== activeBrand)
      .flatMap(([bId, list]) => (list||[]).filter(t => multiBrandFolderIds.has(t.category)).map(t => ({...t, ownerBrandId: bId})));
    const bTemplatesMerged = [...bTemplates, ...crossBrandTemplates];
    const matches = (t) => {
      if (filter !== "all" && t.category !== filter) return false;
      if (!search.trim()) return true;
      const q = search.toLowerCase();
      return (t.name||"").toLowerCase().includes(q) || (t.subject||"").toLowerCase().includes(q);
    };
    const myFiltered = bTemplatesMerged.filter(matches);
    const galleryFiltered = STARTER_GALLERY.filter(matches);
    const catCount = (cat) => bTemplatesMerged.filter(t => t.category === cat).length;

    const renderThumb = (t, ratio=0.32) => (
      <div style={{background:"#f8fafc",border:`1px solid ${C.border}`,borderRadius:8,height:130,overflow:"hidden",position:"relative",pointerEvents:"none"}}>
        <div style={{transform:`scale(${ratio})`,transformOrigin:"top left",width:`${100/ratio}%`,padding:"14px 18px",color:"#1e293b",fontFamily:"Helvetica,Arial,sans-serif"}}>
          {(t.mode==="html" || (!t.mode && /^</.test(t.body||""))) ? (
            <pre style={{fontSize:13,fontFamily:"ui-monospace,monospace",whiteSpace:"pre-wrap",margin:0,lineHeight:1.5}}>{(t.body||"(empty)").slice(0,500)}</pre>
          ) : (
            <div dangerouslySetInnerHTML={{__html: (t.body||"").slice(0, 1200) || "<p style='color:#94a3b8;font-style:italic'>(empty)</p>"}}/>
          )}
        </div>
      </div>
    );

    const TemplateCard = ({ t, onOpen, badge, isStarter, showActions }) => (
      <div onClick={onOpen} style={{background:C.panel,border:`1px solid ${C.border}`,borderRadius:12,padding:10,cursor:"pointer",transition:"all .15s",display:"flex",flexDirection:"column",gap:9}} onMouseEnter={e=>{e.currentTarget.style.background="#101c2e"; e.currentTarget.style.borderColor=t.color||C.accent;}} onMouseLeave={e=>{e.currentTarget.style.background=C.panel; e.currentTarget.style.borderColor=C.border;}}>
        {renderThumb(t)}
        <div style={{display:"flex",alignItems:"flex-start",gap:8}}>
          <span style={{fontSize:18,lineHeight:1,marginTop:1}}>{t.icon || (t.mode==="rich"||/^</.test(t.body||"")?"📧":"<>")}</span>
          <div style={{flex:1,minWidth:0}}>
            <div style={{fontSize:13,fontWeight:800,color:C.text,overflow:"hidden",textOverflow:"ellipsis",whiteSpace:"nowrap"}}>{t.name}</div>
            <div style={{fontSize:10,color:C.dim,textTransform:"uppercase",letterSpacing:".05em",marginTop:1}}>{(bFolders.find(f=>f.id===t.category)?.name) || (TEMPLATE_FOLDERS.find(c=>c.id===t.category)?.label) || t.category || "—"}</div>
            <div style={{fontSize:11,color:C.muted,marginTop:4,overflow:"hidden",textOverflow:"ellipsis",whiteSpace:"nowrap"}}>{t.subject || <em>(no subject)</em>}</div>
          </div>
          {badge && <span style={{background:"#1a1908",color:"#facc15",border:"1px solid #facc1544",borderRadius:5,padding:"1px 6px",fontSize:9,fontWeight:800,letterSpacing:".05em"}}>{badge}</span>}
        </div>
        {showActions && (
          <div onClick={e=>e.stopPropagation()} style={{display:"flex",gap:6,borderTop:`1px solid ${C.border}`,paddingTop:8}}>
            <button onClick={()=>setTemplatePreview(t)} title="Open preview" style={{...btn(C.dim,"#60a5fa"),flex:1,padding:"5px 8px",fontSize:11,display:"flex",alignItems:"center",justifyContent:"center",gap:5}}>👁 Open</button>
            <button onClick={()=>setEditing(t)} title="Edit template" style={{...btn(C.dim,C.accent),flex:1,padding:"5px 8px",fontSize:11,display:"flex",alignItems:"center",justifyContent:"center",gap:5}}>✏️ Edit</button>
            <button onClick={()=>{ if (confirm(`Delete template "${t.name||"Untitled"}"?`)) deleteTemplate(t.id); }} title="Delete template" style={{...btn(C.dim,"#f87171"),flex:1,padding:"5px 8px",fontSize:11,display:"flex",alignItems:"center",justifyContent:"center",gap:5}}>🗑 Delete</button>
          </div>
        )}
        {isStarter && <div style={{fontSize:10,color:C.dim,textAlign:"center",borderTop:`1px dashed ${C.border}`,paddingTop:7}}>Click to customize →</div>}
      </div>
    );

    return (
      <div>
        {/* Sub-tab nav */}
        <div style={{display:"flex",gap:4,marginBottom:14,background:"#090f1c",border:`1px solid ${C.border}`,borderRadius:10,padding:4}}>
          {[["mine",`📂 My Templates (${bTemplates.length})`],["gallery",`✨ Gallery (${STARTER_GALLERY.length})`]].map(([k,l])=>(
            <button key={k} onClick={()=>setTab(k)} style={{flex:1,background:tab===k?"#162035":"transparent",border:"none",borderRadius:7,padding:"9px 12px",color:tab===k?C.accent:C.muted,fontSize:12,fontWeight:tab===k?800:600,cursor:"pointer",fontFamily:"inherit"}}>{l}</button>
          ))}
        </div>

        {/* MINE / GALLERY tabs share search + folder rail layout */}
        {(tab === "mine" || tab === "gallery") && (
          <div style={{display:"flex",gap:14}}>
            {/* Left rail */}
            <div style={{width:220,flexShrink:0}}>
              <div style={{background:C.panel,border:`1px solid ${C.border}`,borderRadius:12,padding:10}}>
                <div style={{fontSize:10,fontWeight:800,color:C.dim,letterSpacing:".06em",padding:"4px 8px 8px"}}>FOLDERS</div>
                {/* "All" pseudo-folder + actual folder tree. Gallery still uses module-level TEMPLATE_FOLDERS
                    as its taxonomy since starters are static; "mine" uses the editable per-brand bFolders. */}
                <div onClick={()=>setFilter("all")} style={{display:"flex",alignItems:"center",gap:7,padding:"7px 9px",borderRadius:7,background:filter==="all"?"#162035":"transparent",cursor:"pointer",marginBottom:2}}>
                  <span style={{flex:1,fontSize:12,color:filter==="all"?C.accent:C.text,fontWeight:filter==="all"?700:500}}>All</span>
                  <span style={{fontSize:10,color:C.muted,fontWeight:700}}>{tab==="mine"?bTemplates.length:STARTER_GALLERY.length}</span>
                </div>
                {tab === "mine" ? (
                  // Render the merged folder tree (this brand's folders + multi-brand folders
                  // from other brands), depth-first. Cross-brand folders are non-editable here
                  // (only the owning brand can edit/delete them) and get a MULTI badge.
                  (() => {
                    const renderFolder = (folder, depth) => {
                      const sel = filter === folder.id;
                      const childCount = bTemplatesMerged.filter(t => t.category === folder.id).length;
                      const kids = bFoldersMerged.filter(f => f.parentId === folder.id);
                      const isCrossBrand = !!folder.ownerBrandId;
                      return (
                        <React.Fragment key={folder.id}>
                          <div className="ff-folder-row" onClick={()=>setFilter(folder.id)} style={{display:"flex",alignItems:"center",gap:6,padding:"7px 9px",paddingLeft: 9 + depth*14, borderRadius:7,background:sel?"#162035":"transparent",cursor:"pointer",marginBottom:2,position:"relative"}}>
                            <span style={{fontSize:13,lineHeight:1}}>{folder.icon||"📁"}</span>
                            <span style={{flex:1,fontSize:12,color:sel?C.accent:C.text,fontWeight:sel?700:500,overflow:"hidden",textOverflow:"ellipsis",whiteSpace:"nowrap"}}>{folder.name}</span>
                            {folder.scope==="group" && <span title="Group folder (shared across seats on this brand)" style={{fontSize:8,fontWeight:800,color:"#86efac",background:"#0a1f12",border:"1px solid #4ade8033",borderRadius:4,padding:"1px 4px",letterSpacing:".05em"}}>GROUP</span>}
                            {folder.scope==="multi_brand" && <span title={`Multi-brand folder — shared across every brand${isCrossBrand?` (owned by ${(brands.find(b=>b.id===folder.ownerBrandId)?.name)||"another brand"})`:""}`} style={{fontSize:8,fontWeight:800,color:"#c4b5fd",background:"#1a1429",border:"1px solid #a78bfa44",borderRadius:4,padding:"1px 4px",letterSpacing:".05em"}}>MULTI</span>}
                            <span style={{fontSize:10,color:C.muted,fontWeight:700}}>{childCount}</span>
                            {!isCrossBrand && <span className="ff-folder-actions" onClick={e=>e.stopPropagation()} style={{display:"none",position:"absolute",right:6,background:sel?"#162035":C.panel,paddingLeft:6,gap:3}}>
                              <button onClick={()=>setFolderModal({mode:"edit", folder})} title="Edit folder" style={{...btn(C.dim,C.muted),padding:"2px 6px",fontSize:11}}>✏️</button>
                              <button onClick={()=>{ if(confirm(`Delete folder "${folder.name}"? Templates inside will move to the first remaining folder.`)) deleteFolder(folder.id); }} title="Delete folder" style={{...btn("#1a0808","#f87171"),padding:"2px 6px",fontSize:11}}>🗑</button>
                            </span>}
                          </div>
                          {kids.map(k => renderFolder(k, depth+1))}
                        </React.Fragment>
                      );
                    };
                    return bFoldersMerged.filter(f => !f.parentId).map(f => renderFolder(f, 0));
                  })()
                ) : (
                  TEMPLATE_FOLDERS.map(c => {
                    const sel = filter === c.id;
                    const count = STARTER_GALLERY.filter(t => t.category === c.id).length;
                    return (
                      <div key={c.id} onClick={()=>setFilter(c.id)} style={{display:"flex",alignItems:"center",gap:7,padding:"7px 9px",borderRadius:7,background:sel?"#162035":"transparent",cursor:"pointer",marginBottom:2}}>
                        <span style={{fontSize:13,lineHeight:1}}>{c.icon}</span>
                        <span style={{flex:1,fontSize:12,color:sel?C.accent:C.text,fontWeight:sel?700:500}}>{c.label}</span>
                        <span style={{fontSize:10,color:C.muted,fontWeight:700}}>{count}</span>
                      </div>
                    );
                  })
                )}
                {tab === "mine" && (
                  <button onClick={()=>setFolderModal({mode:"new", parentId:null})} style={{display:"flex",alignItems:"center",gap:6,width:"100%",background:"transparent",border:`1px dashed ${C.border}`,color:C.muted,borderRadius:7,padding:"7px 9px",marginTop:6,cursor:"pointer",fontSize:11,fontFamily:"inherit",fontWeight:600}} title="Create a new folder">
                    <span style={{fontSize:13}}>＋</span><span>New folder</span>
                  </button>
                )}
              </div>
              <style>{`.ff-folder-row:hover .ff-folder-actions { display: flex !important; }`}</style>
              {tab === "mine" && (
                <button onClick={()=>{ setChooseFolderId(filter !== "all" && bFolders.find(f=>f.id===filter) ? filter : (bFolders[0]?.id || null)); setChooseTypeFor("new"); }} style={{...btn("#091c09","#4ade80",true),width:"100%",marginTop:10,padding:"10px 14px"}}>+ New Template</button>
              )}
            </div>

            {/* Main column */}
            <div style={{flex:1,minWidth:0}}>
              <div style={{display:"flex",gap:8,marginBottom:12,alignItems:"center"}}>
                <input value={search} onChange={e=>setSearch(e.target.value)} placeholder={`🔍 Search ${tab==="mine"?"your templates":"gallery"}…`} style={inp({flex:1,boxSizing:"border-box"})}/>
                {search && <button onClick={()=>setSearch("")} title="Clear search" style={btn(C.dim,C.muted)}>✕</button>}
              </div>

              {tab === "mine" && (
                <>
                  {bTemplates.length === 0 ? (
                    <div style={{background:C.panel,border:`1px solid ${C.border}`,borderRadius:12,padding:"38px 24px",textAlign:"center",color:C.muted}}>
                      <div style={{fontSize:36,marginBottom:10}}>📧</div>
                      <div style={{fontWeight:800,color:C.text,marginBottom:6,fontSize:14}}>No custom templates yet</div>
                      <div style={{fontSize:12,marginBottom:14,maxWidth:420,margin:"0 auto 14px",lineHeight:1.5}}>Start from a blank canvas or pick a starter from the <strong style={{color:C.text}}>Gallery</strong> tab and customize it. Use merge tags like {`{FIRST_NAME}`} and {`{BRAND}`} for personalization.</div>
                      <div style={{display:"flex",gap:8,justifyContent:"center"}}>
                        <button onClick={()=>{ setChooseFolderId(filter !== "all" && bFolders.find(f=>f.id===filter) ? filter : (bFolders[0]?.id || null)); setChooseTypeFor("new"); }} style={btn("#091c09","#4ade80",true)}>+ Blank Template</button>
                        <button onClick={()=>setTab("gallery")} style={btn("#091420","#60a5fa")}>Browse Gallery →</button>
                      </div>
                    </div>
                  ) : myFiltered.length === 0 ? (
                    <div style={{background:C.panel,border:`1px solid ${C.border}`,borderRadius:12,padding:"24px",textAlign:"center",color:C.muted,fontSize:12}}>No templates match your filter.</div>
                  ) : (
                    <div style={{display:"grid",gridTemplateColumns:"repeat(auto-fill,minmax(240px,1fr))",gap:12}}>
                      {myFiltered.map(t => <TemplateCard key={t.id} t={t} onOpen={()=>setEditing(t)} showActions/>)}
                    </div>
                  )}
                </>
              )}

              {tab === "gallery" && (
                <>
                  <div style={{background:"#091420",border:"1px solid #60a5fa33",borderRadius:10,padding:"10px 13px",fontSize:11,color:"#9bb6e0",marginBottom:12,lineHeight:1.5}}>
                    ✨ <strong style={{color:C.text}}>Starter Gallery</strong> — pre-designed templates you can use as a base. Click any to open in the editor with content prefilled. Saving creates a copy in <strong style={{color:C.text}}>My Templates</strong>; the original stays in the gallery.
                  </div>
                  {galleryFiltered.length === 0 ? (
                    <div style={{background:C.panel,border:`1px solid ${C.border}`,borderRadius:12,padding:"24px",textAlign:"center",color:C.muted,fontSize:12}}>No starters match your filter.</div>
                  ) : (
                    <div style={{display:"grid",gridTemplateColumns:"repeat(auto-fill,minmax(240px,1fr))",gap:12}}>
                      {galleryFiltered.map(t => <TemplateCard key={t.id} t={{...t, mode:"rich"}} onOpen={()=>setEditing({ name:t.name, category:t.category, mode:"rich", subject:t.subject, body:t.body, blocks:t.blocks, color:t.color, fromStarter:true })} badge="STARTER" isStarter/>)}
                    </div>
                  )}
                </>
              )}
            </div>
          </div>
        )}

        {/* Template-type chooser — first decision a rep makes when starting a new template:
            block builder for visual marketing emails, or plain/HTML for paste-anything power users. */}
        {chooseTypeFor && (
          <div onClick={()=>setChooseTypeFor(null)} style={{position:"fixed",inset:0,background:"#000c",zIndex:1100,display:"flex",alignItems:"center",justifyContent:"center",padding:16}}>
            <div onClick={e=>e.stopPropagation()} style={{background:C.panel,border:`1px solid ${C.border}`,borderRadius:14,padding:22,width:"100%",maxWidth:520}}>
              <div style={{display:"flex",alignItems:"center",gap:12,marginBottom:14}}>
                <span style={{fontSize:24}}>📧</span>
                <div style={{flex:1}}>
                  <h3 style={{margin:0,color:"#f0f6ff",fontWeight:900,fontSize:16}}>New email template</h3>
                  <div style={{fontSize:12,color:C.muted,marginTop:3}}>Pick a folder and how you want to build it.</div>
                </div>
                <button onClick={()=>setChooseTypeFor(null)} title="Close" style={btn(C.dim,C.muted)}>✕</button>
              </div>
              {/* Folder picker — defaults to the currently-filtered folder (or the first folder).
                  Rendered with "/" separators for nested folders so the parent is always visible. */}
              <div style={{fontSize:9,fontWeight:800,color:C.dim,letterSpacing:".08em",marginBottom:6}}>SAVE TO FOLDER</div>
              <select value={chooseFolderId || ""} onChange={e=>setChooseFolderId(e.target.value||null)} style={inp({width:"100%",boxSizing:"border-box",fontSize:12,padding:"9px 11px",marginBottom:14})}>
                {(bFoldersMerged.length ? bFoldersMerged : TEMPLATE_FOLDERS.map(f=>({...f, name:f.label}))).map(f => {
                  const folderMap = new Map(bFoldersMerged.map(x => [x.id, x]));
                  const chain = []; let cur = f;
                  while (cur) { chain.unshift((cur.icon||"📁")+" "+(cur.name||cur.label)); cur = cur.parentId ? folderMap.get(cur.parentId) : null; if (chain.length > 5) break; }
                  const scopeTag = f.scope==="multi_brand" ? "  🏢" : f.scope==="group" ? "  🌐" : "";
                  return <option key={f.id} value={f.id}>{chain.join(" / ")}{scopeTag}</option>;
                })}
              </select>
              <div style={{fontSize:9,fontWeight:800,color:C.dim,letterSpacing:".08em",marginBottom:6}}>FORMAT</div>
              <div style={{display:"flex",flexDirection:"column",gap:9}}>
                <button onClick={()=>{ const cat = chooseFolderId || bFolders[0]?.id || TEMPLATE_FOLDERS[0]?.id || "marketing"; setChooseTypeFor(null); setEditing({ name:"", category:cat, mode:"rich", subject:"", body:"", color:"#3b82f6", blocks:[] }); }} style={{background:"#091420",border:`1.5px solid ${C.accent}66`,borderRadius:10,padding:"14px 16px",cursor:"pointer",fontFamily:"inherit",textAlign:"left",color:C.text}}>
                  <div style={{display:"flex",alignItems:"center",gap:11}}>
                    <span style={{fontSize:22}}>🎨</span>
                    <div style={{flex:1}}>
                      <div style={{fontSize:14,fontWeight:800,color:"#f0f6ff",marginBottom:2}}>Block builder</div>
                      <div style={{fontSize:11,color:C.muted,lineHeight:1.5}}>Drag visual blocks (banners, images, columns, buttons) — for marketing-style emails. Best for the standard {BRAND.name} workflow.</div>
                    </div>
                  </div>
                </button>
                <button onClick={()=>{ const cat = chooseFolderId || bFolders[0]?.id || TEMPLATE_FOLDERS[0]?.id || "marketing"; setChooseTypeFor(null); setEditing({ name:"", category:cat, mode:"html", subject:"", body:"", color:"#3b82f6" }); }} style={{background:"#090f1c",border:`1.5px solid ${C.border}`,borderRadius:10,padding:"14px 16px",cursor:"pointer",fontFamily:"inherit",textAlign:"left",color:C.text}}>
                  <div style={{display:"flex",alignItems:"center",gap:11}}>
                    <span style={{fontSize:22,fontFamily:"ui-monospace,monospace"}}>{"<>"}</span>
                    <div style={{flex:1}}>
                      <div style={{fontSize:14,fontWeight:800,color:"#f0f6ff",marginBottom:2}}>Plain text / HTML</div>
                      <div style={{fontSize:11,color:C.muted,lineHeight:1.5}}>Paste raw HTML from Mailchimp / Litmus / hand-coded designs, or write a plain-text email. Merge tags still work.</div>
                    </div>
                  </div>
                </button>
              </div>
            </div>
          </div>
        )}

      </div>
    );
  };

  // Automations
  const AutomationsView = () => {
    const [editing, setEditing] = useState(null);
    const [aiOpen, setAiOpen] = useState(false);
    if (editing !== null) {
      return (
        <AutomationRuleBuilder
          initial={editing === "new" ? null : editing}
          stages={bOStages}
          leadStages={bLStages}
          templates={bTemplates}
          brokers={bBrokers}
          onSave={r => { saveAutomation(editing === "new" ? r : {...editing, ...r}); setEditing(null); }}
          onCancel={() => setEditing(null)}
        />
      );
    }
    return (
      <div>
        <div style={{display:"flex",alignItems:"center",gap:10,marginBottom:14}}>
          <Sec style={{margin:0}}>Automation Rules ({bAutos.length})</Sec>
          <button onClick={()=>setAiOpen(true)} style={{...btn("#0a1a30","#38bdf8",true),marginLeft:"auto"}} title="View & toggle the AI-driven automations that run in the background">🤖 AI Automations</button>
          <button onClick={()=>setEditing("new")} style={btn("#091c09","#4ade80",true)}>+ New Automation</button>
        </div>
        {aiOpen && <AiAutomationsModal settings={settings} setSetting={setSetting} hasApiKey={hasApiKey()} onClose={()=>setAiOpen(false)}/>}
        {bAutos.length===0 ? (
          <div style={{background:C.panel,border:`1px solid ${C.border}`,borderRadius:12,padding:"30px 24px",textAlign:"center",color:C.muted}}>
            <div style={{fontSize:32,marginBottom:8}}>⚡</div>
            <div style={{fontWeight:800,color:C.text,marginBottom:5}}>No automations yet</div>
            <div style={{fontSize:12,marginBottom:14,maxWidth:440,margin:"0 auto 14px"}}>Build <strong>WHEN → IF → THEN</strong> rules to auto-send templates, change stages, notify brokers, or flag risky opportunities.</div>
            <button onClick={()=>setEditing("new")} style={btn("#091c09","#4ade80",true)}>+ Create Your First Automation</button>
          </div>
        ) : (
          <div style={{display:"flex",flexDirection:"column",gap:9}}>
            {bAutos.map(rule=>(
              <div key={rule.id} style={{background:C.panel,border:`1px solid ${C.border}`,borderRadius:12,padding:"12px 16px"}}>
                <div style={{display:"flex",alignItems:"center",gap:10,marginBottom:9}}>
                  <button onClick={()=>toggleAutomation(rule.id)} title={rule.enabled?"Disable":"Enable"} style={{background:rule.enabled?"#091c09":"#1a0808",border:`1px solid ${rule.enabled?"#4ade80":"#f87171"}55`,borderRadius:14,padding:"3px 11px",fontSize:10,fontWeight:800,color:rule.enabled?"#4ade80":"#f87171",cursor:"pointer",fontFamily:"inherit"}}>{rule.enabled?"● ON":"○ OFF"}</button>
                  <div style={{flex:1,minWidth:0}}>
                    <div style={{fontSize:14,fontWeight:800,color:C.text}}>{rule.name||"Untitled rule"}</div>
                    <div style={{fontSize:11,color:C.muted}}>{(rule.conditions||[]).length} condition{rule.conditions?.length===1?"":"s"} · {(rule.actions||[]).length} action{rule.actions?.length===1?"":"s"}</div>
                  </div>
                  <button onClick={()=>setEditing(rule)} style={btn(C.dim,C.muted)}>✏️ Edit</button>
                  <button onClick={()=>{ if(confirm(`Delete "${rule.name||"this rule"}"?`)) deleteAutomation(rule.id); }} title="Delete automation" style={btn("#1a0808","#f87171")}>🗑</button>
                </div>
                <div style={{display:"flex",gap:7,fontSize:11,flexWrap:"wrap"}}>
                  <span style={{background:"#091420",color:"#60a5fa",border:"1px solid #60a5fa44",borderRadius:6,padding:"2px 9px"}}>WHEN · {(AUTOMATION_TRIGGERS.find(t=>t.id===rule.trigger?.type)?.label)||"?"}</span>
                  {(rule.conditions||[]).map((c,i)=>(<span key={i} style={{background:"#1a1908",color:"#facc15",border:"1px solid #facc1544",borderRadius:6,padding:"2px 9px"}}>IF · {c.field||"?"} {c.operator} {c.value||"?"}</span>))}
                  {(rule.actions||[]).map((a,i)=>(<span key={i} style={{background:"#091c09",color:"#4ade80",border:"1px solid #4ade8044",borderRadius:6,padding:"2px 9px"}}>THEN · {(AUTOMATION_ACTIONS.find(x=>x.id===a.type)?.label)||a.type}</span>))}
                </div>
                {rule.lastRun && <div style={{fontSize:10,color:C.dim,marginTop:7}}>Last fired {fmtDate(rule.lastRun)} · {rule.runCount||0} run{rule.runCount===1?"":"s"}</div>}
              </div>
            ))}
            <div style={{background:"#1a1908",border:"1px solid #facc1533",borderRadius:10,padding:"10px 14px",fontSize:11,color:"#facc15",marginTop:6}}>
              ⓘ Rules are stored locally and shown here; live execution requires connecting an automation backend (Zapier, Make, or a custom worker).
            </div>
          </div>
        )}
      </div>
    );
  };

  // Broker Hub
  const BrokerHubView = () => {
    const [tab, setTab] = useState("brokers");
    const [showNetForm, setShowNetForm] = useState(false);
    const [showBrkForm, setShowBrkForm] = useState(false);
    const [editingNet, setEditingNet] = useState(null);
    const [editingBrk, setEditingBrk] = useState(null);
    const [netBrokerSearch, setNetBrokerSearch] = useState("");
    const [brokerNoteDraft, setBrokerNoteDraft] = useState("");
    const [networkNoteDraft, setNetworkNoteDraft] = useState("");
    const oppsWithBroker = bOpps.filter(o => o.brokerId).length;
    const allComms = Object.entries(brokerComms).flatMap(([oppId, msgs]) => (msgs||[]).map(m => ({...m, oppId})));

    // ─── Broker profile ─────────────────────────────────────────
    if (brokerDetailId) {
      const broker = bBrokers.find(b => b.id === brokerDetailId);
      if (!broker) {
        // Broker was deleted while viewed — bounce back to the hub.
        return <div style={{padding:"40px 20px",textAlign:"center",color:C.muted}}>
          <div style={{fontSize:32,marginBottom:8}}>🤷</div>
          <div style={{fontWeight:800,color:C.text,marginBottom:5}}>This broker no longer exists.</div>
          <button onClick={()=>setBrokerDetailId(null)} style={btn(C.dim,C.muted)}>← Back to Broker Hub</button>
        </div>;
      }
      const net = bNetworks.find(n => n.id === broker.networkId);
      const assignedOpps = bOpps.filter(o => o.brokerId === broker.id);
      const oppIds = new Set(assignedOpps.map(o => o.id));
      const myComms = Object.entries(brokerComms)
        .filter(([oppId]) => oppIds.has(oppId))
        .flatMap(([oppId, msgs]) => (msgs||[]).filter(m => m.brokerId === broker.id).map(m => ({...m, oppId})))
        .sort((a,b) => new Date(b.at) - new Date(a.at));
      const notesLog = (broker.notesLog||[]).slice().sort((a,b) => new Date(b.at) - new Date(a.at));
      return (
        <div>
          <div style={{display:"flex",alignItems:"center",gap:9,marginBottom:14}}>
            <button onClick={()=>setBrokerDetailId(null)} style={btn(C.dim,C.muted)} title="Back to Broker Hub">← Broker Hub</button>
          </div>
          <div style={{background:C.panel,border:`1px solid ${C.border}`,borderRadius:14,padding:"22px 24px",marginBottom:14,display:"flex",gap:18,alignItems:"flex-start"}}>
            <Ava name={`${broker.firstName} ${broker.lastName}`} size={64}/>
            <div style={{flex:1,minWidth:0}}>
              <div style={{display:"flex",alignItems:"center",gap:8,flexWrap:"wrap",marginBottom:6}}>
                <div style={{fontSize:22,fontWeight:900,color:"#f0f6ff"}}>{broker.firstName} {broker.lastName}</div>
                {net && <RecordLink onClick={()=>openNetwork(net.id)} color={net.color||"#94a3b8"} weight={700}><span style={{background:(net.color||"#1e3a5f")+"22",border:`1px solid ${(net.color||"#1e3a5f")}44`,borderRadius:5,padding:"2px 8px",fontSize:11,fontWeight:700}}>{net.name}</span></RecordLink>}
                {broker.specialty && <span style={{background:"#1a1908",color:"#facc15",border:"1px solid #facc1533",borderRadius:5,padding:"2px 8px",fontSize:11,fontWeight:700}}>{broker.specialty}</span>}
              </div>
              <div style={{fontSize:12,color:C.muted,marginBottom:8}}>{[broker.email, broker.phone].filter(Boolean).join(" · ") || <em>no contact info</em>}</div>
              {broker.commission && <div style={{fontSize:11,color:C.dim,marginBottom:4}}><strong style={{color:C.muted}}>Commission:</strong> {broker.commission}</div>}
              {broker.notes && <div style={{fontSize:12,color:C.muted,marginTop:8,lineHeight:1.55,background:"#090f1c",border:`1px solid ${C.border}`,borderRadius:8,padding:"10px 12px",whiteSpace:"pre-wrap"}}>{broker.notes}</div>}
            </div>
            <div style={{display:"flex",flexDirection:"column",gap:7}}>
              {broker.email && <a href={`mailto:${broker.email}`} title={`Email ${broker.firstName}`} style={{...btn("#091420","#60a5fa"),textDecoration:"none"}}>✉️ Email</a>}
              {broker.phone && <a href={`tel:${broker.phone}`}   title={`Call ${broker.firstName}`} style={{...btn("#091c09","#4ade80"),textDecoration:"none"}}>📞 Call</a>}
              <button onClick={()=>{setEditingBrk(broker);setShowBrkForm(true);}} title="Edit broker" style={btn(C.dim,C.muted)}>✏️ Edit</button>
              <button onClick={()=>{if(confirm(`Delete ${broker.firstName} ${broker.lastName}?`)){deleteBroker(broker.id);setBrokerDetailId(null);}}} title="Delete broker" style={btn("#1a0808","#f87171")}>🗑 Delete</button>
            </div>
          </div>

          {/* Assigned opportunities */}
          <div style={{background:C.panel,border:`1px solid ${C.border}`,borderRadius:12,padding:"14px 16px",marginBottom:14}}>
            <div style={{fontSize:10,fontWeight:800,color:C.dim,letterSpacing:".06em",marginBottom:9}}>ASSIGNED OPPORTUNITIES ({assignedOpps.length})</div>
            {assignedOpps.length === 0 ? (
              <div style={{fontSize:12,color:C.muted,fontStyle:"italic"}}>No opportunities currently assigned to this broker.</div>
            ) : (
              <div style={{display:"flex",flexDirection:"column",gap:6}}>
                {assignedOpps.map(o => (
                  <div key={o.id} style={{display:"flex",alignItems:"center",gap:9,background:"#090f1c",borderRadius:8,padding:"8px 12px"}}>
                    <div style={{flex:1,minWidth:0}}>
                      <RecordLink onClick={()=>{setNav("opps");setSelected({type:"opp",id:o.id});setSubView("detail");setBrokerDetailId(null);}} color={C.text} weight={700}>{o.firstName} {o.lastName}</RecordLink>
                      <div style={{fontSize:10,color:C.muted,marginTop:2}}>{(bOStages.find(s=>s.id===o.stage)?.label)||o.stage}</div>
                    </div>
                    {o.score > 0 && <div style={{fontSize:12,fontWeight:700,color:"#fb923c",width:32,textAlign:"right"}}>{o.score}</div>}
                  </div>
                ))}
              </div>
            )}
          </div>

          {/* Notes log */}
          <div style={{background:C.panel,border:`1px solid ${C.border}`,borderRadius:12,padding:"14px 16px",marginBottom:14}}>
            <div style={{fontSize:10,fontWeight:800,color:C.dim,letterSpacing:".06em",marginBottom:9}}>NOTES LOG ({notesLog.length})</div>
            <div style={{display:"flex",gap:8,marginBottom:11}}>
              <textarea value={brokerNoteDraft} onChange={e=>setBrokerNoteDraft(e.target.value)} placeholder="Add a note about this broker — placement quality, response times, recent conversations, etc." style={{...inp({flex:1,boxSizing:"border-box",resize:"vertical",minHeight:60,fontFamily:"inherit"})}}/>
              <button onClick={()=>{ addBrokerNote(broker.id, brokerNoteDraft); setBrokerNoteDraft(""); }} disabled={!brokerNoteDraft.trim()} style={{...btn("#091c09","#4ade80",!!brokerNoteDraft.trim()),opacity:brokerNoteDraft.trim()?1:0.5,cursor:brokerNoteDraft.trim()?"pointer":"not-allowed",alignSelf:"flex-start"}}>+ Add Note</button>
            </div>
            {notesLog.length === 0 ? (
              <div style={{fontSize:12,color:C.muted,fontStyle:"italic"}}>No notes yet — log placements, conversations, or context for next time.</div>
            ) : (
              <div style={{display:"flex",flexDirection:"column",gap:7}}>
                {notesLog.map(n => (
                  <div key={n.id} style={{background:"#090f1c",border:`1px solid ${C.border}`,borderRadius:8,padding:"9px 12px"}}>
                    <div style={{display:"flex",alignItems:"center",justifyContent:"space-between",marginBottom:5}}>
                      <div style={{fontSize:10,color:C.dim,fontWeight:700}}>{new Date(n.at).toLocaleString("en-US",{month:"short",day:"numeric",year:"numeric",hour:"numeric",minute:"2-digit"})}</div>
                      <button onClick={()=>{ if(confirm("Delete this note?")) deleteBrokerNote(broker.id, n.id); }} title="Delete note" style={{background:"transparent",border:"none",color:C.dim,fontSize:11,cursor:"pointer",fontFamily:"inherit",padding:"2px 6px"}}>✕</button>
                    </div>
                    <div style={{fontSize:12,color:C.text,lineHeight:1.55,whiteSpace:"pre-wrap"}}>{n.body}</div>
                  </div>
                ))}
              </div>
            )}
          </div>

          {/* Communication history */}
          <div style={{background:C.panel,border:`1px solid ${C.border}`,borderRadius:12,padding:"14px 16px"}}>
            <div style={{fontSize:10,fontWeight:800,color:C.dim,letterSpacing:".06em",marginBottom:9}}>COMMUNICATION HISTORY ({myComms.length})</div>
            {myComms.length === 0 ? (
              <div style={{fontSize:12,color:C.muted,fontStyle:"italic"}}>No communications logged yet. Email / SMS / call activity from opportunity profiles will appear here.</div>
            ) : (
              <div style={{display:"flex",flexDirection:"column",gap:6}}>
                {myComms.map((m,i) => {
                  const o = bOpps.find(x => x.id === m.oppId);
                  return (
                    <div key={m.id||i} style={{background:"#090f1c",border:`1px solid ${C.border}`,borderRadius:8,padding:"8px 12px"}}>
                      <div style={{display:"flex",alignItems:"center",gap:8,marginBottom:3}}>
                        <span style={{fontSize:11,fontWeight:700,color:m.kind==="email"?"#60a5fa":m.kind==="sms"?"#a78bfa":m.kind==="call"?"#4ade80":C.muted}}>{m.kind?.toUpperCase()||"NOTE"}</span>
                        {o && <span style={{fontSize:11,color:C.muted}}>· re: <RecordLink onClick={()=>{setNav("opps");setSelected({type:"opp",id:o.id});setSubView("detail");setBrokerDetailId(null);}} color="#60a5fa" weight={600}>{o.firstName} {o.lastName}</RecordLink></span>}
                        <span style={{fontSize:10,color:C.dim,marginLeft:"auto"}}>{new Date(m.at).toLocaleString("en-US",{month:"short",day:"numeric",hour:"numeric",minute:"2-digit"})}</span>
                      </div>
                      {m.body && <div style={{fontSize:12,color:C.text,lineHeight:1.5,whiteSpace:"pre-wrap"}}>{m.body}</div>}
                    </div>
                  );
                })}
              </div>
            )}
          </div>

          {showBrkForm && <BrokerForm initial={editingBrk} networks={bNetworks} onSave={brk=>{saveBroker(editingBrk?{...editingBrk,...brk}:brk);setShowBrkForm(false);setEditingBrk(null);}} onCancel={()=>{setShowBrkForm(false);setEditingBrk(null);}}/>}
        </div>
      );
    }

    // ─── Network profile ────────────────────────────────────────
    if (networkDetailId) {
      const network = bNetworks.find(n => n.id === networkDetailId);
      if (!network) {
        return <div style={{padding:"40px 20px",textAlign:"center",color:C.muted}}>
          <div style={{fontSize:32,marginBottom:8}}>🤷</div>
          <div style={{fontWeight:800,color:C.text,marginBottom:5}}>This network no longer exists.</div>
          <button onClick={()=>setNetworkDetailId(null)} style={btn(C.dim,C.muted)}>← Back to Broker Hub</button>
        </div>;
      }
      const networkBrokers = bBrokers.filter(b => b.networkId === network.id);
      const q = netBrokerSearch.trim().toLowerCase();
      const filteredBrokers = !q ? networkBrokers : networkBrokers.filter(b => {
        const hay = [b.firstName, b.lastName, b.email, b.phone, b.specialty].filter(Boolean).join(" ").toLowerCase();
        return hay.includes(q);
      });
      const oppsViaNetwork = bOpps.filter(o => networkBrokers.some(b => b.id === o.brokerId));
      const notesLog = (network.notesLog||[]).slice().sort((a,b) => new Date(b.at) - new Date(a.at));
      return (
        <div>
          <div style={{display:"flex",alignItems:"center",gap:9,marginBottom:14}}>
            <button onClick={()=>setNetworkDetailId(null)} style={btn(C.dim,C.muted)} title="Back to Broker Hub">← Broker Hub</button>
          </div>
          <div style={{background:C.panel,border:`1px solid ${C.border}`,borderRadius:14,padding:"22px 24px",marginBottom:14,display:"flex",gap:18,alignItems:"flex-start"}}>
            <div style={{width:64,height:64,borderRadius:14,background:network.color||"#1e3a5f",display:"flex",alignItems:"center",justifyContent:"center",fontSize:28,fontWeight:900,color:"#fff",flexShrink:0}}>{network.name?.[0]?.toUpperCase()||"?"}</div>
            <div style={{flex:1,minWidth:0}}>
              <div style={{fontSize:22,fontWeight:900,color:"#f0f6ff",marginBottom:4}}>{network.name}</div>
              <div style={{fontSize:12,color:C.muted,marginBottom:6}}>{networkBrokers.length} broker{networkBrokers.length===1?"":"s"} · {oppsViaNetwork.length} active opp{oppsViaNetwork.length===1?"":"s"} via this network</div>
              {network.website && <div style={{fontSize:12,marginBottom:3}}><a href={network.website.startsWith("http")?network.website:`https://${network.website}`} target="_blank" rel="noreferrer" style={{color:"#60a5fa",textDecoration:"none"}}>{network.website}</a></div>}
              {network.phone && <div style={{fontSize:12,color:C.muted,marginBottom:2}}>📞 {network.phone}</div>}
              {network.email && <div style={{fontSize:12,color:C.muted,marginBottom:2}}>✉️ {network.email}</div>}
              {network.notes && <div style={{fontSize:12,color:C.muted,marginTop:8,lineHeight:1.55,background:"#090f1c",border:`1px solid ${C.border}`,borderRadius:8,padding:"10px 12px",whiteSpace:"pre-wrap"}}>{network.notes}</div>}
            </div>
            <div style={{display:"flex",flexDirection:"column",gap:7}}>
              <button onClick={()=>{setEditingNet(network);setShowNetForm(true);}} title="Edit network" style={btn(C.dim,C.muted)}>✏️ Edit</button>
              <button onClick={()=>{if(confirm(`Delete "${network.name}"? Brokers in this network will remain but be unassigned.`)){deleteNetwork(network.id);setNetworkDetailId(null);}}} title="Delete network" style={btn("#1a0808","#f87171")}>🗑 Delete</button>
            </div>
          </div>

          {/* Searchable broker roster */}
          <div style={{background:C.panel,border:`1px solid ${C.border}`,borderRadius:12,padding:"14px 16px",marginBottom:14}}>
            <div style={{display:"flex",alignItems:"center",gap:9,marginBottom:11}}>
              <div style={{fontSize:10,fontWeight:800,color:C.dim,letterSpacing:".06em"}}>BROKER ROSTER ({filteredBrokers.length}{q?` of ${networkBrokers.length}`:""})</div>
              <input value={netBrokerSearch} onChange={e=>setNetBrokerSearch(e.target.value)} placeholder="🔍 Search brokers in this network…" style={inp({flex:1,boxSizing:"border-box",marginLeft:"auto",fontSize:12,padding:"7px 11px"})}/>
              <button onClick={()=>{setEditingBrk({networkId: network.id});setShowBrkForm(true);}} style={btn("#091c09","#4ade80")} title="Add a broker to this network">+ Add Broker</button>
            </div>
            {networkBrokers.length === 0 ? (
              <div style={{fontSize:12,color:C.muted,fontStyle:"italic",padding:"12px 0"}}>No brokers in this network yet.</div>
            ) : filteredBrokers.length === 0 ? (
              <div style={{fontSize:12,color:C.muted,fontStyle:"italic",padding:"12px 0"}}>No brokers match "{netBrokerSearch}".</div>
            ) : (
              <div style={{display:"flex",flexDirection:"column",gap:6}}>
                {filteredBrokers.map(b => {
                  const assignedCount = bOpps.filter(o => o.brokerId === b.id).length;
                  return (
                    <div key={b.id} style={{display:"flex",alignItems:"center",gap:10,background:"#090f1c",borderRadius:8,padding:"8px 12px"}}>
                      <Ava name={`${b.firstName} ${b.lastName}`} size={32}/>
                      <div style={{flex:1,minWidth:0}}>
                        <div style={{fontSize:13,fontWeight:700}}>
                          <RecordLink onClick={()=>openBroker(b.id)} color={C.text} weight={700}>{b.firstName} {b.lastName}</RecordLink>
                          {b.specialty && <span style={{fontSize:10,color:"#facc15",marginLeft:8,fontWeight:600}}>· {b.specialty}</span>}
                        </div>
                        <div style={{fontSize:11,color:C.muted,overflow:"hidden",textOverflow:"ellipsis",whiteSpace:"nowrap"}}>{[b.email, b.phone].filter(Boolean).join(" · ")||<em>no contact info</em>}</div>
                      </div>
                      {assignedCount>0 && <div style={{fontSize:10,color:"#4ade80",fontWeight:700}}>{assignedCount} opp{assignedCount===1?"":"s"}</div>}
                      {b.email && <a href={`mailto:${b.email}`} title={`Email ${b.firstName}`} style={{...btn("#091420","#60a5fa"),textDecoration:"none",padding:"4px 9px",fontSize:11}}>✉️</a>}
                      {b.phone && <a href={`tel:${b.phone}`}   title={`Call ${b.firstName}`}  style={{...btn("#091c09","#4ade80"),textDecoration:"none",padding:"4px 9px",fontSize:11}}>📞</a>}
                    </div>
                  );
                })}
              </div>
            )}
          </div>

          {/* Notes log */}
          <div style={{background:C.panel,border:`1px solid ${C.border}`,borderRadius:12,padding:"14px 16px"}}>
            <div style={{fontSize:10,fontWeight:800,color:C.dim,letterSpacing:".06em",marginBottom:9}}>NOTES LOG ({notesLog.length})</div>
            <div style={{display:"flex",gap:8,marginBottom:11}}>
              <textarea value={networkNoteDraft} onChange={e=>setNetworkNoteDraft(e.target.value)} placeholder="Add a note about this network — billing terms, relationship updates, etc." style={{...inp({flex:1,boxSizing:"border-box",resize:"vertical",minHeight:60,fontFamily:"inherit"})}}/>
              <button onClick={()=>{ addNetworkNote(network.id, networkNoteDraft); setNetworkNoteDraft(""); }} disabled={!networkNoteDraft.trim()} style={{...btn("#091c09","#4ade80",!!networkNoteDraft.trim()),opacity:networkNoteDraft.trim()?1:0.5,cursor:networkNoteDraft.trim()?"pointer":"not-allowed",alignSelf:"flex-start"}}>+ Add Note</button>
            </div>
            {notesLog.length === 0 ? (
              <div style={{fontSize:12,color:C.muted,fontStyle:"italic"}}>No notes yet — track contract details, key contacts, or relationship history.</div>
            ) : (
              <div style={{display:"flex",flexDirection:"column",gap:7}}>
                {notesLog.map(n => (
                  <div key={n.id} style={{background:"#090f1c",border:`1px solid ${C.border}`,borderRadius:8,padding:"9px 12px"}}>
                    <div style={{display:"flex",alignItems:"center",justifyContent:"space-between",marginBottom:5}}>
                      <div style={{fontSize:10,color:C.dim,fontWeight:700}}>{new Date(n.at).toLocaleString("en-US",{month:"short",day:"numeric",year:"numeric",hour:"numeric",minute:"2-digit"})}</div>
                      <button onClick={()=>{ if(confirm("Delete this note?")) deleteNetworkNote(network.id, n.id); }} title="Delete note" style={{background:"transparent",border:"none",color:C.dim,fontSize:11,cursor:"pointer",fontFamily:"inherit",padding:"2px 6px"}}>✕</button>
                    </div>
                    <div style={{fontSize:12,color:C.text,lineHeight:1.55,whiteSpace:"pre-wrap"}}>{n.body}</div>
                  </div>
                ))}
              </div>
            )}
          </div>

          {showNetForm && <NetworkForm initial={editingNet} onSave={net=>{saveNetwork(editingNet?{...editingNet,...net}:net);setShowNetForm(false);setEditingNet(null);}} onCancel={()=>{setShowNetForm(false);setEditingNet(null);}}/>}
          {showBrkForm && <BrokerForm initial={editingBrk} networks={bNetworks} onSave={brk=>{saveBroker(editingBrk?{...editingBrk,...brk}:brk);setShowBrkForm(false);setEditingBrk(null);}} onCancel={()=>{setShowBrkForm(false);setEditingBrk(null);}}/>}
        </div>
      );
    }

    return (
      <div>
        {/* KPI cards */}
        <div style={{display:"flex",gap:11,marginBottom:16,flexWrap:"wrap"}}>
          {[
            ["Networks", bNetworks.length, "#a78bfa"],
            ["Brokers", bBrokers.length, "#60a5fa"],
            ["Opps w/ Broker", oppsWithBroker, "#4ade80"],
            ["Total Comms", allComms.length, "#fb923c"],
          ].map(([l,v,c])=>(
            <div key={l} style={{background:C.panel,border:`1px solid ${C.border}`,borderRadius:12,padding:"15px 18px",flex:"1 1 130px"}}>
              <div style={{fontSize:22,fontWeight:900,color:c}}>{v}</div>
              <div style={{fontSize:11,color:C.muted,marginTop:2}}>{l}</div>
            </div>
          ))}
        </div>

        {/* Tabs */}
        <div style={{display:"flex",gap:7,marginBottom:14,borderBottom:`1px solid ${C.border}`,alignItems:"center"}}>
          {[["brokers","🤝 Brokers"],["networks","🏢 Networks"],["comms","💬 Communications"]].map(([k,l])=>(
            <button key={k} onClick={()=>setTab(k)} style={{background:"transparent",border:"none",borderBottom:`2px solid ${tab===k?C.accent:"transparent"}`,padding:"10px 14px",color:tab===k?C.text:C.muted,fontWeight:tab===k?800:600,cursor:"pointer",fontSize:13,fontFamily:"inherit"}}>{l}</button>
          ))}
          <div style={{marginLeft:"auto",display:"flex",gap:7}}>
            {tab==="networks" && <button onClick={()=>{setEditingNet(null);setShowNetForm(true);}} style={btn("#091c09","#4ade80",true)}>+ New Network</button>}
            {tab==="brokers"  && <button onClick={()=>{setEditingBrk(null);setShowBrkForm(true);}} style={btn("#091c09","#4ade80",true)}>+ New Broker</button>}
          </div>
        </div>

        {/* Brokers tab */}
        {tab==="brokers" && (
          <div style={{display:"flex",flexDirection:"column",gap:9}}>
            {bBrokers.length===0 ? (
              <div style={{background:C.panel,border:`1px solid ${C.border}`,borderRadius:12,padding:"30px 24px",textAlign:"center",color:C.muted}}>
                <div style={{fontSize:32,marginBottom:8}}>🤝</div>
                <div style={{fontWeight:800,color:C.text,marginBottom:5}}>No brokers yet</div>
                <div style={{fontSize:12,marginBottom:14}}>Add the brokers/consultants you work with so you can assign them to opportunities.</div>
                <button onClick={()=>{setEditingBrk(null);setShowBrkForm(true);}} style={btn("#091c09","#4ade80",true)}>+ Add Your First Broker</button>
              </div>
            ) : bBrokers.map(b=>{
              const net = bNetworks.find(n => n.id === b.networkId);
              const assignedOpps = bOpps.filter(o => o.brokerId === b.id);
              return (
                <div key={b.id} style={{background:C.panel,border:`1px solid ${C.border}`,borderRadius:12,padding:"12px 16px",display:"flex",alignItems:"center",gap:12}}>
                  <Ava name={`${b.firstName} ${b.lastName}`} size={40}/>
                  <div style={{flex:1,minWidth:0}}>
                    <div style={{display:"flex",alignItems:"center",gap:7,marginBottom:3,flexWrap:"wrap"}}>
                      <div style={{fontSize:14,fontWeight:800,color:C.text}}><RecordLink onClick={()=>openBroker(b.id)} color={C.text} weight={800}>{b.firstName} {b.lastName}</RecordLink></div>
                      {net && <RecordLink onClick={()=>openNetwork(net.id)} color={net.color||"#94a3b8"} weight={700}><span style={{background:(net.color||"#1e3a5f")+"22",border:`1px solid ${(net.color||"#1e3a5f")}44`,borderRadius:5,padding:"1px 7px",fontSize:10,fontWeight:700}}>{net.name}</span></RecordLink>}
                      {b.specialty && <span style={{background:"#1a1908",color:"#facc15",border:"1px solid #facc1533",borderRadius:5,padding:"1px 7px",fontSize:10,fontWeight:700}}>{b.specialty}</span>}
                    </div>
                    <div style={{fontSize:11,color:C.muted}}>{[b.email,b.phone].filter(Boolean).join(" · ")||<em>no contact info</em>}</div>
                    {assignedOpps.length>0 && <div style={{fontSize:10,color:"#4ade80",marginTop:3}}>● {assignedOpps.length} active opp{assignedOpps.length===1?"":"s"}</div>}
                  </div>
                  <div style={{display:"flex",gap:6}}>
                    {b.email && <a href={`mailto:${b.email}`} title={`Email ${b.firstName}`} style={{...btn("#091420","#60a5fa"),textDecoration:"none",fontSize:11}}>✉️</a>}
                    {b.phone && <a href={`tel:${b.phone}`}   title={`Call ${b.firstName}`} style={{...btn("#091c09","#4ade80"),textDecoration:"none",fontSize:11}}>📞</a>}
                    <button onClick={()=>{setEditingBrk(b);setShowBrkForm(true);}} title="Edit broker" style={btn(C.dim,C.muted)}>✏️</button>
                    <button onClick={()=>{if(confirm(`Delete ${b.firstName} ${b.lastName}?`)) deleteBroker(b.id);}} title="Delete broker" style={btn("#1a0808","#f87171")}>🗑</button>
                  </div>
                </div>
              );
            })}
          </div>
        )}

        {/* Networks tab */}
        {tab==="networks" && (
          <div style={{display:"grid",gridTemplateColumns:"repeat(auto-fill,minmax(280px,1fr))",gap:11}}>
            {bNetworks.length===0 ? (
              <div style={{gridColumn:"1/-1",background:C.panel,border:`1px solid ${C.border}`,borderRadius:12,padding:"30px 24px",textAlign:"center",color:C.muted}}>
                <div style={{fontSize:32,marginBottom:8}}>🏢</div>
                <div style={{fontWeight:800,color:C.text,marginBottom:5}}>No broker networks yet</div>
                <div style={{fontSize:12,marginBottom:14}}>Add the broker networks / franchise consulting firms (FranNet, IFPG, FBA, etc.) you work with.</div>
                <button onClick={()=>{setEditingNet(null);setShowNetForm(true);}} style={btn("#091c09","#4ade80",true)}>+ Add Your First Network</button>
              </div>
            ) : bNetworks.map(n=>{
              const count = bBrokers.filter(b => b.networkId === n.id).length;
              return (
                <div key={n.id} onClick={()=>openNetwork(n.id)} style={{background:C.panel,border:`1px solid ${C.border}`,borderRadius:12,padding:"14px 16px",cursor:"pointer",transition:"border-color .15s, background .15s"}} onMouseEnter={e=>{e.currentTarget.style.background="#101c2e"; e.currentTarget.style.borderColor=n.color||C.accent;}} onMouseLeave={e=>{e.currentTarget.style.background=C.panel; e.currentTarget.style.borderColor=C.border;}}>
                  <div style={{display:"flex",alignItems:"center",gap:10,marginBottom:8}}>
                    <div style={{width:40,height:40,borderRadius:10,background:n.color||"#1e3a5f",display:"flex",alignItems:"center",justifyContent:"center",fontSize:18,fontWeight:900,color:"#fff"}}>{n.name?.[0]?.toUpperCase()||"?"}</div>
                    <div style={{flex:1,minWidth:0}}>
                      <div style={{fontSize:14,fontWeight:800,color:C.text,overflow:"hidden",textOverflow:"ellipsis",whiteSpace:"nowrap"}}><RecordLink onClick={(e)=>{e?.stopPropagation?.(); openNetwork(n.id);}} color={C.text} weight={800}>{n.name}</RecordLink></div>
                      <div style={{fontSize:11,color:C.muted}}>{count} broker{count===1?"":"s"}</div>
                    </div>
                  </div>
                  {n.website && <div style={{fontSize:11,marginBottom:4}} onClick={e=>e.stopPropagation()}><a href={n.website.startsWith("http")?n.website:`https://${n.website}`} target="_blank" rel="noreferrer" style={{color:"#60a5fa",textDecoration:"none"}}>{n.website}</a></div>}
                  {n.phone   && <div style={{fontSize:11,color:C.muted}}>📞 {n.phone}</div>}
                  {n.email   && <div style={{fontSize:11,color:C.muted}}>✉️ {n.email}</div>}
                  {n.notes   && <div style={{fontSize:11,color:C.dim,marginTop:6,lineHeight:1.45}}>{n.notes}</div>}
                  <div style={{display:"flex",gap:7,marginTop:10}} onClick={e=>e.stopPropagation()}>
                    <button onClick={()=>{setEditingNet(n);setShowNetForm(true);}} title="Edit network" style={{...btn(C.dim,C.muted),fontSize:10,padding:"4px 9px"}}>✏️ Edit</button>
                    <button onClick={()=>{if(confirm(`Delete "${n.name}"? Brokers in this network will remain but be unassigned.`)) deleteNetwork(n.id);}} title="Delete network" style={{...btn("#1a0808","#f87171"),fontSize:10,padding:"4px 9px"}}>🗑</button>
                  </div>
                </div>
              );
            })}
          </div>
        )}

        {/* Communications tab */}
        {tab==="comms" && (
          <div style={{display:"flex",flexDirection:"column",gap:9}}>
            {allComms.length===0 ? (
              <div style={{background:C.panel,border:`1px solid ${C.border}`,borderRadius:12,padding:"30px 24px",textAlign:"center",color:C.muted}}>
                <div style={{fontSize:32,marginBottom:8}}>💬</div>
                <div style={{fontWeight:800,color:C.text,marginBottom:5}}>No broker communications logged yet</div>
                <div style={{fontSize:12}}>Email, SMS, and call activity logged from opportunity profiles will appear here.</div>
              </div>
            ) : allComms.sort((a,b)=>new Date(b.at)-new Date(a.at)).map(m=>{
              const broker = bBrokers.find(x=>x.id===m.brokerId);
              const opp = bOpps.find(o=>o.id===m.oppId);
              return (
                <div key={m.id} style={{background:C.panel,border:`1px solid ${C.border}`,borderRadius:10,padding:"10px 14px"}}>
                  <div style={{display:"flex",alignItems:"center",gap:8,marginBottom:4,flexWrap:"wrap"}}>
                    <span style={{fontSize:14}}>{m.type==="email"?"✉️":m.type==="sms"?"💬":m.type==="call"?"📞":"📝"}</span>
                    <strong style={{fontSize:12,color:C.text}}>{broker ? <RecordLink onClick={()=>openBroker(broker.id)} color={C.text} weight={700}>{broker.firstName} {broker.lastName}</RecordLink> : "Unknown broker"}</strong>
                    <span style={{fontSize:10,color:C.dim}}>re:</span>
                    {opp ? (
                      <button onClick={()=>{setSelected({type:"opp",id:opp.id});setNav("opps");setSubView("detail");}} style={{background:"transparent",border:"none",color:"#60a5fa",fontSize:11,cursor:"pointer",fontFamily:"inherit",padding:0,textDecoration:"underline"}}>{opp.firstName} {opp.lastName}</button>
                    ) : (
                      <span style={{fontSize:11,color:C.dim,fontStyle:"italic"}}>(deleted opp)</span>
                    )}
                    <span style={{marginLeft:"auto",fontSize:10,color:C.dim}}>{fmtDate(m.at)} · {fmtTime(m.at)}</span>
                  </div>
                  {m.subject && <div style={{fontSize:12,color:C.text,marginBottom:3}}><strong>Subject:</strong> {m.subject}</div>}
                  <div style={{fontSize:12,color:"#c8d8ef",whiteSpace:"pre-wrap",lineHeight:1.5}}>{m.body}</div>
                </div>
              );
            })}
          </div>
        )}

        {showNetForm && <NetworkForm initial={editingNet} onSave={(n)=>{saveNetwork({...editingNet,...n});setShowNetForm(false);setEditingNet(null);}} onCancel={()=>{setShowNetForm(false);setEditingNet(null);}}/>}
        {showBrkForm && <BrokerForm initial={editingBrk} networks={bNetworks} onSave={(b)=>{saveBroker({...editingBrk,...b});setShowBrkForm(false);setEditingBrk(null);}} onCancel={()=>{setShowBrkForm(false);setEditingBrk(null);}}/>}
      </div>
    );
  };

  // Settings (comprehensive — categorized, Zoho/GoHighLevel-style)
  // Notifications view — full history with filter + actions
  const NotificationsView = () => {
    const [filter, setFilter] = useState("all"); // all | unread | docusign | lead | system | digest
    const categories = [
      ["all",      "All",       notifications.length],
      ["unread",   "Unread",    notifications.filter(n=>!n.readAt).length],
      ["docusign", "DocuSign",  notifications.filter(n=>n.category==="docusign").length],
      ["lead",     "Pipeline",  notifications.filter(n=>n.category==="lead").length],
      ["system",   "System",    notifications.filter(n=>n.category==="system").length],
      ["digest",   "Digests",   notifications.filter(n=>n.category==="digest").length],
    ];
    const filtered = filter === "all" ? notifications
                   : filter === "unread" ? notifications.filter(n=>!n.readAt)
                   : notifications.filter(n=>n.category===filter);
    const unreadCount = notifications.filter(n=>!n.readAt).length;

    return (
      <div>
        <div style={{display:"flex",alignItems:"center",gap:10,marginBottom:14,flexWrap:"wrap"}}>
          <Sec style={{margin:0}}>All Notifications ({notifications.length}){unreadCount>0?` · ${unreadCount} unread`:""}</Sec>
          <div style={{marginLeft:"auto",display:"flex",gap:7}}>
            {unreadCount>0 && <button onClick={markAllNotificationsRead} style={btn(C.dim,C.muted)}>Mark all read</button>}
            {notifications.length>0 && <button onClick={clearAllNotifications} style={btn("#1a0808","#f87171")}>🗑 Clear all</button>}
          </div>
        </div>

        <div style={{display:"flex",gap:6,marginBottom:14,flexWrap:"wrap",borderBottom:`1px solid ${C.border}`,paddingBottom:6}}>
          {categories.map(([k,l,cnt])=>(
            <button key={k} onClick={()=>setFilter(k)} style={{background:filter===k?"#162035":"transparent",border:"none",borderRadius:7,padding:"5px 11px",color:filter===k?C.text:C.muted,fontSize:12,fontWeight:filter===k?700:500,cursor:"pointer",fontFamily:"inherit"}}>
              {l} <span style={{opacity:.6,fontWeight:600}}>{cnt}</span>
            </button>
          ))}
        </div>

        {filtered.length === 0 && (
          <div style={{background:C.panel,border:`1px solid ${C.border}`,borderRadius:12,padding:"32px 24px",textAlign:"center",color:C.muted}}>
            <div style={{fontSize:28,marginBottom:8}}>🔔</div>
            <div style={{fontWeight:800,color:C.text,marginBottom:4}}>{filter==="unread"?"All caught up":"No notifications yet"}</div>
            <div style={{fontSize:12,maxWidth:380,margin:"0 auto"}}>
              {filter==="all" ? "Notifications appear here when DocuSign envelopes get signed, leads go dark, AI quota gets used, and more." : "Switch to another category or try All."}
            </div>
          </div>
        )}

        <div style={{display:"flex",flexDirection:"column",gap:6}}>
          {filtered.map(n => {
            const isCritical = n.severity === "critical";
            const accent = isCritical ? "#ef4444" : (n.isBanner ? "#4ade80" : "#60a5fa");
            const dot = n.readAt ? "transparent" : accent;
            return (
              <div key={n.id} onClick={()=>openNotificationRecord(n)} style={{
                background: n.readAt ? "#090f1c" : "#0c1422",
                border: `1px solid ${n.readAt ? C.border : accent+"33"}`,
                borderLeft: `3px solid ${accent}`,
                borderRadius: 9, padding: "10px 14px",
                display: "flex", alignItems: "center", gap: 11,
                cursor: n.recordRef ? "pointer" : "default",
                transition: "background .15s",
              }} onMouseEnter={e=>{if(n.recordRef)e.currentTarget.style.background="#101c2e";}} onMouseLeave={e=>e.currentTarget.style.background=n.readAt?"#090f1c":"#0c1422"}>
                <div style={{width:7,height:7,borderRadius:"50%",background:dot,flexShrink:0}}/>
                <span style={{fontSize:16,flexShrink:0}}>✨</span>
                <div style={{flex:1,minWidth:0}}>
                  <div style={{fontSize:13,color:n.readAt?C.muted:C.text,fontWeight:n.readAt?500:700,lineHeight:1.4}}><LinkedTitle notification={n} onNameClick={openNotificationRecord}/></div>
                  <div style={{fontSize:10,color:C.dim,marginTop:2,display:"flex",alignItems:"center",gap:6}}>
                    <span style={{textTransform:"uppercase",letterSpacing:".05em",fontWeight:700}}>{n.category}</span>
                    <span>·</span>
                    <span>{fmtDate(n.createdAt)} · {fmtTime(n.createdAt)}</span>
                    {isCritical && <><span>·</span><span style={{color:"#f87171",fontWeight:700}}>CRITICAL</span></>}
                  </div>
                </div>
                {!n.readAt && <button onClick={(e)=>{e.stopPropagation();markNotificationRead(n.id);}} style={{...btn(C.dim,C.muted),padding:"3px 9px",fontSize:10}}>Mark read</button>}
                {n.recordRef && <span style={{color:C.dim,fontSize:14,flexShrink:0}}>→</span>}
              </div>
            );
          })}
        </div>
      </div>
    );
  };

  // Scheduling — Calendly-style booking hub with 3 sub-tabs.
  // ───────────────────────────────────────────────────────────
  //  DISCOVERY DAY HUB — full lifecycle (planning → day-of → wrap)
  // ───────────────────────────────────────────────────────────
  // Stored under ff4_dday[brand]. Each DiscoveryDay: { id, name, date, location, format, status,
  //   candidateIds, corporateAttendees, agenda, checklist, prepNotes, recapNotes, ...timestamps }
  const DiscoveryDayView = () => {
    const [tab, setTab] = useState("upcoming"); // upcoming | past | drafts | cancelled
    const [editing, setEditing] = useState(null); // {} = new, dday obj = edit
    // Detail panel state hoisted to App (ddayDetailId) so it persists across re-renders
    if (ddayDetailId) {
      const dday = bDdays.find(d => d.id === ddayDetailId);
      if (!dday) return <div style={{padding:"40px 20px",textAlign:"center",color:C.muted}}>
        <div style={{fontSize:32,marginBottom:8}}>🤷</div>
        <div style={{fontWeight:800,color:C.text,marginBottom:5}}>This Discovery Day no longer exists.</div>
        <button onClick={()=>setDdayDetailId(null)} style={btn(C.dim,C.muted)}>← Back</button>
      </div>;
      // Mount the form modal alongside the detail view so the header's "✏️ Edit" button
      // can open it — the listing-view modal mount is past the early return below.
      return (
        <>
          <DiscoveryDayDetail dday={dday} onBack={()=>setDdayDetailId(null)} onEdit={()=>setEditing(dday)} onDelete={()=>{ if(confirm(`Delete "${dday.name||"Discovery Day"}"?`)){ deleteDday(dday.id); setDdayDetailId(null); } }} bOpps={bOpps} bLeads={bLeads} settings={settings} onOpenRecord={(type,id)=>{setNav(type==="opp"?"opps":"leads");setSelected({type,id});setSubView("detail");setDdayDetailId(null);}} toggleItem={(itemId)=>toggleDdayChecklistItem(dday.id,itemId)} addItem={(label,cat)=>addDdayChecklistItem(dday.id,label,cat)} deleteItem={(itemId)=>deleteDdayChecklistItem(dday.id,itemId)} saveDday={saveDday}/>
          {editing && <DiscoveryDayFormModal initial={editing} onSave={(d)=>{saveDday(d);setEditing(null);}} onCancel={()=>setEditing(null)} settings={settings} bOpps={bOpps} bLeads={bLeads}/>}
        </>
      );
    }
    const now = Date.now();
    const upcomingList  = bDdays.filter(d => d.status !== "cancelled" && d.status !== "completed" && (!d.date || new Date(d.date).getTime() >= now - 86400000)).sort((a,b) => new Date(a.date||0) - new Date(b.date||0));
    const pastList      = bDdays.filter(d => d.status === "completed" || (d.date && new Date(d.date).getTime() < now - 86400000 && d.status !== "cancelled")).sort((a,b) => new Date(b.date||0) - new Date(a.date||0));
    const draftList     = bDdays.filter(d => d.status === "draft");
    const cancelledList = bDdays.filter(d => d.status === "cancelled");
    const counts = { upcoming: upcomingList.length, past: pastList.length, drafts: draftList.length, cancelled: cancelledList.length };
    const shown = tab === "upcoming" ? upcomingList : tab === "past" ? pastList : tab === "drafts" ? draftList : cancelledList;
    return (
      <div>
        {/* KPI cards */}
        <div style={{display:"flex",gap:11,marginBottom:16,flexWrap:"wrap"}}>
          {[
            ["Upcoming",  counts.upcoming,  "#60a5fa"],
            ["Past",      counts.past,      "#94a3b8"],
            ["Drafts",    counts.drafts,    "#facc15"],
            ["Cancelled", counts.cancelled, "#f87171"],
          ].map(([l,v,c]) => (
            <div key={l} style={{background:C.panel,border:`1px solid ${C.border}`,borderRadius:12,padding:"15px 18px",flex:"1 1 130px"}}>
              <div style={{fontSize:22,fontWeight:900,color:c}}>{v}</div>
              <div style={{fontSize:11,color:C.muted,marginTop:2}}>{l}</div>
            </div>
          ))}
        </div>
        <div style={{display:"flex",gap:7,marginBottom:14,borderBottom:`1px solid ${C.border}`,alignItems:"center"}}>
          {[["upcoming",`Upcoming (${counts.upcoming})`],["past",`Past (${counts.past})`],["drafts",`Drafts (${counts.drafts})`],["cancelled",`Cancelled (${counts.cancelled})`]].map(([k,l])=>(
            <button key={k} onClick={()=>setTab(k)} style={{background:"transparent",border:"none",borderBottom:`2px solid ${tab===k?C.accent:"transparent"}`,padding:"10px 14px",color:tab===k?C.text:C.muted,fontWeight:tab===k?800:600,cursor:"pointer",fontSize:13,fontFamily:"inherit"}}>{l}</button>
          ))}
          <button onClick={()=>setEditing({})} style={{...btn("#091c09","#4ade80",true),marginLeft:"auto"}} title="Plan a new Discovery Day">+ New Discovery Day</button>
        </div>
        {shown.length === 0 ? (
          <div style={{background:C.panel,border:`1px solid ${C.border}`,borderRadius:12,padding:"40px 24px",textAlign:"center",color:C.muted}}>
            <div style={{fontSize:36,marginBottom:8}}>🎓</div>
            <div style={{fontWeight:800,color:C.text,marginBottom:5}}>No Discovery Days {tab==="upcoming"?"on the calendar":"in this tab"} yet</div>
            <div style={{fontSize:12,marginBottom:14,maxWidth:480,margin:"0 auto 14px",lineHeight:1.5}}>Plan your first Discovery Day — invite candidates, set the format (in-person or virtual), lock in your corporate attendees, and check off a full prep / day-of / follow-up checklist.</div>
            <button onClick={()=>setEditing({})} style={btn("#091c09","#4ade80",true)}>+ Plan a Discovery Day</button>
          </div>
        ) : (
          <div style={{display:"flex",flexDirection:"column",gap:10}}>
            {shown.map(d => {
              const checklist = d.checklist||[];
              const doneCount = checklist.filter(x => x.done).length;
              const pct = checklist.length ? Math.round((doneCount/checklist.length)*100) : 0;
              const stat = DDAY_STATUSES[d.status||"draft"];
              const dateLabel = d.date ? new Date(d.date).toLocaleDateString("en-US",{weekday:"short",month:"short",day:"numeric",year:"numeric"}) : "No date set";
              const timeLabel = d.startTime || "";
              const candCount = (d.candidateIds||[]).length;
              return (
                <div key={d.id} onClick={()=>setDdayDetailId(d.id)} style={{background:C.panel,border:`1px solid ${C.border}`,borderRadius:12,padding:"14px 18px",cursor:"pointer",transition:"background .15s, border-color .15s"}} onMouseEnter={e=>{e.currentTarget.style.background="#101c2e"; e.currentTarget.style.borderColor=stat.color+"55";}} onMouseLeave={e=>{e.currentTarget.style.background=C.panel; e.currentTarget.style.borderColor=C.border;}}>
                  <div style={{display:"flex",alignItems:"flex-start",gap:14}}>
                    <div style={{width:50,height:50,borderRadius:11,background:stat.color+"22",border:`1px solid ${stat.color}55`,display:"flex",alignItems:"center",justifyContent:"center",fontSize:22,flexShrink:0}}>{d.format==="virtual"?"💻":d.format==="hybrid"?"🔀":"🏢"}</div>
                    <div style={{flex:1,minWidth:0}}>
                      <div style={{display:"flex",alignItems:"center",gap:8,marginBottom:4,flexWrap:"wrap"}}>
                        <div style={{fontSize:15,fontWeight:800,color:C.text}}>{d.name||"Untitled Discovery Day"}</div>
                        <span style={{fontSize:9,fontWeight:800,color:stat.color,background:stat.color+"15",border:`1px solid ${stat.color}44`,borderRadius:4,padding:"1px 6px",letterSpacing:".05em"}}>{stat.icon} {stat.label.toUpperCase()}</span>
                        <span style={{fontSize:9,fontWeight:800,color:d.format==="virtual"?"#a78bfa":d.format==="hybrid"?"#facc15":"#4ade80",background:(d.format==="virtual"?"#a78bfa":d.format==="hybrid"?"#facc15":"#4ade80")+"15",border:`1px solid ${(d.format==="virtual"?"#a78bfa":d.format==="hybrid"?"#facc15":"#4ade80")}44`,borderRadius:4,padding:"1px 6px",letterSpacing:".05em"}}>{(d.format||"in_person").replace("_"," ").toUpperCase()}</span>
                      </div>
                      <div style={{fontSize:12,color:C.muted,marginBottom:6}}>📅 {dateLabel}{timeLabel?` · ${timeLabel}`:""}{d.location?` · 📍 ${d.location}`:""}</div>
                      <div style={{display:"flex",gap:14,fontSize:11,color:C.muted,flexWrap:"wrap"}}>
                        <span>👥 <strong style={{color:C.text}}>{candCount}</strong> candidate{candCount===1?"":"s"}</span>
                        <span>🎙️ <strong style={{color:C.text}}>{(d.corporateAttendees||[]).length}</strong> corporate attendee{(d.corporateAttendees||[]).length===1?"":"s"}</span>
                        <span>✓ <strong style={{color:C.text}}>{doneCount}/{checklist.length}</strong> tasks{checklist.length>0?` (${pct}%)`:""}</span>
                      </div>
                      {/* Progress bar */}
                      {checklist.length > 0 && (
                        <div style={{height:5,background:"#090f1c",borderRadius:3,marginTop:8,overflow:"hidden"}}>
                          <div style={{width:`${pct}%`,height:"100%",background:pct===100?"#4ade80":stat.color,borderRadius:3,transition:"width .25s"}}/>
                        </div>
                      )}
                    </div>
                  </div>
                </div>
              );
            })}
          </div>
        )}
        {editing && <DiscoveryDayFormModal initial={editing} onSave={(d)=>{saveDday(d);setEditing(null);}} onCancel={()=>setEditing(null)} settings={settings} bOpps={bOpps} bLeads={bLeads}/>}
      </div>
    );
  };

  // ───────────────────────────────────────────────────────────
  //  FRANCHISEE HUB — awarded franchisees & their territories
  // ───────────────────────────────────────────────────────────
  const FranchiseeHubView = () => {
    const [tab, setTab] = useState("all");
    const [search, setSearch] = useState("");
    const [editing, setEditing] = useState(null);
    if (franchiseeDetailId) {
      const fr = bFranchisees.find(f => f.id === franchiseeDetailId);
      if (!fr) return <div style={{padding:"40px 20px",textAlign:"center",color:C.muted}}>
        <div style={{fontSize:32,marginBottom:8}}>🤷</div>
        <div style={{fontWeight:800,color:C.text,marginBottom:5}}>This franchisee no longer exists.</div>
        <button onClick={()=>setFranchiseeDetailId(null)} style={btn(C.dim,C.muted)}>← Back</button>
      </div>;
      // Render the form modal alongside the detail view so clicking "✏️ Edit" in the detail
      // header actually opens the editor (the listing-view modal mount at the bottom of this
      // component is unreachable while we're in detail-view, since we return early above).
      return (
        <>
          <FranchiseeDetail fr={fr} onBack={()=>setFranchiseeDetailId(null)} onEdit={()=>setEditing(fr)} onDelete={()=>{ if(confirm(`Remove franchisee "${fr.firstName} ${fr.lastName}"?`)){ deleteFranchisee(fr.id); setFranchiseeDetailId(null); } }} territories={bTerr} bFranchisees={bFranchisees} openFranchisee={(id)=>setFranchiseeDetailId(id)} addNote={(body)=>addFranchiseeNote(fr.id,body)} deleteNote={(nid)=>deleteFranchiseeNote(fr.id,nid)}/>
          {editing && <FranchiseeFormModal initial={editing} territories={bTerr} onSave={(f)=>{saveFranchisee(f);setEditing(null);}} onCancel={()=>setEditing(null)} onDelete={editing.id ? ()=>{ if(confirm(`Remove franchisee "${editing.firstName} ${editing.lastName}"?`)){ deleteFranchisee(editing.id); setEditing(null); setFranchiseeDetailId(null); } } : null}/>}
        </>
      );
    }
    const filtered = bFranchisees.filter(f => {
      if (tab !== "all" && f.status !== tab) return false;
      if (!search.trim()) return true;
      const q = search.toLowerCase();
      return [f.firstName, f.lastName, f.email, f.phone, f.businessName].some(v => (v||"").toLowerCase().includes(q));
    });
    const counts = Object.fromEntries(Object.keys(FRANCHISEE_STATUSES).map(s => [s, bFranchisees.filter(f => f.status === s).length]));
    const totalTerritoryAssignments = bFranchisees.reduce((sum, f) => sum + (f.territoryIds||[]).length, 0);
    return (
      <div>
        {/* KPI cards */}
        <div style={{display:"flex",gap:11,marginBottom:16,flexWrap:"wrap"}}>
          {[
            ["Total Franchisees", bFranchisees.length, "#60a5fa"],
            ["Open Units", bFranchisees.filter(f => f.status === "open" || f.status === "multi_unit").reduce((s,f)=>s+(f.unitsOpen||1),0), "#4ade80"],
            ["In Buildout", counts.in_buildout||0, "#f59e0b"],
            ["Territory Assignments", totalTerritoryAssignments, "#a78bfa"],
          ].map(([l,v,c]) => (
            <div key={l} style={{background:C.panel,border:`1px solid ${C.border}`,borderRadius:12,padding:"15px 18px",flex:"1 1 130px"}}>
              <div style={{fontSize:22,fontWeight:900,color:c}}>{v}</div>
              <div style={{fontSize:11,color:C.muted,marginTop:2}}>{l}</div>
            </div>
          ))}
        </div>
        {/* Status filter tabs */}
        <div style={{display:"flex",gap:5,marginBottom:14,flexWrap:"wrap",alignItems:"center"}}>
          <button onClick={()=>setTab("all")} style={{background:tab==="all"?"#162035":"transparent",border:`1px solid ${tab==="all"?C.accent+"55":C.border}`,borderRadius:7,padding:"5px 11px",color:tab==="all"?C.accent:C.muted,fontSize:11,fontWeight:tab==="all"?800:600,cursor:"pointer",fontFamily:"inherit"}}>All ({bFranchisees.length})</button>
          {Object.entries(FRANCHISEE_STATUSES).map(([k,s]) => (
            <button key={k} onClick={()=>setTab(k)} style={{background:tab===k?s.color+"22":"transparent",border:`1px solid ${tab===k?s.color+"66":C.border}`,borderRadius:7,padding:"5px 11px",color:tab===k?s.color:C.muted,fontSize:11,fontWeight:tab===k?800:600,cursor:"pointer",fontFamily:"inherit",display:"flex",alignItems:"center",gap:5}}>
              <span>{s.icon}</span><span>{s.label}</span><span style={{opacity:0.7}}>({counts[k]||0})</span>
            </button>
          ))}
          <button onClick={()=>setEditing({})} style={{...btn("#091c09","#4ade80",true),marginLeft:"auto"}} title="Add a new franchisee">+ New Franchisee</button>
        </div>
        {bFranchisees.length === 0 ? (
          <div style={{background:C.panel,border:`1px solid ${C.border}`,borderRadius:12,padding:"40px 24px",textAlign:"center",color:C.muted}}>
            <div style={{fontSize:36,marginBottom:8}}>🏪</div>
            <div style={{fontWeight:800,color:C.text,marginBottom:5}}>No franchisees yet</div>
            <div style={{fontSize:12,marginBottom:14,maxWidth:480,margin:"0 auto 14px",lineHeight:1.5}}>When a candidate signs, add them here to track their territory assignments, opening status, and post-award activity.</div>
            <button onClick={()=>setEditing({})} style={btn("#091c09","#4ade80",true)}>+ Add Your First Franchisee</button>
          </div>
        ) : (
          <>
            <div style={{marginBottom:12}}>
              <input value={search} onChange={e=>setSearch(e.target.value)} placeholder="🔍 Search by name, email, phone, or business name…" style={inp({width:"100%",boxSizing:"border-box"})}/>
            </div>
            {filtered.length === 0 ? (
              <div style={{background:C.panel,border:`1px solid ${C.border}`,borderRadius:12,padding:"24px",textAlign:"center",color:C.muted,fontSize:12}}>No franchisees match your filter.</div>
            ) : (
              <div style={{display:"grid",gridTemplateColumns:"repeat(auto-fill, minmax(310px, 1fr))",gap:11}}>
                {filtered.map(f => {
                  const stat = FRANCHISEE_STATUSES[f.status||"awarded"];
                  const territoryLabels = (f.territoryIds||[]).map(tid => bTerr.find(t => t.id === tid)?.label).filter(Boolean);
                  return (
                    <div key={f.id} onClick={()=>setFranchiseeDetailId(f.id)} style={{background:C.panel,border:`1px solid ${C.border}`,borderRadius:12,padding:"13px 15px",cursor:"pointer",transition:"background .15s, border-color .15s"}} onMouseEnter={e=>{e.currentTarget.style.background="#101c2e"; e.currentTarget.style.borderColor=stat.color+"55";}} onMouseLeave={e=>{e.currentTarget.style.background=C.panel; e.currentTarget.style.borderColor=C.border;}}>
                      <div style={{display:"flex",alignItems:"flex-start",gap:11,marginBottom:9}}>
                        <Ava name={`${f.firstName||""} ${f.lastName||""}`} size={44}/>
                        <div style={{flex:1,minWidth:0}}>
                          <div style={{fontSize:14,fontWeight:800,color:C.text,overflow:"hidden",textOverflow:"ellipsis",whiteSpace:"nowrap"}}>{f.firstName} {f.lastName}</div>
                          {f.businessName && <div style={{fontSize:11,color:C.muted,marginTop:1,overflow:"hidden",textOverflow:"ellipsis",whiteSpace:"nowrap"}}>{f.businessName}</div>}
                          <span style={{display:"inline-block",fontSize:9,fontWeight:800,color:stat.color,background:stat.color+"15",border:`1px solid ${stat.color}44`,borderRadius:4,padding:"1px 6px",letterSpacing:".05em",marginTop:5}}>{stat.icon} {stat.label.toUpperCase()}</span>
                        </div>
                      </div>
                      <div style={{fontSize:11,color:C.muted,marginBottom:6}}>{[f.email,f.phone].filter(Boolean).join(" · ") || <em>no contact info</em>}</div>
                      {territoryLabels.length > 0 ? (
                        <div style={{display:"flex",flexWrap:"wrap",gap:4,marginTop:8,paddingTop:8,borderTop:`1px solid ${C.border}`}}>
                          {territoryLabels.slice(0,4).map(lab => <span key={lab} style={{fontSize:9,fontWeight:700,color:"#a78bfa",background:"#1a1429",border:"1px solid #a78bfa33",borderRadius:4,padding:"1px 6px"}}>📍 {lab}</span>)}
                          {territoryLabels.length > 4 && <span style={{fontSize:9,fontWeight:700,color:C.muted}}>+{territoryLabels.length-4} more</span>}
                        </div>
                      ) : (
                        <div style={{fontSize:10,color:C.dim,marginTop:8,paddingTop:8,borderTop:`1px solid ${C.border}`,fontStyle:"italic"}}>No territories assigned</div>
                      )}
                    </div>
                  );
                })}
              </div>
            )}
          </>
        )}
        {editing && <FranchiseeFormModal initial={editing} territories={bTerr} onSave={(f)=>{saveFranchisee(f);setEditing(null);}} onCancel={()=>setEditing(null)} onDelete={editing.id ? ()=>{ if(confirm(`Remove franchisee "${editing.firstName} ${editing.lastName}"?`)){ deleteFranchisee(editing.id); setEditing(null); } } : null}/>}
      </div>
    );
  };

  const SchedulingView = () => {
    const [tab, setTab] = useState("bookings"); // bookings | event_types | availability
    const [bookingFilter, setBookingFilter] = useState("upcoming");
    const [showBook, setShowBook] = useState(false);
    const [bookForRec, setBookForRec] = useState(null);
    const [editingEvtType, setEditingEvtType] = useState(null); // event type id or "new"
    const [viewingBooking, setViewingBooking] = useState(null); // booking id

    const now = Date.now();
    const isUpcoming = b => b.status === "scheduled" && new Date(b.startISO).getTime() >= now;
    const isToday = b => { const d = new Date(b.startISO); const today = new Date(); return d.toDateString() === today.toDateString() && b.status !== "cancelled"; };
    const isThisWeek = b => { const t = new Date(b.startISO).getTime(); return b.status !== "cancelled" && t >= now && t - now <= 7*86400000; };
    const isPast = b => new Date(b.endISO).getTime() < now || b.status === "completed";

    const filteredBookings = bBookings.filter(b => {
      if (bookingFilter === "today") return isToday(b);
      if (bookingFilter === "week") return isThisWeek(b);
      if (bookingFilter === "upcoming") return isUpcoming(b);
      if (bookingFilter === "past") return isPast(b);
      if (bookingFilter === "cancelled") return b.status === "cancelled";
      return true;
    }).sort((a,b) => new Date(a.startISO) - new Date(b.startISO));

    const recordName = (ref) => {
      if (!ref) return "(no record)";
      const list = ref.type === "lead" ? bLeads : bOpps;
      const r = list.find(x => x.id === ref.id);
      return r ? `${r.firstName} ${r.lastName}` : "(deleted record)";
    };
    const evtTypeById = (id) => bEventTypes.find(e => e.id === id);

    const navToRecord = (ref) => {
      if (!ref) return;
      setNav(ref.type === "lead" ? "leads" : "opps");
      setSelected({type: ref.type, id: ref.id});
      setSubView("detail");
    };

    return (
      <div>
        {/* Tabs */}
        <div style={{display:"flex",gap:7,marginBottom:14,borderBottom:`1px solid ${C.border}`,alignItems:"center"}}>
          {[["bookings",`📋 Bookings (${bBookings.length})`],["event_types",`📁 Event Types (${bEventTypes.length})`],["availability","⏰ Availability"]].map(([k,l])=>(
            <button key={k} onClick={()=>setTab(k)} style={{background:"transparent",border:"none",borderBottom:`2px solid ${tab===k?C.accent:"transparent"}`,padding:"10px 14px",color:tab===k?C.text:C.muted,fontWeight:tab===k?800:600,cursor:"pointer",fontSize:13,fontFamily:"inherit"}}>{l}</button>
          ))}
          <div style={{marginLeft:"auto",display:"flex",gap:7}}>
            {tab==="bookings"     && <button onClick={()=>{setBookForRec(null);setShowBook(true);}} style={btn("#091c09","#4ade80",true)}>+ Schedule Meeting</button>}
            {tab==="event_types"  && <button onClick={()=>setEditingEvtType("new")} style={btn("#091c09","#4ade80",true)}>+ New Event Type</button>}
          </div>
        </div>

        {/* BOOKINGS TAB */}
        {tab==="bookings" && (
          <>
            <div style={{display:"flex",gap:6,marginBottom:14,flexWrap:"wrap"}}>
              {[["upcoming","Upcoming"],["today","Today"],["week","This Week"],["past","Past"],["cancelled","Cancelled"],["all","All"]].map(([k,l])=>(
                <button key={k} onClick={()=>setBookingFilter(k)} style={{background:bookingFilter===k?"#162035":"transparent",border:`1px solid ${bookingFilter===k?C.accent+"55":C.border}`,borderRadius:6,padding:"4px 10px",color:bookingFilter===k?C.accent:C.muted,fontSize:11,fontWeight:bookingFilter===k?700:500,cursor:"pointer",fontFamily:"inherit"}}>{l}</button>
              ))}
            </div>
            {filteredBookings.length===0 ? (
              <div style={{background:C.panel,border:`1px solid ${C.border}`,borderRadius:12,padding:"32px 24px",textAlign:"center",color:C.muted}}>
                <div style={{fontSize:28,marginBottom:8}}>📅</div>
                <div style={{fontWeight:800,color:C.text,marginBottom:5}}>No bookings in this filter</div>
                <div style={{fontSize:12}}>Click <strong style={{color:C.text}}>+ Schedule Meeting</strong> to book one — or open a lead/opp profile and use the <strong style={{color:C.text}}>📅 Schedule Meeting</strong> button.</div>
              </div>
            ) : (
              <div style={{display:"flex",flexDirection:"column",gap:6}}>
                {filteredBookings.map(b => {
                  const et = evtTypeById(b.eventTypeId);
                  const status = BOOKING_STATUSES[b.status] || {label:b.status, color:C.muted};
                  const start = new Date(b.startISO);
                  return (
                    <div key={b.id} onClick={()=>setViewingBooking(b.id)} style={{background:C.panel,border:`1px solid ${C.border}`,borderLeft:`3px solid ${et?.color||C.accent}`,borderRadius:9,padding:"10px 14px",display:"flex",alignItems:"center",gap:12,cursor:"pointer",transition:"background .15s"}} onMouseEnter={e=>e.currentTarget.style.background="#101c2e"} onMouseLeave={e=>e.currentTarget.style.background=C.panel}>
                      <span style={{fontSize:18}}>{et?.icon||"📅"}</span>
                      <div style={{flex:1,minWidth:0}}>
                        <div style={{fontSize:13,fontWeight:800,color:C.text}}>{et?.name||"(deleted event type)"} · {recordName(b.recordRef)}</div>
                        <div style={{fontSize:11,color:C.muted,marginTop:2}}>{start.toLocaleDateString("en-US",{weekday:"short",month:"short",day:"numeric"})} · {start.toLocaleTimeString("en-US",{hour:"numeric",minute:"2-digit"})} · {b.location || et?.location}</div>
                      </div>
                      <span style={{background:status.color+"22",color:status.color,border:`1px solid ${status.color}55`,borderRadius:5,padding:"2px 9px",fontSize:11,fontWeight:700}}>{status.label}</span>
                    </div>
                  );
                })}
              </div>
            )}
          </>
        )}

        {/* EVENT TYPES TAB */}
        {tab==="event_types" && (
          <div style={{display:"grid",gridTemplateColumns:"repeat(auto-fill,minmax(280px,1fr))",gap:11}}>
            {[...bEventTypes].sort((a,b) => (b.active?1:0) - (a.active?1:0)).map(et => (
              <div key={et.id} style={{background:C.panel,border:`1px solid ${C.border}`,borderLeft:`3px solid ${et.color}`,borderRadius:12,padding:"14px 16px",opacity:et.active?1:0.55}}>
                <div style={{display:"flex",alignItems:"center",gap:10,marginBottom:8}}>
                  <span style={{fontSize:22}}>{et.icon}</span>
                  <div style={{flex:1,minWidth:0}}>
                    <div style={{fontSize:14,fontWeight:800,color:C.text}}>{et.name}{et.isDefault && <span style={{marginLeft:6,fontSize:9,background:"#091c09",color:"#4ade80",border:"1px solid #4ade8033",borderRadius:4,padding:"1px 5px",fontWeight:700,textTransform:"uppercase",letterSpacing:".05em"}}>Default</span>}</div>
                    <div style={{fontSize:11,color:C.muted}}>{et.durationMin} min · {et.location}{!et.active && " · INACTIVE"}</div>
                  </div>
                </div>
                {et.description && <div style={{fontSize:11,color:C.muted,marginBottom:9,lineHeight:1.5}}>{et.description}</div>}
                <div style={{display:"flex",gap:6}}>
                  <button onClick={()=>setEditingEvtType(et.id)} style={{...btn(C.dim,C.muted),fontSize:10,padding:"4px 9px"}}>✏️ Edit</button>
                  <button onClick={()=>toggleEventTypeActive(et.id)} style={{...btn(C.dim,et.active?C.muted:"#4ade80"),fontSize:10,padding:"4px 9px"}}>{et.active?"Deactivate":"Activate"}</button>
                  {!et.isDefault && <button onClick={()=>{ if(confirm(`Delete "${et.name}"?`)) deleteEventType(et.id); }} title="Delete event type" style={{...btn("#1a0808","#f87171"),fontSize:10,padding:"4px 9px"}}>🗑</button>}
                </div>
              </div>
            ))}
          </div>
        )}

        {/* AVAILABILITY TAB */}
        {tab==="availability" && <AvailabilityEditor availability={bAvailability} onSave={saveAvailability}/>}

        {/* Modals */}
        {showBook && <BookMeetingModal
          eventTypes={bEventTypes.filter(e => e.active)}
          availability={bAvailability}
          existingBookings={bBookings}
          assignableRecords={[
            ...bLeads.map(l => ({...l, recType:"lead"})),
            ...bOpps.map(o  => ({...o, recType:"opp"})),
          ]}
          initialRecord={bookForRec}
          onSave={(payload)=>{ saveBooking(payload); setShowBook(false); setBookForRec(null); }}
          onCancel={()=>{ setShowBook(false); setBookForRec(null); }}
        />}
        {editingEvtType !== null && <EventTypeFormModal
          initial={editingEvtType === "new" ? null : bEventTypes.find(e => e.id === editingEvtType)}
          onSave={(et)=>{ saveEventType(et); setEditingEvtType(null); }}
          onCancel={()=>setEditingEvtType(null)}
        />}
        {viewingBooking && (() => {
          const b = bBookings.find(x => x.id === viewingBooking);
          if (!b) return null;
          const et = evtTypeById(b.eventTypeId);
          return <BookingDetailModal
            booking={b}
            eventType={et}
            recordName={recordName(b.recordRef)}
            brand={brand}
            settings={settings}
            onCancel={()=>setViewingBooking(null)}
            onMarkCompleted={()=>{ markBookingCompleted(b.id); setViewingBooking(null); }}
            onCancelBooking={()=>{ if(confirm("Cancel this booking? This cannot be undone.")) { cancelBooking(b.id); setViewingBooking(null); } }}
            onOpenRecord={()=>{ navToRecord(b.recordRef); setViewingBooking(null); }}
          />;
        })()}
      </div>
    );
  };

  // Inline availability editor — uncontrolled inputs commit on blur for stable typing.
  const AvailabilityEditor = ({ availability, onSave }) => {
    const [draft, setDraft] = useState(availability);
    useEffect(()=>{ setDraft(availability); }, [availability]);
    const DAYS = [["mon","Monday"],["tue","Tuesday"],["wed","Wednesday"],["thu","Thursday"],["fri","Friday"],["sat","Saturday"],["sun","Sunday"]];
    const toggleDay = (key) => {
      const next = { ...draft, weeklyHours: { ...draft.weeklyHours, [key]: (draft.weeklyHours[key]||[]).length ? [] : [{start:"09:00",end:"17:00"}] } };
      setDraft(next); onSave(next);
    };
    const updateWindow = (key, field, value) => {
      const win = draft.weeklyHours[key]?.[0] || {start:"09:00",end:"17:00"};
      const next = { ...draft, weeklyHours: { ...draft.weeklyHours, [key]: [{ ...win, [field]: value }] } };
      setDraft(next); onSave(next);
    };
    const updateField = (k, v) => { const next = { ...draft, [k]: v }; setDraft(next); onSave(next); };
    return (
      <div style={{maxWidth:680}}>
        <div style={{background:C.panel,border:`1px solid ${C.border}`,borderRadius:12,padding:"16px 20px",marginBottom:14}}>
          <Sec>Weekly Hours</Sec>
          <div style={{fontSize:11,color:C.muted,marginBottom:12}}>Days are scheduled in your local timezone ({resolveTz(draft.timeZone)}{draft.timeZone==="AUTO"?" · auto":""}). Slots are generated in 30-min increments inside these windows.</div>
          {DAYS.map(([k,label])=>{
            const win = draft.weeklyHours[k]?.[0];
            const enabled = !!win;
            return (
              <div key={k} style={{display:"flex",alignItems:"center",gap:12,padding:"7px 0",borderTop:`1px solid ${C.border}`}}>
                <div style={{display:"flex",alignItems:"center",gap:8,width:120,cursor:"pointer"}} onClick={()=>toggleDay(k)}>
                  <div style={{width:15,height:15,borderRadius:3,border:`1.5px solid ${enabled?"#4ade80":C.border}`,background:enabled?"#4ade80":"transparent",display:"flex",alignItems:"center",justifyContent:"center",color:"#052e16",fontSize:11,fontWeight:900}}>{enabled?"✓":""}</div>
                  <span style={{fontSize:13,color:enabled?C.text:C.muted,fontWeight:600}}>{label}</span>
                </div>
                {enabled ? (
                  <>
                    <input type="time" value={win.start} onChange={e=>updateWindow(k,"start",e.target.value)} style={inp({width:120})}/>
                    <span style={{color:C.dim,fontSize:12}}>to</span>
                    <input type="time" value={win.end} onChange={e=>updateWindow(k,"end",e.target.value)} style={inp({width:120})}/>
                  </>
                ) : (
                  <span style={{fontSize:11,color:C.dim,fontStyle:"italic"}}>Unavailable</span>
                )}
              </div>
            );
          })}
        </div>
        <div style={{background:C.panel,border:`1px solid ${C.border}`,borderRadius:12,padding:"16px 20px",marginBottom:14}}>
          <Sec>Scheduling Rules</Sec>
          <div style={{display:"flex",alignItems:"center",gap:14,padding:"7px 0",borderTop:`1px solid ${C.border}`}}>
            <div style={{flex:1}}>
              <div style={{fontSize:13,fontWeight:700,color:C.text}}>Time Zone</div>
              <div style={{fontSize:11,color:C.muted}}>Slots are generated in this timezone.</div>
            </div>
            <select value={draft.timeZone} onChange={e=>updateField("timeZone",e.target.value)} style={inp({width:280})}>
              {TIMEZONE_GROUPS.map(g => (
                <optgroup key={g.label} label={g.label}>
                  {g.zones.map(([val,lbl]) => <option key={val} value={val}>{lbl}</option>)}
                </optgroup>
              ))}
            </select>
          </div>
          <div style={{display:"flex",alignItems:"center",gap:14,padding:"7px 0",borderTop:`1px solid ${C.border}`}}>
            <div style={{flex:1}}>
              <div style={{fontSize:13,fontWeight:700,color:C.text}}>Minimum Notice (hours)</div>
              <div style={{fontSize:11,color:C.muted}}>Earliest a meeting can be booked from now.</div>
            </div>
            <input type="number" min="0" max="168" defaultValue={draft.minNoticeHours} onBlur={e=>updateField("minNoticeHours",+e.target.value||0)} style={inp({width:90})}/>
          </div>
          <div style={{display:"flex",alignItems:"center",gap:14,padding:"7px 0",borderTop:`1px solid ${C.border}`}}>
            <div style={{flex:1}}>
              <div style={{fontSize:13,fontWeight:700,color:C.text}}>Maximum Advance (days)</div>
              <div style={{fontSize:11,color:C.muted}}>Furthest into the future a booking can be made.</div>
            </div>
            <input type="number" min="1" max="365" defaultValue={draft.maxAdvanceDays} onBlur={e=>updateField("maxAdvanceDays",+e.target.value||60)} style={inp({width:90})}/>
          </div>
          <div style={{display:"flex",alignItems:"center",gap:14,padding:"7px 0",borderTop:`1px solid ${C.border}`}}>
            <div style={{flex:1}}>
              <div style={{fontSize:13,fontWeight:700,color:C.text}}>Default Buffer (min)</div>
              <div style={{fontSize:11,color:C.muted}}>Padding inserted between back-to-back meetings.</div>
            </div>
            <input type="number" min="0" max="120" step="5" defaultValue={draft.defaultBufferMin} onBlur={e=>updateField("defaultBufferMin",+e.target.value||0)} style={inp({width:90})}/>
          </div>
        </div>
      </div>
    );
  };

  const SettingsView = () => {
    // Pull active category from the hoisted state at App scope so a re-render doesn't reset it.
    const cat = settingsCat;
    const setCat = setSettingsCat;
    const [dsExpanded, setDsExpanded] = useState(false);
    const [dsKeyInput, setDsKeyInput] = useState(docusignKey || "");
    const [apiKeyInput, setApiKeyInput] = useState(localStorage.getItem("ff_api_key")||"");
    const importFileRef = useRef(null);

    const CATEGORIES = [
      { id:"profile",       icon:"👤", label:"Profile & Company",     desc:"Your identity and company info used across the app." },
      { id:"brands",        icon:"🏢", label:"Brands",                desc:"Add, edit, and delete franchise brands. FDD upload + brand colors live here." },
      { id:"appearance",    icon:"🎨", label:"Appearance",            desc:"Theme, density, and the look of the app." },
      { id:"notifications", icon:"🔔", label:"Notifications",         desc:"What gets your attention and when." },
      { id:"pipeline",      icon:"📊", label:"Pipeline",              desc:"Default views, scoring thresholds, and pipeline behavior." },
      { id:"intake",        icon:"📝", label:"Lead Intake",           desc:"Lead sources, net worth & liquidity ranges. New sources can wire up integrations." },
      { id:"custom_fields", icon:"➕", label:"Custom Fields",         desc:"Extra fields on leads, opportunities, and brokers. Built-in fields are locked." },
      { id:"team",          icon:"👥", label:"Team & Seats",          desc:"Manage the people who can access this brand. Drives the Assigned To dropdown." },
      { id:"ai",            icon:"🤖", label:"AI",                    desc:"AI scoring, organize, summaries, and the model." },
      { id:"email",         icon:"✉️", label:"Email & Templates",     desc:"Signature, sender details, and template behavior." },
      { id:"integrations",  icon:"🔌", label:"Integrations",          desc:"DocuSign, Twilio, SendGrid, Zapier, Google Calendar, Outlook, Slack." },
      { id:"brokers",       icon:"🤝", label:"Brokers",               desc:"Defaults and behaviors for the Broker Hub." },
      { id:"security",      icon:"🔒", label:"Security",              desc:"Authentication, sessions, and access control." },
      { id:"data",          icon:"💾", label:"Data Management",       desc:"Export, import, restore demo, or wipe everything." },
      { id:"duplicates",    icon:"🔁", label:"Duplicate Leads",       desc:"Leads that matched an existing email or phone. Restore any back to the pipeline." },
      { id:"advanced",      icon:"⚙",  label:"Advanced",              desc:"Webhooks, API, debug, and experimental features." },
    ];
    const currentCat = CATEGORIES.find(c => c.id === cat);

    return (
      <div style={{display:"flex", gap:18, alignItems:"flex-start"}}>
        <div style={{width:210, flexShrink:0, display:"flex", flexDirection:"column", gap:2, position:"sticky", top:0}}>
          <div style={{fontSize:10, fontWeight:800, color:C.muted, letterSpacing:".08em", padding:"4px 11px 8px"}}>SETTINGS</div>
          {CATEGORIES.map(c => (
            <button key={c.id} onClick={()=>setCat(c.id)} style={{ background: cat===c.id ? "#162035" : "transparent", border:"none", borderRadius:9, padding:"9px 11px", color: cat===c.id ? C.text : C.muted, cursor:"pointer", fontFamily:"inherit", textAlign:"left", display:"flex", alignItems:"center", gap:9, fontSize:13, fontWeight: cat===c.id?700:500 }}>
              <span style={{fontSize:14, color: cat===c.id ? C.accent : C.dim}}>{c.icon}</span>
              <span>{c.label}</span>
            </button>
          ))}
        </div>
        <div style={{flex:1, minWidth:0, maxWidth:760}}>
          <div style={{marginBottom:18}}>
            <h2 style={{fontSize:20, fontWeight:900, color:"#f0f6ff", margin:0}}>{currentCat?.label}</h2>
            <div style={{fontSize:12, color:C.muted, marginTop:3}}>{currentCat?.desc}</div>
          </div>
          <div style={{background:C.panel, border:`1px solid ${C.border}`, borderRadius:14, padding:"4px 20px 18px"}}>
            {cat === "profile" && <>
              <SettingsSection title="Your Profile">
                <SettingRow label="Display Name" description="Used in templates as {REP_NAME} and shown on emails you send.">
                  <input defaultValue={settings.repName} onBlur={e=>setSetting("repName", e.target.value)} placeholder="Your full name" style={inp({width:260})}/>
                </SettingRow>
                <SettingRow label="Role / Title" description="Appended to your signature in default email templates.">
                  <input defaultValue={settings.repRole} onBlur={e=>setSetting("repRole", e.target.value)} placeholder="e.g. Franchise Development Manager" style={inp({width:260})}/>
                </SettingRow>
                <SettingRow label="Email" description="Your work email — used as the reply-to on sent envelopes."><input defaultValue={settings.repEmail} onBlur={e=>setSetting("repEmail", e.target.value)} placeholder="you@example.com" style={inp({width:260})}/></SettingRow>
                <SettingRow label="Phone"><input defaultValue={settings.repPhone} onBlur={e=>setSetting("repPhone", e.target.value)} placeholder="555-555-1234" style={inp({width:260})}/></SettingRow>
              </SettingsSection>
              <SettingsSection title="Company">
                <SettingRow label="Company Name" description="Default value for {BRAND} in templates when no brand is active."><input defaultValue={settings.companyName} onBlur={e=>setSetting("companyName", e.target.value)} placeholder="Your franchisor company" style={inp({width:260})}/></SettingRow>
                <SettingRow label="Website"><input defaultValue={settings.companyWebsite} onBlur={e=>setSetting("companyWebsite", e.target.value)} placeholder="https://example.com" style={inp({width:260})}/></SettingRow>
                <SettingRow label="Phone"><input defaultValue={settings.companyPhone} onBlur={e=>setSetting("companyPhone", e.target.value)} placeholder="555-555-1234" style={inp({width:260})}/></SettingRow>
                <SettingRow label="Email"><input defaultValue={settings.companyEmail} onBlur={e=>setSetting("companyEmail", e.target.value)} placeholder="info@example.com" style={inp({width:260})}/></SettingRow>
                <SettingRow label="Address"><input defaultValue={settings.companyAddress} onBlur={e=>setSetting("companyAddress", e.target.value)} placeholder="123 Main St, City, ST" style={inp({width:260})}/></SettingRow>
              </SettingsSection>
              <SettingsSection title="Locale & Formatting">
                <SettingRow label="Timezone">
                  <select value={settings.timezone} onChange={e=>setSetting("timezone", e.target.value)} style={inp({width:300})}>
                    {TIMEZONE_GROUPS.map(g => (
                      <optgroup key={g.label} label={g.label}>
                        {g.zones.map(([val,lbl]) => <option key={val} value={val}>{lbl}</option>)}
                      </optgroup>
                    ))}
                  </select>
                </SettingRow>
                <SettingRow label="Date Format">
                  <select value={settings.dateFormat} onChange={e=>setSetting("dateFormat", e.target.value)} style={inp({width:260})}>
                    {["MMM D, YYYY","MM/DD/YYYY","DD/MM/YYYY","YYYY-MM-DD"].map(f=><option key={f}>{f}</option>)}
                  </select>
                </SettingRow>
                <SettingRow label="Time Format">
                  <select value={settings.timeFormat} onChange={e=>setSetting("timeFormat", e.target.value)} style={inp({width:260})}>
                    <option value="12h">12-hour (1:30 PM)</option><option value="24h">24-hour (13:30)</option>
                  </select>
                </SettingRow>
                <SettingRow label="Currency">
                  <select value={settings.currency} onChange={e=>setSetting("currency", e.target.value)} style={inp({width:260})}>
                    {["USD","CAD","EUR","GBP","AUD","MXN","BRL","INR","JPY"].map(c=><option key={c}>{c}</option>)}
                  </select>
                </SettingRow>
                <SettingRow label="Week Starts On">
                  <select value={settings.weekStartsOn} onChange={e=>setSetting("weekStartsOn", e.target.value)} style={inp({width:260})}>
                    {["Sunday","Monday"].map(d=><option key={d}>{d}</option>)}
                  </select>
                </SettingRow>
              </SettingsSection>
            </>}

            {cat === "brands" && <>
              <SettingsSection title="All Brands">
                <div style={{fontSize:11,color:C.muted,padding:"4px 0 12px",lineHeight:1.55}}>
                  Each brand has its own pipeline, leads, opportunities, brokers, scheduling data, and FDD. Switch between brands from the sidebar tile. <strong style={{color:"#fbbf24"}}>Adding brands is gated to org admins</strong> — rep accounts won't see the + Add Brand button when multi-seat SaaS launches.
                </div>
                <div style={{display:"flex",flexDirection:"column",gap:8,marginBottom:14}}>
                  {brands.map(b => {
                    const isActive = b.id === activeBrand;
                    const leadCount = (leads[b.id]||[]).length;
                    const oppCount  = (opps[b.id]||[]).length;
                    return (
                      <div key={b.id} style={{display:"flex",alignItems:"center",gap:11,background:isActive?"#101c2e":"#090f1c",border:`1px solid ${isActive?C.accent+"55":C.border}`,borderRadius:10,padding:"11px 14px"}}>
                        <div style={{width:40,height:40,borderRadius:10,background:b.color||"#1e3a5f",display:"flex",alignItems:"center",justifyContent:"center",fontSize:20,flexShrink:0}}>{b.emoji||"🏢"}</div>
                        <div style={{flex:1,minWidth:0}}>
                          <div style={{display:"flex",alignItems:"center",gap:8,marginBottom:2}}>
                            <div style={{fontSize:14,fontWeight:800,color:C.text}}>{b.name}</div>
                            {isActive && <span style={{fontSize:9,fontWeight:800,color:"#4ade80",background:"#091c09",border:"1px solid #4ade8055",borderRadius:4,padding:"1px 6px",letterSpacing:".05em"}}>ACTIVE</span>}
                          </div>
                          <div style={{fontSize:11,color:C.muted}}>{[b.industry, `${leadCount} leads`, `${oppCount} opps`].filter(Boolean).join(" · ")}</div>
                          {b.fddData && <div style={{fontSize:10,color:"#4ade80",marginTop:3}}>📊 FDD data uploaded</div>}
                        </div>
                        <div style={{display:"flex",gap:6}}>
                          {!isActive && <button onClick={()=>switchBrand(b.id)} style={btn(C.dim,"#60a5fa")} title="Switch to this brand">Switch</button>}
                          <button onClick={()=>{setModal("editBrand");setModalData({brand:{...b}});}} title="Edit brand details + FDD upload" style={btn(C.dim,C.muted)}>✏️ Edit</button>
                          {brands.length > 1 && <button onClick={()=>{ if(confirm(`Delete "${b.name}"? All leads, opps, and history for this brand will be permanently removed. This cannot be undone.`)) deleteBrand(b.id); }} title="Delete brand (permanent)" style={btn("#1a0808","#f87171")}>🗑</button>}
                        </div>
                      </div>
                    );
                  })}
                </div>
                <button onClick={()=>{setModal("editBrand");setModalData({brand:null});}} style={btn("#091c09","#4ade80",true)} title="Create a new brand (admin only in SaaS)">+ Add Brand</button>
              </SettingsSection>
            </>}

            {cat === "appearance" && <>
              <SettingsSection title="Theme">
                <SettingRow label="Color Scheme" description="The overall light/dark appearance of the app." badge={{text:"PREVIEW", color:"#facc15"}}>
                  <select value={settings.theme} onChange={e=>setSetting("theme", e.target.value)} style={inp({width:200})} disabled>
                    <option value="dark">🌙 Dark</option><option value="light">☀️ Light (coming soon)</option><option value="auto">🔄 Auto (coming soon)</option>
                  </select>
                </SettingRow>
                <SettingRow label="Accent Color" description="Highlight color for active tabs, buttons, and focus rings.">
                  <input type="color" value={settings.accentColor} onChange={e=>setSetting("accentColor", e.target.value)} style={{width:48, height:32, border:"none", borderRadius:7, cursor:"pointer"}}/>
                </SettingRow>
                <SettingRow label="Density">
                  <select value={settings.density} onChange={e=>setSetting("density", e.target.value)} style={inp({width:200})}>
                    <option value="comfortable">Comfortable</option><option value="compact">Compact</option><option value="spacious">Spacious</option>
                  </select>
                </SettingRow>
              </SettingsSection>
              <SettingsSection title="Layout">
                <SettingRow label="Sidebar Default" description="How the left nav appears when you first open the app.">
                  <select value={settings.sidebarDefault} onChange={e=>setSetting("sidebarDefault", e.target.value)} style={inp({width:200})}>
                    <option value="open">Open (full labels)</option><option value="collapsed">Collapsed (icons only)</option>
                  </select>
                </SettingRow>
                <SettingRow label="Show Welcome Banner" description="Display the API-key prompt banner at the top until you connect."><ToggleSwitch checked={settings.showWelcomeBanner} onChange={v=>setSetting("showWelcomeBanner", v)}/></SettingRow>
              </SettingsSection>
            </>}

            {cat === "notifications" && <>
              <SettingsSection title="In-App">
                <SettingRow label="In-App Toasts" description="Show small confirmation toasts when records are saved/changed."><ToggleSwitch checked={settings.inAppToasts} onChange={v=>setSetting("inAppToasts", v)}/></SettingRow>
                <SettingRow label="Toast Duration">
                  <select value={settings.toastDuration} onChange={e=>setSetting("toastDuration", e.target.value)} style={inp({width:200})}>
                    <option value="short">Short (1.8s)</option><option value="medium">Medium (3.2s)</option><option value="long">Long (5.5s)</option>
                  </select>
                </SettingRow>
                <SettingRow label="Sound on Notify" description="Play a subtle sound when toasts fire." badge={{text:"PREVIEW", color:"#facc15"}}><ToggleSwitch checked={settings.soundEnabled} onChange={v=>setSetting("soundEnabled", v)}/></SettingRow>
              </SettingsSection>
              <SettingsSection title="Email & Push">
                <SettingRow label="Email Digest">
                  <select value={settings.emailDigest} onChange={e=>setSetting("emailDigest", e.target.value)} style={inp({width:200})}>
                    <option value="off">Off</option><option value="daily">Daily summary</option><option value="weekly">Weekly summary</option><option value="realtime">Real-time</option>
                  </select>
                </SettingRow>
                <SettingRow label="Browser Push" description="Request permission for desktop browser notifications." badge={{text:"PREVIEW", color:"#facc15"}}><ToggleSwitch checked={settings.browserPush} onChange={v=>setSetting("browserPush", v)}/></SettingRow>
              </SettingsSection>
              <SettingsSection title="Triggers">
                <SettingRow label="Stale Opportunity Alerts" description="Notify me when an opp passes its stale threshold."><ToggleSwitch checked={settings.notifyOnStale} onChange={v=>setSetting("notifyOnStale", v)}/></SettingRow>
                <SettingRow label="New Lead Created"><ToggleSwitch checked={settings.notifyOnNewLead} onChange={v=>setSetting("notifyOnNewLead", v)}/></SettingRow>
                <SettingRow label="Broker Reply"><ToggleSwitch checked={settings.notifyOnBrokerReply} onChange={v=>setSetting("notifyOnBrokerReply", v)}/></SettingRow>
                <SettingRow label="Opportunity Score Drop"><ToggleSwitch checked={settings.notifyOnScoreDrop} onChange={v=>setSetting("notifyOnScoreDrop", v)}/></SettingRow>
              </SettingsSection>
            </>}

            {cat === "pipeline" && <>
              <SettingsSection title="Defaults">
                <SettingRow label="Default View" description="The initial view when you open Leads or Opportunities.">
                  <select value={settings.defaultView} onChange={e=>setSetting("defaultView", e.target.value)} style={inp({width:200})}>
                    <option value="list">☰ List</option><option value="kanban">▦ Kanban</option>
                  </select>
                </SettingRow>
                <SettingRow label="Show Score Rings" description="Display the AI score circle on cards and rows."><ToggleSwitch checked={settings.showScoreRings} onChange={v=>setSetting("showScoreRings", v)}/></SettingRow>
                <SettingRow label="Show Stale Badges" description="Highlight opportunities that are past their stale threshold."><ToggleSwitch checked={settings.showStaleBadges} onChange={v=>setSetting("showStaleBadges", v)}/></SettingRow>
                <SettingRow label="Require Territory" description="Force a Territory value when creating a new lead or opp." badge={{text:"PREVIEW", color:"#facc15"}}><ToggleSwitch checked={settings.requireTerritoryOnNew} onChange={v=>setSetting("requireTerritoryOnNew", v)}/></SettingRow>
              </SettingsSection>
              <SettingsSection title="Scoring Thresholds">
                <SettingRow label="High Score Threshold" description="Scores at/above this turn green (default 75).">
                  <input type="number" min="0" max="100" defaultValue={settings.highScoreThreshold} onBlur={e=>setSetting("highScoreThreshold", +e.target.value||75)} style={inp({width:90})}/>
                </SettingRow>
                <SettingRow label="Low Score Threshold" description="Scores at/below this turn red (default 30).">
                  <input type="number" min="0" max="100" defaultValue={settings.lowScoreThreshold} onBlur={e=>setSetting("lowScoreThreshold", +e.target.value||30)} style={inp({width:90})}/>
                </SettingRow>
              </SettingsSection>
              <SettingsSection title="Automations">
                <SettingRow label="Auto-stage on DocuSign Send" description="Move to FDD Sent / Agreement Sent automatically when an envelope is sent."><ToggleSwitch checked={settings.autoStageOnDocusign} onChange={v=>setSetting("autoStageOnDocusign", v)}/></SettingRow>
              </SettingsSection>
            </>}

            {cat === "intake" && <>
              <LeadIntakeSettings settings={settings} setSetting={setSetting}/>
            </>}

            {cat === "custom_fields" && <>
              <CustomFieldsSettings settings={settings} setSetting={setSetting}/>
            </>}

            {cat === "team" && <>
              <TeamSeatsSettings settings={settings} setSetting={setSetting} brands={brands} activeBrand={activeBrand}/>
            </>}

            {cat === "ai" && <>
              <SettingsSection title="API">
                <SettingRow label="Anthropic API Key" description="Required for live AI features. Without it, mock responses are returned.">
                  <input type="password" value={apiKeyInput} onChange={e=>setApiKeyInput(e.target.value)} onBlur={e=>{ const v=e.target.value.trim(); if(v) localStorage.setItem("ff_api_key", v); else localStorage.removeItem("ff_api_key"); }} placeholder="sk-ant-..." style={inp({width:260, fontFamily:"ui-monospace,monospace", fontSize:12})}/>
                </SettingRow>
              </SettingsSection>
              <SettingsSection title="Model per Feature" subtitle="Pick the model used for each feature. Haiku is the default — fast and ~3× cheaper than Sonnet, ~15× cheaper than Opus.">
                {[
                  ["aiModelScore",     "AI Scoring",   "Runs per opportunity on load and after note edits. Stays cheap on Haiku."],
                  ["aiModelWhatsNext", "What's Next",  "Pipeline-wide single-action recommendation."],
                  ["aiModelSummary",   "AI Summary",   "Per-candidate review when you click ✨ AI Summary. Sonnet helps for deeper reviews."],
                  ["aiModelOrganize",  "AI Organize",  "Pipeline audit across all opps. Sonnet is worth considering here."],
                  ["aiModelFDD",       "FDD Parser",   "Extracts Items 5/6/7/19 from an uploaded FDD PDF. Sonnet handles long docs better."],
                ].map(([key, label, desc])=>(
                  <SettingRow key={key} label={label} description={desc}>
                    <select value={settings[key]||"claude-haiku-4-5"} onChange={e=>setSetting(key, e.target.value)} style={inp({width:200})}>
                      {AI_MODELS.map(m => <option key={m.id} value={m.id}>{m.label} — {m.desc}</option>)}
                    </select>
                  </SettingRow>
                ))}
              </SettingsSection>
              <SettingsSection title="Behavior">
                <SettingRow label="Auto-Score on Load" description="Automatically score all opportunities when the app opens."><ToggleSwitch checked={settings.autoScoreOnLoad} onChange={v=>setSetting("autoScoreOnLoad", v)}/></SettingRow>
                <SettingRow label="Re-score When Note Added" description="Re-run AI scoring whenever a new note is saved on an opportunity."><ToggleSwitch checked={settings.autoScoreOnNote} onChange={v=>setSetting("autoScoreOnNote", v)}/></SettingRow>
                <SettingRow label="Auto-Open AI Summary" description="Automatically open the AI summary modal when an opportunity profile is viewed." badge={{text:"PREVIEW", color:"#facc15"}}><ToggleSwitch checked={settings.aiSummaryAutoOpen} onChange={v=>setSetting("aiSummaryAutoOpen", v)}/></SettingRow>
                <SettingRow label="AI Organize Frequency">
                  <select value={settings.aiOrganizeFrequency} onChange={e=>setSetting("aiOrganizeFrequency", e.target.value)} style={inp({width:200})}>
                    <option value="manual">Manual only</option><option value="daily">Daily reminder</option><option value="weekly">Weekly reminder</option>
                  </select>
                </SettingRow>
                <SettingRow label="Default AI Tone" description="How AI-generated content should sound across summaries and templates.">
                  <select value={settings.aiToneHint} onChange={e=>setSetting("aiToneHint", e.target.value)} style={inp({width:200})}>
                    <option value="professional">Professional</option><option value="friendly">Friendly</option><option value="direct">Direct</option><option value="warm">Warm</option>
                  </select>
                </SettingRow>
              </SettingsSection>
            </>}

            {cat === "email" && <>
              <SettingsSection title="Signature">
                <SettingRow label="Email Signature" description="Appended automatically to the bottom of every email template you send.">
                  <textarea defaultValue={settings.emailSignature} onBlur={e=>setSetting("emailSignature", e.target.value)} rows={5} placeholder={`Best regards,\n${settings.repName||"Your Name"}\n${settings.repRole||""}\n${settings.companyName||""}`} style={{...inp({width:320, resize:"vertical", fontFamily:"inherit", fontSize:12})}}/>
                </SettingRow>
                <SettingRow label="Default From Name"><input defaultValue={settings.fromName} onBlur={e=>setSetting("fromName", e.target.value)} placeholder={settings.repName||"Your Name"} style={inp({width:260})}/></SettingRow>
              </SettingsSection>
              <SettingsSection title="Email Behavior">
                <SettingRow label="Default Footer (HTML)" description="Optional disclaimer or compliance footer.">
                  <textarea defaultValue={settings.defaultEmailFooter} onBlur={e=>setSetting("defaultEmailFooter", e.target.value)} rows={2} placeholder="This message and any attachments are confidential…" style={{...inp({width:320, resize:"vertical", fontFamily:"ui-monospace,monospace", fontSize:11})}}/>
                </SettingRow>
                <SettingRow label="Tracking Pixel" description="Insert a 1x1 tracking pixel to detect when an email is opened." badge={{text:"PREVIEW", color:"#facc15"}}><ToggleSwitch checked={settings.trackingPixel} onChange={v=>setSetting("trackingPixel", v)}/></SettingRow>
                <SettingRow label="Append Unsubscribe Link" description="Add a one-click unsubscribe footer to marketing emails." badge={{text:"PREVIEW", color:"#facc15"}}><ToggleSwitch checked={settings.unsubscribeLink} onChange={v=>setSetting("unsubscribeLink", v)}/></SettingRow>
                <SettingRow label="BCC Archive Address" description="An email address that's BCC'd on every sent email for archival."><input defaultValue={settings.bccArchive} onBlur={e=>setSetting("bccArchive", e.target.value)} placeholder="archive@example.com" style={inp({width:260})}/></SettingRow>
                <SettingRow label="Use Brand Color in Templates" description="Apply your brand's color to marketing template CTAs by default."><ToggleSwitch checked={settings.defaultMergeBrandColor} onChange={v=>setSetting("defaultMergeBrandColor", v)}/></SettingRow>
              </SettingsSection>
            </>}

            {cat === "integrations" && <>
              <SettingsSection title="Connected Apps" subtitle="Manage your third-party connections.">
                {[
                  {id:"docusign", n:"DocuSign API", i:"✍️", d:"Send FDDs & agreements; auto-track sent/viewed/signed status.", c:"#facc15", live:true},
                  {id:"twilio",   n:"Twilio",       i:"📞", d:"In-CRM calling & SMS.",                  c:"#f97316", live:false},
                  {id:"sendgrid", n:"SendGrid",     i:"✉️", d:"Automated email sequences & deliverability.", c:"#60a5fa", live:false},
                  {id:"zapier",   n:"Zapier",       i:"⚡", d:"5,000+ app connections via webhook.",     c:"#a78bfa", live:false},
                  {id:"google_cal", n:"Google Calendar", i:"📆", d:"Two-way sync bookings to your Google Calendar.", c:"#4285f4", live:false},
                  {id:"outlook_cal",n:"Outlook Calendar",i:"📨", d:"Two-way sync bookings to Outlook / Microsoft 365.", c:"#0078d4", live:false},
                  {id:"slack",    n:"Slack",        i:"💬", d:"Forward CRM events to a Slack channel.",  c:"#ec4899", live:false},
                ].map(i=>{
                  const isDocusign = i.id === "docusign";
                  const connected = isDocusign && hasDocusignKey;
                  return (
                    <div key={i.id} style={{paddingTop:13, paddingBottom:13, borderTop:`1px solid ${C.border}`}}>
                      <div style={{display:"flex", alignItems:"center", gap:11}}>
                        <span style={{fontSize:22}}>{i.i}</span>
                        <div style={{flex:1, minWidth:0}}>
                          <div style={{fontWeight:800, color:C.text, fontSize:13, display:"flex", alignItems:"center", gap:7, flexWrap:"wrap"}}>
                            {i.n}
                            {connected && <span style={{background:"#091c09",color:"#4ade80",border:"1px solid #4ade8044",borderRadius:5,padding:"1px 7px",fontSize:10,fontWeight:700}}>● Connected</span>}
                          </div>
                          <div style={{fontSize:11, color:C.muted, marginTop:3}}>{i.d}</div>
                        </div>
                        {i.live ? (
                          <button onClick={()=>{ setDsExpanded(!dsExpanded); if(!dsExpanded) setDsKeyInput(docusignKey||""); }} style={btn(C.dim, connected?"#4ade80":C.accent)}>{connected ? (dsExpanded?"Close":"Manage") : (dsExpanded?"Close":"Connect")}</button>
                        ) : (
                          <button disabled style={{...btn(C.dim,C.muted),opacity:.6,cursor:"not-allowed"}}>Coming soon</button>
                        )}
                      </div>
                      {isDocusign && dsExpanded && (
                        <div style={{marginTop:11, marginLeft:33, background:"#090f1c", border:`1px solid ${C.border}`, borderRadius:10, padding:"14px 16px"}}>
                          <div style={{fontSize:12, color:C.muted, marginBottom:11, lineHeight:1.55}}>Paste your DocuSign <strong style={{color:C.text}}>Integration Key</strong> (Client ID). Stored locally only.</div>
                          <input value={dsKeyInput} onChange={e=>setDsKeyInput(e.target.value)} placeholder="12345678-abcd-1234-abcd-1234567890ab" style={inp({width:"100%", boxSizing:"border-box", fontFamily:"ui-monospace,monospace", fontSize:12})}/>
                          <div style={{background:"#1a1908", border:"1px solid #facc1533", borderRadius:8, padding:"8px 12px", fontSize:11, color:"#facc15", marginTop:10, marginBottom:11}}>ⓘ <strong>Sandbox mode.</strong> Any non-empty key activates the UI. Real DocuSign requires server-side OAuth.</div>
                          <div style={{display:"flex", gap:7, justifyContent:"flex-end"}}>
                            {connected && <button onClick={()=>{ saveDocusignKey(""); setDsKeyInput(""); toast$("DocuSign disconnected."); }} style={btn("#1a0808","#f87171")}>Disconnect</button>}
                            <button onClick={()=>{ saveDocusignKey(dsKeyInput.trim()); toast$(dsKeyInput.trim()?"DocuSign connected!":"DocuSign disconnected."); setDsExpanded(false); }} style={btn("#091c09","#4ade80",true)}>{connected?"Update Key":"Connect"}</button>
                          </div>
                        </div>
                      )}
                    </div>
                  );
                })}
              </SettingsSection>
              <SettingsSection title="Webhook URLs" subtitle="Generic outbound webhooks (stored, not yet wired).">
                <SettingRow label="Zapier Webhook"><input defaultValue={settings.zapierWebhook} onBlur={e=>setSetting("zapierWebhook", e.target.value)} placeholder="https://hooks.zapier.com/..." style={inp({width:320, fontFamily:"ui-monospace,monospace", fontSize:11})}/></SettingRow>
                <SettingRow label="Slack Incoming Webhook"><input defaultValue={settings.slackWebhook} onBlur={e=>setSetting("slackWebhook", e.target.value)} placeholder="https://hooks.slack.com/services/..." style={inp({width:320, fontFamily:"ui-monospace,monospace", fontSize:11})}/></SettingRow>
              </SettingsSection>
            </>}

            {cat === "brokers" && <>
              <SettingsSection title="Display">
                <SettingRow label="Show Broker Section on Leads" description="By default broker assignment only appears on opportunities. Enable to also expose it on leads."><ToggleSwitch checked={settings.showBrokerInLeads} onChange={v=>setSetting("showBrokerInLeads", v)}/></SettingRow>
              </SettingsSection>
              <SettingsSection title="Defaults">
                <SettingRow label="Default Commission Structure" description="Pre-fills the Commission field when adding a new broker."><input defaultValue={settings.defaultCommissionStructure} onBlur={e=>setSetting("defaultCommissionStructure", e.target.value)} style={inp({width:260})}/></SettingRow>
                <SettingRow label="Auto-Assign Broker" description="Auto-assign the broker who originally referred a lead when converting to an opp." badge={{text:"PREVIEW", color:"#facc15"}}><ToggleSwitch checked={settings.autoAssignBroker} onChange={v=>setSetting("autoAssignBroker", v)}/></SettingRow>
              </SettingsSection>
            </>}

            {cat === "security" && <>
              <SettingsSection title="Authentication">
                <SettingRow label="Two-Factor Authentication" description="Require a second factor on sign-in." badge={{text:"COMING SOON", color:"#94a3b8"}}><ToggleSwitch checked={settings.twoFactorEnabled} onChange={v=>setSetting("twoFactorEnabled", v)} disabled/></SettingRow>
                <SettingRow label="Session Timeout (minutes)" description="Sign out automatically after this many idle minutes.">
                  <input type="number" min="5" max="1440" defaultValue={settings.sessionTimeout} onBlur={e=>setSetting("sessionTimeout", +e.target.value||60)} style={inp({width:100})}/>
                </SettingRow>
                <SettingRow label="Password Expiry (days)" description="Force a password reset every N days.">
                  <input type="number" min="0" max="365" defaultValue={settings.passwordExpiry} onBlur={e=>setSetting("passwordExpiry", +e.target.value||90)} style={inp({width:100})}/>
                </SettingRow>
              </SettingsSection>
              <SettingsSection title="Access Control">
                <SettingRow label="IP Allowlist" description="Restrict logins to these IP addresses (one per line)." badge={{text:"PREVIEW", color:"#facc15"}}>
                  <textarea defaultValue={settings.ipAllowlist} onBlur={e=>setSetting("ipAllowlist", e.target.value)} rows={3} placeholder="203.0.113.45" style={{...inp({width:260, resize:"vertical", fontFamily:"ui-monospace,monospace", fontSize:11})}}/>
                </SettingRow>
                <SettingRow label="Audit Log" description="Record significant user actions (record edits, stage changes, deletes)."><ToggleSwitch checked={settings.auditLog} onChange={v=>setSetting("auditLog", v)}/></SettingRow>
              </SettingsSection>
            </>}

            {cat === "data" && <>
              <SettingsSection title="Export & Backup">
                <SettingRow label="Export All Data" description="Download every record, broker, template, and setting as a single JSON file.">
                  <button onClick={exportAllData} style={btn("#091420","#60a5fa")}>📥 Download JSON</button>
                </SettingRow>
                <SettingRow label="Import Data" description="Restore from a previously-exported JSON file. Will overwrite current data.">
                  <input ref={importFileRef} type="file" accept="application/json" style={{display:"none"}} onChange={(e)=>{ if(e.target.files[0]) importAllData(e.target.files[0]); }}/>
                  <button onClick={()=>importFileRef.current?.click()} style={btn("#091420","#60a5fa")}>📤 Choose File…</button>
                </SettingRow>
                <SettingRow label="Data Retention (days)" description="How long to keep deleted records before purging permanently.">
                  <input type="number" min="30" max="3650" defaultValue={settings.dataRetentionDays} onBlur={e=>setSetting("dataRetentionDays", +e.target.value||730)} style={inp({width:120})}/>
                </SettingRow>
              </SettingsSection>
              <SettingsSection title="Reset" subtitle="Destructive actions — these cannot be undone.">
                <SettingRow label="Restore Tutorial Data" description="Reset to the default tutorial pipeline (Best Brand, sample brokers and opportunities).">
                  <button onClick={resetToDemoData} style={btn(C.dim,C.muted)}>↻ Reset to Tutorial</button>
                </SettingRow>
                <SettingRow label="Wipe Everything" description="Delete all data, settings, and API keys. Cannot be undone.">
                  <button onClick={wipeAllData} style={btn("#1a0808","#f87171")}>🗑 Wipe All Data</button>
                </SettingRow>
              </SettingsSection>
            </>}

            {cat === "duplicates" && <>
              <SettingsSection title="Flagged Duplicate Leads" subtitle="Incoming leads whose email or phone matched an existing lead/opp. Their data was merged into the original; restore any to bring them back as standalone leads.">
                {bDupLeads.length === 0 ? (
                  <div style={{padding:"22px 14px",textAlign:"center",color:C.muted,fontSize:13,lineHeight:1.55}}>No duplicate leads flagged yet. When someone creates a new lead with the same email or phone as an existing record, it'll show up here.</div>
                ) : (
                  <div style={{display:"flex",flexDirection:"column",gap:8}}>
                    {bDupLeads.slice().sort((a,b) => new Date(b.flaggedAt||0) - new Date(a.flaggedAt||0)).map(d => {
                      const original = (d.dupOfType === "lead" ? bLeads : bOpps).find(r => r.id === d.dupOf);
                      const reasonLabel = d.dupReason === "both" ? "email + phone" : d.dupReason;
                      return (
                        <div key={d.id} style={{background:"#090f1c",border:`1px solid ${C.border}`,borderRadius:10,padding:"12px 14px",display:"flex",alignItems:"flex-start",gap:11}}>
                          <span style={{fontSize:20,lineHeight:1,marginTop:2}}>🔁</span>
                          <div style={{flex:1,minWidth:0}}>
                            <div style={{display:"flex",alignItems:"center",gap:7,marginBottom:3,flexWrap:"wrap"}}>
                              <span style={{fontSize:13,fontWeight:800,color:C.text}}>{d.firstName||"(no first name)"} {d.lastName||""}</span>
                              <span style={{fontSize:9,color:"#facc15",background:"#1a1908",border:"1px solid #facc1544",borderRadius:5,padding:"1px 7px",fontWeight:700,letterSpacing:".04em"}}>MATCHED {reasonLabel.toUpperCase()}</span>
                            </div>
                            <div style={{fontSize:11,color:C.muted,marginBottom:5}}>
                              {d.email && <span style={{marginRight:14}}>✉️ {d.email}</span>}
                              {d.phone && <span>📱 {d.phone}</span>}
                            </div>
                            <div style={{fontSize:11,color:C.dim,marginBottom:8}}>
                              Merged into <strong style={{color:"#60a5fa"}}>{original ? `${original.firstName||""} ${original.lastName||""}`.trim() : "(original record no longer exists)"}</strong>
                              {d.dupOfType && <span style={{marginLeft:6,fontSize:9,color:d.dupOfType==="opp"?"#4ade80":"#60a5fa",background:d.dupOfType==="opp"?"#091c09":"#091420",border:`1px solid ${d.dupOfType==="opp"?"#4ade8033":"#60a5fa33"}`,borderRadius:4,padding:"1px 6px",fontWeight:700,textTransform:"uppercase"}}>{d.dupOfType}</span>}
                              {d.flaggedAt && <span style={{marginLeft:8}}>· {new Date(d.flaggedAt).toLocaleString("en-US",{month:"short",day:"numeric",year:"numeric",hour:"numeric",minute:"2-digit"})}</span>}
                            </div>
                            <div style={{display:"flex",gap:6,flexWrap:"wrap"}}>
                              <button onClick={()=>restoreDuplicate(d.id)} style={btn("#091c09","#4ade80",true)}>↺ Restore to Leads</button>
                              {original && <button onClick={()=>{ setNav(d.dupOfType==="opp"?"opps":"leads"); setSelected({type:d.dupOfType, id:d.dupOf}); setSubView("detail"); setModal(null); }} style={btn(C.dim,"#60a5fa")}>→ View Original</button>}
                              <button onClick={()=>deleteDuplicate(d.id)} style={btn("#1a0808","#f87171")}>🗑 Delete</button>
                            </div>
                          </div>
                        </div>
                      );
                    })}
                  </div>
                )}
              </SettingsSection>
              <SettingsSection title="How it works">
                <div style={{padding:"6px 13px",fontSize:12,color:C.muted,lineHeight:1.6}}>
                  When you (or an integration) creates a new lead, {BRAND.name} checks whether the same <strong style={{color:C.text}}>email</strong> or <strong style={{color:C.text}}>phone</strong> already exists in your leads or opportunities. If it does:
                  <ul style={{margin:"8px 0 8px 18px",padding:0,lineHeight:1.7}}>
                    <li>Any new fields from the incoming lead are merged into the existing record (existing values are never overwritten).</li>
                    <li>A note is added to the existing record describing the duplicate and what was merged.</li>
                    <li>The duplicate itself is stored here. You can restore it as a standalone lead any time, or delete it.</li>
                  </ul>
                  Phone numbers are matched by digits only — formatting and the US country code don't affect matching.
                </div>
              </SettingsSection>
            </>}

            {cat === "advanced" && <>
              <SettingsSection title="Developer">
                <SettingRow label="Outbound Webhook" description="POST every CRM event to this URL (lead created, stage changed, etc.).">
                  <input defaultValue={settings.webhookUrl} onBlur={e=>setSetting("webhookUrl", e.target.value)} placeholder={`https://your-server.com/${BRAND.nameLower}-webhook`} style={inp({width:320, fontFamily:"ui-monospace,monospace", fontSize:11})}/>
                </SettingRow>
                <SettingRow label="API Access" description={`Generate a ${BRAND.name} API token for external apps.`} badge={{text:"PREVIEW", color:"#facc15"}}><ToggleSwitch checked={settings.apiAccessEnabled} onChange={v=>setSetting("apiAccessEnabled", v)}/></SettingRow>
                <SettingRow label="Debug Mode" description="Print verbose logs to the browser console."><ToggleSwitch checked={settings.debugMode} onChange={v=>setSetting("debugMode", v)}/></SettingRow>
              </SettingsSection>
              <SettingsSection title="Experimental">
                <SettingRow label="Beta Features" description="Opt into in-progress features. May break things." badge={{text:"BETA", color:"#a78bfa", bg:"#160f30"}}><ToggleSwitch checked={settings.betaFeatures} onChange={v=>setSetting("betaFeatures", v)}/></SettingRow>
                <SettingRow label="Custom Kanban Swimlanes" description="Group kanban cards by broker, source, or assignee." badge={{text:"BETA", color:"#a78bfa", bg:"#160f30"}}><ToggleSwitch checked={settings.experimentalKanbanLanes} onChange={v=>setSetting("experimentalKanbanLanes", v)}/></SettingRow>
              </SettingsSection>
              <div style={{marginTop:18, padding:"12px 14px", background:"#090f1c", borderRadius:10, fontSize:11, color:C.dim, fontFamily:"ui-monospace,monospace"}}>
                <div style={{color:C.muted, marginBottom:4}}>Build info</div>
                <div>{BRAND.name} CRM · v5-dev</div>
                <div>Local storage size: {(JSON.stringify(localStorage).length/1024).toFixed(1)} KB</div>
                <div>Active brand: {brand?.name || "(none)"}</div>
              </div>
            </>}
          </div>
        </div>
      </div>
    );
  };

  // ════════════════════════════════════════════════════════
  //  MODALS
  // ════════════════════════════════════════════════════════
  const RecordForm=({type,rec:initRec})=>{
    const stages=type==="lead"?bLStages:bOStages;
    const [form,setForm]=useState(initRec||{stage:stages[0]?.id||"new_lead",firstName:"",lastName:"",email:"",phone:"",company:"",location:"",territory:"",netWorth:"",liquidity:"",source:"",assignedTo:"",brokerId:"",customFields:{},notes:[],activities:[],partners:[]});
    const set=(k,v)=>setForm(f=>({...f,[k]:v}));
    const setCustom=(k,v)=>setForm(f=>({...f,customFields:{...(f.customFields||{}),[k]:v}}));
    // Resolve dropdown option sources from current settings, falling back to the defaults
    // if an admin emptied a list. Net-worth + liquidity are now ranged dropdowns; lead
    // source is a managed dropdown with a "Manage sources…" link to settings; assigned-to
    // shows the rep + any team seats with access to the active brand (placeholder until SaaS).
    const netWorthOptions  = (settings.netWorthOptions  && settings.netWorthOptions.length)  ? settings.netWorthOptions  : DEFAULT_SETTINGS.netWorthOptions;
    const liquidityOptions = (settings.liquidityOptions && settings.liquidityOptions.length) ? settings.liquidityOptions : DEFAULT_SETTINGS.liquidityOptions;
    const sourceOptions    = (settings.leadSources && settings.leadSources.length) ? settings.leadSources.map(s => s.name) : DEFAULT_SETTINGS.leadSources.map(s => s.name);
    const seatOptions      = [
      settings.repName ? settings.repName : null,
      ...((settings.seats||[]).filter(s => !s.brandAccess?.length || s.brandAccess.includes(activeBrand)).map(s => s.name)),
    ].filter(Boolean);
    if (!seatOptions.length) seatOptions.push("You");
    // Broker is now a built-in field on both leads and opps. Stored as `brokerId` (FK to bBrokers),
    // rendered as the broker's "First Last" name in the dropdown so admins/reps don't see raw IDs.
    const brokerOptions = bBrokers.map(b => ({ id: b.id, label: `${b.firstName} ${b.lastName}`.trim() }));
    // Per-field validators (only for email + phone; other fields are free-text).
    const fields=[
      ["firstName","First Name",1,"text"],
      ["lastName","Last Name",1,"text"],
      ["email","Email",2,"text",validateEmail,"e.g. jane@example.com"],
      ["phone","Phone",1,"text",validatePhone,"e.g. 555-123-4567"],
      ["company","Company",1,"text"],
      ["location","Location",1,"text"],
      ["territory","Territory of Interest",2,"text"],
      ["netWorth","Net Worth",1,"select",null,null,netWorthOptions],
      ["liquidity","Liquidity",1,"select",null,null,liquidityOptions],
      ["source","Lead Source",1,"select",null,null,sourceOptions],
      ["assignedTo","Assigned To",1,"select",null,null,seatOptions],
      // Broker is a built-in field on both leads and opps. Values are broker IDs;
      // the select renders the broker's name. Empty = unassigned.
      ["brokerId","Broker",2,"brokerSelect",null,null,brokerOptions],
    ];
    // Custom (admin-defined) fields for this entity type. Sourced from settings.customFields.
    const customFieldDefs = ((settings.customFields||{})[type==="lead"?"leads":"opps"]||[]);
    return(
      <ModalShell title={form.id?`Edit ${type==="lead"?"Lead":"Opportunity"}`:`New ${type==="lead"?"Lead":"Opportunity"}`} onClose={()=>setModal(null)}>
        <div style={{display:"grid",gridTemplateColumns:"1fr 1fr",gap:9}}>
          {fields.map(([k,lbl,span,kind,validate,placeholder,opts])=>{
            const issue = validate ? validate(form[k]) : null;
            return (
              <div key={k} style={{gridColumn:`span ${span}`}}>
                <label style={{display:"block",fontSize:11,color:C.dim,marginBottom:3,fontWeight:700}}>{lbl}</label>
                <div style={{position:"relative"}}>
                  {kind === "select" ? (
                    <select value={form[k]||""} onChange={e=>set(k,e.target.value)} style={inp({width:"100%",boxSizing:"border-box"})}>
                      <option value="">— Select —</option>
                      {opts.map(o => <option key={o} value={o}>{o}</option>)}
                    </select>
                  ) : kind === "brokerSelect" ? (
                    opts.length === 0 ? (
                      <div style={{...inp({width:"100%",boxSizing:"border-box",fontSize:11,color:C.muted,fontStyle:"italic"}),padding:"9px 13px"}}>No brokers yet — add one from Broker Hub first.</div>
                    ) : (
                      <select value={form[k]||""} onChange={e=>set(k,e.target.value)} style={inp({width:"100%",boxSizing:"border-box"})}>
                        <option value="">— Unassigned —</option>
                        {opts.map(o => <option key={o.id} value={o.id}>{o.label}</option>)}
                      </select>
                    )
                  ) : (
                    <input value={form[k]||""} placeholder={placeholder||""} onChange={e=>set(k,e.target.value)} style={inp({width:"100%",boxSizing:"border-box",paddingRight: issue?32:undefined, borderColor: issue ? "#fb923c66" : undefined})}/>
                  )}
                  {issue && <span title={issue} style={{position:"absolute",right:9,top:"50%",transform:"translateY(-50%)",fontSize:13,color:"#fb923c",pointerEvents:"auto",cursor:"help"}}>⚠️</span>}
                </div>
                {k === "source" && <button onClick={()=>{setModal(null);setNav("settings");}} style={{background:"transparent",border:"none",color:"#60a5fa",fontSize:10,cursor:"pointer",fontFamily:"inherit",padding:0,marginTop:3}}>Manage sources →</button>}
                {issue && <div style={{fontSize:10,color:"#fb923c",marginTop:3,lineHeight:1.4}}>{issue}</div>}
              </div>
            );
          })}
          {/* Admin-defined custom fields (any extras the user added in Settings → Custom Fields) */}
          {customFieldDefs.map(cf => (
            <div key={cf.id} style={{gridColumn:`span ${cf.span||1}`}}>
              <label style={{display:"block",fontSize:11,color:C.dim,marginBottom:3,fontWeight:700}}>{cf.label}{cf.required && <span style={{color:"#f87171",marginLeft:3}}>*</span>}</label>
              {cf.type === "textarea" ? (
                <textarea value={(form.customFields||{})[cf.key]||""} onChange={e=>setCustom(cf.key, e.target.value)} placeholder={cf.placeholder||""} rows={2} style={inp({width:"100%",boxSizing:"border-box",resize:"vertical",fontFamily:"inherit"})}/>
              ) : cf.type === "select" ? (
                <select value={(form.customFields||{})[cf.key]||""} onChange={e=>setCustom(cf.key, e.target.value)} style={inp({width:"100%",boxSizing:"border-box"})}>
                  <option value="">— Select —</option>
                  {(cf.options||[]).map(o => <option key={o} value={o}>{o}</option>)}
                </select>
              ) : cf.type === "checkbox" ? (
                <label style={{display:"flex",alignItems:"center",gap:8,fontSize:12,color:C.text,paddingTop:8}}>
                  <input type="checkbox" checked={!!(form.customFields||{})[cf.key]} onChange={e=>setCustom(cf.key, e.target.checked)}/>
                  <span>{cf.placeholder || "Yes"}</span>
                </label>
              ) : (
                <input type={cf.type==="number"?"number":cf.type==="date"?"date":"text"} value={(form.customFields||{})[cf.key]||""} onChange={e=>setCustom(cf.key, cf.type==="number"?Number(e.target.value):e.target.value)} placeholder={cf.placeholder||""} style={inp({width:"100%",boxSizing:"border-box"})}/>
              )}
            </div>
          ))}
          <div style={{gridColumn:"span 2"}}>
            <label style={{display:"block",fontSize:11,color:C.dim,marginBottom:3,fontWeight:700}}>Stage</label>
            <select value={form.stage} onChange={e=>set("stage",e.target.value)} style={inp({width:"100%",boxSizing:"border-box"})}>
              {stages.map(s=><option key={s.id} value={s.id}>{s.label}</option>)}
            </select>
          </div>
        </div>
        <div style={{display:"flex",gap:10,marginTop:18,justifyContent:"flex-end"}}>
          <button onClick={()=>setModal(null)} style={btn(C.dim,C.muted)}>Cancel</button>
          <button onClick={()=>saveRecord(type,form)} style={btn("#091c09","#4ade80",true)}>{form.id?"Save":"Create"}</button>
        </div>
      </ModalShell>
    );
  };

  const BrandModal=({brand:initBrand})=>{
    const [form,setForm]=useState(initBrand||{name:"",industry:"",website:"",emoji:"🏢",color:"#1e3a5f"});
    const set=(k,v)=>setForm(f=>({...f,[k]:v}));
    const fddFileRef=useRef();
    return(
      <ModalShell title={form.id?"Brand Settings":"New Brand"} onClose={()=>setModal(null)} maxW={500}>
        {[["name","Brand Name"],["industry","Industry"],["website","Website URL"]].map(([k,lbl])=>(
          <div key={k} style={{marginBottom:10}}>
            <label style={{display:"block",fontSize:11,color:C.dim,marginBottom:3,fontWeight:700}}>{lbl}</label>
            <input value={form[k]||""} onChange={e=>set(k,e.target.value)} style={inp({width:"100%",boxSizing:"border-box"})}/>
          </div>
        ))}
        <div style={{marginBottom:14}}>
          <div style={{display:"flex",alignItems:"center",gap:11,marginBottom:8}}>
            <div style={{width:54,height:54,borderRadius:12,background:form.color||"#1e3a5f",display:"flex",alignItems:"center",justifyContent:"center",fontSize:30,flexShrink:0,boxShadow:`0 0 18px ${(form.color||"#1e3a5f")}55`}}>{form.emoji||"🏢"}</div>
            <div style={{flex:1,minWidth:0}}>
              <label style={{display:"block",fontSize:11,color:C.dim,marginBottom:3,fontWeight:700}}>Brand Color</label>
              <div style={{display:"flex",alignItems:"center",gap:8}}>
                <input type="color" value={form.color||"#1e3a5f"} onChange={e=>set("color",e.target.value)} style={{width:48,height:32,border:"none",borderRadius:7,cursor:"pointer",padding:0,background:"transparent"}}/>
                <span style={{fontSize:11,color:C.muted,fontFamily:"ui-monospace,monospace"}}>{form.color||"#1e3a5f"}</span>
              </div>
            </div>
          </div>
          <label style={{display:"block",fontSize:11,color:C.dim,marginBottom:5,fontWeight:700}}>Emoji</label>
          <EmojiPatternPicker value={form.emoji||"🏢"} onChange={v=>set("emoji",v)}/>
        </div>
        {/* FDD section if editing existing */}
        {form.id&&(
          <div style={{borderTop:`1px solid ${C.border}`,paddingTop:14,marginTop:4}}>
            <Sec>FDD Data</Sec>
            {form.fddData?(
              <div>
                <div style={{display:"grid",gridTemplateColumns:"1fr 1fr",gap:7,marginBottom:10}}>
                  {Object.entries(form.fddData).filter(([k,v])=>v&&v!=="null"&&k!=="item19Notes").map(([k,v])=>(
                    <div key={k} style={{background:"#090f1c",borderRadius:7,padding:"6px 10px"}}>
                      <div style={{fontSize:10,color:C.dim,fontWeight:700}}>{k}</div>
                      <input value={v||""} onChange={e=>setForm(f=>({...f,fddData:{...f.fddData,[k]:e.target.value}}))} style={{...inp({width:"100%",boxSizing:"border-box",padding:"4px 7px",fontSize:12,marginTop:3})}}/>
                    </div>
                  ))}
                </div>
                {form.fddData.item19Notes&&<div style={{marginBottom:10}}><label style={{display:"block",fontSize:11,color:C.dim,marginBottom:3,fontWeight:700}}>Item 19 Notes</label><textarea value={form.fddData.item19Notes||""} onChange={e=>setForm(f=>({...f,fddData:{...f.fddData,item19Notes:e.target.value}}))} rows={2} style={{...inp({width:"100%",boxSizing:"border-box"}),resize:"vertical",fontFamily:"inherit"}}/></div>}
              </div>
            ):(
              <div style={{textAlign:"center",padding:"12px 0"}}>
                <input type="file" accept=".pdf" ref={fddFileRef} style={{display:"none"}} onChange={e=>{if(e.target.files[0]){parseFDD(e.target.files[0],form.id);setModal(null);}}}/>
                <button onClick={()=>fddFileRef.current?.click()} style={btn("#091c09","#4ade80")}>📄 Upload FDD PDF</button>
                <div style={{fontSize:11,color:C.dim,marginTop:6}}>Auto-extracts Items 5, 6, 7, and 19</div>
              </div>
            )}
            {form.id&&<div style={{display:"flex",gap:8,justifyContent:"flex-end",marginTop:8}}>
              <input type="file" accept=".pdf" ref={fddFileRef} style={{display:"none"}} onChange={e=>{if(e.target.files[0]){parseFDD(e.target.files[0],form.id);setModal(null);}}}/>
              {form.fddData&&<button onClick={()=>fddFileRef.current?.click()} style={btn(C.dim,C.muted)}>↻ Re-upload FDD</button>}
              {brands.length>1&&<button onClick={()=>deleteBrand(form.id)} style={btn("#1a0808","#f87171")}>Delete Brand</button>}
            </div>}
          </div>
        )}
        <div style={{display:"flex",gap:10,justifyContent:"flex-end",marginTop:18}}>
          <button onClick={()=>setModal(null)} style={btn(C.dim,C.muted)}>Cancel</button>
          <button onClick={()=>saveBrand(form)} style={btn("#091c09","#4ade80",true)}>{form.id?"Save Changes":"Create Brand"}</button>
        </div>
      </ModalShell>
    );
  };

  const PartnerForm=({type,id})=>{
    const [form,setForm]=useState({firstName:"",lastName:"",email:"",phone:"",role:""});
    const set=(k,v)=>setForm(f=>({...f,[k]:v}));
    return(
      <ModalShell title="Add Partner" onClose={()=>setModal(null)} maxW={420}>
        {[["firstName","First Name"],["lastName","Last Name"],["email","Email"],["phone","Phone"],["role","Role"]].map(([k,lbl])=>(
          <div key={k} style={{marginBottom:9}}><label style={{display:"block",fontSize:11,color:C.dim,marginBottom:3,fontWeight:700}}>{lbl}</label><input value={form[k]||""} onChange={e=>set(k,e.target.value)} style={inp({width:"100%",boxSizing:"border-box"})}/></div>
        ))}
        <div style={{display:"flex",gap:10,justifyContent:"flex-end",marginTop:14}}>
          <button onClick={()=>setModal(null)} style={btn(C.dim,C.muted)}>Cancel</button>
          <button onClick={()=>addPartner(type,id,form)} style={btn("#091c09","#4ade80",true)}>Add Partner</button>
        </div>
      </ModalShell>
    );
  };

  const StageEditorModal=({type})=>{
    const init=type==="lead"?bLStages:bOStages;
    const [stages,setStages]=useState([...init]);
    const [nl,setNl]=useState(""); const [nc,setNc]=useState("#60a5fa"); const [nd,setNd]=useState("");
    const move=(i,d)=>{const s=[...stages],j=i+d;if(j<0||j>=s.length)return;[s[i],s[j]]=[s[j],s[i]];setStages(s);};
    const upd=(i,k,v)=>setStages(s=>s.map((st,x)=>x===i?{...st,[k]:v}:st));
    return(
      <ModalShell title={`${type==="lead"?"Lead":"Opportunity"} Stages`} onClose={()=>setModal(null)} maxW={560}>
        <div style={{maxHeight:320,overflowY:"auto",display:"flex",flexDirection:"column",gap:6,marginBottom:12}}>
          {stages.map((s,i)=>{
            const isProtected = PROTECTED_STAGE_IDS.has(s.id);
            return (
              <div key={s.id} style={{display:"flex",alignItems:"center",gap:7,background:"#090f1c",borderRadius:8,padding:"7px 11px"}}>
                <div style={{width:6,height:22,borderRadius:3,background:s.color}}/>
                <input value={s.label} onChange={e=>upd(i,"label",e.target.value)} style={{...inp({}),flex:1,padding:"5px 9px",fontSize:12}}/>
                <input type="color" value={s.color} onChange={e=>upd(i,"color",e.target.value)} style={{width:28,height:28,border:"none",borderRadius:5,cursor:"pointer"}}/>
                {type==="opp"&&!isProtected&&<input value={s.staleDays||""} onChange={e=>upd(i,"staleDays",e.target.value?parseInt(e.target.value):undefined)} placeholder="days" style={{...inp({}),width:52,padding:"5px 7px",fontSize:11}}/>}
                <button onClick={()=>move(i,-1)} title="Move stage up" style={btn("#090f1c",C.muted)} disabled={i===0||isProtected}>↑</button>
                <button onClick={()=>move(i,1)} title="Move stage down" style={btn("#090f1c",C.muted)} disabled={i===stages.length-1||isProtected}>↓</button>
                {isProtected
                  ? <span title="Permanent stage — rename allowed, deletion blocked" style={{fontSize:10,color:C.dim,padding:"4px 8px",border:`1px solid ${C.border}`,borderRadius:5,fontWeight:700,letterSpacing:".04em"}}>LOCKED</span>
                  : <button onClick={()=>setStages(s=>s.filter((_,x)=>x!==i))} title="Delete stage" style={btn("#1a0808","#f87171")}>×</button>}
              </div>
            );
          })}
        </div>
        <div style={{display:"flex",gap:7,marginBottom:12}}>
          <input value={nl} onChange={e=>setNl(e.target.value)} placeholder="New stage…" style={{...inp({}),flex:1}}/>
          <input type="color" value={nc} onChange={e=>setNc(e.target.value)} style={{width:32,height:32,border:"none",borderRadius:6,cursor:"pointer"}}/>
          {type==="opp"&&<input value={nd} onChange={e=>setNd(e.target.value)} placeholder="days" style={{...inp({}),width:52,fontSize:11}}/>}
          <button onClick={()=>{if(!nl.trim())return;setStages(s=>{const next=[...s,{id:uid(),label:nl,color:nc,staleDays:nd?parseInt(nd):undefined}];const idxLost=next.findIndex(x=>x.id==="closed_lost");if(idxLost>0&&idxLost<next.length-1){next.push(next.splice(idxLost,1)[0]);}return next;});setNl("");setNd("");}} style={btn("#091c09","#4ade80")}>+ Add</button>
        </div>
        <div style={{display:"flex",gap:10,justifyContent:"flex-end"}}>
          <button onClick={()=>setModal(null)} style={btn(C.dim,C.muted)}>Cancel</button>
          <button onClick={()=>saveStages(type,stages)} style={btn("#091c09","#4ade80",true)}>Save Stages</button>
        </div>
      </ModalShell>
    );
  };

  const TemplateModal=({rec,templateHint})=>{
    const templates = bTemplates;
    const [sel,setSel]=useState(templates[0]||null);
    const allEmails=rec?[rec.email,...(rec.partners||[]).map(p=>p.email)].filter(Boolean):[];
    const ctx = templateContext(rec, brand, settings);
    const sig = settings.emailSignature ? `\n\n${settings.emailSignature}` : "";
    // Render body for both HTML and plain modes; strip HTML to plain text for the mailto: link.
    const stripHtml = s => (s||"").replace(/<[^>]+>/g," ").replace(/\s+/g," ").trim();
    const renderBody = (t) => {
      if (!t) return "";
      if (t.blocks?.length) return fillMergeTags(blocksToHtml(t.blocks, t.color||"#3b82f6"), ctx);
      return fillMergeTags(t.body || "", ctx);
    };
    return(
      <ModalShell title="Email Templates" onClose={()=>setModal(null)} maxW={560}>
        {templates.length === 0 ? (
          <div style={{textAlign:"center",color:C.muted,padding:"22px 14px",fontSize:13,lineHeight:1.55}}>
            No templates yet. Head to <strong style={{color:C.text}}>Templates</strong> to build one or copy from the Gallery.
          </div>
        ) : <>
          <div style={{display:"flex",gap:7,flexWrap:"wrap",marginBottom:13,maxHeight:120,overflowY:"auto"}}>
            {templates.map(t=><button key={t.id} onClick={()=>setSel(t)} style={{...btn(sel?.id===t.id?"#162035":C.bg,sel?.id===t.id?C.accent:C.muted),fontSize:11}}>{t.name}</button>)}
          </div>
          {sel&&<>
            <div style={{background:"#090f1c",borderRadius:8,padding:"7px 12px",marginBottom:8,fontSize:12,color:C.text}}><strong style={{color:C.dim}}>Subject:</strong> {fillMergeTags(sel.subject, ctx)}</div>
            {(sel.mode === "rich" || sel.blocks?.length) ? (
              <div style={{background:"#f8fafc",borderRadius:9,padding:14,fontSize:12,color:"#1e293b",maxHeight:300,overflowY:"auto",margin:"0 0 12px"}} dangerouslySetInnerHTML={{__html: renderBody(sel) + (sig?`<p style="white-space:pre-wrap">${sig}</p>`:"")}}/>
            ) : (
              <pre style={{background:"#090f1c",borderRadius:9,padding:"13px",fontSize:12,color:"#94a3b8",whiteSpace:"pre-wrap",fontFamily:"Georgia,serif",lineHeight:1.8,margin:"0 0 12px",maxHeight:300,overflowY:"auto"}}>{renderBody(sel) + sig}</pre>
            )}
            <div style={{fontSize:11,color:C.dim,marginBottom:11}}>To: {allEmails.join(", ")||"(no email)"}</div>
            <div style={{display:"flex",gap:9,justifyContent:"flex-end"}}>
              <button onClick={()=>{ const txt = (sel.mode==="rich"||sel.blocks?.length) ? stripHtml(renderBody(sel)) : renderBody(sel); navigator.clipboard?.writeText(txt + sig); toast$("Copied!"); }} style={btn("#090f1c","#60a5fa")}>📋 Copy</button>
              {allEmails.length>0&&<a href={`mailto:${allEmails.join(",")}?subject=${encodeURIComponent(fillMergeTags(sel.subject, ctx))}&body=${encodeURIComponent(((sel.mode==="rich"||sel.blocks?.length) ? stripHtml(renderBody(sel)) : renderBody(sel)) + sig)}`} style={{...btn("#091c09","#4ade80",true),textDecoration:"none"}}>✉️ Open in Mail</a>}
            </div>
          </>}
        </>}
      </ModalShell>
    );
  };

  const AiSummaryModal=()=>(
    <ModalShell title="✨ AI Summary" onClose={()=>setModal(null)} maxW={560}>
      {aiLoading?<div style={{textAlign:"center",padding:40,color:"#38bdf8"}}><div style={{fontSize:28,animation:"spin 1s linear infinite"}}>⟳</div><div style={{marginTop:8}}>Analyzing…</div></div>
        :<pre style={{whiteSpace:"pre-wrap",fontFamily:"Georgia,serif",fontSize:13,color:"#c8d8ef",lineHeight:1.8,background:"#090f1c",borderRadius:10,padding:16,maxHeight:460,overflowY:"auto"}}>{aiSummary}</pre>}
      <style>{`@keyframes spin{from{transform:rotate(0)}to{transform:rotate(360deg)}}`}</style>
    </ModalShell>
  );

  const AiOrganizeModal = () => {
    const goToOpp = (oppId) => { setSelected({type:"opp",id:oppId}); setSubView("detail"); setModal(null); };
    const lookupName = (oppId) => { const o = bOpps.find(x=>x.id===oppId); return o ? `${o.firstName} ${o.lastName}` : "(unknown)"; };
    const lookupStageLabel = (sid) => bOStages.find(s=>s.id===sid)?.label || sid;
    const Section = ({ icon, title, color, items, renderItem, empty }) => {
      if (!items || items.length===0) return (
        <div style={{background:"#090f1c",borderRadius:10,padding:"10px 13px",marginBottom:9,border:`1px solid ${C.border}`}}>
          <div style={{display:"flex",alignItems:"center",gap:8}}>
            <span style={{fontSize:16}}>{icon}</span>
            <strong style={{fontSize:12,color:C.muted,letterSpacing:".05em"}}>{title}</strong>
            <span style={{marginLeft:"auto",fontSize:10,color:"#4ade80"}}>✓ {empty}</span>
          </div>
        </div>
      );
      return (
        <div style={{background:C.panel,border:`1px solid ${color}33`,borderRadius:10,padding:"11px 14px",marginBottom:10}}>
          <div style={{display:"flex",alignItems:"center",gap:8,marginBottom:8}}>
            <span style={{fontSize:16}}>{icon}</span>
            <strong style={{fontSize:12,color,letterSpacing:".05em"}}>{title}</strong>
            <span style={{marginLeft:"auto",fontSize:10,color:C.dim,fontWeight:800}}>{items.length}</span>
          </div>
          <div style={{display:"flex",flexDirection:"column",gap:6}}>
            {items.map((it,i) => <div key={i}>{renderItem(it)}</div>)}
          </div>
        </div>
      );
    };
    return (
      <ModalShell title="✨ AI Organize · Pipeline Audit" onClose={()=>setModal(null)} maxW={700}>
        {aiOrganizeLoading && (
          <div style={{textAlign:"center",padding:50,color:"#38bdf8"}}>
            <div style={{fontSize:32,animation:"spin 1s linear infinite"}}>⟳</div>
            <div style={{marginTop:10,fontSize:13}}>Analyzing your pipeline…</div>
            <div style={{marginTop:4,fontSize:11,color:C.dim}}>Looking at {bOpps.length} opportunit{bOpps.length===1?"y":"ies"}</div>
          </div>
        )}
        {aiOrganize?.error && <div style={{background:"#1a0808",border:"1px solid #f8717133",borderRadius:8,padding:"14px 18px",color:"#fca5a5",fontSize:13}}>{aiOrganize.error}</div>}
        {aiOrganize && !aiOrganize.error && (
          <div>
            <Section icon="🛑" title="CONSIDER DISQUALIFYING" color="#f87171" items={aiOrganize.disqualify} empty="No obvious dead-ends." renderItem={(it)=>(
              <div style={{background:"#090f1c",borderRadius:8,padding:"8px 12px",display:"flex",alignItems:"center",gap:10}}>
                <div style={{flex:1,minWidth:0}}>
                  <div style={{fontSize:12,fontWeight:800,color:C.text}}><RecordLink onClick={()=>goToOpp(it.id)} color={C.text} weight={800}>{lookupName(it.id)}</RecordLink></div>
                  <div style={{fontSize:11,color:C.muted,lineHeight:1.4}}>{it.reason}</div>
                </div>
                <button onClick={()=>goToOpp(it.id)} style={btn(C.dim,"#60a5fa")}>View →</button>
              </div>
            )}/>
            <Section icon="↔️" title="LIKELY IN THE WRONG STAGE" color="#facc15" items={aiOrganize.wrong_stage} empty="Stages look right." renderItem={(it)=>(
              <div style={{background:"#090f1c",borderRadius:8,padding:"8px 12px",display:"flex",alignItems:"center",gap:10}}>
                <div style={{flex:1,minWidth:0}}>
                  <div style={{fontSize:12,fontWeight:800,color:C.text}}><RecordLink onClick={()=>goToOpp(it.id)} color={C.text} weight={800}>{lookupName(it.id)}</RecordLink></div>
                  <div style={{fontSize:11,color:C.muted,lineHeight:1.4}}>→ <strong style={{color:"#facc15"}}>{lookupStageLabel(it.suggestedStage)}</strong> · {it.reason}</div>
                </div>
                <button onClick={()=>goToOpp(it.id)} style={btn(C.dim,"#60a5fa")}>View →</button>
              </div>
            )}/>
            <Section icon="📦" title="MISSING / UN-DELIVERED MATERIALS" color="#fb923c" items={aiOrganize.missing_materials} empty="Nothing outstanding." renderItem={(it)=>(
              <div style={{background:"#090f1c",borderRadius:8,padding:"8px 12px",display:"flex",alignItems:"center",gap:10}}>
                <div style={{flex:1,minWidth:0}}>
                  <div style={{fontSize:12,fontWeight:800,color:C.text}}><RecordLink onClick={()=>goToOpp(it.id)} color={C.text} weight={800}>{lookupName(it.id)}</RecordLink> · <span style={{color:"#fb923c"}}>{it.item}</span></div>
                  <div style={{fontSize:11,color:C.muted,lineHeight:1.4}}>{it.reason}</div>
                </div>
                <button onClick={()=>goToOpp(it.id)} style={btn(C.dim,"#60a5fa")}>View →</button>
              </div>
            )}/>
            <Section icon="📈" title="HABITS & TRENDS TO IMPROVE" color="#a78bfa" items={aiOrganize.habits} empty="No bad-habit patterns detected." renderItem={(it)=>(
              <div style={{background:"#090f1c",borderRadius:8,padding:"9px 12px"}}>
                <div style={{fontSize:12,fontWeight:800,color:C.text,marginBottom:3}}>{it.title}</div>
                <div style={{fontSize:11,color:C.muted,lineHeight:1.5}}>{it.detail}</div>
              </div>
            )}/>
            <div style={{display:"flex",justifyContent:"flex-end",gap:8,marginTop:12}}>
              <button onClick={()=>{setAiOrganize(null);runAiOrganize();}} style={btn(C.dim,C.muted)}>↻ Re-analyze</button>
              <button onClick={()=>setModal(null)} style={btn("#091c09","#4ade80")}>Done</button>
            </div>
          </div>
        )}
        <style>{`@keyframes spin{from{transform:rotate(0)}to{transform:rotate(360deg)}}`}</style>
      </ModalShell>
    );
  };

  // ════════════════════════════════════════════════════════
  //  NAV CONFIG (no Brands tab)
  // ════════════════════════════════════════════════════════
  const NAV_ITEMS=[
    {id:"whats_next",icon:"🎯",label:"What's Next?"},
    {id:"leads",     icon:"◎", label:"Leads"},
    {id:"opps",      icon:"◉", label:"Opportunities"},
    {id:"territories",icon:"🗺️",label:"Territories"},
    {id:"analytics", icon:"📊", label:"Analytics"},
    {id:"templates", icon:"📧",label:"Templates"},
    {id:"automations",icon:"⚡",label:"Automations"},
    {id:"brokers",   icon:"🤝",label:"Broker Hub"},
    {id:"scheduling",icon:"📅",label:"Scheduling"},
    {id:"dday",      icon:"🎓",label:"Discovery Day"},
    {id:"franchisees",icon:"🏪",label:"Franchisee Hub"},
    {id:"notifications",icon:"🔔",label:"Notifications"},
    {id:"settings",  icon:"⚙", label:"Settings"},
  ];

  const isRecordNav=nav==="leads"||nav==="opps";
  const recType=nav==="leads"?"lead":"opp";
  const SIDEBAR_W=sidebarOpen?206:52;

  // ── API key banner ────────────────────────────────────────
  const [apiKey,setApiKey]=useState(localStorage.getItem("ff_api_key")||"");
  const [showKeyInput,setShowKeyInput]=useState(false);
  const saveKey=(k)=>{localStorage.setItem("ff_api_key",k);setApiKey(k);setShowKeyInput(false);};
  const ApiKeyBanner=()=>(
    <div style={{position:"fixed",top:0,left:0,right:0,zIndex:600,background:"#0c1a08",borderBottom:"1px solid #4ade8044",padding:"8px 20px",display:"flex",alignItems:"center",gap:12}}>
      <span style={{fontSize:12,color:"#86efac",flex:1}}>🧪 <strong>Mock AI mode</strong> — using hand-tuned sample responses. Add your Anthropic API key for real AI tailored to your data.</span>
      {showKeyInput?(
        <div style={{display:"flex",gap:8,alignItems:"center"}}>
          <input autoFocus defaultValue={apiKey} onKeyDown={e=>{if(e.key==="Enter")saveKey(e.target.value);if(e.key==="Escape")setShowKeyInput(false);}} placeholder="sk-ant-..." style={{background:"#091c09",border:"1px solid #4ade8066",borderRadius:7,padding:"5px 11px",color:"#f0f6ff",fontSize:12,width:300,outline:"none"}}/>
          <button onClick={e=>saveKey(e.target.previousSibling.value)} style={{background:"#4ade80",border:"none",borderRadius:7,padding:"5px 12px",color:"#052e16",fontSize:12,fontWeight:800,cursor:"pointer"}}>Save</button>
          <button onClick={()=>setShowKeyInput(false)} title="Close" style={{background:"transparent",border:"none",color:"#4a5870",fontSize:14,cursor:"pointer"}}>✕</button>
        </div>
      ):(
        <button onClick={()=>setShowKeyInput(true)} style={{background:"transparent",border:"1px solid #4ade8055",borderRadius:7,padding:"4px 12px",color:"#4ade80",fontSize:11,fontWeight:700,cursor:"pointer"}}>+ Add Key</button>
      )}
    </div>
  );

  if(loading) return <div style={{background:C.bg,minHeight:"100vh",display:"flex",alignItems:"center",justifyContent:"center",color:C.muted,fontFamily:"system-ui"}}>Loading…</div>;

  if(brands.length===0){
    return(
      <div style={{background:C.bg,minHeight:"100vh",fontFamily:"'Sora',system-ui,sans-serif",display:"flex",alignItems:"center",justifyContent:"center",flexDirection:"column",gap:20}}>
        {!apiKey&&<ApiKeyBanner/>}
        <link href="https://fonts.googleapis.com/css2?family=Sora:wght@400;600;700;800;900&display=swap" rel="stylesheet"/>
        <div style={{fontSize:48,marginTop:apiKey?0:52}}>🏢</div>
        <div style={{fontSize:22,fontWeight:900,color:"#f0f6ff"}}>Welcome to {BRAND.name}</div>
        <div style={{color:C.muted,fontSize:14}}>Add your first brand to get started</div>
        <button onClick={()=>setModal("editBrand")} style={{...btn("#091c09","#4ade80",true),padding:"14px 32px",fontSize:14}}>+ Add Your First Brand</button>
        {modal==="editBrand"&&<BrandModal brand={null}/>}
      </div>
    );
  }

  // The single top-of-screen banner = most-recent un-dismissed banner-flagged notification.
  // Replacing on new latest is automatic since the array is newest-first.
  const bannerNotif = notifications.find(n => n.isBanner && !n.bannerDismissedAt) || null;
  const apiKeyBannerH = !apiKey ? 42 : 0;
  const notifBannerH = bannerNotif ? 44 : 0;
  return(
    <div style={{background:C.bg,minHeight:"100vh",fontFamily:"'Sora',system-ui,sans-serif",color:C.text}}>
      <TooltipLayer/>
      {!apiKey&&<ApiKeyBanner/>}
      <NotificationBanner notification={bannerNotif} topOffset={apiKeyBannerH} onDismiss={dismissBannerNotification} onView={openNotificationRecord}/>
      <link href="https://fonts.googleapis.com/css2?family=Sora:wght@400;500;600;700;800;900&display=swap" rel="stylesheet"/>
      <div style={{display:"flex",minHeight:"100vh",paddingTop: apiKeyBannerH + notifBannerH}}>

        {/* Sidebar */}
        <div style={{width:SIDEBAR_W,background:C.panel,borderRight:`1px solid ${C.border}`,display:"flex",flexDirection:"column",padding:`18px ${sidebarOpen?11:6}px`,flexShrink:0,transition:"width .2s",overflow:"hidden"}}>
          {/* Logo + collapse toggle. When the sidebar is collapsed the rail is only ~40px
              wide internally, so a horizontal "[logo] [toggle]" row clips the toggle button
              off the right edge (overflow:hidden hides it entirely). Stack the toggle below
              the logo when collapsed so the expand button is fully visible and centered. */}
          <div style={{display:"flex",flexDirection:sidebarOpen?"row":"column",alignItems:"center",justifyContent:sidebarOpen?"space-between":"center",marginBottom:20,gap:sidebarOpen?9:10}}>
            {sidebarOpen ? (
              <div style={{display:"flex",alignItems:"center",gap:9,paddingLeft:2}}>
                <BrandLogo size={34}/>
                <div>
                  <div style={{fontSize:15,fontWeight:900}}>
                    <span style={{background:"linear-gradient(120deg,#e8d5b7,#a87f4a)",WebkitBackgroundClip:"text",WebkitTextFillColor:"transparent"}}>Fran</span>
                    <span style={{color:"#f0f6ff"}}>Chai</span>
                  </div>
                  <div style={{fontSize:9,color:C.dim,letterSpacing:".06em"}}>FRANCHISE CRM</div>
                </div>
              </div>
            ) : (
              <BrandLogo size={28}/>
            )}
            <button onClick={()=>setSidebarOpen(o=>!o)} style={{...btn(C.dim,C.muted),padding:sidebarOpen?"5px 9px":"4px 8px",fontSize:13,flexShrink:0}} title={sidebarOpen?"Collapse sidebar":"Expand sidebar"}>
              {sidebarOpen?"◀":"▶"}
            </button>
          </div>

          {/* Brand tile — primary action is switching between brands. Add-brand and
              brand-editing both live behind Settings → Brands (gated by permission level
              in SaaS multi-seat mode) so a rep can't accidentally create new orgs from
              the sidebar. The popover only switches the active brand. */}
          {/* Tile glow + chevron use the brand's own color so the rep gets an at-a-glance
              signal of which brand they're in. The brand NAME is rendered in brand color only
              when the color is bright enough to read against the dark panel — otherwise it
              falls back to white. Threshold uses sRGB relative luminance so dark navies, browns,
              blacks etc. don't become unreadable. Falls back to the app accent for unset colors. */}
          <div style={{position:"relative",marginBottom: sidebarOpen?14:10}}>
            {(() => {
              const brandColor = brand?.color || C.accent;
              // Parse #rrggbb (no shorthand). Returns 0..1 luminance.
              const lum = (() => {
                const m = /^#?([0-9a-f]{6})$/i.exec(brandColor || "");
                if (!m) return 1;
                const r = parseInt(m[1].slice(0,2), 16)/255;
                const g = parseInt(m[1].slice(2,4), 16)/255;
                const b = parseInt(m[1].slice(4,6), 16)/255;
                const adj = (c) => c <= 0.03928 ? c/12.92 : Math.pow((c+0.055)/1.055, 2.4);
                return 0.2126*adj(r) + 0.7152*adj(g) + 0.0722*adj(b);
              })();
              // Below ~0.22 luminance, brand color is too dark to read against the #0f1a2e tile —
              // swap the name text to a soft white. The colored border / glow still convey identity.
              const nameColor = lum < 0.22 ? "#f0f6ff" : brandColor;
              if (sidebarOpen) {
                return (
                  <button onClick={()=>setBrandPickerOpen(o=>!o)} title="Switch brand" style={{background:"#0f1a2e",border:`1.5px solid ${brandColor}55`,borderRadius:9,padding:"8px 10px",cursor:"pointer",fontFamily:"inherit",width:"100%",textAlign:"left",display:"flex",alignItems:"center",gap:8,boxShadow:`0 0 16px ${brandColor}33`,transition:"box-shadow .15s, border-color .15s"}}>
                    <span style={{fontSize:18,flexShrink:0}}>{brand?.emoji||"🏢"}</span>
                    <div style={{flex:1,minWidth:0}}>
                      <div style={{fontSize:12,fontWeight:800,color:nameColor,overflow:"hidden",textOverflow:"ellipsis",whiteSpace:"nowrap"}}>{brand?.name||"Brand"}</div>
                      <div style={{fontSize:9,color:C.muted}}>{brands.length>1?`${brands.length} brands · click to switch`:"Click to switch"}</div>
                    </div>
                    <span style={{fontSize:10,color:nameColor,flexShrink:0,transform:brandPickerOpen?"rotate(180deg)":"none",transition:"transform .15s",opacity:0.8}}>▾</span>
                  </button>
                );
              }
              return (
                <button onClick={()=>setBrandPickerOpen(o=>!o)} style={{background:"#0f1a2e",border:`1.5px solid ${brandColor}55`,borderRadius:9,padding:"8px",width:"100%",fontSize:18,textAlign:"center",cursor:"pointer",fontFamily:"inherit",boxShadow:`0 0 14px ${brandColor}33`,transition:"box-shadow .15s, border-color .15s"}} title={brand?.name||"Switch brand"}>
                  {brand?.emoji||"🏢"}
                </button>
              );
            })()}
            {/* Brand picker popover. Click-away closes via the overlay. Switch-only — no
                add/edit affordances here, those live in Settings → Brands. */}
            {brandPickerOpen && (
              <>
                <div onClick={()=>setBrandPickerOpen(false)} style={{position:"fixed",inset:0,zIndex:60}}/>
                <div style={{position:"absolute",top:"100%",left:0,right:0,marginTop:6,background:C.panel,border:`1px solid ${C.border}`,borderRadius:10,padding:6,zIndex:61,boxShadow:"0 8px 24px #000a",minWidth:sidebarOpen?undefined:200,...(sidebarOpen?{}:{left:"100%",right:"auto",marginLeft:6,marginTop:0,top:0})}}>
                  <div style={{fontSize:9,fontWeight:800,color:C.dim,letterSpacing:".06em",padding:"6px 9px 4px"}}>SWITCH BRAND</div>
                  <div style={{maxHeight:380,overflowY:"auto"}}>
                    {brands.map(b => {
                      const sel = b.id === activeBrand;
                      return (
                        <button key={b.id} onClick={()=>{switchBrand(b.id);setBrandPickerOpen(false);}} style={{width:"100%",display:"flex",alignItems:"center",gap:9,padding:"8px 10px",background:sel?"#162035":"transparent",border:"none",borderRadius:7,cursor:"pointer",textAlign:"left",fontFamily:"inherit",marginBottom:2}}>
                          <span style={{fontSize:18}}>{b.emoji||"🏢"}</span>
                          <div style={{flex:1,minWidth:0}}>
                            <div style={{fontSize:12,fontWeight:sel?800:600,color:sel?"#60a5fa":C.text,overflow:"hidden",textOverflow:"ellipsis",whiteSpace:"nowrap"}}>{b.name}</div>
                            {b.industry && <div style={{fontSize:9,color:C.muted,overflow:"hidden",textOverflow:"ellipsis",whiteSpace:"nowrap"}}>{b.industry}</div>}
                          </div>
                          {sel && <span style={{fontSize:11,color:"#4ade80",flexShrink:0}}>✓</span>}
                        </button>
                      );
                    })}
                  </div>
                  <div style={{borderTop:`1px solid ${C.border}`,marginTop:4,paddingTop:6,paddingLeft:9,paddingRight:9,paddingBottom:4,fontSize:10,color:C.dim,lineHeight:1.4}}>Manage brands & FDD in <span style={{cursor:"pointer",color:"#60a5fa",fontWeight:700}} onClick={()=>{setNav("settings");setBrandPickerOpen(false);}}>Settings → Brands</span></div>
                </div>
              </>
            )}
          </div>

          <nav style={{display:"flex",flexDirection:"column",gap:3}}>
            {NAV_ITEMS.map(n=>{
              const isSel = nav===n.id;
              const handleNavClick = () => {
                setNav(n.id);
                if (n.id!=="whats_next") setWhatsNext(null);
                if (isRecordNav) setSubView("list");
                if (n.id==="templates"){setTemplatesTab("mine");setTemplatesFilter("all");setTemplatesSearch("");setTemplatesEditing(null);setTemplatesChooseType(null);}
                if (n.id==="brokers"){setBrokerDetailId(null);setNetworkDetailId(null);}
                if (n.id==="dday"){setDdayDetailId(null);}
                if (n.id==="franchisees"){setFranchiseeDetailId(null);}
              };
              // The "What's Next?" row gets a right-side "🚀 Jump" button that skips the
              // summary view and navigates straight to the AI-recommended record. Same
              // function as the Quick Action jump button on a record profile + the list-
              // page jump button — gives reps a fast "next, please" workflow.
              const showJumpBtn = n.id === "whats_next" && sidebarOpen;
              return (
                <div key={n.id} style={{display:"flex",alignItems:"stretch",gap:3,position:"relative"}}>
                  <button onClick={handleNavClick}
                    title={!sidebarOpen ? n.label : undefined}
                    style={{flex:1,background:isSel?"#162035":"transparent",border:"none",borderRadius:9,padding:sidebarOpen?"8px 11px":"8px",color:isSel?C.text:C.muted,cursor:"pointer",fontWeight:isSel?700:500,fontSize:12,textAlign:"left",display:"flex",alignItems:"center",gap:sidebarOpen?9:0,justifyContent:sidebarOpen?"flex-start":"center",fontFamily:"inherit"}}>
                    <span style={{color:isSel?C.accent:C.dim,fontSize:n.id==="settings"?20:14,lineHeight:1}}>{n.icon}</span>
                    {sidebarOpen&&<span>{n.label}</span>}
                    {n.id==="opps"&&staleOpps.length>0&&sidebarOpen&&<span style={{marginLeft:"auto",background:"#f8717122",color:"#fca5a5",borderRadius:20,padding:"1px 6px",fontSize:9,fontWeight:800}}>{staleOpps.length}</span>}
                    {n.id==="notifications"&&notifications.filter(x=>!x.readAt).length>0&&sidebarOpen&&<span style={{marginLeft:"auto",background:"#4ade8022",color:"#86efac",borderRadius:20,padding:"1px 6px",fontSize:9,fontWeight:800}}>{notifications.filter(x=>!x.readAt).length}</span>}
                  </button>
                  {showJumpBtn && (
                    <button onClick={(e)=>{ e.stopPropagation(); jumpToNext(); }} disabled={jumpingToNext} title="Jump straight to the next AI-recommended lead/opp (no summary)" style={{background:isSel?"#091420":"#0c1422",border:`1px solid ${C.accent}55`,borderRadius:9,padding:"0 9px",color:jumpingToNext?C.dim:"#60a5fa",cursor:jumpingToNext?"wait":"pointer",fontFamily:"inherit",fontSize:13,flexShrink:0,display:"flex",alignItems:"center"}}>{jumpingToNext?"⏳":"🚀"}</button>
                  )}
                </div>
              );
            })}
          </nav>

          {/* Stage counts */}
          {sidebarOpen&&isRecordNav&&activeBrand&&(
            <div style={{marginTop:16,borderTop:`1px solid ${C.border}`,paddingTop:12}}>
              <div style={{fontSize:9,color:C.dim,fontWeight:800,marginBottom:7,letterSpacing:".07em"}}>STAGES</div>
              {(nav==="leads"?bLStages:bOStages).slice(0,9).map(s=>(
                <div key={s.id} style={{display:"flex",justifyContent:"space-between",padding:"2px 3px"}}>
                  <div style={{fontSize:9,color:C.dim,overflow:"hidden",textOverflow:"ellipsis",whiteSpace:"nowrap",maxWidth:130}}>{s.label}</div>
                  <div style={{fontSize:9,fontWeight:700,color:s.color}}>{(nav==="leads"?bLeads:bOpps).filter(r=>r.stage===s.id).length}</div>
                </div>
              ))}
            </div>
          )}

          {/* FDD quick ref */}
          {sidebarOpen&&brand?.fddData&&(
            <div style={{marginTop:"auto"}}>
              <button onClick={()=>{setModal("editBrand");setModalData({brand:{...brand}});}} style={{...btn("#091c09","#4ade80"),width:"100%",fontSize:11}}>📊 FDD Quick Ref</button>
            </div>
          )}
        </div>

        {/* Main */}
        <div style={{flex:1,display:"flex",flexDirection:"column",overflow:"hidden"}}>
          {/* Topbar */}
          <div style={{background:C.panel,borderBottom:`1px solid ${C.border}`,padding:"12px 20px",display:"flex",alignItems:"center",gap:11,flexShrink:0}}>
            {subView==="detail"&&isRecordNav&&<button onClick={()=>setSubView("list")} style={btn(C.dim,C.muted)}>← Back</button>}
            <h1 style={{margin:0,fontSize:15,fontWeight:900,color:"#f0f6ff"}}>
              {nav==="whats_next"&&"What's Next"}
              {nav==="leads"&&(subView==="detail"&&liveRec?`${liveRec.firstName} ${liveRec.lastName}`:"Leads")}
              {nav==="opps"&&(subView==="detail"&&liveRec?`${liveRec.firstName} ${liveRec.lastName}`:"Opportunities")}
              {nav==="territories"&&"Territory Map"}
              {nav==="analytics"&&"Analytics"}
              {nav==="templates"&&"Templates"}
              {nav==="automations"&&"Automations"}
              {nav==="brokers"&&"Broker Hub"}
              {nav==="scheduling"&&"Scheduling"}
              {nav==="notifications"&&"Notifications"}
              {nav==="settings"&&"Settings"}
            </h1>
            <div style={{marginLeft:"auto",display:"flex",gap:8,alignItems:"center"}}>
              {isRecordNav&&subView!=="detail"&&<>
                <button onClick={()=>setSubView(v=>v==="list"?"kanban":"list")} style={btn("#1a1308","#fb923c",true)}>{subView==="list"?"▦ Kanban":"☰ List"}</button>
                {nav==="opps" && <button onClick={runAiOrganize} disabled={aiOrganizeLoading} style={btn("#0a1a30","#38bdf8",true)}>{aiOrganizeLoading?"⟳ Analyzing…":"✨ AI Organize"}</button>}
                <button onClick={()=>{setModal("editRecord");setModalData({type:recType,rec:null});}} style={btn("#091c09","#4ade80",true)}>+ New {nav==="leads"?"Lead":"Opportunity"}</button>
                <button onClick={()=>{setModal("stageEditor");setModalData({type:recType});}} style={btn(C.dim,C.muted)}><span style={{fontSize:17,lineHeight:1,verticalAlign:"-2px",marginRight:4}}>⚙</span>Stages</button>
              </>}
            </div>
          </div>

          {/* Content */}
          <div style={{flex:1,overflowY:"auto",padding:nav==="territories"?0:20,display:"flex",flexDirection:"column"}}>
            {nav==="whats_next"&&<WhatsNextView/>}
            {nav==="leads"&&subView==="list"&&<ListView type="lead"/>}
            {nav==="leads"&&subView==="kanban"&&<KanbanView type="lead"/>}
            {nav==="leads"&&subView==="detail"&&<DetailView/>}
            {nav==="opps"&&subView==="list"&&<ListView type="opp"/>}
            {nav==="opps"&&subView==="kanban"&&<KanbanView type="opp"/>}
            {nav==="opps"&&subView==="detail"&&<DetailView/>}
            {nav==="territories"&&(
              <div style={{flex:1,padding:16,display:"flex",flexDirection:"column",minHeight:0}}>
                <TerritoryMap
                  territories={bTerr}
                  onSave={saveTerr}
                  onDelete={deleteTerr}
                  assignableRecords={[
                    ...bLeads.map(l => ({...l, recType:"lead"})),
                    ...bOpps.map(o  => ({...o, recType:"opp"})),
                  ]}
                  onNavigate={(a)=>{
                    if (a.type === "lead") { setNav("leads"); setSelected({type:"lead", id:a.id}); setSubView("detail"); }
                    else { setNav("opps"); setSelected({type:"opp", id:a.id}); setSubView("detail"); }
                  }}
                />
              </div>
            )}
            {nav==="analytics"&&<AnalyticsView/>}
            {/* Call TemplatesView as a function (not a component) so React reconciles its
                returned JSX directly against App's tree. Treating it as a component
                makes React see a new component type on every App re-render (since the
                function is redefined inside the render), which unmounts the inner template
                editor and wipes its block state — the bug where collapsing the sidebar reset
                the canvas. As a plain function call, the JSX structure stays stable across
                re-renders and TemplateEditor's state is preserved. */}
            {nav==="templates"&&TemplatesView()}
            {nav==="automations"&&<AutomationsView/>}
            {nav==="brokers"&&<BrokerHubView/>}
            {nav==="scheduling"&&<SchedulingView/>}
            {nav==="dday"&&<DiscoveryDayView/>}
            {nav==="franchisees"&&<FranchiseeHubView/>}
            {nav==="notifications"&&<NotificationsView/>}
            {nav==="settings"&&<SettingsView/>}
          </div>
        </div>
      </div>

      {/* Auto-surfaced report popup (currently: territory-unlock report after deleting an
          off-limits area). Reuses ViewReportModal so the print/CSV/edit affordances and the
          clickable-name behavior all match the Analytics > Reports viewer. The report has
          already been saved into ff4_reports by the action that generated it, so the user
          can also find it from the Reports tab later. */}
      {reportPopup && (
        <ViewReportModal
          report={reportPopup}
          stages={bOStages}
          onClose={()=>setReportPopup(null)}
          onPrint={()=>printReport(reportPopup)}
          onCsv={()=>downloadReportCsv(reportPopup)}
          onOpenRecord={(id)=>{
            const isOpp = bOpps.find(o => o.id === id);
            setNav(isOpp ? "opps" : "leads");
            setSelected({ type: isOpp ? "opp" : "lead", id });
            setSubView("detail");
            setReportPopup(null);
          }}
          onOpenBroker={(id)=>{ openBroker(id); setReportPopup(null); }}
          onOpenNetwork={(id)=>{ openNetwork(id); setReportPopup(null); }}
        />
      )}

      {/* Folder create/edit modal */}
      {folderModal&&(()=>{
        const isEdit = folderModal.mode === "edit";
        const initial = isEdit ? folderModal.folder : { name:"", icon:"📁", color:"#3b82f6", parentId: folderModal.parentId||null, scope:"private" };
        return <FolderModal initial={initial} folders={bFolders} onCancel={()=>setFolderModal(null)} onSave={(f)=>{ saveFolder(isEdit?{...folderModal.folder, ...f}:f); setFolderModal(null); }} onDelete={isEdit ? ()=>{ if(confirm(`Delete folder "${folderModal.folder.name}"? Templates inside will move to the first remaining folder.`)){ deleteFolder(folderModal.folder.id); setFolderModal(null);} } : null}/>;
      })()}

      {/* Template preview modal */}
      {templatePreview&&(
        <div onClick={()=>setTemplatePreview(null)} style={{position:"fixed",inset:0,background:"#000c",zIndex:1001,display:"flex",alignItems:"center",justifyContent:"center",padding:24}}>
          <div onClick={e=>e.stopPropagation()} style={{background:C.panel,border:`1px solid ${C.border}`,borderRadius:14,width:"100%",maxWidth:720,maxHeight:"92vh",display:"flex",flexDirection:"column",overflow:"hidden"}}>
            <div style={{display:"flex",alignItems:"center",justifyContent:"space-between",padding:"14px 18px",borderBottom:`1px solid ${C.border}`}}>
              <div style={{minWidth:0,flex:1}}>
                <div style={{fontSize:11,color:C.dim,letterSpacing:".06em",textTransform:"uppercase"}}>Preview</div>
                <div style={{fontSize:15,fontWeight:800,color:C.text,marginTop:2,overflow:"hidden",textOverflow:"ellipsis",whiteSpace:"nowrap"}}>{templatePreview.name||"Untitled template"}</div>
                {templatePreview.subject && <div style={{fontSize:12,color:C.muted,marginTop:3,overflow:"hidden",textOverflow:"ellipsis",whiteSpace:"nowrap"}}>{templatePreview.subject}</div>}
              </div>
              <div style={{display:"flex",gap:8}}>
                <button onClick={()=>{ setEditing(templatePreview); setTemplatePreview(null); }} title="Open in editor" style={btn(C.dim,C.accent)}>✏️ Edit</button>
                <button onClick={()=>setTemplatePreview(null)} title="Close preview" style={btn(C.dim,C.muted)}>✕</button>
              </div>
            </div>
            <div style={{flex:1,overflow:"auto",background:"#f8fafc",padding:20}}>
              {(templatePreview.mode==="html") ? (
                <pre style={{fontFamily:"ui-monospace,monospace",fontSize:12,whiteSpace:"pre-wrap",color:"#1e293b",margin:0}}>{templatePreview.body||"(empty)"}</pre>
              ) : (
                <div style={{background:"#fff",borderRadius:8,padding:0,boxShadow:"0 2px 14px #0003",overflow:"hidden"}} dangerouslySetInnerHTML={{__html: (templatePreview.blocks?.length ? blocksToHtml(templatePreview.blocks, templatePreview.color) : (templatePreview.body || "<p style='padding:20px;color:#94a3b8;font-style:italic;text-align:center'>(empty template)</p>")) }}/>
              )}
            </div>
          </div>
        </div>
      )}

      {/* Stage confirm dialog */}
      {stageConfirm&&(
        <div style={{position:"fixed",inset:0,background:"#000c",zIndex:400,display:"flex",alignItems:"center",justifyContent:"center"}}>
          <div style={{background:C.panel,border:`1px solid ${C.border}`,borderRadius:16,padding:"28px 32px",maxWidth:380,textAlign:"center"}}>
            <div style={{fontSize:28,marginBottom:12}}>🔄</div>
            <div style={{fontSize:17,fontWeight:900,color:"#f0f6ff",marginBottom:8}}>Change Stage?</div>
            <div style={{fontSize:13,color:C.muted,marginBottom:24,lineHeight:1.6}}>
              Move <strong style={{color:C.text}}>{liveRec?.firstName} {liveRec?.lastName}</strong> to<br/>
              <strong style={{color:C.accent}}>{stageConfirm.label}</strong>?
            </div>
            <div style={{display:"flex",gap:12,justifyContent:"center"}}>
              <button onClick={()=>setStageConfirm(null)} style={btn(C.dim,C.muted)}>Cancel</button>
              <button onClick={confirmStageChange} style={btn("#091c09","#4ade80",true)}>Yes, Move</button>
            </div>
          </div>
        </div>
      )}
      {closedLostFor&&(()=>{
        const opp = bOpps.find(o => o.id === closedLostFor.id);
        return (
          <LostReasonModal
            candidateName={opp ? `${opp.firstName} ${opp.lastName}` : "this candidate"}
            onCancel={()=>setClosedLostFor(null)}
            onConfirm={confirmCloseLost}
          />
        );
      })()}

      {/* Modals */}
      {modal==="confirmDelete"&&modalData.rec&&(
        <div onClick={()=>{setModal(null);setModalData({});}} style={{position:"fixed",inset:0,background:"#000c",zIndex:1100,display:"flex",alignItems:"center",justifyContent:"center",padding:16}}>
          <div onClick={e=>e.stopPropagation()} style={{background:C.panel,border:`1px solid ${C.border}`,borderRadius:14,padding:22,width:"100%",maxWidth:440}}>
            <div style={{display:"flex",alignItems:"center",gap:12,marginBottom:12}}>
              <div style={{fontSize:26}}>🗑</div>
              <div>
                <h3 style={{margin:0,color:"#f0f6ff",fontWeight:900,fontSize:16}}>Delete {modalData.type==="opp"?"opportunity":"lead"}?</h3>
                <div style={{fontSize:12,color:C.muted,marginTop:3}}>{modalData.rec.firstName} {modalData.rec.lastName}</div>
              </div>
            </div>
            <div style={{fontSize:12,color:C.muted,lineHeight:1.55,padding:"10px 12px",background:"#1a0808",border:"1px solid #f8717133",borderRadius:8,marginBottom:14}}>
              This permanently removes the record, its notes, activity log, and any linked communications. This cannot be undone.
            </div>
            <div style={{display:"flex",gap:8,justifyContent:"flex-end"}}>
              <button onClick={()=>{setModal(null);setModalData({});}} style={btn(C.dim,C.muted)}>Cancel</button>
              <button onClick={()=>{deleteRecord(modalData.type,modalData.rec.id);setModal(null);setModalData({});}} style={btn("#1a0808","#f87171",true)}>🗑 Delete</button>
            </div>
          </div>
        </div>
      )}
      {modal==="editRecord"&&modalData.type&&<RecordForm type={modalData.type} rec={modalData.rec}/>}
      {modal==="editBrand"&&<BrandModal brand={modalData.brand}/>}
      {modal==="newPartner"&&<PartnerForm type={modalData.type} id={modalData.id}/>}
      {modal==="stageEditor"&&<StageEditorModal type={modalData.type}/>}
      {modal==="template"&&<TemplateModal rec={modalData.rec} templateHint={modalData.templateHint}/>}
      {modal==="aiSummary"&&<AiSummaryModal/>}
      {modal==="aiOrganize"&&<AiOrganizeModal/>}
      {modal==="sendDocusign"&&liveRec&&selected?.type==="opp"&&(
        <SendDocusignModal
          rec={liveRec}
          onCancel={()=>{ setModal(null); setModalData({}); }}
          onSend={(payload)=>{ sendDocusignEnvelope(liveRec.id, payload); setModal(null); setModalData({}); }}
        />
      )}
      {modal==="bookMeeting"&&modalData.rec&&(
        <BookMeetingModal
          eventTypes={bEventTypes.filter(e => e.active)}
          availability={bAvailability}
          existingBookings={bBookings}
          assignableRecords={[
            ...bLeads.map(l => ({...l, recType:"lead"})),
            ...bOpps .map(o => ({...o, recType:"opp"})),
          ]}
          initialRecord={{...modalData.rec, recType: modalData.type}}
          onCancel={()=>{ setModal(null); setModalData({}); }}
          onSave={(payload)=>{ saveBooking(payload); setModal(null); setModalData({}); }}
        />
      )}

      {/* Toast */}
      {toast&&<div style={{position:"fixed",bottom:20,right:20,zIndex:500,background:toast.type==="err"?"#1a0808":"#091c09",border:`1px solid ${toast.type==="err"?"#f87171":"#4ade80"}`,borderRadius:12,padding:"10px 18px",color:toast.type==="err"?"#f87171":"#4ade80",fontSize:13,fontWeight:700,boxShadow:"0 8px 32px #00000099"}}>{toast.msg}</div>}
    </div>
  );
}

ReactDOM.createRoot(document.getElementById('root')).render(
  React.createElement(App)
);
