// data.jsx — mock data store + state context

const TODAY = new Date("2026-05-16");
const daysAgo = (n) => { const d = new Date(TODAY); d.setDate(d.getDate() - n); return d.toISOString(); };
const daysAhead = (n) => { const d = new Date(TODAY); d.setDate(d.getDate() + n); return d.toISOString(); };

// ─────────────────────────────────────────────────────────────────────
// Master SOP / template (editable in Admin → Templates)
// ─────────────────────────────────────────────────────────────────────
const DEFAULT_TEMPLATE = {
  steps: [
    { id: "welcome",  label: "Welcome",            required: true,  enabled: true,  description: "Personal intro from {{freelancer.name}}, scope summary, and what to expect.", est: "2 min" },
    { id: "intake",   label: "Intake form",        required: true,  enabled: true,  description: "Collect company info, key contacts, timezone, brand assets, and project goals.", est: "5 min" },
    { id: "agree",    label: "Agreement & NDA",    required: true,  enabled: true,  description: "Service agreement, statement of work, and NDA — bundled into a single signature flow.", est: "4 min" },
    { id: "invoice",  label: "Deposit invoice",    required: true,  enabled: true,  description: "First invoice (50% deposit or first month) with card / bank / wire options.", est: "2 min" },
    { id: "files",    label: "Brand & assets",     required: false, enabled: true,  description: "Upload logos, brand guidelines, existing tools access, and credentials.", est: "3 min" },
    { id: "kickoff",  label: "Schedule kickoff",   required: true,  enabled: true,  description: "Pick a 30-minute kickoff time within the next 7 business days.", est: "1 min" },
  ],
  intake_fields: [
    { id: "legal_name",  label: "Legal company name", type: "text",     required: true  },
    { id: "primary_contact", label: "Primary contact name", type: "text", required: true },
    { id: "primary_email", label: "Primary contact email", type: "email", required: true },
    { id: "phone",  label: "Phone (optional)",   type: "tel",      required: false },
    { id: "timezone",    label: "Working timezone",   type: "select",   required: true, options: ["PT", "MT", "CT", "ET", "GMT", "CET", "PHT", "AEST"] },
    { id: "company_size",label: "Company size",       type: "select",   required: false, options: ["Just me", "2–10", "11–50", "51–200", "200+"] },
    { id: "industry",    label: "Industry",           type: "text",     required: false },
    { id: "website",     label: "Website",            type: "url",      required: false },
    { id: "goals",       label: "Top 3 goals for this engagement", type: "textarea", required: true },
    { id: "kpi",         label: "How will we measure success?",     type: "textarea", required: false },
    { id: "tools",       label: "Tools / stack we'll be working in", type: "text",   required: false },
    { id: "comm_pref",   label: "Preferred communication channel",  type: "select", required: true, options: ["Slack", "Email", "Loom + Email", "WhatsApp", "Teams"] },
  ],
  agreement_clauses: [
    { id: "scope", label: "Scope of work", body: "Contractor agrees to perform the services described in Exhibit A — Statement of Work, dated {{date}}. Additional work outside this scope will be quoted separately.", reviewed_by: "Hawthorne Legal LLP", reviewed_at: "2026-02-14", risk: "low", source: "ABA Model Independent Contractor Agreement §2.1, adapted" },
    { id: "term",  label: "Term & termination", body: "This Agreement begins on {{start_date}} and continues until either party provides 14 days written notice of termination. Pro-rated fees apply for partial periods.", reviewed_by: "Hawthorne Legal LLP", reviewed_at: "2026-02-14", risk: "low", source: "Standard 14-day mutual notice" },
    { id: "fees",  label: "Fees & payment", body: "Client agrees to compensation of {{rate}} as outlined in Exhibit B — Payment Schedule. Invoices are net 7. Late payments accrue 1.5% interest per month.", reviewed_by: "Hawthorne Legal LLP", reviewed_at: "2026-02-14", risk: "low", source: "UCC §2-709 compliant" },
    { id: "ip",    label: "Ownership & IP", body: "All deliverables created under this Agreement become the Client's property upon receipt of full payment. Contractor retains the right to display anonymized work in their portfolio unless otherwise specified.", reviewed_by: "Hawthorne Legal LLP", reviewed_at: "2026-02-14", risk: "medium", source: "Work-for-hire alternative — payment-contingent assignment" },
    { id: "conf",  label: "Confidentiality", body: "Each party agrees to keep confidential all non-public information shared during the engagement, in perpetuity. This obligation survives termination.", reviewed_by: "Hawthorne Legal LLP", reviewed_at: "2026-02-14", risk: "low", source: "Mutual NDA — standard form" },
    { id: "indem", label: "Indemnification", body: "Each party will defend, indemnify and hold harmless the other from third-party claims arising from their own gross negligence or willful misconduct.", reviewed_by: "Hawthorne Legal LLP", reviewed_at: "2026-02-14", risk: "medium", source: "Mutual carve-out indemnity — gross negligence only" },
    { id: "law",   label: "Governing law", body: "This Agreement is governed by the laws of the freelancer's state of residence, without regard to conflict of law principles.", reviewed_by: null, reviewed_at: null, risk: "high", source: "⚠ Jurisdiction-dependent — needs review per state" },
  ],
  welcome_message: "Hi {{client.contact}} — I'm thrilled to be working with {{client.company}}. This portal walks you through everything we need to get started, in about 15 minutes. Everything saves as you go.",
  deposit_pct: 50,
  net_days: 7,
  documents: [
    { id: "service_agreement", label: "Service Agreement",  desc: "Master contract — scope, IP, payment terms, governing law.",     enabled: true,  source: "ai", icon: "shield"      },
    { id: "sow",               label: "Statement of Work",   desc: "Exhibit A — deliverables, milestones, timeline, compensation.",   enabled: true,  source: "ai", icon: "briefcase"   },
    { id: "nda",               label: "Mutual NDA",          desc: "Standard mutual non-disclosure. Often bundled with agreement.",    enabled: true,  source: "ai", icon: "shield"      },
    { id: "invoice",           label: "Deposit invoice",     desc: "First invoice with payment links from connected providers.",       enabled: true,  source: "ai", icon: "money"       },
    { id: "payment_schedule",  label: "Payment schedule",    desc: "Future milestones with dates and amounts.",                        enabled: true,  source: "ai", icon: "calendar"    },
    { id: "welcome_packet",    label: "Welcome packet (PDF)", desc: "Branded intro PDF — your process, FAQs, contact info.",          enabled: false, source: "ai", icon: "doc"         },
    { id: "w9",                label: "W-9 / tax form",      desc: "Required by US clients for contractors over $600/year.",          enabled: false, source: "static", file: null, icon: "doc"     },
  ],
  jurisdiction: "US-CA",
  legal_disclaimer: "This document was generated from attorney-reviewed templates. It is not a substitute for legal advice. For high-value engagements ($25K+) or unusual terms, consult a licensed attorney in your jurisdiction.",
  audit_log: [
    { id: 1, when: "2026-02-14", who: "Hawthorne Legal LLP", action: "Annual template review — 6 of 7 clauses approved for US-CA, US-NY, US-TX" },
    { id: 2, when: "2026-03-02", who: "Alex Mira",            action: "Added 'IP — payment-contingent assignment' as default" },
    { id: 3, when: "2026-04-18", who: "Hawthorne Legal LLP",  action: "GDPR clause added for EU clients" },
    { id: 4, when: "2026-05-09", who: "Alex Mira",            action: "Changed default deposit to 50% (was 25%)" },
  ],
};

// ─────────────────────────────────────────────────────────────────────
// Jurisdictions — used to swap clauses & set governing law
// ─────────────────────────────────────────────────────────────────────
const JURISDICTIONS = [
  { id: "US-CA", label: "United States — California",    flag: "🇺🇸", reviewed: true,  notes: "Non-compete clauses void by statute. Strong IC protections." },
  { id: "US-NY", label: "United States — New York",      flag: "🇺🇸", reviewed: true,  notes: "Freelance Isn't Free Act applies — written contracts required >$800." },
  { id: "US-TX", label: "United States — Texas",         flag: "🇺🇸", reviewed: true,  notes: "Permissive non-compete with reasonable scope. No state income tax." },
  { id: "US-FL", label: "United States — Florida",       flag: "🇺🇸", reviewed: false, notes: "Needs review — non-compete enforceability varies." },
  { id: "GB",    label: "United Kingdom",                flag: "🇬🇧", reviewed: true,  notes: "IR35 status determination required. VAT registration threshold £85K." },
  { id: "PH",    label: "Philippines",                   flag: "🇵🇭", reviewed: true,  notes: "Data Privacy Act 2012. BIR Form 2307 may be required." },
  { id: "EU",    label: "European Union (general)",      flag: "🇪🇺", reviewed: true,  notes: "GDPR data-processing addendum required when handling personal data." },
  { id: "CA",    label: "Canada",                        flag: "🇨🇦", reviewed: false, notes: "Needs review — provincial variation significant." },
  { id: "AU",    label: "Australia",                     flag: "🇦🇺", reviewed: false, notes: "Needs review — ABN required, GST threshold AUD 75K." },
];

// ─────────────────────────────────────────────────────────────────────
// Sample inputs for non-JD intake modes
// ─────────────────────────────────────────────────────────────────────
const SAMPLE_INPUTS = {
  jd: `JOB TITLE: Systems & Technical Project Manager
Reports To: Chief Operating Officer
Department: Operations / Technology
Employment Type: Full-Time
Location: Remote

The AI Systems & Technical Project Manager owns the implementation and ongoing administration of the company's AI-powered internal platforms — including ticket management, project management, billing, and accounting systems — built using AI-assisted development tools like Lovable.dev.

KEY RESPONSIBILITIES
• Lead implementation of AI-built internal systems
• Operate AI development tools end-to-end
• Maintain ticket lifecycle, classification, routing
• AI-generated support documentation pipeline
• Coordinate dev ↔ support handoffs and weekly status to COO

40 hours / week · $1,500 / month`,
  scope: `Project: Stand up the internal AI systems function at Veltrix AI.

What we need:
— Admin of our ticket / PM / billing platforms (all built in Lovable)
— Real ticket lifecycle: classification, routing, queue health
— A documentation pipeline that actually keeps up with product changes
— Weekly status to COO + dev-support coordination

Time: full-time, 40 hrs/wk
Budget: $1,500/mo
Start: ASAP, ideally within 2 weeks`,
  voice: `[Loom · 4 min 12 sec · auto-transcribed]

Hey — so, the deal is, we built our entire internal tooling stack on Lovable. Ticket system, PM, billing, accounting — all of it AI-built. Problem is, nobody owns it. Things break, docs are stale, support and engineering don't talk.

What I need is someone who can OWN this. Not a developer — someone who can prompt the AI tools, configure the systems, keep documentation flowing, and basically be the bridge between dev and support.

I'm thinking full-time, 40 hours, around fifteen hundred a month. Reports directly to me — I'm the COO. Could start in two weeks. The big deliverables would be: get the ticket system actually working, build out a real docs pipeline, and run a weekly cadence with the dev team.`,
  convo: `From: Maren Okafor <maren@veltrix.ai>
Subject: Re: introduction

Hi Alex — yes, you came highly recommended. Quick brief:

We built our internal stack on Lovable. Now we need someone to run it. Specifically: ticket management, PM, billing/accounting platforms. We need configuration, admin, AI-generated docs, and dev-support coordination.

Full-time, $1,500/mo, 40 hrs/week. I'm the COO, you'd report to me. Start in 2 weeks if possible.

Let me know if you're up for it and we can move on paperwork.

Maren`,
  blank: "",
};

// ─────────────────────────────────────────────────────────────────────
// Clients
// ─────────────────────────────────────────────────────────────────────
const CLIENTS = [];

// ─────────────────────────────────────────────────────────────────────
// Activity feed for admin dashboard
// ─────────────────────────────────────────────────────────────────────
const ACTIVITY = [
  { id: 1, who: "Sasha Reine", company: "Hollow & Co.", action: "Paid deposit invoice", amount: "$1,100", t: "2h ago", kind: "paid", clientId: "hollow" },
  { id: 2, who: "Iliana Voss", company: "Brightline Studio", action: "Uploaded brand assets (12 files)", t: "5h ago", kind: "upload", clientId: "brightline" },
  { id: 3, who: "Maren Okafor", company: "Veltrix AI", action: "Completed intake form", t: "yesterday", kind: "intake", clientId: "veltrix" },
  { id: 4, who: "Pieter Hauser", company: "Northwind Logistics", action: "Opened portal link", t: "today", kind: "open", clientId: "northwind" },
  { id: 5, who: "Iliana Voss", company: "Brightline Studio", action: "Booked kickoff for May 22, 10:00 AM ET", t: "1d ago", kind: "schedule", clientId: "brightline" },
  { id: 6, who: "Sasha Reine", company: "Hollow & Co.", action: "Signed Service Agreement", t: "3d ago", kind: "sign", clientId: "hollow" },
];

