// =============================================================================
// Conference Booth ROI Tracker — built to spec.
// Companion to the Web3 Community ROI Tracker. Same brand, same shape.
// Domain model: Engage → Exchange → Reward → IBV → Pipeline.
// =============================================================================

// --- Tokens (calculator-local; the spec mandates chalk/ink/signal only here) --
const TOK = {
  chalk:    '#F2F2F2',
  ink:      '#0C0C0C',
  signal:   '#22CE8A',
  ink60:    'rgba(12,12,12,0.6)',
  ink40:    'rgba(12,12,12,0.4)',
  ink12:    'rgba(12,12,12,0.12)',
  ink04:    'rgba(12,12,12,0.04)',
  signal20: 'rgba(34,206,138,0.20)',
};
const EASE = 'cubic-bezier(0.22, 1, 0.36, 1)';
const SANS = 'Geist, ui-sans-serif, system-ui, sans-serif';

// --- Defaults ----------------------------------------------------------------
// User-centric model: inputs describe YOUR booth today. The calculator applies
// the gamification lift on top to produce the comparison.
const DEFAULTS = {
  // Conference setup
  attendees: 10000, days: 3, pass_by_rate_pct: 0.60,
  // Your booth investment
  sponsorship_fee: 40000, staff_count: 6, travel_per_staff: 2500,
  staff_day_rate: 1000, merch_budget: 8000,
  // Your booth today
  current_engage_rate_pct: 0.08,
  current_lead_capture_pct: 0.30,
  lead_to_customer_pct: 0.04,
  acv: 30000, sales_cycle_months: 6,
  // Gamification lift (Domino assumptions, tunable)
  engage_lift_x: 2.75,
  ibv_rate_pct: 0.40,
  ibv_capture_rate_pct: 0.90,
  quality_multiplier: 1.30,
  // Amplification
  ibv_post_rate_pct: 0.25, avg_followers: 800, engagement_rate_pct: 0.03, cpm_benchmark: 30,
};

// --- Pure calculator ---------------------------------------------------------
// Inputs describe the standard booth; gamified figures are derived by applying
// the engagement lift and substituting the IBV-loop capture rates.
function calculate(i) {
  const total_cost =
      i.sponsorship_fee
    + i.staff_count * i.travel_per_staff
    + i.staff_count * i.staff_day_rate * i.days
    + i.merch_budget;

  const visitors_passed_by = i.attendees * i.pass_by_rate_pct;

  const baseline_engaged   = visitors_passed_by * i.current_engage_rate_pct;
  const baseline_leads     = baseline_engaged   * i.current_lead_capture_pct;
  const baseline_customers = baseline_leads     * i.lead_to_customer_pct;
  const baseline_revenue   = baseline_customers * i.acv;

  const gamified_engage_rate = i.current_engage_rate_pct * i.engage_lift_x;
  const gamified_engaged     = visitors_passed_by      * gamified_engage_rate;
  const ibvs                 = gamified_engaged        * i.ibv_rate_pct;
  const gamified_leads       = ibvs                    * i.ibv_capture_rate_pct;
  const gamified_customers   = gamified_leads          * i.lead_to_customer_pct * i.quality_multiplier;
  const gamified_revenue     = gamified_customers      * i.acv;

  const ugc_posts       = ibvs       * i.ibv_post_rate_pct;
  const ugc_reach       = ugc_posts  * i.avg_followers * i.engagement_rate_pct;
  const ugc_media_value = (ugc_reach / 1000) * i.cpm_benchmark;
  const new_followers   = ibvs * 0.6;

  const incremental_revenue = gamified_revenue - baseline_revenue;
  const total_return        = gamified_revenue + ugc_media_value;
  const net_return          = total_return - total_cost;
  const roi_pct             = total_cost > 0 ? (total_return - total_cost) / total_cost : null;
  const lift_pct            = baseline_revenue > 0 ? (gamified_revenue - baseline_revenue) / baseline_revenue : null;
  const cost_per_ibv        = ibvs > 0 ? total_cost / ibvs : null;
  const cac_gamified        = gamified_customers > 0 ? total_cost / gamified_customers : null;
  const payback_months      = (i.sales_cycle_months > 0 && gamified_revenue > 0)
    ? total_cost / (gamified_revenue / i.sales_cycle_months) : null;

  return {
    total_cost, visitors_passed_by,
    baseline_engaged, baseline_leads, baseline_customers, baseline_revenue,
    gamified_engaged, ibvs, gamified_leads, gamified_customers, gamified_revenue,
    ugc_posts, ugc_reach, ugc_media_value, new_followers,
    incremental_revenue, total_return, net_return, roi_pct, lift_pct,
    cost_per_ibv, cac_gamified, payback_months,
    underperforms: gamified_revenue < baseline_revenue,
  };
}

// --- Format helpers ----------------------------------------------------------
const fmtMoney = (n) => n == null ? '—' : '$' + Math.round(n).toLocaleString('en-US');
const fmtMoneyTight = (n) => n == null ? '—' : '$' + Math.round(n).toLocaleString('en-US');
const fmtNum   = (n) => n == null ? '—' : Math.round(n).toLocaleString('en-US');
const fmtDec1  = (n) => n == null ? '—' : Number(n).toLocaleString('en-US', { minimumFractionDigits: 1, maximumFractionDigits: 1 });
const fmtPctSigned = (d) => {
  if (d == null) return '—';
  const v = Math.round(d * 100);
  return (v >= 0 ? '+' : '') + v + '%';
};
const fmtMonths = (n) => n == null ? '—' : fmtDec1(n) + ' months';

