// Runs via Babel Standalone in the browser (see index.html). No bundler needed.
const { useState, useEffect } = React;

// Generic icons (not exact brand logos, for IP reasons) used on the action buttons.
function Mail({ size, color, strokeWidth }) {
  return (
    <svg width={size} height={size} viewBox="0 0 24 24" fill="none" stroke={color} strokeWidth={strokeWidth} strokeLinecap="round" strokeLinejoin="round">
      <rect x="2" y="4" width="20" height="16" rx="2" />
      <path d="m22 6-10 7L2 6" />
    </svg>
  );
}
function Instagram({ size, color, strokeWidth }) {
  return (
    <svg width={size} height={size} viewBox="0 0 24 24" fill="none" stroke={color} strokeWidth={strokeWidth} strokeLinecap="round" strokeLinejoin="round">
      <rect x="2" y="2" width="20" height="20" rx="5" />
      <circle cx="12" cy="12" r="4" />
      <circle cx="17.5" cy="6.5" r="1" fill={color} stroke="none" />
    </svg>
  );
}
function Facebook({ size, color, strokeWidth }) {
  return (
    <svg width={size} height={size} viewBox="0 0 24 24" fill="none" stroke={color} strokeWidth={strokeWidth} strokeLinecap="round" strokeLinejoin="round">
      <path d="M18 2h-3a5 5 0 0 0-5 5v3H7v4h3v8h4v-8h3l1-4h-4V7a1 1 0 0 1 1-1h3z" />
    </svg>
  );
}

const TONES = ["Luxury", "Cozy & Family", "Modern & Minimal", "Investor-Focused"];
const LENGTHS = ["Short (MLS ~250 char)", "Standard", "Long (Website)"];
const PROPERTY_TYPES = ["Single-Family", "Condo", "Townhome", "Land"];
const POST_TYPES = ["Just Listed", "Open House", "Just Sold", "Price Drop", "Market Update"];
const PLATFORMS = ["Instagram", "Facebook", "LinkedIn", "TikTok"];
const VIDEO_TYPES = ["Listing Walkthrough", "Just Listed Teaser", "Market Update", "Neighborhood Guide"];
const SITUATIONS = [
  "Post-showing",
  "Post-offer accepted",
  "Post-offer rejected",
  "Cold re-engagement",
  "Just closed (thank you)",
];
const EMAIL_TONES = ["Warm", "Professional", "Urgent"];

const CONTACT_ROLES = [
  { value: "buyer", label: "Buyer" },
  { value: "seller", label: "Seller" },
  { value: "buyer_agent", label: "Buyer's Agent" },
  { value: "seller_agent", label: "Seller's Agent" },
  { value: "other", label: "Other" },
];

const PROJECT_SIDES = [
  { value: "buyer_side", label: "Representing Buyer" },
  { value: "seller_side", label: "Representing Seller" },
];

// Client Tracker / Pipeline. Mirrors the fixed stage lists validated
// server-side in server.js -- keep these two in sync if they ever change.
const CLIENT_TYPE_LABELS = { buyer: "Buyer", seller: "Seller", both: "Both" };
const CLIENT_TYPES = Object.keys(CLIENT_TYPE_LABELS);
const BUYER_STAGES = ["Searching", "Touring", "Offer Submitted", "Under Contract", "Closing"];
const SELLER_STAGES = ["Prepping Listing", "Live/Marketing", "Showings", "Offer Received", "Under Contract", "Closing"];
function stagesForClientType(clientType) {
  if (clientType === "buyer") return BUYER_STAGES;
  if (clientType === "seller") return SELLER_STAGES;
  if (clientType === "both") return [...new Set([...BUYER_STAGES, ...SELLER_STAGES])];
  return [];
}
// Real elapsed days from last_contact_date, never guessed. null means "not yet contacted".
function daysSince(dateStr) {
  if (!dateStr) return null;
  const ms = Date.now() - new Date(dateStr).getTime();
  return Math.max(0, Math.floor(ms / (1000 * 60 * 60 * 24)));
}

async function apiCall(url, method, body) {
  const response = await fetch(url, {
    method,
    headers: { "Content-Type": "application/json" },
    body: body ? JSON.stringify(body) : undefined,
  });
  const data = await response.json();
  if (!response.ok) {
    throw new Error(data?.error?.message || "Request failed.");
  }
  return data;
}

const fetchProjects = () => apiCall("/api/projects", "GET");
const createProject = (project) => apiCall("/api/projects", "POST", project);
const updateProject = (id, project) => apiCall(`/api/projects/${id}`, "PUT", project);
const deleteProjectApi = (id) => apiCall(`/api/projects/${id}`, "DELETE");
const addContactApi = (projectId, contact) => apiCall(`/api/projects/${projectId}/contacts`, "POST", contact);
const updateContactApi = (id, contact) => apiCall(`/api/contacts/${id}`, "PUT", contact);
const deleteContactApi = (id) => apiCall(`/api/contacts/${id}`, "DELETE");
const fetchOpenHouseEvents = (projectId) => apiCall(`/api/projects/${projectId}/open-house-events`, "GET");
const createOpenHouseEvent = (projectId, eventDate) => apiCall(`/api/projects/${projectId}/open-house-events`, "POST", { eventDate });
const deleteOpenHouseEvent = (id) => apiCall(`/api/open-house-events/${id}`, "DELETE");
const updateVisitorStatus = (id, followUpStatus) => apiCall(`/api/open-house-visitors/${id}`, "PUT", { followUpStatus });
const fetchAllContacts = () => apiCall("/api/contacts", "GET");
const createUnlinkedContactApi = (contact) => apiCall("/api/contacts", "POST", contact);
const updateContactPipelineApi = (id, pipeline) => apiCall(`/api/contacts/${id}/pipeline`, "PATCH", pipeline);
const logContactApi = (id) => apiCall(`/api/contacts/${id}/log-contact`, "POST");

const INTEREST_LABELS = { low: "Just Looking", medium: "Interested", high: "Very Interested" };
const FOLLOWUP_STATUSES = [
  { value: "not_started", label: "Not Started" },
  { value: "drafted", label: "Drafted" },
  { value: "sent", label: "Sent" },
];

// Every field on an offer, grouped for display. "key" is camelCase (matches
// what the frontend/backend JSON uses), the backend maps it to the matching
// snake_case column automatically.
const OFFER_FIELDS = [
  { key: "buyerName", label: "Buyer Name", group: "Buyer" },
  { key: "buyerAgentName", label: "Buyer's Agent", group: "Buyer" },

  { key: "offerPrice", label: "Offer Price", group: "Price & Commission" },
  { key: "sellerConcessions", label: "Seller Concessions", group: "Price & Commission" },
  { key: "sellerPaidListCommission", label: "Seller-Paid List Side Commission", group: "Price & Commission" },
  { key: "sellerPaidBuyCommission", label: "Seller-Paid Buy Side Commission", group: "Price & Commission" },
  { key: "sellerPaidTotalCommission", label: "Seller-Paid Total Commission", group: "Price & Commission" },
  { key: "effectivePriceAfterCommission", label: "Effective Price After Commission", group: "Price & Commission" },
  { key: "buyerPaidCommissionPct", label: "Buyer-Paid Commission %", group: "Price & Commission" },
  { key: "buyerPaidCommission", label: "Buyer-Paid Commission", group: "Price & Commission" },

  { key: "emdAmount", label: "EMD Amount", group: "Earnest Money" },
  { key: "emdPct", label: "EMD %", group: "Earnest Money" },
  { key: "emdDeliveryDays", label: "EMD Delivery (# Days)", group: "Earnest Money" },
  { key: "emdPaidVia", label: "EMD Paid Via", group: "Earnest Money" },

  { key: "loanAmount", label: "Loan Amount", group: "Closing & Financing" },
  { key: "closeOfEscrow", label: "Close of Escrow", group: "Closing & Financing" },
  { key: "loanPct", label: "Loan %", group: "Closing & Financing" },
  { key: "downPaymentBalance", label: "Balance of Down Payment", group: "Closing & Financing" },
  { key: "downPaymentPct", label: "Balance of Down Payment %", group: "Closing & Financing" },
  { key: "buyerFundsToClose", label: "Buyer Funds to Close", group: "Closing & Financing" },

  { key: "loanContingencyDays", label: "Loan Contingency (# Days)", group: "Contingencies" },
  { key: "appraisalContingencyDays", label: "Appraisal Contingency (# Days)", group: "Contingencies" },
  { key: "inspectionContingencyDays", label: "Inspection Contingency (# Days)", group: "Contingencies" },
  { key: "insuranceContingencyDays", label: "Insurance Contingency (# Days)", group: "Contingencies" },
  { key: "saleOfBuyerPropertyContingency", label: "Sale of Buyer's Property Contingency", group: "Contingencies" },
  { key: "sellerContingencies", label: "Seller Contingencies", group: "Contingencies", multiline: true },
  { key: "asIs", label: "As-Is", group: "Contingencies", type: "yesno" },

  { key: "includedItems", label: "Included Items", group: "Inclusions & Exclusions", multiline: true },
  { key: "excludedItems", label: "Excluded Items", group: "Inclusions & Exclusions", multiline: true },

  { key: "brokerageFeeSide", label: "Brokerage Fee Charged To", group: "Brokerage Fee", type: "select", options: ["Buyer's Agent", "Seller's Agent"] },
  { key: "brokerageFeeAmount", label: "Brokerage Fee Amount", group: "Brokerage Fee" },

  { key: "homeWarranty", label: "Home Warranty (Amount / Who Pays)", group: "Other Terms", multiline: true },
  { key: "liquidatedDamagesInitialed", label: "Liquidated Damages Initialed", group: "Other Terms", type: "yesno" },
  { key: "arbitrationInitialed", label: "Arbitration Initialed", group: "Other Terms", type: "yesno" },
  { key: "otherTerms", label: "Other Terms / Comments", group: "Other Terms", multiline: true },
];

const OFFER_FIELD_GROUPS = [...new Set(OFFER_FIELDS.map((f) => f.group))];

const OFFER_SUMMARY_FIELDS = ["buyerName", "offerPrice", "effectivePriceAfterCommission", "closeOfEscrow", "loanContingencyDays"];

// Exports the comparison as a CSV, the simplest real way to get structured data
// into Excel or Google Sheets without adding a new client-side library.
function downloadComparisonCSV(project, comparisonGroups) {
  const escapeCell = (v) => `"${String(v).replace(/"/g, '""')}"`;
  const lines = [];
  lines.push(["", ...project.offers.map((o) => o.buyer_name || "(no buyer name)")].map(escapeCell).join(","));
  comparisonGroups.forEach((g) => {
    lines.push([g.group].map(escapeCell).join(","));
    g.rows.forEach((row) => {
      lines.push([row.label, ...row.values].map(escapeCell).join(","));
    });
  });
  const csv = lines.join("\n");
  const blob = new Blob([csv], { type: "text/csv;charset=utf-8;" });
  const url = URL.createObjectURL(blob);
  const a = document.createElement("a");
  a.href = url;
  a.download = `offer-comparison-${(project.address || "project").replace(/[^a-z0-9]/gi, "-").toLowerCase()}.csv`;
  document.body.appendChild(a);
  a.click();
  document.body.removeChild(a);
  URL.revokeObjectURL(url);
}

function blankOfferForm() {
  const form = { rawText: "" };
  OFFER_FIELDS.forEach((f) => { form[f.key] = ""; });
  return form;
}

function offerFromApi(offer) {
  // Converts the snake_case row from the API into the camelCase shape the form uses.
  const form = { rawText: offer.raw_text || "", id: offer.id };
  OFFER_FIELDS.forEach((f) => {
    const snakeKey = f.key.replace(/[A-Z]/g, (c) => "_" + c.toLowerCase());
    form[f.key] = offer[snakeKey] || "";
  });
  return form;
}

const addOfferApi = (projectId, offerData) => apiCall(`/api/projects/${projectId}/offers`, "POST", offerData);
const updateOfferApi = (id, offerData) => apiCall(`/api/offers/${id}`, "PUT", offerData);
const deleteOfferApi = (id) => apiCall(`/api/offers/${id}`, "DELETE");

// Comps: reference-only, no AI analysis or valuation opinion attached.
// "radius" is deliberately not part of what gets parsed, it's a judgment
// call the agent makes themselves when reviewing, same as an appraiser would.
const COMP_FIELDS = [
  { key: "address", label: "Address" },
  { key: "price", label: "Price" },
  { key: "daysOnMarket", label: "Days on Market" },
  { key: "closeDate", label: "Close Date (or \"Pending\")" },
  { key: "beds", label: "Beds" },
  { key: "baths", label: "Baths" },
  { key: "sqft", label: "Sqft" },
  { key: "lotSize", label: "Lot Size" },
];

const RADIUS_OPTIONS = ["0.25mi", "0.5mi", "1mi"];

function blankCompForm() {
  const form = { rawText: "", radius: "" };
  COMP_FIELDS.forEach((f) => { form[f.key] = ""; });
  return form;
}

function compFromApi(comp) {
  const form = { rawText: comp.raw_text || "", radius: comp.radius || "", id: comp.id };
  COMP_FIELDS.forEach((f) => {
    const snakeKey = f.key.replace(/[A-Z]/g, (c) => "_" + c.toLowerCase());
    form[f.key] = comp[snakeKey] || "";
  });
  return form;
}

const addCompApi = (projectId, compData) => apiCall(`/api/projects/${projectId}/comps`, "POST", compData);
const updateCompApi = (id, compData) => apiCall(`/api/comps/${id}`, "PUT", compData);
const deleteCompApi = (id) => apiCall(`/api/comps/${id}`, "DELETE");

const FIELD_LABEL = {
  fontFamily: "'Source Sans 3', sans-serif",
  fontSize: 11,
  fontWeight: 600,
  letterSpacing: "0.04em",
  textTransform: "uppercase",
  color: "#CDD0D6",
  marginBottom: 6,
  display: "block",
};

const CARD = {
  background: "#202A3E",
  border: "1px solid #000000",
  borderRadius: 15,
  padding: 24,
  color: "#FFFFFF",
};

// Matches the landing page's "Your Toolkit" / "Building Next" section labels exactly.
const EYEBROW_LABEL = {
  fontFamily: "'Source Sans 3', sans-serif",
  letterSpacing: "0.08em",
  textTransform: "uppercase",
  fontSize: 12,
  color: "#6AE4FF",
  fontWeight: 600,
  marginBottom: 10,
};

const COMPLIANCE_RULES = `CRITICAL COMPLIANCE RULE: You must strictly follow U.S. Fair Housing Act guidelines. NEVER include language that expresses, implies, or could be construed as a preference, limitation, or discrimination based on: race, color, national origin, religion, sex, familial status, or disability.

This includes SOFT and common-sounding phrases agents use without thinking, not just obvious ones. Examples you must catch and rewrite, not just the extreme cases: "great for a growing family," "perfect for families," "great for empty nesters," "walking distance to church," "no kids," "ideal for singles," "safe neighborhood," "exclusive neighborhood," "quiet, mature community." Any phrase implying WHO should live there (based on age, family status, religion, race, etc.) is a violation, even if it sounds like normal real estate marketing.

DO NOT REFUSE OR ERROR OUT when you see risky input. Instead, silently rewrite it into compliant language that keeps the underlying selling point but describes the PROPERTY, not the imagined resident. Never mention in the output that you changed anything, just deliver clean, compliant copy every time.`;

const STYLE_RULES = `WRITING STYLE RULES:
- Never use em dashes anywhere in the output. Use periods, commas, or parentheses instead. This is a hard rule.
- Never use emojis anywhere in the output.
- Do not repeat the same feature or descriptive phrase more than once. Mention each distinctive detail once, in its most impactful spot.
- Keep the voice natural and specific, never generic filler like "must see" or "won't last long" unless the agent's tone setting calls for urgency.
- Never adopt a defensive or persuasive-argument tone, such as framing a small size or modest price as "not a concession, but a choice," or a property as "earning its place not through X alone, but through Y." Avoid this rhetorical pattern in any form ("not just X, but Y" used to argue merit). Simply describe the property honestly and positively. Do not argue against an objection nobody raised.
- Avoid generic real estate cliches and abstract editorializing: no "ready for its next chapter," "quiet confidence," "earns its place," "endless possibilities," or similar vague phrases that describe a feeling about the home rather than the home itself. Describe concrete features, layout, and light, not abstract merit.
- If the property has few notable features, do not pad the copy with generic filler to hit a length target. A shorter, honest, well-written description is always better than a longer one stuffed with vague phrases like "the possibilities are endless" or "a place to make memories." It is fine to fall short of a length target when there is genuinely little to say.
- Avoid real estate marketing clichés and overwrought metaphors: no "ready for its next chapter," "quiet confidence," "earns its place," "this isn't just a house, it's a home," or similar stock flourishes. Even at a luxury tone, stay concrete and specific rather than abstract and poetic.`;

const EMAIL_HUMAN_RULES = `EMAIL VOICE RULES (this is personal correspondence, not marketing copy):
- This is a real person emailing a real client about a transaction that likely involves hundreds of thousands or millions of dollars. Write like a competent, busy professional who respects the client's intelligence, not like a copywriter or a brand.
- Avoid effusive or performative warmth: no "I'm so excited," "wonderful news," "amazing opportunity," "thrilled to," or similar inflated enthusiasm. Genuine warmth comes from being direct and specific, not from intensifiers.
- No poetic, metaphorical, or "flowery" language of any kind in an email. Save vivid description for listing copy, not personal correspondence.
- Prefer short, plain, direct sentences over long, ornate ones. If a sentence could be said out loud in a normal conversation, it's probably right. If it sounds like it belongs in an ad, cut it.
- Sound calm and competent rather than eager to please. The agent doesn't need to oversell themselves in an email, their job is to give useful information and a clear next step.
- This applies at every tone setting. Each tone should sound distinctly different from the others, described below, not just a generic "professional but nice" default.`;

const TONE_GUIDANCE = {
  Warm: `TONE: Warm. This should feel personal, not generic. Reference something specific from the situation (what they said, what they saw, how the interaction actually went), the way someone who's paying attention would. Contractions are fine. It should feel like it's written to this specific person, not a template. The warmth comes from specificity and genuine attentiveness, not from enthusiasm words or exclamation points. This should read as noticeably more personal than "Professional," not just a slightly softer version of it.`,
  Professional: `TONE: Professional. Businesslike, efficient, courteous. Get to the point. Still polite and respectful, but this is about information and next steps, not building personal rapport. Shorter and more clipped than "Warm."`,
  Urgent: `TONE: Urgent. Convey real time pressure through plain, factual statements (a deadline, a competing offer, a market condition), never by questioning the client's seriousness, commitment, or interest. Do NOT use phrasing like "if you're serious about this" or "if you really want this" or anything that implies doubt about the client's intent. State the time-sensitive fact directly, then give a clear, specific next step. Respect the client's autonomy to decide.`,
  Reassuring: `TONE: Reassuring. The client may be anxious or uncertain. Acknowledge the specific thing they're likely worried about without being dismissive of it, then give them a concrete reason for confidence ONLY if one genuinely exists based on known facts, a plan, or a real next step, never vague comfort words like "don't worry" or "it'll be fine." If there is no real, grounded reason for optimism (for example, a clear rejection with nothing indicating reconsideration), it is better to be honestly brief and professional than to manufacture false reassurance. Never invent hope that isn't grounded in what's actually known or stated.`,
};

const NEGOTIATION_ACCURACY_RULES = `NEGOTIATION & ACCURACY RULES (grounded in NAR ethical standards and standard real estate negotiation practice):
- Never interpret standard professional courtesy language (e.g. "we'll reach out if anything changes," "we appreciate your patience") as a substantive signal, commitment, or increased likelihood of a reversed outcome. Take such phrases at face value only, do not read hidden meaning into them.
- Never assert or imply what another party (seller, buyer, other agent) might do, think, or reconsider unless the received communication explicitly said so. Do not invent hope, momentum, or reasons for optimism that were not actually given.
- Do not invent SPECIFIC HYPOTHETICAL SCENARIOS for why a deal might change, such as guessing that a competing buyer's financing might fall through or that a timeline might shift. Even phrased as a mere possibility rather than a prediction, naming a specific invented scenario is still fabrication, since nothing in the original communication suggested that particular scenario. Stick to acknowledging the outcome itself; do not speculate about hypothetical paths back to a different one.
- When a negotiation outcome is final or clearly stated (e.g. a rejected offer), acknowledge it plainly and move forward professionally rather than reframing it as still open, uncertain, or contingent on some imagined future event.
- Keep any characterization of the other party's conduct strictly professional and factual, never speculative, critical, or personal, consistent with standard industry ethics around not disparaging other real estate professionals.
- Stick to what was actually stated or documented. Do not add assumed details, outcomes, or next steps that were not part of the actual exchange.
- This is about accuracy, not flatness: still match the requested tone and the agent's calibrated voice fully, just never at the expense of asserting or speculating about something as fact that isn't in the source material.`;

const AUDIENCE_GUIDANCE = {
  General: "",
  "Investor (Multi-Unit)": `AUDIENCE ANGLE: Investor, likely evaluating a duplex/multi-unit or income property. Emphasize cash flow potential, rental income possibility, unit configuration, separate entrances/utilities if relevant, and neighborhood rental demand. Speak to ROI and practicality over lifestyle or emotion. Do not fabricate specific numbers (cap rate, rent estimates) that weren't provided, just note the property lends itself to this kind of analysis.`,
  "Tech-Forward Buyer": `AUDIENCE ANGLE: Tech-forward buyer. Emphasize any smart home features, high-speed connectivity, home office/workspace potential, and modern efficient systems if present in the provided features. Keep the tone confident and specific rather than gimmicky. Do not invent smart features that weren't listed.`,
  "Athlete / Public Figure": `AUDIENCE ANGLE: Athlete or public figure who likely values privacy and discretion. Emphasize privacy, security, gated or set-back positioning, and low-visibility access if such features are present. Avoid language that reveals excessive personal detail about a potential buyer. Keep tone understated and confident, not showy.`,
  "Move-Up Buyer": `AUDIENCE ANGLE: Move-up buyer, someone building equity and moving to a larger or higher-value home. Emphasize upgrades, additional space, quality finishes, and how this property represents a step up. This is about budget trajectory and property quality, not the buyer's age or family composition, so do not reference life stage, family, or age in any way.`,
  "Luxury Buyer": `AUDIENCE ANGLE: Luxury buyer. Emphasize craftsmanship, finishes, exclusivity, and prestige of the property itself. Understated confidence, not showy or over-the-top.`,
  "Low-Maintenance / Single-Level Living": `AUDIENCE ANGLE: Buyer wanting a low-maintenance, easy-to-navigate home. Emphasize single-level layout, low upkeep, walkability, and simplicity of living, purely as physical property features. CRITICAL FAIR HOUSING NOTE: never reference age, retirement, life stage, or who the home is "for." Describe the property's physical characteristics only, the same way you would for any buyer.`,
  "Space & Storage Focus": `AUDIENCE ANGLE: Buyer who needs more space and storage. Emphasize bedroom count, closet/storage space, flexible rooms, and layout functionality, purely as physical property features. CRITICAL FAIR HOUSING NOTE: never reference family, children, schools, or who the home is "for." Describe the property's physical characteristics only, the same way you would for any buyer.`,
};

async function persistVoiceProfile(descriptor) {
  try {
    await fetch("/api/voice", {
      method: "PUT",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify({ voiceProfile: descriptor || "" }),
    });
  } catch (e) {
    console.error("Could not save voice profile to account", e);
  }
}

function voiceBlock(voiceProfile) {
  if (!voiceProfile) return "";
  return `\n\nVOICE MATCHING: Write in this agent's established voice as closely as possible, based on this profile derived from their own past writing: ${voiceProfile}`;
}

function formatPrice(value) {
  const digits = String(value || "").replace(/[^0-9]/g, "");
  if (!digits) return "";
  return Number(digits).toLocaleString("en-US");
}

const EMAIL_DETAIL_PLACEHOLDERS = {
  "Post-showing": "loved the kitchen but worried about the price...",
  "Post-offer accepted": "closing date, any next steps to mention...",
  "Post-offer rejected": "what they liked, reason for the pass if known...",
  "Cold re-engagement": "how long since last contact, what they were looking for...",
  "Just closed (thank you)": "a specific moment from the process worth mentioning...",
};

// Model + effort level are controlled server-side in server.js, not here.
function wait(ms) {
  return new Promise((resolve) => setTimeout(resolve, ms));
}

async function callClaude(systemPrompt, userPrompt, maxTokens, attempt) {
  const attemptNum = attempt || 1;
  const MAX_ATTEMPTS = 5;

  const hardenedSystemPrompt = `${systemPrompt}

ABSOLUTE OUTPUT RULE: Respond with ONLY the raw JSON object. No explanation, no commentary, no apology, no markdown fences, nothing before or after the JSON, no matter how minimal or unusual the input is. This rule applies even if the property is very small, has few features, or the request seems unusual. Never break character to comment on the input, just fill the schema with your best honest writing.`;

  let response;
  try {
    response = await fetch("/api/generate", {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify({
        max_tokens: maxTokens,
        system: hardenedSystemPrompt,
        messages: [{ role: "user", content: userPrompt }],
      }),
    });
  } catch (networkErr) {
    // Transient connection failures happen more than once in a row sometimes.
    // Silently retry up to MAX_ATTEMPTS total before bothering the user.
    if (attemptNum < MAX_ATTEMPTS) {
      await wait(500 * attemptNum);
      return callClaude(systemPrompt, userPrompt, maxTokens, attemptNum + 1);
    }
    throw new Error(`API_ERROR:::Could not reach the API after ${MAX_ATTEMPTS} attempts.`);
  }
  const data = await response.json();

  if (!response.ok || data.type === "error" || !data.content) {
    const apiMsg = data?.error?.message || `HTTP ${response.status}`;
    if (attemptNum < MAX_ATTEMPTS) {
      await wait(500 * attemptNum);
      return callClaude(systemPrompt, userPrompt, maxTokens, attemptNum + 1);
    }
    throw new Error(`API_ERROR:::${apiMsg}`);
  }

  const rawText = data.content
    .filter((b) => b.type === "text")
    .map((b) => b.text)
    .join("\n")
    .replace(/```json|```/g, "")
    .trim();

  // Safety net: extract the JSON object even if the model added stray text
  // around it, rather than failing the whole generation.
  const firstBrace = rawText.indexOf("{");
  const lastBrace = rawText.lastIndexOf("}");
  const jsonSlice =
    firstBrace !== -1 && lastBrace !== -1 && lastBrace > firstBrace
      ? rawText.slice(firstBrace, lastBrace + 1)
      : rawText;

  try {
    return JSON.parse(jsonSlice);
  } catch (parseErr) {
    // Surface the actual raw text so we can see what broke, instead of a blind guess.
    const preview = rawText.length > 600 ? rawText.slice(0, 600) + "... [truncated]" : rawText;
    throw new Error(`RAW_RESPONSE:::${preview}`);
  }
}

function describeError(err) {
  const msg = err && err.message ? err.message : "";
  if (msg.startsWith("RAW_RESPONSE:::")) {
    return "JSON parse failed. Raw model output below:\n\n" + msg.replace("RAW_RESPONSE:::", "");
  }
  if (msg.startsWith("API_ERROR:::")) {
    return "API error: " + msg.replace("API_ERROR:::", "") + "\n\nIf this says something about rate limits or being overloaded, wait a moment and try again.";
  }
  return "Generation failed (network issue). Try again.";
}

function copyToClipboard(text, onDone) {
  const fallback = () => {
    const textarea = document.createElement("textarea");
    textarea.value = text;
    textarea.style.position = "fixed";
    textarea.style.opacity = "0";
    document.body.appendChild(textarea);
    textarea.focus();
    textarea.select();
    try {
      document.execCommand("copy");
    } catch (e) {
      console.error("Fallback copy failed", e);
    }
    document.body.removeChild(textarea);
  };
  if (navigator.clipboard && navigator.clipboard.writeText) {
    navigator.clipboard.writeText(text).catch(fallback);
  } else {
    fallback();
  }
  onDone();
}

function CopyButton({ text }) {
  const [copied, setCopied] = useState(false);
  return (
    <button
      className="copy-btn"
      onClick={() => copyToClipboard(text, () => setCopied(true) & setTimeout(() => setCopied(false), 1500))}
    >
      {copied ? "Copied" : "Copy"}
    </button>
  );
}

function ActionButton({ label, onClick, icon: Icon, color }) {
  return (
    <button
      className="copy-btn"
      onClick={onClick}
      style={{ display: "inline-flex", alignItems: "center", gap: 6 }}
    >
      {Icon && (
        <span
          style={{
            display: "inline-flex",
            alignItems: "center",
            justifyContent: "center",
            width: 16,
            height: 16,
            borderRadius: "50%",
            background: color || "#1C2B2E",
          }}
        >
          <Icon size={10} color="#FFFFFF" strokeWidth={2.5} />
        </span>
      )}
      {label}
    </button>
  );
}

function openPlatform(platform) {
  const urls = {
    Instagram: "https://www.instagram.com/",
    Facebook: "https://www.facebook.com/",
    LinkedIn: "https://www.linkedin.com/feed/",
    TikTok: "https://www.tiktok.com/upload",
  };
  window.open(urls[platform] || urls.Instagram, "_blank", "noopener,noreferrer");
}

function openMailCompose(provider, subject, body) {
  const su = encodeURIComponent(subject || "");
  const bo = encodeURIComponent(body || "");
  const urls = {
    Gmail: `https://mail.google.com/mail/?view=cm&fs=1&su=${su}&body=${bo}`,
    Outlook: `https://outlook.live.com/mail/0/deeplink/compose?subject=${su}&body=${bo}`,
    Yahoo: `https://compose.mail.yahoo.com/?subject=${su}&body=${bo}`,
  };
  window.open(urls[provider], "_blank", "noopener,noreferrer");
}

function openMailDefault(subject, body) {
  // mailto hands off to whatever mail app is actually installed and set as
  // default on this device (Gmail app, Outlook app, Apple Mail, etc). No
  // browser, no login screen, opens straight into a filled-in compose view.
  const su = encodeURIComponent(subject || "");
  const bo = encodeURIComponent(body || "");
  const mailtoUrl = `mailto:?subject=${su}&body=${bo}`;
  const link = document.createElement("a");
  link.href = mailtoUrl;
  link.rel = "noopener noreferrer";
  document.body.appendChild(link);
  link.click();
  document.body.removeChild(link);
}