// Notification mockup contents
const NOTIFICATIONS = [
  { type: "client-signed",   subject: "Sasha Reine signed your Service Agreement",  preview: "View signed PDF →", t: "Mon, May 13",  kind: "in" },
  { type: "invoice-paid",    subject: "Hollow & Co. paid Invoice #1042",            preview: "$1,100.00 via card",  t: "Mon, May 13", kind: "in" },
  { type: "invite-sent",     subject: "Onboarding invite sent to Pieter Hauser",    preview: "Northwind Logistics", t: "Today",       kind: "out" },
  { type: "reminder",        subject: "Reminder sent: Veltrix AI — Agreement pending", preview: "Auto-nudge after 48h", t: "Today",   kind: "out" },
];

// Relative time label, e.g. "just now", "3h ago", "2d ago", "May 14".
function _relTime(ts) {
  if (!ts) return "";
  const d = new Date(ts); if (isNaN(d)) return "";
  const s = Math.max(0, (Date.now() - d.getTime()) / 1000);
  if (s < 90) return "just now";
  const m = s / 60; if (m < 60) return Math.round(m) + "m ago";
  const h = m / 60; if (h < 24) return Math.round(h) + "h ago";
  const days = h / 24; if (days < 7) return Math.round(days) + "d ago";
  try { return d.toLocaleDateString("en-US", { month: "short", day: "numeric" }); } catch { return ""; }
}

// Public-facing domains (Cloudflare DNS → Firebase Hosting, same site):
//   work.trendvibecreatives.com/portfolio  → prospects (lead funnel)
//   portal.trendvibecreatives.com/?client= → active clients (onboarding)
const PORTFOLIO_ORIGIN = "https://work.trendvibecreatives.com";
const PORTAL_ORIGIN = "https://portal.trendvibecreatives.com";

const generateReferralLink = (id) =>
  `${PORTFOLIO_ORIGIN}/portfolio?ref=${id}`;
const generatePortalLink = (company) =>
  `${PORTAL_ORIGIN}/?client=${clientSlug(company)}`;

// Build a REAL activity feed from actual client events — no demo data.
function deriveActivity(clients) {
  const out = []; let id = 1;
  (clients || []).forEach(c => {
    const base = { who: c.contact, company: c.company, clientId: c.id };

    // Lead-specific events
    if (c.status === "lead") {
      if (c.contactedDonnaAt) out.push({ ...base, id: id++, kind: "lead_contact", action: "Sent a message via portfolio", ts: c.contactedDonnaAt });
      if (c.portfolioVisits && c.portfolioVisits.length > 0) {
        const last = c.portfolioVisits[c.portfolioVisits.length - 1];
        out.push({ ...base, id: id++, kind: "lead_visit", action: `Viewed your portfolio${c.portfolioVisits.length > 1 ? ` (${c.portfolioVisits.length}×)` : ""}`, ts: last.ts });
      }
      if (c.leadAddedAt) out.push({ ...base, id: id++, kind: "open", action: "Added as a lead", ts: c.leadAddedAt });
      return;
    }

    if (c.signature && c.signature.signedAt) out.push({ ...base, id: id++, kind: "sign", action: "Signed the agreement", ts: c.signature.signedAt });
    if (c.intake && c.intake.submittedAt) out.push({ ...base, id: id++, kind: "intake", action: "Completed the intake form", ts: c.intake.submittedAt });
    if (c.files && c.files.length) out.push({ ...base, id: id++, kind: "upload", action: `Uploaded ${c.files.length} file${c.files.length > 1 ? "s" : ""}`, ts: (c.files[c.files.length - 1] || {}).uploadedAt || c.invitedAt });
    if (c.notified && c.notified.paid) out.push({ ...base, id: id++, kind: "paid", action: "Marked the deposit paid", ts: c.notified.paid });
    if (c.billing && c.billing.sent) Object.values(c.billing.sent).forEach(ts => out.push({ ...base, id: id++, kind: "paid", action: "Invoice sent", ts }));

    // Real-time portal visits (with email open status)
    const lastVisit = (c.portalVisits && c.portalVisits.length > 0) ? c.portalVisits[c.portalVisits.length - 1] : null;
    const emailOpened = c.portalNudgeSentAt && c.notified && c.notified.opened;
    if (lastVisit) {
      let action = "Opened their portal";
      if (emailOpened) {
        const nudgeSent = new Date(c.portalNudgeSentAt);
        const portalVisit = new Date(lastVisit.ts);
        if (portalVisit < nudgeSent) {
          action = "Visited portal (before nudge)";
        } else {
          const minDiff = (portalVisit - nudgeSent) / 60000;
          if (minDiff < 1) action = "Clicked email → opened portal";
          else if (minDiff < 5) action = `Opened email, visited portal ${Math.round(minDiff)}m later`;
          else action = `Opened email, but visited portal much later (${Math.round(minDiff)}m)`;
        }
      }
      out.push({ ...base, id: id++, kind: "open", action, ts: lastVisit.ts, visits: c.portalVisits.length, emailOpened });
    } else if (c.notified && c.notified.opened) {
      out.push({ ...base, id: id++, kind: "open", action: "Opened email · portal not visited", ts: c.notified.opened });
    }

    if (c.invitedAt) out.push({ ...base, id: id++, kind: "open", action: "Added as a client", ts: c.invitedAt });
  });
  return out.filter(x => x.ts).sort((a, b) => new Date(b.ts) - new Date(a.ts)).slice(0, 10)
    .map(x => ({ ...x, t: _relTime(x.ts) }));
}

// Notifications are the same real events, framed for the Inbox.
function deriveNotifications(clients) {
  return deriveActivity(clients).slice(0, 8).map(a => ({
    type: a.kind, subject: `${a.who} — ${a.action}`, preview: a.company, t: a.t,
    kind: (a.action === "Invoice sent" || a.action === "Added as a client" || a.action === "Added as a lead") ? "out" : "in",
    clientId: a.clientId,
  }));
}

// Default freelancer profile
const DEFAULT_FREELANCER = {
  name: "Donna",
  title: "Founder & Creative Director",
  brand: "TrendVibe Creatives",
  tagline: "Onboarding, handled.",
  email: "mail@trendvibecreatives.com",
  timezone: "PT",
};

// ─────────────────────────────────────────────────────────────────────
// Signature providers — native by default, external connectors optional
// ─────────────────────────────────────────────────────────────────────
const DEFAULT_SIGNATURE_PROVIDERS = [
  {
    id: "native", name: "Native e-sign", tagline: "Built-in · free · ESIGN Act compliant",
    region: "US, EU, UK, AU, CA, PH, and 180+ countries",
    cost: "Free · unlimited", legal: "ESIGN Act · eIDAS SES · UETA",
    status: "connected", isDefault: true,
    captures: ["Typed signature", "IP address", "Timestamp (UTC)", "User-agent", "Tamper-evident PDF hash", "Email confirmation receipt"],
    note: "Holds up in court for routine business contracts. For real estate, wills, or notarized docs, use an external provider with KBA.",
    color: "#1B4332",
  },
  {
    id: "docusign", name: "DocuSign", tagline: "Enterprise standard · trusted brand",
    region: "Global · 1M+ businesses", cost: "$10–65/user/mo + per envelope",
    legal: "ESIGN · eIDAS QES available · 21 CFR Part 11 (FDA)",
    status: "not_connected",
    captures: ["Native plus: KBA identity verification, notary, qualified e-signatures, attorney-defensible certificate"],
    note: "Required by some enterprise clients and regulated industries (healthcare, finance, gov).",
    color: "#FFCC22",
    popular_for: "Enterprise clients",
  },
  {
    id: "hellosign", name: "Dropbox Sign", tagline: "Solid mid-tier alternative",
    region: "Global · ex-HelloSign", cost: "$20–30/mo",
    legal: "ESIGN · eIDAS SES/AES",
    status: "not_connected",
    captures: ["Native plus: audit trail PDF, in-person signing, signer attachments"],
    note: "Cheaper than DocuSign with similar enterprise features. Good middle ground.",
    color: "#0061FF",
  },
  {
    id: "pandadoc", name: "PandaDoc", tagline: "Best for proposals & sales docs",
    region: "Global · proposal-first",
    cost: "$19–49/user/mo",
    legal: "ESIGN · eIDAS SES",
    status: "not_connected",
    captures: ["Native plus: proposal templates, embedded payments, tracking pixels"],
    note: "Overkill for simple agreements. Worth it if you also use PandaDoc for proposals.",
    color: "#3DD179",
  },
  {
    id: "adobesign", name: "Adobe Sign", tagline: "If you live in Adobe already",
    region: "Global", cost: "$15–30/user/mo",
    legal: "ESIGN · eIDAS QES · CFR Part 11",
    status: "not_connected",
    captures: ["Native plus: Adobe Creative Cloud integration, advanced workflows"],
    note: "Fine, but no real advantage unless you're already paying for Creative Cloud.",
    color: "#FA0F00",
  },
];

// ─────────────────────────────────────────────────────────────────────
// Payment providers — connected once in Settings, then every invoice
// auto-generates the right link / QR / bank details from this list.
// ─────────────────────────────────────────────────────────────────────
const DEFAULT_PAYMENT_PROVIDERS = [
  {
    id: "stripe", name: "Stripe", tagline: "Card, Apple Pay, Google Pay, Link",
    region: "Global · 46 countries", fees: "2.9% + $0.30",
    payout: "2 business days", connect: "OAuth",
    status: "not_connected", account: null, live: true,
    paymentUrl: null, // Stripe Payment Link URL — paste from Stripe Dashboard
    badge: "Recommended",
    linkFormat: "checkout.stripe.com/c/pay/cs_live_•••",
    note: "Per-invoice checkout link is generated automatically. Client pays with card, link is marked paid in real time.",
    color: "#635BFF",
  },
  {
    id: "wise", name: "Wise", tagline: "Multi-currency bank transfer · low FX",
    region: "Global · popular for PH & SEA", fees: "0.4 – 1%",
    payout: "Same day (in-region)", connect: "API key + business account",
    status: "not_connected", account: null,
    linkFormat: "wise.com/pay/business/yourname",
    note: "Wise generates a payment request link with amount + reference pre-filled. Client pays from any bank.",
    color: "#00B9FF",
    popular_for: "Philippines, India, LATAM",
  },
  {
    id: "payoneer", name: "Payoneer", tagline: "Global receiving accounts",
    region: "Global · #1 for OnlineJobs.ph workers", fees: "Up to 3% (intl)",
    payout: "Same day", connect: "Email + receiving account ID",
    status: "not_connected", account: null,
    linkFormat: "payoneer.com/request/•••",
    note: "Sends a payment request. Client receives an email with one-click pay options.",
    color: "#FF4800",
    popular_for: "Filipino VAs",
  },
  {
    id: "paypal", name: "PayPal", tagline: "Card or PayPal balance — embedded Smart Buttons",
    region: "Global · 200+ markets", fees: "3.49% + $0.49 (intl)",
    payout: "1 business day", connect: "Client ID (developer.paypal.com)",
    status: "not_connected", account: null,
    clientId: null,   // PayPal app Client ID — enables embedded Smart Buttons (zero backend)
    paymentUrl: null, // paypal.me/username fallback if no clientId
    linkFormat: "paypal.com/invoice/p/#•••",
    note: "PayPal Smart Buttons embedded directly in the portal — zero backend, client pays without leaving the page.",
    color: "#003087",
  },
  {
    id: "gcash", name: "GCash", tagline: "Local Philippines · instant",
    region: "Philippines only", fees: "Free for personal",
    payout: "Instant", connect: "Mobile + QR upload",
    status: "not_connected", account: null,
    linkFormat: "QR code + reference number",
    note: "We show your QR + a reference number on the invoice. Client pays in the GCash app and replies with the receipt.",
    color: "#007DFE",
    popular_for: "Philippines",
  },
  {
    id: "bank", name: "Bank transfer", tagline: "ACH / wire / manual",
    region: "Anywhere", fees: "Bank fees only",
    payout: "1–5 business days", connect: "Just your account details",
    status: "not_connected", account: null,
    linkFormat: "Bank details on the invoice",
    note: "We show full transfer instructions on the invoice. Manual mark-as-paid when the wire lands.",
    color: "#0F1A14",
  },
];