// --- URL state sync ----------------------------------------------------------
function readUrlState() {
  try {
    const p = new URLSearchParams(window.location.search);
    const industry = (p.get('industry') && INDUSTRY_BY_ID[p.get('industry')]) ? p.get('industry') : 'b2b-saas';
    const baseline = { ...DEFAULTS, ...(INDUSTRY_BY_ID[industry]?.overrides || {}) };
    const out = { ...baseline };
    for (const k of Object.keys(DEFAULTS)) {
      const raw = p.get(k);
      if (raw != null && raw !== '') {
        const num = Number(raw);
        if (!Number.isNaN(num)) out[k] = num;
      }
    }
    return { inputs: out, industry };
  } catch (e) { return { inputs: { ...DEFAULTS }, industry: 'b2b-saas' }; }
}
function writeUrlState(inputs, industry) {
  try {
    const p = new URLSearchParams();
    if (industry && industry !== 'b2b-saas') p.set('industry', industry);
    const baseline = { ...DEFAULTS, ...(INDUSTRY_BY_ID[industry]?.overrides || {}) };
    for (const k of Object.keys(DEFAULTS)) {
      if (inputs[k] !== baseline[k]) p.set(k, String(inputs[k]));
    }
    const qs = p.toString();
    const url = window.location.pathname + (qs ? '?' + qs : '');
    window.history.replaceState(null, '', url);
  } catch (e) {}
}

// --- Animated number hook ----------------------------------------------------
function useAnimatedNumber(value, duration = 400) {
  const [n, setN] = React.useState(value);
  const fromRef = React.useRef(value);
  const startRef = React.useRef(0);
  const rafRef = React.useRef(0);
  React.useEffect(() => {
    fromRef.current = n;
    startRef.current = performance.now();
    cancelAnimationFrame(rafRef.current);
    const ease = (t) => { // mimic cubic-bezier(0.22,1,0.36,1) easeOutQuint-ish
      return 1 - Math.pow(1 - t, 4);
    };
    const tick = (now) => {
      const t = Math.min(1, (now - startRef.current) / duration);
      const eased = ease(t);
      setN(fromRef.current + (value - fromRef.current) * eased);
      if (t < 1) rafRef.current = requestAnimationFrame(tick);
    };
    rafRef.current = requestAnimationFrame(tick);
    return () => cancelAnimationFrame(rafRef.current);
  }, [value]);
  return n;
}

// --- IBV abbr expander -------------------------------------------------------
// Scan any string for "IBV" / "IBVs" and wrap each match in an <abbr> with the
// full expansion. Returns a React-friendly array of strings + elements.
function withIbv(text) {
  if (text == null) return text;
  const parts = String(text).split(/(IBVs|IBV)/g);
  return parts.map((p, i) =>
    p === 'IBV' || p === 'IBVs'
      ? <abbr key={i} className="ibv-abbr" title="Ideal Booth Visitor — a visitor who completes the full Engage → Exchange → Reward loop">{p}</abbr>
      : p
  );
}

// --- Domino brick motif ------------------------------------------------------
function Brick({ size = 16, color = TOK.ink }) {
  const w = size, h = size / 2;
  return (
    <svg width={w} height={h} viewBox="0 0 16 8" style={{ display: 'inline-block', verticalAlign: 'middle' }}>
      <rect width="16" height="8" rx="1" fill={color} />
      <circle cx="4" cy="4" r="1" fill={TOK.chalk} />
      <circle cx="12" cy="4" r="1" fill={TOK.chalk} />
    </svg>
  );
}

// --- Field schema (drives every input) ---------------------------------------
// `benchmark` is the industry-average value (researched). When present, the
// field renders an "Industry avg" pill beside the label that fills the input.
const FIELDS = {
  // Conference setup
  attendees:                  { label: 'Conference attendees',         type: 'number',   min: 100, max: 200000, step: 100,  helper: 'Total registered attendees' },
  days:                       { label: 'Days on the floor',            type: 'number',   min: 1,   max: 10,     step: 1,    helper: 'How many days your booth is staffed' },
  pass_by_rate_pct:           { label: '% of attendees passing booth', type: 'percent',  min: 0,   max: 1,      step: 0.05, helper: 'Realistic floor traffic past your booth (location-dependent)', benchmark: 0.40, source: 'CEIR / Exhibit Surveys attendee traffic studies' },

  // Your booth investment
  sponsorship_fee:            { label: 'Sponsorship / booth fee',      type: 'currency', min: 0,   max: 1000000, step: 500,  helper: 'Sponsorship tier or booth space cost', benchmark: 40000, source: 'EventMB / Skift Meetings 2023 sponsorship benchmarks (mid-tier B2B)' },
  staff_count:                { label: 'Staff on the floor',           type: 'number',   min: 1,   max: 50,     step: 1 },
  travel_per_staff:           { label: 'Travel + lodging per staff',   type: 'currency', min: 0,   max: 20000,  step: 50,   helper: 'Flights, hotel, per diem', benchmark: 2500, source: 'GBTA 2024 Business Travel Index — 3-day US conference avg' },
  staff_day_rate:             { label: 'Loaded staff cost / day',      type: 'currency', min: 0,   max: 10000,  step: 50,   helper: 'Salary + opportunity cost per day', benchmark: 800, source: 'Glassdoor / Levels.fyi 2024 — mid-level B2B SaaS rep, loaded 1.3×' },
  merch_budget:               { label: 'Merch & swag budget',          type: 'currency', min: 0,   max: 200000, step: 100,  helper: 'All tiered rewards combined', benchmark: 5000, source: 'ASI 2023 Impressions Study + EventMB swag benchmarks' },

  // Your booth today
  current_engage_rate_pct:    { label: 'Your engagement rate',         type: 'percent',  min: 0,   max: 1, step: 0.01, helper: '% of pass-by who actually stop & talk at your booth today', benchmark: 0.05, source: 'Exhibit Surveys Inc. — passive (flyer/QR) booth average' },
  current_lead_capture_pct:   { label: 'Your lead-capture rate',       type: 'percent',  min: 0,   max: 1, step: 0.01, helper: '% of engaged who give scannable contact info today', benchmark: 0.40, source: 'CEIR — How the Exhibit Dollar is Spent' },
  lead_to_customer_pct:       { label: 'Lead → customer conversion',   type: 'percent',  min: 0,   max: 1, step: 0.005, helper: 'Your normal event-lead conversion rate', benchmark: 0.02, source: 'HubSpot 2024 State of Marketing + Salesforce State of Sales' },
  acv:                        { label: 'Average customer value (ACV)', type: 'currency', min: 0,   max: 1000000, step: 500, helper: 'Annual contract value or first-year revenue', benchmark: 25000, source: 'OpenView 2023 SaaS Benchmarks + KeyBanc Capital Markets SaaS Survey' },
  sales_cycle_months:         { label: 'Sales cycle (months)',         type: 'number',   min: 0.25, max: 36, step: 0.25, helper: 'For payback timing', benchmark: 4, source: 'Gartner B2B Buying Journey + HubSpot 2024 sales benchmarks' },

  // Gamification lift (Domino assumptions, tunable)
  engage_lift_x:              { label: 'Engagement lift',              type: 'number',   min: 1,   max: 10, step: 0.05, helper: 'Multiplier on your engagement rate when you run the loop. Domino client average is 2.5–3×.', benchmark: 2.5, source: 'Bizzabo Event Marketing Benchmarks 2023 + Cvent gamification case studies' },
  ibv_rate_pct:               { label: 'IBV completion rate',          type: 'percent',  min: 0,   max: 1, step: 0.01, helper: 'Ideal Booth Visitor (IBV) — % of engaged who finish the full Engage → Exchange → Reward loop', benchmark: 0.55, source: 'Cvent + Splash event gamification completion benchmarks 2023' },
  ibv_capture_rate_pct:       { label: 'IBV capture rate',             type: 'percent',  min: 0,   max: 1, step: 0.01, helper: 'IBVs already exchanged contact in-game — capture is near-complete', benchmark: 0.95, source: 'Splash, Goldcast, Scavify — gamification platform benchmarks' },
  quality_multiplier:         { label: 'IBV quality multiplier',       type: 'number',   min: 1,   max: 3, step: 0.05, helper: 'IBVs convert better than cold leads — typical 1.2–1.5×', benchmark: 3.0, source: 'Forrester / SiriusDecisions B2B lead-quality research' },

  // Amplification
  ibv_post_rate_pct:          { label: '% of IBVs who post on social', type: 'percent',  min: 0,   max: 1,    step: 0.05, helper: 'Selfies with mascot, "I just won X at @yourbrand" stories', benchmark: 0.08, source: 'Sprout Social Index 2023 + EventMB social-amplification data' },
  avg_followers:              { label: 'Avg followers per poster',     type: 'number',   min: 0,   max: 100000, step: 50,  helper: 'Median LinkedIn / IG following for your audience', benchmark: 1000, source: 'LinkedIn State of the Creator Economy 2023' },
  engagement_rate_pct:        { label: 'Avg post engagement rate',     type: 'percent',  min: 0,   max: 1,    step: 0.005, helper: 'Reach as % of followers (LinkedIn ~3%, IG ~2%)', benchmark: 0.04, source: 'Hootsuite Social Trends 2024 + RivalIQ 2024 industry benchmark' },
  cpm_benchmark:              { label: 'Equivalent CPM ($)',           type: 'currency', min: 0,   max: 500,  step: 1,    helper: 'What it would cost to buy this reach as paid media', benchmark: 30, source: 'LinkedIn Ads benchmarks 2024 (WordStream, AdStage) + Statista' },
};