function ListingTab({ property, setProperty, voiceProfile, projects, selectedProjectId, onSelectProject, onProjectSaved }) {
  const [tone, setTone] = useState("Luxury");
  const [length, setLength] = useState("Standard");
  const [output, setOutput] = useState(null);
  const [loading, setLoading] = useState(false);
  const [error, setError] = useState(null);
  const [saving, setSaving] = useState(false);
  const [saveMsg, setSaveMsg] = useState(null);

  const update = (key) => (e) => setProperty((p) => ({ ...p, [key]: e.target.value }));
  const canGenerate = property.beds && property.baths && property.sqft && property.price && property.features.trim();

  async function handleSaveToProject() {
    if (!selectedProjectId) return;
    setSaving(true);
    setSaveMsg(null);
    try {
      const existing = projects.find((p) => p.id === selectedProjectId);
      await updateProject(selectedProjectId, { ...existing, ...property });
      onProjectSaved();
      setSaveMsg("Saved.");
      setTimeout(() => setSaveMsg(null), 2000);
    } catch (err) {
      setSaveMsg("Could not save.");
    } finally {
      setSaving(false);
    }
  }

  async function handleGenerate() {
    if (!canGenerate) return;
    setLoading(true);
    setError(null);
    setOutput(null);

    const systemPrompt = `You are a professional real estate copywriter. You write MLS listing descriptions for licensed agents.

${COMPLIANCE_RULES}

${STYLE_RULES}
- In the "short" MLS-compressed version, prioritize the single most distinctive feature rather than cramming in everything.
${voiceBlock(voiceProfile)}

Respond ONLY with valid JSON, no markdown fences, no preamble, in this exact shape:
{"long": "the full description", "short": "MLS-compressed version under 250 characters"}`;

    const lengthSpec =
      length === "Short (MLS ~250 char)"
        ? "The 'long' field should be brief, roughly 60-90 words, punchy."
        : length === "Long (Website)"
        ? "The 'long' field should aim for 180-250 words IF the property has enough genuine detail to support that naturally (describing layout, light, materials, surrounding feel, lifestyle). If the input is minimal and there truly isn't enough real material, it is fine to write less. Never mention word count, length, or this decision anywhere in the output. Just write the best honest description the input supports, then stop."
        : "The 'long' field should be a standard MLS-length description, roughly 100-140 words.";

    const userPrompt = `Write a real estate listing description.

Property type: ${property.propertyType}
Address: ${property.address || "(not provided, omit from copy)"}
Beds: ${property.beds} / Baths: ${property.baths}
Square footage: ${property.sqft}
Price: $${formatPrice(property.price)}
Key features: ${property.features}
Tone: ${tone}

LENGTH REQUIREMENT: ${lengthSpec}

Write both the "long" version per the length requirement, and a "short" MLS-compressed version (under 250 characters).`;

    try {
      const result = await callClaude(systemPrompt, userPrompt, 2000);
      setOutput(result);
    } catch (err) {
      console.error(err);
      setError(describeError(err));
    } finally {
      setLoading(false);
    }
  }

  return (
    <>
      <div style={CARD}>
        <div style={{ marginBottom: 18 }}>
          <label style={FIELD_LABEL}>Project (optional)</label>
          <select
            className="field-input"
            value={selectedProjectId || ""}
            onChange={(e) => onSelectProject(e.target.value ? Number(e.target.value) : null)}
          >
            <option value="">None</option>
            {projects.map((p) => (
              <option key={p.id} value={p.id}>{p.address || "(no address)"}</option>
            ))}
          </select>
        </div>
        <div style={{ display: "grid", gridTemplateColumns: "1fr 1fr", gap: 16 }}>
          <div style={{ gridColumn: "1 / -1" }}>
            <label style={FIELD_LABEL}>Address (optional)</label>
            <input className="field-input" placeholder="e.g. 412 Almaden Rd" value={property.address} onChange={update("address")} />
          </div>
          <div>
            <label style={FIELD_LABEL}>Property Type</label>
            <select className="field-input" value={property.propertyType} onChange={update("propertyType")}>
              {PROPERTY_TYPES.map((t) => <option key={t}>{t}</option>)}
            </select>
          </div>
          <div>
            <label style={FIELD_LABEL}>Price ($)</label>
            <input className="field-input" placeholder="1,250,000" value={property.price} onChange={update("price")} />
          </div>
          <div>
            <label style={FIELD_LABEL}>Beds</label>
            <input className="field-input" placeholder="3" value={property.beds} onChange={update("beds")} />
          </div>
          <div>
            <label style={FIELD_LABEL}>Baths</label>
            <input className="field-input" placeholder="2" value={property.baths} onChange={update("baths")} />
          </div>
          <div style={{ gridColumn: "1 / -1" }}>
            <label style={FIELD_LABEL}>Square Footage</label>
            <input className="field-input" placeholder="1,850" value={property.sqft} onChange={update("sqft")} />
          </div>
          <div style={{ gridColumn: "1 / -1" }}>
            <label style={FIELD_LABEL}>Key Features</label>
            <textarea className="field-input" rows={3} placeholder="updated kitchen, pool, mountain views..." value={property.features} onChange={update("features")} />
          </div>
          <div>
            <label style={FIELD_LABEL}>Tone</label>
            <select className="field-input" value={tone} onChange={(e) => setTone(e.target.value)}>
              {TONES.map((t) => <option key={t}>{t}</option>)}
            </select>
          </div>
          <div>
            <label style={FIELD_LABEL}>Length</label>
            <select className="field-input" value={length} onChange={(e) => setLength(e.target.value)}>
              {LENGTHS.map((l) => <option key={l}>{l}</option>)}
            </select>
          </div>
        </div>
        <button className="generate-btn" disabled={!canGenerate || loading} onClick={handleGenerate}>
          {loading ? "Writing..." : "Generate Listing"}
        </button>
        {!canGenerate && <div className="hint">Fill in beds, baths, sqft, price, and at least one feature.</div>}
        {error && <div className="error">{error}</div>}

        {selectedProjectId && (
          <div style={{ marginTop: 12 }}>
            <ActionButton label={saving ? "Saving..." : "Save Changes to Project"} onClick={handleSaveToProject} />
            {saveMsg && <span style={{ fontSize: 12, color: "#CDD0D6", marginLeft: 10 }}>{saveMsg}</span>}
            <div className="hint" style={{ marginTop: 6 }}>
              Editing these fields only affects this draft. Nothing is saved back to the project unless you click this.
            </div>
          </div>
        )}
      </div>

      {output && (
        <div style={{ ...CARD, borderLeft: "3px solid #6AE4FF", marginTop: 20 }}>
          <div className="output-header">
            <span style={{ ...FIELD_LABEL, marginBottom: 0 }}>Full Description</span>
            <CopyButton text={output.long} />
          </div>
          <p className="output-text">{output.long}</p>
          <div className="output-header">
            <span style={{ ...FIELD_LABEL, marginBottom: 0 }}>MLS-Compressed Version</span>
            <CopyButton text={output.short} />
          </div>
          <p className="output-text" style={{ marginBottom: 0 }}>{output.short}</p>
        </div>
      )}
    </>
  );
}

function SocialTab({ property, voiceProfile, projects, selectedProjectId, onSelectProject }) {
  const [postType, setPostType] = useState("Just Listed");
  const [videoType, setVideoType] = useState("Listing Walkthrough");
  const [platform, setPlatform] = useState("Instagram");
  const [audience, setAudience] = useState("General");
  const [output, setOutput] = useState(null);
  const [loading, setLoading] = useState(false);
  const [error, setError] = useState(null);

  const hasPropertyInfo = property.beds && property.price;
  const isTikTok = platform === "TikTok";

  async function handleGenerate() {
    setLoading(true);
    setError(null);
    setOutput(null);

    const propertyBlock = `Property: ${property.propertyType || "home"}, ${property.beds || "?"} bed / ${property.baths || "?"} bath, ${property.sqft || "?"} sqft
Price: $${formatPrice(property.price) || "contact for pricing"}
Address: ${property.address || "(omit specific address)"}
Key features: ${property.features || "not specified"}`;

    if (isTikTok) {
      const systemPrompt = `You are a real estate video content strategist writing a short TikTok script for an agent to film themselves, based on real, current best practices for real estate TikTok content.

${COMPLIANCE_RULES}

${STYLE_RULES}

Structure every script in three parts:
- HOOK: a single spoken line for the first 1-3 seconds, leading with the most compelling detail (price, a standout feature, or a surprising fact). This is what stops someone from scrolling.
- SHOT LIST: 3-5 short, concrete shots a single agent can film with just a phone, no crew needed. Cover: an exterior push shot, 2-3 interior detail or walkthrough shots highlighting the property's actual standout features, and a text-overlay shot with price and beds/baths.
- CTA: one specific closing line inviting engagement, such as asking viewers to comment a keyword for more info, not a generic "message me."

${AUDIENCE_GUIDANCE[audience]}
${voiceBlock(voiceProfile)}

Respond ONLY with valid JSON, no markdown fences, no preamble, in this exact shape:
{"hook": "the hook line", "shotList": ["shot 1", "shot 2", "shot 3"], "cta": "the closing line"}`;

      const userPrompt = `Write a ${videoType} TikTok script.

${propertyBlock}
Video type: ${videoType}`;

      try {
        const result = await callClaude(systemPrompt, userPrompt, 1200);
        setOutput({ platformType: "tiktok", ...result });
      } catch (err) {
        console.error(err);
        setError(describeError(err));
      } finally {
        setLoading(false);
      }
      return;
    }

    const platformGuidance =
      platform === "LinkedIn"
        ? `LinkedIn works differently than other social platforms for real estate: plain listing announcements ("Just listed!" or "Congratulations to my clients!") consistently underperform on this platform. What actually performs is content that leads with a market insight, a lesson, or the strategy behind a deal, written the way an accomplished, credible professional would share expertise with their network, not the way a listing gets marketed. Frame the post around what was learned or what a buyer/seller should know, using the property as the supporting example rather than the headline. No hashtag-heavy caption style, LinkedIn posts read as professional short-form writing, not ad copy.`
        : `Match ${platform} conventions: Instagram captions are punchy with line breaks and 5-8 relevant hashtags. Facebook captions are slightly longer, more conversational, 2-4 hashtags max.`;

    const systemPrompt = `You are a social media copywriter for a real estate agent. You write ${platform} captions.

${COMPLIANCE_RULES}

${STYLE_RULES}

${platformGuidance}

${AUDIENCE_GUIDANCE[audience]}
${voiceBlock(voiceProfile)}

Respond ONLY with valid JSON, no markdown fences, no preamble, in this exact shape:
{"caption": "the caption text with line breaks as \\n", "hashtags": "space-separated hashtags, or an empty string for LinkedIn"}`;

    const userPrompt = `Write a ${postType} social caption for ${platform}.

${propertyBlock}
Post type: ${postType}`;

    try {
      const result = await callClaude(systemPrompt, userPrompt, 1200);
      setOutput({ platformType: "caption", ...result });
    } catch (err) {
      console.error(err);
      setError(describeError(err));
    } finally {
      setLoading(false);
    }
  }

  return (
    <>
      <div style={CARD}>
        <div style={{ marginBottom: 16 }}>
          <label style={FIELD_LABEL}>Project (optional)</label>
          <select
            className="field-input"
            value={selectedProjectId || ""}
            onChange={(e) => onSelectProject(e.target.value ? Number(e.target.value) : null)}
          >
            <option value="">None</option>
            {projects.map((p) => (
              <option key={p.id} value={p.id}>{p.address || "(no address)"}</option>
            ))}
          </select>
        </div>
        {hasPropertyInfo ? (
          <div className="carryover">
            Using property from Listing tab: {property.beds}bd / {property.baths}ba, ${formatPrice(property.price) || "?"}
          </div>
        ) : (
          <div className="carryover" style={{ color: "#EB5757" }}>
            No property loaded yet. Fill in the Listing tab first for best results, or generate a general post below.
          </div>
        )}
        <div style={{ display: "grid", gridTemplateColumns: "1fr 1fr", gap: 16, marginTop: 16 }}>
          {isTikTok ? (
            <div>
              <label style={FIELD_LABEL}>Video Type</label>
              <select className="field-input" value={videoType} onChange={(e) => setVideoType(e.target.value)}>
                {VIDEO_TYPES.map((t) => <option key={t}>{t}</option>)}
              </select>
            </div>
          ) : (
            <div>
              <label style={FIELD_LABEL}>Post Type</label>
              <select className="field-input" value={postType} onChange={(e) => setPostType(e.target.value)}>
                {POST_TYPES.map((t) => <option key={t}>{t}</option>)}
              </select>
            </div>
          )}
          <div>
            <label style={FIELD_LABEL}>Platform</label>
            <select className="field-input" value={platform} onChange={(e) => setPlatform(e.target.value)}>
              {PLATFORMS.map((p) => <option key={p}>{p}</option>)}
            </select>
          </div>
          <div style={{ gridColumn: "1 / -1" }}>
            <label style={FIELD_LABEL}>Target Audience (optional)</label>
            <select className="field-input" value={audience} onChange={(e) => setAudience(e.target.value)}>
              {Object.keys(AUDIENCE_GUIDANCE).map((a) => <option key={a}>{a}</option>)}
            </select>
          </div>
        </div>
        <button className="generate-btn" disabled={loading} onClick={handleGenerate}>
          {loading ? "Writing..." : isTikTok ? "Generate Script" : "Generate Caption"}
        </button>
        {error && <div className="error">{error}</div>}
      </div>

      {output && output.platformType === "tiktok" && (
        <div style={{ ...CARD, borderLeft: "3px solid #6AE4FF", marginTop: 20 }}>
          <span style={{ ...FIELD_LABEL, marginBottom: 8, display: "block" }}>Hook</span>
          <p className="output-text">{output.hook}</p>

          <span style={{ ...FIELD_LABEL, marginBottom: 8, display: "block" }}>Shot List</span>
          <p className="output-text">
            {(output.shotList || []).map((s, i) => `${i + 1}. ${s}`).join("\n")}
          </p>

          <span style={{ ...FIELD_LABEL, marginBottom: 8, display: "block" }}>CTA</span>
          <p className="output-text" style={{ marginBottom: 12 }}>{output.cta}</p>

          <div style={{ display: "flex", flexWrap: "wrap", gap: 8 }}>
            <CopyButton text={`HOOK\n${output.hook}\n\nSHOT LIST\n${(output.shotList || []).map((s, i) => `${i + 1}. ${s}`).join("\n")}\n\nCTA\n${output.cta}`} />
            <ActionButton
              label="Open TikTok"
              onClick={() => openPlatform("TikTok")}
            />
          </div>
        </div>
      )}

      {output && output.platformType === "caption" && (
        <div style={{ ...CARD, borderLeft: "3px solid #6AE4FF", marginTop: 20 }}>
          <div style={{ marginBottom: 8 }}>
            <span style={{ ...FIELD_LABEL, marginBottom: 8, display: "block" }}>Caption</span>
            <div style={{ display: "flex", flexWrap: "wrap", gap: 8 }}>
              <CopyButton text={output.hashtags ? `${output.caption}\n\n${output.hashtags}` : output.caption} />
              <ActionButton
                label={`Open ${platform}`}
                icon={platform === "Instagram" ? Instagram : platform === "Facebook" ? Facebook : undefined}
                color={platform === "Instagram" ? "#C13584" : platform === "Facebook" ? "#1877F2" : platform === "LinkedIn" ? "#0A66C2" : undefined}
                onClick={() => {
                  copyToClipboard(output.hashtags ? `${output.caption}\n\n${output.hashtags}` : output.caption, () => {});
                  openPlatform(platform);
                }}
              />
            </div>
          </div>
          <p className="output-text">{output.caption}</p>

          {output.hashtags && <p className="output-text" style={{ color: "#6AE4FF", marginBottom: 0 }}>{output.hashtags}</p>}
        </div>
      )}
    </>
  );
}

function EmailTab({ voiceProfile, projects, selectedProjectId, onSelectProject, prefillContactName, prefillContactId, prefillDetail, prefillSituation }) {
  const [contactName, setContactName] = useState(prefillContactName || "");
  const [selectedContactId, setSelectedContactId] = useState(null);
  const [situation, setSituation] = useState(prefillSituation || "Post-showing");
  const [detail, setDetail] = useState(prefillDetail || "");
  const [tone, setTone] = useState("Warm");
  const [includePhotoLink, setIncludePhotoLink] = useState(true);
  const [output, setOutput] = useState(null);
  const [loading, setLoading] = useState(false);
  const [error, setError] = useState(null);

  const selectedProject = projects.find((p) => p.id === selectedProjectId);
  const projectContacts = selectedProject ? selectedProject.contacts : [];
  const selectedContact = projectContacts.find((c) => c.id === selectedContactId);

  const canGenerate = (selectedContact ? selectedContact.name : contactName).trim().length > 0;

  function handleContactSelect(contactId) {
    setSelectedContactId(contactId);
    const contact = projectContacts.find((c) => c.id === contactId);
    if (contact) setContactName(contact.name);
  }

  async function handleGenerate() {
    if (!canGenerate) return;
    setLoading(true);
    setError(null);
    setOutput(null);

    const contextBlock = selectedContact
      ? `\n\nCONTEXT: This email is part of an ongoing project at ${selectedProject.address || "an active listing"}. The recipient is ${selectedContact.name}, their role is ${CONTACT_ROLES.find((r) => r.value === selectedContact.role)?.label || selectedContact.role}${selectedContact.brokerage ? ` at ${selectedContact.brokerage}` : ""}. Use this real context to write something specific and genuine, not generic.`
      : "";

    const systemPrompt = `You are a real estate agent's assistant writing follow-up emails to clients.

${COMPLIANCE_RULES}

${STYLE_RULES}
- Keep emails concise, genuine, never pushy or salesy. No exclamation-point overload.
- Avoid vague, catch-all closing lines like "whether it's X or just a question, I'm always happy to help." Close with something specific to the situation, or a clear simple next step, not a generic offer of availability.

${EMAIL_HUMAN_RULES}

${TONE_GUIDANCE[tone]}

${NEGOTIATION_ACCURACY_RULES}
${voiceBlock(voiceProfile)}${contextBlock}

Respond ONLY with valid JSON, no markdown fences, no preamble, in this exact shape:
{"subject": "subject line", "body": "email body with line breaks as \\n"}`;

    const userPrompt = `Write a follow-up email.

Contact name: ${selectedContact ? selectedContact.name : contactName}
Situation: ${situation}
Key detail to reference: ${detail || "none provided, keep it general but warm"}
Tone: ${tone}`;

    try {
      const result = await callClaude(systemPrompt, userPrompt, 1200);
      if (includePhotoLink && selectedProject && selectedProject.photo_album_url) {
        result.body = `${result.body}\n\nPhotos of the property: ${selectedProject.photo_album_url}`;
      }
      setOutput(result);
      // Auto-reset: generating a Follow-Up for a known contact bumps their
      // last_contact_date. Fire-and-forget -- a logging failure shouldn't
      // block the agent from seeing the email they just generated.
      const contactIdToLog = selectedContact ? selectedContact.id : prefillContactId;
      if (contactIdToLog) logContactApi(contactIdToLog).catch((e) => console.error("Could not log contact:", e));
    } catch (err) {
      console.error(err);
      setError(describeError(err));
    } finally {
      setLoading(false);
    }
  }

  return (
    <>
      <div style={CARD}>
        <div style={{ display: "grid", gridTemplateColumns: "1fr 1fr", gap: 16, marginBottom: 16 }}>
          <div>
            <label style={FIELD_LABEL}>Project (optional)</label>
            <select
              className="field-input"
              value={selectedProjectId || ""}
              onChange={(e) => { onSelectProject(e.target.value ? Number(e.target.value) : null); setSelectedContactId(null); }}
            >
              <option value="">None</option>
              {projects.map((p) => (
                <option key={p.id} value={p.id}>{p.address || "(no address)"}</option>
              ))}
            </select>
          </div>
          <div>
            <label style={FIELD_LABEL}>Contact</label>
            <select
              className="field-input"
              value={selectedContactId || ""}
              onChange={(e) => handleContactSelect(e.target.value ? Number(e.target.value) : null)}
              disabled={!selectedProject}
            >
              <option value="">
                {selectedProject ? "Select a contact..." : "Pick a project first"}
              </option>
              {projectContacts.map((c) => (
                <option key={c.id} value={c.id}>{c.name} ({CONTACT_ROLES.find((r) => r.value === c.role)?.label || c.role})</option>
              ))}
            </select>
          </div>
        </div>
        <div style={{ display: "grid", gridTemplateColumns: "1fr 1fr", gap: 16 }}>
          <div style={{ gridColumn: "1 / -1" }}>
            <label style={FIELD_LABEL}>Contact Name</label>
            <input
              className="field-input"
              placeholder="Sarah Chen"
              value={selectedContact ? selectedContact.name : contactName}
              onChange={(e) => setContactName(e.target.value)}
              disabled={!!selectedContact}
            />
            {selectedContact && <div className="hint">Pulled from project contacts. Choose "Select a contact..." above to type a name manually instead.</div>}
          </div>
          <div>
            <label style={FIELD_LABEL}>Situation</label>
            <select className="field-input" value={situation} onChange={(e) => setSituation(e.target.value)}>
              {SITUATIONS.map((s) => <option key={s}>{s}</option>)}
            </select>
          </div>
          <div>
            <label style={FIELD_LABEL}>Tone</label>
            <select className="field-input" value={tone} onChange={(e) => setTone(e.target.value)}>
              {EMAIL_TONES.map((t) => <option key={t}>{t}</option>)}
            </select>
          </div>
          <div style={{ gridColumn: "1 / -1" }}>
            <label style={FIELD_LABEL}>Key Detail (optional)</label>
            <textarea className="field-input" rows={2} placeholder={EMAIL_DETAIL_PLACEHOLDERS[situation] || "any context to reference..."} value={detail} onChange={(e) => setDetail(e.target.value)} />
          </div>
        </div>
        {selectedProject && selectedProject.photo_album_url && (
          <label style={{ display: "flex", alignItems: "center", gap: 8, fontSize: 13, color: "#CDD0D6", marginTop: 12, cursor: "pointer" }}>
            <input type="checkbox" checked={includePhotoLink} onChange={(e) => setIncludePhotoLink(e.target.checked)} />
            Include photo album link in this email
          </label>
        )}
        <button className="generate-btn" disabled={!canGenerate || loading} onClick={handleGenerate}>
          {loading ? "Writing..." : "Generate Email"}
        </button>
        {!canGenerate && <div className="hint">Enter a contact name to generate.</div>}
        {error && <div className="error">{error}</div>}
      </div>

      {output && (
        <div style={{ ...CARD, borderLeft: "3px solid #6AE4FF", marginTop: 20 }}>
          <div className="output-header">
            <span style={{ ...FIELD_LABEL, marginBottom: 0 }}>Subject</span>
            <CopyButton text={output.subject} />
          </div>
          <p className="output-text">{output.subject}</p>
          <div className="output-header">
            <span style={{ ...FIELD_LABEL, marginBottom: 0 }}>Body</span>
            <CopyButton text={output.body} />
          </div>
          <p className="output-text" style={{ marginBottom: 0 }}>{output.body}</p>
          <div style={{ marginTop: 16, paddingTop: 16, borderTop: "1px dashed #000000" }}>
            <button
              className="generate-btn"
              style={{ marginTop: 0 }}
              onClick={() => {
                copyToClipboard(`Subject: ${output.subject}\n\n${output.body}`, () => {});
                openMailDefault(output.subject, output.body);
              }}
            >
              Open in Mail App
            </button>
            <div className="hint" style={{ marginBottom: 12 }}>
              Opens directly in whichever mail app is set as default on this device (Gmail app, Outlook app, Apple Mail, etc), already filled in. Also copied to clipboard as backup.
            </div>
            <span style={{ ...FIELD_LABEL, marginBottom: 8, display: "block" }}>Or open webmail directly</span>
            <div style={{ display: "flex", flexWrap: "wrap", gap: 8, alignItems: "center" }}>
              <ActionButton label="Gmail" icon={Mail} color="#EA4335" onClick={() => { copyToClipboard(`Subject: ${output.subject}\n\n${output.body}`, () => {}); openMailCompose("Gmail", output.subject, output.body); }} />
              <ActionButton label="Outlook" icon={Mail} color="#0078D4" onClick={() => { copyToClipboard(`Subject: ${output.subject}\n\n${output.body}`, () => {}); openMailCompose("Outlook", output.subject, output.body); }} />
              <ActionButton label="Yahoo" icon={Mail} color="#6001D2" onClick={() => { copyToClipboard(`Subject: ${output.subject}\n\n${output.body}`, () => {}); openMailCompose("Yahoo", output.subject, output.body); }} />
              <CopyButton text={`Subject: ${output.subject}\n\n${output.body}`} />
            </div>
            <div className="hint">
              If webmail lands on your inbox instead of a new draft, click Compose, it's already copied to paste. Outlook specifically may drop you on Microsoft's homepage instead if you're not signed in there, staying signed into Outlook.com in your browser avoids this.
            </div>
          </div>
        </div>
      )}
    </>
  );
}

function ReplyTab({ voiceProfile, projects, selectedProjectId, onSelectProject, prefillContactName, prefillContactId }) {
  const [receivedEmail, setReceivedEmail] = useState("");
  const [contactName, setContactName] = useState(prefillContactName || "");
  const [selectedContactId, setSelectedContactId] = useState(null);
  const [tone, setTone] = useState("Warm");
  const [length, setLength] = useState("Standard");
  const [includePhotoLink, setIncludePhotoLink] = useState(true);

  const [analysis, setAnalysis] = useState(null);
  const [analyzing, setAnalyzing] = useState(false);
  const [analyzeError, setAnalyzeError] = useState(null);

  const [output, setOutput] = useState(null);
  const [loading, setLoading] = useState(false);
  const [error, setError] = useState(null);

  const selectedProject = projects.find((p) => p.id === selectedProjectId);
  const projectContacts = selectedProject ? selectedProject.contacts : [];
  const selectedContact = projectContacts.find((c) => c.id === selectedContactId);

  const canAnalyze = receivedEmail.trim().length > 10;
  const canGenerate = receivedEmail.trim().length > 10;

  function handleContactSelect(contactId) {
    setSelectedContactId(contactId);
    const contact = projectContacts.find((c) => c.id === contactId);
    if (contact) setContactName(contact.name);
  }

  async function handleAnalyze() {
    if (!canAnalyze) return;
    setAnalyzing(true);
    setAnalyzeError(null);
    setAnalysis(null);
    setOutput(null);

    const systemPrompt = `You are helping a real estate agent understand an email they received from a client or lead. Read the email and classify it.

Respond ONLY with valid JSON, no markdown fences, no preamble, in this exact shape:
{"situation": "a short 3-6 word read on what's going on, e.g. 'Price concern, still interested' or 'Ready to move forward'", "suggestedTone": "one of: Warm, Professional, Urgent, Reassuring", "inferredName": "the sender's first name if it appears in the email signature or greeting context, otherwise empty string"}`;

    const userPrompt = `Here is the email the agent received:\n\n${receivedEmail}`;

    try {
      const result = await callClaude(systemPrompt, userPrompt, 300);
      setAnalysis(result);
      if (result.suggestedTone) setTone(result.suggestedTone);
      if (result.inferredName && !contactName && !selectedContact) setContactName(result.inferredName);
    } catch (err) {
      console.error(err);
      setAnalyzeError(describeError(err));
    } finally {
      setAnalyzing(false);
    }
  }

  async function handleGenerate() {
    if (!canGenerate) return;
    setLoading(true);
    setError(null);
    setOutput(null);

    const contextBlock = selectedContact
      ? `\n\nCONTEXT: This is part of an ongoing project at ${selectedProject.address || "an active listing"}. The sender is ${selectedContact.name}, their role is ${CONTACT_ROLES.find((r) => r.value === selectedContact.role)?.label || selectedContact.role}${selectedContact.brokerage ? ` at ${selectedContact.brokerage}` : ""}. Use this real context to write something specific and genuine, not generic.`
      : "";

    const systemPrompt = `You are a real estate agent's assistant writing a reply to an email the agent received from a client or lead.

${COMPLIANCE_RULES}

${STYLE_RULES}
- Keep the reply concise, genuine, never pushy or salesy. No exclamation-point overload.
- Avoid vague, catch-all closing lines like "whether it's X or just a question, I'm always happy to help." Close with something specific to the situation, or a clear simple next step, not a generic offer of availability.
- Directly address what the sender actually said. Do not write a generic reply, reference their specific question or concern.

${EMAIL_HUMAN_RULES}

${TONE_GUIDANCE[tone]}

This is a reply within an existing email thread that already has a subject line, so do not generate one.

${NEGOTIATION_ACCURACY_RULES}
${voiceBlock(voiceProfile)}${contextBlock}

Respond ONLY with valid JSON, no markdown fences, no preamble, in this exact shape:
{"body": "email body with line breaks as \\n"}`;

    const lengthSpec =
      length === "Short (quick acknowledgment)"
        ? "Keep it brief, a few sentences, a quick acknowledgment and one clear next step."
        : "Write a full, complete reply that addresses everything the sender raised.";

    const contactForPrompt = selectedContact ? selectedContact.name : contactName;
    const userPrompt = `The agent received this email:\n\n${receivedEmail}\n\nContact name (if known): ${contactForPrompt || "unknown"}\nTone: ${tone}\nLength: ${lengthSpec}\n\nWrite the agent's reply.`;

    try {
      const result = await callClaude(systemPrompt, userPrompt, 1200);
      if (includePhotoLink && selectedProject && selectedProject.photo_album_url) {
        result.body = `${result.body}\n\nPhotos of the property: ${selectedProject.photo_album_url}`;
      }
      setOutput(result);
      // Auto-reset: generating a Reply for a known contact bumps their
      // last_contact_date. Fire-and-forget -- a logging failure shouldn't
      // block the agent from seeing the reply they just generated.
      const contactIdToLog = selectedContact ? selectedContact.id : prefillContactId;
      if (contactIdToLog) logContactApi(contactIdToLog).catch((e) => console.error("Could not log contact:", e));
    } catch (err) {
      console.error(err);
      setError(describeError(err));
    } finally {
      setLoading(false);
    }
  }

  return (
    <>
      <div style={CARD}>
        <div style={{ display: "grid", gridTemplateColumns: "1fr 1fr", gap: 16, marginBottom: 16 }}>
          <div>
            <label style={FIELD_LABEL}>Project (optional)</label>
            <select
              className="field-input"
              value={selectedProjectId || ""}
              onChange={(e) => { onSelectProject(e.target.value ? Number(e.target.value) : null); setSelectedContactId(null); }}
            >
              <option value="">None</option>
              {projects.map((p) => (
                <option key={p.id} value={p.id}>{p.address || "(no address)"}</option>
              ))}
            </select>
          </div>
          <div>
            <label style={FIELD_LABEL}>Contact</label>
            <select
              className="field-input"
              value={selectedContactId || ""}
              onChange={(e) => handleContactSelect(e.target.value ? Number(e.target.value) : null)}
              disabled={!selectedProject}
            >
              <option value="">
                {selectedProject ? "Select a contact..." : "Pick a project first"}
              </option>
              {projectContacts.map((c) => (
                <option key={c.id} value={c.id}>{c.name} ({CONTACT_ROLES.find((r) => r.value === c.role)?.label || c.role})</option>
              ))}
            </select>
          </div>
        </div>

        <label style={FIELD_LABEL}>Paste the Email You Received</label>
        <textarea
          className="field-input"
          rows={7}
          placeholder="Paste the full email here, including their signature if there is one..."
          value={receivedEmail}
          onChange={(e) => { setReceivedEmail(e.target.value); setAnalysis(null); }}
        />

        <button className="outline-btn" style={{ width: "100%" }} disabled={!canAnalyze || analyzing} onClick={handleAnalyze}>
          {analyzing ? "Reading..." : "Analyze"}
        </button>
        {!canAnalyze && <div className="hint">Paste an email above to analyze it.</div>}
        {analyzeError && <div className="error">{analyzeError}</div>}

        {analysis && (
          <div className="carryover" style={{ marginTop: 12 }}>
            Sounds like: <strong>{analysis.situation}</strong>. Tone and name below have been pre-filled, feel free to adjust.
          </div>
        )}

        <div style={{ display: "grid", gridTemplateColumns: "1fr 1fr", gap: 16, marginTop: 16 }}>
          <div>
            <label style={FIELD_LABEL}>Contact Name (optional)</label>
            <input
              className="field-input"
              placeholder="Sarah Chen"
              value={selectedContact ? selectedContact.name : contactName}
              onChange={(e) => setContactName(e.target.value)}
              disabled={!!selectedContact}
            />
            {selectedContact && <div className="hint">Pulled from project contacts.</div>}
          </div>
          <div>
            <label style={FIELD_LABEL}>Tone</label>
            <select className="field-input" value={tone} onChange={(e) => setTone(e.target.value)}>
              {["Warm", "Professional", "Urgent", "Reassuring"].map((t) => <option key={t}>{t}</option>)}
            </select>
          </div>
          <div style={{ gridColumn: "1 / -1" }}>
            <label style={FIELD_LABEL}>Length</label>
            <select className="field-input" value={length} onChange={(e) => setLength(e.target.value)}>
              <option>Short (quick acknowledgment)</option>
              <option>Standard</option>
            </select>
          </div>
        </div>

        {selectedProject && selectedProject.photo_album_url && (
          <label style={{ display: "flex", alignItems: "center", gap: 8, fontSize: 13, color: "#CDD0D6", marginTop: 12, cursor: "pointer" }}>
            <input type="checkbox" checked={includePhotoLink} onChange={(e) => setIncludePhotoLink(e.target.checked)} />
            Include photo album link in this reply
          </label>
        )}

        <button className="generate-btn" disabled={!canGenerate || loading} onClick={handleGenerate}>
          {loading ? "Writing..." : "Generate Reply"}
        </button>
        {error && <div className="error">{error}</div>}
      </div>

      {output && (
        <div style={{ ...CARD, borderLeft: "3px solid #6AE4FF", marginTop: 20 }}>
          <div className="output-header">
            <span style={{ ...FIELD_LABEL, marginBottom: 0 }}>Reply</span>
            <CopyButton text={output.body} />
          </div>
          <p className="output-text" style={{ marginBottom: 0 }}>{output.body}</p>
          <div style={{ marginTop: 16, paddingTop: 16, borderTop: "1px dashed #000000" }}>
            <div className="hint" style={{ marginBottom: 12 }}>
              This is meant to be pasted into your reply within the existing email thread, so it keeps that thread's subject line. Copy above, or use the buttons below if you'd rather start a fresh email instead.
            </div>
            <button
              className="generate-btn"
              style={{ marginTop: 0 }}
              onClick={() => {
                copyToClipboard(output.body, () => {});
                openMailDefault("", output.body);
              }}
            >
              Open in Mail App
            </button>
            <div className="hint" style={{ marginBottom: 12 }}>
              Opens a new email in whichever mail app is default on this device, body already filled in. Also copied to clipboard as backup.
            </div>
            <span style={{ ...FIELD_LABEL, marginBottom: 8, display: "block" }}>Or open webmail directly</span>
            <div style={{ display: "flex", flexWrap: "wrap", gap: 8, alignItems: "center" }}>
              <ActionButton label="Gmail" icon={Mail} color="#EA4335" onClick={() => { copyToClipboard(output.body, () => {}); openMailCompose("Gmail", "", output.body); }} />
              <ActionButton label="Outlook" icon={Mail} color="#0078D4" onClick={() => { copyToClipboard(output.body, () => {}); openMailCompose("Outlook", "", output.body); }} />
              <ActionButton label="Yahoo" icon={Mail} color="#6001D2" onClick={() => { copyToClipboard(output.body, () => {}); openMailCompose("Yahoo", "", output.body); }} />
              <CopyButton text={output.body} />
            </div>
            <div className="hint">
              If webmail lands on your inbox instead of a new draft, click Compose, it's already copied to paste. Outlook specifically may drop you on Microsoft's homepage instead if you're not signed in there, staying signed into Outlook.com in your browser avoids this.
            </div>
          </div>
        </div>
      )}
    </>
  );
}