// ─────────────────────────────────────────────────────────────────────
// AI brand extraction (mock) — given a site URL, return a brand kit
// ─────────────────────────────────────────────────────────────────────
const BRAND_PRESETS = {
  "veltrix.ai": {
    palette: ["#0F2A1F", "#9FE870", "#1A1F1B", "#F2F2EC"],
    primary: "#0F2A1F", accent: "#9FE870", surface: "#F2F2EC", ink: "#0F1A14",
    logo: { mark: "V", bg: "#0F2A1F", fg: "#9FE870" },
    font: { display: "Söhne", body: "Inter" },
    voice: "Confident · technical · plain-spoken",
    tagline: "AI-native systems for ops teams.",
  },
  "brightlinestudio.com": {
    palette: ["#1E1A14", "#C9A961", "#F4E8DB", "#8A5E2C"],
    primary: "#1E1A14", accent: "#C9A961", surface: "#F4E8DB", ink: "#1E1A14",
    logo: { mark: "B", bg: "#1E1A14", fg: "#F4E8DB" },
    font: { display: "Editorial New", body: "Söhne" },
    voice: "Editorial · warm · considered",
    tagline: "An identity studio in Brooklyn.",
  },
  "hollowandco.com": {
    palette: ["#5B3F8A", "#A28FC5", "#E8DEF0", "#0F0A14"],
    primary: "#5B3F8A", accent: "#A28FC5", surface: "#FBF7F2", ink: "#0F0A14",
    logo: { mark: "H", bg: "#5B3F8A", fg: "#FBF7F2" },
    font: { display: "GT Sectra", body: "Söhne" },
    voice: "Quiet · intentional · premium",
    tagline: "Slow fashion, made to last.",
  },
  "northwind.co": {
    palette: ["#2C4D8A", "#6B8CC4", "#DDE6F4", "#0A1224"],
    primary: "#2C4D8A", accent: "#6B8CC4", surface: "#F4F6FB", ink: "#0A1224",
    logo: { mark: "N", bg: "#2C4D8A", fg: "#fff" },
    font: { display: "Söhne", body: "Söhne" },
    voice: "Plain · operational · no-nonsense",
    tagline: "Freight, modernized.",
  },
};

function extractBrand(rawUrl, companyHint) {
  const host = (rawUrl || "").toLowerCase()
    .replace(/^https?:\/\//, "")
    .replace(/^www\./, "")
    .replace(/\/.*$/, "")
    .trim();
  if (!host) return { brand: null };
  if (BRAND_PRESETS[host]) {
    return { brand: { url: host, detected: true, ...BRAND_PRESETS[host] } };
  }
  // Procedural fallback — deterministic colors based on hostname
  const seed = host.split("").reduce((a, c) => a + c.charCodeAt(0), 0);
  const hues = [210, 260, 25, 150, 340, 190];
  const h = hues[seed % hues.length];
  const primary = `oklch(0.38 0.10 ${h})`;
  const accent  = `oklch(0.74 0.13 ${h})`;
  const surface = `oklch(0.96 0.02 ${h})`;
  return {
    brand: {
      url: host, detected: true,
      palette: [primary, accent, surface, "#0F1A14"],
      primary, accent, surface, ink: "#0F1A14",
      logo: { mark: ((companyHint || host)[0] || "•").toUpperCase(), bg: primary, fg: "#fff" },
      font: { display: "Geist", body: "Geist" },
      voice: "Friendly · professional · direct",
      tagline: host,
    }
  };
}

// ─────────────────────────────────────────────────────────────────────
// AI extraction (mock) — given JD + rate, return populated client object
// ─────────────────────────────────────────────────────────────────────
function extractFromJD(jd, rate) {
  // Heuristic extraction — for demo we always produce the Veltrix client when
  // certain keywords appear; otherwise generate sensible defaults.
  const txt = (jd || "").toLowerCase();
  const isPM = /system|technical|project manager|ai|ticket/.test(txt);
  if (isPM) {
    return {
      role_title: "Systems & Technical Project Manager",
      company: "Veltrix AI",
      contact: "Maren Okafor",
      email: "maren@veltrix.ai",
      role: "Chief Operating Officer",
      engagementType: "Full-time, monthly retainer",
      hoursPerWeek: 40,
      rate: Number(rate) || 1500,
      rateLabel: `$${(Number(rate)||1500).toLocaleString()} / month`,
      deliverables: [
        "Own implementation + admin of AI-built internal systems",
        "Operate AI development tools (Lovable.dev) end-to-end",
        "Maintain ticket lifecycle, classification & routing",
        "AI-generated support documentation pipeline",
        "Coordinate dev ↔ support handoffs and weekly status",
      ],
      milestones: [
        { name: "Onboarding & system audit", date: daysAhead(14), pct: 0 },
        { name: "Ticket system v1 live",      date: daysAhead(30), pct: 0 },
        { name: "Docs pipeline + 25 articles", date: daysAhead(60), pct: 0 },
        { name: "Quarterly review with COO",  date: daysAhead(90), pct: 0 },
      ],
    };
  }
  return {
    role_title: "Project consultant",
    company: "New Client Co.",
    contact: "",
    email: "",
    role: "",
    engagementType: "Monthly retainer",
    hoursPerWeek: 20,
    rate: Number(rate) || 0,
    rateLabel: `$${(Number(rate)||0).toLocaleString()} / month`,
    deliverables: ["Discovery & roadmap", "Implementation", "Reporting & handoff"],
    milestones: [{ name: "Discovery", date: daysAhead(14), pct: 0 }],
  };
}

// ─────────────────────────────────────────────────────────────────────
// REAL AI extraction via Claude (claude-opus-4-8)
//  • LOCAL dev: calls Anthropic directly using the key in config.local.js
//    (with the browser-access header). Key only visible on your machine.
//  • PRODUCTION: if anthropic.proxyUrl is set, POSTs to your Firebase
//    Function instead — the key lives server-side, never in the browser.
// Returns a client-shaped object (same as extractFromJD) or throws.
// ─────────────────────────────────────────────────────────────────────
async function extractFromJD_AI({ text, rate, rateType }) {
  const cfg = (window.RELAY_CONFIG && window.RELAY_CONFIG.anthropic) || {};
  const proxyUrl = cfg.proxyUrl || "";
  const model = cfg.model || "claude-opus-4-8";
  const rateNum = Number(rate) || 0;
  const unit = ["hour", "week", "month", "project"].includes(rateType) ? rateType : "month";

  const toClient = (raw) => ({
    role_title: raw.role_title || "Consultant",
    company: raw.company || "",
    contact: raw.contact || "",
    email: raw.email || "",
    role: raw.contact_role || "Primary contact",
    engagementType: raw.engagementType || (rateType === "project" ? "Fixed-price project" : "Monthly retainer"),
    hoursPerWeek: raw.hoursPerWeek || null,
    rate: rateNum,
    rateLabel: `$${rateNum.toLocaleString()} / ${unit}`,
    deliverables: (raw.deliverables && raw.deliverables.length) ? raw.deliverables : ["Discovery & roadmap", "Implementation", "Reporting & handoff"],
    milestones: (raw.milestones && raw.milestones.length ? raw.milestones : [{ name: "Discovery", weeksFromStart: 2 }])
      .map(m => ({ name: m.name, date: daysAhead((Number(m.weeksFromStart) || 2) * 7), pct: 0 })),
  });

  // Production path — go through the Firebase Function (key stays server-side)
  if (proxyUrl) {
    const res = await fetch(proxyUrl, {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify({ text, rate: rateNum, rateType }),
    });
    if (!res.ok) {
      let d = ""; try { d = (await res.json()).error || ""; } catch {}
      throw new Error(`AI extract failed ${res.status}: ${d || res.statusText}`);
    }
    return toClient(await res.json());
  }

  // Local dev path — call Anthropic directly
  if (!cfg.apiKey) throw new Error("No Anthropic key or proxy configured");

  const schema = {
    type: "object",
    additionalProperties: false,
    properties: {
      role_title: { type: "string" },
      company: { type: "string" },
      contact: { type: "string" },
      contact_role: { type: "string" },
      email: { type: "string" },
      engagementType: { type: "string" },
      hoursPerWeek: { type: "integer" },
      deliverables: { type: "array", items: { type: "string" } },
      milestones: {
        type: "array",
        items: {
          type: "object",
          additionalProperties: false,
          properties: { name: { type: "string" }, weeksFromStart: { type: "integer" } },
          required: ["name", "weeksFromStart"],
        },
      },
    },
    required: ["role_title", "company", "contact", "contact_role", "email", "engagementType", "hoursPerWeek", "deliverables", "milestones"],
  };

  const system = "You extract structured freelance engagement details from a pasted job description, scope brief, email thread, or call transcript. Infer sensible values from the text. role_title is the freelancer's role. engagementType is a short phrase like 'Full-time, monthly retainer' or 'Fixed-price, 6 week project'. hoursPerWeek is the weekly hours (0 if a fixed-price project with no weekly hours). deliverables is 3–6 concrete outcomes. milestones is 2–4 checkpoints, each with a weeksFromStart offset. If a field is genuinely unknown, use an empty string (or 0 for hoursPerWeek). Output only the structured fields.";

  const res = await fetch("https://api.anthropic.com/v1/messages", {
    method: "POST",
    headers: {
      "x-api-key": cfg.apiKey,
      "anthropic-version": "2023-06-01",
      "anthropic-dangerous-direct-browser-access": "true",
      "content-type": "application/json",
    },
    body: JSON.stringify({
      model,
      max_tokens: 2000,
      system,
      messages: [{ role: "user", content: `Agreed rate: ${rateNum} per ${unit}.\n\nSource material:\n${text || "(no description provided — produce sensible defaults for a generic consulting engagement)"}` }],
      output_config: { format: { type: "json_schema", schema } },
    }),
  });
  if (!res.ok) {
    let d = ""; try { d = (await res.json()).error?.message || ""; } catch {}
    throw new Error(`Claude ${res.status}: ${d || res.statusText}`);
  }
  const data = await res.json();
  const block = (data.content || []).find(b => b.type === "text");
  if (!block) throw new Error("Claude returned no text");
  return toClient(JSON.parse(block.text));
}

// ─────────────────────────────────────────────────────────────────────
// React context
// ─────────────────────────────────────────────────────────────────────
const AppContext = React.createContext(null);

// ─────────────────────────────────────────────────────────────────────
// Toast / notification helper — global, used by every button action
// ─────────────────────────────────────────────────────────────────────
const ToastContext = React.createContext(null);
function ToastProvider({ children }) {
  const [toasts, setToasts] = React.useState([]);
  const idRef = React.useRef(0);

  const toast = React.useCallback((message, opts = {}) => {
    const id = ++idRef.current;
    setToasts(ts => [...ts, { id, message, kind: opts.kind || "info", icon: opts.icon }]);
    setTimeout(() => setToasts(ts => ts.filter(t => t.id !== id)), opts.duration || 2800);
  }, []);

  const copyToClipboard = React.useCallback((text, label) => {
    try {
      if (navigator.clipboard) {
        navigator.clipboard.writeText(text);
      } else {
        const ta = document.createElement("textarea");
        ta.value = text;
        document.body.appendChild(ta); ta.select(); document.execCommand("copy"); document.body.removeChild(ta);
      }
      toast(label || `Copied: ${text}`, { kind: "success" });
    } catch {
      toast("Couldn't copy — select manually", { kind: "warn" });
    }
  }, [toast]);

  return (
    <ToastContext.Provider value={{ toast, copyToClipboard }}>
      {children}
      <div style={{ position: "fixed", bottom: 20, left: "50%", transform: "translateX(-50%)", display: "flex", flexDirection: "column-reverse", gap: 8, zIndex: 2147483647, pointerEvents: "none" }}>
        {toasts.map(t => (
          <div key={t.id} style={{
            background: t.kind === "success" ? "#1B4332" : t.kind === "warn" ? "#8A5E2C" : t.kind === "danger" ? "#B23A2E" : "#0F1A14",
            color: "#fff", padding: "10px 16px", borderRadius: 999,
            fontSize: 13, fontWeight: 500, letterSpacing: "-0.005em",
            boxShadow: "0 8px 24px rgba(0,0,0,0.18), 0 2px 6px rgba(0,0,0,0.12)",
            animation: "toastIn 0.22s cubic-bezier(.2,.7,.3,1)",
            display: "flex", alignItems: "center", gap: 8, maxWidth: 460,
          }}>
            <span style={{ width: 6, height: 6, borderRadius: "50%", background: t.kind === "success" ? "#80E0A7" : "#fff", opacity: 0.9 }}/>
            {t.message}
          </div>
        ))}
      </div>
      <style>{`@keyframes toastIn { from { opacity: 0; transform: translateY(8px); } to { opacity: 1; transform: translateY(0); } }`}</style>
    </ToastContext.Provider>
  );
}
const useToast = () => React.useContext(ToastContext) || { toast: () => {}, copyToClipboard: () => {} };

// ─────────────────────────────────────────────────────────────────────
// localStorage helpers — persist all app state across page refreshes
// ─────────────────────────────────────────────────────────────────────
const LS_PREFIX = "relay_v1_";
function lsLoad(key, fallback) {
  try { const s = localStorage.getItem(LS_PREFIX + key); return s ? JSON.parse(s) : fallback; } catch { return fallback; }
}
function lsSave(key, val) {
  try { localStorage.setItem(LS_PREFIX + key, JSON.stringify(val)); } catch {}
}
function lsClear() {
  Object.keys(localStorage).filter(k => k.startsWith(LS_PREFIX)).forEach(k => localStorage.removeItem(k));
}

// ─────────────────────────────────────────────────────────────────────
// Firestore live sync — same workspace doc visible to every signed-in
// admin browser/device. Writes from anywhere fan out via onSnapshot,
// keeping Chrome ↔ Edge ↔ phone in lockstep.
// ─────────────────────────────────────────────────────────────────────
const WORKSPACE_DOC = "trendvibe";
let _fsDb = null;
function getFirestore() {
  if (_fsDb) return _fsDb;
  const cfg = window.RELAY_CONFIG && window.RELAY_CONFIG.firebase;
  if (!cfg || !window.firebase || !window.firebase.firestore) return null;
  try {
    if (!window.firebase.apps.length) window.firebase.initializeApp(cfg);
    _fsDb = window.firebase.firestore();
    return _fsDb;
  } catch (e) { console.warn("Firestore init failed", e); return null; }
}

// Subscribe to live workspace state. cb({ clients, template, tweaks }).
// Returns an unsubscribe fn. Falls back to no-op when Firestore isn't ready.
function subscribeWorkspace(cb) {
  const db = getFirestore();
  if (!db) return () => {};
  const ref = db.collection("workspaces").doc(WORKSPACE_DOC);
  return ref.onSnapshot(
    (snap) => { if (snap.exists) cb(snap.data() || {}); },
    (err) => console.warn("Firestore subscribe error", err)
  );
}

// Write a slice ({clients} | {template} | {tweaks}) up to the workspace doc.
// Debounced per-key so rapid edits don't spam the network. Pending writes are
// tracked so callers can force-flush them (on tab close, manual sync, etc.).
const _writeTimers = {};
const _pendingWrites = {};

function _flushWrite(key) {
  const partial = _pendingWrites[key];
  if (!partial) return null;
  const db = getFirestore();
  if (!db) return null;
  delete _pendingWrites[key];
  clearTimeout(_writeTimers[key]);
  const ref = db.collection("workspaces").doc(WORKSPACE_DOC);
  return ref.set({ ...partial, updatedAt: Date.now() }, { merge: true })
    .catch(e => console.warn("Firestore write error", e));
}

function pushWorkspace(partial) {
  const db = getFirestore();
  if (!db) return;
  // Keep clients in MAP form even if a caller hands us an array, so the cloud
  // representation never flips back to array (which would break per-client writes).
  if (partial && Array.isArray(partial.clients)) {
    const map = {};
    partial.clients.forEach(c => { if (c && c.id) map[c.id] = c; });
    partial = { ...partial, clients: map };
    _clientsShape = "map";
  }
  const key = Object.keys(partial)[0] || "all";
  // Store the latest payload — subsequent calls within the debounce window
  // simply replace it, so we always write the newest state.
  _pendingWrites[key] = partial;
  clearTimeout(_writeTimers[key]);
  _writeTimers[key] = setTimeout(() => _flushWrite(key), 400);
}

// ─────────────────────────────────────────────────────────────────────
// Per-client cloud writes (MAP representation)
// Firestore stores `clients` as a MAP keyed by client id — NOT an array — so
// two devices editing DIFFERENT clients write different field paths
// (`clients.<idA>` vs `clients.<idB>`) and can never overwrite each other.
// Reads normalize the map back into a sorted array for the rest of the app.
// ─────────────────────────────────────────────────────────────────────
let _clientsShape = "none"; // 'none' | 'array' | 'map' — last-seen cloud representation

// Normalize whatever the cloud holds (array OR map) into a sorted array.
// Side effect: records the cloud's current shape so writers know whether a
// one-time array→map migration is still needed.
function _clientsArrayFromCloud(raw) {
  if (Array.isArray(raw)) { _clientsShape = "array"; return raw; }
  if (raw && typeof raw === "object") {
    _clientsShape = "map";
    return Object.values(raw)
      .filter(c => c && c.id)
      .sort((a, b) => String(b.invitedAt || "").localeCompare(String(a.invitedAt || "")));
  }
  return [];
}

const _clientTimers = {};
const _pendingClients = {};

function _flushClient(id) {
  if (!(id in _pendingClients)) return null;
  const payload = _pendingClients[id];
  const db = getFirestore();
  if (!db) return null;
  delete _pendingClients[id];
  clearTimeout(_clientTimers[id]);
  const ref = db.collection("workspaces").doc(WORKSPACE_DOC);
  const FieldPath = window.firebase.firestore.FieldPath;
  if (payload === "__DELETE__") {
    const FieldValue = window.firebase.firestore.FieldValue;
    return ref.update(new FieldPath("clients", id), FieldValue.delete())
      .catch(e => console.warn("client delete error", e));
  }
  // update() with a FieldPath sets exactly that one client entry — removed
  // fields within the client are dropped, other clients untouched.
  return ref.update(new FieldPath("clients", id), payload, "updatedAt", Date.now())
    .catch(e => console.warn("client write error", e));
}

function _scheduleClient(id, payload) {
  _pendingClients[id] = payload;
  clearTimeout(_clientTimers[id]);
  _clientTimers[id] = setTimeout(() => _flushClient(id), 400);
}

// One-time migration: replace an array-shaped `clients` field with a map,
// preserving every client. update() replaces the whole field wholesale.
function _migrateClientsToMap(allClients) {
  const db = getFirestore();
  if (!db) return Promise.resolve();
  const map = {};
  (allClients || []).forEach(c => { if (c && c.id) map[c.id] = c; });
  _clientsShape = "map";
  const ref = db.collection("workspaces").doc(WORKSPACE_DOC);
  return ref.update({ clients: map, updatedAt: Date.now() })
    .catch(() => ref.set({ clients: map, updatedAt: Date.now() }, { merge: true }))
    .catch(e => { _clientsShape = "none"; console.warn("clients migrate error", e); });
}

// Entry point the provider calls when local `clients` changes. Pushes ONLY the
// clients that actually changed (and deletes removed ones) — never the whole array.
function syncClientsToCloud(changed, deletedIds, allClients) {
  const db = getFirestore();
  if (!db) return;
  if (_clientsShape !== "map") {
    // Cloud still holds an array (or nothing) — migrate once. The migration
    // writes ALL current clients, so it already includes these changes.
    _migrateClientsToMap(allClients);
    return;
  }
  (changed || []).forEach(c => { if (c && c.id) _scheduleClient(c.id, c); });
  (deletedIds || []).forEach(id => _scheduleClient(id, "__DELETE__"));
}

// "Push everything from this device to the cloud" — reads the persisted local
// state and MERGES it up. This is ADDITIVE and SAFE: it adds/updates the clients
// this browser knows about, but NEVER deletes a cloud client that this browser
// happens not to have (which previously wiped clients created on other devices).
// This is what the sidebar "Force sync to cloud" + Templates "Sync" buttons call.
function forceSyncFromLocal() {
  const db = getFirestore();
  if (!db) return Promise.resolve();
  const clients = lsLoad("clients", []);
  const template = lsLoad("template", null);
  const profile = lsLoad("profile", null);
  const map = {};
  (Array.isArray(clients) ? clients : []).forEach(c => { if (c && c.id) map[c.id] = c; });
  _clientsShape = "map";
  const ref = db.collection("workspaces").doc(WORKSPACE_DOC);
  const payload = { clients: map, updatedAt: Date.now() };
  if (template && typeof template === "object") payload.template = template;
  if (profile && typeof profile === "object") payload.profile = profile;
  // set({merge:true}) deep-merges the clients MAP — keys present in `map` are
  // added/updated; cloud clients NOT in `map` are left untouched. No more wipes.
  return ref.set(payload, { merge: true });
}

// Force-flush every pending write immediately. Used on tab hide / unload and
// by the manual "Force sync to cloud" button. Returns a promise that resolves
// when all in-flight writes complete.
function flushAllWorkspaceWrites() {
  const keys = Object.keys(_pendingWrites);
  const promises = keys.map(k => _flushWrite(k)).filter(Boolean);
  Object.keys(_pendingClients).forEach(id => { const p = _flushClient(id); if (p) promises.push(p); });
  return Promise.all(promises);
}

// Build a URL-friendly slug from a company name, e.g. "KGP Consultants" → "kgp-consultants"
function clientSlug(company) {
  return String(company || "").toLowerCase().normalize("NFKD").replace(/[̀-ͯ]/g, "").replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "") || "client";
}