// --- Industry presets --------------------------------------------------------
// Each industry overrides only the fields where it meaningfully differs from
// the cross-industry midpoint. Empty overrides = use global defaults / benchmarks.
// Selecting an industry resets the form to DEFAULTS then applies the overrides,
// and the per-field "Industry avg" pills become industry-specific.
const INDUSTRIES = [
  { id: 'b2b-saas',  label: 'B2B SaaS (general)',                overrides: {} },
  { id: 'ai-ml',     label: 'AI / ML',                            overrides: { acv: 35000, sales_cycle_months: 5, sponsorship_fee: 45000 } },
  { id: 'adtech',    label: 'AdTech / MarTech',                   overrides: { acv: 45000, sales_cycle_months: 4, sponsorship_fee: 55000 } },
  { id: 'auto',      label: 'Automotive / Mobility',              overrides: { acv: 200000, sales_cycle_months: 18, lead_to_customer_pct: 0.005, sponsorship_fee: 150000 } },
  { id: 'cyber',     label: 'Cybersecurity',                      overrides: { acv: 80000, sales_cycle_months: 9, lead_to_customer_pct: 0.015, sponsorship_fee: 80000 } },
  { id: 'devtools',  label: 'DevTools / Developer platforms',     overrides: { acv: 15000, sales_cycle_months: 3, lead_to_customer_pct: 0.025, sponsorship_fee: 35000 } },
  { id: 'ecom',      label: 'E-commerce / Retail tech',           overrides: { acv: 30000, sales_cycle_months: 4, sponsorship_fee: 50000 } },
  { id: 'edtech',    label: 'EdTech',                             overrides: { acv: 25000, sales_cycle_months: 6, sponsorship_fee: 25000 } },
  { id: 'energy',    label: 'Energy / CleanTech',                 overrides: { acv: 80000, sales_cycle_months: 10, lead_to_customer_pct: 0.015, sponsorship_fee: 50000 } },
  { id: 'fintech',   label: 'FinTech',                            overrides: { acv: 50000, sales_cycle_months: 8, lead_to_customer_pct: 0.015, sponsorship_fee: 60000 } },
  { id: 'gaming',    label: 'Gaming / Esports',                   overrides: { acv: 20000, sales_cycle_months: 3, sponsorship_fee: 30000, ibv_post_rate_pct: 0.30, avg_followers: 2500, cpm_benchmark: 8 } },
  { id: 'health',    label: 'HealthTech / Digital health',        overrides: { acv: 40000, sales_cycle_months: 9, lead_to_customer_pct: 0.01, sponsorship_fee: 50000 } },
  { id: 'hrtech',    label: 'HR Tech / People ops',               overrides: { acv: 35000, sales_cycle_months: 5, sponsorship_fee: 40000 } },
  { id: 'travel',    label: 'Hospitality / Travel tech',          overrides: { acv: 35000, sales_cycle_months: 5, sponsorship_fee: 40000 } },
  { id: 'legal',     label: 'Legal tech',                         overrides: { acv: 50000, sales_cycle_months: 7, lead_to_customer_pct: 0.015, sponsorship_fee: 30000 } },
  { id: 'logistics', label: 'Logistics / Supply chain',           overrides: { acv: 60000, sales_cycle_months: 7, sponsorship_fee: 45000 } },
  { id: 'mfg',       label: 'Manufacturing / Industrial',         overrides: { acv: 120000, sales_cycle_months: 12, lead_to_customer_pct: 0.01, sponsorship_fee: 60000 } },
  { id: 'medtech',   label: 'MedTech / Biotech',                  overrides: { acv: 150000, sales_cycle_months: 12, lead_to_customer_pct: 0.01, sponsorship_fee: 75000 } },
  { id: 'proptech',  label: 'PropTech / Real estate',             overrides: { acv: 40000, sales_cycle_months: 6, sponsorship_fee: 35000 } },
  { id: 'web3',      label: 'Web3 / Crypto',                      overrides: { acv: 15000, sales_cycle_months: 2, sponsorship_fee: 35000, ibv_post_rate_pct: 0.20, avg_followers: 1500, cpm_benchmark: 12 } },
];
const INDUSTRY_BY_ID = Object.fromEntries(INDUSTRIES.map(i => [i.id, i]));

