// Tossit — Quote Survey wizard (mobile-first)
// Standalone page at Quote.html. Each answer is stored in state and
// surfaced to the thank-you screen so a real backend / analytics layer
// can map them to a tier/price later. Pricing logic below is intentionally
// thin — replace `computeResult` with real mapping when ready.

// QUESTIONS — fetched, not hardcoded.
// site/questions.json is the single source of truth for the question set and the
// answer wording; the CRM call panel fetches the SAME file, so an answer can
// never mean two different things in the two tools. The copy below is only a
// lifeboat for a failed fetch. Edit questions.json, not this.
const QUESTIONS_FALLBACK = {
  reason: [
  { id: "reset", name: "Want to clear out and reset my space",
    desc: "Reclaim a room, garage, or entire floor. No move, no listing." },
  { id: "list", name: "Getting a house ready to list",
    desc: "Prep for photos, showings, walk-throughs." },
  { id: "moved", name: "Just moved in and have leftover stuff",
    desc: "Remove boxes, old furniture from the prior owner, packing debris." },
  { id: "estate", name: "Senior moves & estate clear-outs",
    desc: "Whole-home help for downsizing, transitions, and clearing a lifetime of belongings with care." }],

  volume: [
  { id: "q1", level: 1, name: "A pickup load", short: "A pickup load",
    desc: "A couch and a few boxes." },
  { id: "q2", level: 2, name: "Half a single garage bay", short: "Half a bay",
    desc: "A wall of boxes plus some furniture." },
  { id: "q3", level: 3, name: "Three quarters of a single garage bay", short: "¾ of a bay",
    desc: "Several solid piles or large items." },
  { id: "q4", level: 4, name: "A full single garage bay", short: "A full bay",
    desc: "You couldn't park a car in there." },
  { id: "q5", level: 4, name: "A single bay and a half", short: "A bay and a half",
    desc: "Spilling past one bay." },
  { id: "handoff", level: 4, name: "More than two bays, or several rooms", short: "Several rooms",
    desc: "We'll set the exact number at a quick walkthrough." }],

  home_size: [
  { id: "small", name: "Up to 1,800 sq ft", short: "Up to 1,800" },
  { id: "standard", name: "1,800 – 2,800 sq ft", short: "1,800 – 2,800" },
  { id: "large", name: "2,800 – 4,000 sq ft", short: "2,800 – 4,000" },
  { id: "estate", name: "Over 4,000 sq ft", short: "Over 4,000" }],

  clearout: [
  { id: "p25", pct: 25, name: "The last 25% or less", short: "25% or less",
    desc: "Most furniture and belongings are already out — just leftovers to clear." },
  { id: "p50", pct: 50, name: "About half the home", short: "About half",
    desc: "A fair amount still in place across rooms, closets, and the garage." },
  { id: "p80", pct: 80, name: "Most of the home", short: "Most of it",
    desc: "Largely furnished and full — a major declutter to get list-ready." },
  { id: "p100", pct: 100, name: "The entire home", short: "Entire home",
    desc: "Fully occupied or long-occupied — everything needs to go." }],

  fullness_estate: [
  { id: "light", short: "Light", name: "Light — it's partly furnished / already thinned out",
    desc: "A lot has already been moved or given away. There's furniture and some belongings, but it doesn't feel \"full.\"" },
  { id: "typical", short: "Typical", name: "Typical — it looks like a normal lived-in home",
    desc: "Most rooms have furniture, closets and cabinets have \"life stuff\" in them, but not wall-to-wall." },
  { id: "heavy", short: "Heavy", name: "Heavy — it feels very full or packed",
    desc: "Many rooms, closets, basement/garage are very full. It would feel overwhelming for family to clear on their own." }]

};

let Q = QUESTIONS_FALLBACK;
const dataListeners = new Set();

fetch("questions.json", { cache: "no-cache" }).
then((r) => {if (!r.ok) throw 0;return r.json();}).
then((j) => {
  const q = j && j.questions;
  if (!q) return;
  const next = {};
  Object.keys(QUESTIONS_FALLBACK).forEach((k) => {
    const opts = q[k] && q[k].options;
    next[k] = Array.isArray(opts) && opts.length ? opts : QUESTIONS_FALLBACK[k];
  });
  Q = next;
  dataListeners.forEach((f) => f());
}).
catch(() => {});

const AREA_OPTIONS = [
{ id: "garage", name: "Garage" },
{ id: "bedroom", name: "Bedroom / office" },
{ id: "living", name: "Living / dining area" },
{ id: "basement", name: "Basement / storage room" },
{ id: "whole", name: "Whole house / many rooms" },
{ id: "yard", name: "Yard / outside" }];


const ACCESS_OPTIONS = [
{ id: "easy", name: "Easy",
  desc: "Driveway, garage, or first floor." },
{ id: "normal", name: "Normal",
  desc: "Inside, up to 1 flight of stairs." },
{ id: "hard", name: "Hard",
  desc: "2+ flights, or a long walk from parking." }];


const TIMING_OPTIONS = [
{ id: "soon", name: "In the next few days" },
{ id: "week", name: "This week" },
{ id: "month", name: "This month" },
{ id: "price", name: "Just pricing for now" }];


const HEAVY_OPTIONS = [
{ id: "construction", name: "Construction / renovation debris",
  desc: "Tile, drywall, lumber, roofing." },
{ id: "concrete", name: "Concrete, dirt, rock" },
{ id: "special", name: "Piano, safe, hot tub, gym equipment" },
{ id: "appliance", name: "Appliances with refrigerant",
  desc: "Fridge, freezer, A/C." },
{ id: "demolition", name: "Demolition or tear-down required" },
{ id: "none", name: "None of these" }];


// Short phrases for the "You mentioned …" heads-up sentence.
const HEAVY_PHRASES = {
  construction: "construction debris",
  concrete: "concrete, dirt or rock",
  special: "a piano, safe, hot tub or gym equipment",
  appliance: "appliances with refrigerant",
  demolition: "demolition or tear-down work"
};

// Anything in `heavy` other than "none" means special handling.
function hasHeavy(heavy) {
  return (heavy || []).some((id) => id !== "none");
}

// Natural-language join: "a", "a and b", "a, b and c".
function joinPhrases(list) {
  if (list.length === 0) return "";
  if (list.length === 1) return list[0];
  if (list.length === 2) return list[0] + " and " + list[1];
  return list.slice(0, -1).join(", ") + " and " + list[list.length - 1];
}