// Find a client by slug OR by id (so old links keep working)
function findClientByKey(clients, key) {
  if (!key) return null;
  const k = String(key).toLowerCase();
  return clients.find(c => c.id === key) || clients.find(c => clientSlug(c.company) === k) || null;
}

// ─────────────────────────────────────────────────────────────────────
// Brevo (Sendinblue) transactional email — invoice reminders
// ─────────────────────────────────────────────────────────────────────
function loadBrevoConfig() {
  // Keys from config.local.js take priority; anything entered in the UI
  // (saved to localStorage) fills in or overrides per field.
  const fromFile = (window.RELAY_CONFIG && window.RELAY_CONFIG.brevo) || {};
  const base = {
    apiKey: fromFile.apiKey || "",
    senderName: fromFile.senderName || "",
    senderEmail: fromFile.senderEmail || "",
    proxyUrl: fromFile.proxyUrl || "",
    reminders: { beforeDue: true, onDue: true, afterDue: true },
    log: [],
  };
  const saved = lsLoad("brevo", null);
  if (!saved) return base;
  return {
    ...base, ...saved,
    apiKey: saved.apiKey || base.apiKey,
    senderName: saved.senderName || base.senderName,
    senderEmail: saved.senderEmail || base.senderEmail,
    proxyUrl: saved.proxyUrl || base.proxyUrl,
    reminders: { ...base.reminders, ...(saved.reminders || {}) },
  };
}
function saveBrevoConfig(cfg) { lsSave("brevo", cfg); }

// Sends an email through Brevo. Two modes:
//  • PRODUCTION (published site): if brevo.proxyUrl is set, POST to your Firebase
//    Function — the secret API key lives server-side, never in the browser.
//  • LOCAL DEV: if no proxyUrl, call Brevo directly using the apiKey from config.
// Returns { ok, messageId } or throws.
async function sendBrevoEmail({ apiKey, senderName, senderEmail, toEmail, toName, subject, htmlContent }) {
  const proxyUrl = (window.RELAY_CONFIG && window.RELAY_CONFIG.brevo && window.RELAY_CONFIG.brevo.proxyUrl) || "";

  if (proxyUrl) {
    // Secure path — key stays in the Firebase Function, not the browser
    const res = await fetch(proxyUrl, {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify({ senderName, senderEmail, toEmail, toName, subject, htmlContent }),
    });
    if (!res.ok) {
      let detail = "";
      try { detail = (await res.json()).error || ""; } catch { detail = await res.text().catch(() => ""); }
      const err = new Error(`Send failed ${res.status}: ${detail || res.statusText}`);
      err.status = res.status;
      throw err;
    }
    return res.json();
  }

  // Direct path (local development only)
  if (!apiKey) throw new Error("No Brevo API key or proxy configured");
  const res = await fetch("https://api.brevo.com/v3/smtp/email", {
    method: "POST",
    headers: {
      "api-key": apiKey,
      "Content-Type": "application/json",
      "accept": "application/json",
    },
    body: JSON.stringify({
      sender: { name: senderName || "TrendVibe Creatives", email: senderEmail },
      to: [{ email: toEmail, name: toName || toEmail }],
      subject,
      htmlContent,
    }),
  });
  if (!res.ok) {
    let detail = "";
    try { detail = (await res.json()).message || ""; } catch { detail = await res.text().catch(() => ""); }
    const err = new Error(`Brevo error ${res.status}: ${detail || res.statusText}`);
    err.status = res.status;
    throw err;
  }
  return res.json();
}

// Builds a polished reminder email for an unpaid invoice.
// kind: "before" (due soon) | "due" (due today) | "after" (overdue)
function buildReminderEmail({ client, freelancer, amount, currencyCode, dueDate, invoiceNo, payLink, kind }) {
  const first = (client.contact || "there").split(" ")[0];
  const money = fmtMoney(amount, currencyCode || "USD");
  const due = fmtDate(dueDate);
  const accent = freelancer.accent || "#1B4332";

  const copy = {
    before: {
      subject: `Reminder: invoice ${invoiceNo} for ${money} is due ${due}`,
      lead: `Just a friendly heads-up that your invoice is due on <strong>${due}</strong>.`,
      tone: "No rush — this is just so it doesn't slip by.",
    },
    due: {
      subject: `Invoice ${invoiceNo} for ${money} is due today`,
      lead: `Your invoice for <strong>${money}</strong> is due <strong>today</strong>.`,
      tone: "Whenever you get a moment today is perfect.",
    },
    after: {
      subject: `Past due: invoice ${invoiceNo} for ${money}`,
      lead: `Your invoice for <strong>${money}</strong> was due on <strong>${due}</strong> and is now past due.`,
      tone: "If you've already sent it, please ignore this — and thank you!",
    },
  }[kind] || {};

  const payButton = payLink
    ? `<a href="${payLink}" style="display:inline-block;background:${accent};color:#fff;text-decoration:none;padding:12px 22px;border-radius:8px;font-weight:600;font-size:15px;margin:18px 0;">Pay ${money} now →</a>`
    : `<div style="margin:18px 0;padding:12px 16px;background:#F4F4F0;border-radius:8px;font-size:14px;">Open your portal to pay: ${money}</div>`;

  const html = `
  <div style="font-family:-apple-system,Segoe UI,Roboto,sans-serif;max-width:560px;margin:0 auto;color:#0F1A14;line-height:1.6;">
    <div style="padding:24px 0;border-bottom:2px solid ${accent};">
      <span style="font-size:20px;font-weight:700;color:${accent};">${freelancer.brand || "Relay"}</span>
    </div>
    <div style="padding:24px 0;">
      <p>Hi ${first},</p>
      <p>${copy.lead}</p>
      <table style="width:100%;border-collapse:collapse;margin:16px 0;font-size:14px;">
        <tr><td style="padding:6px 0;color:#5B635E;">Invoice</td><td style="padding:6px 0;text-align:right;font-weight:600;">${invoiceNo}</td></tr>
        <tr><td style="padding:6px 0;color:#5B635E;">Amount</td><td style="padding:6px 0;text-align:right;font-weight:600;">${money}</td></tr>
        <tr><td style="padding:6px 0;color:#5B635E;">Due date</td><td style="padding:6px 0;text-align:right;font-weight:600;">${due}</td></tr>
      </table>
      ${payButton}
      <p style="color:#5B635E;font-size:13px;">${copy.tone}</p>
      <p style="margin-top:20px;">Thanks,<br/>${freelancer.name}<br/><span style="color:#5B635E;font-size:13px;">${freelancer.title || ""} · ${freelancer.brand || ""}</span></p>
    </div>
    <div style="padding:16px 0;border-top:1px solid #E5E5DF;color:#8B928D;font-size:12px;">
      This reminder was sent automatically by ${freelancer.brand || "Relay"}. Reply to this email to reach ${freelancer.name.split(" ")[0]} directly.
    </div>
  </div>`;

  return { subject: copy.subject, html };
}