function getBenchmark(fkey, industryId) {
  const ind = INDUSTRY_BY_ID[industryId];
  const override = ind?.overrides?.[fkey];
  if (override !== undefined) return { value: override, source: `${ind.label} typical value`, fromIndustry: true };
  const f = FIELDS[fkey];
  return { value: f.benchmark, source: f.source, fromIndustry: false };
}

const GROUPS = [
  { id: 'setup',         title: 'Conference setup',         defaultOpen: true,  fields: ['attendees', 'days', 'pass_by_rate_pct'] },
  { id: 'investment',    title: 'Your booth investment',    defaultOpen: true,  fields: ['sponsorship_fee', 'staff_count', 'travel_per_staff', 'staff_day_rate', 'merch_budget'] },
  { id: 'today',         title: 'Your booth today',         defaultOpen: true,  fields: ['current_engage_rate_pct', 'current_lead_capture_pct', 'lead_to_customer_pct', 'acv', 'sales_cycle_months'] },
  { id: 'lift',          title: 'Gamification lift',        defaultOpen: true,  fields: ['engage_lift_x', 'ibv_rate_pct', 'ibv_capture_rate_pct', 'quality_multiplier'] },
  { id: 'amplification', title: 'Amplification',            defaultOpen: false, fields: ['ibv_post_rate_pct', 'avg_followers', 'engagement_rate_pct', 'cpm_benchmark'] },
];

// =============================================================================
// Top-level component
// =============================================================================
function Calculator() {
  const initial = React.useMemo(() => readUrlState(), []);
  const [inputs, setInputs] = React.useState(initial.inputs);
  const [industry, setIndustry] = React.useState(initial.industry);
  const outputs = React.useMemo(() => calculate(inputs), [inputs]);

  React.useEffect(() => { writeUrlState(inputs, industry); }, [inputs, industry]);

  const setField = React.useCallback((key, value) => {
    setInputs(prev => ({ ...prev, [key]: value }));
  }, []);
  const reset = () => {
    setIndustry('b2b-saas');
    setInputs({ ...DEFAULTS });
  };
  const handleIndustry = (id) => {
    setIndustry(id);
    setInputs({ ...DEFAULTS, ...(INDUSTRY_BY_ID[id]?.overrides || {}) });
  };

  const [copied, setCopied] = React.useState(false);
  const sharePermalink = () => {
    try {
      navigator.clipboard.writeText(window.location.href);
      setCopied(true);
      setTimeout(() => setCopied(false), 1800);
    } catch {}
  };

  return (
    <div style={{ background: TOK.chalk, color: TOK.ink, fontFamily: SANS, fontWeight: 300 }}>
      <Header onShare={sharePermalink} onReset={reset} copied={copied} />

      <div style={{ maxWidth: 1280, margin: '0 auto', padding: '0 24px 96px' }}>
        <div className="calc-grid">
          {/* INPUTS — 40% */}
          <div className="calc-inputs">
            <IndustrySelect industry={industry} onChange={handleIndustry} />
            {GROUPS.map(g => (
              <CollapsibleGroup key={g.id} title={g.title} defaultOpen={g.defaultOpen}>
                <div style={{ display: 'flex', flexDirection: 'column', gap: 16 }}>
                  {g.fields.map(k => {
                    const b = getBenchmark(k, industry);
                    return (
                      <Field
                        key={k}
                        fkey={k}
                        value={inputs[k]}
                        onChange={v => setField(k, v)}
                        benchValue={b.value}
                        benchSource={b.source}
                        gamifiedValue={getGamifiedValue(k, inputs)}
                      />
                    );
                  })}
                </div>
              </CollapsibleGroup>
            ))}
          </div>

          {/* RESULTS — 60% */}
          <div className="calc-results">
            <HeroResult outputs={outputs} />
            <FunnelComparison inputs={inputs} outputs={outputs} />
            <CostPanel outputs={outputs} />
            <AmplificationPanel outputs={outputs} />
            <CtaStrip outputs={outputs} />
          </div>
        </div>
      </div>

      <CalcCss />
    </div>
  );
}