function ContactRow({ contact, projectId, onChanged }) {
  const [editing, setEditing] = useState(false);
  const [form, setForm] = useState({ ...contact });
  const [saving, setSaving] = useState(false);
  const [error, setError] = useState(null);

  async function handleSave() {
    setSaving(true);
    setError(null);
    try {
      await updateContactApi(contact.id, form);
      onChanged();
      setEditing(false);
    } catch (err) {
      setError(err.message);
    } finally {
      setSaving(false);
    }
  }

  async function handleDelete() {
    if (!window.confirm(`Remove ${contact.name} from this project?`)) return;
    try {
      await deleteContactApi(contact.id);
      onChanged();
    } catch (err) {
      setError(err.message);
    }
  }

  if (!editing) {
    return (
      <div className="detail-item" style={{ marginBottom: 10, cursor: "default" }}>
        <div className="item-text">
          <span className="detail-item-label">{contact.name}</span>
          <span className="detail-item-meta">
            {CONTACT_ROLES.find((r) => r.value === contact.role)?.label || contact.role}
            {contact.brokerage ? ` · ${contact.brokerage}` : ""}
            {contact.email ? ` · ${contact.email}` : ""}
          </span>
        </div>
        <div style={{ display: "flex", gap: 8, flexShrink: 0 }}>
          <ActionButton label="Edit" onClick={() => setEditing(true)} />
          <ActionButton label="Remove" onClick={handleDelete} />
        </div>
      </div>
    );
  }

  return (
    <div style={{ background: "#202A3E", border: "1px solid #000000", borderRadius: 15, padding: "18px 22px", marginBottom: 10 }}>
      <div style={{ display: "grid", gridTemplateColumns: "1fr 1fr", gap: 10, marginBottom: 12 }}>
        <input className="field-input" placeholder="Name" value={form.name} onChange={(e) => setForm({ ...form, name: e.target.value })} />
        <select className="field-input" value={form.role} onChange={(e) => setForm({ ...form, role: e.target.value })}>
          {CONTACT_ROLES.map((r) => <option key={r.value} value={r.value}>{r.label}</option>)}
        </select>
        <input className="field-input" placeholder="Email" value={form.email || ""} onChange={(e) => setForm({ ...form, email: e.target.value })} />
        <input className="field-input" placeholder="Phone" value={form.phone || ""} onChange={(e) => setForm({ ...form, phone: e.target.value })} />
        <input className="field-input" placeholder="Brokerage" value={form.brokerage || ""} onChange={(e) => setForm({ ...form, brokerage: e.target.value })} />
      </div>
      <div style={{ display: "flex", gap: 8 }}>
        <ActionButton label={saving ? "Saving..." : "Save"} onClick={handleSave} />
        <ActionButton label="Cancel" onClick={() => setEditing(false)} />
      </div>
      {error && <div className="error">{error}</div>}
    </div>
  );
}

function AddContactForm({ projectId, onAdded }) {
  const [showForm, setShowForm] = useState(false);
  const [name, setName] = useState("");
  const [role, setRole] = useState("buyer");
  const [email, setEmail] = useState("");
  const [phone, setPhone] = useState("");
  const [brokerage, setBrokerage] = useState("");
  const [saving, setSaving] = useState(false);
  const [error, setError] = useState(null);

  async function handleAdd() {
    if (!name.trim()) return;
    setSaving(true);
    setError(null);
    try {
      await addContactApi(projectId, { name, role, email, phone, brokerage });
      setName(""); setEmail(""); setPhone(""); setBrokerage(""); setRole("buyer");
      setShowForm(false);
      onAdded();
    } catch (err) {
      setError(err.message);
    } finally {
      setSaving(false);
    }
  }

  if (!showForm) {
    return <div className="add-btn" onClick={() => setShowForm(true)}>+ Add Contact</div>;
  }

  return (
    <div style={{ background: "#202A3E", border: "1px solid #000000", borderRadius: 15, padding: "18px 22px", marginBottom: 10 }}>
      <div style={{ display: "grid", gridTemplateColumns: "1fr 1fr", gap: 10, marginBottom: 12 }}>
        <input className="field-input" placeholder="Name" value={name} onChange={(e) => setName(e.target.value)} />
        <select className="field-input" value={role} onChange={(e) => setRole(e.target.value)}>
          {CONTACT_ROLES.map((r) => <option key={r.value} value={r.value}>{r.label}</option>)}
        </select>
        <input className="field-input" placeholder="Email (optional)" value={email} onChange={(e) => setEmail(e.target.value)} />
        <input className="field-input" placeholder="Phone (optional)" value={phone} onChange={(e) => setPhone(e.target.value)} />
        <input className="field-input" placeholder="Brokerage (optional)" value={brokerage} onChange={(e) => setBrokerage(e.target.value)} />
      </div>
      <div style={{ display: "flex", gap: 8 }}>
        <ActionButton label={saving ? "Adding..." : "Add Contact"} onClick={handleAdd} />
        <ActionButton label="Cancel" onClick={() => setShowForm(false)} />
      </div>
      {error && <div className="error">{error}</div>}
    </div>
  );
}

function VisitorRow({ visitor, projectId, onContactAdded, onFollowUp }) {
  const [status, setStatus] = useState(visitor.follow_up_status || "not_started");
  const [saving, setSaving] = useState(false);
  const [added, setAdded] = useState(false);
  const [error, setError] = useState(null);

  async function handleStatusChange(newStatus) {
    setStatus(newStatus);
    try {
      await updateVisitorStatus(visitor.id, newStatus);
    } catch (err) {
      setError(err.message);
    }
  }

  async function handleAddAsContact() {
    setSaving(true);
    setError(null);
    try {
      await addContactApi(projectId, {
        name: visitor.name,
        role: visitor.role === "buyer_agent" ? "buyer_agent" : "buyer",
        email: visitor.email,
        phone: visitor.phone,
        brokerage: visitor.brokerage,
      });
      setAdded(true);
      onContactAdded();
    } catch (err) {
      setError(err.message);
    } finally {
      setSaving(false);
    }
  }

  function handleFollowUpClick() {
    if (!onFollowUp) return;
    // Built only from real, saved visitor fields, nothing invented.
    const parts = [];
    parts.push(INTEREST_LABELS[visitor.interest_level] || visitor.interest_level || "Interest level not noted");
    if (visitor.wants_disclosures) parts.push("wants disclosures");
    if (visitor.wants_photos) parts.push("wants photos of the property");
    onFollowUp(visitor.name, parts.join(", "));
  }

  return (
    <div style={{ padding: "12px 0", borderBottom: "1px solid #202A3E" }}>
      <div style={{ display: "flex", justifyContent: "space-between", alignItems: "flex-start" }}>
        <div>
          <div style={{ fontSize: 14, fontWeight: 500 }}>{visitor.name}</div>
          <div style={{ fontSize: 12, color: "#CDD0D6" }}>
            {visitor.role === "buyer_agent" ? "Buyer's Agent" : "Buyer"}
            {visitor.brokerage ? ` · ${visitor.brokerage}` : ""}
            {visitor.email ? ` · ${visitor.email}` : ""}
            {visitor.phone ? ` · ${visitor.phone}` : ""}
          </div>
          <div style={{ fontSize: 12, color: "#6AE4FF", marginTop: 2 }}>
            {INTEREST_LABELS[visitor.interest_level] || visitor.interest_level || "Interest not set"}
            {visitor.wants_disclosures ? " · Wants disclosures" : ""}
            {visitor.wants_photos ? " · Wants photos" : ""}
          </div>
        </div>
        <div style={{ display: "flex", gap: 8 }}>
          {onFollowUp && <ActionButton label="Follow Up" onClick={handleFollowUpClick} />}
          <ActionButton
            label={added ? "Added" : saving ? "Adding..." : "Add as Contact"}
            onClick={handleAddAsContact}
          />
        </div>
      </div>
      <div style={{ marginTop: 8, display: "flex", alignItems: "center", gap: 8 }}>
        <span style={{ fontSize: 11, color: "#CDD0D6" }}>Follow-up:</span>
        <select className="field-input" style={{ width: "auto", padding: "4px 8px", fontSize: 11 }} value={status} onChange={(e) => handleStatusChange(e.target.value)}>
          {FOLLOWUP_STATUSES.map((s) => <option key={s.value} value={s.value}>{s.label}</option>)}
        </select>
      </div>
      {error && <div className="error">{error}</div>}
    </div>
  );
}

function CollapsibleSection({ title, defaultOpen = true, children }) {
  const [open, setOpen] = useState(defaultOpen);
  return (
    <div style={{ marginTop: 20, paddingTop: 20, borderTop: "1px dashed #000000" }}>
      <div
        onClick={() => setOpen(!open)}
        style={{ display: "flex", justifyContent: "space-between", alignItems: "center", marginBottom: open ? 8 : 0, cursor: "pointer" }}
      >
        <span style={{ ...FIELD_LABEL, marginBottom: 0 }}>{title}</span>
        <span style={{ color: "#6AE4FF", fontSize: 12 }}>{open ? "Hide ▲" : "Show ▼"}</span>
      </div>
      {open && children}
    </div>
  );
}

function OpenHouseSection({ project, onProjectContactsChanged, onFollowUpVisitor }) {
  const [events, setEvents] = useState([]);
  const [loading, setLoading] = useState(true);
  const [newDate, setNewDate] = useState("");
  const [creating, setCreating] = useState(false);
  const [error, setError] = useState(null);
  const [expandedEventId, setExpandedEventId] = useState(null);
  const [copiedEventId, setCopiedEventId] = useState(null);

  function refresh() {
    setLoading(true);
    fetchOpenHouseEvents(project.id)
      .then(setEvents)
      .catch((err) => setError(err.message))
      .finally(() => setLoading(false));
  }

  useEffect(() => {
    refresh();
  }, [project.id]);

  async function handleCreateEvent() {
    if (!newDate) return;
    setCreating(true);
    setError(null);
    try {
      await createOpenHouseEvent(project.id, newDate);
      setNewDate("");
      refresh();
    } catch (err) {
      setError(err.message);
    } finally {
      setCreating(false);
    }
  }

  async function handleDeleteEvent(id) {
    if (!window.confirm("Delete this open house event? This also removes its sign-in list.")) return;
    try {
      await deleteOpenHouseEvent(id);
      refresh();
    } catch (err) {
      setError(err.message);
    }
  }

  function signInUrl(eventId) {
    return `${window.location.origin}/signin.html?event=${eventId}`;
  }

  function handleCopyLink(eventId) {
    copyToClipboard(signInUrl(eventId), () => {
      setCopiedEventId(eventId);
      setTimeout(() => setCopiedEventId(null), 1500);
    });
  }

  return (
    <>
      {loading && <div className="hint">Loading...</div>}
      {!loading && events.length === 0 && <div className="hint">No open house events yet for this project.</div>}

      {events.map((event) => (
        <div key={event.id} style={{ marginBottom: 12, background: "#202A3E", border: "1px solid #000000", borderRadius: 15, padding: 22 }}>
          <div
            onClick={() => setExpandedEventId(expandedEventId === event.id ? null : event.id)}
            style={{ display: "flex", justifyContent: "space-between", alignItems: "center", cursor: "pointer" }}
          >
            <div style={{ fontSize: 16, fontWeight: 600 }}>
              {new Date(event.event_date.slice(0, 10) + "T00:00:00").toLocaleDateString("en-US", { month: "short", day: "numeric", year: "numeric" })}
              <span style={{ fontSize: 12, color: "#CDD0D6", fontWeight: 400 }}> · {event.visitors.length} visitor{event.visitors.length === 1 ? "" : "s"}</span>
            </div>
            <span style={{ color: "#6AE4FF", fontSize: 12 }}>{expandedEventId === event.id ? "Hide ▲" : "Show ▼"}</span>
          </div>

          {expandedEventId === event.id && (
            <div style={{ marginTop: 12, paddingTop: 12, borderTop: "1px dashed #000000" }}>
              <div style={{ display: "flex", gap: 16, alignItems: "flex-start", marginBottom: 12, flexWrap: "wrap" }}>
                <div style={{ background: "#FFFFFF", padding: 10, border: "1px solid #000000", borderRadius: 6 }}>
                  <img
                    src={`https://api.qrserver.com/v1/create-qr-code/?size=180x180&data=${encodeURIComponent(signInUrl(event.id))}`}
                    alt="QR code for sign-in"
                    width={180}
                    height={180}
                  />
                </div>
                <div style={{ flex: 1, minWidth: 160 }}>
                  <div className="hint" style={{ marginTop: 0, marginBottom: 12 }}>
                    Print or display this at the sign-in table. Visitors scan it with their phone camera, no app needed on their end.
                  </div>
                  <div style={{ display: "flex", gap: 8, flexWrap: "wrap" }}>
                    <ActionButton label={copiedEventId === event.id ? "Copied" : "Copy Link"} onClick={() => handleCopyLink(event.id)} />
                    <ActionButton label="Delete Event" onClick={() => handleDeleteEvent(event.id)} />
                  </div>
                </div>
              </div>
              {event.visitors.length === 0 && <div className="hint">No sign-ins yet.</div>}
              {event.visitors.map((v) => (
                <VisitorRow key={v.id} visitor={v} projectId={project.id} onContactAdded={onProjectContactsChanged} onFollowUp={onFollowUpVisitor} />
              ))}
            </div>
          )}
        </div>
      ))}

      <div style={{ display: "flex", gap: 8, marginTop: 12, alignItems: "center", flexWrap: "wrap" }}>
        <input
          className="field-input"
          type="date"
          style={{ width: "auto" }}
          value={newDate}
          onChange={(e) => setNewDate(e.target.value)}
        />
        <ActionButton label={creating ? "Creating..." : "+ New Event"} onClick={handleCreateEvent} />
      </div>
      {error && <div className="error">{error}</div>}
    </>
  );
}

function OfferForm({ form, setForm, onSave, onCancel, onDelete, saving }) {
  const [openGroups, setOpenGroups] = useState(() => new Set([OFFER_FIELD_GROUPS[0]]));

  function toggleGroup(group) {
    setOpenGroups((prev) => {
      const next = new Set(prev);
      if (next.has(group)) next.delete(group);
      else next.add(group);
      return next;
    });
  }

  return (
    <div className="split-editor">
      <div className="split-editor-paste">
        <label style={FIELD_LABEL}>Original Pasted Text</label>
        <textarea
          className="field-input split-editor-textarea"
          value={form.rawText || ""}
          onChange={(e) => setForm({ ...form, rawText: e.target.value })}
        />
        <div className="hint">Keep this visible while you check the parsed fields on the right against it.</div>
      </div>

      <div className="split-editor-fields">
        {OFFER_FIELD_GROUPS.map((group) => {
          const groupFields = OFFER_FIELDS.filter((f) => f.group === group);
          const isOpen = openGroups.has(group);
          return (
            <div key={group} className="accordion-group" style={{ marginBottom: 10 }}>
              <div className="accordion-header" onClick={() => toggleGroup(group)}>
                <span className="accordion-header-name">{group}</span>
                <span className="accordion-header-count">
                  {groupFields.length} field{groupFields.length === 1 ? "" : "s"}
                  <span className={`accordion-chevron ${isOpen ? "open" : ""}`} style={{ marginLeft: 8 }}>▾</span>
                </span>
              </div>
              {isOpen && (
                <div className="accordion-body">
                  <div style={{ display: "grid", gridTemplateColumns: "1fr 1fr", gap: 10 }}>
                    {groupFields.map((f) => (
                      <div key={f.key} style={f.multiline ? { gridColumn: "1 / -1" } : undefined}>
                        <label style={{ ...FIELD_LABEL, fontSize: 10.5 }}>{f.label}</label>
                        {f.type === "yesno" ? (
                          <select className="field-input" value={form[f.key] || ""} onChange={(e) => setForm({ ...form, [f.key]: e.target.value })} onClick={(e) => e.stopPropagation()}>
                            <option value="">Not specified</option>
                            <option value="Yes">Yes</option>
                            <option value="No">No</option>
                          </select>
                        ) : f.type === "select" ? (
                          <select className="field-input" value={form[f.key] || ""} onChange={(e) => setForm({ ...form, [f.key]: e.target.value })} onClick={(e) => e.stopPropagation()}>
                            <option value="">Not specified</option>
                            {f.options.map((opt) => <option key={opt} value={opt}>{opt}</option>)}
                          </select>
                        ) : f.multiline ? (
                          <textarea
                            className="field-input"
                            rows={2}
                            placeholder="Not found in source"
                            value={form[f.key] || ""}
                            onChange={(e) => setForm({ ...form, [f.key]: e.target.value })}
                            onClick={(e) => e.stopPropagation()}
                          />
                        ) : (
                          <input
                            className="field-input"
                            placeholder="Not found in source"
                            value={form[f.key] || ""}
                            onChange={(e) => setForm({ ...form, [f.key]: e.target.value })}
                            onClick={(e) => e.stopPropagation()}
                          />
                        )}
                      </div>
                    ))}
                  </div>
                </div>
              )}
            </div>
          );
        })}

        <div style={{ display: "flex", gap: 8, marginTop: 8, flexWrap: "wrap" }}>
          <ActionButton label={saving ? "Saving..." : "Save Offer"} onClick={onSave} />
          <ActionButton label="Cancel" onClick={onCancel} />
          {onDelete && <ActionButton label="Delete Offer" onClick={onDelete} />}
        </div>
      </div>
    </div>
  );
}

function formatMoneyValue(value) {
  if (!value) return "";
  const trimmed = String(value).trim();
  return trimmed.startsWith("$") ? trimmed : `$${trimmed}`;
}

function OfferRow({ offer, onChanged }) {
  const [editing, setEditing] = useState(false);
  const [form, setForm] = useState(() => offerFromApi(offer));
  const [saving, setSaving] = useState(false);
  const [error, setError] = useState(null);

  async function handleSave() {
    setSaving(true);
    setError(null);
    try {
      await updateOfferApi(offer.id, form);
      onChanged();
      setEditing(false);
    } catch (err) {
      setError(err.message);
    } finally {
      setSaving(false);
    }
  }

  async function handleDelete() {
    if (!window.confirm(`Delete the offer from ${offer.buyer_name || "this buyer"}?`)) return;
    try {
      await deleteOfferApi(offer.id);
      onChanged();
    } catch (err) {
      setError(err.message);
    }
  }

  if (!editing) {
    return (
      <div onClick={() => setEditing(true)} className="project-row" style={{ marginBottom: 10, flexDirection: "column", alignItems: "stretch", justifyContent: "flex-start" }}>
        <div style={{ display: "flex", justifyContent: "space-between", alignItems: "center" }}>
          <div style={{ fontSize: 16, fontWeight: 700 }}>{offer.buyer_name || "(no buyer name)"}</div>
          <div style={{ display: "flex", gap: 14, alignItems: "center" }}>
            <span style={{ color: "#6AE4FF", fontSize: 13, fontFamily: "'Source Sans 3', sans-serif" }}>Edit →</span>
            <span
              onClick={(e) => { e.stopPropagation(); handleDelete(); }}
              style={{ color: "#EB5757", fontSize: 13, cursor: "pointer", fontFamily: "'Source Sans 3', sans-serif" }}
            >
              Delete
            </span>
          </div>
        </div>
        <div style={{ fontFamily: "'Source Sans 3', sans-serif", fontSize: 13, color: "#CDD0D6", letterSpacing: "normal", marginTop: 4 }}>
          {offer.offer_price ? formatMoneyValue(offer.offer_price) : "No price"}
          {offer.effective_price_after_commission ? ` · Effective: ${formatMoneyValue(offer.effective_price_after_commission)}` : ""}
          {offer.close_of_escrow ? ` · Close: ${offer.close_of_escrow}` : ""}
        </div>
      </div>
    );
  }

  return (
    <div style={{ background: "#202A3E", border: "1px solid #000000", borderRadius: 15, padding: 22, marginBottom: 10 }}>
      <OfferForm form={form} setForm={setForm} onSave={handleSave} onCancel={() => setEditing(false)} onDelete={handleDelete} saving={saving} />
      {error && <div className="error">{error}</div>}
    </div>
  );
}

function OffersSection({ project, onProjectChanged }) {
  const [adding, setAdding] = useState(false);
  const [rawText, setRawText] = useState("");
  const [parsing, setParsing] = useState(false);
  const [parseError, setParseError] = useState(null);
  const [reviewForm, setReviewForm] = useState(null);
  const [savingReview, setSavingReview] = useState(false);
  const [showComparison, setShowComparison] = useState(false);

  async function handleParse() {
    if (!rawText.trim()) return;
    setParsing(true);
    setParseError(null);

    const fieldList = OFFER_FIELDS.map((f) => `"${f.key}": ""`).join(",\n  ");

    const systemPrompt = `You are extracting structured data from a real estate purchase offer that an agent has pasted in (from a spreadsheet row, an email, or a document).

CRITICAL ACCURACY RULE: Only extract information explicitly present in the pasted text. If a field is not clearly stated, return an empty string "" for it. NEVER guess, estimate, round, or infer a plausible-sounding value for anything not actually present. A blank field is the correct, honest answer when information is genuinely missing. This data affects real financial and legal decisions, an invented number is a serious error, far worse than an honest blank.

Do not characterize, judge, or comment on the buyer as a person. Only extract the factual terms of the offer itself.

Field meanings, for context (extract the VALUE for each, not the label):
${OFFER_FIELDS.map((f) => `- ${f.key}: ${f.label}`).join("\n")}

Respond ONLY with valid JSON, no markdown fences, no preamble, in this exact shape (every value a string, empty string if not found):
{
  ${fieldList}
}`;

    const userPrompt = `Here is the pasted offer information:\n\n${rawText}`;

    try {
      const result = await callClaude(systemPrompt, userPrompt, 1800);
      setReviewForm({ ...blankOfferForm(), ...result, rawText });
      setAdding(false);
    } catch (err) {
      setParseError(describeError(err));
    } finally {
      setParsing(false);
    }
  }

  async function handleSaveReview() {
    setSavingReview(true);
    setParseError(null);
    try {
      await addOfferApi(project.id, reviewForm);
      onProjectChanged();
      setReviewForm(null);
      setRawText("");
    } catch (err) {
      setParseError(err.message);
    } finally {
      setSavingReview(false);
    }
  }

  // Pure client-side, built directly from the offers already saved and reviewed by the
  // agent. No AI call: the safest comparison is one with zero chance of a model
  // paraphrasing or misreading a number. Rows where every offer is blank are dropped.
  function buildComparisonGroups() {
    const groups = [];
    OFFER_FIELD_GROUPS.forEach((group) => {
      const groupFields = OFFER_FIELDS.filter((f) => f.group === group);
      const rows = groupFields
        .map((f) => {
          const snakeKey = f.key.replace(/[A-Z]/g, (c) => "_" + c.toLowerCase());
          const values = project.offers.map((o) => o[snakeKey] || "");
          const allBlank = values.every((v) => !v);
          return allBlank ? null : { label: f.label, values: values.map((v) => v || "N/A") };
        })
        .filter(Boolean);
      if (rows.length > 0) groups.push({ group, rows });
    });
    return groups;
  }

  const gridColumns = `1.4fr repeat(${project.offers.length}, 1fr)`;

  return (
    <>
      {project.offers.length === 0 && !adding && !reviewForm && <div className="hint">No offers yet on this project.</div>}

      {!reviewForm && project.offers.map((o) => (
        <OfferRow key={o.id} offer={o} onChanged={onProjectChanged} />
      ))}

      {reviewForm ? (
        <div style={{ marginTop: 12, padding: 16, border: "1px solid #6AE4FF", borderRadius: 15 }}>
          <div className="hint" style={{ marginBottom: 12 }}>
            Review the parsed fields below before saving. Anything blank means it wasn't found in the pasted text, worth a check against the original if that seems off.
          </div>
          <OfferForm form={reviewForm} setForm={setReviewForm} onSave={handleSaveReview} onCancel={() => setReviewForm(null)} saving={savingReview} />
          {parseError && <div className="error">{parseError}</div>}
        </div>
      ) : adding ? (
        <div style={{ marginTop: 12 }}>
          <label style={FIELD_LABEL}>Paste Offer Details</label>
          <textarea
            className="field-input"
            rows={8}
            placeholder="Paste the offer details here, from your spreadsheet, an email, wherever you have them..."
            value={rawText}
            onChange={(e) => setRawText(e.target.value)}
          />
          <div style={{ display: "flex", gap: 8, marginTop: 8 }}>
            <ActionButton label={parsing ? "Parsing..." : "Parse Offer"} onClick={handleParse} />
            <ActionButton label="Cancel" onClick={() => { setAdding(false); setRawText(""); }} />
          </div>
          {parseError && <div className="error">{parseError}</div>}
        </div>
      ) : (
        <div style={{ marginTop: 12, display: "flex", gap: 8, flexWrap: "wrap" }}>
          <ActionButton label="+ Add Offer" onClick={() => setAdding(true)} />
          {project.offers.length >= 2 && (
            <ActionButton label={showComparison ? "Hide Comparison" : "Analyze Offers"} onClick={() => setShowComparison((v) => !v)} />
          )}
        </div>
      )}

      {showComparison && project.offers.length >= 2 && (
        <div className="comparison-grid">
          <div className="comparison-row comparison-header-row" style={{ gridTemplateColumns: gridColumns }}>
            <div className="comparison-label"></div>
            {project.offers.map((o) => (
              <div key={o.id} className="comparison-col-header">{o.buyer_name || "(no buyer name)"}</div>
            ))}
          </div>
          {buildComparisonGroups().map((g) => (
            <div key={g.group}>
              <div className="comparison-group-label">{g.group}</div>
              {g.rows.map((row) => (
                <div key={row.label} className="comparison-row" style={{ gridTemplateColumns: gridColumns }}>
                  <div className="comparison-label">{row.label}</div>
                  {row.values.map((v, i) => (
                    <div key={i} className="comparison-val">{v}</div>
                  ))}
                </div>
              ))}
            </div>
          ))}
          <div className="hint" style={{ marginTop: 14 }}>
            Fields where no offer has a value are left out. No recommendation is made, that call belongs to you and your client.
          </div>
          <div className="export-row" style={{ display: "flex", gap: 10, marginTop: 20 }}>
            <ActionButton label="Export as PDF" onClick={() => window.print()} />
            <ActionButton label="Export as Excel" onClick={() => downloadComparisonCSV(project, buildComparisonGroups())} />
          </div>
        </div>
      )}

      {/* Hidden except when printing/exporting to PDF, see .print-only in the global stylesheet */}
      <div className="print-only">
        <div style={{ fontFamily: "'Inter', sans-serif", padding: 40, color: "#1C2B2E", background: "#FFFFFF" }}>
          <div style={{ fontFamily: "'Open Sans', sans-serif", fontWeight: 700, fontSize: 32, marginBottom: 4 }}>The Agent's Toolkit</div>
          <div style={{ fontSize: 15, color: "#555", marginBottom: 32 }}>
            Offer Comparison{project.address ? ` — ${project.address}` : ""}
          </div>
          <table style={{ width: "100%", borderCollapse: "collapse", fontSize: 13 }}>
            <thead>
              <tr>
                <td style={{ padding: "10px 12px", fontWeight: 700, borderBottom: "2px solid #1C2B2E" }}></td>
                {project.offers.map((o) => (
                  <td key={o.id} style={{ padding: "10px 12px", fontWeight: 700, borderBottom: "2px solid #1C2B2E", textAlign: "center" }}>
                    {o.buyer_name || "(no buyer name)"}
                  </td>
                ))}
              </tr>
            </thead>
            <tbody>
              {buildComparisonGroups().map((g) => (
                <React.Fragment key={g.group}>
                  <tr>
                    <td colSpan={project.offers.length + 1} style={{ padding: "16px 12px 6px", fontWeight: 700, fontSize: 11, letterSpacing: "0.05em", textTransform: "uppercase", color: "#888" }}>
                      {g.group}
                    </td>
                  </tr>
                  {g.rows.map((row) => (
                    <tr key={row.label}>
                      <td style={{ padding: "6px 12px", borderBottom: "1px solid #E5E0D5", color: "#555" }}>{row.label}</td>
                      {row.values.map((v, i) => (
                        <td key={i} style={{ padding: "6px 12px", borderBottom: "1px solid #E5E0D5", textAlign: "center" }}>{v}</td>
                      ))}
                    </tr>
                  ))}
                </React.Fragment>
              ))}
            </tbody>
          </table>
          <div style={{ fontSize: 11, color: "#999", marginTop: 24 }}>
            Fields where no offer has a value are left out. This report presents facts only, no recommendation is made.
          </div>
        </div>
      </div>
    </>
  );
}