// Builds a friendly "your portal is ready" nudge email — no invoice content.
// Optional: note (personal message inserted above the standard copy),
//           subjectOverride (replaces the default subject line).
function buildPortalNudgeEmail({ client, freelancer, note, subjectOverride }) {
  const first = (client.contact || "there").split(" ")[0];
  const brand = freelancer.brand || "TrendVibe Creatives";
  const accent = freelancer.accent || "#1B4332";
  const portalUrl = generatePortalLink(client.company);
  const subject = subjectOverride || `Your ${brand} portal is ready — here's your link`;
  const noteHtml = note ? `<p style="margin:0 0 16px;font-size:15px;">${String(note).replace(/\n/g, "<br/>")}</p>` : "";
  const html = `
  <div style="font-family:-apple-system,Segoe UI,Roboto,sans-serif;max-width:560px;margin:0 auto;color:#0F1A14;line-height:1.6;">
    <div style="padding:24px 0;border-bottom:2px solid ${accent};">
      <span style="font-size:20px;font-weight:700;color:${accent};">${brand}</span>
    </div>
    <div style="padding:28px 0;">
      <p style="margin:0 0 16px;font-size:15px;">Hi ${first},</p>
      ${noteHtml}
      <p style="margin:0 0 16px;font-size:15px;">${note ? "Your onboarding portal is all set and ready whenever you get a chance." : "Just a quick note — your onboarding portal is all set and ready whenever you get a chance to take a look."}</p>
      <div style="margin:24px 0;text-align:center;">
        <a href="${portalUrl}" style="display:inline-block;background:${accent};color:#fff;text-decoration:none;padding:13px 26px;border-radius:8px;font-weight:600;font-size:15px;">Open your portal →</a>
      </div>
      <div style="background:#F4F4F0;border-radius:8px;padding:14px 16px;margin:20px 0;font-size:13px;">
        <div style="color:#5B635E;margin-bottom:4px;">Access code</div>
        <div style="font-weight:600;font-size:16px;letter-spacing:1px;">trendvibe2026</div>
      </div>
      <p style="margin:16px 0 0;font-size:14px;color:#5B635E;">Reply to this email if you have any questions — happy to help.</p>
    </div>
    <div style="padding:16px 0;border-top:1px solid #E8EDE9;font-size:12px;color:#5B635E;">
      ${freelancer.name ? `${freelancer.name} · ` : ""}${brand}
    </div>
  </div>`;
  return { subject, html };
}

// Builds a fresh invoice email (not a reminder) for a specific billing row.
// Default editable copy for the invoice email. Tokens: {first} {period}
// {amount} {due} {invoice} {brand}. Donna can override these per client.
const INVOICE_EMAIL_DEFAULTS = {
  subject: "Invoice {invoice} — {amount} due {due}",
  intro:   "Here is your invoice for {dates} with {brand}.",
  closing: "Thank you — looking forward to another great week!",
};

function buildInvoiceEmail({ client, freelancer, rowLabel, amount, currencyCode, dueDate, invoiceNo, payLink, custom, periodRange }) {
  const first = (client.contact || "there").split(" ")[0];
  const money = fmtMoney(amount, currencyCode || "USD");
  const due = fmtDate(dueDate);
  const dates = periodRange || rowLabel || "";
  const brand = freelancer.brand || "TrendVibe Creatives";
  const accent = freelancer.accent || "#1B4332";
  const c = custom || {};

  // Plain-text token fill (for the subject line).
  const fillPlain = (s) => String(s || "")
    .replace(/\{first\}/g, first).replace(/\{period\}/g, rowLabel || "").replace(/\{dates\}/g, dates)
    .replace(/\{amount\}/g, money).replace(/\{due\}/g, due)
    .replace(/\{invoice\}/g, invoiceNo).replace(/\{brand\}/g, brand);
  // HTML-safe token fill (for body text she typed) — escape first, then inject
  // tokens, bolding the emphasized ones. Prevents her text from breaking layout.
  const esc = (s) => String(s || "").replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
  const fillHtml = (s) => esc(s)
    .replace(/\{first\}/g, esc(first))
    .replace(/\{period\}/g, `<strong>${esc(rowLabel || "")}</strong>`)
    .replace(/\{dates\}/g, `<strong>${esc(dates)}</strong>`)
    .replace(/\{amount\}/g, `<strong>${money}</strong>`)
    .replace(/\{due\}/g, esc(due))
    .replace(/\{invoice\}/g, esc(invoiceNo))
    .replace(/\{brand\}/g, esc(brand))
    .replace(/\n/g, "<br/>");

  const subject = fillPlain(c.subject || INVOICE_EMAIL_DEFAULTS.subject);
  const introHtml = fillHtml(c.intro || INVOICE_EMAIL_DEFAULTS.intro);
  const closingHtml = fillHtml(c.closing || INVOICE_EMAIL_DEFAULTS.closing);

  const payButton = payLink
    ? `<a href="${payLink}" style="display:inline-block;background:${accent};color:#fff;text-decoration:none;padding:12px 22px;border-radius:8px;font-weight:600;font-size:15px;margin:18px 0;">Pay ${money} now →</a>`
    : `<div style="margin:18px 0;padding:12px 16px;background:#F4F4F0;border-radius:8px;font-size:14px;">Please send payment of ${money} to your invoice portal.</div>`;
  const html = `
  <div style="font-family:-apple-system,Segoe UI,Roboto,sans-serif;max-width:560px;margin:0 auto;color:#0F1A14;line-height:1.6;">
    <div style="padding:24px 0;border-bottom:2px solid ${accent};">
      <span style="font-size:20px;font-weight:700;color:${accent};">${brand}</span>
    </div>
    <div style="padding:24px 0;">
      <p>Hi ${esc(first)},</p>
      <p>${introHtml}</p>
      <table style="width:100%;border-collapse:collapse;margin:16px 0;font-size:14px;">
        <tr><td style="padding:6px 0;color:#5B635E;">Invoice</td><td style="padding:6px 0;text-align:right;font-weight:600;">${invoiceNo}</td></tr>
        <tr><td style="padding:6px 0;color:#5B635E;">Amount</td><td style="padding:6px 0;text-align:right;font-weight:600;">${money}</td></tr>
        <tr><td style="padding:6px 0;color:#5B635E;">Due date</td><td style="padding:6px 0;text-align:right;font-weight:600;">${due}</td></tr>
      </table>
      ${payButton}
      <p style="color:#5B635E;font-size:13px;">${closingHtml}</p>
      <p style="margin-top:20px;">Thanks,<br/>${freelancer.name}<br/><span style="color:#5B635E;font-size:13px;">${freelancer.title || ""} · ${freelancer.brand || ""}</span></p>
    </div>
    <div style="padding:16px 0;border-top:1px solid #E5E5DF;color:#8B928D;font-size:12px;">
      Sent by ${brand}. Reply to this email with any questions.
    </div>
  </div>`;
  return { subject, htmlContent: html };
}

// ─────────────────────────────────────────────────────────────────────
// Printable document generator — opens a real Agreement / SOW / NDA /
// Invoice in a new window the client can print or save as PDF.
// ─────────────────────────────────────────────────────────────────────
// Which docs support free-text per-client editing (data-driven ones don't)
const EDITABLE_DOCS = ["service_agreement", "sow", "nda", "welcome_packet"];
const DOC_TITLES = { service_agreement: "Services Agreement", agreement: "Services Agreement", sow: "Statement of Work", nda: "Mutual NDA", invoice: "Invoice", payment_schedule: "Payment Schedule", welcome_packet: "Welcome Packet" };

// Plain-text version of a doc (for the editor). Use "# Heading" for sections,
// "- item" for bullets. Blank lines separate paragraphs.
function buildDocText({ docKey, client, freelancer, template }) {
  const fd = (d) => { try { return new Date(d).toLocaleDateString("en-US", { month: "long", day: "numeric", year: "numeric" }); } catch { return d; } };
  const today = fd(new Date()), start = fd(client.startDate);
  const fill = (s) => String(s || "")
    .replace(/\{\{date\}\}/g, today).replace(/\{\{rate\}\}/g, client.rateLabel || "")
    .replace(/\{\{start_date\}\}/g, start).replace(/\{\{client\.company\}\}/g, client.company || "")
    .replace(/\{\{client\.contact\}\}/g, client.contact || "").replace(/\{\{freelancer\.name\}\}/g, freelancer.name || "");
  if (docKey === "service_agreement" || docKey === "agreement") {
    const clauses = ((template && template.agreement_clauses) || []).map((c, i) => `# ${i + 1}. ${c.label}\n${fill(c.body)}`).join("\n\n");
    return `This agreement is entered into on ${today} by and between ${freelancer.name}, doing business as ${freelancer.brand}, an independent contractor ("Contractor"), and ${client.company} ("Client"), regarding the services described in Exhibit A — Statement of Work.\n\n${clauses}`;
  }
  if (docKey === "sow") {
    return `# Parties\nService Provider: ${freelancer.name} d/b/a ${freelancer.brand}\nClient: ${client.company} (${client.contact}, ${client.role || "primary contact"})\n\n# Engagement\n${client.role_title} — ${(client.engagementType || "").toLowerCase()}.\n\n# Deliverables\n${(client.deliverables || []).map(d => "- " + d).join("\n")}\n\n# Timeline\n${(client.milestones || []).map(m => "- " + m.name + " — " + fd(m.date)).join("\n")}\n\n# Compensation\n${client.rateLabel}.`;
  }
  if (docKey === "nda") {
    return `${freelancer.name} ("Contractor") and ${client.company} ("Client") agree to keep confidential all non-public information shared in connection with the engagement.\n\n# Definition\n"Confidential Information" means any business, technical, financial, customer or product information that is marked or reasonably understood to be confidential.\n\n# Obligations\nEach party will use Confidential Information only to perform under the engagement, protect it with reasonable care, and limit access to people with a need to know.\n\n# Exceptions\nDoes not include information that is public, independently developed, or rightfully received from a third party.\n\n# Term\nThis NDA survives termination of any underlying engagement in perpetuity.`;
  }
  if (docKey === "welcome_packet") {
    return `Hi ${(client.contact || "there").split(" ")[0]}, I'm thrilled to be working with ${client.company}.\n\n# What we're doing\n${client.role_title} — ${client.engagementType}. ${client.rateLabel}.\n\n# Deliverables\n${(client.deliverables || []).map(d => "- " + d).join("\n")}\n\n# How to reach me\n${freelancer.name} · ${freelancer.email} · replies within 4 hours.`;
  }
  return "";
}