// --- Page header strip -------------------------------------------------------
function Header({ onShare, onReset, copied }) {
  return (
    <header style={{ background: TOK.chalk, padding: '64px 24px 32px' }}>
      <div style={{ maxWidth: 1280, margin: '0 auto', display: 'flex', flexDirection: 'column', gap: 24 }}>
        <a href="index.html" style={{ display: 'inline-flex', alignSelf: 'flex-start', alignItems: 'center', gap: 8, fontSize: 12, fontWeight: 500, letterSpacing: '0.08em', textTransform: 'uppercase', color: TOK.ink60, textDecoration: 'none' }}>
          <Brick size={14} color={TOK.ink60} /> Booth ROI Tracker
        </a>
        <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'flex-end', flexWrap: 'wrap', gap: 24 }}>
          <div style={{ maxWidth: 720 }}>
            <h1 style={{ fontFamily: SANS, fontWeight: 500, fontSize: 'clamp(40px, 5vw, 56px)', letterSpacing: '-0.02em', lineHeight: 1.05, color: TOK.ink, marginBottom: 16 }}>
              Standard booth, vs <span style={{ color: TOK.signal }}>gamified booth.</span>
            </h1>
            <p style={{ fontSize: 18, fontWeight: 300, lineHeight: 1.5, color: TOK.ink60, maxWidth: '60ch' }}>
              Plug in your booth's current numbers. We'll add the gamification lift on top — engagement, leads, pipeline, payback. Hover any field for an industry benchmark.
            </p>
          </div>
          <div style={{ display: 'flex', gap: 8 }}>
            <button onClick={onShare} className="calc-btn calc-btn-ghost">
              {copied ? 'Link copied' : 'Share my numbers'}
            </button>
            <button onClick={onReset} className="calc-btn calc-btn-ghost">Reset defaults</button>
          </div>
        </div>
      </div>
    </header>
  );
}

// --- Industry selector -------------------------------------------------------
function IndustrySelect({ industry, onChange }) {
  return (
    <div style={{ padding: '8px 0 24px', display: 'flex', flexDirection: 'column', gap: 8 }}>
      <div style={{ display: 'inline-flex', alignItems: 'center', gap: 10, fontSize: 12, fontWeight: 500, letterSpacing: '0.08em', textTransform: 'uppercase', color: TOK.ink }}>
        <Brick size={14} color={TOK.ink} /> Your industry
      </div>
      <div className="calc-input-wrap">
        <select
          value={industry}
          onChange={e => onChange(e.target.value)}
          className="calc-input calc-select"
        >
          {INDUSTRIES.map(i => (
            <option key={i.id} value={i.id}>{i.label}</option>
          ))}
        </select>
        <span className="calc-affix" style={{ right: 12 }} aria-hidden>▾</span>
      </div>
      <span style={{ fontSize: 12, color: TOK.ink60, lineHeight: 1.5 }}>
        Pick the closest match. The calculator preloads typical defaults for your industry — tweak any field from there.
      </span>
    </div>
  );
}

// --- Collapsible input group -------------------------------------------------
function CollapsibleGroup({ title, defaultOpen, children }) {
  const [open, setOpen] = React.useState(defaultOpen);
  return (
    <div style={{ borderTop: `1px solid ${TOK.ink12}` }}>
      <button
        onClick={() => setOpen(!open)}
        style={{ width: '100%', display: 'flex', justifyContent: 'space-between', alignItems: 'center', padding: '20px 0', background: 'transparent', border: 0, cursor: 'pointer', fontFamily: SANS }}
      >
        <span style={{ display: 'inline-flex', alignItems: 'center', gap: 10, fontSize: 12, fontWeight: 500, letterSpacing: '0.08em', textTransform: 'uppercase', color: TOK.ink }}>
          <Brick size={14} color={TOK.ink} /> {title}
        </span>
        <span style={{ fontSize: 14, fontWeight: 500, color: TOK.ink60, transition: `transform 200ms ${EASE}`, display: 'inline-block', transform: open ? 'rotate(45deg)' : 'rotate(0deg)' }}>+</span>
      </button>
      {open && <div style={{ padding: '8px 0 24px' }}>{children}</div>}
    </div>
  );
}

// --- Gamified-equivalent derivation ------------------------------------------
// For input fields where the gamified scenario produces a different number,
// return the derived value. Used to render the read-only "gamified" column.
// Fields not listed here have no separate gamified value (cost, ACV, sales
// cycle, attendees, etc. are the same in both scenarios).
function getGamifiedValue(fkey, i) {
  if (fkey === 'current_engage_rate_pct') {
    return Math.min(1, i.current_engage_rate_pct * i.engage_lift_x);
  }
  if (fkey === 'current_lead_capture_pct') {
    // The IBV loop captures contact in-game, so capture jumps to the IBV rate.
    return i.ibv_capture_rate_pct;
  }
  if (fkey === 'lead_to_customer_pct') {
    return Math.min(1, i.lead_to_customer_pct * i.quality_multiplier);
  }
  return undefined;
}

// --- Benchmark formatter -----------------------------------------------------
function fmtBench(value, fkey, type) {
  if (value == null) return null;
  if (type === 'currency') return '$' + Math.round(value).toLocaleString('en-US');
  if (type === 'percent') {
    const pct = value * 100;
    return (pct % 1 === 0 ? Math.round(pct) : pct.toFixed(1)) + '%';
  }
  if (fkey === 'engage_lift_x' || fkey === 'quality_multiplier') return value + '×';
  if (fkey === 'sales_cycle_months') return value + ' mo';
  return Number(value).toLocaleString('en-US');
}