// ---------------------------------------------------------------
// PRICING — read, never computed.
// The canonical table is pricing-table.json, served next to this page and
// fetched at runtime. The CRM call-quoting panel fetches the SAME file, so one
// price edit moves both. The copy below is only a lifeboat for a failed fetch.
// Never edit prices here — edit pricing-table.json.
// ---------------------------------------------------------------
const PRICING_FALLBACK = {
  version: "2026-08-25.1 (offline copy)",
  pathMinimum: { moved: 595 },
  rungs: { R1: "Quick Toss", R2: "Basic Reset", R3: "Deep Reset", R4: "Reset Project", R5: "Large Reset",
    R6: "Whole-Home Reset", R7: "Extended Clear-Out", R8: "Full Clear-Out", R9: "Maximum Clear-Out" },
  outcomes: [
  { reason: "reset", volume: "q1", rung: "R1", low: 295, high: 295, confirm: "photos" },
  { reason: "reset", volume: "q2", rung: "R2", low: 495, high: 495, confirm: "photos" },
  { reason: "reset", volume: "q3", rung: "R3", low: 795, high: 795, confirm: "photos" },
  { reason: "reset", volume: "q4", rung: "R4", low: 1095, high: 1395, confirm: "photos" },
  { reason: "reset", volume: "q5", rung: "R5", low: 1595, high: 1995, confirm: "walkthrough" },
  { reason: "moved", home_size: "small", fullness: "light", rung: "R2", low: 595, high: 595, confirm: "photos" },
  { reason: "moved", home_size: "small", fullness: "typical", rung: "R3", low: 795, high: 795, confirm: "photos" },
  { reason: "moved", home_size: "small", fullness: "heavy", rung: "R5", low: 1595, high: 1995, confirm: "walkthrough" },
  { reason: "moved", home_size: "standard", fullness: "light", rung: "R3", low: 795, high: 795, confirm: "photos" },
  { reason: "moved", home_size: "standard", fullness: "typical", rung: "R4", low: 1095, high: 1395, confirm: "photos" },
  { reason: "moved", home_size: "standard", fullness: "heavy", rung: "R5", low: 1595, high: 1995, confirm: "walkthrough" },
  { reason: "moved", home_size: "large", fullness: "light", rung: "R4", low: 1095, high: 1395, confirm: "photos" },
  { reason: "moved", home_size: "large", fullness: "typical", rung: "R5", low: 1595, high: 1995, confirm: "walkthrough" },
  { reason: "moved", home_size: "large", fullness: "heavy", rung: "R6", low: 2295, high: 2895, confirm: "walkthrough" },
  { reason: "moved", home_size: "estate", fullness: "light", rung: "R4", low: 1095, high: 1395, confirm: "photos" },
  { reason: "moved", home_size: "estate", fullness: "typical", rung: "R6", low: 2295, high: 2895, confirm: "walkthrough" },
  { reason: "moved", home_size: "estate", fullness: "heavy", rung: "R7", low: 3395, high: 4195, confirm: "walkthrough" },
  { reason: "list", home_size: "small", clearout: "p25", rung: "R3", low: 895, high: 1095, confirm: "photos" },
  { reason: "list", home_size: "small", clearout: "p50", rung: "R4", low: 1395, high: 1795, confirm: "photos" },
  { reason: "list", home_size: "small", clearout: "p80", rung: "R5", low: 1995, high: 2495, confirm: "walkthrough" },
  { reason: "list", home_size: "small", clearout: "p100", rung: "R6", low: 2895, high: 3695, confirm: "walkthrough" },
  { reason: "list", home_size: "standard", clearout: "p25", rung: "R4", low: 1395, high: 1795, confirm: "photos" },
  { reason: "list", home_size: "standard", clearout: "p50", rung: "R5", low: 1995, high: 2495, confirm: "walkthrough" },
  { reason: "list", home_size: "standard", clearout: "p80", rung: "R6", low: 2895, high: 3695, confirm: "walkthrough" },
  { reason: "list", home_size: "standard", clearout: "p100", rung: "R7", low: 4295, high: 5295, confirm: "walkthrough" },
  { reason: "list", home_size: "large", clearout: "p25", rung: "R5", low: 1995, high: 2495, confirm: "walkthrough" },
  { reason: "list", home_size: "large", clearout: "p50", rung: "R6", low: 2895, high: 3695, confirm: "walkthrough" },
  { reason: "list", home_size: "large", clearout: "p80", rung: "R7", low: 4295, high: 5295, confirm: "walkthrough" },
  { reason: "list", home_size: "large", clearout: "p100", rung: "R8", low: 5995, high: 7295, confirm: "walkthrough" },
  { reason: "list", home_size: "estate", clearout: "p25", rung: "R5", low: 1995, high: 2495, confirm: "walkthrough" },
  { reason: "list", home_size: "estate", clearout: "p50", rung: "R7", low: 4295, high: 5295, confirm: "walkthrough" },
  { reason: "list", home_size: "estate", clearout: "p80", rung: "R8", low: 5995, high: 7295, confirm: "walkthrough" },
  { reason: "list", home_size: "estate", clearout: "p100", rung: "R9", low: 8795, high: null, confirm: "walkthrough" },
  { reason: "estate", home_size: "small", fullness: "light", rung: "R4", low: 2195, high: 2795, confirm: "walkthrough" },
  { reason: "estate", home_size: "small", fullness: "typical", rung: "R5", low: 3195, high: 3995, confirm: "walkthrough" },
  { reason: "estate", home_size: "small", fullness: "heavy", rung: "R6", low: 4595, high: 5795, confirm: "walkthrough" },
  { reason: "estate", home_size: "standard", fullness: "light", rung: "R5", low: 3195, high: 3995, confirm: "walkthrough" },
  { reason: "estate", home_size: "standard", fullness: "typical", rung: "R6", low: 4595, high: 5795, confirm: "walkthrough" },
  { reason: "estate", home_size: "standard", fullness: "heavy", rung: "R7", low: 6795, high: 8395, confirm: "walkthrough" },
  { reason: "estate", home_size: "large", fullness: "light", rung: "R6", low: 4595, high: 5795, confirm: "walkthrough" },
  { reason: "estate", home_size: "large", fullness: "typical", rung: "R7", low: 6795, high: 8395, confirm: "walkthrough" },
  { reason: "estate", home_size: "large", fullness: "heavy", rung: "R8", low: 9595, high: 11595, confirm: "walkthrough" },
  { reason: "estate", home_size: "estate", fullness: "light", rung: "R6", low: 4595, high: 5795, confirm: "walkthrough" },
  { reason: "estate", home_size: "estate", fullness: "typical", rung: "R8", low: 9595, high: 11595, confirm: "walkthrough" },
  { reason: "estate", home_size: "estate", fullness: "heavy", rung: "R9", low: 13995, high: null, confirm: "walkthrough" }]

};

let PRICING = PRICING_FALLBACK;
const pricingListeners = new Set();

fetch("pricing-table.json", { cache: "no-cache" }).
then((r) => {if (!r.ok) throw 0;return r.json();}).
then((t) => {
  if (!t || !Array.isArray(t.outcomes) || !t.outcomes.length) return;
  const rungs = {};
  (t.rungs || []).forEach((r) => {rungs[r.id] = r.name;});
  PRICING = {
    version: t.version,
    pathMinimum: (t.derivation || {}).path_minimum || PRICING_FALLBACK.pathMinimum,
    rungs: Object.keys(rungs).length ? rungs : PRICING_FALLBACK.rungs,
    outcomes: t.outcomes
  };
  pricingListeners.forEach((f) => f());
}).
catch(() => {});

// Re-renders the estimator once the live question set lands.
function useQuestions() {
  const [, bump] = React.useReducer((x) => x + 1, 0);
  React.useEffect(() => {
    dataListeners.add(bump);
    return () => dataListeners.delete(bump);
  }, []);
  return Q;
}

// Re-renders the estimator once the live table lands.
function usePricingTable() {
  const [, bump] = React.useReducer((x) => x + 1, 0);
  React.useEffect(() => {
    pricingListeners.add(bump);
    return () => pricingListeners.delete(bump);
  }, []);
  return PRICING;
}

// Saved sessions from before the table used p75 for the listing "most of it" answer.
function normalizeAnswers(a) {
  return { ...a, clearout: a.clearout === "p75" ? "p80" : a.clearout };
}

// One lookup, four paths. Nothing here multiplies or rounds.
//
// Reset and move-in each have their OWN volume rows in the table: a move-in
// prices one rung above a reset at the same stated volume, because a move-in
// carries packing debris and unwanted furniture, not one tidy pile.
//
// The top answer on the ladder — "more than two bays" — has an open-ended row on
// both volume paths: a starting number with no ceiling ($2,295+ on a reset,
// $3,395+ on a move-in), so nobody leaves without a figure. Both always route to
// a walkthrough, where the do-not-exceed cap gets set. Home size is collected on
// the move-in path for the record; it never prices.
function lookupOutcome(answers) {
  const a = normalizeAnswers(answers);
  const q =
  a.reason === "reset" || a.reason === "moved" ? { reason: a.reason, volume: a.volume } :
  a.reason === "list" ? { reason: "list", home_size: a.homeSize, clearout: a.clearout } :
  { reason: "estate", home_size: a.homeSize, fullness: a.fullness };
  if (Object.values(q).some((v) => v == null)) return null;
  const o = PRICING.outcomes.find((x) => Object.keys(q).every((k) => x[k] === q[k]));
  if (!o) return null;

  // The table declares a floor for the move-in path.
  const floor = ((PRICING.pathMinimum || {}).moved) || 0;
  if (a.reason === "moved" && floor && o.low < floor) {
    return { ...o, low: floor, high: o.high == null ? null : Math.max(o.high, floor) };
  }
  return o;
}