function _escHtml(s) { return String(s).replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;"); }
function _docTextToHTML(text) {
  const lines = String(text).replace(/\r/g, "").split("\n");
  let html = "", para = [], list = [];
  const flushPara = () => { if (para.length) { html += "<p>" + para.map(_escHtml).join("<br>") + "</p>"; para = []; } };
  const flushList = () => { if (list.length) { html += "<ul>" + list.map(li => "<li>" + _escHtml(li) + "</li>").join("") + "</ul>"; list = []; } };
  for (const raw of lines) {
    const line = raw.replace(/\s+$/, "");
    if (/^#\s+/.test(line)) { flushPara(); flushList(); html += "<h2>" + _escHtml(line.replace(/^#\s+/, "")) + "</h2>"; }
    else if (/^-\s+/.test(line)) { flushPara(); list.push(line.replace(/^-\s+/, "")); }
    else if (line.trim() === "") { flushPara(); flushList(); }
    else { flushList(); para.push(line); }
  }
  flushPara(); flushList();
  return html;
}

function buildDocHTML({ docKey, client, freelancer, template, forPdf }) {
  const fd = (d) => { try { return new Date(d).toLocaleDateString("en-US", { month: "long", day: "numeric", year: "numeric" }); } catch { return d; } };
  const today = fd(new Date());
  const start = fd(client.startDate);
  // Signature state — inject the client's real signature + date when they've signed.
  const _sig = client.signature || null;
  const _signedDate = _sig && _sig.signedAt ? fd(_sig.signedAt) : null;
  const _cName = _sig && _sig.name ? _escHtml(_sig.name) : "—";
  const _cBar = _signedDate ? `${_escHtml(client.contact || "")} · signed ${_signedDate}` : `${client.contact || ""} · pending`;
  const _cColor = _sig && _sig.name ? "#1B1B1B" : "#aaa";
  const _fBar = `${freelancer.name} · ${_signedDate || today}`;
  const money = (n) => "$" + Number(n || 0).toLocaleString("en-US");
  const fill = (s) => String(s || "")
    .replace(/\{\{date\}\}/g, today)
    .replace(/\{\{rate\}\}/g, client.rateLabel || "")
    .replace(/\{\{start_date\}\}/g, start)
    .replace(/\{\{client\.company\}\}/g, client.company || "")
    .replace(/\{\{client\.contact\}\}/g, client.contact || "")
    .replace(/\{\{freelancer\.name\}\}/g, freelancer.name || "");
  const disclaimer = (template && template.legal_disclaimer) || "";
  const total = client.rate || 0;
  const isRetainer = !!client.hoursPerWeek;
  const deposit = isRetainer ? total : Math.round(total * 0.5);
  const netDays = (client.billing && client.billing.netDays) || (template && template.net_days) || 7;

  // Billing-plan-aware figures (weekly/biweekly/monthly/one-time)
  const bil = client.billing;
  let firstLabel, firstAmount, scheduleRows;
  if (bil && bil.frequency) {
    if (bil.frequency === "onetime") {
      const dep = Math.round((bil.amount || 0) * ((bil.depositPct || 50) / 100));
      firstLabel = `${bil.depositPct || 50}% deposit`; firstAmount = dep;
      scheduleRows = [[`Deposit (${bil.depositPct || 50}%)`, dep], ["Final on delivery", (bil.amount || 0) - dep]];
    } else {
      const u = { weekly: "Week", biweekly: "Period", monthly: "Month" }[bil.frequency] || "Month";
      firstLabel = `${u} 1`; firstAmount = bil.amount || 0;
      scheduleRows = Array.from({ length: bil.count || 1 }, (_, i) => [`${u} ${i + 1}`, bil.amount || 0]);
    }
  } else {
    firstLabel = isRetainer ? "First period" : "50% project deposit"; firstAmount = deposit;
    scheduleRows = isRetainer ? [["Month 1", total], ["Month 2", total], ["Month 3", total]] : [["Deposit (50%)", deposit], ["Final (50%) on delivery", total - deposit]];
  }

  let title = "Document", body = "";

  // Override priority: this client's custom text → template-level custom body → generated default.
  const perClient = client.docOverrides && client.docOverrides[docKey];
  const tplDoc = template && template.documents && template.documents.find(d => d.id === docKey);
  const override = (perClient && typeof perClient.body === "string" && perClient.body.trim()) ? perClient
    : (tplDoc && typeof tplDoc.body === "string" && tplDoc.body.trim()) ? { body: tplDoc.body } : null;
  if (override) {
    title = DOC_TITLES[docKey] || "Document";
    const needsSig = docKey === "service_agreement" || docKey === "agreement" || docKey === "nda" || docKey === "sow";
    const sig = needsSig ? `
      <div style="margin-top:36px;display:grid;grid-template-columns:1fr 1fr;gap:32px;border-top:1px solid #E5E5DF;padding-top:24px">
        <div><div class="muted" style="font-size:10.5px;text-transform:uppercase;letter-spacing:.05em">Contractor</div>
          <div style="font-family:'Instrument Serif',serif;font-size:22px;font-style:italic">${freelancer.name}</div>
          <div style="border-top:1px solid #0F1A14;padding-top:6px;font-size:11.5px;color:#5B635E">${_fBar}</div></div>
        <div><div class="muted" style="font-size:10.5px;text-transform:uppercase;letter-spacing:.05em">Client</div>
          <div style="font-family:'Instrument Serif',serif;font-size:22px;font-style:italic;color:${_cColor};min-height:30px">${_cName}</div>
          <div style="border-top:1px solid #E5E5DF;padding-top:6px;font-size:11.5px;color:#5B635E">${_cBar}</div></div>
      </div>` : "";
    body = `
      <div class="row"><div><h1>${title}</h1><div class="muted">Between ${freelancer.brand} and ${client.company}</div></div>
      <div class="muted" style="text-align:right">Effective<br><strong>${start}</strong></div></div>
      ${_docTextToHTML(override.body)}${sig}`;
  } else if (docKey === "service_agreement" || docKey === "agreement") {
    title = "Service Agreement";
    const clauses = (template && template.agreement_clauses ? template.agreement_clauses.filter(c => c.id !== "law" || true) : [])
      .map((c, i) => `<h2>${i + 1}. ${c.label}</h2><p>${fill(c.body)}</p>`).join("");
    body = `
      <div class="row"><div><h1>Services Agreement</h1><div class="muted">Between ${freelancer.brand} and ${client.company}</div></div>
      <div class="muted" style="text-align:right">Effective<br><strong>${start}</strong></div></div>
      <p>This agreement ("Agreement") is entered into on ${today} by and between <strong>${freelancer.name}</strong>, doing business as <strong>${freelancer.brand}</strong>, an independent contractor ("Contractor"), and <strong>${client.company}</strong> ("Client"), regarding the services described in Exhibit A — Statement of Work.</p>
      ${clauses}
      <div class="sig"><div><div class="muted">CONTRACTOR</div><div class="sigName">${freelancer.name}</div><div class="bar">${_fBar}</div></div>
      <div><div class="muted">CLIENT</div><div class="sigName" style="color:${_cColor}">${_cName}</div><div class="bar">${_cBar}</div></div></div>`;
  } else if (docKey === "sow") {
    title = "Statement of Work";
    body = `
      <div class="row"><div><h1>Statement of Work</h1><div class="muted">Exhibit A · SOW-${clientSlug(client.company).toUpperCase()}-001</div></div>
      <div class="muted" style="text-align:right">Effective<br><strong>${start}</strong></div></div>
      <h2>Parties</h2><p><strong>Service Provider:</strong> ${freelancer.name} d/b/a ${freelancer.brand}<br><strong>Client:</strong> ${client.company} (${client.contact}, ${client.role || "primary contact"})</p>
      <h2>Engagement</h2><p>${client.role_title} — ${(client.engagementType || "").toLowerCase()}, ${isRetainer ? client.hoursPerWeek + " hours per week" : "fixed-scope project"}.</p>
      <h2>Deliverables</h2><ol>${(client.deliverables || []).map(d => `<li>${d}</li>`).join("")}</ol>
      <h2>Timeline</h2><ul>${(client.milestones || []).map(m => `<li>${m.name} — ${fd(m.date)}</li>`).join("")}</ul>
      <h2>Compensation</h2><p>${client.rateLabel}. ${isRetainer ? "Invoiced in advance, net " + ((template && template.net_days) || 7) + " days." : "50% deposit on signature; remainder upon delivery."}</p>
      <div class="sig"><div><div class="muted">CONTRACTOR</div><div class="sigName">${freelancer.name}</div><div class="bar">${_fBar}</div></div>
      <div><div class="muted">CLIENT</div><div class="sigName" style="color:${_cColor}">${_cName}</div><div class="bar">${_cBar}</div></div></div>`;
  } else if (docKey === "nda") {
    title = "Mutual NDA";
    body = `
      <h1>Mutual Non-Disclosure Agreement</h1><div class="muted">Effective ${today}</div>
      <p>${freelancer.name} ("Contractor") and ${client.company} ("Client") agree to keep confidential all non-public information shared in connection with the engagement.</p>
      <h2>Definition</h2><p>"Confidential Information" means any business, technical, financial, customer or product information that is marked or reasonably understood to be confidential, including credentials, pricing, customer lists, source code, and unreleased product plans.</p>
      <h2>Obligations</h2><p>Each party will (a) use Confidential Information only to perform under the engagement, (b) protect it with reasonable care, and (c) limit access to people with a need to know who are bound by similar obligations.</p>
      <h2>Exceptions</h2><p>Confidential Information does not include information that is publicly available, independently developed, or rightfully received from a third party.</p>
      <h2>Term</h2><p>This NDA is effective from the date above and survives termination of any underlying engagement in perpetuity.</p>
      <div class="sig"><div><div class="muted">CONTRACTOR</div><div class="sigName">${freelancer.name}</div><div class="bar">${_fBar}</div></div>
      <div><div class="muted">CLIENT</div><div class="sigName" style="color:${_cColor}">${_cName}</div><div class="bar">${_cBar}</div></div></div>`;
  } else if (docKey === "invoice") {
    title = "Invoice";
    body = `
      <div class="row"><div><h1>Invoice</h1><div class="muted">#INV-${(client.id || "").toUpperCase()}-001</div></div>
      <div class="muted" style="text-align:right">${freelancer.brand}</div></div>
      <table style="margin-top:20px"><tr><td><strong>From</strong><br>${freelancer.name}<br>${freelancer.email}</td><td><strong>Bill to</strong><br>${client.company}<br>Attn: ${client.contact}<br>${client.email}</td></tr>
      <tr><td>Issued: ${today}</td><td>Due: ${start}</td></tr></table>
      <table style="margin-top:24px"><tr><th>Description</th><th class="right">Amount</th></tr>
      <tr><td>${firstLabel} — ${client.role_title}</td><td class="right">${money(firstAmount)}</td></tr>
      <tr><td><strong>Total due</strong></td><td class="right"><strong>${money(firstAmount)}</strong></td></tr></table>
      ${(bil && bil.payLink) ? `<p style="margin-top:20px"><a href="${bil.payLink}" style="display:inline-block;background:#1B4332;color:#fff;text-decoration:none;padding:10px 20px;border-radius:6px;font-weight:600;">Pay ${money(firstAmount)} online →</a></p>` : ""}
      <p class="muted" style="margin-top:24px">Payable via card, PayPal, or bank transfer. Net ${netDays} days. Reference the invoice number on transfers.</p>`;
  } else if (docKey === "payment_schedule") {
    title = "Payment Schedule";
    body = `<h1>Payment Schedule</h1><div class="muted">${client.company} · ${client.rateLabel}</div>
      <table style="margin-top:20px"><tr><th>Milestone</th><th class="right">Amount</th></tr>
      ${scheduleRows.map(r => `<tr><td>${r[0]}</td><td class="right">${money(r[1])}</td></tr>`).join("")}
      <tr><td><strong>Total</strong></td><td class="right"><strong>${money(scheduleRows.reduce((s, r) => s + r[1], 0))}</strong></td></tr></table>
      <p class="muted" style="margin-top:16px">Terms: Net ${netDays} days${(bil && bil.lateFee) ? ` · ${bil.lateFee}% late fee / month` : ""}.</p>`;
  } else if (docKey === "welcome_packet") {
    title = "Welcome Packet";
    body = `<h1>Welcome to ${freelancer.brand}</h1><p>Hi ${(client.contact || "there").split(" ")[0]}, I'm thrilled to be working with ${client.company}.</p>
      <h2>What we're doing</h2><p>${client.role_title} — ${client.engagementType}. ${client.rateLabel}.</p>
      <h2>Deliverables</h2><ul>${(client.deliverables || []).map(d => `<li>${d}</li>`).join("")}</ul>
      <h2>How to reach me</h2><p>${freelancer.name} · ${freelancer.email} · replies within 4 hours.</p>`;
  } else {
    title = "Document";
    body = `<h1>${docKey}</h1><p>This document will be generated here.</p>`;
  }

  const stampHtml = _sig ? `<p class="muted" style="margin-top:28px;border-top:1px solid #eee;padding-top:14px;font-size:10.5px;">Electronically signed by ${_cName} on ${_signedDate}${_sig.tz ? ` (${_escHtml(_sig.tz)})` : ""}. Recorded via the ${freelancer.brand} onboarding portal.</p>` : "";
  const discHtml = disclaimer ? `<p class="muted" style="margin-top:${_sig ? "12px" : "36px"};${_sig ? "" : "border-top:1px solid #eee;padding-top:16px;"}font-size:10.5px;">${disclaimer}</p>` : "";

  // PDF mode: a self-contained, scoped fragment (no toolbar) that html2pdf can render.
  if (forPdf) {
    return `<style>
      .rdoc{font-family:Georgia,'Times New Roman',serif;color:#1B1B1B;max-width:720px;margin:0 auto;padding:8px 16px;line-height:1.7;font-size:13px;}
      .rdoc h1{font-size:26px;margin:0 0 6px;}
      .rdoc h2{font-size:11.5px;text-transform:uppercase;letter-spacing:.08em;color:#5B635E;margin:22px 0 8px;}
      .rdoc p{margin:0 0 12px;} .rdoc ol,.rdoc ul{margin:0 0 12px;padding-left:20px;} .rdoc li{margin-bottom:4px;}
      .rdoc .muted{color:#5B635E;font-size:12px;}
      .rdoc .row{display:flex;justify-content:space-between;align-items:flex-start;gap:24px;margin-bottom:20px;}
      .rdoc .sig{margin-top:36px;display:grid;grid-template-columns:1fr 1fr;gap:32px;border-top:1px solid #ccc;padding-top:20px;}
      .rdoc .sigName{font-style:italic;font-size:22px;margin:6px 0;}
      .rdoc .bar{border-top:1px solid #000;padding-top:6px;font-size:11px;color:#5B635E;}
      .rdoc table{width:100%;border-collapse:collapse;font-size:12px;} .rdoc td,.rdoc th{padding:8px 0;border-bottom:1px solid #eee;text-align:left;vertical-align:top;}
      .rdoc .right{text-align:right;}
    </style><div class="rdoc">${body}${stampHtml}${discHtml}</div>`;
  }

  return `<!doctype html><html><head><meta charset="utf-8"><title>${title} — ${freelancer.brand}</title>
  <style>
    @page{margin:1in;}
    body{font-family:Georgia,'Times New Roman',serif;color:#1B1B1B;max-width:720px;margin:0 auto;padding:64px 32px 48px;line-height:1.7;font-size:13px;}
    h1{font-size:28px;margin:0 0 6px;}
    h2{font-size:11.5px;text-transform:uppercase;letter-spacing:.08em;color:#5B635E;margin:24px 0 8px;}
    p{margin:0 0 12px;} ol,ul{margin:0 0 12px;padding-left:20px;} li{margin-bottom:4px;}
    .muted{color:#5B635E;font-size:12px;}
    .row{display:flex;justify-content:space-between;align-items:flex-start;gap:24px;margin-bottom:20px;}
    .sig{margin-top:40px;display:grid;grid-template-columns:1fr 1fr;gap:32px;border-top:1px solid #ccc;padding-top:20px;}
    .sigName{font-style:italic;font-size:22px;margin:6px 0;}
    .bar{border-top:1px solid #000;padding-top:6px;font-size:11px;color:#5B635E;}
    table{width:100%;border-collapse:collapse;font-size:12px;} td,th{padding:8px 0;border-bottom:1px solid #eee;text-align:left;vertical-align:top;}
    .right{text-align:right;}
    .toolbar{position:fixed;top:0;left:0;right:0;background:#0F1A14;color:#fff;padding:10px 16px;font-family:ui-sans-serif,system-ui,sans-serif;font-size:13px;display:flex;justify-content:space-between;align-items:center;}
    .toolbar button{background:#fff;color:#0F1A14;border:0;padding:7px 16px;border-radius:6px;font-weight:600;cursor:pointer;}
    @media print{.toolbar,.spacer{display:none!important;} body{padding:0;}}
  </style></head><body>
  <div class="toolbar"><span>${title} · ${client.company}</span><button onclick="window.print()">⬇ Save as PDF / Print</button></div>
  <div class="spacer" style="height:40px"></div>
  ${body}
  ${stampHtml}
  ${discHtml}
  </body></html>`;
}

// Returns just the inner body HTML for a doc override (for inline React rendering).
// Returns null when no per-client override exists so components fall back to their default rendering.
function buildDocInlineHTML({ docKey, client, freelancer, template }) {
  const perClient = client.docOverrides && client.docOverrides[docKey];
  if (!perClient || typeof perClient.body !== "string" || !perClient.body.trim()) return null;
  const fd = (d) => { try { return new Date(d).toLocaleDateString("en-US", { month: "long", day: "numeric", year: "numeric" }); } catch { return d; } };
  const today = fd(new Date()), start = fd(client.startDate);
  const fill = (s) => String(s || "")
    .replace(/\{\{date\}\}/g, today).replace(/\{\{rate\}\}/g, client.rateLabel || "")
    .replace(/\{\{start_date\}\}/g, start).replace(/\{\{client\.company\}\}/g, client.company || "")
    .replace(/\{\{client\.contact\}\}/g, client.contact || "").replace(/\{\{freelancer\.name\}\}/g, freelancer.name || "");
  return fill(_docTextToHTML(perClient.body));
}

// ─────────────────────────────────────────────────────────────────────
// Real calendar invites — .ics file + Google Calendar link (zero backend)
// ─────────────────────────────────────────────────────────────────────
function _pad2(n) { return String(n).padStart(2, "0"); }
function _icsStamp(d) {
  return d.getUTCFullYear() + _pad2(d.getUTCMonth() + 1) + _pad2(d.getUTCDate())
    + "T" + _pad2(d.getUTCHours()) + _pad2(d.getUTCMinutes()) + "00Z";
}
// Combine a base Date with a "10:00 AM" style string → a real Date
function combineDateTime(baseDate, timeStr) {
  const d = new Date(baseDate);
  const m = String(timeStr || "").match(/(\d+):(\d+)\s*(AM|PM)/i);
  if (m) {
    let h = parseInt(m[1], 10); const min = parseInt(m[2], 10); const ap = m[3].toUpperCase();
    if (ap === "PM" && h !== 12) h += 12;
    if (ap === "AM" && h === 12) h = 0;
    d.setHours(h, min, 0, 0);
  }
  return d;
}
function buildICS({ title, description, start, durationMin = 30, location, organizerName, organizerEmail }) {
  const end = new Date(start.getTime() + durationMin * 60000);
  const uid = "relay-" + start.getTime() + "@relayhq";
  const lines = [
    "BEGIN:VCALENDAR", "VERSION:2.0", "PRODID:-//Relay//Onboarding//EN", "CALSCALE:GREGORIAN", "METHOD:PUBLISH",
    "BEGIN:VEVENT",
    "UID:" + uid,
    "DTSTAMP:" + _icsStamp(new Date()),
    "DTSTART:" + _icsStamp(start),
    "DTEND:" + _icsStamp(end),
    "SUMMARY:" + (title || "Kickoff call"),
    "DESCRIPTION:" + String(description || "").replace(/\n/g, "\\n"),
    location ? "LOCATION:" + location : "",
    organizerEmail ? ("ORGANIZER;CN=" + (organizerName || "") + ":mailto:" + organizerEmail) : "",
    "STATUS:CONFIRMED",
    "BEGIN:VALARM", "TRIGGER:-PT30M", "ACTION:DISPLAY", "DESCRIPTION:Reminder", "END:VALARM",
    "END:VEVENT", "END:VCALENDAR",
  ].filter(Boolean);
  return lines.join("\r\n");
}
function downloadICS(filename, ics) {
  const blob = new Blob([ics], { 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(); document.body.removeChild(a);
  setTimeout(() => URL.revokeObjectURL(url), 1000);
}
function googleCalUrl({ title, details, start, durationMin = 30, location }) {
  const end = new Date(start.getTime() + durationMin * 60000);
  const params = new URLSearchParams({
    action: "TEMPLATE",
    text: title || "Kickoff call",
    dates: _icsStamp(start) + "/" + _icsStamp(end),
    details: details || "",
    location: location || "",
  });
  return "https://calendar.google.com/calendar/render?" + params.toString();
}
function loadSchedulingConfig() {
  const fromFile = (window.RELAY_CONFIG && window.RELAY_CONFIG.scheduling) || {};
  return { bookingUrl: fromFile.bookingUrl || "" };
}

// Fire-and-forget admin notification when a client hits a milestone in the portal.
// Routes through the secure /api/send-email Cloud Function (Brevo key never in browser).
// Deduped via client.notified[event] — caller updates the client after success.
const EVENT_COPY = {
  opened: { subject: "opened their portal",  line: "just opened their onboarding portal."  },
  intake: { subject: "submitted the intake form", line: "just submitted their intake form." },
  signed: { subject: "signed the agreement", line: "just signed the Service Agreement, SOW, and NDA." },
  paid:   { subject: "paid the deposit",     line: "just marked the deposit invoice as paid." },
};

// Render the signature record into an HTML evidence block. This block goes into
// the "signed" notification email so it doubles as your archived proof.
function _signatureEvidenceHtml(sig) {
  if (!sig) return "";
  const when = (() => { try { return new Date(sig.signedAt).toLocaleString("en-US", { dateStyle: "long", timeStyle: "short" }); } catch { return sig.signedAt; } })();
  const docs = (sig.docsAccepted || []).map(d => ({ agreement: "Service Agreement", sow: "Statement of Work", nda: "Mutual NDA" }[d] || d)).join(", ");
  return `
    <div style="margin-top:22px;padding:16px 18px;border:1px solid #E5E5DF;border-radius:10px;background:#FBFBF7;font-family:system-ui,sans-serif">
      <div style="font-size:10.5px;text-transform:uppercase;letter-spacing:0.06em;color:#5B635E;font-weight:600;margin-bottom:10px">Signature record</div>
      <table style="width:100%;border-collapse:collapse;font-size:13px;color:#0F1A14">
        <tr><td style="padding:5px 14px 5px 0;color:#5B635E;width:120px">Typed name</td><td style="padding:5px 0;font-weight:500;font-family:'Instrument Serif',serif;font-style:italic;font-size:18px">${sig.name || ""}</td></tr>
        <tr><td style="padding:5px 14px 5px 0;color:#5B635E">Signed at</td><td style="padding:5px 0">${when}</td></tr>
        <tr><td style="padding:5px 14px 5px 0;color:#5B635E">Documents</td><td style="padding:5px 0">${docs}</td></tr>
        <tr><td style="padding:5px 14px 5px 0;color:#5B635E">Timezone</td><td style="padding:5px 0">${sig.tz || "—"}</td></tr>
        <tr><td style="padding:5px 14px 5px 0;color:#5B635E;vertical-align:top">Device</td><td style="padding:5px 0;font-size:11px;color:#5B635E;word-break:break-all">${sig.userAgent || "—"}</td></tr>
      </table>
      <div style="margin-top:14px;font-size:11px;color:#8B928D">Keep this email as your signed-record archive. The same data is also stored on the client's record in your admin.</div>
    </div>`;
}
async function notifyAdminOfClientEvent({ client, event, freelancer }) {
  const copy = EVENT_COPY[event];
  if (!copy) return false;
  // Notifications send TO and FROM the verified Brevo sender domain. Brevo will
  // silently drop messages from any other sender, so we always use mail@…com.
  const cfg = (window.RELAY_CONFIG && window.RELAY_CONFIG.notifyEmail) || "mail@trendvibecreatives.com";
  const fromName  = (freelancer && freelancer.brand) || "TrendVibe Creatives";
  const fromEmail = "mail@trendvibecreatives.com";
  const portalLink = generatePortalLink(client.company);
  const subject = `${client.company} — ${client.contact} ${copy.subject}`;
  const evidence = event === "signed" ? _signatureEvidenceHtml(client.signature) : "";
  const htmlContent = `
    <div style="font-family:system-ui,sans-serif;font-size:14px;line-height:1.55;color:#0F1A14;max-width:560px">
      <p><strong>${client.contact}</strong> (${client.company}) ${copy.line}</p>
      ${evidence}
      <p style="color:#5B635E;font-size:13px;margin:18px 0 0">
        <a href="${portalLink}" style="color:#1B4332">View ${client.company}'s portal →</a>
      </p>
      <hr style="border:0;border-top:1px solid #E5E5DF;margin:24px 0"/>
      <p style="color:#8B928D;font-size:11.5px;margin:0">Sent automatically by your Relay onboarding portal.</p>
    </div>`;
  try {
    const r = await fetch("/api/send-email", {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify({
        senderName: fromName, senderEmail: fromEmail,
        toEmail: cfg, toName: fromName,
        subject, htmlContent,
      }),
    });
    return r.ok;
  } catch (e) { console.warn("notifyAdmin failed", e); return false; }
}

// ─────────────────────────────────────────────────────────────────────
// Signed-document PDFs → Firebase Storage → emailed as real attachments
// ─────────────────────────────────────────────────────────────────────
let _fsStorage = null;
function getStorage() {
  if (_fsStorage) return _fsStorage;
  const cfg = window.RELAY_CONFIG && window.RELAY_CONFIG.firebase;
  if (!cfg || !window.firebase || !window.firebase.storage) return null;
  try {
    if (!window.firebase.apps.length) window.firebase.initializeApp(cfg);
    _fsStorage = window.firebase.storage();
    return _fsStorage;
  } catch (e) { console.warn("Storage init failed", e); return null; }
}

async function _docToPdfBlob({ docKey, client, freelancer, template }) {
  if (!window.html2pdf) throw new Error("PDF library not loaded — refresh the page and retry.");
  const html = buildDocHTML({ docKey, client, freelancer, template, forPdf: true });
  const opt = {
    margin: [0.6, 0.55, 0.6, 0.55],
    image: { type: "jpeg", quality: 0.98 },
    html2canvas: { scale: 2, useCORS: true, backgroundColor: "#ffffff" },
    jsPDF: { unit: "in", format: "letter", orientation: "portrait" },
    pagebreak: { mode: ["css", "legacy"] },
  };
  return await window.html2pdf().set(opt).from(html, "string").outputPdf("blob");
}

// Generate signed PDFs for the executed docs, archive them to Storage, and email
// them to the client as real attachments. Returns [{ name, url }].
async function emailSignedDocs({ client, freelancer, template, docKeys, onProgress }) {
  const storage = getStorage();
  if (!storage) throw new Error("Storage isn't available (sign in as admin and retry).");
  const keys = docKeys || ["service_agreement", "sow", "nda"];
  const stamp = Date.now();
  const attachments = [];
  for (const key of keys) {
    if (onProgress) onProgress(`Rendering ${DOC_TITLES[key] || key}…`);
    const blob = await _docToPdfBlob({ docKey: key, client, freelancer, template });
    const safe = (DOC_TITLES[key] || key).replace(/[^a-z0-9]+/gi, "-");
    const fileName = `${safe}-${clientSlug(client.company)}.pdf`;
    const path = `signed-docs/${client.id}/${stamp}-${fileName}`;
    const ref = storage.ref().child(path);
    await ref.put(blob, { contentType: "application/pdf" });
    const url = await ref.getDownloadURL();
    attachments.push({ name: fileName, url, key });
  }
  if (onProgress) onProgress("Sending email…");
  const first = (client.contact || "there").split(" ")[0];
  const brand = freelancer.brand || "TrendVibe Creatives";
  const html = `
    <div style="font-family:system-ui,sans-serif;font-size:14px;line-height:1.6;color:#0F1A14;max-width:560px;">
      <p>Hi ${first},</p>
      <p>Thank you for signing. Attached are your fully-executed documents with ${brand}:</p>
      <ul>${attachments.map(a => `<li>${a.name}</li>`).join("")}</ul>
      <p>Please keep these for your records. Reply to this email with any questions.</p>
      <p style="margin-top:18px;">Thanks,<br/>${freelancer.name}<br/><span style="color:#5B635E;font-size:13px;">${freelancer.title || ""} · ${brand}</span></p>
    </div>`;
  const r = await fetch("/api/send-email", {
    method: "POST", headers: { "Content-Type": "application/json" },
    body: JSON.stringify({
      senderName: brand, senderEmail: "mail@trendvibecreatives.com",
      toEmail: client.email, toName: client.contact,
      subject: `Your signed documents with ${brand}`,
      htmlContent: html, attachments: attachments.map(a => ({ name: a.name, url: a.url })),
    }),
  });
  if (!r.ok) { const e = await r.json().catch(() => ({})); throw new Error(e.error || "Email failed to send"); }
  return attachments;
}

Object.assign(window, {
  lsClear, lsLoad, lsSave, loadBrevoConfig, saveBrevoConfig, sendBrevoEmail, buildReminderEmail,
  combineDateTime, buildICS, downloadICS, googleCalUrl, loadSchedulingConfig, buildDocHTML, buildDocText, buildDocInlineHTML, buildInvoiceEmail, buildPortalNudgeEmail, INVOICE_EMAIL_DEFAULTS, EDITABLE_DOCS, DOC_TITLES, clientSlug, findClientByKey, subscribeWorkspace, pushWorkspace, flushAllWorkspaceWrites, forceSyncFromLocal,
  notifyAdminOfClientEvent, getStorage, emailSignedDocs,
  generateReferralLink, generatePortalLink, PORTFOLIO_ORIGIN, PORTAL_ORIGIN,
});

// Overlay keys from config.local.js onto the payment providers so editing
// that file always takes effect (it wins over previously-saved state).
function applyConfigToProviders(providers) {
  const cfg = (window.RELAY_CONFIG) || {};
  return providers.map(p => {
    if (p.id === "stripe" && cfg.stripe && cfg.stripe.paymentLink) {
      return { ...p, status: "connected", paymentUrl: cfg.stripe.paymentLink, account: cfg.stripe.paymentLink };
    }
    if (p.id === "paypal" && cfg.paypal) {
      if (cfg.paypal.clientId) {
        return { ...p, status: "connected", clientId: cfg.paypal.clientId, account: "PayPal Smart Buttons (embedded)" };
      }
      if (cfg.paypal.meUsername) {
        const link = "paypal.me/" + cfg.paypal.meUsername;
        return { ...p, status: "connected", clientId: null, paymentUrl: link, account: link };
      }
    }
    return p;
  });
}

function AppProvider({ children, tweaks }) {
  const initialClients = React.useMemo(() => lsLoad("clients", CLIENTS), []);
  const initialTemplate = React.useMemo(() => lsLoad("template", DEFAULT_TEMPLATE), []);
  const [clients, setClients] = React.useState(initialClients);
  const [template, setTemplate] = React.useState(initialTemplate);
  const [paymentProviders, setPaymentProviders] = React.useState(() => applyConfigToProviders(lsLoad("payment_providers", DEFAULT_PAYMENT_PROVIDERS)));
  const [signatureProviders, setSignatureProviders] = React.useState(() => lsLoad("signature_providers", DEFAULT_SIGNATURE_PROVIDERS));
  // Freelancer profile (name/title/brand/email) — synced through Firestore so it
  // shows on EVERY device, including the client portal. Falls back to tweaks/defaults.
  const [profile, setProfile] = React.useState(() => lsLoad("profile", null));
  // Real, derived from actual client events — recomputed whenever clients change.
  const activity = React.useMemo(() => deriveActivity(clients), [clients]);
  const notifications = React.useMemo(() => deriveNotifications(clients), [clients]);
  const [activeClientId, setActiveClientId] = React.useState(() => lsLoad("activeClientId", null));

  // `fromCloud` blocks the echo back to Firestore when a change came FROM Firestore.
  const fromCloud = React.useRef(false);
  // `hydrated` blocks pushes until we know what's authoritative.
  //  - If localStorage already had data, that IS authoritative for this browser —
  //    we hydrate immediately and any unpushed edits will reach Firestore.
  //  - If localStorage was empty, wait for Firestore snapshot before pushing
  //    (prevents a fresh tab from writing an empty array over real data).
  const hydrated = React.useRef(Array.isArray(initialClients) && initialClients.length > 0);
  // Track when WE last pushed locally — used to ignore Firestore snapshots that
  // are older than our most recent local edit (prevents stale cloud → wipe local).
  const lastLocalPushAt = React.useRef(0);

  // Subscribe to Firestore once on mount — cloud state replaces local on snapshot
  // ONLY when the snapshot is newer than our last local edit (conflict guard).
  React.useEffect(() => {
    const unsub = subscribeWorkspace((data) => {
      const snapUpdatedAt = typeof data.updatedAt === "number" ? data.updatedAt : 0;
      // Guard 1: if we have pending client writes queued (debounce not yet fired),
      // never let a snapshot overwrite them — wait for our write to land first.
      if (Object.keys(_pendingClients).length > 0) {
        hydrated.current = true;
        return;
      }
      // Guard 2: reject any snapshot written to Firestore BEFORE our last local
      // edit. The old guard (+2000) only caught snapshots >2s stale — snapshots
      // from 100ms before an edit would slip through and wipe local changes.
      if (snapUpdatedAt > 0 && lastLocalPushAt.current > 0 && snapUpdatedAt < lastLocalPushAt.current) {
        hydrated.current = true;
        return;
      }
      fromCloud.current = true;
      // Clients may arrive as an array (legacy) or a map (current) — normalize.
      if (data.clients !== undefined) setClients(_clientsArrayFromCloud(data.clients));
      if (data.template && typeof data.template === "object") setTemplate(data.template);
      if (data.profile && typeof data.profile === "object") { setProfile(data.profile); try { lsSave("profile", data.profile); } catch {} }
      // First snapshot received — local pushes are now safe.
      hydrated.current = true;
      setTimeout(() => { fromCloud.current = false; }, 0);
    });
    // Safety net: if Firestore isn't reachable in 3s, allow pushes anyway
    // so a fully-offline / misconfigured environment still persists locally.
    const t = setTimeout(() => { hydrated.current = true; }, 3000);
    return () => { clearTimeout(t); unsub && unsub(); };
  }, []);

  // Flush any pending Firestore writes before the tab disappears. This catches
  // the "edit then close tab within the 400ms debounce window" data-loss case.
  React.useEffect(() => {
    const onHide = () => { if (document.visibilityState === "hidden") flushAllWorkspaceWrites(); };
    const onUnload = () => { flushAllWorkspaceWrites(); };
    document.addEventListener("visibilitychange", onHide);
    window.addEventListener("beforeunload", onUnload);
    window.addEventListener("pagehide", onUnload);
    return () => {
      document.removeEventListener("visibilitychange", onHide);
      window.removeEventListener("beforeunload", onUnload);
      window.removeEventListener("pagehide", onUnload);
    };
  }, []);

  // Persist locally AND push to Firestore — but only the CLIENTS THAT CHANGED,
  // never the whole array. updateClient() gives the edited client a new object
  // reference while leaving others identical, so a reference diff tells us
  // exactly what to push. This is what lets two devices edit different clients
  // at the same time without overwriting each other.
  const prevClientsRef = React.useRef(clients);
  React.useEffect(() => {
    lsSave("clients", clients);
    if (fromCloud.current || !hydrated.current) { prevClientsRef.current = clients; return; }
    const prev = prevClientsRef.current;
    const currIds = new Set(clients.map(c => c.id));
    const changed = clients.filter(c => prev.find(p => p.id === c.id) !== c); // new ref = added/edited
    const deletedIds = prev.filter(c => !currIds.has(c.id)).map(c => c.id);
    if (changed.length || deletedIds.length) {
      lastLocalPushAt.current = Date.now();
      syncClientsToCloud(changed, deletedIds, clients);
    }
    prevClientsRef.current = clients;
  }, [clients]);
  React.useEffect(() => {
    lsSave("template", template);
    if (!fromCloud.current && hydrated.current) {
      lastLocalPushAt.current = Date.now();
      pushWorkspace({ template });
    }
  }, [template]);
  React.useEffect(() => { lsSave("payment_providers", paymentProviders); }, [paymentProviders]);
  React.useEffect(() => { lsSave("signature_providers", signatureProviders); }, [signatureProviders]);
  React.useEffect(() => { lsSave("activeClientId", activeClientId); }, [activeClientId]);

  // Save the freelancer profile and sync it to every device (incl. client portals).
  const saveProfile = (patch) => {
    const next = { ...(profile || {}), ...patch };
    setProfile(next);
    try { lsSave("profile", next); } catch {}
    lastLocalPushAt.current = Date.now();
    pushWorkspace({ profile: next });
  };

  // Updates progress / status for a client
  const updateClient = (id, patch) => {
    setClients(cs => cs.map(c => c.id === id ? { ...c, ...patch } : c));
  };

  const deleteClient = (id) => {
    setClients(cs => {
      const next = cs.filter(c => c.id !== id);
      // Keep the active selection valid
      if (id === activeClientId) setActiveClientId(next[0]?.id || null);
      return next;
    });
  };

  const updateProvider = (id, patch) => {
    setPaymentProviders(ps => ps.map(p => p.id === id ? { ...p, ...patch } : p));
  };

  const updateSignatureProvider = (id, patch) => {
    setSignatureProviders(ps => ps.map(p => p.id === id ? { ...p, ...patch } : p));
  };

  // Priority: synced profile (Firestore, all devices) → local tweaks → defaults.
  const _p = profile || {};
  const freelancer = {
    name: _p.name || tweaks.freelancerName || DEFAULT_FREELANCER.name,
    title: _p.title || tweaks.freelancerTitle || DEFAULT_FREELANCER.title,
    brand: _p.brand || tweaks.brandName || DEFAULT_FREELANCER.brand,
    tagline: DEFAULT_FREELANCER.tagline,
    email: _p.email || tweaks.freelancerEmail || DEFAULT_FREELANCER.email,
    timezone: DEFAULT_FREELANCER.timezone,
    accent: tweaks.accent || "#1B4332",
    currency: tweaks.currency || "USD",
  };

  return (
    <AppContext.Provider value={{
      clients, setClients, updateClient, deleteClient,
      template, setTemplate,
      paymentProviders, setPaymentProviders, updateProvider,
      signatureProviders, setSignatureProviders, updateSignatureProvider,
      activity, notifications,
      activeClientId, setActiveClientId,
      freelancer, tweaks, saveProfile,
    }}>
      {children}
    </AppContext.Provider>
  );
}

const useApp = () => React.useContext(AppContext);

Object.assign(window, { AppProvider, AppContext, useApp, ToastProvider, useToast, extractFromJD, extractFromJD_AI, extractBrand, DEFAULT_TEMPLATE, DEFAULT_FREELANCER, DEFAULT_PAYMENT_PROVIDERS, DEFAULT_SIGNATURE_PROVIDERS, daysAgo, daysAhead, TODAY, JURISDICTIONS, SAMPLE_INPUTS });