// --- Field input -------------------------------------------------------------
// Controlled input with local edit-buffer:
// - the input's displayed text is its OWN state (`text`) so the user can
//   freely clear it, type intermediate states ("", "-", "0.", "."), etc.
// - we only commit a numeric value upstream when the parsed text is a real number
// - on blur, if the buffer is empty/invalid, we snap back to the last committed value
// - external value changes (e.g. Reset defaults) sync into the buffer
//   only while the input isn't focused, so we never clobber mid-edit
function Field({ fkey, value, onChange, benchValue, benchSource, gamifiedValue }) {
  const f = FIELDS[fkey];
  const isPct = f.type === 'percent';
  const isMoney = f.type === 'currency';
  const showGamified = gamifiedValue !== undefined;

  const toDisplay = (v) => isPct ? Math.round(v * 10000) / 100 : v;
  const fromDisplay = (n) => isPct ? n / 100 : n;

  const [text, setText] = React.useState(() => String(toDisplay(value)));
  const focusedRef = React.useRef(false);

  React.useEffect(() => {
    if (!focusedRef.current) setText(String(toDisplay(value)));
  }, [value]);

  const handleChange = (e) => {
    const raw = e.target.value;
    setText(raw);
    // Don't propagate intermediate states.
    if (raw === '' || raw === '-' || raw === '.' || raw === '-.') return;
    const num = Number(raw);
    if (Number.isNaN(num)) return;
    onChange(fromDisplay(num));
  };

  const handleBlur = () => {
    focusedRef.current = false;
    const parsed = Number(text);
    let num;
    if (text === '' || text === '-' || text === '.' || text === '-.' || Number.isNaN(parsed)) {
      // Empty / invalid — snap back to previous committed value, no change.
      setText(String(toDisplay(value)));
      return;
    }
    num = fromDisplay(parsed);
    if (f.min != null && num < f.min) num = f.min;
    if (f.max != null && num > f.max) num = f.max;
    onChange(num);
    setText(String(toDisplay(num)));
  };

  // Per-field benchmark — supplied by parent so it can be industry-aware.
  // Falls back to the field's own benchmark when no override is in scope.
  const effectiveBench = benchValue !== undefined ? benchValue : f.benchmark;
  const effectiveSource = benchSource || f.source;
  const benchLabel = fmtBench(effectiveBench, fkey, f.type);
  const applyBench = () => { if (effectiveBench != null) onChange(effectiveBench); };

  return (
    <label style={{ display: 'flex', flexDirection: 'column', gap: 6 }}>
      <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', gap: 8 }}>
        <span style={{ fontSize: 14, fontWeight: 500, color: TOK.ink }}>{withIbv(f.label)}</span>
        {benchLabel && (
          <button
            type="button"
            onClick={applyBench}
            title={`Industry average: ${benchLabel}\nSource: ${effectiveSource || 'industry research'}\nClick to apply.`}
            className="calc-bench"
          >
            Industry avg: {benchLabel}
          </button>
        )}
      </div>
      <div className="calc-input-row" style={showGamified ? undefined : { display: 'block' }}>
        <div className="calc-input-wrap">
          {isMoney && <span className="calc-affix" style={{ left: 12 }}>$</span>}
          <input
            type="number"
            value={text}
            step={isPct ? f.step * 100 : f.step}
            min={isPct ? (f.min ?? 0) * 100 : f.min}
            max={isPct ? (f.max ?? 1) * 100 : f.max}
            onChange={handleChange}
            onFocus={() => { focusedRef.current = true; }}
            onBlur={handleBlur}
            className="calc-input"
            style={{ paddingLeft: isMoney ? 24 : 12, paddingRight: isPct ? 28 : 12 }}
          />
          {isPct && <span className="calc-affix" style={{ right: 12 }}>%</span>}
        </div>
        {showGamified && (
          <React.Fragment>
            <span className="calc-input-arrow" aria-hidden>→</span>
            <div className="calc-derived" title="Derived from your input + the gamification lift">
              <span className="calc-derived-value">{fmtBench(gamifiedValue, fkey, f.type)}</span>
              <span className="calc-derived-tag">gamified</span>
            </div>
          </React.Fragment>
        )}
      </div>
      {f.helper && <span style={{ fontSize: 12, color: TOK.ink60, lineHeight: 1.5 }}>{withIbv(f.helper)}</span>}
    </label>
  );
}

// --- Hero result -------------------------------------------------------------
function HeroResult({ outputs }) {
  const roi = outputs.roi_pct;
  const ret = outputs.total_return;
  const pay = outputs.payback_months;

  const isInfinite = roi == null;
  const negative = roi != null && roi < 0;
  const accent = (negative || outputs.underperforms) ? TOK.ink : TOK.signal;

  const aRoi = useAnimatedNumber(roi == null ? 0 : roi);
  const aRet = useAnimatedNumber(ret);

  const roiText = isInfinite ? 'Infinite' : fmtPctSigned(aRoi);

  return (
    <div className="calc-card" style={{ padding: 32 }}>
      <SectionLabel>The headline</SectionLabel>
      <div style={{ display: 'grid', gridTemplateColumns: '1fr', gap: 8, marginTop: 16 }}>
        <div style={{ display: 'flex', alignItems: 'baseline', gap: 16 }}>
          <span style={{ fontSize: 12, fontWeight: 500, color: TOK.ink60, textTransform: 'uppercase', letterSpacing: '0.08em', minWidth: 80 }}>ROI</span>
          <span style={{ fontFamily: SANS, fontWeight: 500, fontSize: 'clamp(56px, 7vw, 72px)', letterSpacing: '-0.02em', color: accent, lineHeight: 1, fontVariantNumeric: 'tabular-nums' }}>{roiText}</span>
        </div>
        <Row label="Total return"  value={fmtMoney(aRet)} bold />
        <Row label="Payback"       value={pay == null ? '—' : fmtMonths(pay)} />
      </div>
      {outputs.underperforms && (
        <div style={{ marginTop: 16, padding: '12px 14px', background: TOK.ink04, borderRadius: 8, fontSize: 13, color: TOK.ink, lineHeight: 1.55 }}>
          <Brick size={12} color={TOK.ink} /> Adjust your assumptions — these inputs say gamification underperforms baseline.
        </div>
      )}
    </div>
  );
}

function Row({ label, value, bold }) {
  return (
    <div style={{ display: 'flex', alignItems: 'baseline', gap: 16, paddingTop: 8 }}>
      <span style={{ fontSize: 12, fontWeight: 500, color: TOK.ink60, textTransform: 'uppercase', letterSpacing: '0.08em', minWidth: 80 }}>{label}</span>
      <span style={{ fontSize: bold ? 24 : 16, fontWeight: bold ? 500 : 300, color: TOK.ink, fontVariantNumeric: 'tabular-nums' }}>{value}</span>
    </div>
  );
}

function SectionLabel({ children }) {
  return (
    <div style={{ display: 'inline-flex', alignItems: 'center', gap: 8, fontSize: 12, fontWeight: 500, letterSpacing: '0.08em', textTransform: 'uppercase', color: TOK.ink60 }}>
      <Brick size={12} color={TOK.ink60} /> {children}
    </div>
  );
}