const PATH_COPY = {
  reset: { priceLabel: "Normal household items",
    lead: "Resets are priced by how much is going." },
  moved: { priceLabel: "New Home Reset",
    lead: "New home resets are priced by how much is going, all of it together: packing materials, furniture you don't want, and whatever was left behind." },
  list: { priceLabel: "Listing prep",
    lead: "Listing prep is priced per home — the size, plus how much still needs to go." },
  estate: { priceLabel: "Estate clear-out",
    lead: "Estate and downsizing work is priced per home, with care." }
};

// "a Deep Reset" vs "an Extended Clear-Out".
function article(name) {
  return /^[aeiou]/i.test(name) ? "an" : "a";
}

// Thousands separators. The table is the source of the numbers; this only formats.
function fmtNum(n) {
  return n == null ? "" : Number(n).toLocaleString("en-US");
}

// ---------------------------------------------------------------
// Result mapping — a lookup against pricing-table.json. No arithmetic.
// ---------------------------------------------------------------
function computeResult(answers) {
  const o = lookupOutcome(answers);
  if (!o) {
    return {
      tier: "Custom project",
      tierEm: "Custom project",
      needsWalkthrough: true,
      priceLabel: "Custom project",
      headline: "Let's set this one in person.",
      body: [
      "Projects like this get one price at a quick walkthrough, with a do‑not‑exceed cap for the whole job.",
      "The final bill will not go over that number."]

    };
  }
  const name = PRICING.rungs[o.rung] || "Reset";
  const walk = o.confirm === "walkthrough";
  const flat = o.high != null && o.low === o.high;
  const copy = PATH_COPY[answers.reason] || PATH_COPY.reset;
  const body = walk ?
  [copy.lead + " This is a ballpark — we set your exact do‑not‑exceed cap at a quick walkthrough.",
  "Whatever we agree to, the final price won't go past that cap."] :
  flat ?
  ["This is an estimate based on what you told us.",
  "Send a few photos and we'll lock in a flat price that won't change in the driveway."] :
  [copy.lead + " Send a few photos and we'll confirm your exact flat price inside this range.",
  "Once it's set, the price won't change as long as it matches what we see on site."];

  return {
    tier: name,
    tierEm: name,
    rung: o.rung,
    estimate: flat ? o.low : undefined,
    rangeLow: flat ? undefined : o.low,
    rangeHigh: flat || o.high == null ? undefined : o.high,
    rangeOpen: !flat && o.high == null,
    needsWalkthrough: walk,
    priceLabel: copy.priceLabel,
    headline: (flat ? "Most jobs like this are " : "This is ") + article(name) + " " + name + ".",
    body: body
  };
}
window.computeResult = computeResult;

// Format a phone number as ###-###-#### while typing.
function formatPhone(value) {
  const d = (value || "").replace(/\D/g, "").slice(0, 10);
  if (d.length <= 3) return d;
  if (d.length <= 6) return d.slice(0, 3) + "-" + d.slice(3);
  return d.slice(0, 3) + "-" + d.slice(3, 6) + "-" + d.slice(6);
}

// ---------------------------------------------------------------
// UI primitives
// ---------------------------------------------------------------
function ProgressBar({ step, total }) {
  return (
    <div className="qs__progress-segs" role="progressbar"
    aria-valuenow={step + 1} aria-valuemin={1} aria-valuemax={total}>
      {Array.from({ length: total }).map((_, i) =>
      <span key={i}
      className={"qs__progress-seg" + (
      i < step ? " is-done" : i === step ? " is-active" : "")} />
      )}
    </div>);

}

function OptionRow({ option, selected, onClick, variant = "radio" }) {
  return (
    <button type="button"
    className={"qs__opt" + (variant === "check" ? " qs__opt--check" : "") + (selected ? " is-on" : "")}
    onClick={onClick}
    aria-pressed={selected}>
      <span className="qs__opt-dot" aria-hidden />
      <span className="qs__opt-body">
        <span className="qs__opt-name">
          {option.name}
          {option.badge &&
          <span style={{ display: "inline-block", marginLeft: 8, padding: "3px 8px", borderRadius: 999, background: "var(--qs-ink)", color: "var(--qs-on-ink)", font: "700 10px/1 var(--font-sans)", letterSpacing: "0.1em", textTransform: "uppercase", verticalAlign: "middle", whiteSpace: "nowrap", position: "relative", top: -1 }}>{option.badge}</span>}
        </span>
        {option.desc && <span className="qs__opt-desc">{option.desc}</span>}
      </span>
    </button>);

}

function VolumeRow({ option, selected, onClick }) {
  return (
    <button type="button"
    className={"qs__vol" + (selected ? " is-on" : "")}
    onClick={onClick}
    aria-pressed={selected}>
      <span className="qs__vol-viz" data-level={option.level} aria-hidden>
        <span className={option.level >= 1 ? "on" : ""} />
        <span className={option.level >= 2 ? "on" : ""} />
        <span className={option.level >= 3 ? "on" : ""} />
        <span className={option.level >= 4 ? "on" : ""} />
      </span>
      <span className="qs__vol-body">
        <span className="qs__vol-name">{option.name}</span>
        <span className="qs__vol-desc">{option.desc}</span>
      </span>
      <span className="qs__vol-check" aria-hidden />
    </button>);

}

// ---------------------------------------------------------------
// Step screens
// ---------------------------------------------------------------
function StepReason({ value, onChange }) {
  return (
    <>
      <div className="qs__head">
        <h1 className="qs__title">Tell us what you are up <em>to.</em></h1>
        <p className="qs__sub">One quick question, then we get specific.</p>
      </div>
      <div className="qs__options">
        {Q.reason.map((o) =>
        <OptionRow key={o.id} option={o}
        selected={value === o.id}
        onClick={() => onChange(o.id)} />
        )}
      </div>
    </>);

}

// Home size — asked on every path and stored on the lead. It only sets the price
// when the volume answer is the hand-off; otherwise it is for the record.
function HomeSizeBlock({ homeSize, onHomeSizeChange }) {
  return (
    <>
      <div className="qs__qhead">
        <span className="qs__qlabel">How big is the home?</span>
      </div>
      <div className="qs__options">
        {Q.home_size.map((o) =>
        <OptionRow key={o.id} option={o}
        selected={homeSize === o.id}
        onClick={() => onHomeSizeChange(o.id)} />
        )}
      </div>
    </>);

}

function FullnessBlock({ fullness, options, question, onFullnessChange }) {
  return (
    <>
      <div className="qs__qhead">
        <span className="qs__qlabel">{question}</span>
      </div>
      <div className="qs__options">
        {options.map((o) =>
        <OptionRow key={o.id} option={o}
        selected={fullness === o.id}
        onClick={() => onFullnessChange(o.id)} />
        )}
      </div>
    </>);

}

function StepScope({ volume, onVolumeChange }) {
  return (
    <>
      <div className="qs__head">
        <h1 className="qs__title">How much is <em>going?</em></h1>
        <p className="qs__sub">If you pictured it all stacked in a garage, how much space would it take?</p>
      </div>

      <div className="qs__volume">
        {Q.volume.map((o) => <VolumeRow key={o.id} option={o}
        selected={volume === o.id}
        onClick={() => onVolumeChange(o.id)} />
        )}
      </div>

    </>);

}

// The move-in path asks two things, one screen each: the home, then the pile.
// Home size is for the record — it never selects a price.
function StepMovedSize({ homeSize, onHomeSizeChange }) {
  return (
    <>
      <div className="qs__head">
        <h1 className="qs__title">First — how big is <em>your new place?</em></h1>
        <p className="qs__sub">A rough number is fine. This helps us plan the crew and the truck — it doesn't change your price.</p>
      </div>

      <div className="qs__options">
        {Q.home_size.map((o) =>
        <OptionRow key={o.id} option={o}
        selected={homeSize === o.id}
        onClick={() => onHomeSizeChange(o.id)} />
        )}
      </div>
    </>);

}

function StepMovedVolume({ volume, onVolumeChange }) {
  return (
    <>
      <div className="qs__head">
        <h1 className="qs__title">And how much is <em>going?</em></h1>
        <p className="qs__sub">Packing materials, furniture you don't want, whatever the previous owner left — all of it together. If you pictured it stacked in a garage, how much space would it take?</p>
      </div>

      <div className="qs__volume">
        {Q.volume.map((o) => <VolumeRow key={o.id} option={o}
        selected={volume === o.id}
        onClick={() => onVolumeChange(o.id)} />
        )}
      </div>
    </>);

}