function CompForm({ form, setForm, onSave, onCancel, onDelete, saving, requireRadius }) {
  const canSave = !requireRadius || !!form.radius;
  return (
    <div className="split-editor">
      <div className="split-editor-paste">
        <label style={FIELD_LABEL}>Original Pasted Text</label>
        <textarea
          className="field-input split-editor-textarea"
          value={form.rawText || ""}
          onChange={(e) => setForm({ ...form, rawText: e.target.value })}
        />
        <div className="hint">Keep this visible while you check the parsed fields on the right against it.</div>
      </div>

      <div className="split-editor-fields">
        <div style={{ display: "grid", gridTemplateColumns: "1fr 1fr", gap: 10, marginBottom: 14 }}>
          {COMP_FIELDS.map((f) => (
            <div key={f.key} style={f.key === "address" ? { gridColumn: "1 / -1" } : undefined}>
              <label style={{ ...FIELD_LABEL, fontSize: 10.5 }}>{f.label}</label>
              <input
                className="field-input"
                placeholder="Not found in source"
                value={form[f.key] || ""}
                onChange={(e) => setForm({ ...form, [f.key]: e.target.value })}
              />
            </div>
          ))}
        </div>

        <div style={{ marginBottom: 14 }}>
          <label style={FIELD_LABEL}>Radius From Subject Property</label>
          <select className="field-input" value={form.radius || ""} onChange={(e) => setForm({ ...form, radius: e.target.value })}>
            <option value="">Select radius...</option>
            {RADIUS_OPTIONS.map((r) => <option key={r} value={r}>{r}</option>)}
          </select>
          <div className="hint">Your call, not parsed automatically, same as an appraiser would judge it.</div>
        </div>

        <div style={{ display: "flex", gap: 8, flexWrap: "wrap" }}>
          <ActionButton label={saving ? "Saving..." : "Save Comp"} onClick={canSave ? onSave : undefined} />
          <ActionButton label="Cancel" onClick={onCancel} />
          {onDelete && <ActionButton label="Delete Comp" onClick={onDelete} />}
        </div>
        {requireRadius && !form.radius && <div className="hint">Pick a radius before saving.</div>}
      </div>
    </div>
  );
}

function CompRow({ comp, onChanged }) {
  const [editing, setEditing] = useState(false);
  const [form, setForm] = useState(() => compFromApi(comp));
  const [saving, setSaving] = useState(false);
  const [error, setError] = useState(null);

  async function handleSave() {
    setSaving(true);
    setError(null);
    try {
      await updateCompApi(comp.id, form);
      onChanged();
      setEditing(false);
    } catch (err) {
      setError(err.message);
    } finally {
      setSaving(false);
    }
  }

  async function handleDelete() {
    if (!window.confirm(`Delete the comp at ${comp.address || "this address"}?`)) return;
    try {
      await deleteCompApi(comp.id);
      onChanged();
    } catch (err) {
      setError(err.message);
    }
  }

  if (!editing) {
    return (
      <div onClick={() => setEditing(true)} className="project-row" style={{ marginBottom: 10, flexDirection: "column", alignItems: "stretch", justifyContent: "flex-start" }}>
        <div style={{ display: "flex", justifyContent: "space-between", alignItems: "center" }}>
          <div style={{ fontSize: 16, fontWeight: 700 }}>{comp.address || "(no address)"}</div>
          <div style={{ display: "flex", gap: 14, alignItems: "center" }}>
            <span style={{ color: "#6AE4FF", fontSize: 13, fontFamily: "'Source Sans 3', sans-serif" }}>Edit →</span>
            <span
              onClick={(e) => { e.stopPropagation(); handleDelete(); }}
              style={{ color: "#EB5757", fontSize: 13, cursor: "pointer", fontFamily: "'Source Sans 3', sans-serif" }}
            >
              Delete
            </span>
          </div>
        </div>
        <div style={{ fontFamily: "'Source Sans 3', sans-serif", fontSize: 13, color: "#CDD0D6", letterSpacing: "normal", marginTop: 4 }}>
          {comp.price ? formatMoneyValue(comp.price) : "No price"}
          {comp.beds ? ` · ${comp.beds}bd` : ""}
          {comp.baths ? `/${comp.baths}ba` : ""}
          {comp.sqft ? ` · ${comp.sqft} sqft` : ""}
          {comp.days_on_market ? ` · ${comp.days_on_market} DOM` : ""}
          {comp.close_date ? ` · ${comp.close_date}` : ""}
        </div>
      </div>
    );
  }

  return (
    <div style={{ background: "#202A3E", border: "1px solid #000000", borderRadius: 15, padding: 22, marginBottom: 10 }}>
      <CompForm form={form} setForm={setForm} onSave={handleSave} onCancel={() => setEditing(false)} onDelete={handleDelete} saving={saving} requireRadius />
      {error && <div className="error">{error}</div>}
    </div>
  );
}

function CompsSection({ project, onProjectChanged }) {
  const [adding, setAdding] = useState(false);
  const [rawText, setRawText] = useState("");
  const [parsing, setParsing] = useState(false);
  const [parseError, setParseError] = useState(null);
  const [reviewForm, setReviewForm] = useState(null);
  const [savingReview, setSavingReview] = useState(false);

  async function handleParse() {
    if (!rawText.trim()) return;
    setParsing(true);
    setParseError(null);

    const fieldList = COMP_FIELDS.map((f) => `"${f.key}": ""`).join(",\n  ");

    const systemPrompt = `You are extracting structured data from a comparable property (a "comp") that an agent has pasted in, usually copied from an MLS listing, Redfin, Zillow, or similar.

CRITICAL ACCURACY RULE: Only extract information explicitly present in the pasted text. If a field is not clearly stated, return an empty string "" for it. NEVER guess, estimate, round, or infer a plausible-sounding value for anything not actually present. A blank field is the correct, honest answer when information is genuinely missing. This data may factor into real pricing decisions, an invented number is a serious error, far worse than an honest blank.

If the property hasn't closed yet, closeDate should be "Pending" only if the source text actually indicates that, otherwise leave it blank.

Field meanings, for context (extract the VALUE for each, not the label):
${COMP_FIELDS.map((f) => `- ${f.key}: ${f.label}`).join("\n")}

Respond ONLY with valid JSON, no markdown fences, no preamble, in this exact shape (every value a string, empty string if not found):
{
  ${fieldList}
}`;

    const userPrompt = `Here is the pasted comp information:\n\n${rawText}`;

    try {
      const result = await callClaude(systemPrompt, userPrompt, 800);
      setReviewForm({ ...blankCompForm(), ...result, rawText });
      setAdding(false);
    } catch (err) {
      setParseError(describeError(err));
    } finally {
      setParsing(false);
    }
  }

  async function handleSaveReview() {
    if (!reviewForm.radius) return;
    setSavingReview(true);
    setParseError(null);
    try {
      await addCompApi(project.id, reviewForm);
      onProjectChanged();
      setReviewForm(null);
      setRawText("");
    } catch (err) {
      setParseError(err.message);
    } finally {
      setSavingReview(false);
    }
  }

  const grouped = RADIUS_OPTIONS.map((r) => ({
    radius: r,
    comps: project.comps.filter((c) => c.radius === r),
  })).filter((g) => g.comps.length > 0);

  const ungrouped = project.comps.filter((c) => !RADIUS_OPTIONS.includes(c.radius));

  return (
    <>
      {project.comps.length === 0 && !adding && !reviewForm && <div className="hint">No comps added yet for this project.</div>}

      {!reviewForm && grouped.map((g) => (
        <div key={g.radius} style={{ marginBottom: 14 }}>
          <div style={{ fontSize: 11, fontWeight: 600, letterSpacing: "0.04em", textTransform: "uppercase", color: "#6AE4FF", marginBottom: 4 }}>
            Within {g.radius}
          </div>
          {g.comps.map((c) => <CompRow key={c.id} comp={c} onChanged={onProjectChanged} />)}
        </div>
      ))}
      {!reviewForm && ungrouped.length > 0 && (
        <div style={{ marginBottom: 14 }}>
          <div style={{ fontSize: 11, fontWeight: 600, letterSpacing: "0.04em", textTransform: "uppercase", color: "#CDD0D6", marginBottom: 4 }}>
            No Radius Set
          </div>
          {ungrouped.map((c) => <CompRow key={c.id} comp={c} onChanged={onProjectChanged} />)}
        </div>
      )}

      {reviewForm ? (
        <div style={{ marginTop: 12, padding: 16, border: "1px solid #6AE4FF", borderRadius: 15 }}>
          <div className="hint" style={{ marginBottom: 12 }}>
            Review the parsed fields below, pick a radius, then save. Anything blank means it wasn't found in the pasted text.
          </div>
          <CompForm form={reviewForm} setForm={setReviewForm} onSave={handleSaveReview} onCancel={() => setReviewForm(null)} saving={savingReview} requireRadius />
          {parseError && <div className="error">{parseError}</div>}
        </div>
      ) : adding ? (
        <div style={{ marginTop: 12 }}>
          <label style={FIELD_LABEL}>Paste Comp Details</label>
          <textarea
            className="field-input"
            rows={6}
            placeholder="Paste what you copied from MLS, Redfin, Zillow, wherever you have it..."
            value={rawText}
            onChange={(e) => setRawText(e.target.value)}
          />
          <div style={{ display: "flex", gap: 8, marginTop: 8 }}>
            <ActionButton label={parsing ? "Parsing..." : "Parse Comp"} onClick={handleParse} />
            <ActionButton label="Cancel" onClick={() => { setAdding(false); setRawText(""); }} />
          </div>
          {parseError && <div className="error">{parseError}</div>}
        </div>
      ) : (
        <ActionButton label="+ Add Comp" onClick={() => setAdding(true)} />
      )}
    </>
  );
}

function ProjectEditor({ project, onChanged, onDeleted, onClose, onDirtyChange }) {
  const isNew = !project.id;
  const initialForm = {
    address: project.address || "",
    propertyType: project.property_type || "Single-Family",
    beds: project.beds || "",
    baths: project.baths || "",
    sqft: project.sqft || "",
    price: project.price || "",
    features: project.features || "",
    side: project.side || "buyer_side",
    photoAlbumUrl: project.photo_album_url || "",
  };
  const [form, setForm] = useState(initialForm);
  const [saving, setSaving] = useState(false);
  const [error, setError] = useState(null);

  useEffect(() => {
    const dirty = JSON.stringify(form) !== JSON.stringify(initialForm);
    onDirtyChange(dirty);
  }, [form]);

  useEffect(() => {
    // Clear the dirty flag when this editor unmounts (e.g. after a successful save+close).
    return () => onDirtyChange(false);
  }, []);

  async function handleSave() {
    setSaving(true);
    setError(null);
    try {
      if (isNew) {
        await createProject(form);
      } else {
        await updateProject(project.id, { ...form, status: project.status });
      }
      onChanged();
      onClose();
    } catch (err) {
      setError(err.message);
    } finally {
      setSaving(false);
    }
  }

  async function handleDelete() {
    if (!window.confirm(`Delete the project for ${project.address || "this address"}? This also removes its contacts.`)) return;
    try {
      await deleteProjectApi(project.id);
      onDeleted();
      onClose();
    } catch (err) {
      setError(err.message);
    }
  }

  return (
    <div style={{ maxWidth: 880, margin: "0 auto" }}>
      <ToolHeader title={isNew ? "New Project" : "Edit Project Details"} onBack={onClose} />

      <div style={{ display: "grid", gridTemplateColumns: "1fr 1fr", gap: 16, marginTop: 20 }}>
        <div style={{ gridColumn: "1 / -1" }}>
          <label style={FIELD_LABEL}>Address</label>
          <input className="field-input" placeholder="e.g. 412 Almaden Rd" value={form.address} onChange={(e) => setForm({ ...form, address: e.target.value })} />
        </div>
        <div>
          <label style={FIELD_LABEL}>Representing</label>
          <select className="field-input" value={form.side} onChange={(e) => setForm({ ...form, side: e.target.value })}>
            {PROJECT_SIDES.map((s) => <option key={s.value} value={s.value}>{s.label}</option>)}
          </select>
        </div>
        <div>
          <label style={FIELD_LABEL}>Property Type</label>
          <select className="field-input" value={form.propertyType} onChange={(e) => setForm({ ...form, propertyType: e.target.value })}>
            {PROPERTY_TYPES.map((t) => <option key={t}>{t}</option>)}
          </select>
        </div>
        <div>
          <label style={FIELD_LABEL}>Beds</label>
          <input className="field-input" value={form.beds} onChange={(e) => setForm({ ...form, beds: e.target.value })} />
        </div>
        <div>
          <label style={FIELD_LABEL}>Baths</label>
          <input className="field-input" value={form.baths} onChange={(e) => setForm({ ...form, baths: e.target.value })} />
        </div>
        <div>
          <label style={FIELD_LABEL}>Sqft</label>
          <input className="field-input" value={form.sqft} onChange={(e) => setForm({ ...form, sqft: e.target.value })} />
        </div>
        <div>
          <label style={FIELD_LABEL}>Price</label>
          <input className="field-input" value={form.price} onChange={(e) => setForm({ ...form, price: e.target.value })} />
        </div>
        <div style={{ gridColumn: "1 / -1" }}>
          <label style={FIELD_LABEL}>Key Features</label>
          <textarea className="field-input" rows={2} value={form.features} onChange={(e) => setForm({ ...form, features: e.target.value })} />
        </div>
        <div style={{ gridColumn: "1 / -1" }}>
          <label style={FIELD_LABEL}>Photo Album Link (optional)</label>
          <input
            className="field-input"
            placeholder="Google Drive, Google Photos, or any shareable album link"
            value={form.photoAlbumUrl}
            onChange={(e) => setForm({ ...form, photoAlbumUrl: e.target.value })}
          />
          <div className="hint">Paste a link to an album you already manage. Available to include in Follow-Up and Reply emails for this project.</div>
        </div>
      </div>

      <div style={{ display: "flex", gap: 8, marginTop: 20 }}>
        <ActionButton label={saving ? "Saving..." : "Save Project"} onClick={handleSave} />
        {!isNew && <ActionButton label="Delete Project" onClick={handleDelete} />}
      </div>
      {error && <div className="error">{error}</div>}
    </div>
  );
}

// Lightweight, self-contained: fetches this one project's open house events to compute
// a real "needs attention" count (visitors still at follow_up_status "not_started").
// Renders nothing if there's nothing to flag, so it's safe to drop into any row.
function ProjectAttentionBadge({ projectId, onCount }) {
  const [count, setCount] = useState(null);

  useEffect(() => {
    let cancelled = false;
    fetchOpenHouseEvents(projectId)
      .then((events) => {
        if (cancelled) return;
        const needing = events.reduce(
          (sum, ev) => sum + ev.visitors.filter((v) => (v.follow_up_status || "not_started") === "not_started").length,
          0
        );
        setCount(needing);
        if (onCount) onCount(projectId, needing);
      })
      .catch(() => { if (!cancelled) setCount(0); });
    return () => { cancelled = true; };
  }, [projectId]);

  if (!count) return null;
  return <span className="attention-badge">{count}</span>;
}

// Aggregates real open-house follow-up data across every project, for the Home screen.
// Fetches once per project (same underlying API as ProjectAttentionBadge, just summed up).
// Renders nothing at all if there's genuinely nothing to show, no placeholder, no fake zero-state text.
function HomeAttentionSection({ projects, onFollowUpVisitor, onOpenClient }) {
  const [visitorPending, setVisitorPending] = useState([]); // [{ projectId, address, visitorName, detail }]
  const [clientPending, setClientPending] = useState([]); // [{ contactId, name, reason, days }]
  const [loaded, setLoaded] = useState(false);

  useEffect(() => {
    let cancelled = false;

    const visitorPromise =
      projects.length === 0
        ? Promise.resolve([])
        : Promise.all(
            projects.map((p) =>
              fetchOpenHouseEvents(p.id)
                .then((events) => {
                  const items = [];
                  events.forEach((ev) => {
                    ev.visitors.forEach((v) => {
                      if ((v.follow_up_status || "not_started") === "not_started") {
                        const parts = [];
                        parts.push(INTEREST_LABELS[v.interest_level] || v.interest_level || "");
                        if (v.wants_disclosures) parts.push("wants disclosures");
                        if (v.wants_photos) parts.push("wants photos");
                        items.push({ projectId: p.id, address: p.address || "(no address)", visitorName: v.name, detail: parts.filter(Boolean).join(", ") });
                      }
                    });
                  });
                  return items;
                })
                .catch(() => [])
            )
          ).then((results) => results.flat());

    const clientPromise = fetchAllContacts()
      .then((contacts) =>
        contacts
          .filter((c) => c.active_status === "active" && c.waiting_reason)
          .map((c) => ({ contactId: c.id, name: c.name, reason: c.waiting_reason, days: daysSince(c.last_contact_date) }))
      )
      .catch(() => []);

    Promise.all([visitorPromise, clientPromise]).then(([visitors, clients]) => {
      if (cancelled) return;
      setVisitorPending(visitors);
      setClientPending(clients);
      setLoaded(true);
    });
    return () => { cancelled = true; };
  }, [projects]);

  if (!loaded) return null;
  const combined = [
    ...visitorPending.map((v) => ({ kind: "visitor", ...v })),
    ...clientPending.map((c) => ({ kind: "client", ...c })),
  ];
  if (combined.length === 0) return null;

  function itemLabel(item) {
    if (item.kind === "visitor") return item.visitorName;
    return item.name;
  }
  function itemDetail(item) {
    if (item.kind === "visitor") return `${item.address}${item.detail ? ` · ${item.detail}` : ""}`;
    return `${item.days === null ? "Not yet contacted" : `${item.days} day${item.days === 1 ? "" : "s"} since contact`} — ${item.reason}`;
  }
  function openItem(item) {
    if (item.kind === "visitor") onFollowUpVisitor(item.projectId, item.visitorName, item.detail);
    else onOpenClient(item.contactId);
  }

  const first = combined[0];
  const rest = combined.length - 1;

  return (
    <div className="section-eyebrow" style={{ marginTop: 0, marginBottom: 32 }}>
      Needs Your Attention
      <div style={{ marginTop: 10 }}>
        <div className="tool-item" style={{ marginBottom: rest > 0 ? 10 : 0 }} onClick={() => openItem(first)}>
          <div className="item-text">
            <span className="item-name">{itemLabel(first)}</span>
            <span className="item-desc">{itemDetail(first)}</span>
          </div>
          <span className="item-arrow">→</span>
        </div>
        {rest > 0 && (
          <div className="tool-item" onClick={() => openItem(combined[1])}>
            <div className="item-text">
              <span className="item-name">{rest} more need{rest === 1 ? "s" : ""} attention</span>
            </div>
            <span className="item-arrow">→</span>
          </div>
        )}
      </div>
    </div>
  );
}

function ProjectsManager({ projects, onProjectsChange, onClose, isDirty, onDirtyChange, guardNavigate, editingProjectId, setEditingProjectId, detailView, setDetailView, onSelectProject, onOpenTool, onFollowUpVisitor }) {
  const [attentionCounts, setAttentionCounts] = useState({});
  const guardingBack = useJustMounted(200, [detailView, editingProjectId]);

  function handleAttentionCount(projectId, count) {
    setAttentionCounts((prev) => (prev[projectId] === count ? prev : { ...prev, [projectId]: count }));
  }

  const editingProject =
    editingProjectId === "new"
      ? { contacts: [], offers: [], comps: [] }
      : editingProjectId
      ? projects.find((p) => p.id === editingProjectId)
      : null;

  if (editingProjectId && editingProjectId !== "new" && !editingProject) {
    // The project we were viewing no longer exists (e.g. deleted elsewhere). Fall back to the list.
    setEditingProjectId(null);
    setDetailView(null);
  }

  function openProject(id) {
    setEditingProjectId(id);
    onSelectProject(id);
    setDetailView(id === "new" ? "edit" : null);
  }

  function backToList() {
    guardNavigate(() => {
      setEditingProjectId(null);
      setDetailView(null);
    });
  }

  function backToDetail() {
    guardNavigate(() => setDetailView(null));
  }

  // Creating a brand-new project: nothing else makes sense to show until it exists.
  if (editingProjectId === "new") {
    return (
      <ProjectEditor
        project={editingProject}
        onChanged={onProjectsChange}
        onDeleted={onProjectsChange}
        onDirtyChange={onDirtyChange}
        onClose={backToList}
      />
    );
  }

  if (!editingProject) {
    // PROJECTS LIST
    return (
      <div style={{ maxWidth: 880, margin: "0 auto" }}>
        <div style={{ display: "flex", justifyContent: "space-between", alignItems: "center", marginBottom: 24 }}>
          <div style={{ display: "flex", alignItems: "center", gap: 14 }}>
            <button
              onClick={onClose}
              disabled={guardingBack}
              style={{ background: "#202A3E", border: "1px solid #000000", color: "#FFFFFF", fontSize: 18, cursor: guardingBack ? "default" : "pointer", padding: "8px 14px", borderRadius: 8 }}
            >
              ←
            </button>
            <h2 style={{ fontWeight: 700, fontSize: 28, color: "#FFFFFF", margin: 0 }}>Projects</h2>
          </div>
          <span className="voice-link" onClick={() => onOpenTool("voice", false)}>Voice</span>
        </div>

        <div className="add-btn" onClick={() => openProject("new")}>+ New Project</div>

        {projects.length === 0 && (
          <div className="hint">No projects yet. Click "+ New Project" to save your first address.</div>
        )}

        {projects.map((p) => (
          <div
            key={p.id}
            className={`project-row ${attentionCounts[p.id] > 0 ? "needs-attention" : ""}`}
            onClick={() => openProject(p.id)}
            style={{ marginBottom: 12 }}
          >
            <div className="item-text">
              <span className="project-row-address">{p.address || "(no address)"}</span>
              <span className="project-row-meta">
                {PROJECT_SIDES.find((s) => s.value === p.side)?.label || "No side set"} · {p.contacts.length} contact{p.contacts.length === 1 ? "" : "s"}
              </span>
            </div>
            <ProjectAttentionBadge projectId={p.id} onCount={handleAttentionCount} />
          </div>
        ))}
      </div>
    );
  }

  // We have a real, existing project selected. Route by detailView.
  if (detailView === "edit") {
    return <ProjectEditor project={editingProject} onChanged={onProjectsChange} onDeleted={() => { onProjectsChange(); backToList(); }} onDirtyChange={onDirtyChange} onClose={backToDetail} />;
  }
  if (detailView === "contacts") {
    return <ContactsView project={editingProject} onChanged={onProjectsChange} onBack={backToDetail} />;
  }
  if (detailView === "offers") {
    return <OffersView project={editingProject} onChanged={onProjectsChange} onBack={backToDetail} />;
  }
  if (detailView === "comps") {
    return <CompsView project={editingProject} onChanged={onProjectsChange} onBack={backToDetail} />;
  }
  if (detailView === "openhouse") {
    return <OpenHouseView project={editingProject} onChanged={onProjectsChange} onBack={backToDetail} onOpenTool={onOpenTool} onSelectProject={onSelectProject} onFollowUpVisitor={onFollowUpVisitor} />;
  }

  // detailView === null: the Project Detail overview
  return (
    <div style={{ maxWidth: 880, margin: "0 auto" }}>
      <div style={{ display: "flex", alignItems: "center", gap: 14, marginBottom: 24 }}>
        <button
          onClick={backToList}
          disabled={guardingBack}
          style={{ background: "#202A3E", border: "1px solid #000000", color: "#FFFFFF", fontSize: 18, cursor: guardingBack ? "default" : "pointer", padding: "8px 14px", borderRadius: 8 }}
        >
          ←
        </button>
        <h2 style={{ fontWeight: 700, fontSize: 24, color: "#FFFFFF", margin: 0 }}>
          {editingProject.address || "(no address)"}
          <span className="side-tag">{PROJECT_SIDES.find((s) => s.value === editingProject.side)?.label || "Side not set"}</span>
        </h2>
      </div>

      <div className="section-divider">Project Info</div>
      <div className="info-grid">
        <div className="info-tile" onClick={() => setDetailView("edit")}>
          <div className="info-tile-top"><div className="info-icon">✎</div></div>
          <div className="info-tile-label" style={{ fontSize: 15, fontWeight: 600, color: "#FFFFFF", marginBottom: 4 }}>Edit Details</div>
          <div className="info-tile-label">Address, price, beds, and more</div>
        </div>
        <div className="info-tile" onClick={() => setDetailView("contacts")}>
          <div className="info-tile-top"><div className="info-icon">▤</div></div>
          <div className="info-tile-num">{editingProject.contacts.length}</div>
          <div className="info-tile-label">Contact{editingProject.contacts.length === 1 ? "" : "s"}</div>
        </div>
        <div className="info-tile" onClick={() => setDetailView("offers")}>
          <div className="info-tile-top"><div className="info-icon">☰</div></div>
          <div className="info-tile-num">{editingProject.offers.length}</div>
          <div className="info-tile-label">Offer{editingProject.offers.length === 1 ? "" : "s"}</div>
        </div>
        <div className="info-tile" onClick={() => setDetailView("comps")}>
          <div className="info-tile-top"><div className="info-icon">⌂</div></div>
          <div className="info-tile-num">{editingProject.comps.length}</div>
          <div className="info-tile-label">Comp{editingProject.comps.length === 1 ? "" : "s"} saved</div>
        </div>
        <div className="info-tile" onClick={() => setDetailView("openhouse")} style={{ gridColumn: "1 / -1" }}>
          <div className="info-tile-top">
            <div className="info-icon">▦</div>
            <ProjectAttentionBadge projectId={editingProject.id} />
          </div>
          <div className="info-tile-label" style={{ fontSize: 15, fontWeight: 600, color: "#FFFFFF" }}>Open House Events</div>
        </div>
      </div>

      <div className="section-divider">Tools for This Project</div>
      <div className="detail-item" style={{ marginBottom: 10 }} onClick={() => onOpenTool("listing", true)}>
        <div className="item-text"><span className="detail-item-label">Listing</span><span className="detail-item-meta">Create your own listing in whatever tone and length you desire.</span></div>
        <span className="row-arrow">→</span>
      </div>
      <div className="detail-item" style={{ marginBottom: 10 }} onClick={() => onOpenTool("social", true)}>
        <div className="item-text"><span className="detail-item-label">Social</span><span className="detail-item-meta">Turn your listing into a caption ready for Instagram, Facebook, LinkedIn, or TikTok.</span></div>
        <span className="row-arrow">→</span>
      </div>
      <div className="detail-item" style={{ marginBottom: 10 }} onClick={() => onOpenTool("email", true)}>
        <div className="item-text"><span className="detail-item-label">Follow-Up</span><span className="detail-item-meta">Draft a warm, professional follow-up in seconds. Connect a contact first so it's personal, not generic.</span></div>
        <span className="row-arrow">→</span>
      </div>
      <div className="detail-item" onClick={() => onOpenTool("reply", true)}>
        <div className="item-text"><span className="detail-item-label">Reply</span><span className="detail-item-meta">Paste in any email, from a client, another agent, or a potential buyer. Connect a contact first for extra context.</span></div>
        <span className="row-arrow">→</span>
      </div>
    </div>
  );
}

function ContactsView({ project, onChanged, onBack }) {
  return (
    <div style={{ maxWidth: 880, margin: "0 auto" }}>
      <ToolHeader title="Contacts" onBack={onBack} />
      <AddContactForm projectId={project.id} onAdded={onChanged} />
      {project.contacts.length === 0 && <div className="hint">No contacts yet.</div>}
      {project.contacts.map((c) => (
        <ContactRow key={c.id} contact={c} projectId={project.id} onChanged={onChanged} />
      ))}
    </div>
  );
}

function OffersView({ project, onChanged, onBack }) {
  return (
    <div style={{ maxWidth: 880, margin: "0 auto" }}>
      <ToolHeader title="Offers" onBack={onBack} />
      <OffersSection project={project} onProjectChanged={onChanged} />
    </div>
  );
}

function CompsView({ project, onChanged, onBack }) {
  return (
    <div style={{ maxWidth: 880, margin: "0 auto" }}>
      <ToolHeader title="Comps" onBack={onBack} />
      <CompsSection project={project} onProjectChanged={onChanged} />
    </div>
  );
}

function OpenHouseView({ project, onChanged, onBack, onOpenTool, onSelectProject, onFollowUpVisitor }) {
  return (
    <div style={{ maxWidth: 880, margin: "0 auto" }}>
      <ToolHeader title="Open House Events" onBack={onBack} />
      <OpenHouseSection
        project={project}
        onProjectContactsChanged={onChanged}
        onFollowUpVisitor={(visitorName, detailText) => onFollowUpVisitor(project.id, visitorName, detailText)}
      />
    </div>
  );
}