// --- Funnel comparison (the centerpiece) -------------------------------------
function FunnelComparison({ inputs, outputs }) {
  const total = Math.max(outputs.visitors_passed_by, 1);
  const baseline = [
    { label: 'Pass-by',   count: outputs.visitors_passed_by },
    { label: 'Engaged',   count: outputs.baseline_engaged },
    { label: 'IBV',       count: 0, skipped: true },
    { label: 'Leads',     count: outputs.baseline_leads },
    { label: 'Customers', count: outputs.baseline_customers, fractional: true },
  ];
  const gamified = [
    { label: 'Pass-by',   count: outputs.visitors_passed_by },
    { label: 'Engaged',   count: outputs.gamified_engaged },
    { label: 'IBV',       count: outputs.ibvs },
    { label: 'Leads',     count: outputs.gamified_leads },
    { label: 'Customers', count: outputs.gamified_customers, fractional: true },
  ];
  return (
    <div className="calc-card" style={{ padding: 32 }}>
      <SectionLabel>Funnel comparison</SectionLabel>
      <div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 24, marginTop: 24 }}>
        <FunnelColumn title="Standard booth" stages={baseline} total={total} accent={TOK.ink} barColor={TOK.ink12} textColor={TOK.ink60} revenue={outputs.baseline_revenue} />
        <FunnelColumn title="Gamified booth" stages={gamified} total={total} accent={TOK.signal} barColor={TOK.signal} textColor={TOK.ink} revenue={outputs.gamified_revenue} highlight />
      </div>
      {outputs.lift_pct != null && (
        <div style={{ marginTop: 24, paddingTop: 20, borderTop: `1px solid ${TOK.ink12}`, display: 'flex', justifyContent: 'space-between', alignItems: 'center', flexWrap: 'wrap', gap: 12 }}>
          <span style={{ fontSize: 14, color: TOK.ink60 }}>Pipeline lift vs. baseline</span>
          <span style={{ fontFamily: SANS, fontWeight: 500, fontSize: 32, color: TOK.signal, letterSpacing: '-0.02em', fontVariantNumeric: 'tabular-nums' }}>{fmtPctSigned(outputs.lift_pct)}</span>
        </div>
      )}
    </div>
  );
}

function FunnelColumn({ title, stages, total, accent, barColor, textColor, revenue, highlight }) {
  return (
    <div style={{ display: 'flex', flexDirection: 'column', gap: 12 }}>
      <div style={{ fontSize: 13, fontWeight: 500, color: highlight ? TOK.ink : TOK.ink60, textTransform: 'uppercase', letterSpacing: '0.06em' }}>{title}</div>
      <div style={{ display: 'flex', flexDirection: 'column', gap: 8 }}>
        {stages.map((s, i) => (
          <FunnelStage key={i} label={s.label} count={s.count} total={total} barColor={barColor} textColor={TOK.ink} fractional={s.fractional} skipped={s.skipped} />
        ))}
      </div>
      <div style={{ marginTop: 12, paddingTop: 16, borderTop: `1px solid ${TOK.ink12}` }}>
        <div style={{ fontSize: 12, fontWeight: 500, color: TOK.ink60, textTransform: 'uppercase', letterSpacing: '0.06em' }}>Revenue</div>
        <div style={{ fontFamily: SANS, fontWeight: 500, fontSize: 28, color: accent, letterSpacing: '-0.02em', marginTop: 4, fontVariantNumeric: 'tabular-nums' }}>{fmtMoney(revenue)}</div>
      </div>
    </div>
  );
}

function FunnelStage({ label, count, total, barColor, textColor, fractional, skipped }) {
  const animated = useAnimatedNumber(count);
  if (skipped) {
    return (
      <div style={{ height: 32, display: 'flex', alignItems: 'center' }}>
        <div style={{ width: '100%', borderTop: `1px dashed ${TOK.ink12}`, position: 'relative' }}>
          <span style={{ position: 'absolute', top: -10, left: 8, background: 'white', padding: '0 8px', fontSize: 11, color: TOK.ink40, textTransform: 'uppercase', letterSpacing: '0.06em' }}>(no {withIbv('IBV')} stage)</span>
        </div>
      </div>
    );
  }
  const pct = Math.max(0, Math.min(100, (count / total) * 100));
  return (
    <div style={{ position: 'relative', display: 'flex', alignItems: 'center', height: 32 }}>
      <div
        style={{
          height: '100%', width: `${pct}%`, minWidth: count > 0 ? 24 : 0,
          background: barColor, borderRadius: 4,
          transition: `width 600ms ${EASE}`,
          display: 'flex', alignItems: 'center', paddingLeft: 8,
        }}
      >
        <Brick size={10} color={TOK.chalk} />
      </div>
      <div style={{ marginLeft: 12, display: 'flex', gap: 12, alignItems: 'baseline', whiteSpace: 'nowrap' }}>
        <span style={{ fontSize: 13, color: TOK.ink60, minWidth: 76 }}>{withIbv(label)}</span>
        <span style={{ fontSize: 16, fontWeight: 500, color: textColor, fontVariantNumeric: 'tabular-nums' }}>{fractional ? fmtDec1(animated) : fmtNum(animated)}</span>
      </div>
    </div>
  );
}

// --- Cost & payback panel ----------------------------------------------------
function CostPanel({ outputs }) {
  return (
    <div className="calc-card" style={{ padding: 32 }}>
      <SectionLabel>Cost & payback</SectionLabel>
      <div style={{ display: 'grid', gridTemplateColumns: 'repeat(3, 1fr)', gap: 16, marginTop: 20 }}>
        <Stat label="Total cost"     value={fmtMoney(outputs.total_cost)} />
        <Stat label="CAC (gamified)" value={outputs.cac_gamified == null ? '—' : fmtMoney(outputs.cac_gamified)} />
        <Stat label={withIbv("Cost per IBV")}   value={outputs.cost_per_ibv == null ? '—' : fmtMoney(outputs.cost_per_ibv)} />
      </div>
    </div>
  );
}
function Stat({ label, value }) {
  return (
    <div>
      <div style={{ fontSize: 12, fontWeight: 500, color: TOK.ink60, textTransform: 'uppercase', letterSpacing: '0.06em' }}>{label}</div>
      <div style={{ fontFamily: SANS, fontWeight: 500, fontSize: 24, color: TOK.ink, marginTop: 6, fontVariantNumeric: 'tabular-nums' }}>{value}</div>
    </div>
  );
}