function StepListScope({ homeSize, clearout, onHomeSizeChange, onClearoutChange }) {
  return (
    <>
      <div className="qs__head">
        <h1 className="qs__title">Getting it ready <em>to list?</em></h1>
        <p className="qs__sub">Two quick questions and we'll show you a price.</p>
      </div>

      <div className="qs__qhead">
        <span className="qs__qlabel">How big is the home?</span>
      </div>
      <div className="qs__options">
        {Q.home_size.map((o) =>
        <OptionRow key={o.id} option={o}
        selected={homeSize === o.id}
        onClick={() => onHomeSizeChange(o.id)} />
        )}
      </div>

      <div className="qs__qhead">
        <span className="qs__qlabel">How much of the home's contents need to go?</span>
      </div>
      <div className="qs__options">
        {Q.clearout.map((o) =>
        <OptionRow key={o.id} option={o}
        selected={clearout === o.id}
        onClick={() => onClearoutChange(o.id)} />
        )}
      </div>
    </>);

}

function StepEstateScope({ homeSize, fullness, onHomeSizeChange, onFullnessChange }) {
  return (
    <>
      <div className="qs__head">
        <h1 className="qs__title">A little about <em>the home.</em></h1>
        <p className="qs__sub">Two quick questions, then we'll show you a ballpark.</p>
      </div>

      <div className="qs__qhead">
        <span className="qs__qlabel">How big is the home?</span>
      </div>
      <div className="qs__options">
        {Q.home_size.map((o) =>
        <OptionRow key={o.id} option={o}
        selected={homeSize === o.id}
        onClick={() => onHomeSizeChange(o.id)} />
        )}
      </div>

      <div className="qs__qhead">
        <span className="qs__qlabel">How much is left in the home right now?</span>
      </div>
      <p className="qs__sub" style={{ textAlign: "left", margin: "0 0 12px" }}>This just helps us plan the right crew and number of days — no judgment.</p>
      <div className="qs__options">
        {Q.fullness_estate.map((o) =>
        <OptionRow key={o.id} option={o}
        selected={fullness === o.id}
        onClick={() => onFullnessChange(o.id)} />
        )}
      </div>
    </>);

}

function StepHeavy({ heavy, onHeavyChange }) {
  const selected = heavy || [];
  const toggle = (id) => {
    if (id === "none") {
      onHeavyChange(selected.includes("none") ? [] : ["none"]);
      return;
    }
    const next = selected.filter((x) => x !== "none");
    onHeavyChange(next.includes(id) ? next.filter((x) => x !== id) : [...next, id]);
  };
  return (
    <>
      <div className="qs__head">
        <h1 className="qs__title">Any heavy or special items <em>in the mix?</em></h1>
        <p className="qs__sub">This is the only thing that can change your price from the instant estimate.</p>
      </div>
      <div className="qs__options">
        {HEAVY_OPTIONS.map((o) =>
        <OptionRow key={o.id} option={o} variant="check"
        selected={selected.includes(o.id)}
        onClick={() => toggle(o.id)} />
        )}
      </div>
    </>);

}

function StepAccess({ access, timing, onAccessChange, onTimingChange }) {
  return (
    <>
      <div className="qs__head">
        <h1 className="qs__title">Getting in, getting <em>it done.</em></h1>
        <p className="qs__sub">Two more taps and we'll show you a price.</p>
      </div>

      <div className="qs__qhead">
        <span className="qs__qlabel">Access to the items</span>
      </div>
      <div className="qs__options">
        {ACCESS_OPTIONS.map((o) =>
        <OptionRow key={o.id} option={o}
        selected={access === o.id}
        onClick={() => onAccessChange(o.id)} />
        )}
      </div>

      <div className="qs__qhead">
        <span className="qs__qlabel">When would you like this done?</span>
      </div>
      <div className="qs__options">
        {TIMING_OPTIONS.map((o) =>
        <OptionRow key={o.id} option={o}
        selected={timing === o.id}
        onClick={() => onTimingChange(o.id)} />
        )}
      </div>
    </>);

}