// Guards against a real timing gap in this app's no-build-step architecture: JSX is compiled
// live in the browser on every page load, so there's a brief window right after a screen
// renders where it's visually painted but not yet fully interactive. A click landing in that
// window does nothing, silently. 200ms is well below what a person notices as a delay, but
// closes the gap. Re-triggers whenever deps change, so it covers screens reached via internal
// navigation within an already-mounted component (e.g. Project Detail -> Offers), not just
// the first mount. Scoped to back-navigation buttons specifically, where this was reported.
function useJustMounted(ms, deps) {
  const [justMounted, setJustMounted] = useState(true);
  useEffect(() => {
    setJustMounted(true);
    const t = setTimeout(() => setJustMounted(false), ms);
    return () => clearTimeout(t);
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, deps || []);
  return justMounted;
}

function ToolHeader({ title, oneLiner, onBack }) {
  const guarding = useJustMounted(200);
  return (
    <div style={{ marginBottom: 8 }}>
      <div style={{ display: "flex", alignItems: "center", gap: 14, marginBottom: 4 }}>
        <button
          onClick={onBack}
          disabled={guarding}
          style={{ background: "#202A3E", border: "1px solid #000000", color: "#FFFFFF", fontSize: 18, cursor: guarding ? "default" : "pointer", padding: "8px 14px", borderRadius: 8 }}
        >
          ←
        </button>
        <h2 style={{ fontWeight: 700, fontSize: 28, color: "#FFFFFF", margin: 0 }}>{title}</h2>
      </div>
      {oneLiner && <div className="tool-one-liner">{oneLiner}</div>}
    </div>
  );
}

function VoiceTab({ voiceProfile, onVoiceProfileChange }) {
  // First-time setup (no profile yet)
  const [samples, setSamples] = useState("");
  const [calibrating, setCalibrating] = useState(false);
  const [calibrateError, setCalibrateError] = useState(null);

  // Refining an existing profile with a specific correction/note
  const [extraNote, setExtraNote] = useState("");
  const [refining, setRefining] = useState(false);
  const [refineError, setRefineError] = useState(null);
  const [notesOpen, setNotesOpen] = useState(false);

  // Viewing the current profile (open by default on landing, can be hidden)
  const [profileOpen, setProfileOpen] = useState(true);

  // Recalibrating from scratch (hidden until requested)
  const [showRecalibrate, setShowRecalibrate] = useState(false);

  // Confirm step before clearing
  const [showClearConfirm, setShowClearConfirm] = useState(false);

  const canCalibrate = samples.trim().length > 80;
  const canRefine = extraNote.trim().length > 5;

  async function handleCalibrate() {
    if (!canCalibrate) return;
    setCalibrating(true);
    setCalibrateError(null);

    const systemPrompt = `You are analyzing writing samples to build a concise "voice profile" for a real estate agent, so future AI-generated copy (listings, captions, emails) can match how this specific person actually writes.

Read the samples and describe: typical sentence length and rhythm, formality level, word choices they favor or avoid, any recurring phrases or verbal habits, and overall tone. Be specific and actionable for another writer to follow. "Professional but warm" is too vague. "Uses short sentences, rarely more than 15 words, favors concrete nouns over adjectives, tends to open emails with the client's first name and a short direct sentence" is useful.

Do not critique the writing or comment on its quality, just describe the voice itself. Do not assume the agent works on a team, has an assistant, or any other detail not evident from the samples themselves.

Respond ONLY with valid JSON, no markdown fences, no preamble, in this exact shape:
{"voiceDescriptor": "a 100-150 word description of the writing voice, written as instructions for another writer to follow"}`;

    const userPrompt = `Here are writing samples from this agent (past emails, listings, or texts to clients):\n\n${samples}`;

    try {
      const result = await callClaude(systemPrompt, userPrompt, 500);
      onVoiceProfileChange(result.voiceDescriptor);
      setSamples("");
      setShowRecalibrate(false);
    } catch (err) {
      console.error(err);
      setCalibrateError(describeError(err));
    } finally {
      setCalibrating(false);
    }
  }

  async function handleRefine() {
    if (!canRefine) return;
    setRefining(true);
    setRefineError(null);

    const systemPrompt = `You are refining an existing "voice profile" for a real estate agent based on their direct feedback.

Here is the current voice profile:
${voiceProfile}

The agent has given this correction or preference:
${extraNote}

Produce an UPDATED voice profile that incorporates this feedback. If the feedback is a correction (something to stop doing), don't just remove the incorrect detail, replace it with an explicit rule stating the correct alternative, so it's actively enforced going forward rather than left as a silent gap. For example, if told "don't say I use 'we', I work solo," the updated profile should state "writes in first-person singular 'I', not 'we', since they work solo" rather than simply deleting the old sentence about "we." Keep everything else that's still accurate unchanged. Do not add new assumptions beyond what's stated. Keep it specific and actionable for another writer to follow, 100-150 words.

Respond ONLY with valid JSON, no markdown fences, no preamble, in this exact shape:
{"voiceDescriptor": "the updated 100-150 word description"}`;

    try {
      const result = await callClaude(systemPrompt, "Update the voice profile now.", 500);
      onVoiceProfileChange(result.voiceDescriptor);
      setExtraNote("");
    } catch (err) {
      console.error(err);
      setRefineError(describeError(err));
    } finally {
      setRefining(false);
    }
  }

  function handleConfirmClear() {
    onVoiceProfileChange("");
    setShowClearConfirm(false);
    setShowRecalibrate(false);
  }

  // --- State 1: no profile yet, first-time setup ---
  if (!voiceProfile) {
    return (
      <div style={CARD}>
        <p style={{ fontSize: 13.5, color: "#CDD0D6", lineHeight: 1.6, marginTop: 0 }}>
          Paste 2-3 examples of your own writing below (past emails, listing descriptions, or texts to clients, whatever's genuinely yours). This gets analyzed once to learn your natural voice, then every tab in this toolkit will write in that voice automatically. This is saved to your account, so it'll be there the next time you log in, on this device or any other.
        </p>
        <label style={FIELD_LABEL}>Paste Writing Samples</label>
        <div style={{ position: "relative" }}>
          <textarea
            className="field-input"
            rows={8}
            style={{ paddingBottom: 60 }}
            placeholder="Paste a past email here, then a blank line, then another sample..."
            value={samples}
            onChange={(e) => setSamples(e.target.value)}
          />
          <button
            className="generate-btn"
            style={{ position: "absolute", bottom: 12, right: 12, width: "auto", marginTop: 0, padding: "12px 20px" }}
            disabled={!canCalibrate || calibrating}
            onClick={handleCalibrate}
          >
            {calibrating ? "Learning your voice..." : "Calibrate My Voice"}
          </button>
        </div>
        {!canCalibrate && <div className="hint">Paste at least a couple of real sentences, ideally 2-3 separate examples.</div>}
        {calibrateError && <div className="error">{calibrateError}</div>}
      </div>
    );
  }

  // --- State 2: active profile, refinement + recalibrate + clear ---
  return (
    <div style={CARD}>
      <button
        onClick={() => setProfileOpen((v) => !v)}
        style={{
          width: "100%",
          display: "flex",
          justifyContent: "space-between",
          alignItems: "center",
          background: "none",
          border: "none",
          padding: 0,
          cursor: "pointer",
          textAlign: "left",
        }}
      >
        <span style={{ ...FIELD_LABEL, marginBottom: 0 }}>View Your Active Voice Profile</span>
        <span style={{ color: "#6AE4FF", fontSize: 12, fontWeight: 600 }}>{profileOpen ? "Hide ▲" : "Show ▼"}</span>
      </button>

      {profileOpen && (
        <div
          style={{
            marginTop: 14,
            background: "#202A3E",
            border: "1px solid #000000",
            borderRadius: 15,
            overflow: "hidden",
          }}
        >
          <div
            style={{
              background: "#17202E",
              padding: "16px 20px",
              fontSize: 14,
              letterSpacing: "0.05em",
              textTransform: "uppercase",
              color: "#6AE4FF",
              fontWeight: 700,
            }}
          >
            Personalized Voice Summary
          </div>
          <p
            style={{
              fontFamily: "'Source Sans 3', sans-serif",
              fontWeight: 400,
              fontSize: 15,
              lineHeight: 1.85,
              letterSpacing: "normal",
              color: "#FFFFFF",
              margin: 0,
              padding: "20px",
            }}
          >
            {voiceProfile}
          </p>
        </div>
      )}

      <div style={{ marginTop: 20, paddingTop: 20, borderTop: "1px dashed #000000" }}>
        <button
          onClick={() => setNotesOpen((v) => !v)}
          style={{
            width: "100%",
            display: "flex",
            justifyContent: "space-between",
            alignItems: "center",
            background: "none",
            border: "none",
            padding: 0,
            cursor: "pointer",
            textAlign: "left",
          }}
        >
          <span style={{ ...FIELD_LABEL, marginBottom: 0 }}>Add Extra Notes</span>
          <span style={{ color: "#6AE4FF", fontSize: 12, fontWeight: 600 }}>{notesOpen ? "Hide ▲" : "Show ▼"}</span>
        </button>

        {notesOpen && (
          <div style={{ marginTop: 14 }}>
            <p style={{ fontSize: 12.5, color: "#CDD0D6", marginTop: 0, marginBottom: 10 }}>
              Didn't quite land? Tell it what to fix, it'll update the profile above instead of starting over.
            </p>
            <textarea
              className="field-input"
              rows={3}
              placeholder={'e.g. "I don\'t like that you said I use first-person plural \'we\', I work solo, use \'I\' instead"'}
              value={extraNote}
              onChange={(e) => setExtraNote(e.target.value)}
            />
            <button className="generate-btn" disabled={!canRefine || refining} onClick={handleRefine}>
              {refining ? "Updating..." : "Refine My Voice"}
            </button>
            {refineError && <div className="error">{refineError}</div>}
          </div>
        )}
      </div>

      <div style={{ marginTop: 20, paddingTop: 20, borderTop: "1px dashed #000000" }}>
        {!showRecalibrate ? (
          <div>
            <button className="copy-btn" onClick={() => setShowRecalibrate(true)}>
              Recalibrate From Scratch
            </button>
            <div className="hint" style={{ marginTop: 8 }}>
              Replaces your profile with a brand new one built from fresh samples. Voice matching stays on the whole time, this just swaps what it's based on.
            </div>
          </div>
        ) : (
          <div>
            <label style={FIELD_LABEL}>Paste New Writing Samples</label>
            <p style={{ fontSize: 12.5, color: "#CDD0D6", marginTop: -4, marginBottom: 10 }}>
              This replaces your current profile entirely rather than refining it.
            </p>
            <textarea
              className="field-input"
              rows={8}
              placeholder="Paste a past email here, then a blank line, then another sample..."
              value={samples}
              onChange={(e) => setSamples(e.target.value)}
            />
            <div style={{ display: "flex", gap: 8, marginTop: 12 }}>
              <button className="generate-btn" style={{ marginTop: 0 }} disabled={!canCalibrate || calibrating} onClick={handleCalibrate}>
                {calibrating ? "Learning your voice..." : "Save New Voice"}
              </button>
              <ActionButton label="Cancel" onClick={() => { setShowRecalibrate(false); setSamples(""); }} />
            </div>
            {calibrateError && <div className="error">{calibrateError}</div>}
          </div>
        )}
      </div>

      <div style={{ marginTop: 20, paddingTop: 20, borderTop: "1px dashed #000000" }}>
        {!showClearConfirm ? (
          <div>
            <button className="copy-btn" onClick={() => setShowClearConfirm(true)}>
              Clear My Voice Profile
            </button>
            <div className="hint" style={{ marginTop: 8 }}>
              Turns voice matching off completely. Every tab goes back to writing without a personal voice until you set one up again from scratch.
            </div>
          </div>
        ) : (
          <div className="carryover" style={{ borderColor: "#EB5757" }}>
            <strong>Are you sure you want to clear your voice profile?</strong>
            <p style={{ margin: "6px 0 12px", fontSize: 13 }}>
              This can't be undone, you'd need to calibrate from scratch again.
            </p>
            <div style={{ display: "flex", gap: 8 }}>
              <ActionButton label="Yes, Clear It" onClick={handleConfirmClear} />
              <ActionButton label="Cancel" onClick={() => setShowClearConfirm(false)} />
            </div>
          </div>
        )}
      </div>
    </div>
  );
}

// --- Client Tracker / Pipeline ---

const CLIENT_STATUS_FILTERS = ["All", "Needs Follow-Up", "Active", "Inactive"];
const CLIENT_ROLE_FILTERS = ["All", "Buyer", "Seller", "Both"];

function dayCountLabel(lastContactDate) {
  const days = daysSince(lastContactDate);
  if (days === null) return "Not yet contacted";
  if (days === 0) return "Contacted today";
  return `${days} day${days === 1 ? "" : "s"} since contact`;
}

function ClientTypeBadge({ clientType }) {
  return <span className="side-tag">{CLIENT_TYPE_LABELS[clientType] || clientType}</span>;
}

function ClientStatusBadge({ activeStatus }) {
  const isActive = activeStatus === "active";
  return (
    <span className="side-tag" style={!isActive ? { color: "#CDD0D6", borderColor: "#CDD0D6" } : undefined}>
      {isActive ? "Active" : "Inactive"}
    </span>
  );
}

function StageTrack({ label, stages, currentStage, onSelectStage }) {
  const currentIndex = stages.indexOf(currentStage);
  return (
    <div style={{ marginBottom: 18 }}>
      {label && <div className="output-label">{label}</div>}
      <div style={{ display: "flex", flexDirection: "column", gap: 4 }}>
        {stages.map((stage, i) => {
          const isCurrent = stage === currentStage;
          const isPast = currentIndex >= 0 && i < currentIndex;
          return (
            <div key={stage} className="stage-row" onClick={() => onSelectStage(stage)} style={{ display: "flex", alignItems: "center", gap: 10, cursor: "pointer" }}>
              <span
                style={{
                  width: 8, height: 8, borderRadius: "50%", flexShrink: 0,
                  background: isCurrent ? "#6AE4FF" : isPast ? "#CDD0D6" : "transparent",
                  border: isCurrent || isPast ? "none" : "1px solid #CDD0D6",
                  opacity: isCurrent ? 1 : isPast ? 0.6 : 0.35,
                }}
              />
              <span
                style={{
                  fontFamily: "'Source Sans 3', sans-serif", fontSize: 14,
                  fontWeight: isCurrent ? 700 : 400,
                  color: isCurrent ? "#FFFFFF" : "#CDD0D6",
                  opacity: isCurrent ? 1 : isPast ? 0.85 : 0.5,
                }}
              >
                {stage}
              </span>
            </div>
          );
        })}
      </div>
    </div>
  );
}

// "Both" clients get two independent tracks since current_stage is one field
// that can only ever match one list at a time (shared terminal stages like
// "Under Contract" bold in both tracks when that's the stored value).
function StageChecklist({ clientType, currentStage, onSelectStage }) {
  const hint = !currentStage && <div className="hint" style={{ marginBottom: 12 }}>Tap a stage to set it</div>;
  if (clientType === "both") {
    return (
      <>
        {hint}
        <StageTrack label="Buying" stages={BUYER_STAGES} currentStage={currentStage} onSelectStage={onSelectStage} />
        <StageTrack label="Selling" stages={SELLER_STAGES} currentStage={currentStage} onSelectStage={onSelectStage} />
      </>
    );
  }
  return (
    <>
      {hint}
      <StageTrack stages={stagesForClientType(clientType)} currentStage={currentStage} onSelectStage={onSelectStage} />
    </>
  );
}

function FilterChipRow({ options, value, onChange }) {
  return (
    <div className="send-row" style={{ marginBottom: 10 }}>
      {options.map((opt) => (
        <button key={opt} className={`send-chip${value === opt ? " active" : ""}`} onClick={() => onChange(opt)}>
          {opt}
        </button>
      ))}
    </div>
  );
}

function ClientCard({ contact, onClick }) {
  const isActive = contact.active_status === "active";
  return (
    <div className="project-row" onClick={onClick}>
      <div className="item-text">
        <div style={{ display: "flex", alignItems: "center", gap: 4, flexWrap: "wrap" }}>
          <span className="project-row-address">{contact.name}</span>
          <ClientTypeBadge clientType={contact.client_type} />
          <ClientStatusBadge activeStatus={contact.active_status} />
        </div>
        <span className="project-row-meta">
          {!isActive
            ? contact.inactive_history_note || "No history noted yet"
            : contact.current_stage
            ? <strong style={{ color: "#FFFFFF" }}>{contact.current_stage}</strong>
            : "Stage not set"}
          {contact.project_address ? ` · ${contact.project_address}` : ""}
        </span>
        {isActive && contact.waiting_reason && (
          <span style={{ fontFamily: "'Source Sans 3', sans-serif", fontSize: 12.5, color: "#EB5757", marginTop: 2, display: "block" }}>
            {dayCountLabel(contact.last_contact_date)} — {contact.waiting_reason}
          </span>
        )}
      </div>
      <span className="row-arrow">→</span>
    </div>
  );
}

function AddClientForm({ projects, onCreated }) {
  const [showForm, setShowForm] = useState(false);
  const [name, setName] = useState("");
  const [email, setEmail] = useState("");
  const [phone, setPhone] = useState("");
  const [role, setRole] = useState("buyer"); // "buyer" | "seller"
  const [projectId, setProjectId] = useState("");
  const [saving, setSaving] = useState(false);
  const [error, setError] = useState(null);

  const needsProject = role === "seller";
  const canSubmit = name.trim().length > 0 && (!needsProject || projectId);

  async function handleAdd() {
    if (!canSubmit) return;
    setSaving(true);
    setError(null);
    try {
      const contact = await createUnlinkedContactApi({
        name,
        email,
        phone,
        client_type: role,
        project_id: needsProject ? Number(projectId) : undefined,
      });
      setName(""); setEmail(""); setPhone(""); setRole("buyer"); setProjectId("");
      setShowForm(false);
      onCreated(contact);
    } catch (err) {
      setError(err.message);
    } finally {
      setSaving(false);
    }
  }

  if (!showForm) {
    return <div className="add-btn" onClick={() => setShowForm(true)}>+ Add Client</div>;
  }

  return (
    <div style={{ background: "#202A3E", border: "1px solid #000000", borderRadius: 15, padding: "18px 22px", marginBottom: 10 }}>
      <div className="send-row" style={{ marginBottom: 12 }}>
        <button className={`send-chip${role === "buyer" ? " active" : ""}`} onClick={() => setRole("buyer")}>Buyer</button>
        <button className={`send-chip${role === "seller" ? " active" : ""}`} onClick={() => setRole("seller")}>Seller</button>
      </div>
      {needsProject && (
        projects.length === 0 ? (
          <div className="hint" style={{ marginBottom: 12 }}>
            You don't have any projects yet -- create one first, then add this seller from its Contacts tab (or come back here once you have).
          </div>
        ) : (
          <div style={{ marginBottom: 12 }}>
            <label style={FIELD_LABEL}>Project</label>
            <select className="field-input" value={projectId} onChange={(e) => setProjectId(e.target.value)}>
              <option value="">Select a project...</option>
              {projects.map((p) => (
                <option key={p.id} value={p.id}>{p.address || `Project #${p.id}`}</option>
              ))}
            </select>
          </div>
        )
      )}
      <div style={{ display: "grid", gridTemplateColumns: "1fr 1fr", gap: 10, marginBottom: 12 }}>
        <input className="field-input" placeholder="Name" value={name} onChange={(e) => setName(e.target.value)} />
        <input className="field-input" placeholder="Email (optional)" value={email} onChange={(e) => setEmail(e.target.value)} />
        <input className="field-input" placeholder="Phone (optional)" value={phone} onChange={(e) => setPhone(e.target.value)} />
      </div>
      <div style={{ display: "flex", gap: 8 }}>
        <ActionButton label={saving ? "Adding..." : "Add Client"} onClick={canSubmit ? handleAdd : undefined} />
        <ActionButton label="Cancel" onClick={() => setShowForm(false)} />
      </div>
      {error && <div className="error">{error}</div>}
    </div>
  );
}

function ClientList({ contacts, projects, onOpenContact, onCreated }) {
  const [statusFilter, setStatusFilter] = useState("All");
  const [roleFilter, setRoleFilter] = useState("All");
  const [showRoleFilter, setShowRoleFilter] = useState(false);

  const filtered = contacts.filter((c) => {
    if (roleFilter !== "All" && CLIENT_TYPE_LABELS[c.client_type] !== roleFilter) return false;
    if (statusFilter === "Active" && c.active_status !== "active") return false;
    if (statusFilter === "Inactive" && c.active_status !== "inactive") return false;
    if (statusFilter === "Needs Follow-Up" && !(c.active_status === "active" && c.waiting_reason)) return false;
    return true;
  });

  function toggleRoleFilter() {
    if (showRoleFilter) setRoleFilter("All");
    setShowRoleFilter(!showRoleFilter);
  }

  return (
    <>
      <div style={{ marginBottom: 20, paddingBottom: 20, borderBottom: "1px solid #000000" }}>
        <FilterChipRow options={CLIENT_STATUS_FILTERS} value={statusFilter} onChange={setStatusFilter} />
        {showRoleFilter ? (
          <FilterChipRow options={CLIENT_ROLE_FILTERS} value={roleFilter} onChange={setRoleFilter} />
        ) : null}
        <div className="voice-link" style={{ display: "inline-block", cursor: "pointer" }} onClick={toggleRoleFilter}>
          {showRoleFilter ? "Hide role filter" : "Filter by role"}
        </div>
      </div>
      <AddClientForm projects={projects} onCreated={onCreated} />
      {filtered.length === 0 ? (
        <div className="hint">No clients match this filter.</div>
      ) : (
        filtered.map((c) => <ClientCard key={c.id} contact={c} onClick={() => onOpenContact(c.id)} />)
      )}
    </>
  );
}

function ClientDetail({ contact, projects, onBack, onOpenProject, onUpdated, onFollowUp }) {
  const [savingStage, setSavingStage] = useState(false);
  const [editingReason, setEditingReason] = useState(false);
  const [reasonDraft, setReasonDraft] = useState(contact.waiting_reason || "");
  const [savingReason, setSavingReason] = useState(false);
  const [loggingContact, setLoggingContact] = useState(false);
  const [error, setError] = useState(null);
  const guardingBack = useJustMounted(200, [contact.id]);
  const [stageDraft, setStageDraft] = useState(contact.current_stage);

  useEffect(() => {
    setStageDraft(contact.current_stage);
  }, [contact.id, contact.current_stage]);

  const [convertTarget, setConvertTarget] = useState(null); // null | "buyer" | "seller" | "both"
  const [convertProjectId, setConvertProjectId] = useState("");
  const [converting, setConverting] = useState(false);

  function handleSelectStageDraft(stage) {
    setStageDraft(stage);
  }

  async function handleSaveStage() {
    setSavingStage(true);
    setError(null);
    try {
      await updateContactPipelineApi(contact.id, { current_stage: stageDraft });
      onUpdated();
    } catch (err) {
      setError(err.message);
    } finally {
      setSavingStage(false);
    }
  }

  function handleCancelStage() {
    setStageDraft(contact.current_stage);
  }

  const stageDirty = stageDraft !== contact.current_stage;

  async function handleSaveReason() {
    setSavingReason(true);
    setError(null);
    try {
      await updateContactPipelineApi(contact.id, { waiting_reason: reasonDraft });
      setEditingReason(false);
      onUpdated();
    } catch (err) {
      setError(err.message);
    } finally {
      setSavingReason(false);
    }
  }

  async function handleLogContact() {
    setLoggingContact(true);
    setError(null);
    try {
      await logContactApi(contact.id);
      onUpdated();
    } catch (err) {
      setError(err.message);
    } finally {
      setLoggingContact(false);
    }
  }

  async function handleMarkActive(newClientType) {
    setError(null);
    try {
      await updateContactPipelineApi(contact.id, { active_status: "active", client_type: newClientType });
      onUpdated();
    } catch (err) {
      setError(err.message);
    }
  }

  function convertNeedsProject(targetType) {
    return targetType !== "buyer" && !contact.project_id;
  }

  async function handleConvert(targetType) {
    if (convertNeedsProject(targetType)) {
      setConvertTarget(targetType);
      setConvertProjectId("");
      return;
    }
    setConverting(true);
    setError(null);
    try {
      await updateContactPipelineApi(contact.id, { client_type: targetType, current_stage: null });
      onUpdated();
    } catch (err) {
      setError(err.message);
    } finally {
      setConverting(false);
    }
  }

  async function handleConfirmConvert() {
    if (!convertProjectId) return;
    setConverting(true);
    setError(null);
    try {
      await updateContactPipelineApi(contact.id, { client_type: convertTarget, project_id: Number(convertProjectId), current_stage: null });
      setConvertTarget(null);
      onUpdated();
    } catch (err) {
      setError(err.message);
    } finally {
      setConverting(false);
    }
  }

  const [markingInactive, setMarkingInactive] = useState(false);
  const [historyDraft, setHistoryDraft] = useState(contact.inactive_history_note || "");
  const [savingInactive, setSavingInactive] = useState(false);

  async function handleConfirmInactive() {
    setSavingInactive(true);
    setError(null);
    try {
      await updateContactPipelineApi(contact.id, { active_status: "inactive", inactive_history_note: historyDraft });
      setMarkingInactive(false);
      onUpdated();
    } catch (err) {
      setError(err.message);
    } finally {
      setSavingInactive(false);
    }
  }

  const isActive = contact.active_status === "active";

  return (
    <>
      <div style={{ display: "flex", alignItems: "center", gap: 10, marginBottom: 6, flexWrap: "wrap" }}>
        <button className="copy-btn" disabled={guardingBack} onClick={onBack} style={{ padding: "8px 14px" }}>←</button>
        <h2 style={{ fontSize: 22, fontWeight: 700, margin: 0 }}>{contact.name}</h2>
        <ClientTypeBadge clientType={contact.client_type} />
        <ClientStatusBadge activeStatus={contact.active_status} />
      </div>
      {contact.updated_at && (
        <div style={{ fontSize: 12, color: "#CDD0D6", marginBottom: 10 }}>
          Last edited {new Date(contact.updated_at).toLocaleDateString()}
        </div>
      )}
      {contact.project_address && (
        <div className="voice-link" style={{ marginBottom: 20, display: "inline-block", cursor: "pointer" }} onClick={() => onOpenProject(contact.project_id)}>
          {contact.project_address} →
        </div>
      )}

      {isActive ? (
        <>
          <div style={CARD}>
            <div className="output-label">Current Stage</div>
            <StageChecklist clientType={contact.client_type} currentStage={stageDraft} onSelectStage={handleSelectStageDraft} />
            {stageDirty && (
              <div style={{ display: "flex", gap: 8, marginTop: 12 }}>
                <ActionButton label={savingStage ? "Saving..." : "Save"} onClick={savingStage ? undefined : handleSaveStage} />
                <ActionButton label="Cancel" onClick={handleCancelStage} />
              </div>
            )}
          </div>

          <div style={{ ...CARD, marginTop: 16 }}>
            <div className="output-label">Last Contact</div>
            <p style={{ margin: 0, fontFamily: "'Source Sans 3', sans-serif", fontSize: 14 }}>
              {contact.last_contact_date
                ? `${new Date(contact.last_contact_date).toLocaleDateString()} · ${dayCountLabel(contact.last_contact_date)}`
                : "Not yet contacted"}
            </p>
          </div>

          {contact.waiting_reason || editingReason ? (
            <div style={{ ...CARD, marginTop: 16, borderLeft: "3px solid #EB5757" }}>
              <div className="output-label" style={{ color: "#EB5757" }}>Needs a Check-In</div>
              {!editingReason ? (
                <>
                  <p style={{ margin: "0 0 12px", fontFamily: "'Source Sans 3', sans-serif", fontSize: 14 }}>
                    {dayCountLabel(contact.last_contact_date)} — {contact.waiting_reason}
                  </p>
                  <ActionButton label="Update Reason" onClick={() => { setReasonDraft(contact.waiting_reason || ""); setEditingReason(true); }} />
                </>
              ) : (
                <>
                  <textarea
                    className="field-input"
                    style={{ minHeight: 70, marginBottom: 10 }}
                    value={reasonDraft}
                    onChange={(e) => setReasonDraft(e.target.value)}
                    placeholder="What are they waiting on, in your own words?"
                  />
                  <div style={{ display: "flex", gap: 8 }}>
                    <ActionButton label={savingReason ? "Saving..." : "Save"} onClick={handleSaveReason} />
                    <ActionButton label="Cancel" onClick={() => setEditingReason(false)} />
                  </div>
                </>
              )}
            </div>
          ) : null}

          <button className="generate-btn" style={{ marginTop: 20 }} onClick={() => onFollowUp(contact)}>Generate Follow-Up</button>

          <div style={{ display: "flex", gap: 8, marginTop: 10, flexWrap: "wrap" }}>
            <ActionButton label={loggingContact ? "Logging..." : "Log a Contact"} onClick={handleLogContact} />
            {!contact.waiting_reason && !editingReason && (
              <ActionButton label="Note a Waiting Reason" onClick={() => { setReasonDraft(""); setEditingReason(true); }} />
            )}
          </div>

          <div
            className="voice-link"
            style={{ color: "#CDD0D6", marginTop: 22, cursor: "pointer", display: "inline-block" }}
            onClick={() => { setHistoryDraft(contact.inactive_history_note || ""); setMarkingInactive(true); }}
          >
            Mark Inactive
          </div>

          <div style={{ marginTop: 14 }}>
            {CLIENT_TYPES.filter((t) => t !== contact.client_type).map((t) => (
              <span
                key={t}
                className="voice-link"
                style={{ color: "#CDD0D6", marginRight: 16, cursor: "pointer" }}
                onClick={converting ? undefined : () => handleConvert(t)}
              >
                Convert to {CLIENT_TYPE_LABELS[t]}
              </span>
            ))}
          </div>

          {convertTarget && (
            <div style={{ ...CARD, marginTop: 16 }}>
              <div className="output-label">Convert to {CLIENT_TYPE_LABELS[convertTarget]}</div>
              {projects.length === 0 ? (
                <div className="hint" style={{ marginBottom: 10 }}>
                  You don't have any projects yet -- link one before converting to {CLIENT_TYPE_LABELS[convertTarget]}.
                </div>
              ) : (
                <>
                  <div className="hint" style={{ marginBottom: 10 }}>
                    An active {CLIENT_TYPE_LABELS[convertTarget].toLowerCase()} needs a linked project.
                  </div>
                  <select className="field-input" style={{ marginBottom: 10 }} value={convertProjectId} onChange={(e) => setConvertProjectId(e.target.value)}>
                    <option value="">Select a project...</option>
                    {projects.map((p) => (
                      <option key={p.id} value={p.id}>{p.address || `Project #${p.id}`}</option>
                    ))}
                  </select>
                </>
              )}
              <div style={{ display: "flex", gap: 8 }}>
                {projects.length > 0 && (
                  <ActionButton label={converting ? "Converting..." : "Confirm Conversion"} onClick={convertProjectId && !converting ? handleConfirmConvert : undefined} />
                )}
                <ActionButton label="Cancel" onClick={() => setConvertTarget(null)} />
              </div>
            </div>
          )}

          {markingInactive && (
            <div style={{ ...CARD, marginTop: 16 }}>
              <div className="output-label">Mark as Inactive</div>
              <div className="hint" style={{ marginBottom: 10 }}>
                Optional -- what happened, in your own words (e.g. "Bought a house, June 2026"). You can add this later too.
              </div>
              <textarea
                className="field-input"
                style={{ minHeight: 70, marginBottom: 10 }}
                value={historyDraft}
                onChange={(e) => setHistoryDraft(e.target.value)}
                placeholder="No history noted yet."
              />
              <div style={{ display: "flex", gap: 8 }}>
                <ActionButton label={savingInactive ? "Saving..." : "Confirm Inactive"} onClick={handleConfirmInactive} />
                <ActionButton label="Cancel" onClick={() => setMarkingInactive(false)} />
              </div>
            </div>
          )}
        </>
      ) : (
        <>
          <div style={CARD}>
            <div className="output-label">History</div>
            <p style={{ margin: 0, fontFamily: "'Source Sans 3', sans-serif", fontSize: 14 }}>
              {contact.inactive_history_note || "No history noted yet."}
            </p>
          </div>
          <div style={{ display: "flex", gap: 8, marginTop: 20, flexWrap: "wrap" }}>
            <ActionButton label="Mark As Ready to Buy Again" onClick={() => handleMarkActive("buyer")} />
            {contact.project_id && <ActionButton label="Mark As Ready to Sell Again" onClick={() => handleMarkActive("seller")} />}
          </div>
        </>
      )}
      {error && <div className="error">{error}</div>}
    </>
  );
}

function ClientTracker({ projects, openContactId, setOpenContactId, onBack, onOpenProject, onFollowUpForClient }) {
  const [contacts, setContacts] = useState([]);
  const [loaded, setLoaded] = useState(false);
  const guardingBack = useJustMounted(200, [openContactId]);

  function refresh() {
    fetchAllContacts()
      .then((data) => { setContacts(data); setLoaded(true); })
      .catch((err) => console.error("Could not load contacts", err));
  }

  useEffect(() => { refresh(); }, []);

  // Merges the just-created contact straight into state and opens its detail
  // view immediately -- no refetch round-trip, so there's no gap where
  // openContactId is set but the contact isn't in `contacts` yet.
  function handleContactCreated(contact) {
    setContacts((prev) => [contact, ...prev]);
    setOpenContactId(contact.id);
  }

  const openContact = contacts.find((c) => c.id === openContactId);

  return (
    <div style={{ maxWidth: 900, margin: "0 auto" }}>
      {openContact ? (
        <ClientDetail
          contact={openContact}
          projects={projects}
          onBack={() => setOpenContactId(null)}
          onOpenProject={onOpenProject}
          onUpdated={refresh}
          onFollowUp={onFollowUpForClient}
        />
      ) : (
        <>
          <div style={{ display: "flex", alignItems: "center", gap: 10, marginBottom: 20 }}>
            <button className="copy-btn" disabled={guardingBack} onClick={onBack} style={{ padding: "8px 14px" }}>←</button>
            <h2 style={{ fontSize: 22, fontWeight: 700, margin: 0 }}>Client Tracker</h2>
          </div>
          {!loaded ? (
            <div className="hint">Loading...</div>
          ) : (
            <ClientList contacts={contacts} projects={projects} onOpenContact={setOpenContactId} onCreated={handleContactCreated} />
          )}
        </>
      )}
    </div>
  );
}

function Dashboard({ agent, onLogout }) {
  const [activeTab, setActiveTab] = useState("home");
  const [toolReturnView, setToolReturnView] = useState("home"); // "home" | "project-detail"
  const [property, setProperty] = useState({
    address: "",
    propertyType: "Single-Family",
    beds: "",
    baths: "",
    sqft: "",
    price: "",
    features: "",
  });
  const [voiceProfile, setVoiceProfileState] = useState(agent.voiceProfile || "");
  const [projects, setProjects] = useState([]);
  const [selectedProjectId, setSelectedProjectId] = useState(null);
  const [showProjectsManager, setShowProjectsManager] = useState(false);
  const [editingProjectId, setEditingProjectId] = useState(null); // number | "new" | null (null = project list)
  const [projectDetailView, setProjectDetailView] = useState(null); // null (overview) | "edit" | "contacts" | "offers" | "comps" | "openhouse"
  const [showClientTracker, setShowClientTracker] = useState(false);
  const [clientTrackerContactId, setClientTrackerContactId] = useState(null); // null (list) | a contact id (detail)
  const [emailPrefill, setEmailPrefill] = useState(null); // { name, contactId, detail, situation } | null, set when opening Follow-Up from a visitor or client
  const [projectFormDirty, setProjectFormDirty] = useState(false);
  const [pendingNavigation, setPendingNavigation] = useState(null); // holds a function to run if the person confirms leaving

  function guardNavigate(navigateFn) {
    if (projectFormDirty) {
      setPendingNavigation(() => navigateFn);
    } else {
      navigateFn();
    }
  }

  // fromProject: true when opened from inside a project's own Tools list (pre-fills
  // project data and makes the tool's back button return to that project instead of Home).
  function openTool(tabId, fromProject) {
    guardNavigate(() => {
      if (fromProject && selectedProjectId) {
        handleSelectProject(selectedProjectId);
      }
      setEmailPrefill(null); // don't leak a stale visitor prefill into a normal Follow-Up open
      pickOneLiner();
      setToolReturnView(fromProject ? "project-detail" : "home");
      setShowProjectsManager(false);
      setActiveTab(tabId);
    });
  }

  // Opens Follow-Up pre-filled with a specific open house visitor's real info, returning
  // to the Open House view (not the generic project detail) when the back button is used.
  function openFollowUpForVisitor(projectId, visitorName, detailText) {
    guardNavigate(() => {
      handleSelectProject(projectId);
      setEmailPrefill({ name: visitorName, detail: detailText, situation: "Post-showing" });
      pickOneLiner();
      setToolReturnView("openhouse");
      setShowProjectsManager(false);
      setActiveTab("email");
    });
  }

  // Opens Follow-Up pre-filled from a Client Tracker contact, returning to that same
  // contact's detail view (not the tracker list) when the back button is used.
  function openFollowUpForClient(contact) {
    guardNavigate(() => {
      if (contact.project_id) handleSelectProject(contact.project_id);
      setEmailPrefill({ name: contact.name, contactId: contact.id, detail: "", situation: "Post-showing" });
      pickOneLiner();
      setToolReturnView("clienttracker");
      setShowProjectsManager(false);
      setShowClientTracker(false);
      setActiveTab("email");
    });
  }

  function goBackFromTool() {
    if (toolReturnView === "project-detail") {
      setShowProjectsManager(true);
      setProjectDetailView(null);
      setActiveTab("home");
    } else if (toolReturnView === "openhouse") {
      setShowProjectsManager(true);
      setProjectDetailView("openhouse");
      setActiveTab("home");
    } else if (toolReturnView === "clienttracker") {
      setShowClientTracker(true);
      setActiveTab("home");
    } else {
      setActiveTab("home");
    }
  }

  function handleProjectsButtonClick() {
    guardNavigate(() => {
      setShowProjectsManager(true);
      setActiveTab("home");
    });
  }

  function handleClientTrackerButtonClick() {
    guardNavigate(() => {
      setClientTrackerContactId(null);
      setShowClientTracker(true);
      setActiveTab("home");
    });
  }

  // Jumps straight to a specific client's detail view, e.g. from the Home nudge.
  function openClientFromHome(contactId) {
    guardNavigate(() => {
      setClientTrackerContactId(contactId);
      setShowClientTracker(true);
      setActiveTab("home");
    });
  }

  function goHome() {
    guardNavigate(() => {
      setShowProjectsManager(false);
      setShowClientTracker(false);
      setActiveTab("home");
    });
  }

  function confirmLeave() {
    setProjectFormDirty(false);
    if (pendingNavigation) pendingNavigation();
    setPendingNavigation(null);
  }

  function cancelLeave() {
    setPendingNavigation(null);
  }

  useEffect(() => {
    refreshProjects();
  }, []);

  function refreshProjects() {
    fetchProjects()
      .then(setProjects)
      .catch((err) => console.error("Could not load projects", err));
  }

  function handleSelectProject(projectId) {
    setSelectedProjectId(projectId);
    const project = projects.find((p) => p.id === projectId);
    if (project) {
      setProperty({
        address: project.address || "",
        propertyType: project.property_type || "Single-Family",
        beds: project.beds || "",
        baths: project.baths || "",
        sqft: project.sqft || "",
        price: project.price || "",
        features: project.features || "",
      });
    }
  }

  function handleVoiceProfileChange(descriptor) {
    setVoiceProfileState(descriptor);
    persistVoiceProfile(descriptor);
  }

  const TOOL_DEFS = [
    { id: "listing", label: "Listing", desc: "Create your own listing in whatever tone and length you desire." },
    { id: "social", label: "Social", desc: "Turn your listing into a caption ready for Instagram, Facebook, LinkedIn, or TikTok." },
    { id: "email", label: "Follow-Up", desc: "Draft a warm, professional follow-up without starting from scratch." },
    { id: "reply", label: "Reply", desc: "Paste in any email, from a client, another agent, or a potential buyer, and get back a reply that actually addresses it." },
    { id: "voice", label: "Voice", desc: "Teach the AI to sound like you, once, for every tool." },
  ];

  const PROJECT_TOOL_DESCS = {
    listing: "Create your own listing in whatever tone and length you desire.",
    social: "Turn your listing into a caption ready for Instagram, Facebook, LinkedIn, or TikTok.",
    email: "Draft a warm, professional follow-up in seconds. Connect a contact first so it's personal, not generic.",
    reply: "Paste in any email, from a client, another agent, or a potential buyer. Connect a contact first for extra context.",
  };

  const ONE_LINERS = [
    "You've got this, let's make it happen.",
    "Another deal, another win, go get it.",
    "You're closer to closing than you think.",
    "Let's give this one everything you've got.",
    "You know exactly what to do here.",
    "Go show them why you're the right agent for this.",
  ];
  const [oneLiner, setOneLiner] = useState("");

  function pickOneLiner() {
    setOneLiner(ONE_LINERS[Math.floor(Math.random() * ONE_LINERS.length)]);
  }

  return (
    <div style={{ minHeight: "100%", background: "#17202E", fontFamily: "'Open Sans', system-ui, sans-serif", color: "#FFFFFF", position: "relative", letterSpacing: "-0.036em" }}>
      <style>{`
        * { box-sizing: border-box; }
        body { background: #17202E; }
        input, textarea, select { font-family: 'Open Sans', sans-serif; }
        input:focus, textarea:focus, select:focus { outline: 2px solid #6AE4FF; outline-offset: 2px; }
        .field-input { width: 100%; padding: 12px 14px; border: 1px solid #000000; border-radius: 8px; background: #202A3E; font-size: 14px; color: #FFFFFF; font-family: inherit; }
        .field-input::placeholder { color: #CDD0D6; opacity: 0.45; }
        .copy-btn { font-size: 13px; font-weight: 600; background: none; border: 1px solid #000000; color: #FFFFFF; padding: 10px 16px; border-radius: 80px; cursor: pointer; transition: border-color 0.15s ease; font-family: inherit; letter-spacing: -0.036em; }
        .copy-btn:hover { border-color: #6AE4FF; }
        .generate-btn { margin-top: 20px; width: 100%; padding: 15px; background: #FFFFFF; color: #000000; border: none; border-radius: 80px; font-size: 14px; font-weight: 700; cursor: pointer; font-family: inherit; letter-spacing: -0.036em; }
        .generate-btn:disabled { background: #202A3E; color: #CDD0D6; cursor: not-allowed; }
        .hint { font-family: 'Source Sans 3', sans-serif; font-size: 12px; color: #CDD0D6; margin-top: 8px; letter-spacing: normal; }
        .error { font-size: 12px; color: #EB5757; margin-top: 8px; white-space: pre-wrap; font-family: 'Source Sans 3', monospace; }
        .output-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 8px; }
        .output-text { font-family: 'Source Sans 3', sans-serif; font-size: 14px; line-height: 1.75; color: #FFFFFF; white-space: pre-wrap; margin-bottom: 24px; letter-spacing: normal; }
        .carryover { font-family: 'Source Sans 3', sans-serif; font-size: 13px; color: #CDD0D6; background: #202A3E; border: 1px dashed #000000; padding: 12px 16px; border-radius: 10px; letter-spacing: normal; }
        .tab-btn { background: none; border: none; padding: 12px 14px; font-size: 12px; font-weight: 600; color: #CDD0D6; cursor: pointer; border-bottom: 3px solid transparent; white-space: nowrap; flex-shrink: 0; font-family: inherit; }
        .tab-btn.active { color: #FFFFFF; border-bottom: 3px solid #6AE4FF; }
        .tab-row { display: flex; gap: 2px; overflow-x: auto; -webkit-overflow-scrolling: touch; scrollbar-width: none; }
        .tab-row::-webkit-scrollbar { display: none; }
        .split-editor { display: grid; grid-template-columns: 1fr 1fr; gap: 20px; align-items: start; }
        .split-editor-paste { position: sticky; top: 16px; }
        .split-editor-textarea { min-height: 480px; resize: vertical; }
        @media (max-width: 720px) {
          .split-editor { grid-template-columns: 1fr; }
          .split-editor-paste { position: static; }
          .split-editor-textarea { min-height: 220px; }
        }

        /* ===== Terminal redesign system ===== */
        .tool-list { display: grid; grid-template-columns: repeat(3, 1fr); gap: 14px; text-align: left; max-width: 1000px; margin: 0 auto; }
        @media (max-width: 900px) { .tool-list { grid-template-columns: repeat(2, 1fr); } }
        @media (max-width: 600px) { .tool-list { grid-template-columns: 1fr; } }

        .tool-item, .detail-item, .project-row {
          background: #202A3E; border: 1px solid #000000; border-radius: 15px; padding: 22px; margin-bottom: 0;
          cursor: pointer; display: flex; justify-content: space-between; align-items: center; transition: border-color 0.15s ease;
        }
        .tool-item:hover, .detail-item:hover, .project-row:hover { border-color: #6AE4FF; }
        .item-text { display: flex; flex-direction: column; gap: 4px; }
        .item-name, .detail-item-label, .project-row-address { font-size: 16px; font-weight: 700; color: #FFFFFF; }
        .item-desc, .detail-item-meta, .project-row-meta { font-family: 'Source Sans 3', sans-serif; font-size: 13px; color: #CDD0D6; font-weight: 400; letter-spacing: normal; }
        .item-arrow, .row-arrow { color: #6AE4FF; font-size: 16px; flex-shrink: 0; margin-left: 16px; }

        .tool-one-liner { font-style: italic; font-size: 14px; color: #CDD0D6; margin: -4px 0 6px; }
        .tool-landing-intro { font-size: 15px; color: #CDD0D6; margin: 4px 0 28px; line-height: 1.6; max-width: 600px; }

        .add-btn { display: block; width: 100%; padding: 18px 22px; background: transparent; border: 1px dashed #CDD0D6; border-radius: 15px; color: #CDD0D6; font-size: 14px; font-weight: 400; text-align: center; cursor: pointer; margin-bottom: 20px; font-family: inherit; letter-spacing: -0.036em; }
        .add-btn:hover { border-color: #6AE4FF; color: #6AE4FF; }

        .needs-attention { border: 1px solid #6AE4FF !important; }
        .attention-badge { font-family: 'Source Sans 3', sans-serif; font-size: 11px; font-weight: 700; background: #6AE4FF; color: #17202E; padding: 4px 11px; border-radius: 80px; white-space: nowrap; }
        .side-tag { font-family: 'Source Sans 3', sans-serif; font-size: 11px; font-weight: 600; color: #6AE4FF; border: 1px solid #6AE4FF; padding: 4px 12px; border-radius: 80px; margin-left: 14px; vertical-align: middle; }
        .voice-link { font-family: 'Source Sans 3', sans-serif; font-size: 13px; font-weight: 600; color: #6AE4FF; cursor: pointer; }

        .spacious-field label { font-family: 'Source Sans 3', sans-serif; display: block; font-size: 11px; font-weight: 600; text-transform: uppercase; letter-spacing: 0.04em; color: #CDD0D6; margin-bottom: 8px; }
        .field-pair { display: grid; grid-template-columns: 1fr 1fr; gap: 14px; }
        .outline-btn { padding: 13px 20px; border-radius: 8px; border: 1px solid #6AE4FF; background: none; color: #6AE4FF; font-size: 14px; font-weight: 400; cursor: pointer; font-family: inherit; letter-spacing: -0.036em; }
        .note-box { font-family: 'Source Sans 3', sans-serif; background: rgba(106,228,255,0.08); border: 1px dashed #6AE4FF; border-radius: 10px; padding: 14px 18px; font-size: 13.5px; color: #6AE4FF; letter-spacing: normal; }
        .context-pill { display: inline-block; font-family: 'Source Sans 3', sans-serif; font-size: 12px; font-weight: 600; color: #6AE4FF; background: rgba(106,228,255,0.1); border: 1px solid #6AE4FF; padding: 6px 14px; border-radius: 80px; margin-bottom: 20px; letter-spacing: normal; }

        .output-card { background: #202A3E; border: 1px solid #000000; border-left: 3px solid #6AE4FF; border-radius: 15px; padding: 22px; margin-top: 22px; }
        .output-label { font-family: 'Source Sans 3', sans-serif; font-size: 11px; font-weight: 700; text-transform: uppercase; letter-spacing: 0.05em; color: #6AE4FF; margin-bottom: 10px; }
        .send-row { display: flex; gap: 8px; flex-wrap: wrap; }
        .send-chip { padding: 10px 16px; border-radius: 80px; border: 1px solid #000000; background: #17202E; font-family: 'Source Sans 3', sans-serif; font-size: 12.5px; font-weight: 400; color: #FFFFFF; cursor: pointer; }
        .send-chip:hover { border-color: #6AE4FF; }
        .send-chip.active { border-color: #6AE4FF; color: #6AE4FF; }
        .stage-row { padding: 6px 8px; margin: -6px -8px; border-radius: 8px; transition: background 0.15s ease; }
        .stage-row:hover { background: rgba(106,228,255,0.08); }

        .accordion-group { background: #202A3E; border: 1px solid #000000; border-radius: 15px; margin-bottom: 10px; overflow: hidden; }
        .accordion-header { padding: 16px 20px; display: flex; justify-content: space-between; align-items: center; cursor: pointer; }
        .accordion-header-name { font-size: 14px; font-weight: 600; color: #FFFFFF; }
        .accordion-header-count { font-family: 'Source Sans 3', sans-serif; font-size: 12px; color: #CDD0D6; }
        .accordion-chevron { color: #6AE4FF; font-size: 12px; transition: transform 0.2s ease; display: inline-block; }
        .accordion-chevron.open { transform: rotate(180deg); }
        .accordion-body { padding: 0 20px 18px; }
        .accordion-field { margin-bottom: 12px; }
        .accordion-field label { font-family: 'Source Sans 3', sans-serif; display: block; font-size: 10.5px; text-transform: uppercase; color: #CDD0D6; margin-bottom: 5px; }

        .comparison-grid { margin-top: 16px; background: #202A3E; border: 1px solid #000000; border-radius: 15px; padding: 8px 22px; }
        .comparison-row { display: grid; gap: 16px; padding: 15px 0; border-bottom: 1px solid rgba(0,0,0,0.5); align-items: center; }
        .comparison-header-row { border-bottom: none; padding-bottom: 8px; }
        .comparison-label { font-family: 'Source Sans 3', sans-serif; font-size: 11.5px; font-weight: 600; text-transform: uppercase; letter-spacing: 0.03em; color: #CDD0D6; }
        .comparison-col-header { text-align: center; font-size: 12px; font-weight: 700; color: #FFFFFF; background: #17202E; border: 1px solid #000000; border-radius: 80px; padding: 8px 6px; }
        .comparison-val { text-align: center; font-family: 'Source Sans 3', sans-serif; font-size: 13.5px; color: #FFFFFF; }
        .comparison-group-label { font-family: 'Source Sans 3', sans-serif; font-size: 10.5px; font-weight: 700; text-transform: uppercase; letter-spacing: 0.04em; color: #CDD0D6; padding: 14px 0 4px; }

        .section-divider { font-family: 'Source Sans 3', sans-serif; font-size: 11px; font-weight: 700; text-transform: uppercase; letter-spacing: 0.06em; color: #CDD0D6; margin: 22px 0 10px; }
        .section-divider:first-of-type { margin-top: 0; }

        .info-grid { display: grid; grid-template-columns: repeat(4, 1fr); gap: 14px; margin-bottom: 8px; }
        @media (max-width: 900px) { .info-grid { grid-template-columns: repeat(2, 1fr); } }
        @media (max-width: 560px) { .info-grid { grid-template-columns: 1fr; } }
        .info-tile { background: #202A3E; border: 1px solid #000000; border-radius: 15px; padding: 26px 22px; cursor: pointer; transition: border-color 0.15s ease; }
        .info-tile:hover { border-color: #6AE4FF; }
        .info-tile-top { display: flex; justify-content: space-between; align-items: flex-start; margin-bottom: 20px; }
        .info-icon { width: 34px; height: 34px; border-radius: 50%; background: #17202E; border: 1px solid #6AE4FF; display: flex; align-items: center; justify-content: center; color: #6AE4FF; font-size: 14px; flex-shrink: 0; }
        .info-tile-num { font-size: 36px; font-weight: 700; margin-bottom: 4px; }
        .info-tile-label { font-family: 'Source Sans 3', sans-serif; font-size: 13px; color: #CDD0D6; letter-spacing: normal; }

        .print-only { display: none; }
        @media print {
          body * { visibility: hidden; }
          .print-only, .print-only * { visibility: visible; }
          .print-only { display: block; position: absolute; top: 0; left: 0; width: 100%; }
        }
      `}</style>

      <div style={{ position: "relative", zIndex: 2, borderBottom: "1px solid #000000", padding: "20px 32px" }}>
        <div style={{ maxWidth: 1000, margin: "0 auto", display: "flex", justifyContent: "space-between", alignItems: "center" }}>
          <div onClick={goHome} style={{ display: "flex", alignItems: "center", gap: 10, cursor: "pointer" }}>
            <div style={{ width: 26, height: 26, borderRadius: 8, background: "#6AE4FF", display: "flex", alignItems: "center", justifyContent: "center", color: "#17202E", fontWeight: 700, fontSize: 13 }}>AT</div>
            <span style={{ fontWeight: 700, fontSize: 16 }}>The Agent's Toolkit</span>
          </div>
          <div style={{ display: "flex", alignItems: "center", gap: 16 }}>
            <span style={{ fontFamily: "'Source Sans 3', sans-serif", fontSize: 13, color: "#CDD0D6" }}>{agent.name}</span>
            <button
              onClick={onLogout}
              style={{ background: "transparent", border: "1px solid #6AE4FF", color: "#6AE4FF", fontSize: 13, padding: "9px 16px", borderRadius: 8, cursor: "pointer", fontFamily: "inherit", letterSpacing: "-0.036em" }}
            >
              Log Out
            </button>
          </div>
        </div>
      </div>

      {pendingNavigation && (
        <div style={{ position: "fixed", inset: 0, background: "rgba(23,32,46,0.75)", display: "flex", alignItems: "center", justifyContent: "center", zIndex: 1000, padding: 24 }}>
          <div style={{ background: "#202A3E", border: "1px solid #000000", borderRadius: 15, padding: 28, maxWidth: 380, width: "100%" }}>
            <h3 style={{ fontWeight: 700, fontSize: 19, marginBottom: 8, color: "#FFFFFF" }}>Leave without saving?</h3>
            <p style={{ fontFamily: "'Source Sans 3', sans-serif", fontSize: 13.5, color: "#CDD0D6", lineHeight: 1.6, marginBottom: 22, letterSpacing: "normal" }}>
              You have unsaved changes to this project. If you leave now, those changes will be lost.
            </p>
            <div style={{ display: "flex", gap: 8 }}>
              <button className="outline-btn" style={{ flex: 1 }} onClick={cancelLeave}>Stay &amp; Review</button>
              <button className="generate-btn" style={{ flex: 1, margin: 0 }} onClick={confirmLeave}>Leave Without Saving</button>
            </div>
          </div>
        </div>
      )}

      <div style={{ position: "relative", zIndex: 1, maxWidth: 1000, margin: "0 auto", padding: "32px" }}>
        {showProjectsManager ? (
          <ProjectsManager
            projects={projects}
            onProjectsChange={refreshProjects}
            onClose={() => guardNavigate(() => setShowProjectsManager(false))}
            isDirty={projectFormDirty}
            onDirtyChange={setProjectFormDirty}
            guardNavigate={guardNavigate}
            editingProjectId={editingProjectId}
            setEditingProjectId={setEditingProjectId}
            detailView={projectDetailView}
            setDetailView={setProjectDetailView}
            onSelectProject={setSelectedProjectId}
            onOpenTool={openTool}
            onFollowUpVisitor={openFollowUpForVisitor}
          />
        ) : showClientTracker ? (
          <ClientTracker
            projects={projects}
            openContactId={clientTrackerContactId}
            setOpenContactId={setClientTrackerContactId}
            onBack={() => guardNavigate(() => setShowClientTracker(false))}
            onOpenProject={(projectId) => {
              handleSelectProject(projectId);
              setShowClientTracker(false);
              setShowProjectsManager(true);
              setProjectDetailView(null);
            }}
            onFollowUpForClient={openFollowUpForClient}
          />
        ) : activeTab === "home" ? (
          <div style={{ maxWidth: 900, margin: "0 auto" }}>
            <div style={{ fontSize: 24, fontWeight: 700, marginBottom: 28 }}>
              Good {new Date().getHours() < 12 ? "morning" : new Date().getHours() < 18 ? "afternoon" : "evening"}, {agent.name}
            </div>
            <HomeAttentionSection projects={projects} onFollowUpVisitor={openFollowUpForVisitor} onOpenClient={openClientFromHome} />
            <div style={EYEBROW_LABEL}>Manage</div>
            <div className="tool-list" style={{ marginBottom: 28 }}>
              <div className="tool-item" onClick={handleProjectsButtonClick}>
                <div className="item-text">
                  <span className="item-name">Projects</span>
                  <span className="item-desc">{projects.length} active</span>
                </div>
                <span className="item-arrow">→</span>
              </div>
              <div className="tool-item" onClick={handleClientTrackerButtonClick}>
                <div className="item-text">
                  <span className="item-name">Client Tracker</span>
                  <span className="item-desc">Every contact, one pipeline.</span>
                </div>
                <span className="item-arrow">→</span>
              </div>
            </div>
            <div style={EYEBROW_LABEL}>Tools</div>
            <div className="tool-list">
              {TOOL_DEFS.map((t) => (
                <div key={t.id} className="tool-item" onClick={() => openTool(t.id, false)}>
                  <div className="item-text">
                    <span className="item-name">{t.label}</span>
                    <span className="item-desc">{t.desc}</span>
                  </div>
                  <span className="item-arrow">→</span>
                </div>
              ))}
            </div>
          </div>
        ) : (
          <>
            {activeTab === "listing" && (
              <>
                <ToolHeader title="Listing" oneLiner={oneLiner} onBack={goBackFromTool} />
                <ListingTab
                  property={property}
                  setProperty={setProperty}
                  voiceProfile={voiceProfile}
                  projects={projects}
                  selectedProjectId={selectedProjectId}
                  onSelectProject={handleSelectProject}
                  onProjectSaved={refreshProjects}
                />
              </>
            )}
            {activeTab === "social" && (
              <>
                <ToolHeader title="Social" oneLiner={oneLiner} onBack={goBackFromTool} />
                <SocialTab
                  property={property}
                  voiceProfile={voiceProfile}
                  projects={projects}
                  selectedProjectId={selectedProjectId}
                  onSelectProject={handleSelectProject}
                />
              </>
            )}
            {activeTab === "email" && (
              <>
                <ToolHeader title="Follow-Up" oneLiner={oneLiner} onBack={goBackFromTool} />
                {emailPrefill && (
                  <div className="context-pill">
                    {toolReturnView === "clienttracker"
                      ? `${emailPrefill.name} · Client Follow-Up`
                      : `${projects.find((p) => p.id === selectedProjectId)?.address || "Project"} · Open House Follow-Up`}
                  </div>
                )}
                <EmailTab
                  voiceProfile={voiceProfile}
                  projects={projects}
                  selectedProjectId={selectedProjectId}
                  onSelectProject={handleSelectProject}
                  prefillContactName={emailPrefill ? emailPrefill.name : undefined}
                  prefillContactId={emailPrefill ? emailPrefill.contactId : undefined}
                  prefillDetail={emailPrefill ? emailPrefill.detail : undefined}
                  prefillSituation={emailPrefill ? emailPrefill.situation : undefined}
                />
              </>
            )}
            {activeTab === "reply" && (
              <>
                <ToolHeader title="Reply" oneLiner={oneLiner} onBack={goBackFromTool} />
                <ReplyTab
                  voiceProfile={voiceProfile}
                  projects={projects}
                  selectedProjectId={selectedProjectId}
                  onSelectProject={handleSelectProject}
                />
              </>
            )}
            {activeTab === "voice" && (
              <>
                <ToolHeader title="Voice" oneLiner={oneLiner} onBack={goBackFromTool} />
                <VoiceTab voiceProfile={voiceProfile} onVoiceProfileChange={handleVoiceProfileChange} />
              </>
            )}
          </>
        )}
      </div>
    </div>
  );
}

// ===== Hero background shader (deliberate exception to the zero-blur /
// restrained-color design system rules, signed off explicitly) =====
// Adapted from a 21st.dev "Neuro Noise" Shader Builder component
// (https://shaders.paper.design/neuro-noise, Paper Shaders, Apache-2.0).
// Stripped of TypeScript/Next.js/shadcn — this app has no build step
// (Babel Standalone, "react" preset only, see public/index.html), so the
// component is plain JS and lives inline here rather than under a
// components/ui folder or an @/ import alias.

const SHADER_VERT = `attribute vec2 a_position;
void main() {
  gl_Position = vec4(a_position, 0.0, 1.0);
}`;

const SHADER_FRAG = `#ifdef GL_FRAGMENT_PRECISION_HIGH
precision highp float;
#else
precision mediump float;
#endif

uniform vec3 u_colors[8];
uniform vec4 u_scene;
uniform vec4 u_shape;
uniform vec4 u_surface;
uniform vec4 u_finish;
uniform vec4 u_transform;
uniform vec4 u_space;
uniform vec4 u_cursor;

#define u_resolution u_scene.xy
#define u_time u_scene.z
#define u_colorCount u_scene.w
#define u_scale u_shape.x
#define u_intensity u_shape.y
#define u_paramA u_shape.z
#define u_warp u_shape.w
#define u_detail u_surface.x
#define u_contrast u_surface.y
#define u_brightness u_surface.z
#define u_saturation u_surface.w
#define u_hue u_finish.x
#define u_vignette u_finish.y
#define u_blur u_finish.z
#define u_grain u_finish.w
#ifdef GL_FRAGMENT_PRECISION_HIGH
#define u_seed u_transform.x
#else
#define u_seed mod(u_transform.x, 31.0)
#endif
#define u_rotate u_transform.y
#define u_drift u_transform.z
#define u_oklab u_transform.w
#define u_offset u_space.xy
#define u_mouse u_space.zw
#define u_cursorPresence u_cursor.x
#define u_cursorEffect u_cursor.y
#define u_cursorStrength u_cursor.z
#define u_cursorRadius u_cursor.w

float hash21(vec2 p) {
#ifndef GL_FRAGMENT_PRECISION_HIGH
  p = mod(p, 31.0);
#endif
  p = fract(p * vec2(234.34, 435.345));
  p += dot(p, p + 34.23);
  return fract(p.x * p.y);
}

float grainHash(vec2 p) {
  vec3 p3 = fract(vec3(p.xyx) * 0.1031);
  p3 += dot(p3, p3.yzx + 33.33);
  return fract((p3.x + p3.y) * p3.z);
}

vec2 hash22(vec2 p) {
#ifndef GL_FRAGMENT_PRECISION_HIGH
  p = mod(p, 31.0);
#endif
  float n = sin(dot(p, vec2(41.0, 289.0)));
  return fract(vec2(15731.743, 7892.321) * n);
}

float noise(vec2 p) {
  vec2 i = floor(p);
  vec2 f = fract(p);
  vec2 u = f * f * (3.0 - 2.0 * f);
  return mix(
    mix(hash21(i), hash21(i + vec2(1.0, 0.0)), u.x),
    mix(hash21(i + vec2(0.0, 1.0)), hash21(i + vec2(1.0, 1.0)), u.x),
    u.y);
}

float fbm(vec2 p) {
  float v = 0.0;
  float a = 0.5;
  for (int i = 0; i < 5; i++) {
    v += a * noise(p);
    p = p * 2.03 + vec2(17.0, 9.2);
    a *= 0.5;
  }
  return v;
}

vec3 srgbToLinear(vec3 c) {
  return mix(c / 12.92, pow((c + 0.055) / 1.055, vec3(2.4)),
    step(0.04045, c));
}
vec3 linearToSrgb(vec3 c) {
  return mix(c * 12.92, 1.055 * pow(max(c, vec3(0.0)), vec3(1.0 / 2.4)) - 0.055,
    step(0.0031308, c));
}
vec3 linToOklab(vec3 c) {
  float l = 0.4122214708 * c.r + 0.5363325363 * c.g + 0.0514459929 * c.b;
  float m = 0.2119034982 * c.r + 0.6806995451 * c.g + 0.1073969566 * c.b;
  float s = 0.0883024619 * c.r + 0.2817188376 * c.g + 0.6299787005 * c.b;
  l = pow(max(l, 0.0), 1.0 / 3.0);
  m = pow(max(m, 0.0), 1.0 / 3.0);
  s = pow(max(s, 0.0), 1.0 / 3.0);
  return vec3(
    0.2104542553 * l + 0.7936177850 * m - 0.0040720468 * s,
    1.9779984951 * l - 2.4285922050 * m + 0.4505937099 * s,
    0.0259040371 * l + 0.7827717662 * m - 0.8086757660 * s);
}
vec3 oklabToLin(vec3 c) {
  float l = c.x + 0.3963377774 * c.y + 0.2158037573 * c.z;
  float m = c.x - 0.1055613458 * c.y - 0.0638541728 * c.z;
  float s = c.x - 0.0894841775 * c.y - 1.2914855480 * c.z;
  l = l * l * l; m = m * m * m; s = s * s * s;
  return vec3(
    4.0767416621 * l - 3.3077115913 * m + 0.2309699292 * s,
    -1.2684380046 * l + 2.6097574011 * m - 0.3413193965 * s,
    -0.0041960863 * l - 0.7034186147 * m + 1.7076147010 * s);
}
vec3 mixColour(vec3 a, vec3 b, float t) {
  if (u_oklab > 0.5) {
    vec3 la = linToOklab(srgbToLinear(a));
    vec3 lb = linToOklab(srgbToLinear(b));
    return clamp(linearToSrgb(oklabToLin(mix(la, lb, t))), 0.0, 1.0);
  }
  return mix(a, b, t);
}

vec3 palette(float x) {
  float n = max(u_colorCount - 1.0, 1.0);
  float f = clamp(x, 0.0, 1.0) * n;
  vec3 col = u_colors[0];
  for (int i = 0; i < 7; i++) {
    if (float(i) < n)
      col = mixColour(col, u_colors[i + 1],
        smoothstep(0.0, 1.0, clamp(f - float(i), 0.0, 1.0)));
  }
  return col;
}

vec3 hueRotate(vec3 col, float a) {
  const mat3 toYIQ = mat3(0.299, 0.596, 0.211,
                          0.587, -0.274, -0.523,
                          0.114, -0.322, 0.312);
  const mat3 toRGB = mat3(1.0, 1.0, 1.0,
                          0.956, -0.272, -1.106,
                          0.621, -0.647, 1.703);
  vec3 yiq = toYIQ * col;
  float ca = cos(a), sa = sin(a);
  yiq = vec3(yiq.x, yiq.y * ca - yiq.z * sa, yiq.y * sa + yiq.z * ca);
  return toRGB * yiq;
}

vec3 shade(vec2 uv, vec2 p, float t) {
  vec2 q = p * (1.6 + u_intensity * 2.4);
  float field = 0.0;
  float weight = 0.55;
  for (int i = 0; i < 6; i++) {
    float fi = float(i);
    q += vec2(
      sin(q.y * (1.7 + fi * 0.09) + t * (0.35 + fi * 0.04) + u_seed),
      cos(q.x * (1.5 + fi * 0.11) - t * (0.28 + fi * 0.03))
    ) * (0.22 + u_intensity * 0.14);
    float filaments = abs(sin(q.x + q.y + fi * 0.72));
    field += weight / (0.08 + filaments);
    weight *= 0.62;
    q = q.yx * vec2(-1.08, 1.04);
  }
  float glow = 1.0 - exp(-field * (0.018 + u_paramA * 0.04));
  return palette(clamp(glow, 0.0, 1.0));
}

void main() {
  vec2 uv = gl_FragCoord.xy / u_resolution.xy;
  vec2 screenUv = uv;
  vec2 p = (gl_FragCoord.xy - 0.5 * u_resolution.xy)
    / min(u_resolution.x, u_resolution.y);
  float cursorMask = 0.0;

  if (u_cursorPresence > 0.001) {
    vec2 cursor = (0.5 * u_mouse * u_resolution.xy)
      / min(u_resolution.x, u_resolution.y);
    vec2 cursorDelta = p - cursor;
    if (u_cursorEffect < 0.5) {
      p += cursor * u_cursorPresence * u_cursorStrength * 0.55;
    } else {
      float cursorDistance = length(cursorDelta);
      vec2 cursorDirection = cursorDelta / max(cursorDistance, 0.0001);
      cursorMask = u_cursorPresence
        * (1.0 - smoothstep(0.0, u_cursorRadius, cursorDistance));
      if (u_cursorEffect < 1.5) {
        p -= cursorDirection * cursorMask * u_cursorStrength * 0.24;
      } else if (u_cursorEffect < 2.5) {
        float cursorAngle = cursorMask * u_cursorStrength * 2.2;
        float cc = cos(cursorAngle), cs = sin(cursorAngle);
        p = cursor + mat2(cc, -cs, cs, cc) * cursorDelta;
      } else if (u_cursorEffect < 3.5) {
        float ripple = sin(
          cursorDistance / max(u_cursorRadius, 0.001) * 18.0 - u_time * 5.0);
        p -= cursorDirection * ripple * cursorMask * u_cursorStrength * 0.07;
      }
    }
  }

  uv = p * min(u_resolution.x, u_resolution.y) / u_resolution.xy + 0.5;
  p *= u_scale;
  if (abs(u_rotate) > 0.0001) {
    float cr = cos(u_rotate), sr = sin(u_rotate);
    p = mat2(cr, -sr, sr, cr) * p;
  }
  p += u_offset;
  if (u_drift > 0.0001)
    p += u_drift * vec2(sin(u_time * 0.31), cos(u_time * 0.23));
  if (u_warp > 0.0) {
    p += u_warp * (vec2(
      fbm(p * u_detail + u_seed),
      fbm(p * u_detail + vec2(5.2, 1.3))) - 0.5);
  }
  vec3 col;
  if (u_blur > 0.0) {
    float e = u_blur;
    float pe = e * u_scale;
    vec2 uvE = vec2(e) * min(u_resolution.x, u_resolution.y) / u_resolution.xy;
    col  = shade(uv, p, u_time) * 0.36;
    col += shade(uv + vec2(uvE.x, 0.0), p + vec2(pe, 0.0), u_time) * 0.16;
    col += shade(uv - vec2(uvE.x, 0.0), p - vec2(pe, 0.0), u_time) * 0.16;
    col += shade(uv + vec2(0.0, uvE.y), p + vec2(0.0, pe), u_time) * 0.16;
    col += shade(uv - vec2(0.0, uvE.y), p - vec2(0.0, pe), u_time) * 0.16;
  } else {
    col = shade(uv, p, u_time);
  }
  if (abs(u_contrast - 1.0) > 0.0001)
    col = (col - 0.5) * u_contrast + 0.5;
  if (abs(u_saturation - 1.0) > 0.0001) {
    float luma = dot(col, vec3(0.299, 0.587, 0.114));
    col = mix(vec3(luma), col, u_saturation);
  }
  if (abs(u_hue) > 0.0001)
    col = hueRotate(col, u_hue);
  if (abs(u_brightness) > 0.0001)
    col += u_brightness;
  if (u_vignette > 0.0001) {
    float vd = length(screenUv - 0.5) * 1.41421356;
    col *= 1.0 - u_vignette * smoothstep(0.35, 1.0, vd);
  }
  if (u_cursorPresence > 0.001 && u_cursorEffect > 3.5)
    col += (vec3(0.18) + col * 0.12) * cursorMask * u_cursorStrength;
  if (u_grain > 0.0001)
    col += (grainHash(
      gl_FragCoord.xy + vec2(u_seed * 17.0, u_seed * 31.0)) - 0.5) * u_grain;
  gl_FragColor = vec4(clamp(col, 0.0, 1.0), 1.0);
}
`;

const SHADER_UNIFORMS = {
  colors: [[0.09019607843137255,0.12549019607843137,0.1803921568627451],[0.0392156862745098,0.27450980392156865,0.35294117647058826],[0.41568627450980394,0.8941176470588236,1],[0.8627450980392157,0.9725490196078431,1],[0.8627450980392157,0.9725490196078431,1],[0.8627450980392157,0.9725490196078431,1],[0.8627450980392157,0.9725490196078431,1],[0.8627450980392157,0.9725490196078431,1]],
  colorCount: 4,
  scale: 1.260,
  intensity: 0.350,
  paramA: 0.280,
  warp: 0.000,
  detail: 1.824,
  contrast: 1.005,
  brightness: -0.030,
  saturation: 1.480,
  hue: 0.0873,
  vignette: 0.000,
  blur: 0.0012,
  grain: 0.098,
  seed: 1.0,
  rotate: 0.0000,
  offsetX: 0.000,
  offsetY: 0.000,
  drift: 0.204,
  cursorEnabled: true,
  cursorEffect: 3.0,
  cursorStrength: 0.450,
  cursorRadius: 0.460,
  oklab: 0.0,
  timeScale: 0.860,
};

const shaderPendingContextReleases = new WeakMap();

function ShaderBackground({ className }) {
  const canvasRef = React.useRef(null);

  useEffect(() => {
    const canvas = canvasRef.current;
    if (!canvas) return;
    const pendingRelease = shaderPendingContextReleases.get(canvas);
    if (pendingRelease !== undefined) window.clearTimeout(pendingRelease);
    shaderPendingContextReleases.delete(canvas);
    const gl = canvas.getContext("webgl", { antialias: false });
    if (!gl) return;

    const compile = (type, src) => {
      const s = gl.createShader(type);
      gl.shaderSource(s, src);
      gl.compileShader(s);
      return s;
    };
    const program = gl.createProgram();
    const vertexShader = compile(gl.VERTEX_SHADER, SHADER_VERT);
    const fragmentShader = compile(gl.FRAGMENT_SHADER, SHADER_FRAG);
    gl.attachShader(program, vertexShader);
    gl.attachShader(program, fragmentShader);
    gl.linkProgram(program);
    gl.deleteShader(vertexShader);
    gl.deleteShader(fragmentShader);
    gl.useProgram(program);

    const buf = gl.createBuffer();
    gl.bindBuffer(gl.ARRAY_BUFFER, buf);
    gl.bufferData(
      gl.ARRAY_BUFFER,
      new Float32Array([-1, -1, 3, -1, -1, 3]),
      gl.STATIC_DRAW,
    );
    const loc = gl.getAttribLocation(program, "a_position");
    gl.enableVertexAttribArray(loc);
    gl.vertexAttribPointer(loc, 2, gl.FLOAT, false, 0, 0);

    const uni = {
      colors: gl.getUniformLocation(program, "u_colors"),
      scene: gl.getUniformLocation(program, "u_scene"),
      shape: gl.getUniformLocation(program, "u_shape"),
      surface: gl.getUniformLocation(program, "u_surface"),
      finish: gl.getUniformLocation(program, "u_finish"),
      transform: gl.getUniformLocation(program, "u_transform"),
      space: gl.getUniformLocation(program, "u_space"),
      cursor: gl.getUniformLocation(program, "u_cursor"),
    };
    gl.uniform3fv(uni.colors, new Float32Array(SHADER_UNIFORMS.colors.flat()));
    gl.uniform4f(
      uni.shape,
      SHADER_UNIFORMS.scale,
      SHADER_UNIFORMS.intensity,
      SHADER_UNIFORMS.paramA,
      SHADER_UNIFORMS.warp,
    );
    gl.uniform4f(
      uni.surface,
      SHADER_UNIFORMS.detail,
      SHADER_UNIFORMS.contrast,
      SHADER_UNIFORMS.brightness,
      SHADER_UNIFORMS.saturation,
    );
    gl.uniform4f(
      uni.finish,
      SHADER_UNIFORMS.hue,
      SHADER_UNIFORMS.vignette,
      SHADER_UNIFORMS.blur,
      SHADER_UNIFORMS.grain,
    );
    gl.uniform4f(
      uni.transform,
      SHADER_UNIFORMS.seed,
      SHADER_UNIFORMS.rotate,
      SHADER_UNIFORMS.drift,
      SHADER_UNIFORMS.oklab,
    );
    gl.uniform4f(
      uni.cursor,
      0,
      SHADER_UNIFORMS.cursorEffect,
      SHADER_UNIFORMS.cursorStrength,
      SHADER_UNIFORMS.cursorRadius,
    );

    let targetX = 0;
    let targetY = 0;
    let targetPresence = 0;
    let mouseX = 0;
    let mouseY = 0;
    let cursorPresence = 0;
    let pointerKnown = false;
    let pointerClientX = 0;
    let pointerClientY = 0;
    let bounds = canvas.getBoundingClientRect();
    let raf = 0;
    let lastNow = null;
    let visible = document.visibilityState === "visible";
    let inView = true;
    let disposed = false;
    const start = performance.now();
    const prefersReducedMotion = window.matchMedia && window.matchMedia("(prefers-reduced-motion: reduce)").matches;
    const timeAnimated = !prefersReducedMotion && Math.abs(SHADER_UNIFORMS.timeScale) > 0.0001;

    const resizeCanvas = () => {
      const dpr = Math.min(window.devicePixelRatio || 1, 2);
      const rawWidth = Math.max(1, Math.round(bounds.width * dpr));
      const rawHeight = Math.max(1, Math.round(bounds.height * dpr));
      const pixelScale = Math.min(
        1,
        Math.sqrt(2000000 / Math.max(1, rawWidth * rawHeight)),
      );
      const width = Math.max(1, Math.round(rawWidth * pixelScale));
      const height = Math.max(1, Math.round(rawHeight * pixelScale));
      if (canvas.width !== width || canvas.height !== height) {
        canvas.width = width;
        canvas.height = height;
        gl.viewport(0, 0, width, height);
      }
    };

    function requestRender() {
      if (!disposed && visible && inView && raf === 0) {
        raf = requestAnimationFrame(render);
      }
    }

    const updatePointerTarget = () => {
      if (!pointerKnown) return;
      if (bounds.width === 0 || bounds.height === 0) return;
      const inside =
        pointerClientX >= bounds.left &&
        pointerClientX <= bounds.right &&
        pointerClientY >= bounds.top &&
        pointerClientY <= bounds.bottom;
      if (!inside) {
        targetPresence = 0;
        requestRender();
        return;
      }
      const nextX = ((pointerClientX - bounds.left) / bounds.width) * 2 - 1;
      const nextY = -(((pointerClientY - bounds.top) / bounds.height) * 2 - 1);
      if (targetPresence === 0 && cursorPresence < 0.01) {
        mouseX = nextX;
        mouseY = nextY;
      }
      targetX = nextX;
      targetY = nextY;
      targetPresence = 1;
      requestRender();
    };
    const onPointerMove = (event) => {
      pointerKnown = true;
      pointerClientX = event.clientX;
      pointerClientY = event.clientY;
      bounds = canvas.getBoundingClientRect();
      updatePointerTarget();
    };
    const onPointerLeave = () => {
      pointerKnown = false;
      targetPresence = 0;
      requestRender();
    };
    const updateLayout = () => {
      bounds = canvas.getBoundingClientRect();
      resizeCanvas();
      updatePointerTarget();
      requestRender();
    };
    window.addEventListener("resize", updateLayout);
    if (SHADER_UNIFORMS.cursorEnabled && !prefersReducedMotion) {
      window.addEventListener("pointermove", onPointerMove, { passive: true });
      window.addEventListener("pointercancel", onPointerLeave);
      window.addEventListener("scroll", updateLayout, true);
      window.addEventListener("blur", onPointerLeave);
      document.documentElement.addEventListener("pointerleave", onPointerLeave);
    }

    const resizeObserver = new ResizeObserver(updateLayout);
    resizeObserver.observe(canvas);
    const intersectionObserver = new IntersectionObserver(([entry]) => {
      inView = entry ? entry.isIntersecting : true;
      if (inView) requestRender();
      else if (raf !== 0) {
        cancelAnimationFrame(raf);
        raf = 0;
        lastNow = null;
      }
    });
    intersectionObserver.observe(canvas);
    const onVisibilityChange = () => {
      visible = document.visibilityState === "visible";
      if (visible) requestRender();
      else if (raf !== 0) {
        cancelAnimationFrame(raf);
        raf = 0;
        lastNow = null;
      }
    };
    document.addEventListener("visibilitychange", onVisibilityChange);

    function render(now) {
      raf = 0;
      if (disposed || !visible || !inView) return;
      const dt = lastNow === null ? 0 : Math.min((now - lastNow) / 1000, 0.1);
      lastNow = now;
      const follow = 1 - Math.exp(-12 * dt);
      mouseX += (targetX - mouseX) * follow;
      mouseY += (targetY - mouseY) * follow;
      cursorPresence += (targetPresence - cursorPresence) * follow;
      resizeCanvas();
      const width = canvas.width;
      const height = canvas.height;
      gl.uniform4f(
        uni.scene,
        width,
        height,
        ((now - start) / 1000) * SHADER_UNIFORMS.timeScale,
        SHADER_UNIFORMS.colorCount,
      );
      gl.uniform4f(
        uni.space,
        SHADER_UNIFORMS.offsetX,
        SHADER_UNIFORMS.offsetY,
        mouseX,
        mouseY,
      );
      gl.uniform4f(
        uni.cursor,
        SHADER_UNIFORMS.cursorEnabled ? cursorPresence : 0,
        SHADER_UNIFORMS.cursorEffect,
        SHADER_UNIFORMS.cursorStrength,
        SHADER_UNIFORMS.cursorRadius,
      );
      gl.drawArrays(gl.TRIANGLES, 0, 3);
      const pointerSettling =
        Math.abs(targetX - mouseX) > 0.001 ||
        Math.abs(targetY - mouseY) > 0.001 ||
        Math.abs(targetPresence - cursorPresence) > 0.001;
      if (timeAnimated || pointerSettling) requestRender();
      else lastNow = null;
    }
    requestRender();
    return () => {
      disposed = true;
      cancelAnimationFrame(raf);
      resizeObserver.disconnect();
      intersectionObserver.disconnect();
      document.removeEventListener("visibilitychange", onVisibilityChange);
      window.removeEventListener("resize", updateLayout);
      if (SHADER_UNIFORMS.cursorEnabled) {
        window.removeEventListener("pointermove", onPointerMove);
        window.removeEventListener("pointercancel", onPointerLeave);
        window.removeEventListener("scroll", updateLayout, true);
        window.removeEventListener("blur", onPointerLeave);
        document.documentElement.removeEventListener(
          "pointerleave",
          onPointerLeave,
        );
      }
      gl.deleteBuffer(buf);
      gl.deleteProgram(program);
      const releaseTimer = window.setTimeout(() => {
        if (shaderPendingContextReleases.get(canvas) !== releaseTimer) return;
        shaderPendingContextReleases.delete(canvas);
        gl.getExtension("WEBGL_lose_context") && gl.getExtension("WEBGL_lose_context").loseContext();
        canvas.width = 1;
        canvas.height = 1;
      }, 0);
      shaderPendingContextReleases.set(canvas, releaseTimer);
    };
  }, []);

  return (
    <canvas ref={canvasRef} className={className} style={{ display: "block", width: "100%", height: "100%" }} />
  );
}

// Flips a grid to visible the first time it scrolls into view, once only
// (observer disconnects after firing) — not a repeat-on-every-scroll effect.
function useRevealOnScroll() {
  const ref = React.useRef(null);
  const [visible, setVisible] = useState(false);

  useEffect(() => {
    const node = ref.current;
    if (!node) return;
    const observer = new IntersectionObserver(
      ([entry]) => {
        if (entry.isIntersecting) {
          setVisible(true);
          observer.disconnect();
        }
      },
      // Extends the trigger zone 400px below the actual viewport so the
      // fade-in starts, and finishes, before the card is actually scrolled
      // into view — otherwise fast scrolling outpaces the animation and
      // catches cards at partial opacity, letting the shader behind them
      // show through (looks like it's bleeding through the card, but it's
      // really just a card that's still fading in).
      { threshold: 0.15, rootMargin: "0px 0px 400px 0px" },
    );
    observer.observe(node);
    return () => observer.disconnect();
  }, []);

  return [ref, visible];
}

// Counts up from 0 to target once `active` turns true. Snaps straight to the
// final value under prefers-reduced-motion instead of animating.
function useCountUp(target, active) {
  const [value, setValue] = useState(0);

  useEffect(() => {
    if (!active) return;
    const prefersReducedMotion = window.matchMedia && window.matchMedia("(prefers-reduced-motion: reduce)").matches;
    if (prefersReducedMotion) {
      setValue(target);
      return;
    }
    const duration = 700;
    const start = performance.now();
    let raf;
    function tick(now) {
      const progress = Math.min((now - start) / duration, 1);
      setValue(Math.round(progress * target));
      if (progress < 1) raf = requestAnimationFrame(tick);
    }
    raf = requestAnimationFrame(tick);
    return () => cancelAnimationFrame(raf);
  }, [active, target]);

  return value;
}

// Cinematic scroll-pinned intro: a tall (220vh) wrapper with a sticky 100vh
// pin inside it. Scroll position through that wrapper drives progress 0..1,
// applied directly to refs every frame (no React state, matching the
// shader/tilt pattern elsewhere) — no animation library, no scroll-jacking
// beyond a normal sticky pin. The headline reveals across the first half,
// a cyan glow builds behind it as the "bold" beat, then the whole overlay
// fades out over the last stretch to hand off to the real hero underneath.
// Superseded the previous hero cursor-tilt effect — stacking scroll-driven
// and cursor-driven motion on the same reveal read as competing rather than
// additive, so the tilt was retired in favor of this single, clearer driver.
// Headline + subtext + both CTAs — the settled look of the hero, shared
// verbatim by the static (post-scroll-to-top) hero and by CinematicIntro
// once it completes. One markup source so the two can never drift apart
// visually again (they used to be two separately hand-written versions —
// different headline size, missing subtext/buttons in the pinned one).
function LandingHero({ onEnterApp, animateIn }) {
  // A single wrapping div, not a Fragment — CinematicIntro's pin is a flex
  // row container, and two bare siblings here would become two flex items
  // laid out side by side instead of stacked (found via screenshot: the
  // headline and subtext/buttons rendered as two columns instead of one
  // centered column).
  return (
    <div>
      <div className={animateIn ? "landing-static-hero-in" : undefined} style={{ textAlign: "center", padding: "100px 24px 40px" }}>
        <h1 style={{ fontSize: "clamp(48px, 9vw, 116px)", fontWeight: 700, lineHeight: 1.3, letterSpacing: "-0.036em", marginBottom: 0 }}>
          Every listing, every reply, in your <span style={{ color: "#6AE4FF" }}>voice</span>.
        </h1>
      </div>
      <section
        style={{
          position: "relative",
          display: "flex",
          alignItems: "center",
          justifyContent: "center",
          padding: "40px 24px 80px",
          textAlign: "center",
        }}
      >
        <div style={{ maxWidth: 1000, margin: "0 auto" }}>
          <p className="landing-hero-in-2" style={{ fontFamily: "'Source Sans 3', sans-serif", fontSize: 20, color: "#CDD0D6", maxWidth: 580, margin: "0 auto 44px", lineHeight: 1.6, letterSpacing: "normal" }}>
            Generate listings, captions, and follow-ups that actually sound like you, then track every deal from first contact to closed.
          </p>
          <div className="landing-hero-in-3" style={{ display: "flex", gap: 16, justifyContent: "center", marginBottom: 12 }}>
            <button className="shiny-cta" onClick={onEnterApp} style={{ borderRadius: 80, padding: "18px 32px", fontSize: 17, fontWeight: 600, cursor: "pointer", fontFamily: "inherit", letterSpacing: "-0.036em" }}>
              <span>Enter App</span>
            </button>
            <a href="#tools" className="landing-flow-btn" style={{ background: "transparent", border: "1px solid #6AE4FF", borderRadius: 80, padding: "18px 30px", fontSize: 17, fontFamily: "inherit", letterSpacing: "-0.036em" }}>
              <span className="landing-flow-btn-arrow landing-flow-btn-arrow-in">→</span>
              <span className="landing-flow-btn-text">See what's inside</span>
              <span className="landing-flow-btn-circle" />
              <span className="landing-flow-btn-arrow landing-flow-btn-arrow-out">→</span>
            </a>
          </div>
        </div>
      </section>
    </div>
  );
}

function CinematicIntro({ onComplete, onEnterApp }) {
  const wrapperRef = React.useRef(null);
  const pinRef = React.useRef(null);
  const glowRef = React.useRef(null);
  const line1Ref = React.useRef(null);
  const line2Ref = React.useRef(null);
  // Two separate one-way ratchets, not one combined flag: the header should
  // reveal partway through the headline building in, well before the pin's
  // content actually swaps to the fully settled hero (subtext + buttons).
  // Both are "once true, never false again" regardless of scroll direction.
  const headerShownRef = React.useRef(false);
  const settledRef = React.useRef(false);
  const [settled, setSettled] = useState(false);

  useEffect(() => {
    // Nothing left to drive once settled — the pin's content has switched
    // to the static LandingHero markup below, which doesn't animate on
    // scroll at all, so there's no more scroll-linked state to compute.
    if (settled) return;

    const wrapper = wrapperRef.current;
    if (!wrapper) return;
    let raf = 0;

    function clamp01(v) {
      return Math.min(Math.max(v, 0), 1);
    }
    function lerp(a, b, t) {
      return a + (b - a) * t;
    }
    function mapRange(v, inMin, inMax, outMin, outMax) {
      const t = clamp01((v - inMin) / (inMax - inMin));
      return lerp(outMin, outMax, t);
    }

    function apply(progress) {
      const line1 = line1Ref.current;
      const line2 = line2Ref.current;
      const glow = glowRef.current;
      const pin = pinRef.current;
      if (!line1 || !line2 || !glow || !pin) return;

      line1.style.opacity = mapRange(progress, 0, 0.18, 0, 1);
      line1.style.transform = `translateY(${mapRange(progress, 0, 0.18, 24, 0)}px)`;

      line2.style.opacity = mapRange(progress, 0.14, 0.36, 0, 1);
      line2.style.transform = `translateY(${mapRange(progress, 0.14, 0.36, 24, 0)}px)`;

      const glowIn = mapRange(progress, 0.28, 0.6, 0, 1);
      const glowOut = mapRange(progress, 0.82, 1, 0, 1);
      glow.style.opacity = glowIn * (1 - glowOut);
      glow.style.transform = `scale(${mapRange(progress, 0.28, 1, 0.6, 1.3)})`;

      // Header ratchet: fires once the headline is fully built in (line2
      // finishes at 0.36) and the user has kept scrolling a bit further —
      // "a bit further down the header fades in", distinctly after the
      // headline, not at the same instant. Never un-fires on reverse scroll.
      if (progress >= 0.42 && !headerShownRef.current) {
        headerShownRef.current = true;
        if (onComplete) onComplete();
      }

      // Settle ratchet: fires once continued scrolling reaches the fully
      // final state — swaps the pin's children to the real LandingHero
      // (headline + subtext + both buttons), matching the static hero
      // exactly. Deliberately well after the header ratchet above, not
      // bundled into the same instant: the header appears mid-buildup,
      // full settle happens further down still. Once armed, this effect
      // tears down entirely (see the `if (settled) return` above) so
      // nothing computed here can ever run again — scrolling back up from
      // anywhere, on this pass or any later one, shows the settled hero
      // instantly with zero animation.
      if (progress >= 0.62 && !settledRef.current) {
        settledRef.current = true;
        setSettled(true);
      }
    }

    function computeAndApply() {
      raf = 0;
      const rect = wrapper.getBoundingClientRect();
      const total = wrapper.offsetHeight - window.innerHeight;
      const progress = total > 0 ? clamp01(-rect.top / total) : 0;
      apply(progress);
    }

    function onScroll() {
      if (!raf) raf = requestAnimationFrame(computeAndApply);
    }

    window.addEventListener("scroll", onScroll, { passive: true });
    window.addEventListener("resize", onScroll);
    computeAndApply();
    return () => {
      window.removeEventListener("scroll", onScroll);
      window.removeEventListener("resize", onScroll);
      cancelAnimationFrame(raf);
    };
  }, [settled]);

  return (
    <div ref={wrapperRef} className="landing-intro-wrapper">
      <div ref={pinRef} className="landing-intro-pin">
        {settled ? (
          <LandingHero onEnterApp={onEnterApp} animateIn={true} />
        ) : (
          <>
            <div ref={glowRef} className="landing-intro-glow" />
            <div style={{ position: "relative", zIndex: 1, textAlign: "center", padding: "0 24px" }}>
              <div ref={line1Ref} className="landing-intro-line" style={{ fontSize: "clamp(36px, 6.5vw, 84px)", fontWeight: 700, color: "#FFFFFF", letterSpacing: "-0.036em", lineHeight: 1.2 }}>
                Every listing, every reply,
              </div>
              <div ref={line2Ref} className="landing-intro-line" style={{ fontSize: "clamp(36px, 6.5vw, 84px)", fontWeight: 700, color: "#FFFFFF", letterSpacing: "-0.036em", lineHeight: 1.2 }}>
                in your <span style={{ color: "#6AE4FF" }}>voice</span>.
              </div>
            </div>
          </>
        )}
      </div>
    </div>
  );
}

// Nav pill whose fill radiates outward from wherever the pointer entered
// (or from center, on keyboard focus). Plain CSS transform + React state,
// no animation library, matching this app's no-build-step constraint.
function OriginNavLink({ href, children }) {
  const linkRef = React.useRef(null);
  const [hovered, setHovered] = useState(false);
  const [origin, setOrigin] = useState({ x: 0, y: 0 });
  const [coverSize, setCoverSize] = useState(0);

  function updateOrigin(x, y) {
    const node = linkRef.current;
    if (!node) return;
    const rect = node.getBoundingClientRect();
    const diameter = 2 * Math.max(
      Math.hypot(x, y),
      Math.hypot(rect.width - x, y),
      Math.hypot(x, rect.height - y),
      Math.hypot(rect.width - x, rect.height - y),
    );
    setOrigin({ x, y });
    setCoverSize(Math.ceil(diameter));
  }

  return (
    <a
      ref={linkRef}
      href={href}
      className="landing-origin-link"
      onPointerEnter={(e) => {
        const rect = e.currentTarget.getBoundingClientRect();
        updateOrigin(e.clientX - rect.left, e.clientY - rect.top);
        setHovered(true);
      }}
      onPointerLeave={() => setHovered(false)}
      onFocus={() => {
        const node = linkRef.current;
        if (node) {
          const rect = node.getBoundingClientRect();
          updateOrigin(rect.width / 2, rect.height / 2);
        }
        setHovered(true);
      }}
      onBlur={() => setHovered(false)}
    >
      <span
        className="landing-origin-link-fill"
        style={{
          width: coverSize,
          height: coverSize,
          left: origin.x,
          top: origin.y,
          transform: hovered && coverSize > 0 ? "translate(-50%, -50%) scale(1)" : "translate(-50%, -50%) scale(0)",
        }}
      />
      <span className="landing-origin-link-text" style={{ color: hovered ? "#17202E" : "#FFFFFF" }}>
        {children}
      </span>
    </a>
  );
}

function LandingPage({ onEnterApp }) {
  const prefersReducedMotion = window.matchMedia && window.matchMedia("(prefers-reduced-motion: reduce)").matches;
  // introPlayed: the cinematic intro has completed at least once this session
  // (reveals the header — no reason to keep hiding it once seen).
  // collapsed: the user has scrolled back to the top after having seen it —
  // only then do we swap to the static, non-pinned headline. Swapping any
  // earlier (e.g. the instant the intro completes, still scrolled deep into
  // it) would shrink the page above the viewport and yank scroll position;
  // waiting until they're back at the top makes the swap invisible instead.
  const [introPlayed, setIntroPlayed] = useState(false);
  const [collapsed, setCollapsed] = useState(false);

  useEffect(() => {
    if (!introPlayed || collapsed) return;
    function onScroll() {
      if (window.scrollY < 50) setCollapsed(true);
    }
    window.addEventListener("scroll", onScroll, { passive: true });
    onScroll();
    return () => window.removeEventListener("scroll", onScroll);
  }, [introPlayed, collapsed]);

  const showStaticHero = prefersReducedMotion || collapsed;
  const showHeader = prefersReducedMotion || introPlayed;

  const [toolsGridRef, toolsGridVisible] = useRevealOnScroll();
  const [manageGridRef, manageGridVisible] = useRevealOnScroll();
  const [roadmapGridRef, roadmapGridVisible] = useRevealOnScroll();
  const [statsRef, statsVisible] = useRevealOnScroll();
  const toolsCount = useCountUp(5, statsVisible);
  const platformsCount = useCountUp(4, statsVisible);

  return (
    <div style={{ minHeight: "100vh", background: "#17202E", color: "#FFFFFF", fontFamily: "'Open Sans', sans-serif", letterSpacing: "-0.036em" }}>
      <style>{`
        .landing-tool-card { position: relative; z-index: 1; isolation: isolate; background: #202A3E; border: 1px solid #000000; border-radius: 15px; padding: 24px; cursor: pointer; transition: border-color 220ms cubic-bezier(0.16,1,0.3,1), transform 220ms cubic-bezier(0.16,1,0.3,1); }
        .landing-tool-card:hover { border-color: #6AE4FF; transform: translateY(-2px); }
        .landing-tool-card:hover .landing-tool-icon { background: #6AE4FF; color: #17202E; }
        .landing-tool-top { display: flex; align-items: center; gap: 12px; margin-bottom: 20px; }
        .landing-tool-icon { width: 34px; height: 34px; border-radius: 50%; background: #17202E; border: 1px solid #6AE4FF; display: flex; align-items: center; justify-content: center; color: #6AE4FF; font-size: 15px; flex-shrink: 0; transition: background 220ms ease, color 220ms ease; }
        .landing-roadmap-tile:hover .landing-tool-icon { background: rgba(106,228,255,0.18); }
        .landing-tool-name { font-size: 20px; font-weight: 600; }
        .landing-tool-desc { font-family: 'Source Sans 3', sans-serif; font-size: 14px; color: #CDD0D6; line-height: 1.5; margin-bottom: 22px; letter-spacing: normal; min-height: 42px; }
        .landing-tool-bottom { display: flex; align-items: center; justify-content: space-between; }
        .landing-tool-pill { font-family: 'Source Sans 3', sans-serif; font-size: 12px; background: #17202E; color: #CDD0D6; padding: 6px 12px; border-radius: 80px; }
        .landing-roadmap-tile { position: relative; z-index: 1; isolation: isolate; background: #17202E; border: 1px solid #000000; border-radius: 15px; padding: 32px 20px; text-align: center; }
        .landing-roadmap-badge { position: absolute; top: 14px; right: 14px; font-family: 'Source Sans 3', sans-serif; font-size: 10px; font-weight: 600; letter-spacing: 0.04em; text-transform: uppercase; color: #6AE4FF; border: 1px solid #6AE4FF; border-radius: 80px; padding: 3px 9px; }

        /* Workspace cards get more room since there are only 2 — larger icon,
           rounded-square container (the existing 8px "small element" token,
           not a new value) instead of Tools' circle, larger title. */
        .landing-workspace-card { padding: 36px; }
        .landing-workspace-icon { width: 44px; height: 44px; border-radius: 8px; font-size: 18px; }
        .landing-workspace-name { font-size: 24px; }
        .landing-header-name { white-space: nowrap; }
        .landing-nav-links { display: flex; gap: 32px; }
        @media (max-width: 640px) {
          .landing-nav-links { display: none; }
        }
        .landing-page-shader {
          position: fixed;
          inset: 0;
          z-index: 0;
          pointer-events: none;
          -webkit-mask-image: linear-gradient(to bottom, transparent 0%, black 32%, black 82%, transparent 100%);
          mask-image: linear-gradient(to bottom, transparent 0%, black 32%, black 82%, transparent 100%);
        }

        /* Enter App buttons: shiny animated border, ported to the app's white/black/cyan colors */
        @property --gradient-angle { syntax: "<angle>"; initial-value: 0deg; inherits: false; }
        @property --gradient-angle-offset { syntax: "<angle>"; initial-value: 0deg; inherits: false; }
        @property --gradient-percent { syntax: "<percentage>"; initial-value: 5%; inherits: false; }
        @property --gradient-shine { syntax: "<color>"; initial-value: #E0FBFF; inherits: false; }

        .shiny-cta {
          --shiny-cta-bg: #FFFFFF;
          --shiny-cta-bg-subtle: #EDEDED;
          --shiny-cta-fg: #000000;
          --shiny-cta-highlight: #6AE4FF;
          --shiny-cta-highlight-subtle: #B8F2FF;
          --animation: gradient-angle linear infinite;
          --duration: 3s;
          --shadow-size: 2px;
          --transition: 800ms cubic-bezier(0.25, 1, 0.5, 1);
          isolation: isolate;
          position: relative;
          overflow: hidden;
          outline-offset: 4px;
          background: linear-gradient(var(--shiny-cta-bg), var(--shiny-cta-bg)) padding-box,
            conic-gradient(
              from calc(var(--gradient-angle) - var(--gradient-angle-offset)),
              transparent,
              var(--shiny-cta-highlight) var(--gradient-percent),
              var(--gradient-shine) calc(var(--gradient-percent) * 2),
              var(--shiny-cta-highlight) calc(var(--gradient-percent) * 3),
              transparent calc(var(--gradient-percent) * 4)
            ) border-box;
          border: 1px solid var(--shiny-cta-bg-subtle);
          transition: var(--transition);
          transition-property: --gradient-angle-offset, --gradient-percent, --gradient-shine;
        }
        .shiny-cta::before, .shiny-cta::after, .shiny-cta span::before {
          content: "";
          pointer-events: none;
          position: absolute;
          inset-inline-start: 50%;
          inset-block-start: 50%;
          translate: -50% -50%;
          z-index: -1;
        }
        .shiny-cta:active { translate: 0 1px; }
        .shiny-cta::before {
          --size: calc(100% - var(--shadow-size) * 3);
          --position: 2px;
          --space: calc(var(--position) * 2);
          width: var(--size);
          height: var(--size);
          background: radial-gradient(circle at var(--position) var(--position), var(--shiny-cta-fg) calc(var(--position) / 4), transparent 0) padding-box;
          background-size: var(--space) var(--space);
          background-repeat: space;
          mask-image: conic-gradient(from calc(var(--gradient-angle) + 45deg), black, transparent 10% 90%, black);
          border-radius: inherit;
          opacity: 0.15;
          z-index: -1;
        }
        .shiny-cta::after {
          --animation: shimmer linear infinite;
          width: 100%;
          aspect-ratio: 1;
          background: linear-gradient(-50deg, transparent, var(--shiny-cta-highlight), transparent);
          mask-image: radial-gradient(circle at bottom, transparent 40%, black);
          opacity: 0.35;
        }
        .shiny-cta span { z-index: 1; }
        .shiny-cta span::before {
          --size: calc(100% + 1rem);
          width: var(--size);
          height: var(--size);
          background: radial-gradient(ellipse at bottom, var(--shiny-cta-highlight) 0%, transparent 70%);
          opacity: 0;
          transition: opacity var(--transition);
          animation: calc(var(--duration) * 1.5) breathe linear infinite;
        }
        .shiny-cta, .shiny-cta::before, .shiny-cta::after {
          animation: var(--animation) var(--duration), var(--animation) calc(var(--duration) / 0.4) reverse paused;
          animation-composition: add;
        }
        .shiny-cta:is(:hover, :focus-visible) {
          --gradient-percent: 20%;
          --gradient-angle-offset: 95deg;
          --gradient-shine: var(--shiny-cta-highlight-subtle);
        }
        .shiny-cta:is(:hover, :focus-visible), .shiny-cta:is(:hover, :focus-visible)::before, .shiny-cta:is(:hover, :focus-visible)::after {
          animation-play-state: running;
        }
        .shiny-cta:is(:hover, :focus-visible) span::before { opacity: 1; }
        @keyframes gradient-angle { to { --gradient-angle: 360deg; } }
        @keyframes shimmer { to { rotate: 360deg; } }
        @keyframes breathe { from, to { scale: 1; } 50% { scale: 1.2; } }

        /* "See what's inside": flowing arrow + expanding-circle reveal, kept to the app's cyan/transparent ghost-button colors */
        .landing-flow-btn {
          position: relative;
          display: inline-flex;
          align-items: center;
          overflow: hidden;
          color: #6AE4FF;
          cursor: pointer;
          text-decoration: none;
          transition: border-color 600ms cubic-bezier(0.23,1,0.32,1), border-radius 600ms cubic-bezier(0.23,1,0.32,1);
        }
        .landing-flow-btn:hover { border-color: transparent; border-radius: 12px; }
        .landing-flow-btn:active { scale: 0.95; }
        .landing-flow-btn-arrow {
          position: absolute;
          font-size: 17px;
          color: #6AE4FF;
          z-index: 2;
          transition: left 800ms cubic-bezier(0.34,1.56,0.64,1), right 800ms cubic-bezier(0.34,1.56,0.64,1), color 500ms ease;
        }
        .landing-flow-btn-arrow-in { left: -25%; }
        .landing-flow-btn:hover .landing-flow-btn-arrow-in { left: 16px; color: #000000; }
        .landing-flow-btn-arrow-out { right: 16px; }
        .landing-flow-btn:hover .landing-flow-btn-arrow-out { right: -25%; color: #000000; }
        .landing-flow-btn-text {
          position: relative;
          z-index: 1;
          transform: translateX(-12px);
          transition: transform 800ms ease, color 500ms ease;
        }
        .landing-flow-btn:hover .landing-flow-btn-text { transform: translateX(12px); color: #000000; }
        .landing-flow-btn-circle {
          position: absolute;
          top: 50%;
          left: 50%;
          transform: translate(-50%, -50%);
          width: 16px;
          height: 16px;
          background: #6AE4FF;
          border-radius: 50%;
          opacity: 0;
          z-index: 0;
          transition: width 800ms cubic-bezier(0.19,1,0.22,1), height 800ms cubic-bezier(0.19,1,0.22,1), opacity 800ms cubic-bezier(0.19,1,0.22,1);
        }
        .landing-flow-btn:hover .landing-flow-btn-circle { width: 220px; height: 220px; opacity: 1; }

        /* Tools / Coming Soon nav links: fill radiates from the pointer position on hover/focus */
        .landing-origin-link {
          position: relative;
          display: inline-flex;
          align-items: center;
          justify-content: center;
          overflow: hidden;
          border: 1px solid #000000;
          border-radius: 80px;
          padding: 8px 16px;
          font-size: 14px;
          text-decoration: none;
          cursor: pointer;
        }
        .landing-origin-link-fill {
          position: absolute;
          border-radius: 50%;
          background: #6AE4FF;
          pointer-events: none;
          z-index: 0;
          transition: transform 500ms cubic-bezier(0.16,1,0.3,1);
        }
        .landing-origin-link-text { position: relative; z-index: 1; transition: color 300ms ease; }

        /* Shared tools/roadmap grid, with the mobile breakpoints .tool-list/.info-grid already use elsewhere */
        .landing-card-grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 16px; }
        @media (max-width: 900px) { .landing-card-grid { grid-template-columns: repeat(2, 1fr); } }
        @media (max-width: 600px) { .landing-card-grid { grid-template-columns: 1fr; } }
        /* Manage row is sized for exactly 2 cards. Scoped to >600px so it doesn't
           fight the inherited .landing-card-grid mobile breakpoint (same class of
           bug as the roadmap fill above — an unconditional override placed after
           a narrower media query wins at every width, including mobile). */
        @media (min-width: 601px) {
          .landing-manage-grid { grid-template-columns: repeat(2, 1fr); max-width: 760px; margin: 0 auto; }
        }

        /* Second, subtler ambient motion for the Workspace/Roadmap sections —
           a different visual language from the hero's sharp filament shader
           (a slow soft glow, not a repeat), so the page doesn't feel like it
           runs out of motion once past the hero. */
        .landing-section-ambient { position: relative; overflow: hidden; }
        .landing-ambient-glow {
          position: absolute;
          top: -120px;
          right: -100px;
          width: 480px;
          height: 480px;
          background: radial-gradient(circle, rgba(106,228,255,0.16) 0%, transparent 70%);
          border-radius: 50%;
          pointer-events: none;
          z-index: -1;
          animation: landing-ambient-drift 25s ease-in-out infinite alternate;
        }
        @keyframes landing-ambient-drift {
          0% { transform: translate(0, 0); }
          100% { transform: translate(-50px, 50px); }
        }
        @media (prefers-reduced-motion: reduce) {
          .landing-ambient-glow { animation: none; }
        }

        /* Hero entrance — for the subhead/buttons that follow the cinematic
           intro (or, under reduced motion, follow the static headline
           fallback directly). */
        @keyframes landing-hero-in {
          from { opacity: 0; transform: translateY(16px); }
          to { opacity: 1; transform: translateY(0); }
        }
        .landing-hero-in-2 { animation: landing-hero-in 700ms cubic-bezier(0.16,1,0.3,1) 120ms both; }
        .landing-hero-in-3 { animation: landing-hero-in 700ms cubic-bezier(0.16,1,0.3,1) 240ms both; }
        @media (prefers-reduced-motion: reduce) {
          .landing-hero-in-2, .landing-hero-in-3 { animation: none; }
        }

        /* Cinematic scroll-pinned intro */
        .landing-intro-wrapper { position: relative; height: 220vh; }
        .landing-intro-pin { position: sticky; top: 0; height: 100vh; display: flex; align-items: center; justify-content: center; overflow: hidden; }
        .landing-intro-glow {
          position: absolute;
          width: 900px;
          height: 900px;
          border-radius: 50%;
          background: radial-gradient(circle, rgba(106,228,255,0.32) 0%, rgba(106,228,255,0.08) 45%, transparent 70%);
          opacity: 0;
          pointer-events: none;
          z-index: 0;
        }
        .landing-intro-line { opacity: 0; will-change: opacity, transform; }

        /* Smooths the swap from the cinematic intro to the settled static
           header/headline — was an instant pop, now a brief crossfade. */
        @keyframes landing-static-hero-in { from { opacity: 0; } to { opacity: 1; } }
        .landing-static-hero-in { animation: landing-static-hero-in 500ms ease both; }
        /* Only span 2 columns above the single-column breakpoint — a span on a
           1-column grid forces the browser to create a phantom implicit column,
           which breaks the mobile stack back into two columns. */
        @media (min-width: 601px) { .landing-roadmap-fill { grid-column: span 2; } }

        /* One-time scroll-in reveal, staggered per card, plays once via IntersectionObserver */
        .landing-card-grid > * {
          opacity: 0;
          transform: translateY(10px);
          transition: opacity 500ms ease, transform 500ms ease;
        }
        .landing-card-grid.landing-reveal-visible > * { opacity: 1; transform: translateY(0); }
        .landing-card-grid > *:nth-child(1) { transition-delay: 0ms; }
        .landing-card-grid > *:nth-child(2) { transition-delay: 70ms; }
        .landing-card-grid > *:nth-child(3) { transition-delay: 140ms; }
        .landing-card-grid > *:nth-child(4) { transition-delay: 210ms; }
        .landing-card-grid > *:nth-child(5) { transition-delay: 280ms; }
        .landing-card-grid > *:nth-child(6) { transition-delay: 350ms; }

        /* Respect the OS-level motion preference: keep the hover/focus state cues
           (color, border) but drop the continuous animation and transform-based motion. */
        @media (prefers-reduced-motion: reduce) {
          .shiny-cta, .shiny-cta::before, .shiny-cta::after, .shiny-cta span::before {
            animation: none !important;
          }
          .landing-tool-card, .landing-tool-icon,
          .landing-flow-btn, .landing-flow-btn-arrow, .landing-flow-btn-text, .landing-flow-btn-circle,
          .landing-origin-link-fill, .landing-origin-link-text,
          .landing-card-grid > * {
            transition: none !important;
          }
          .landing-tool-card:hover { transform: none !important; }
          .landing-card-grid > * { opacity: 1 !important; transform: none !important; }
        }
      `}</style>

      <ShaderBackground className="landing-page-shader" />

      <div style={{ position: "relative", zIndex: 1 }}>
      {showHeader && (
        <div className={!prefersReducedMotion ? "landing-static-hero-in" : undefined} style={{ position: "sticky", top: 0, zIndex: 10, background: "linear-gradient(to bottom, #17202E 0%, #17202E 70%, rgba(23,32,46,0) 100%)", paddingBottom: 40, marginBottom: -40 }}>
          <header style={{ display: "flex", alignItems: "center", justifyContent: "space-between", gap: 16, padding: "22px 32px", maxWidth: 1200, margin: "0 auto" }}>
            <div style={{ display: "flex", alignItems: "center", gap: 10, fontWeight: 700, fontSize: 18 }}>
              <div style={{ width: 26, height: 26, borderRadius: 8, background: "#6AE4FF", display: "flex", alignItems: "center", justifyContent: "center", color: "#17202E", fontWeight: 700, fontSize: 13, flexShrink: 0 }}>AT</div>
              <span className="landing-header-name">The Agent's Toolkit</span>
            </div>
            <div className="landing-nav-links">
              <OriginNavLink href="#tools">Tools</OriginNavLink>
              <OriginNavLink href="#manage">Workspace</OriginNavLink>
              <OriginNavLink href="#roadmap">Coming Soon</OriginNavLink>
            </div>
            <button className="shiny-cta" onClick={onEnterApp} style={{ borderRadius: 80, padding: "12px 22px", fontSize: 14, fontWeight: 600, cursor: "pointer", fontFamily: "inherit", letterSpacing: "-0.036em", flexShrink: 0 }}>
              <span>Enter App</span>
            </button>
          </header>
        </div>
      )}

      {showStaticHero ? (
        <LandingHero onEnterApp={onEnterApp} animateIn={collapsed} />
      ) : (
        <CinematicIntro onComplete={() => setIntroPlayed(true)} onEnterApp={onEnterApp} />
      )}

      <section id="tools" style={{ maxWidth: 1200, margin: "0 auto", padding: "0 24px" }}>
        <div style={{ fontFamily: "'Source Sans 3', sans-serif", letterSpacing: "0.08em", textTransform: "uppercase", fontSize: 12, color: "#6AE4FF", fontWeight: 600, marginBottom: 10 }}>Your Toolkit</div>
        <h2 style={{ fontSize: 36, fontWeight: 700, marginBottom: 8 }}>Five tools, one voice.</h2>
        <p style={{ fontFamily: "'Source Sans 3', sans-serif", fontSize: 16, color: "#CDD0D6", marginBottom: 36, maxWidth: 560, letterSpacing: "normal" }}>
          Every tool below writes the way you do, once you've calibrated your voice, it carries through everywhere.
        </p>

        <div ref={toolsGridRef} className={`landing-card-grid${toolsGridVisible ? " landing-reveal-visible" : ""}`}>
          <div className="landing-tool-card" onClick={onEnterApp}>
            <div className="landing-tool-top"><div className="landing-tool-icon">✎</div><div className="landing-tool-name">Listing</div></div>
            <div className="landing-tool-desc">Writes in whatever tone and length you choose. Opened from a project, your address, price, and beds are already filled in.</div>
            <div className="landing-tool-bottom"><span className="landing-tool-pill">MLS to full site copy</span><span style={{ color: "#6AE4FF" }}>→</span></div>
          </div>
          <div className="landing-tool-card" onClick={onEnterApp}>
            <div className="landing-tool-top"><div className="landing-tool-icon">◨</div><div className="landing-tool-name">Social</div></div>
            <div className="landing-tool-desc">Not a generic caption spun for engagement. Grounded in real, cited research on what actually performs on Instagram, Facebook, LinkedIn, and TikTok.</div>
            <div className="landing-tool-bottom"><span className="landing-tool-pill">4 platforms</span><span style={{ color: "#6AE4FF" }}>→</span></div>
          </div>
          <div className="landing-tool-card" onClick={onEnterApp}>
            <div className="landing-tool-top"><div className="landing-tool-icon">✉</div><div className="landing-tool-name">Follow-Up</div></div>
            <div className="landing-tool-desc">Pick a real contact from your project instead of a blank name field. Sending one resets their follow-up clock automatically.</div>
            <div className="landing-tool-bottom"><span className="landing-tool-pill">Resets the clock on send</span><span style={{ color: "#6AE4FF" }}>→</span></div>
          </div>
          <div className="landing-tool-card" onClick={onEnterApp}>
            <div className="landing-tool-top"><div className="landing-tool-icon">↩</div><div className="landing-tool-name">Reply</div></div>
            <div className="landing-tool-desc">Paste in a rejected offer and it won't invent false hope or a reason to reconsider. It replies to what was actually said.</div>
            <div className="landing-tool-bottom"><span className="landing-tool-pill">Reads the room first</span><span style={{ color: "#6AE4FF" }}>→</span></div>
          </div>
          <div className="landing-tool-card" onClick={onEnterApp}>
            <div className="landing-tool-top"><div className="landing-tool-icon">◎</div><div className="landing-tool-name">Voice</div></div>
            <div className="landing-tool-desc">Calibrate it once from real samples of your writing. Correct something later and every tool carries the fix from there.</div>
            <div className="landing-tool-bottom"><span className="landing-tool-pill">Set up once</span><span style={{ color: "#6AE4FF" }}>→</span></div>
          </div>
        </div>
      </section>

      <section id="manage" className="landing-section-ambient" style={{ maxWidth: 1200, margin: "80px auto 0", padding: "0 24px" }}>
        <div className="landing-ambient-glow" />
        <div style={{ fontFamily: "'Source Sans 3', sans-serif", letterSpacing: "0.08em", textTransform: "uppercase", fontSize: 12, color: "#6AE4FF", fontWeight: 600, marginBottom: 10 }}>Your Workspace</div>
        <h2 style={{ fontSize: 36, fontWeight: 700, marginBottom: 8 }}>Stay organized, not just fast.</h2>
        <p style={{ fontFamily: "'Source Sans 3', sans-serif", fontSize: 16, color: "#CDD0D6", marginBottom: 36, maxWidth: 560, letterSpacing: "normal" }}>
          Two places to keep every deal and every client on track.
        </p>
        <div ref={manageGridRef} className={`landing-card-grid landing-manage-grid${manageGridVisible ? " landing-reveal-visible" : ""}`}>
          <div className="landing-tool-card landing-workspace-card" onClick={onEnterApp}>
            <div className="landing-tool-top"><div className="landing-tool-icon landing-workspace-icon">▤</div><div className="landing-tool-name landing-workspace-name">Projects</div></div>
            <div className="landing-tool-desc">A stranger scans a QR code at your open house and becomes a tracked lead in your pipeline before they've left the driveway.</div>
            <div className="landing-tool-bottom"><span className="landing-tool-pill">Log in to see yours</span><span style={{ color: "#6AE4FF" }}>→</span></div>
          </div>
          <div className="landing-tool-card landing-workspace-card" onClick={onEnterApp}>
            <div className="landing-tool-top"><div className="landing-tool-icon landing-workspace-icon">◇</div><div className="landing-tool-name landing-workspace-name">Client Tracker</div></div>
            <div className="landing-tool-desc">No AI guessing why a lead went quiet. You write the real reason in your own words, right next to exactly how many days it's been.</div>
            <div className="landing-tool-bottom"><span className="landing-tool-pill">Log in to see yours</span><span style={{ color: "#6AE4FF" }}>→</span></div>
          </div>
        </div>
      </section>

      <section id="roadmap" className="landing-section-ambient" style={{ maxWidth: 1200, margin: "80px auto 0", padding: "0 24px" }}>
        <div className="landing-ambient-glow" />
        <div style={{ fontFamily: "'Source Sans 3', sans-serif", letterSpacing: "0.08em", textTransform: "uppercase", fontSize: 12, color: "#6AE4FF", fontWeight: 600, marginBottom: 10 }}>Building Next</div>
        <h2 style={{ fontSize: 36, fontWeight: 700, marginBottom: 8 }}>What's coming to your toolkit.</h2>
        <p style={{ fontFamily: "'Source Sans 3', sans-serif", fontSize: 16, color: "#CDD0D6", marginBottom: 36, maxWidth: 560, letterSpacing: "normal" }}>
          Reserved space for what's next, nothing here is built yet, this is just where it'll live.
        </p>
        <div ref={roadmapGridRef} className={`landing-card-grid${roadmapGridVisible ? " landing-reveal-visible" : ""}`}>
          <div className="landing-roadmap-tile">
            <div className="landing-roadmap-badge">Planned</div>
            <div className="landing-tool-icon" style={{ margin: "0 auto 14px" }}>⧗</div>
            <div style={{ fontSize: 15, fontWeight: 600, marginBottom: 6 }}>Calendar Sync</div>
            <div style={{ fontFamily: "'Source Sans 3', sans-serif", fontSize: 12.5, color: "#CDD0D6", lineHeight: 1.45, letterSpacing: "normal" }}>Google and Outlook first, showings and closings on your real calendar.</div>
          </div>
          <div className="landing-roadmap-tile">
            <div className="landing-roadmap-badge">Planned</div>
            <div className="landing-tool-icon" style={{ margin: "0 auto 14px" }}>▭</div>
            <div style={{ fontSize: 15, fontWeight: 600, marginBottom: 6 }}>Listing Photo Analysis</div>
            <div style={{ fontFamily: "'Source Sans 3', sans-serif", fontSize: 12.5, color: "#CDD0D6", lineHeight: 1.45, letterSpacing: "normal" }}>Upload your photos, the AI reads what's actually in them and writes from that.</div>
          </div>
          <div className="landing-roadmap-tile">
            <div className="landing-roadmap-badge">Planned</div>
            <div className="landing-tool-icon" style={{ margin: "0 auto 14px" }}>⌂</div>
            <div style={{ fontSize: 15, fontWeight: 600, marginBottom: 6 }}>Home Inspection Reader</div>
            <div style={{ fontFamily: "'Source Sans 3', sans-serif", fontSize: 12.5, color: "#CDD0D6", lineHeight: 1.45, letterSpacing: "normal" }}>Upload the report, get major defects flagged and a repair request drafted.</div>
          </div>
          <div className="landing-roadmap-tile">
            <div className="landing-roadmap-badge">Planned</div>
            <div className="landing-tool-icon" style={{ margin: "0 auto 14px" }}>❐</div>
            <div style={{ fontSize: 15, fontWeight: 600, marginBottom: 6 }}>Document Reader</div>
            <div style={{ fontFamily: "'Source Sans 3', sans-serif", fontSize: 12.5, color: "#CDD0D6", lineHeight: 1.45, letterSpacing: "normal" }}>Upload any contract or agreement, key dates and terms pulled out automatically.</div>
          </div>
          <div className="landing-roadmap-tile landing-roadmap-fill" style={{ display: "flex", alignItems: "center", justifyContent: "center", border: "1px dashed rgba(255,255,255,0.15)", background: "transparent" }}>
            <div style={{ fontFamily: "'Source Sans 3', sans-serif", fontSize: 13, color: "#CDD0D6" }}>More room here as the roadmap grows.</div>
          </div>
        </div>
      </section>

      <div ref={statsRef} style={{ marginTop: 100, padding: "0 24px" }}>
        <div style={{ maxWidth: 600, margin: "0 auto", border: "1px solid #000000", borderRadius: 15, display: "flex" }}>
          <div style={{ flex: 1, textAlign: "center", padding: "32px 24px" }}>
            <div style={{ fontSize: 56, fontWeight: 700 }}>{toolsCount}</div>
            <div style={{ fontFamily: "'Source Sans 3', sans-serif", fontSize: 14, color: "#CDD0D6" }}>Tools, One Voice</div>
          </div>
          <div style={{ width: 1, background: "#000000" }} />
          <div style={{ flex: 1, textAlign: "center", padding: "32px 24px" }}>
            <div style={{ fontSize: 56, fontWeight: 700 }}>{platformsCount}</div>
            <div style={{ fontFamily: "'Source Sans 3', sans-serif", fontSize: 14, color: "#CDD0D6" }}>Platforms Covered</div>
          </div>
        </div>
      </div>

      <footer style={{ textAlign: "center", padding: "32px 24px 60px", fontSize: 13, color: "#CDD0D6", opacity: 0.6 }}>
        The Agent's Toolkit
      </footer>
      </div>
    </div>
  );
}

function AuthGate({ onAuthenticated }) {
  const [mode, setMode] = useState("login"); // "login" | "signup"
  const [name, setName] = useState("");
  const [email, setEmail] = useState("");
  const [password, setPassword] = useState("");
  const [submitting, setSubmitting] = useState(false);
  const [error, setError] = useState(null);

  async function handleSubmit(e) {
    e.preventDefault();
    setSubmitting(true);
    setError(null);

    const url = mode === "login" ? "/api/auth/login" : "/api/auth/signup";
    const body = mode === "login" ? { email, password } : { name, email, password };

    try {
      const response = await fetch(url, {
        method: "POST",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify(body),
      });
      const data = await response.json();
      if (!response.ok) {
        throw new Error(data?.error?.message || "Something went wrong.");
      }
      onAuthenticated(data);
    } catch (err) {
      setError(err.message);
    } finally {
      setSubmitting(false);
    }
  }

  return (
    <div style={{ minHeight: "100vh", background: "#17202E", display: "flex", alignItems: "center", justifyContent: "center", fontFamily: "'Open Sans', system-ui, sans-serif", padding: 24, letterSpacing: "-0.036em" }}>
      <style>{`
        * { box-sizing: border-box; }
        .auth-input {
          font-family: 'Open Sans', sans-serif; width: 100%; padding: 13px 15px; border: 1px solid #000000;
          border-radius: 8px; background: #17202E; font-size: 14px; color: #FFFFFF; margin-bottom: 12px; letter-spacing: -0.036em;
        }
        .auth-input::placeholder { color: #CDD0D6; opacity: 0.5; }
        .auth-input:focus { outline: 2px solid #6AE4FF; outline-offset: 2px; }
        .auth-link { color: #6AE4FF; text-decoration: none; font-weight: 600; }
        .auth-link:hover { text-decoration: underline; }
      `}</style>

      <div style={{ width: "100%", maxWidth: 380 }}>
        {mode === "signup" ? (
          <>
            <div style={{ textAlign: "center", fontSize: 40, letterSpacing: "-0.03em", fontWeight: 700, color: "#6AE4FF", WebkitTextStroke: "1.5px #000000", marginBottom: 12 }}>
              The Agent's Toolkit
            </div>
            <div style={{ textAlign: "center", fontSize: 24, color: "#CDD0D6", fontWeight: 400, marginBottom: 28 }}>Get started</div>
          </>
        ) : (
          <>
            <div style={{ fontSize: 12, letterSpacing: "0.14em", textTransform: "uppercase", color: "#6AE4FF", fontWeight: 700, marginBottom: 10, textAlign: "center" }}>
              The Agent's Toolkit
            </div>
            <h1 style={{ fontWeight: 700, fontSize: 30, color: "#FFFFFF", margin: "0 0 28px", textAlign: "center" }}>
              Welcome back
            </h1>
          </>
        )}

        <form
          onSubmit={handleSubmit}
          style={{ background: "#202A3E", border: "1px solid #000000", borderRadius: 15, padding: 28 }}
        >
          {mode === "signup" && (
            <input className="auth-input" placeholder="Full name" value={name} onChange={(e) => setName(e.target.value)} required />
          )}
          <input className="auth-input" type="email" placeholder="Email" value={email} onChange={(e) => setEmail(e.target.value)} required />
          <input className="auth-input" type="password" placeholder="Password" value={password} onChange={(e) => setPassword(e.target.value)} required minLength={8} />

          <button
            type="submit"
            disabled={submitting}
            style={{
              width: "100%",
              padding: 14,
              background: submitting ? "#202A3E" : mode === "signup" ? "#6AE4FF" : "#FFFFFF",
              color: mode === "signup" ? "#17202E" : "#000000",
              border: "none",
              borderRadius: 80,
              fontSize: 14,
              fontWeight: 700,
              fontFamily: "inherit",
              letterSpacing: "-0.036em",
              cursor: submitting ? "not-allowed" : "pointer",
              marginTop: 4,
            }}
          >
            {submitting ? "..." : mode === "login" ? "Log In" : "Create Account"}
          </button>

          {error && <div style={{ fontFamily: "'Source Sans 3', sans-serif", fontSize: 13, color: "#EB5757", marginTop: 12, letterSpacing: "normal" }}>{error}</div>}
        </form>

        <div style={{ textAlign: "center", marginTop: 20, fontSize: 13, color: "#CDD0D6" }}>
          {mode === "login" ? (
            <>Don't have an account? <a href="#" onClick={(e) => { e.preventDefault(); setMode("signup"); setError(null); }} className="auth-link">Sign up</a></>
          ) : (
            <>Already have an account? <a href="#" onClick={(e) => { e.preventDefault(); setMode("login"); setError(null); }} className="auth-link">Log in</a></>
          )}
        </div>
      </div>
    </div>
  );
}

function AgentToolkit() {
  const [agent, setAgent] = useState(null);
  const [checked, setChecked] = useState(false);
  const [showLanding, setShowLanding] = useState(true);

  useEffect(() => {
    fetch("/api/auth/me")
      .then((r) => (r.ok ? r.json() : null))
      .then((data) => setAgent(data))
      .catch(() => setAgent(null))
      .finally(() => setChecked(true));
  }, []);

  function handleLogout() {
    fetch("/api/auth/logout", { method: "POST" }).finally(() => {
      setAgent(null);
      setShowLanding(true);
    });
  }

  if (!checked) {
    return <div style={{ minHeight: "100vh", background: "#17202E" }} />;
  }

  if (!agent) {
    if (showLanding) {
      return <LandingPage onEnterApp={() => setShowLanding(false)} />;
    }
    return <AuthGate onAuthenticated={setAgent} />;
  }

  return <Dashboard agent={agent} onLogout={handleLogout} />;
}

const root = ReactDOM.createRoot(document.getElementById("root"));
root.render(<AgentToolkit />);