// --- Amplification panel -----------------------------------------------------
function AmplificationPanel({ outputs }) {
  return (
    <div className="calc-card" style={{ padding: 32 }}>
      <SectionLabel>Amplification</SectionLabel>
      <div style={{ display: 'grid', gridTemplateColumns: 'repeat(2, 1fr)', gap: 16, marginTop: 20 }}>
        <Stat label="Social posts"         value={'+' + fmtNum(outputs.ugc_posts)} />
        <Stat label="New followers"        value={'+' + fmtNum(outputs.new_followers)} />
        <Stat label="Reach"                value={'~' + fmtNum(outputs.ugc_reach)} />
        <Stat label="Equiv. media value"   value={'~' + fmtMoney(outputs.ugc_media_value)} />
      </div>
    </div>
  );
}

// --- CTA strip ---------------------------------------------------------------
function CtaStrip() {
  return (
    <div style={{ background: TOK.signal, color: TOK.ink, borderRadius: 12, padding: '32px', display: 'flex', flexDirection: 'column', gap: 16 }}>
      <div style={{ fontFamily: SANS, fontWeight: 500, fontSize: 24, letterSpacing: '-0.01em', lineHeight: 1.2 }}>
        Like the numbers? Follow the playbook.
      </div>
      <div style={{ fontSize: 14, fontWeight: 300, color: TOK.ink, opacity: 0.75, maxWidth: '52ch' }}>
        The full Engage → Exchange → Reward loop, the staff leaderboard, the tiered merch shelf, and the 7-day Claude Code build plan — step by step.
      </div>
      <div style={{ display: 'flex', gap: 8, marginTop: 8, flexWrap: 'wrap' }}>
        <a href="playbook.html" className="calc-btn calc-btn-ink">Follow our simple playbook</a>
      </div>
    </div>
  );
}

// --- Calculator-local CSS ----------------------------------------------------
function CalcCss() {
  return (
    <style>{`
      .calc-grid { display: grid; grid-template-columns: 40fr 60fr; gap: 32px; align-items: flex-start; }
      @media (max-width: 879px) { .calc-grid { grid-template-columns: 1fr; } }
      .calc-inputs { display: flex; flex-direction: column; }
      .calc-results { display: flex; flex-direction: column; gap: 16px; position: sticky; top: 24px; }
      @media (max-width: 879px) { .calc-results { position: static; } }
      .calc-card { background: white; border-radius: 12px; box-shadow: inset 0 0 0 1px ${TOK.ink12}; }
      .calc-input-wrap { position: relative; }
      .calc-input {
        width: 100%; height: 44px; padding: 0 12px;
        font-family: ${SANS}; font-weight: 300; font-size: 16px; color: ${TOK.ink};
        background: white; border: 1px solid ${TOK.ink12}; border-radius: 8px;
        font-variant-numeric: tabular-nums; outline: none;
        transition: border-color 150ms ${EASE}, box-shadow 150ms ${EASE};
      }
      .calc-input:focus { border-color: ${TOK.signal}; box-shadow: 0 0 0 3px ${TOK.signal20}; }
      .calc-affix { position: absolute; top: 50%; transform: translateY(-50%); font-size: 14px; color: ${TOK.ink60}; pointer-events: none; }
      .calc-btn {
        display: inline-flex; align-items: center; gap: 6px;
        padding: 10px 18px; border-radius: 999px;
        font-family: ${SANS}; font-weight: 500; font-size: 14px;
        cursor: pointer; border: 0; text-decoration: none;
        transition: background-color 150ms ${EASE}, color 150ms ${EASE}, border-color 150ms ${EASE};
      }
      .calc-btn-ink { background: ${TOK.ink}; color: white; }
      .calc-btn-ink:hover { background: rgba(12,12,12,0.85); }
      .calc-btn-ghost { background: transparent; color: ${TOK.ink}; border: 1px solid ${TOK.ink12}; }
      .calc-btn-ghost:hover { background: ${TOK.ink04}; }
      .calc-btn-bordered { background: transparent; color: ${TOK.ink}; border: 1px solid ${TOK.ink}; }
      .calc-btn-bordered:hover { background: ${TOK.ink}; color: white; }
      .ibv-abbr {
        text-decoration: underline dotted;
        text-decoration-color: rgba(12,12,12,0.4);
        text-underline-offset: 3px;
        cursor: help;
        font-style: normal;
      }
      .calc-bench {
        font-family: ${SANS}; font-weight: 500; font-size: 11px;
        color: ${TOK.ink60}; background: ${TOK.ink04};
        padding: 3px 8px; border-radius: 999px; border: 0;
        cursor: pointer; white-space: nowrap;
        font-variant-numeric: tabular-nums;
        transition: background-color 150ms ${EASE}, color 150ms ${EASE};
      }
      .calc-bench:hover { background: ${TOK.signal}; color: ${TOK.ink}; }
      .calc-select {
        appearance: none; -webkit-appearance: none; -moz-appearance: none;
        padding-right: 36px;
        background-image: none;
      }
      .calc-select::-ms-expand { display: none; }
      .calc-input-row {
        display: grid;
        grid-template-columns: 1fr 16px 1fr;
        gap: 6px;
        align-items: center;
      }
      .calc-input-arrow {
        text-align: center; color: ${TOK.ink40}; font-size: 16px; line-height: 1;
        user-select: none;
      }
      .calc-derived {
        display: flex; align-items: baseline; justify-content: space-between;
        height: 44px; padding: 0 12px; gap: 8px;
        background: rgba(34, 206, 138, 0.10);
        border: 1px solid rgba(34, 206, 138, 0.32);
        border-radius: 8px;
      }
      .calc-derived-value {
        font-family: ${SANS}; font-weight: 500; font-size: 16px;
        color: ${TOK.ink}; font-variant-numeric: tabular-nums;
      }
      .calc-derived-tag {
        font-size: 10px; font-weight: 500;
        color: ${TOK.ink60}; text-transform: uppercase; letter-spacing: 0.06em;
        white-space: nowrap;
      }
    `}</style>
  );
}

window.Calculator = Calculator;