function StepEstimate({ answers, onGetGuarantee, onBallpark }) {
  const result = computeResult(answers);

  const heavyPicked = hasHeavy(answers.heavy);
  const heavyText = joinPhrases(
    (answers.heavy || []).filter((id) => id !== "none").map((id) => HEAVY_PHRASES[id] || id)
  );
  const priceText = result.estimate ? `$${fmtNum(result.estimate)}` :
  result.rangeLow ? `$${fmtNum(result.rangeLow)}+` : "your estimate";
  // The number we promise not to exceed — used in the "what happens next" steps
  // and under the CTA, so the cap is stated in the customer's own dollars.
  const capText = result.estimate ? `$${fmtNum(result.estimate)}` :
  result.rangeLow && result.rangeHigh ? `$${fmtNum(result.rangeLow)}–$${fmtNum(result.rangeHigh)}` :
  result.rangeLow ? `$${fmtNum(result.rangeLow)}+` : "your quoted price";

  // Pretty labels for the detail rows in the ticket stub.
  const reasonLabel = (Q.reason.find((o) => o.id === answers.reason) || {}).name || "—";
  const volumeLabel = (Q.volume.find((o) => o.id === answers.volume) || {}).short || "—";
  const homeSizeLabel = (Q.home_size.find((o) => o.id === answers.homeSize) || {}).short || "—";
  const byVolume = !!answers.volume;
  const sizeLabel = byVolume || answers.reason === "reset" ? volumeLabel :
  answers.homeSize ? homeSizeLabel : volumeLabel;
  const clearoutLabel = (Q.clearout.find((o) => o.id === normalizeAnswers(answers).clearout) || {}).short || "—";
  const fullnessList = Q.fullness_estate;
  const fullnessLabel = (fullnessList.find((o) => o.id === answers.fullness) || {}).short || "—";
  const fullnessKey = answers.reason === "moved" ? "Left behind" : "In home";
  const accessLabel = (ACCESS_OPTIONS.find((o) => o.id === answers.access) || {}).name || "—";

  return (
    <>
      <div className="qs__head">
        <h1 className="qs__title qs__title--sm">Based on what you <em>told us…</em></h1>
        <p className="qs__sub">Here's our instant estimate. Lock it in below.</p>
      </div>

      {/* Ticket — the moment. Hero price on a blue stub, perforated divider, details below. */}
      <div className="qs__ticket" role="group" aria-label="Your quote">
        <div className="qs__ticket-top">
          <div className="qs__ticket-bg" aria-hidden />
          <div className="qs__ticket-header">
            <span className="qs__ticket-brand">
              <span className="qs__ticket-dot" aria-hidden />
              Tossit Quote
            </span>
            <span className="qs__ticket-ref">Estimate</span>
          </div>

          {result.estimate ?
          <div className="qs__ticket-hero">
              <span className="qs__ticket-eyebrow">Estimated project cost</span>
              <div className="qs__ticket-amount">
                <span className="qs__ticket-currency">$</span>
                <span className="qs__ticket-num">{fmtNum(result.estimate)}</span>
              </div>
              <div className="qs__ticket-max">
                <span className="qs__ticket-max-meta">Project Estimate</span>
              </div>
            </div> :
          result.rangeLow ?
          <div className="qs__ticket-hero">
              <span className="qs__ticket-eyebrow">{result.needsWalkthrough ? "Estimated range" : result.rangeHigh ? "Estimated project cost" : "Starts at"}</span>
              <div className={"qs__ticket-amount" + (result.rangeHigh ? " qs__ticket-amount--range" : "")}>
                <span className="qs__ticket-currency">$</span>
                <span className="qs__ticket-num">{result.rangeHigh ? `${fmtNum(result.rangeLow)} – ${fmtNum(result.rangeHigh)}` : `${fmtNum(result.rangeLow)}+`}</span>
              </div>
              <div className="qs__ticket-max">
                <span className="qs__ticket-max-meta">{result.needsWalkthrough ? "Do‑not‑exceed cap set at walkthrough" : result.rangeHigh ? "Typical range" : "Final One‑Price set by photos"}</span>
              </div>
            </div> :

          <div className="qs__ticket-hero">
              <span className="qs__ticket-eyebrow">Estimated project cost</span>
              <div className="qs__ticket-amount qs__ticket-amount--word">
                <span className="qs__ticket-num">Custom</span>
              </div>
              <div className="qs__ticket-rule" aria-hidden>
                <span className="qs__ticket-rule-line" />
                <span className="qs__ticket-rule-tick">After walkthrough</span>
                <span className="qs__ticket-rule-line" />
              </div>
              <div className="qs__ticket-max">
                <span className="qs__ticket-max-meta">
                  We'll send your exact number after a quick walkthrough.
                </span>
              </div>
            </div>
          }
        </div>

        <div className="qs__ticket-perf" aria-hidden>
          <span className="qs__ticket-notch qs__ticket-notch--l" />
          <span className="qs__ticket-notch qs__ticket-notch--r" />
        </div>

        <div className="qs__ticket-bottom">
          <div className="qs__ticket-tier">{result.tier}</div>
          <div className="qs__ticket-grid">
            <div className="qs__ticket-cell">
              <span className="qs__ticket-k">Reason</span>
              <span className="qs__ticket-v">{reasonLabel}</span>
            </div>
            <div className="qs__ticket-cell">
              <span className="qs__ticket-k">{byVolume ? "Volume" : "Size"}</span>
              <span className="qs__ticket-v">{sizeLabel}</span>
            </div>
            {answers.reason === "moved" && !!answers.homeSize &&
            <div className="qs__ticket-cell">
              <span className="qs__ticket-k">Home</span>
              <span className="qs__ticket-v">{homeSizeLabel}</span>
            </div>}
            <div className="qs__ticket-cell">
              <span className="qs__ticket-k">Access</span>
              <span className="qs__ticket-v">{accessLabel}</span>
            </div>
            {answers.reason === "list" &&
            <div className="qs__ticket-cell">
              <span className="qs__ticket-k">To clear</span>
              <span className="qs__ticket-v">{clearoutLabel}</span>
            </div>}
            {!!answers.fullness && !byVolume &&
            <div className="qs__ticket-cell">
              <span className="qs__ticket-k">{fullnessKey}</span>
              <span className="qs__ticket-v">{fullnessLabel}</span>
            </div>}
          </div>
          {result.body && result.body[0] &&
          <p className="qs__ticket-fine">{result.body[0]}</p>
          }
        </div>
      </div>

      <p className="qs__fine" style={{ opacity: 0.45, fontSize: 11, marginTop: 6 }}>Pricing table {PRICING.version}</p>

      {/* Ballpark sentence + guarantee CTA */}
      {heavyPicked ?
      <div className="qs__heads">
        <h3 className="qs__heads-title">
          <svg viewBox="0 0 24 24" fill="none" aria-hidden>
            <path d="M12 9v4M12 17h.01M10.3 3.86 1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.7 3.86a2 2 0 0 0-3.4 0Z"
            stroke="currentColor" strokeWidth="1.6" strokeLinecap="round" strokeLinejoin="round" />
          </svg>
          Heads up on heavy/special items
        </h3>
        <p>
          You mentioned <strong>{heavyText}</strong>. Your instant estimate of <strong>{priceText}</strong> covers
          the normal household junk. Heavy/special items are quoted separately from photos so we can get you an
          honest, flat number and avoid any driveway surprises.
        </p>
        {!result.needsWalkthrough &&
        <p>Send us a few quick photos on the next step and we'll text you one flat quote for everything.</p>}
      </div> :
      result.estimate &&
      <p className="qs__estimate-line">
        Jobs like this are considered a <strong>{result.tier}</strong> at <strong>${fmtNum(result.estimate)}</strong>. To confirm and get you a guaranteed price, you can lock it in below.
      </p>
      }
      {result.needsWalkthrough ?
      <>
        <p className="qs__estimate-line">
          {result.rangeHigh && <>Projects this size usually land between <strong>${fmtNum(result.rangeLow)}–${fmtNum(result.rangeHigh)}</strong>. </>}
          We don't guess on jobs this big, because that's how driveway haggling starts – and we hate that as much as you.
        </p>
        <p className="qs__fine">
          Instead, we do a quick walkthrough and give you one project price with a do‑not‑exceed cap for the whole job. The final bill will not go over that number.
        </p>
      </> :
      result.rangeLow &&
      <>
        <p className="qs__estimate-line">
          {result.rangeHigh ?
          <>Projects this size usually run <strong>${fmtNum(result.rangeLow)}–${fmtNum(result.rangeHigh)}</strong>.</> :
          <>Jobs this size start at <strong>${fmtNum(result.rangeLow)}</strong>. We set the exact number — and the cap it won't go past — at the walkthrough.</>}
        </p>
        <p className="qs__fine">
          We don't guess on jobs this size because that's how driveway haggling starts — and we hate that as much as you.
        </p>
        <p className="qs__fine" style={{ marginTop: 10 }}>
          {result.caveat ? result.caveat :
          <>Send us a few quick photos and we'll send you one One‑Price that won't change as long as the photos match what we see on site.</>}
        </p>
      </>
      }

      {result.needsWalkthrough ?
      <div className="qs__guarantee-block">
        <h2 className="qs__subhead">Get your exact price at a <em>walkthrough</em></h2>
        <p className="qs__subhead-note">
          <strong>Scheduling a quick walkthrough doesn't obligate you to book.</strong> We look at the home, then send one project price with a do‑not‑exceed cap — the final price won't go past it.
        </p>
        <ul className="qs__guarantee">
          <li>A quick, no‑pressure <strong>15–20 minute</strong> walkthrough — in person or by video.</li>
          <li>You get <strong>one project price with a hard <i>do not exceed cap</i></strong> in writing.</li>
          <li>No driveway haggling, no "actually it's double," ever.</li>
        </ul>
        <p className="qs__fine" style={{ marginTop: 16 }}>Drop your details on the next step and we'll text you to line up a 15–20 minute slot that works for you.</p>
      </div> :
      <div className="qs__guarantee-block">
        <h2 className="qs__subhead">Lock in your <em>One‑Price Guarantee</em></h2>
        <ul className="qs__guarantee">
          <li><strong>One flat price in writing <span style={{ fontWeight: "normal" }}>with</span> do not exceed cap</strong> <i><u>before</u></i> we schedule.</li>
          <li>If the photos match, the price <strong>will not change</strong> when we arrive.</li>
          <li>No driveway haggling, no "actually it's double" — ever.</li>
        </ul>
        <div className="qs__nextsteps">
          <p className="qs__nextsteps-h">Lock in your price by</p>
          <ol className="qs__steps">
            <li>Enter your info on the next screen.</li>
            <li>We text you asking for a few photos.</li>
            <li>We confirm your price and cap the project cost.</li>
          </ol>
        </div>
      </div>
      }

      <div className="qs__cta-stack">
        <button type="button" className="qs__btn" onClick={onGetGuarantee}>
          {result.needsWalkthrough ? "Schedule my walkthrough" : "Lock in my One‑Price Guarantee"}
        </button>
      </div>

      <p className="qs__cta-reassure">
        {result.needsWalkthrough ?
        <>Doesn't obligate you to book — it just gets you a firm price with a hard cap.</> :
        <>Doesn't obligate you to book — we just confirm and cap your project cost.</>}
      </p>

      <p className="qs__fine" style={{ marginTop: 16, maxWidth: "75%", marginLeft: "auto", marginRight: "auto", textAlign: "center", fontSize: 13, fontWeight: 600, color: "var(--qs-ink-2)" }}>
        {result.needsWalkthrough ?
        <>Whether you're home or not right now, we'll send a quick text so you can schedule a walkthrough whenever it's convenient.</> :
        <>Whether you're home or not right now, we'll send a quick text you can reply to with photos whenever it's convenient.</>}
      </p>

      <div className="qs__cta-stack" style={{ marginTop: 8 }}>
        <button type="button" className="qs__textlink" onClick={onBallpark}>
          I just wanted a ballpark number
        </button>
      </div>
    </>);

}

function StepCapture({ contact, errors, needsWalkthrough, onContactChange, onSubmit }) {
  const set = (k) => (e) => onContactChange({ ...contact, [k]: e.target.value });
  return (
    <>
      <div className="qs__head">
        <h1 className="qs__title">{needsWalkthrough ? <>Let's set up your <em>walkthrough.</em></> : <>Who should we <em>reach out to?</em></>}</h1>
        <p className="qs__sub">{needsWalkthrough ?
          <>Drop your info and we'll text you a few times that work — pick one and you're set.</> :
          <>Drop your info and we'll text you — just reply with a few photos when it's convenient and you're set.</>}


        </p>
      </div>

      <p className="qs__fine"></p>

      <div className="qs__form">
        <div className="qs__field">
          <label className="qs__flabel" htmlFor="qs-name">First name</label>
          <input id="qs-name" className={"qs__input" + (errors.name ? " is-err" : "")}
          type="text" autoComplete="given-name" placeholder="First name"
          value={contact.name} onChange={set("name")} />
          {errors.name ?
          <span className="qs__ferr">{errors.name}</span> :
          <span className="qs__fhelp">{needsWalkthrough ? "So we know who we're meeting." : "So we know who to text."}</span>
          }
        </div>
        <div className="qs__field">
          <label className="qs__flabel" htmlFor="qs-phone">Mobile number</label>
          <input id="qs-phone" className={"qs__input" + (errors.phone ? " is-err" : "")}
          type="tel" inputMode="tel" placeholder="801-555-1234"
          value={contact.phone} onChange={(e) => onContactChange({ ...contact, phone: formatPhone(e.target.value) })} />
          {errors.phone ?
          <span className="qs__ferr">{errors.phone}</span> :
          <span className="qs__fhelp">{needsWalkthrough ? "We'll text you here to find a walkthrough time — never sold, never spammed." : "We'll text you here so you can reply with photos — never sold, never spammed."}</span>
          }
        </div>
        <div className="qs__field">
          <label className="qs__flabel" htmlFor="qs-zip">Zip code</label>
          <input id="qs-zip" className={"qs__input" + (errors.zip ? " is-err" : "")}
          type="text" inputMode="numeric" autoComplete="postal-code" maxLength={10} placeholder="e.g. 84101"
          value={contact.zip} onChange={set("zip")} />
          {errors.zip ?
          <span className="qs__ferr">{errors.zip}</span> :
          <span className="qs__fhelp">To make sure you're in our service area.</span>
          }
        </div>
        <div className="qs__field">
          <label className="qs__flabel" htmlFor="qs-email">
            Email <span className="qs__flabel-opt">{needsWalkthrough ? "for your walkthrough details" : "for your formal quote"}</span>
          </label>
          <input id="qs-email" className={"qs__input" + (errors.email ? " is-err" : "")}
          type="email" inputMode="email" placeholder="you@example.com"
          value={contact.email} onChange={set("email")} />
          {errors.email &&
          <span className="qs__ferr">{errors.email}</span>
          }
        </div>
        <div className="qs__field">
          <label className="qs__flabel" htmlFor="qs-hear">
            How did you first hear about TOSSIT?
          </label>
          <select id="qs-hear" className={"qs__input" + (errors.hear ? " is-err" : "")}
          value={contact.hear || ""}
          onChange={(e) => {
            const v = e.target.value;
            onContactChange({ ...contact, hear: v, ...(v !== "realtor" ? { realtor: "" } : {}) });
          }}>
            <option value="" disabled>Select one…</option>
            <option value="google">Google search</option>
            <option value="social">Facebook / Instagram</option>
            <option value="nextdoor">Nextdoor</option>
            <option value="craigslist">Craigslist</option>
            <option value="van">Saw your van</option>
            <option value="realtor">From my realtor</option>
            <option value="referral">From a friend or family member</option>
            <option value="other">Other</option>
          </select>
          {errors.hear ?
          <span className="qs__ferr">{errors.hear}</span> :
          <span className="qs__fhelp">Helps us know what's working.</span>
          }
        </div>
        {contact.hear === "realtor" &&
        <div className="qs__field">
          <label className="qs__flabel" htmlFor="qs-realtor">
            Which realtor? <span className="qs__flabel-opt">optional</span>
          </label>
          <input id="qs-realtor" className="qs__input"
          type="text" placeholder="Realtor's name"
          value={contact.realtor || ""} onChange={set("realtor")} />
          <span className="qs__fhelp">So we can thank them for sending you our way.</span>
        </div>
        }
      </div>

      <b style={{ display: "block", fontSize: 12, marginTop: 20, maxWidth: "80%", marginLeft: "auto", marginRight: "auto", textAlign: "center" }}>{needsWalkthrough ?
      <>We'll text you to find a time that works, then walk the home with you — no pressure, and scheduling doesn't obligate you to book. You'll get one project price with a do‑not‑exceed cap. <u>The final price won't go past it.</u></> :
      <>We'll text you so you can reply with a few photos, then we'll send back your locked‑in One‑Price quote. If we ever mis‑judge a job from your photos, that's on us — not you. <u>The price stands.</u></>}</b>

      {Object.keys(errors).length > 0 &&
      <div className="qs__errsum" role="alert">
        <strong>Please finish a few things before submitting:</strong>
        <ul>
          {["name", "phone", "zip", "email", "hear"].filter((k) => errors[k]).map((k) =>
          <li key={k}>{{ name: "First name", phone: "Mobile number", zip: "Zip code", email: "Email", hear: "How you heard about us" }[k]} — {errors[k]}</li>
          )}
        </ul>
      </div>
      }

      <div className="qs__cta-stack">
        <button type="button" className="qs__btn" onClick={onSubmit}>{needsWalkthrough ? "Text me to schedule my walkthrough" : "Text me my One‑Price quote"}

        </button>
      </div>

      <p className="qs__consent-text" style={{ fontSize: 12, marginTop: 14, textAlign: "center", color: "var(--qs-ink-3)" }}>
        By providing your mobile number, you agree to receive text messages from Tossit about your quote, scheduling, and your request. Providing a number is optional and is not a condition of purchase. Message frequency varies. Msg &amp; data rates may apply. Reply STOP to opt out, HELP for help.
        <span className="qs__consent-note" style={{ display: "block", marginTop: 4 }}>See our <a href="privacy.html" target="_blank" rel="noopener">Privacy Policy</a> and <a href="terms.html" target="_blank" rel="noopener">Messaging Terms</a>.</span>
      </p>
    </>);

}

// ---------------------------------------------------------------
// Unique quote reference — stamped once at lock-in.
// Timestamp base (collision-proof over time) + random tail (collision-proof
// within the same instant). Ambiguous characters (0/O, 1/I) omitted so it's
// easy to read back over the phone.
// ---------------------------------------------------------------
function makeQuoteRef() {
  const chars = "ABCDEFGHJKMNPQRSTUVWXYZ23456789";
  const rand = (n) => Array.from({ length: n }, () => chars[Math.floor(Math.random() * chars.length)]).join("");
  const stamp = Date.now().toString(36).toUpperCase().slice(-4);
  return `TS-${stamp}-${rand(4)}`;
}

// ---------------------------------------------------------------
// Thank-you screen
// ---------------------------------------------------------------
function ThankYou({ answers, contact, quoteRef }) {
  const result = computeResult(answers);

  return (
    <div className="qs__thanks">
      <div className="qs__thanks-mark" aria-hidden>
        <svg viewBox="0 0 32 32"><path d="M7 16.5l5.5 5.5L25 9" fill="none" stroke="currentColor"
          strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round" /></svg>
      </div>
      <h1 className="qs__thanks-title">You're all set <em>on our end.</em></h1>
      <p className="qs__thanks-sub">
        {result.needsWalkthrough ?
        <>We just texted {formatPhone(contact.phone) || "your number"} with a few times that work. Reply with the one that
        suits you and we'll come take a look — no pressure, no obligation to book.</> :
        <>We just texted {formatPhone(contact.phone) || "your number"}. Reply with a few photos and we'll send back
        your flat One‑Price quote.</>}
      </p>

      <div className="qs__thanks-card">
        <div className="qs__thanks-row">
          <span className="qs__thanks-k">Quote #</span>
          <span className="qs__thanks-v" style={{ fontFamily: "var(--font-mono, ui-monospace, Menlo, monospace)", letterSpacing: "0.06em" }}>{quoteRef || "—"}</span>
        </div>
        <div className="qs__thanks-row">
          <span className="qs__thanks-k">Match</span>
          <span className="qs__thanks-v">{result.tier}</span>
        </div>
        <div className="qs__thanks-row">
          <span className="qs__thanks-k">Estimate</span>
          <span className="qs__thanks-v">{result.estimate ? `~$${result.estimate}` : result.rangeLow ? result.rangeHigh ? `$${result.rangeLow}–$${result.rangeHigh}` : `$${result.rangeLow}+` : result.priceLabel}</span>
        </div>
        <div className="qs__thanks-row">
          <span className="qs__thanks-k">{result.needsWalkthrough ? "Walkthrough" : "Photos"}</span>
          <span className="qs__thanks-v">{result.needsWalkthrough ? "Scheduling by text" : "By text"}</span>
        </div>
        <div className="qs__thanks-row">
          <span className="qs__thanks-k">Text to</span>
          <span className="qs__thanks-v">{formatPhone(contact.phone) || "—"}</span>
        </div>
        {contact.email &&
        <div className="qs__thanks-row">
          <span className="qs__thanks-k">Email</span>
          <span className="qs__thanks-v">{contact.email}</span>
        </div>
        }
      </div>

      <p className="qs__fine" style={{ marginTop: 24 }}>
        Hold onto quote <strong>{quoteRef || "—"}</strong> — reference it any time you reach out and we'll pull up
        everything. {result.needsWalkthrough ?
        <>Need a different time? Reply to the text — it's the fastest way to reach a person here.</> :
        <>Need to change something? Reply to the text — it's the fastest way to reach a person here.</>}
      </p>
    </div>);

}

// ---------------------------------------------------------------
// Wizard shell
// ---------------------------------------------------------------
function Survey() {
  const [step, setStep] = React.useState(0);
  const didPreviewJump = React.useRef(false);
  const [submitted, setSubmitted] = React.useState(false);
  const [errors, setErrors] = React.useState({});
  // Real, unique quote reference. Null until the customer locks in on the
  // capture screen — that's the moment the quote actually posts.
  const [quoteRef, setQuoteRef] = React.useState(null);

  // Preview shortcut: Quote.html?preview=oneprice (flat price) or ?preview=walkthrough
  // jumps straight to the estimate screen with a representative answer set.
  // Editing/QA only — never linked from the site.
  const PREVIEW = new URLSearchParams(window.location.search).get("preview");
  const PREVIEW_ANSWERS = {
    oneprice:    { reason: "reset", areas: ["garage"], volume: "q2", heavy: [], access: "easy", timing: "soon", homeSize: null, clearout: null, fullness: null },
    movedbay:    { reason: "moved", areas: [], volume: "q4", heavy: [], access: "easy", timing: "soon", homeSize: "standard", clearout: null, fullness: null },
    walkthrough: { reason: "list", areas: [], volume: null, heavy: [], access: "easy", timing: "soon", homeSize: "large", clearout: "p100", fullness: null },
    moved:       { reason: "moved", areas: [], volume: "q3", heavy: [], access: "easy", timing: "soon", homeSize: "standard", clearout: null, fullness: null }
  };
  const IS_PREVIEW = !!PREVIEW_ANSWERS[PREVIEW];

  const [answers, setAnswers] = React.useState(
  PREVIEW_ANSWERS[PREVIEW] || {
    reason: null,
    areas: [],
    volume: null,
    heavy: [],
    access: null,
    timing: null,
    homeSize: null,
    clearout: null,
    fullness: null
  });
  const [contact, setContact] = React.useState({
    name: "", phone: "", email: "", zip: "", hear: ""
  });

  // Re-render when the live pricing table or question set finishes loading.
  usePricingTable();
  useQuestions();

  // "Just moved in" and "Getting ready to list" run shorter, size-based branches,
  // but every flow asks about heavy/specialty items before access.
  const STEPS =
  answers.reason === "moved" ? ["reason", "movedSize", "movedVolume", "heavy", "access", "estimate", "capture"] :
  answers.reason === "list" ? ["reason", "listScope", "heavy", "access", "estimate", "capture"] :
  answers.reason === "estate" ? ["reason", "estateScope", "heavy", "access", "estimate", "capture"] :
  ["reason", "scope", "heavy", "access", "estimate", "capture"];
  const TOTAL = STEPS.length;
  const current = STEPS[Math.min(step, STEPS.length - 1)] || "reason";

  const set = (patch) => setAnswers((a) => ({ ...a, ...patch }));

  // Switching paths clears every branch-specific answer. Otherwise a leftover
  // home size from an abandoned flow can price a job off questions the customer
  // was never shown.
  const setReason = (reason) =>
  setAnswers((a) => a.reason === reason ? a : {
    ...a, reason, volume: null, homeSize: null, clearout: null, fullness: null
  });

  // Fathom conversion events. trackOnce() de-dupes per session, so moving
  // back and forth through the estimator never double-counts.
  React.useEffect(() => {
    if (window.trackOnce) window.trackOnce("estimate_started");
  }, []);

  React.useEffect(() => {
    if (IS_PREVIEW && !didPreviewJump.current) {
      const i = STEPS.indexOf("estimate");
      if (i < 0) return;
      didPreviewJump.current = true;
      setStep(i);
    }
  }, [PREVIEW]);

  // Result screen reached — the price range is on screen.
  React.useEffect(() => {
    if (current === "estimate" && window.trackOnce) window.trackOnce("estimate_completed");
  }, [current]);

  const goTo = (n) => {
    setStep(() => Math.max(0, Math.min(n, TOTAL - 1)));
    window.scrollTo({ top: 0, behavior: "auto" });
  };

  // Final submit — from the lead-capture screen. Only the phone is required.
  const submitQuote = () => {
    const e = {};
    if (!contact.name.trim()) e.name = "Let us know who to text.";
    if (contact.phone.trim() && contact.phone.replace(/\D/g, "").length < 10) e.phone = "That doesn't look like a full number.";
    if (!contact.zip.trim()) e.zip = "We need your zip to confirm you're in our area.";
    if (!contact.email.trim()) e.email = "Add an email for your formal quote.";else
    if (!/^[^@\s]+@[^@\s]+\.[^@\s]+$/.test(contact.email.trim())) e.email = "That doesn't look like a valid email.";
    if (!contact.hear) e.hear = "Pick one so we know what's working.";
    setErrors(e);
    if (Object.keys(e).length) return;

    // Mint a fresh, unique number for THIS submit. Never reused from a saved
    // session, never persisted — a returning visitor always gets a new one.
    const ref = makeQuoteRef();
    setQuoteRef(ref);
    if (window.dataLayer) window.dataLayer.push({ event: "quote_submitted", quoteRef: ref, answers, contact });

    // → send the lead to the CRM (Supabase). Fire-and-forget.
    // One result, one price string — the database, Kit, and the on-screen ticket
    // all show the same number. Fixed price -> "495"; range -> "2500–5000".
    const r = computeResult(answers);
    let priceText = "";
    if (r.estimate != null) priceText = String(r.estimate);
    else if (r.rangeLow != null && r.rangeHigh != null) priceText = `${r.rangeLow}–${r.rangeHigh}${r.rangeOpen ? "+" : ""}`;
    else if (r.rangeLow != null) priceText = `${r.rangeLow}+`;

    if (window.sb) {
      const row = {
        source_page:  'Quote.html',
        estimate_no:  ref,
        name:         contact.name,
        phone:        contact.phone,
        email:        contact.email || null,
        zip:          contact.zip || null,
        sms_consent:  !!contact.phone.trim(),
        hear:         contact.hear,
        source_ref:   contact.realtor || null,
        reason:       answers.reason,
        areas:        answers.areas   && answers.areas.length ? answers.areas : null,
        volume:       answers.volume,
        home_size:    answers.homeSize,
        clearout:     answers.clearout,
        fullness:     answers.fullness,
        heavy:        answers.heavy   && answers.heavy.length ? answers.heavy : null,
        access:       answers.access,
        timing:       answers.timing,
        quoted_tier:  r.tier || null,
        quoted_price: (r.estimate ?? r.rangeLow) ?? null,
        quoted_low:   r.estimate != null ? r.estimate : r.rangeLow ?? null,
        quoted_high:  r.estimate != null ? r.estimate : r.rangeHigh ?? null,
        quoted_range: priceText,
        needs_walkthrough: !!r.needsWalkthrough
      };
      window.sb.from('leads').insert(row).then(({ error }) => {
        if (!error) return;
        console.error('lead insert', error);
        // If the range columns don't exist yet, Postgres rejects the whole row.
        // Never lose a lead over a schema gap — retry with the original columns.
        const missingColumn = error.code === 'PGRST204' || /column .* does not exist|Could not find the/i.test(error.message || '');
        if (!missingColumn) return;
        const { quoted_low, quoted_high, quoted_range, needs_walkthrough, ...core } = row;
        console.warn('Retrying lead insert without range columns. Add quoted_low, quoted_high, quoted_range, needs_walkthrough to the leads table.');
        window.sb.from('leads').insert(core).then(({ error: e2 }) => { if (e2) console.error('lead insert retry', e2); });
      });
    }

    // → send the subscriber to Kit. Providing a mobile number and submitting the
    // form is the opt-in for transactional texts (quote, scheduling, reminders).
    {
      const price = priceText;
      fetch('/api/kit-subscribe', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({
          email: contact.email,
          first_name: (contact.name || '').trim().split(/\s+/)[0] || undefined,
          fields: {
            quote_reference: ref,
            quote_price: price,
            project_type: r.tier || '',
            needs_walkthrough: r.needsWalkthrough ? 'yes' : 'no'
          }
        })
      }).catch((err) => console.error('kit subscribe', err));
    }

    setSubmitted(true);
    if (window.trackOnce) window.trackOnce(r.needsWalkthrough ? "lead_submitted_walkthrough" : "lead_submitted");
    window.scrollTo({ top: 0, behavior: "auto" });
    // Clear this session so a returning visitor starts a brand-new quote.
    try { localStorage.removeItem("tossit_quote_v1"); } catch {}
  };

  // Footer Next drives the four question steps (0-3).
  const canContinue =
  current === "reason" && !!answers.reason ||
  current === "scope" && !!answers.volume ||
  current === "heavy" && (answers.heavy || []).length > 0 ||
  current === "movedSize" && !!answers.homeSize ||
  current === "movedVolume" && !!answers.volume ||
  current === "listScope" && !!answers.homeSize && !!answers.clearout ||
  current === "estateScope" && !!answers.homeSize && !!answers.fullness ||
  current === "access" && !!answers.access && !!answers.timing;

  const onNext = () => {
    setStep((s) => Math.min(s + 1, TOTAL - 1));
    window.scrollTo({ top: 0, behavior: "auto" });
  };
  const onBack = () => {
    if (step === 0) {
      window.location.href = "index.html";
      return;
    }
    setStep((s) => Math.max(s - 1, 0));
    window.scrollTo({ top: 0, behavior: "auto" });
  };

  // Persist progress (analytics-friendly, also survives a refresh)
  React.useEffect(() => {
    if (IS_PREVIEW) return;
    try {
      const saved = localStorage.getItem("tossit_quote_v1");
      if (saved) {
        const s = JSON.parse(saved);
        if (s.answers) setAnswers((a) => ({ ...a, ...s.answers }));
        if (s.contact) setContact((c) => ({ ...c, ...s.contact }));
        if (typeof s.step === "number") setStep(s.step);
      }
    } catch {}
  }, []);
  React.useEffect(() => {
    // A preview visit never touches the visitor's real saved session.
    if (IS_PREVIEW) return;
    // Once submitted, keep the session cleared so returning visitors start fresh.
    if (submitted) { try { localStorage.removeItem("tossit_quote_v1"); } catch {} return; }
    // The quote number is never persisted — it belongs to the database.
    try {localStorage.setItem("tossit_quote_v1", JSON.stringify({ step, answers, contact }));} catch {}
  }, [step, answers, contact, submitted]);

  if (submitted) {
    return (
      <div className="qs">
        <header className="qs__top">
          <div className="qs__topbar">
            <div className="qs__progress-segs" aria-hidden>
              {Array.from({ length: TOTAL }).map((_, i) =>
              <span key={i} className="qs__progress-seg is-done" />
              )}
            </div>
            <button type="button" className="qs__close"
            onClick={() => {window.location.href = "index.html";}}
            aria-label="Close">
              <svg viewBox="0 0 18 18" fill="none">
                <path d="M4 4l10 10M14 4L4 14" stroke="currentColor"
                strokeWidth="1.6" strokeLinecap="round" />
              </svg>
            </button>
          </div>
        </header>
        <ThankYou answers={answers} contact={contact} quoteRef={quoteRef} />
      </div>);

  }

  return (
    <div className="qs">
      <header className="qs__top">
        <div className="qs__topbar">
          <button type="button" className="qs__back" onClick={onBack}
          hidden={step === 0} aria-label="Go back">
            <svg viewBox="0 0 18 18" fill="none">
              <path d="M11 4L6 9l5 5" stroke="currentColor"
              strokeWidth="1.6" strokeLinecap="round" strokeLinejoin="round" />
            </svg>
            Back
          </button>
          <ProgressBar step={step} total={TOTAL} />
          <button type="button" className="qs__close"
          onClick={() => {window.location.href = "index.html";}}
          aria-label="Exit">
            <svg viewBox="0 0 18 18" fill="none">
              <path d="M4 4l10 10M14 4L4 14" stroke="currentColor"
              strokeWidth="1.6" strokeLinecap="round" />
            </svg>
          </button>
        </div>
      </header>

      <main className="qs__main">
        {current === "reason" && <StepReason value={answers.reason} onChange={(v) => setReason(v)} />}
        {current === "scope" && <StepScope volume={answers.volume}
        onVolumeChange={(v) => set({ volume: v, homeSize: null, fullness: null })} />}
        {current === "heavy" && <StepHeavy heavy={answers.heavy}
        onHeavyChange={(v) => set({ heavy: v })} />}
        {current === "movedSize" && <StepMovedSize homeSize={answers.homeSize}
        onHomeSizeChange={(v) => set({ homeSize: v })} />}
        {current === "movedVolume" && <StepMovedVolume volume={answers.volume}
        onVolumeChange={(v) => set({ volume: v, fullness: null })} />}
        {current === "listScope" && <StepListScope homeSize={answers.homeSize} clearout={answers.clearout}
        onHomeSizeChange={(v) => set({ homeSize: v })}
        onClearoutChange={(v) => set({ clearout: v })} />}
        {current === "estateScope" && <StepEstateScope homeSize={answers.homeSize} fullness={answers.fullness}
        onHomeSizeChange={(v) => set({ homeSize: v })}
        onFullnessChange={(v) => set({ fullness: v })} />}
        {current === "access" && <StepAccess access={answers.access} timing={answers.timing}
        onAccessChange={(v) => set({ access: v })}
        onTimingChange={(v) => set({ timing: v })} />}
        {current === "estimate" && <StepEstimate answers={answers}
        onGetGuarantee={() => {if (window.trackOnce) window.trackOnce(computeResult(answers).needsWalkthrough ? "walkthrough_cta_clicked" : "oneprice_cta_clicked");goTo(STEPS.indexOf("capture"));}}
        onBallpark={() => {window.location.href = "index.html";}} />}
        {current === "capture" && <StepCapture contact={contact} errors={errors}
        needsWalkthrough={!!computeResult(answers).needsWalkthrough}
        onContactChange={setContact} onSubmit={submitQuote} />}
      </main>

      {!(["estimate", "capture"].includes(current)) &&
      <div className="qs__cta">
        <div className="qs__cta-inner">
          <button type="button"
          className="qs__btn"
          onClick={onNext}
          disabled={!canContinue}
          aria-disabled={!canContinue}>
            Next step
          </button>
        </div>
      </div>
      }
    </div>);

}
window.Survey = Survey;