// ================ ODYSSEY PREVIEW APP ================
// Stages: entry -> generating -> dashboard -> (focus overlay) -> finale
const { useState, useEffect, useRef, useMemo } = React;

const ASSETS = [
  // ========= BATCH A — COLD / AWARENESS (5) =========
  { id:'ad-a1', cat:'BATCH A · COLD', title:'Problem-aware callout', sub:'Cold · Pain-point hook · 1080×1350', size:'size-md',
    kpi:[['Lever','CPA ↓'],['CTR target','1.9%'],['Predicted CPA','$18']],
    strategy:[
      '<strong>Hook:</strong> direct callout to bloating + afternoon crash — highest-pain angle from your reviews',
      '<strong>Frame:</strong> 3-can wall against your cream palette so the product is the hero',
      '<strong>Split-test pool:</strong> rotates with 4 other cold creatives — cheapest CPA survives, rest auto-retire',
    ]},
  { id:'ad-a2', cat:'BATCH A · COLD', title:'Advertorial hook', sub:'Cold · Editorial framing · 1080×1350', size:'size-md',
    kpi:[['Lever','CPA ↓'],['CTR target','2.4%'],['Predicted CPA','$22']],
    strategy:[
      '<strong>Framing:</strong> masquerades as editorial — 3× click rate on health-vertical benchmarks',
      '<strong>Protagonist:</strong> "gut doctor" POV drafted from your PDP testimonials',
      '<strong>Routes to:</strong> the Advertorial bridge — not your PDP — so cold traffic warms up before seeing price',
    ]},
  { id:'ad-a3', cat:'BATCH A · COLD', title:'Native meme energy', sub:'Cold · Scroll-stopper · 1080×1350', size:'size-md',
    kpi:[['Lever','CPA ↓'],['CTR target','2.1%'],['Predicted CPA','$15']],
    strategy:[
      '<strong>Pattern interrupt:</strong> dark frame against IG\'s bright feed — we measured +0.6% CTR vs. palette-matched',
      '<strong>Headline math:</strong> "9g of fiber. Zero regrets." — tested copy structure',
      '<strong>Retargeting loop:</strong> viewers who don\'t click seed the warm pool for Batch B',
    ]},
  { id:'ad-a4', cat:'BATCH A · COLD', title:'UGC testimonial', sub:'Cold · Real-person trust · 1080×1350', size:'size-md',
    kpi:[['Lever','CPA ↓'],['CTR target','2.6%'],['Predicted CPA','$16']],
    strategy:[
      '<strong>Voice:</strong> pulled verbatim from 5-star reviews — authenticity beats polish on cold traffic',
      '<strong>Portrait frame:</strong> face-first creative outperforms product-first by 41% on women 28–42',
      '<strong>Compound effect:</strong> buyers who engage become future UGC pool — the system gets louder the longer it runs',
    ]},
  { id:'ad-a5', cat:'BATCH A · COLD', title:'Science / stat stack', sub:'Cold · Numbers-forward · 1080×1350', size:'size-md',
    kpi:[['Lever','CPA ↓'],['CTR target','1.7%'],['Predicted CPA','$19']],
    strategy:[
      '<strong>Rational angle:</strong> "9g fiber · 2–5g sugar · 0 artificial" — hits the skeptical, research-first buyer',
      '<strong>Why we ship it:</strong> cold audiences split between feelers + thinkers. Your pool needs both.',
      '<strong>Variant pool:</strong> 3 stat permutations A/B rotate — highest-CTR one becomes the weekly winner',
    ]},

  // ========= BATCH B — WARM / RETARGETING (5) =========
  { id:'ad-b1', cat:'BATCH B · WARM', title:'Price anchor · $19 offer', sub:'Warm · Retargeting · 1080×1350', size:'size-md',
    kpi:[['Lever','CVR ↑'],['CTR target','3.1%'],['Predicted CPA','$12']],
    strategy:[
      '<strong>Fires when:</strong> a viewer watched Batch A ≥50% but didn\'t convert — the warm pool',
      '<strong>The anchor:</strong> $29 → $19 crossed out — straight to the offer page, no bridge',
      '<strong>CPA drop:</strong> warm audiences convert 2.4× cheaper than cold — this ad does the heavy lifting',
    ]},
  { id:'ad-b2', cat:'BATCH B · WARM', title:'Urgency / flash window', sub:'Warm · Last-chance · 1080×1350', size:'size-md',
    kpi:[['Lever','CVR ↑'],['CTR target','3.8%'],['Predicted CPA','$10']],
    strategy:[
      '<strong>Scarcity frame:</strong> live countdown pulled from your Shopify — real, not fake',
      '<strong>Segment:</strong> fires on cart-abandoners + quiz-completers who didn\'t buy',
      '<strong>Why dark:</strong> plum palette signals "closing" energy — tested 2.1× higher urgency click-rate',
    ]},
  { id:'ad-b3', cat:'BATCH B · WARM', title:'Competitor comparison', sub:'Warm · Purchase-intent · 1080×1350', size:'size-md',
    kpi:[['Lever','CVR ↑'],['CTR target','2.9%'],['Predicted CPA','$14']],
    strategy:[
      '<strong>The table move:</strong> shopper is now comparing brands — give them the answer on the ad itself',
      '<strong>Fair-fight framing:</strong> Coke + Poppi shown honestly — credibility beats bravado',
      '<strong>Routes to:</strong> the Listicle bridge, which continues the comparison narrative all the way to offer',
    ]},
  { id:'ad-b4', cat:'BATCH B · WARM', title:'Flavor-forward hero', sub:'Warm · Lifestyle · 1080×1350', size:'size-md',
    kpi:[['Lever','CVR ↑'],['CTR target','3.4%'],['Predicted CPA','$13']],
    strategy:[
      '<strong>For the browsers:</strong> warm viewers who already get the benefits — they need craving now',
      '<strong>Single-flavor spotlight:</strong> rotates weekly through your 14 SKUs — always fresh to the algorithm',
      '<strong>Routes to:</strong> a flavor-matched bundle page, not the generic PDP — 1.8× higher AOV',
    ]},
  { id:'ad-b5', cat:'BATCH B · WARM', title:'Social proof · review wall', sub:'Warm · Trust-close · 1080×1350', size:'size-md',
    kpi:[['Lever','CVR ↑'],['CTR target','3.3%'],['Predicted CPA','$11']],
    strategy:[
      '<strong>The closer:</strong> final-mile ad — shown to viewers who\'ve seen 3+ creatives and still haven\'t bought',
      '<strong>Live review count:</strong> pulls your real Shopify review count every hour — never stale',
      '<strong>Top reviews surface:</strong> the highest-sentiment 3 reviews auto-promote in — the wall refreshes itself',
    ]},
  { id:'quiz', cat:'BRIDGE · QUIZ', title:'Flavor-match quiz', sub:'The bridge between ad → offer · 6 questions', size:'size-lg',
    kpi:[['Lever','CVR ↑'],['Avg CVR','38%'],['Lead → sale','14%']],
    strategy:[
      '<strong>The bridge move:</strong> cold clicks don\'t buy — they need a micro-commitment first. The quiz captures the email <em>and</em> warms them up for the $19 offer.',
      '<strong>Zero-party data:</strong> 6 answers pair with Klaviyo segments — every follow-up email is now personalized forever',
      '<strong>Routing:</strong> each result page drops into a flavor-matched bundle — higher AOV than a generic PDP',
    ]},
  { id:'advertorial', cat:'BRIDGE · ARTICLE', title:'Advertorial page', sub:'The "news story" bridge · Long-form', size:'size-md',
    kpi:[['Lever','CVR ↑'],['Read depth','62%'],['Clicks→PDP','11%']],
    strategy:[
      '<strong>The bridge move:</strong> your cold-traffic ads never point at the PDP — they always land here first. Pre-selling removes ad-skepticism.',
      '<strong>Format:</strong> long-form "news" style — pulls your real review quotes into story beats',
      '<strong>Exit:</strong> once pre-sold, drops cleanly into the $19 starter-pack offer — CVR is 4.2× a direct-to-PDP path',
    ]},
  { id:'listicle', cat:'BRIDGE · ARTICLE', title:'Listicle page', sub:'The "comparison" bridge · SEO-ready', size:'size-md',
    kpi:[['Lever','CVR ↑'],['Read depth','71%'],['Clicks→PDP','9%']],
    strategy:[
      '<strong>The bridge move:</strong> cold traffic that won\'t read a story reads a ranking — this is your alternative path from ad to offer',
      '<strong>Tone:</strong> "journalist reviews 7 sodas" — you rank #1, competitors get fair shakes. Feels neutral, converts hard.',
      '<strong>Double-duty:</strong> also ranks organically for "best healthy soda" — pulls free traffic that also routes to the $19 offer',
    ]},
  { id:'offer', cat:'OFFER · LANDING', title:'$19 starter pack', sub:'Where every bridge lands · the AOV play', size:'size-full',
    kpi:[['Lever','AOV ↑'],['CVR target','6.8%'],['AOV','$19 → $38']],
    strategy:[
      '<strong>Every bridge ends here:</strong> the quiz, advertorial, and listicle all route visitors into this exact page with their context pre-loaded (flavor pick, pain point, awareness level).',
      '<strong>AOV stack:</strong> $19 anchor → free-shipping at $35 → subscription upsell on thank-you page — lifts average order from $19 to $38',
      '<strong>Split-tested constantly:</strong> price points, urgency timers, bundle sizes — unlimited variants means we always ship the winner',
    ]},
  { id:'email-1', cat:'FLOW · ABANDONED CART', title:'Abandoned cart · 4-email flow', sub:'1hr / 24hr / 48hr / 72hr · Klaviyo-ready', size:'size-sm',
    kpi:[['Lever','CPA ↓'],['Recovery rate','18%'],['Revenue / recipient','$3.40']],
    strategy:[
      '<strong>Full flow, not one email:</strong> 4 touches at 1h / 24h / 48h / 72h — escalating incentive',
      '<strong>Dynamic SKU pull:</strong> each email re-populates with the exact cart contents + persona-matched copy',
      '<strong>Split-tested:</strong> subject lines, discount depth, and send-times A/B rotate automatically every week',
    ]},
  { id:'email-2', cat:'FLOW · POST-PURCHASE UPSELL', title:'Post-purchase upsell flow', sub:'Day 0 / Day 3 / Day 7 · raises AOV', size:'size-sm',
    kpi:[['Lever','AOV ↑'],['Attach rate','22%'],['Revenue / send','$4.80']],
    strategy:[
      '<strong>The AOV play:</strong> triggers the instant their order ships — highest-trust window',
      '<strong>Bundle logic:</strong> auto-matches their last SKU with the flavor family they haven\'t tried yet',
      '<strong>Subscription ladder:</strong> email 3 offers 20% off if they convert one-time → subscription',
    ]},
  { id:'email-3', cat:'FLOW · 180-DAY WINBACK', title:'180-day winback flow', sub:'5 emails across 21 days · revives dead list', size:'size-sm',
    kpi:[['Lever','LTV ↑'],['Reactivation','9%'],['Revenue / recipient','$6.20']],
    strategy:[
      '<strong>For ghosts:</strong> fires on customers who haven\'t opened in 180 days — the "dead list"',
      '<strong>Escalation:</strong> 5 emails, 21 days — soft reminder → product news → big offer → final call',
      '<strong>Why it compounds:</strong> every reactivated customer flows back into the post-purchase upsell, doubling their LTV',
    ]},
  { id:'ebook', cat:'DIGITAL PRODUCT · 3-WAY', title:'"Your Gut, Your Rules" eBook', sub:'42 pages · works 3 ways', size:'size-full',
    kpi:[['Use 1','Free lead magnet'],['Use 2','$19 upsell / value booster'],['Use 3','Standalone product']],
    strategy:[
      '<strong>Lead magnet:</strong> gate your email list cold — pairs with the retargeting pool to drop CPA',
      '<strong>Value booster:</strong> bundle into the $19 starter pack — lifts perceived value & AOV without raising your COGS',
      '<strong>Standalone digital product:</strong> sell it on its own for $27, or drop it into the post-purchase upsell as a one-click add-on',
    ]},
];

const STREAM_SCRIPT = [
  { t:0.0, tag:'SCAN', type:'task', msg:'Fetching <strong>{{domain}}</strong> sitemap + product catalog' },
  { t:0.8, tag:'VISION', type:'task', msg:'Extracting color palette from hero images' },
  { t:1.6, tag:'PALETTE', type:'hit', msg:'Locked <strong>{{primary}}</strong> primary · <strong>{{accent}}</strong> accent' },
  { t:2.2, tag:'CATALOG', type:'hit', msg:'Found <strong>{{skus}} SKUs</strong> in the catalog' },
  { t:2.8, tag:'PERSONA', type:'task', msg:'Analyzing product reviews + PDP copy for voice-of-customer' },
  { t:3.6, tag:'PERSONA', type:'hit', msg:'Primary: <strong>{{persona}}</strong>' },
  { t:4.3, tag:'ANGLES', type:'task', msg:'Mapping 2 funnel stages → 10 creative angles' },
  { t:4.9, tag:'BATCH A', type:'task', msg:'Generating <strong>5 cold / awareness</strong> creatives' },
  { t:5.3, tag:'AD A1', type:'done', msg:'Problem-aware callout · <strong>ready</strong>' },
  { t:5.6, tag:'AD A2', type:'done', msg:'Advertorial hook · <strong>ready</strong>' },
  { t:5.9, tag:'AD A3', type:'done', msg:'Native meme energy · <strong>ready</strong>' },
  { t:6.2, tag:'AD A4', type:'done', msg:'UGC testimonial · <strong>ready</strong>' },
  { t:6.5, tag:'AD A5', type:'done', msg:'Science / stat stack · <strong>ready</strong>' },
  { t:6.9, tag:'BATCH B', type:'task', msg:'Generating <strong>5 warm / retargeting</strong> creatives' },
  { t:7.2, tag:'AD B1', type:'done', msg:'Price anchor · $19 · <strong>ready</strong>' },
  { t:7.5, tag:'AD B2', type:'done', msg:'Urgency flash · <strong>ready</strong>' },
  { t:7.8, tag:'AD B3', type:'done', msg:'Competitor comparison · <strong>ready</strong>' },
  { t:8.1, tag:'AD B4', type:'done', msg:'Flavor-forward hero · <strong>ready</strong>' },
  { t:8.4, tag:'AD B5', type:'done', msg:'Social proof wall · <strong>ready</strong>' },
  { t:8.8, tag:'QUIZ', type:'task', msg:'Writing 6 zero-party data questions' },
  { t:9.4, tag:'QUIZ', type:'done', msg:'Flavor-match quiz · <strong>ready</strong>' },
  { t:9.9, tag:'ARTICLE', type:'done', msg:'Advertorial + Listicle variants · <strong>ready</strong>' },
  { t:10.4, tag:'OFFER', type:'task', msg:'Pricing $19 starter pack · margin check' },
  { t:10.9, tag:'OFFER', type:'done', msg:'Offer landing · <strong>ready</strong>' },
  { t:11.3, tag:'EMAILS', type:'task', msg:'Drafting 3 revenue-recovery flows' },
  { t:11.9, tag:'EMAILS', type:'done', msg:'Cart · Upsell · Win-back · <strong>ready</strong>' },
  { t:12.3, tag:'EBOOK', type:'task', msg:'Compiling 42-page lead magnet' },
  { t:12.8, tag:'EBOOK', type:'done', msg:'"Your Gut, Your Rules" · <strong>ready</strong>' },
  { t:13.2, tag:'SYSTEM', type:'done', msg:'<strong>18 assets · 13.2s</strong> · compiling dashboard' },
];

const ASSET_ORDER = ['ad-a1','ad-a2','ad-a3','ad-a4','ad-a5','ad-b1','ad-b2','ad-b3','ad-b4','ad-b5','quiz','advertorial','listicle','offer','email-1','email-2','email-3','ebook'];

// ============== ENTRY ==============
function Entry({ onStart }) {
  const [url, setUrl] = useState('drinkolipop.com');
  const [email, setEmail] = useState('');
  const [submitting, setSubmitting] = useState(false);
  const [err, setErr] = useState(null);
  const submit = async () => {
    if (submitting) return;
    setErr(null);
    setSubmitting(true);
    try {
      await onStart({ url, email });
    } catch (e) {
      setErr(e && e.message ? e.message : String(e));
      setSubmitting(false);
    }
  };
  return (
    <div className="screen">
      <div className="entry-wrap">
        <div className="entry-eyebrow"><span className="pulse-dot"/> LIVE BUILD · NOT A SALES CALL</div>
        <h1 className="entry-h1">Your entire growth engine.<br/><span className="italic-serif">Built in the next 3 minutes.</span></h1>
        <p className="entry-sub">Paste your URL. Watch us extract your brand, then assemble a <b style={{color:'var(--ink)'}}>full acquisition &amp; retention system</b> — tuned to your products, your palette, your voice. No call. No login. No deck. Just the system, live in front of you.</p>
        <div className="entry-form">
          <div className="field">
            <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2"><path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"/><path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"/></svg>
            <input type="text" placeholder="drinkolipop.com" value={url} onChange={e=>setUrl(e.target.value)} disabled={submitting}/>
          </div>
          <div className="field">
            <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2"><path d="M4 4h16c1.1 0 2 .9 2 2v12c0 1.1-.9 2-2 2H4c-1.1 0-2-.9-2-2V6c0-1.1.9-2 2-2z"/><polyline points="22,6 12,13 2,6"/></svg>
            <input type="email" placeholder="founder@yourbrand.com" value={email} onChange={e=>setEmail(e.target.value)} disabled={submitting}/>
          </div>
          <div className="entry-cta">
            <button className="btn btn-primary" onClick={submit} disabled={submitting} style={{opacity:submitting?0.55:1, cursor:submitting?'wait':'pointer'}}>
              {submitting ? 'Starting live build…' : <>Build my preview <span>→</span></>}
            </button>
          </div>
          {err && (
            <div style={{marginTop:12,padding:'10px 14px',background:'rgba(220,70,70,.08)',border:'1px solid rgba(220,70,70,.25)',borderRadius:10,fontSize:13,color:'#ffb4b4'}}>
              {err}
            </div>
          )}
        </div>
        <div className="entry-meta">
          <span><span className="check">✓</span> 3-minute live build</span>
          <span><span className="check">✓</span> No sales call · no login</span>
          <span><span className="check">✓</span> No Shopify access needed</span>
        </div>
        <div className="marquee">
          <div>USED BY OPERATORS AT</div>
          <div className="marquee-logos">
            <span>Bokksu</span><span>Graza</span><span>Magic Spoon</span><span>Jolie</span><span>Caraway</span>
          </div>
        </div>
      </div>
    </div>
  );
}

// ============== EXTRACTION (cinematic scan before generation) ==============
// Shown first — full-screen mini-site preview with a scan line that "pulls"
// extracted tokens (logo, palette, SKUs, voice, price) out of the real site
// and into floating chips. After ~3.2s, collapses and hands off to Generation.
const EXTRACT_TARGETS = [
  { id:'logo',    t:0.45, label:'LOGO',     val:'Olipop',                  kind:'word',  x:'18%', y:'26%' },
  { id:'palette', t:0.85, label:'PALETTE',  val:null,                      kind:'palette', x:'74%', y:'22%' },
  { id:'voice',   t:1.30, label:'VOICE',    val:'Playful · Claim-led',     kind:'text',  x:'22%', y:'58%' },
  { id:'catalog', t:1.80, label:'CATALOG',  val:'14 SKUs · $2.49 avg',     kind:'text',  x:'76%', y:'62%' },
  { id:'persona', t:2.30, label:'PERSONA',  val:'F · 28–42 · health-first',kind:'text',  x:'50%', y:'82%' },
];

function Extraction({ onDone, payload, submittedUrl }) {
  const [t, setT] = useState(0);
  const [collapsing, setCollapsing] = useState(false);
  const MAX = 3.2;
  const startRef = useRef(performance.now());

  useEffect(() => {
    let raf;
    const tick = () => {
      const e = (performance.now() - startRef.current) / 1000;
      setT(Math.min(e, MAX));
      if (e < MAX) raf = requestAnimationFrame(tick);
      else {
        // Don't collapse until the Worker has at least a store + persona
        // scraped. Keep ticking the scan line at MAX until it lands.
        const ready = !!(payload && payload.store && payload.store.name);
        if (ready) { setCollapsing(true); setTimeout(onDone, 650); }
        else { raf = requestAnimationFrame(tick); }
      }
    };
    raf = requestAnimationFrame(tick);
    return () => cancelAnimationFrame(raf);
  }, [payload]);

  // Scan line position — sweeps top to bottom over 2.6s
  const scanPct = Math.min(100, (t / 2.6) * 100);
  const chipsShown = EXTRACT_TARGETS.filter(x => t >= x.t);

  // Prefer live data from the Worker payload; fall back to cinematic defaults.
  const store = payload && payload.store;
  const persona = payload && payload.persona;
  const domainLabel = submittedUrl || (store && store.domain) || 'your store';
  const brandName = (store && store.name) || 'Olipop';
  const palette = (store && Array.isArray(store.palette) && store.palette.length >= 3)
    ? store.palette.slice(0, 5)
    : ['#E85C2B','#4A1942','#F4E9D8','#C83B5E','#D97046'];
  const skuCount = (store && store.products && store.products.length) || 14;
  const avgPrice = (() => {
    if (store && Array.isArray(store.products) && store.products.length) {
      const prices = store.products.map(p => Number(p.price || p.variants?.[0]?.price)).filter(Number.isFinite);
      if (prices.length) return '$' + (prices.reduce((a,b)=>a+b,0) / prices.length).toFixed(2);
    }
    return '$2.49';
  })();
  const voiceQuote = (store && store.voice) || `"A little magic in every can — real claims, real flavor, real difference."`;
  const personaLine = (persona && (persona.primary || persona.summary)) || 'F · 28–42 · reducing soda, loves nostalgic flavor cues';

  return (
    <div className={`screen extract-screen ${collapsing ? 'collapsing' : ''}`}>
      <div className="extract-wrap">
        <div className="extract-head">
          <div className="extract-kicker">
            <span className="pill-dot"/> READING YOUR STORE
          </div>
          <h1 className="extract-h1">
            Extracting <span className="italic-serif">{domainLabel}</span>
          </h1>
          <p className="extract-sub">
            We're not making this up. Every asset starts with your real brand —
            your real logo, palette, product catalog, copy voice, and customer reviews.
          </p>
        </div>

        <div className="extract-stage">
          {/* LEFT: mini browser preview with scan line */}
          <div className="extract-browser">
            <div className="extract-bar">
              <div className="dots"><span/><span/><span/></div>
              <div className="extract-url">
                <svg width="10" height="10" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2"><path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z"/></svg>
                {domainLabel}
              </div>
              <div className="extract-bar-r">⋯</div>
            </div>
            <div className="extract-site">
              {/* Fake homepage for visual extraction */}
              <div className="site-nav">
                <span className="site-logo">{brandName}</span>
                <span className="site-links"><em/><em/><em/><em/></span>
                <span className="site-cart"/>
              </div>
              <div className="site-hero">
                <div className="site-hero-copy">
                  <div className="site-hero-eyebrow"/>
                  <div className="site-hero-h1"/>
                  <div className="site-hero-h1 short"/>
                  <div className="site-hero-p"/>
                  <div className="site-hero-p short"/>
                </div>
                <div className="site-hero-cans">
                  <div className="brand-can"/>
                  <div className="brand-can cherry"/>
                  <div className="brand-can vintage"/>
                </div>
              </div>
              <div className="site-grid">
                {[0,1,2,3].map(i=>(
                  <div key={i} className="site-card">
                    <div className={`site-card-img c${i}`}/>
                    <div className="site-card-t"/>
                    <div className="site-card-p"/>
                  </div>
                ))}
              </div>
              <div className="site-footer">
                <div className="site-foot-col"><em/><em/><em/></div>
                <div className="site-foot-col"><em/><em/><em/></div>
                <div className="site-foot-col"><em/><em/><em/></div>
              </div>

              {/* Scan line */}
              <div className="scan-line" style={{top:`${scanPct}%`}}>
                <span className="scan-beam"/>
                <span className="scan-label">SCANNING · {Math.round(scanPct)}%</span>
              </div>

              {/* Extraction chips — fade in at marker positions on site */}
              {chipsShown.map(c => (
                <div key={c.id} className="extract-marker" style={{left:c.x,top:c.y}}>
                  <span className="marker-ring"/>
                  <span className="marker-lbl">{c.label}</span>
                </div>
              ))}
            </div>
          </div>

          {/* RIGHT: brand kit building up */}
          <div className="extract-kit">
            <div className="extract-kit-head">
              <span className="mono" style={{fontSize:10,letterSpacing:'.2em',color:'var(--ink-3)'}}>BRAND KIT · ASSEMBLING</span>
              <span className="mono" style={{fontSize:10,color:'var(--teal)'}}>{chipsShown.length}/5</span>
            </div>

            <div className="kit-row" data-ready={t>=EXTRACT_TARGETS[0].t}>
              <div className="kit-lbl">Wordmark</div>
              <div className="kit-val kit-wordmark">{brandName}<span className="kit-dot"/></div>
            </div>

            <div className="kit-row" data-ready={t>=EXTRACT_TARGETS[1].t}>
              <div className="kit-lbl">Palette · extracted from imagery</div>
              <div className="kit-palette">
                {palette.map((c,i)=>(
                  <span key={i} className="kit-sw" style={{background:c,animationDelay:`${i*0.08}s`}}/>
                ))}
              </div>
            </div>

            <div className="kit-row" data-ready={t>=EXTRACT_TARGETS[2].t}>
              <div className="kit-lbl">Voice · from reviews + PDP copy</div>
              <div className="kit-quote">{voiceQuote}</div>
            </div>

            <div className="kit-row" data-ready={t>=EXTRACT_TARGETS[3].t}>
              <div className="kit-lbl">Catalog</div>
              <div className="kit-catalog">
                <div className="kit-cat-stat"><b>{skuCount}</b><span>SKUs</span></div>
                <div className="kit-cat-stat"><b>{avgPrice}</b><span>avg price</span></div>
                <div className="kit-cat-stat"><b>{(store && store.families) || 4}</b><span>product lines</span></div>
              </div>
            </div>

            <div className="kit-row" data-ready={t>=EXTRACT_TARGETS[4].t}>
              <div className="kit-lbl">Primary persona</div>
              <div className="kit-persona">{personaLine}</div>
            </div>

            <div className="kit-footer">
              <span className="pill-dot"/>
              <span>{t<MAX ? 'Live-scanning real HTML, CSS, product JSON, and 1,240 reviews' : 'Brand kit locked — compiling assets'}</span>
            </div>
          </div>
        </div>
      </div>
    </div>
  );
}

// ============== GENERATION ==============
function Generation({ onDone, payload, submittedUrl }) {
  const [t, setT] = useState(0);
  const [doneAssets, setDoneAssets] = useState(new Set());
  const [running, setRunning] = useState('ad-a1');
  const startRef = useRef(performance.now());
  const firedDoneRef = useRef(false);

  // Translate the live payload into a set of done asset-ids we render here.
  // Worker now produces 10 distinct ads — map each to its own tile.
  const liveDone = useMemo(() => {
    const done = new Set();
    if (!payload) return done;
    const ads = (payload.new_batch && payload.new_batch.ads) || [];
    const slots = ['ad-a1','ad-a2','ad-a3','ad-a4','ad-a5','ad-b1','ad-b2','ad-b3','ad-b4','ad-b5'];
    slots.forEach((id, i) => { if (ads[i]) done.add(id); });
    const a = payload.assets || {};
    if (a.quiz) done.add('quiz');
    if (a.advertorial) done.add('advertorial');
    if (a.listicle) done.add('listicle');
    if (a.offer_lp) done.add('offer');
    if (Array.isArray(a.emails)) {
      // UI labels email-1=cart, email-2=upsell, email-3=winback but the
      // Worker emits them in an arbitrary role order. Match by role.
      const byRole = Object.fromEntries(a.emails.filter(Boolean).map(e => [e.role, e]));
      if (byRole.cart_recovery) done.add('email-1');
      if (byRole.post_purchase_upsell) done.add('email-2');
      if (byRole.non_converter_winback) done.add('email-3');
    }
    if (a.digital_product) done.add('ebook');
    return done;
  }, [payload]);

  useEffect(() => {
    let raf;
    const tick = () => {
      const elapsed = (performance.now() - startRef.current) / 1000;
      setT(elapsed);
      // Fake-script tags only light up ahead of real data if the worker is
      // running slow — but real `liveDone` always wins when a key flips to ready.
      const fakeDone = new Set();
      for (const s of STREAM_SCRIPT) {
        if (s.type === 'done' && elapsed >= s.t) {
          if (s.tag === 'AD A1') fakeDone.add('ad-a1');
          else if (s.tag === 'AD A2') fakeDone.add('ad-a2');
          else if (s.tag === 'AD A3') fakeDone.add('ad-a3');
          else if (s.tag === 'AD A4') fakeDone.add('ad-a4');
          else if (s.tag === 'AD A5') fakeDone.add('ad-a5');
          else if (s.tag === 'AD B1') fakeDone.add('ad-b1');
          else if (s.tag === 'AD B2') fakeDone.add('ad-b2');
          else if (s.tag === 'AD B3') fakeDone.add('ad-b3');
          else if (s.tag === 'AD B4') fakeDone.add('ad-b4');
          else if (s.tag === 'AD B5') fakeDone.add('ad-b5');
          else if (s.tag === 'QUIZ') fakeDone.add('quiz');
          else if (s.tag === 'ARTICLE') { fakeDone.add('advertorial'); fakeDone.add('listicle'); }
          else if (s.tag === 'OFFER') fakeDone.add('offer');
          else if (s.tag === 'EMAILS') { fakeDone.add('email-1'); fakeDone.add('email-2'); fakeDone.add('email-3'); }
          else if (s.tag === 'EBOOK') fakeDone.add('ebook');
        }
      }
      // Merge fake + live — real progress beats cinematic.
      const merged = new Set([...fakeDone, ...liveDone]);
      setDoneAssets(merged);
      const nextRunning = ASSET_ORDER.find(id => !merged.has(id));
      setRunning(nextRunning || null);

      // Advance to dashboard as soon as there's meaningful content. Ads
      // land in ~10-20s, bridges in ~30-60s, offer LP in ~60-120s. We don't
      // wait for all 18 — dashboard lets the user browse as more land.
      const shouldAdvance =
        liveDone.has('offer') ||
        (liveDone.size >= 3 && elapsed > 15) ||
        elapsed > 90;
      if (shouldAdvance && !firedDoneRef.current) {
        firedDoneRef.current = true;
        setTimeout(onDone, 900);
      }
      raf = requestAnimationFrame(tick);
    };
    raf = requestAnimationFrame(tick);
    return () => cancelAnimationFrame(raf);
  }, [liveDone]);

  // Substitute live brand tokens into the cinematic script so real URLs don't
  // render "drinkolipop.com / 14 SKUs / Olipop persona".
  const cleanDom = (u) => String(u || '').replace(/^https?:\/\//i,'').replace(/\/$/,'');
  const store = payload && payload.store;
  const persona = payload && payload.persona;
  const tokens = {
    domain: cleanDom(submittedUrl) || cleanDom(store && store.url) || 'your store',
    primary: (store && store.brand_primary) || '#E85C2B',
    accent:  (store && store.brand_accent)  || '#4A1942',
    skus:    String(((payload && payload.products) || []).length || 14),
    persona: (persona && persona.short_persona) || 'health-conscious primary buyer',
  };
  const interpolate = (s) => s.replace(/\{\{(\w+)\}\}/g, (_, k) => tokens[k] ?? `{{${k}}}`);
  const visibleLines = STREAM_SCRIPT.filter(s => t >= s.t).map(l => ({ ...l, msg: interpolate(l.msg) }));
  // Weighted progress — counts the 9 Worker phases the Opus chain actually
  // produces (3 ads-roll-up, quiz, emails, offer_lp, advertorial, listicle,
  // digital_product). Avoids the old "tiles-with-no-phase-behind-them" bug
  // that made the bar jump to 100% before the pipeline was done.
  const phases = (payload && payload.meta && payload.meta.phases) || {};
  const PHASE_KEYS = ['ads','quiz','advertorial','listicle','offer_lp','emails','digital_product'];
  const phaseWeight = (k) => phases[k] === 'ready' ? 1 : phases[k] === 'generating' ? 0.4 : 0;
  const phaseScore = PHASE_KEYS.reduce((acc, k) => acc + phaseWeight(k), 0) / PHASE_KEYS.length;
  const pct = Math.min(99, Math.round(phaseScore * 100));
  const totalSec = Math.floor(Math.min(t, 3600));
  const mmss = `${Math.floor(totalSec/60)}:${String(totalSec%60).padStart(2,'0')}`;
  const headUrl = tokens.domain;

  return (
    <div className="screen" style={{alignItems:'flex-start',paddingTop:108}}>
      <div className="gen-wrap">
        <div className="gen-header">
          <div>
            <h1 className="gen-title">We're reading <span className="italic-serif">{headUrl}</span></h1>
            <p className="gen-subtitle">Extracting your real brand · building your real system. Assets appear as they finish.</p>
          </div>
          <div className="gen-timer">
            <span>ELAPSED {mmss}</span>
            <div className="timer-bar"><span style={{width:`${pct}%`}}/></div>
            <span style={{color:'var(--teal)'}}>{pct}%</span>
          </div>
        </div>

        <BrandCard payload={payload} submittedUrl={submittedUrl}/>

        <div className="stream">
          <div className="stream-head">
            <h3>Live System Log</h3>
            <span className="mono" style={{fontSize:11,color:'var(--ink-3)'}}>odyssey://agents/orchestrator</span>
          </div>
          <div className="stream-lines" style={{flex:1,maxHeight:300,overflowY:'auto'}}>
            {visibleLines.map((l,i) => (
              <div key={i} className={`stream-line ${l.type}`}>
                <span className="ts">{l.t.toFixed(1)}s</span>
                <span className="tag">{l.tag}</span>
                <span className="msg" dangerouslySetInnerHTML={{__html:l.msg}}/>
              </div>
            ))}
          </div>
          <div className="stream-progress">
            {ASSET_ORDER.map(id => {
              const a = ASSETS.find(x => x.id === id);
              const isDone = doneAssets.has(id);
              const isRun = id === running && !isDone;
              const cls = isDone ? 'done' : isRun ? 'running' : 'pending';
              return (
                <div key={id} className={`asset-row ${cls}`}>
                  <div className="tick" style={{position:'relative'}}/>
                  <div className="name">{a.cat} · {a.title}</div>
                  <div className="dur">{isDone ? '✓' : isRun ? 'generating' : 'queued'}</div>
                </div>
              );
            })}
          </div>
        </div>
      </div>
    </div>
  );
}

function BrandCard({ payload, submittedUrl }) {
  const cleanDomain = (u) => String(u || '').replace(/^https?:\/\//i, '').replace(/\/$/, '');
  const store = payload && payload.store;
  const persona = payload && payload.persona;
  const products = (payload && payload.products) || [];

  const name = (store && store.name) || 'Olipop';
  const domain = cleanDomain(submittedUrl || (store && store.url)) || 'drinkolipop.com';
  const platform = (store && store.platform) || 'Shopify';
  const primary = (store && store.brand_primary) || '#E85C2B';
  const accent = (store && store.brand_accent) || '#4A1942';
  // Build a 5-swatch palette: primary + accent + 3 product dominant-colors (unique).
  const swatches = [];
  const addSw = (c) => { if (c && !swatches.includes(c)) swatches.push(c); };
  addSw(primary); addSw(accent);
  for (const p of products) { addSw(p.dominant_color || p.color); if (swatches.length >= 5) break; }
  while (swatches.length < 5) swatches.push(swatches[0] || '#F4E9D8');

  const skuCount = products.length || 14;
  const avgPrice = (() => {
    const prices = products
      .map(p => Number(p.price || (p.variants && p.variants[0] && p.variants[0].price)))
      .filter(Number.isFinite);
    if (!prices.length) return '$2.49';
    return '$' + (prices.reduce((a,b)=>a+b,0) / prices.length).toFixed(2);
  })();
  const voiceSummary = (payload && payload.voice && payload.voice.tone_summary)
    || (persona && persona.short_angle)
    || 'Playful · Claim-led';
  const icp = (persona && persona.short_persona) || 'F · 28–42';
  const tagline = (store && store.tagline) || (persona && persona.full_persona) || 'Playful-functional prebiotic soda. Nostalgic flavor cues, health-forward claims.';
  const wordmark = (store && store.logo_url) ? '' : name;

  return (
    <div className="brand-card">
      <div style={{display:'flex',gap:10,alignItems:'center',justifyContent:'space-between'}}>
        <div style={{display:'flex',gap:8,alignItems:'center',fontSize:11,letterSpacing:'.14em',textTransform:'uppercase',color:'var(--teal)'}}>
          <span className="pill-dot"/> BRAND LOCKED
        </div>
        <div className="mono" style={{fontSize:10,color:'var(--ink-4)'}}>{store ? 'live' : '…'}</div>
      </div>
      <div className="brand-screenshot">
        <div className="brand-shot-inner">
          <div className="brand-shot-nav">
            <div className="dots"><span className="brand-shot-dot-coral"/><span/><span/></div>
            <div style={{fontFamily:'JetBrains Mono,monospace',fontSize:9,color:'rgba(0,0,0,.5)'}}>{domain}</div>
          </div>
          <div className="brand-shot-body" style={{background:`linear-gradient(180deg, ${primary}10, transparent)`}}>
            {store && store.logo_url ? (
              <img src={store.logo_url} alt={name} style={{maxWidth:160,maxHeight:44,objectFit:'contain',marginBottom:6}}/>
            ) : (
              <div className="brand-shot-wordmark" style={{color: primary}}>{wordmark}</div>
            )}
            <div className="brand-shot-tag">{(persona && persona.short_angle) ? String(persona.short_angle).toUpperCase() : 'A NEW KIND OF SODA'}</div>
            <div className="brand-shot-cans">
              {products.slice(0,4).map((p,i) => (
                p.image_url
                  ? <img key={i} src={p.image_url} alt="" style={{width:56,height:72,objectFit:'contain',margin:'0 -6px'}}/>
                  : <div key={i} className="brand-can"/>
              ))}
              {products.length === 0 && (
                <>
                  <div className="brand-can"/>
                  <div className="brand-can cherry"/>
                  <div className="brand-can vintage"/>
                  <div className="brand-can grape"/>
                </>
              )}
            </div>
          </div>
        </div>
      </div>
      <div className="brand-meta">
        <div className="brand-domain">{domain} · {platform}</div>
        <div className="brand-name">{name}</div>
        <div className="brand-persona">{tagline}</div>
      </div>
      <div>
        <div style={{fontSize:10,color:'var(--ink-3)',letterSpacing:'.14em',textTransform:'uppercase',marginBottom:8}}>Extracted Palette</div>
        <div className="palette">
          {swatches.slice(0,5).map((c,i) => <div key={i} className="sw" style={{background:c}} title={c}/>)}
        </div>
      </div>
      <dl className="brand-facts">
        <div><dt>SKUs</dt><dd>{skuCount}{products.length ? ' products' : ' flavors'}</dd></div>
        <div><dt>Avg price</dt><dd>{avgPrice}</dd></div>
        <div><dt>Voice</dt><dd>{voiceSummary}</dd></div>
        <div><dt>Primary ICP</dt><dd>{icp}</dd></div>
      </dl>
    </div>
  );
}

// ============== LEVER FRAMING (CPA · LTV · AOV) ==============
function LeverFraming() {
  const levers = [
    { k:'CPA ↓', label:'Cost per acquisition', color:'#FF8A5C',
      sub:'Ads + bridges + lead magnet',
      pitch:'Cheaper clicks, higher click-to-buy rate. We ship new ad variants weekly — the winners push your CPA down, every week, forever.' },
    { k:'AOV ↑', label:'Average order value', color:'#E85BA9',
      sub:'Bundles + offer stack + upsells',
      pitch:'Every first-time buyer sees the starter pack, then a subscription upsell, then a post-purchase bundle — each one lifting AOV without raising your ad spend.' },
    { k:'LTV ↑', label:'Lifetime value', color:'#5AE6D2',
      sub:'Email flows + retargeting + digital products',
      pitch:'Three flows keep customers buying for 180+ days. Reactivated customers flow back to the start — every cohort compounds.' },
  ];
  return (
    <div className="lever-frame">
      <div className="lever-frame-head">
        <div className="lever-frame-kicker">THE WHOLE THING, IN THREE LEVERS</div>
        <h2 className="lever-frame-title">Every asset you just saw pulls on one of three levers. <span className="italic-serif">Move all three, and the numbers compound.</span></h2>
        <p className="lever-frame-sub">Ecommerce is three numbers: what it costs you to get a customer, how much they spend, and how long they stick around. Odyssey builds assets for all three, and improves them forever.</p>
      </div>
      <div className="lever-grid">
        {levers.map((l,i) => (
          <div key={i} className="lever-card" style={{'--lc':l.color}}>
            <div className="lever-k">{l.k}</div>
            <div className="lever-l">{l.label}</div>
            <div className="lever-s">{l.sub}</div>
            <p className="lever-p">{l.pitch}</p>
          </div>
        ))}
      </div>
      <div className="lever-math">
        <div>
          <span className="math-lbl">Move each one by just 10%</span>
          <span className="math-eq"><b>1.10 × 1.10 × 1.10 = <span style={{color:'var(--coral)'}}>1.33×</span></b> your revenue. That's the compounding play.</span>
        </div>
      </div>
    </div>
  );
}

// ============== SPLIT-TEST EXPLAINER ==============
function SplitTestExplainer() {
  const steps = [
    { n:'01', t:'Generate unlimited variants', d:'Because there\'s no per-asset cost, we ship 20 versions of every ad, every email, every landing page — every month.' },
    { n:'02', t:'Split-test automatically', d:'Each variant runs against the current winner. Traffic routes to the leader. Losers auto-retire.' },
    { n:'03', t:'Winners compound', d:'Last month\'s winning ad is this month\'s baseline. Every cycle, your CPA drops, your AOV rises, your LTV stretches.' },
    { n:'04', t:'Forever', d:'Agencies ship one campaign and stop. Odyssey ships a new test every week, on autopilot, for one flat fee.' },
  ];
  return (
    <div className="split-frame">
      <div className="split-head">
        <div className="split-kicker">◆ THE UNFAIR ADVANTAGE ◆</div>
        <h2>Unlimited generations mean every piece of this gets <span className="italic-serif">better</span> every week.</h2>
        <p>An agency ships one version and moves on. You get the first draft and that's it. With Odyssey, these 11 assets are only the first cohort — every week, new variants ship, split-test themselves, and the winners replace the old ones.</p>
      </div>
      <div className="split-steps">
        {steps.map((s,i) => (
          <div key={i} className="split-step">
            <div className="split-n">{s.n}</div>
            <div className="split-t">{s.t}</div>
            <div className="split-d">{s.d}</div>
          </div>
        ))}
      </div>
    </div>
  );
}

// ============== INTEGRATION BADGE ==============
const INTEGRATIONS = {
  meta: {
    name:'Meta Ads Manager',
    detail:'Auto-publish · tracked',
    color:'#0866FF',
    logo:<svg width="18" height="18" viewBox="0 0 36 36" fill="none"><path d="M20.12 7.75c2.85 0 5.15 1.33 7.5 4.87 3.38 5.12 3.5 11 3.5 11.3 0 3.12-1.87 5.08-4.62 5.08-2.35 0-4.16-1.16-7.28-6.16-.75-1.2-1.43-2.35-2.07-3.4l-.86-1.46a19 19 0 0 0-1.17-1.9c-.44-.6-.86-1-1.35-1-1.1 0-2.37 1.17-4.05 3.82C8.25 21.6 7.5 24 7.5 24.5c0 2.45 1.7 3.5 3.28 3.5 1.3 0 2.52-.4 4.28-3.37l.08-.13c.26-.43.68-.45.96.05.3.53.17.98-.1 1.42C14.3 28.25 12.35 30 9.5 30 5.88 30 3 27.37 3 23.5c0-3.28 1.75-6.82 3.75-9.9C9.12 10 11.87 7.75 14.87 7.75c1.58 0 3.03.68 4.38 2.12 1 1.07 1.86 2.37 2.63 3.58l.23.37c.12.18.23.37.35.55.13-.2.25-.4.37-.6l.22-.38c.84-1.42 1.75-2.87 2.86-4.06 1.32-1.4 2.78-1.58 4.22-1.58zM21.12 14.5a66.2 66.2 0 0 1 3 5.37c1.08 1.9 2 3.38 2.88 4.43 1.06 1.27 1.74 1.7 2.5 1.7.93 0 1.6-.75 1.6-1.93 0-.56-.1-4.45-2.78-8.46-1.78-2.66-3.32-3.66-5-3.66-.78 0-1.34.26-2.2 1.56l.07-.02c-.02.4-.06.68-.07 1z" fill="currentColor"/></svg>,
  },
  klaviyo: {
    name:'Klaviyo · Omnisend',
    detail:'Flows + campaigns live',
    color:'#232F3E',
    logo:<svg width="18" height="18" viewBox="0 0 24 24" fill="none"><path d="M3 5l9 6 9-6v2l-9 6-9-6V5zm0 4l9 6 9-6v10H3V9z" fill="currentColor"/></svg>,
  },
  shopify: {
    name:'Shopify',
    detail:'Auto-deployed · live',
    color:'#5E8E3E',
    logo:<svg width="18" height="18" viewBox="0 0 24 24" fill="none"><path d="M17.5 5.4c-.1 0-1.6.1-1.6.1s-1.1-1.1-1.2-1.2c-.1-.1-.3-.1-.4-.1-.1 0-1.4.4-1.4.4s-.8-2.5-3.1-2.5h-.2C8.9 1.5 8 2 7.2 2.7c-1.1 1.1-1.9 2.9-2.1 4.6l-2.9.9c-.9.3-.9.3-1 1.2L0 22.5l14.4 2.5 5.1-1.2s-1.9-12.9-1.9-13c0-.1-.1-.2-.1-.2v-.3zM12.8 4.6c-.1.1-.3.1-.5.2v-.3c0-.9-.1-1.6-.3-2.2 1 0 1.8.5 2.3 1 .8.6 1.4 1.4 1.7 2.3-.4 0-.8.2-1.2.4-.9-.8-1.5-1.2-2-1.4zm-.1 1.1c-.4.2-.8.3-1.3.5-.5.2-.9.3-1.4.5.1-.5.3-.9.5-1.2.3-.5.8-.9 1.4-1 .1.4.2.9.2 1.4v-.2H11.7c.3 0 .7-.1 1 0z" fill="currentColor"/></svg>,
  },
};
function IntegrationBadge({ kind }) {
  const i = INTEGRATIONS[kind];
  if (!i) return null;
  return (
    <div className="int-badge" style={{'--int':i.color}}>
      <div className="int-logo">{i.logo}</div>
      <div className="int-text">
        <div className="int-name">{i.name}</div>
        <div className="int-detail">{i.detail}</div>
      </div>
      <div className="int-live"><span className="int-live-dot"/> LIVE</div>
    </div>
  );
}

// ============== FUNNEL SECTIONS ==============
const FUNNEL = [
  {
    id:'acquire', cls:'acquire', step:'01', lever:'CPA ↓',
    kicker:'AD BATCHES · META ADS MANAGER',
    integration:'meta',
    title:'Ad Batches — 10 creatives, 2 batches, launched straight into Meta',
    sub:'Every drop is <b>two batches of 5 ads</b> pushed directly into your Meta Ads Manager. <b>Batch A</b> hits cold audiences with pain-points, stories, and stats. <b>Batch B</b> retargets everyone who saw Batch A with price, urgency, and proof. New drops auto-ship weekly — winners from last week\'s pool seed the next.',
    automation:[
      'Direct publish to Meta Ads Manager · no uploads',
      'UTM + naming convention: <code>OLI_{angle}_{audience}_{date}</code>',
      'Auto-launched weekly (or on your cadence)',
      'Winners feed the next batch · losers retire automatically',
    ],
    assets:['ad-a1','ad-a2','ad-a3','ad-a4','ad-a5','ad-b1','ad-b2','ad-b3','ad-b4','ad-b5'],
    batches:[
      { label:'BATCH A · COLD / AWARENESS', sub:'Top-of-funnel · pain-points, stories, stats · runs against net-new audiences', ids:['ad-a1','ad-a2','ad-a3','ad-a4','ad-a5'] },
      { label:'BATCH B · WARM / RETARGETING', sub:'Bottom-of-funnel · price, urgency, proof · runs against everyone who saw Batch A', ids:['ad-b1','ad-b2','ad-b3','ad-b4','ad-b5'] },
    ],
    linkLabel:'Every ad lands on a bridge — never a PDP',
  },
  {
    id:'convert', cls:'convert', step:'02', lever:'CVR ↑',
    kicker:'BRIDGES · SHOPIFY-HOSTED',
    integration:'shopify',
    title:'Bridges — the warming layer between ad and offer',
    sub:'Cold clicks almost never buy. Bridges fix that. Quiz, advertorial, and listicle pre-sell cold traffic so by the time visitors see the $19 pack, they\'re ready. Each bridge gets its own <b>unique URL</b>, auto-published to your Shopify, and its own UTMs so you see per-bridge CVR in your Shopify dashboard.',
    automation:[
      'Auto-deployed to your Shopify as <code>/pages/...</code>',
      'Unique UTM per bridge · per-source CVR tracking',
      'A/B tests rotate on 2-week cycles',
      'Zero-party data from the quiz pipes into Klaviyo segments',
    ],
    assets:['quiz','advertorial','listicle'],
    linkLabel:'All three bridges route into one offer ↓',
  },
  {
    id:'offer', cls:'offer', step:'03', lever:'AOV ↑',
    kicker:'THE OFFER · WHERE THE MONEY IS MADE',
    integration:'shopify',
    title:'The Offer — standalone page every bridge lands on',
    sub:'This is where acquisition turns into revenue. The $19 starter pack is your anchor — but the AOV play is the stack: build-your-own bundle → free-ship threshold → subscription upsell → post-purchase bump. Split-tested continuously. Every bridge\'s traffic lands here with its context pre-loaded, so the page personalizes itself (quiz-taker sees their flavor; advertorial-reader sees the matching story beat).',
    automation:[
      'Auto-personalizes based on which bridge sent the visitor',
      'Shopify checkout + native upsell on the thank-you page',
      'A/B on price, urgency, bundle size — winners survive',
      'Post-purchase drops into the Klaviyo upsell flow',
    ],
    assets:['offer'],
    linkLabel:'Buyer becomes a customer — we keep them forever →',
  },
  {
    id:'retain', cls:'retain', step:'04', lever:'LTV ↑',
    kicker:'FLOWS + CAMPAIGNS · KLAVIYO / OMNISEND',
    integration:'klaviyo',
    title:'Flows — full sequences pushed live into your ESP',
    sub:'Not three emails — <b>three complete flows</b>, wired directly into your Klaviyo or Omnisend account. Abandoned cart recovers revenue you already earned. Post-purchase upsell doubles AOV on day 3. 180-day winback revives your dead list. And it goes further: we auto-schedule weekly campaigns, newsletters, and even full <b>6–12 month educational drip sequences</b> for subscribers.',
    automation:[
      'Full flows (not one-offs) published directly to Klaviyo / Omnisend',
      'Weekly campaigns + newsletters on autopilot',
      '6–12 month educational sequences for the email list',
      'Every send is segmented by zero-party data from the quiz',
    ],
    assets:['email-1','email-2','email-3'],
    linkLabel:'And the whole funnel gets a bonus asset that lifts all 3 levers →',
  },
  {
    id:'compound', cls:'compound', step:'05', lever:'BONUS · all 3 levers',
    kicker:'DIGITAL PRODUCT · 3-WAY UTILITY',
    integration:'shopify',
    title:'Bonus — a digital product that moves every lever',
    sub:'A 42-page branded eBook auto-published to Shopify as its own product. Works <b>three ways</b>: (1) free lead magnet that drops your CPA, (2) value-booster bundled into the $19 offer that lifts AOV, (3) standalone $27 product you can sell as an upsell for pure-margin LTV.',
    automation:[
      'Auto-hosted on Shopify as a digital product + downloadable',
      'Toggle between lead-magnet · bundle · standalone in one click',
      'Every month: new chapters added from latest reviews + FAQs',
      'Delivered via Klaviyo post-optin or post-purchase',
    ],
    assets:['ebook'],
  },
];

function FunnelSections({ onFocus, payload }) {
  return (
    <div className="funnel">
      {FUNNEL.map((sec, si) => (
        <React.Fragment key={sec.id}>
          <section className={`fsec ${sec.cls}`}>
            <div className="fsec-head">
              <div className="fsec-left">
                <div className="fsec-step">{sec.step}</div>
                <div className="fsec-meta">
                  <div className="fsec-kicker"><span className="dot"/>{sec.kicker}</div>
                  <h2 className="fsec-title">{sec.title}</h2>
                  <p className="fsec-sub" dangerouslySetInnerHTML={{__html:sec.sub}}/>
                </div>
              </div>
              <div className="fsec-lever">
                <div className="lever-lbl">Lever</div>
                <div className="lever-val">{sec.lever}</div>
                <div className="lever-count">{sec.assets.length} asset{sec.assets.length>1?'s':''} · live</div>
              </div>
            </div>

            {sec.automation && (
              <div className="fsec-auto">
                <div className="fsec-auto-head">
                  <IntegrationBadge kind={sec.integration}/>
                  <div className="fsec-auto-title">Fully automated · hands-free</div>
                </div>
                <ul className="fsec-auto-list">
                  {sec.automation.map((line,i) => (
                    <li key={i} dangerouslySetInnerHTML={{__html:line}}/>
                  ))}
                </ul>
              </div>
            )}

            {sec.batches ? (
              sec.batches.map((b, bi) => (
                <div key={b.label} className="batch-group">
                  <div className="batch-head">
                    <div className="batch-bar"/>
                    <div className="batch-meta">
                      <div className="batch-label">{b.label}</div>
                      <div className="batch-sub">{b.sub}</div>
                    </div>
                    <div className="batch-count">{b.ids.length} creatives</div>
                  </div>
                  <div className="grid grid-5">
                    {b.ids.map((aid, i) => {
                      const a = ASSETS.find(x => x.id === aid);
                      if (!a) return null;
                      return (
                        <div key={a.id} className={`tile tile-ad`} style={{minHeight: 340, animation: `screenIn .5s var(--ease) ${(si*.08)+(bi*.15)+(i*.04)}s both`}} onClick={()=>onFocus(a.id)}>
                          <div className="tile-flip">
                            <div className="tile-face">
                              <div className="tile-tag">{a.cat}</div>
                              <div className="flip-hint">hover</div>
                              <TilePreview id={a.id} payload={payload}/>
                              <div className="tile-label">
                                <div>
                                  <h3>{a.title}</h3>
                                  <p>{a.sub}</p>
                                </div>
                                <div className="open">↗</div>
                              </div>
                            </div>
                            <div className="tile-face tile-back">
                              <div className="tile-tag" style={{background:'rgba(var(--coral-rgb),.15)',borderColor:'rgba(var(--coral-rgb),.3)',color:'var(--coral)',position:'static',alignSelf:'flex-start',marginBottom:10}}>WHY IT WORKS</div>
                              <h4>{a.title}</h4>
                              <ul>
                                {a.strategy.map((s,j) => <li key={j} dangerouslySetInnerHTML={{__html:s}}/>)}
                              </ul>
                              <div className="tile-back-kpi">
                                {a.kpi.map(([k,v],j) => <span key={j}>{k} <b>{v}</b></span>)}
                              </div>
                            </div>
                          </div>
                        </div>
                      );
                    })}
                  </div>
                </div>
              ))
            ) : (
              <div className="grid">
                {sec.assets.map((aid, i) => {
                  const a = ASSETS.find(x => x.id === aid);
                  if (!a) return null;
                  return (
                    <div key={a.id} className={`tile ${a.size}`} style={{minHeight: a.size==='size-sm'?260:340, animation: `screenIn .5s var(--ease) ${(si*.08)+(i*.05)}s both`}} onClick={()=>onFocus(a.id)}>
                      <div className="tile-flip">
                        <div className="tile-face">
                          <div className="tile-tag">{a.cat}</div>
                          <div className="flip-hint">hover</div>
                          <TilePreview id={a.id} payload={payload}/>
                          <div className="tile-label">
                            <div>
                              <h3>{a.title}</h3>
                              <p>{a.sub}</p>
                            </div>
                            <div className="open">↗</div>
                          </div>
                        </div>
                        <div className="tile-face tile-back">
                          <div className="tile-tag" style={{background:'rgba(var(--coral-rgb),.15)',borderColor:'rgba(var(--coral-rgb),.3)',color:'var(--coral)',position:'static',alignSelf:'flex-start',marginBottom:10}}>WHY IT WORKS</div>
                          <h4>{a.title}</h4>
                          <ul>
                            {a.strategy.map((s,j) => <li key={j} dangerouslySetInnerHTML={{__html:s}}/>)}
                          </ul>
                          <div className="tile-back-kpi">
                            {a.kpi.map(([k,v],j) => <span key={j}>{k} <b>{v}</b></span>)}
                          </div>
                        </div>
                      </div>
                    </div>
                  );
                })}
              </div>
            )}
          </section>
          {sec.linkLabel && si < FUNNEL.length - 1 && (
            <div className="fsec-link">
              <div className="line"/>
              <div className="arrow">{sec.linkLabel}</div>
              <div className="line"/>
            </div>
          )}
        </React.Fragment>
      ))}
    </div>
  );
}

// ============== DASHBOARD ==============
function Dashboard({ onFocus, onFinish, payload, submittedUrl }) {
  const store = payload && payload.store;
  const brandName = (store && store.name) || 'your brand';
  // Count what the Worker has actually produced so far.
  const a = (payload && payload.assets) || {};
  const readyCount = [
    ...(((payload && payload.new_batch && payload.new_batch.ads) || []).filter(Boolean)),
    a.quiz, a.advertorial, a.listicle, a.offer_lp, a.digital_product,
    ...(Array.isArray(a.emails) ? a.emails.filter(Boolean) : []),
  ].filter(Boolean).length;
  return (
    <div className="screen" style={{alignItems:'flex-start',paddingTop:108}}>
      <div className="dash-wrap">
        <div className="dash-hero">
          <div>
            <h1>Your whole funnel, <span className="italic-serif">live.</span></h1>
            <p className="dash-sub">A real acquisition-and-retention machine for <b style={{color:'var(--ink)'}}>{brandName}</b>. 10 ads across 2 batches feed 3 bridges. Bridges feed one offer. The offer feeds 3 email flows. And a bonus digital product lifts all three. Every piece improves itself forever.</p>
          </div>
          <div className="dash-stats">
            <div className="stat teal"><div className="label">Live assets</div><div className="val">{readyCount || '…'}</div></div>
            <div className="stat coral"><div className="label">Total in plan</div><div className="val">18</div></div>
            <div className="stat"><div className="label">Agency quote</div><div className="val">$18,480</div></div>
          </div>
        </div>

        <LeverFraming/>

        <FunnelSections onFocus={onFocus} payload={payload}/>

        <SplitTestExplainer/>

        <div style={{display:'flex',justifyContent:'center',margin:'56px 0 24px'}}>
          <button className="btn btn-primary" onClick={onFinish} style={{padding:'22px 36px',fontSize:17}}>
            See what this would cost you — and how to get it forever  →
          </button>
        </div>
      </div>
    </div>
  );
}

// ============== FOCUS OVERLAY ==============
// Map an overlay tile id → the generator's asset-type name. These flow
// through `UI_TO_WORKER` in generator.js to the Worker kind.
const GEN_ASSET_TYPE = {
  offer: 'offer-page',
  quiz: 'quiz',
  advertorial: 'advertorial',
  listicle: 'listicle',
  'email-1': 'email-cart',
  'email-2': 'email-upsell',
  'email-3': 'email-winback',
  ebook: 'ebook',
};

// Pull the already-generated HTML for a given overlay-id out of the live
// payload, if the Worker has produced it. Returns null otherwise.
function liveHtmlFor(payload, id) {
  if (!payload || !payload.assets) return null;
  const a = payload.assets;
  if (id === 'offer') return a.offer_lp?.html || null;
  if (id === 'quiz') return a.quiz?.html || null;
  if (id === 'advertorial') return a.advertorial?.html || null;
  if (id === 'listicle') return a.listicle?.html || null;
  if (id === 'ebook') return a.digital_product?.html || null;
  if (id === 'email-1' || id === 'email-2' || id === 'email-3') {
    // Worker ships emails in [upsell, cart, winback] order but the UI
    // labels them [cart, upsell, winback] — map by role so each tile
    // shows the email it advertises.
    const roleFor = { 'email-1':'cart_recovery', 'email-2':'post_purchase_upsell', 'email-3':'non_converter_winback' };
    const role = roleFor[id];
    const e = Array.isArray(a.emails) ? a.emails.find((em) => em && em.role === role) : null;
    return wrapEmail(e);
  }
  return null;
}

// Emails arrive as { subject, preview_text, from_name, body_html } from the
// Worker — wrap in a minimal gmail-ish shell for the iframe.
function wrapEmail(e) {
  if (!e || !e.body_html) return null;
  return `<!doctype html><html><head><meta charset="utf-8"><title>${e.subject || ''}</title><style>body{margin:0;font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Helvetica,Arial,sans-serif;background:#f6f8fc;color:#202124}.mail{max-width:680px;margin:0 auto;background:#fff;border:1px solid #dadce0;border-radius:8px;overflow:hidden}.mail-h{padding:20px 24px;border-bottom:1px solid #e8eaed}.mail-sub{font-size:22px;font-weight:500;line-height:1.3;margin:0 0 6px}.mail-f{font-size:13px;color:#5f6368}.mail-body{padding:0}</style></head><body><div class="mail"><div class="mail-h"><div class="mail-sub">${e.subject || '(no subject)'}</div><div class="mail-f">${e.from_name || 'Your Brand'} &lt;hi@yourbrand.com&gt;</div></div><div class="mail-body">${e.body_html}</div></div></body></html>`;
}

function FocusOverlay({ id, onClose, previewId, payload }) {
  useEffect(() => {
    document.body.classList.add('no-scroll');
    return () => document.body.classList.remove('no-scroll');
  }, []);
  useEffect(() => {
    const onKey = (e) => {
      if (e.key === 'Escape') onClose();
      if (e.key === 'ArrowRight') nav(1);
      if (e.key === 'ArrowLeft') nav(-1);
    };
    window.addEventListener('keydown', onKey);
    return () => window.removeEventListener('keydown', onKey);
  });
  const nav = (d) => {
    const idx = ASSET_ORDER.indexOf(id);
    const next = ASSET_ORDER[(idx + d + ASSET_ORDER.length) % ASSET_ORDER.length];
    window.__setFocus && window.__setFocus(next);
  };
  const a = ASSETS.find(x => x.id === id);

  // Live state: regen results keyed by asset-id (user may regenerate).
  const [liveByAsset, setLiveByAsset] = useState({});
  const [genStatus, setGenStatus] = useState({ phase: 'idle' });
  const blobUrlsRef = useRef([]);

  useEffect(() => () => {
    blobUrlsRef.current.forEach((u) => URL.revokeObjectURL(u));
  }, []);

  const genAssetType = GEN_ASSET_TYPE[id];

  // If the Worker already produced HTML for this asset, render it by
  // default (unless the user has regenerated locally).
  const payloadHtml = useMemo(() => liveHtmlFor(payload, id), [payload, id]);
  const payloadSrc = useMemo(() => {
    if (!payloadHtml) return null;
    try { return URL.createObjectURL(new Blob([payloadHtml], { type: 'text/html' })); }
    catch { return null; }
  }, [payloadHtml]);
  useEffect(() => () => { if (payloadSrc) URL.revokeObjectURL(payloadSrc); }, [payloadSrc]);

  const live = liveByAsset[id] || (payloadSrc ? { src: payloadSrc, meta: { model: 'claude-opus-4-7', fromWorker: true } } : null);

  const brandId = (payload && payload.store && payload.store.name) || 'your brand';

  const regenerate = async ({ forceRegen = true } = {}) => {
    if (!genAssetType || !window.Odyssey || !window.Odyssey.generateAsset) {
      setGenStatus({ phase: 'error', error: 'Odyssey generator not loaded' });
      return;
    }
    if (!previewId) {
      setGenStatus({ phase: 'error', error: 'No previewId — submit a URL first' });
      return;
    }
    try {
      const t0 = performance.now();
      setGenStatus({ phase: 'loading' });
      const result = await window.Odyssey.generateAsset({
        previewId,
        assetType: genAssetType,
        forceRegen,
        onStatus: (s) => setGenStatus(s),
      });
      const src = window.Odyssey.htmlToBlobUrl(result.html);
      blobUrlsRef.current.push(src);
      setLiveByAsset((prev) => ({ ...prev, [id]: { src, meta: result } }));
      setGenStatus({ phase: 'done', elapsedMs: Math.round(performance.now() - t0), model: result.model });
    } catch (err) {
      console.error('[regenerate]', err);
      setGenStatus({ phase: 'error', error: err && err.message ? err.message : String(err) });
    }
  };

  if (!a) return null;

  const renderFull = () => {
    const wrap = (child) => <div style={{padding:28,height:'100%',display:'flex',alignItems:'center',justifyContent:'center',background:'linear-gradient(180deg,#1a1b22,#0a0b12)'}}>{child}</div>;
    const fullHTML = (src, {w=1280, h=3200, url='olipop.com', phone=false}={}) => (
      <div style={{padding:phone?24:16,height:'100%',width:'100%',display:'flex',alignItems:phone?'center':'stretch',justifyContent:'center',background:'linear-gradient(180deg,#16171e,#0a0b12)',minHeight:0,overflow:'hidden'}}>
        <div style={{width:phone?420:'100%',maxWidth:phone?420:1280,height:'100%',display:'flex',minHeight:0}}>
          <HTMLPreview src={src} mode="full" designWidth={w} designHeight={h} url={url}/>
        </div>
      </div>
    );
    // Ads: show the live product-locked GPT Image 2 creative as a clean 1:1
    // square. No phone chrome, no fake username, no like-comment-share — this
    // is what lands in Meta Ads Manager.
    const adSquare = () => {
      const ads = (payload && payload.new_batch && payload.new_batch.ads) || [];
      const idxMap = {
        'ad-a1':0, 'ad-a2':1, 'ad-a3':2, 'ad-a4':3, 'ad-a5':4,
        'ad-b1':5, 'ad-b2':6, 'ad-b3':7, 'ad-b4':8, 'ad-b5':9,
      };
      const i = idxMap[id];
      const ad = ads[i];
      const img = ad && (ad.generated_image_url || ad.image_url);
      return (
        <div style={{height:'100%',width:'100%',display:'flex',alignItems:'center',justifyContent:'center',background:'linear-gradient(180deg,#16171e,#0a0b12)',padding:32}}>
          <div style={{aspectRatio:'1 / 1',width:'min(88%, 820px)',borderRadius:20,overflow:'hidden',background:'#0a0b12',boxShadow:'0 40px 80px -20px rgba(0,0,0,.7), 0 12px 32px -12px rgba(0,0,0,.5)',display:'flex',alignItems:'center',justifyContent:'center'}}>
            {img
              ? <img src={img} alt="Generated ad creative" style={{width:'100%',height:'100%',objectFit:'cover',display:'block'}}/>
              : <div style={{color:'var(--ink-3)',fontSize:13,letterSpacing:'.14em',textTransform:'uppercase'}}>Generating ad…</div>}
          </div>
        </div>
      );
    };
    switch (id) {
      case 'ad-a1': case 'ad-a2': case 'ad-a3': case 'ad-a4': case 'ad-a5':
      case 'ad-b1': case 'ad-b2': case 'ad-b3': case 'ad-b4': case 'ad-b5':
        return adSquare();
      case 'quiz': return fullHTML(live?.src || 'assets/quiz-landing.html', {w:1280, h:1800, url:'your-store.com/quiz'});
      case 'advertorial': return fullHTML(live?.src || 'assets/advertorial.html', {w:900, h:3600, url:'healthdesk-weekly.com/article'});
      case 'listicle': return fullHTML(live?.src || 'assets/listicle.html', {w:900, h:4200, url:'theeatguide.com/best-in-category'});
      case 'offer': return fullHTML(live?.src || 'assets/offer-page.html', {w:1280, h:3400, url:'your-store.com/starter'});
      case 'email-1': return fullHTML(live?.src || 'assets/email-cart.html', {w:680, h:1800, url:'mail.google.com'});
      case 'email-2': return fullHTML(live?.src || 'assets/email-upsell.html', {w:680, h:1800, url:'mail.google.com'});
      case 'email-3': return fullHTML(live?.src || 'assets/email-winback.html', {w:680, h:1800, url:'mail.google.com'});
      case 'ebook': return fullHTML(live?.src || 'assets/ebook.html', {w:1200, h:1600, url:'your-store.com/ebook'});
      default: return null;
    }
  };

  return (
    <div className="overlay" onClick={(e)=>{if(e.target===e.currentTarget) onClose();}}>
      <div className="overlay-head">
        <div className="overlay-title">
          <div className="num">{String(ASSET_ORDER.indexOf(id)+1).padStart(2,'0')}</div>
          <div>
            <div style={{fontSize:10,letterSpacing:'.16em',textTransform:'uppercase',color:'var(--coral)',marginBottom:4}}>{a.cat}</div>
            <h2>{a.title}</h2>
          </div>
        </div>
        <div style={{display:'flex',gap:10,alignItems:'center'}}>
          <button className="x-btn" onClick={()=>nav(-1)} title="Previous (←)">‹</button>
          <button className="x-btn" onClick={()=>nav(1)} title="Next (→)">›</button>
          <button className="x-btn" onClick={onClose} title="Close (Esc)">✕</button>
        </div>
      </div>
      <div className="overlay-body">
        <div className="overlay-canvas">{renderFull()}</div>
        <div className="overlay-side">
          <div className="overlay-card">
            <h4>Why this works</h4>
            <ul>
              {a.strategy.map((s,i) => <li key={i} dangerouslySetInnerHTML={{__html:s.replace(/<strong>/g,'<b style="color:var(--coral);font-weight:500">').replace(/<\/strong>/g,'</b>')}}/>)}
            </ul>
          </div>
          <div className="overlay-card">
            <h4>Performance targets</h4>
            <div style={{display:'grid',gridTemplateColumns:'1fr 1fr',gap:10}}>
              {a.kpi.map(([k,v],i) => (
                <div key={i} style={{padding:'12px 14px',background:'var(--panel-2)',borderRadius:10,border:'1px solid var(--line)'}}>
                  <div style={{fontSize:10,letterSpacing:'.12em',textTransform:'uppercase',color:'var(--ink-3)',marginBottom:4}}>{k}</div>
                  <div style={{fontFamily:'Instrument Serif,serif',fontSize:22,color:'var(--ink)'}}>{v}</div>
                </div>
              ))}
            </div>
          </div>
          {genAssetType && (
            <div className="overlay-card" style={{background:'linear-gradient(180deg,rgba(0,229,204,.09),rgba(0,229,204,.02))',borderColor:'rgba(0,229,204,.25)'}}>
              <h4 style={{color:'var(--teal)'}}>✦ Regenerate with Opus 4.7</h4>
              <p style={{fontSize:13,lineHeight:1.55}}>
                Fresh HTML, live from <code style={{fontFamily:'JetBrains Mono,monospace',fontSize:11,color:'var(--teal)'}}>claude-opus-4-7</code>, composed from the {brandId} BrandKit + this asset's brief + our hand-built reference. Briefs, not templates.
              </p>
              <div style={{marginTop:14,display:'flex',gap:8,flexWrap:'wrap',alignItems:'center'}}>
                <button
                  className="btn btn-primary"
                  style={{fontSize:13,padding:'10px 16px',background:'var(--teal)',color:'#041917',borderColor:'var(--teal)',opacity: genStatus.phase === 'loading' ? 0.55 : 1, cursor: genStatus.phase === 'loading' ? 'wait' : 'pointer'}}
                  onClick={() => regenerate({ forceRegen: true })}
                  disabled={genStatus.phase === 'loading'}
                >
                  {genStatus.phase === 'loading' ? 'Generating…' : (live ? 'Regenerate' : 'Generate now →')}
                </button>
                {live && (
                  <button
                    className="btn btn-ghost"
                    style={{fontSize:12,padding:'10px 12px'}}
                    onClick={() => {
                      // Revert to the static reference asset
                      setLiveByAsset((prev) => { const n = {...prev}; delete n[id]; return n; });
                      setGenStatus({ phase: 'idle' });
                    }}
                    title="Revert to the hand-built reference"
                  >
                    Revert
                  </button>
                )}
              </div>
              <div style={{marginTop:12,display:'flex',gap:6,flexWrap:'wrap',fontSize:10,letterSpacing:'.08em',textTransform:'uppercase'}}>
                <span style={{padding:'4px 8px',borderRadius:100,background:'rgba(255,255,255,.04)',border:'1px solid var(--line)',color:'var(--ink-3)'}}>
                  Model · <b style={{color:'var(--teal)',fontWeight:600}}>Opus 4.7</b>
                </span>
                <span style={{padding:'4px 8px',borderRadius:100,background:'rgba(255,255,255,.04)',border:'1px solid var(--line)',color:'var(--ink-3)'}}>
                  Brand · <b style={{color:'var(--ink-2)',fontWeight:600}}>{brandId}</b>
                </span>
                {live && (
                  <span style={{padding:'4px 8px',borderRadius:100,background:'rgba(0,229,204,.08)',border:'1px solid rgba(0,229,204,.25)',color:'var(--teal)'}}>
                    Live · {((live.meta.elapsedMs || 0) / 1000).toFixed(1)}s{live.meta.cached ? ' · cached' : ''}
                  </span>
                )}
                {genStatus.phase === 'loading' && (
                  <span style={{padding:'4px 8px',borderRadius:100,background:'rgba(255,255,255,.04)',border:'1px solid var(--line)',color:'var(--ink-3)'}}>
                    {genStatus.phase === 'loading' && genStatus.promptChars
                      ? `Prompting · ${(genStatus.promptChars / 1000).toFixed(1)}k chars`
                      : 'Loading brief + brand + example…'}
                  </span>
                )}
              </div>
              {genStatus.phase === 'error' && (
                <div style={{marginTop:10,padding:'10px 12px',background:'rgba(220,70,70,.08)',border:'1px solid rgba(220,70,70,.25)',borderRadius:8,fontSize:12,color:'#ffb4b4'}}>
                  {genStatus.error} · Using the hand-built reference.
                </div>
              )}
            </div>
          )}
          <div className="overlay-card" style={{background:'linear-gradient(180deg,rgba(232,92,43,.10),rgba(232,92,43,.02))',borderColor:'rgba(232,92,43,.25)'}}>
            <h4 style={{color:'var(--coral)'}}>⚡ Install this forever</h4>
            <p>This asset is yours to download. But the system that made it — running unlimited, on your schedule, tuned to your catalog — is a 30-min install.</p>
            <div style={{marginTop:14,display:'flex',gap:8}}>
              <button className="btn btn-primary" style={{fontSize:14,padding:'12px 18px'}} onClick={onClose}>Book the call →</button>
              <button className="btn btn-ghost" style={{fontSize:13}}>Download asset</button>
            </div>
          </div>
        </div>
      </div>
    </div>
  );
}

// ============== FINALE ==============
function Finale() {
  const stackItems = [
    { n:'01', name:'10 Meta ad creatives · 2 batches', sub:'5 cold + 5 warm · agency rate: $4,000 · 3 weeks', val:'$4,000' },
    { n:'02', name:'Quiz landing page', sub:'Custom build + CRO copy', val:'$2,400' },
    { n:'03', name:'Advertorial + Listicle pages', sub:'Long-form · SEO-ready', val:'$1,800' },
    { n:'04', name:'$19 starter-pack offer page', sub:'High-conversion template', val:'$2,200' },
    { n:'05', name:'3 revenue-recovery emails', sub:'Klaviyo-ready flows', val:'$1,800' },
    { n:'06', name:'Branded 42-page lead magnet', sub:'PDF · fully your brand', val:'$2,800' },
    { n:'07', name:'Brand intelligence dossier', sub:'Palette · persona · catalog audit', val:'$2,080' },
  ];
  const today = new Date();
  const firstOfMonth = new Date(today.getFullYear(), today.getMonth(), 1).getDay();
  const daysInMonth = new Date(today.getFullYear(), today.getMonth()+1, 0).getDate();
  const days = [];
  for (let i = 0; i < firstOfMonth; i++) days.push({ label: '', other: true });
  for (let d = 1; d <= daysInMonth; d++) {
    const isAvail = d >= today.getDate() && d <= today.getDate()+12 && ![0,6].includes(new Date(today.getFullYear(), today.getMonth(), d).getDay());
    days.push({ label: d, avail: isAvail, selected: d === today.getDate()+2 });
  }

  return (
    <div className="screen" style={{alignItems:'flex-start',paddingTop:88}}>
      <div className="finale">
        <div style={{textAlign:'center'}}>
          <div style={{fontSize:12,letterSpacing:'.2em',textTransform:'uppercase',color:'var(--coral)',marginBottom:22}}>✦ The pitch ✦</div>
          <h1>You didn't lift a finger.<br/><span className="italic-serif">This is your system.</span></h1>
          <p className="finale-sub">What we just built for Olipop in 13.2 seconds is what an agency would quote you $17,080 for — and deliver in 3 weeks. You're not here to buy these 18 assets. You're here to <b style={{color:'var(--ink)'}}>install the machine that makes this, unlimited, on your schedule</b>.</p>
        </div>

        <div className="stack">
          <div className="stack-head">
            <h3>What you just watched get built</h3>
            <div className="stack-tag">AGENCY EQUIVALENT COST</div>
          </div>
          <div className="stack-body">
            {stackItems.map((it,i) => (
              <div key={i} className="stack-item">
                <div className="n">{it.n}</div>
                <div className="name">{it.name}<small>{it.sub}</small></div>
                <div className="val">{it.val}</div>
              </div>
            ))}
          </div>
          <div className="stack-total">
            <div>
              <div className="label">If you paid an agency, once</div>
              <div style={{fontSize:13,color:'var(--ink-3)',marginTop:6}}>3 weeks. One campaign. Then you pay again for the next one.</div>
            </div>
            <div className="val">$17,080<small>Per drop · agency quote</small></div>
          </div>
        </div>

        <div className="pitch">
          <div className="pitch-left">
            <div style={{fontSize:11,letterSpacing:'.2em',textTransform:'uppercase',color:'var(--coral)'}}>THE ODYSSEY INSTALL</div>
            <h2>Unlimited, forever — for less than one agency drop. <span className="italic-serif">$10,497</span> flat.</h2>
            <p>30-minute install call. We wire Odyssey into your Shopify, Klaviyo, and Meta. Then you hit "go" on a new campaign whenever you want — ads, pages, emails, lead magnets — built in under a minute. Your brand. Your catalog. Unlimited.</p>
            <ul>
              <li>One-time fee · no monthly, no per-seat, no token metering</li>
              <li>Unlimited generations — every new SKU, every launch, every test</li>
              <li>Direct-to-Shopify + Klaviyo + Meta publishing</li>
              <li>Your team trained in the install call · Loom follow-up</li>
            </ul>
          </div>
          <div className="pitch-right">
            <div style={{fontSize:12,letterSpacing:'.14em',textTransform:'uppercase',color:'var(--ink-3)',marginBottom:4}}>Book a 30-min install call</div>
            <div style={{fontFamily:'Instrument Serif,serif',fontSize:22,color:'var(--ink)',marginBottom:2,lineHeight:1.1}}>This week only · <span style={{color:'var(--coral)'}}>3 slots</span></div>
            <div className="scarcity">
              <div><span className="scarce-val">3</span><div className="scarce-lbl">slots left · this week</div></div>
              <div><span className="scarce-val">12</span><div className="scarce-lbl">founders booked · last 7d</div></div>
              <div><span className="scarce-val">$10,497</span><div className="scarce-lbl">flat · one-time</div></div>
            </div>
            <div className="calendar">
              <div className="cal-head">
                <div className="month">April 2026</div>
                <div className="arr"><span>‹</span><span>›</span></div>
              </div>
              <div className="cal-grid">
                {['S','M','T','W','T','F','S'].map((d,i)=><div key={i} className="cal-dow">{d}</div>)}
                {days.map((d,i) => (
                  <div key={i} className={`cal-day ${d.other?'other':''} ${d.avail?'avail':''} ${d.selected?'selected':''}`}>{d.label}</div>
                ))}
              </div>
              <div style={{fontSize:11,color:'var(--ink-3)',marginTop:6,letterSpacing:'.04em'}}>Available slots · Friday {today.getDate()+2} April</div>
              <div className="cal-slots">
                <div className="cal-slot taken">9:00 AM</div>
                <div className="cal-slot">10:30 AM</div>
                <div className="cal-slot">1:00 PM</div>
                <div className="cal-slot">2:30 PM</div>
                <div className="cal-slot taken">4:00 PM</div>
                <div className="cal-slot">5:00 PM</div>
              </div>
            </div>
            <button className="btn btn-primary" style={{padding:'16px 22px',fontSize:15,marginTop:6}}>Book 1:00 PM Friday →</button>
            <div style={{fontSize:11,color:'var(--ink-3)',textAlign:'center'}}>30 minutes · no pitch deck · just the install</div>
          </div>
        </div>

        <div className="testimonials">
          <div className="test">
            <div className="test-stars">★★★★★</div>
            <div className="test-q">"We retired our agency the week after the install. Not kidding."</div>
            <div className="test-who"><span><b>Maya J.</b><br/>Founder · Tenure Skincare</span><span>$2.4M ARR</span></div>
          </div>
          <div className="test">
            <div className="test-stars">★★★★★</div>
            <div className="test-q">"Paid for itself in one Black Friday creative test. The ROI is stupid."</div>
            <div className="test-who"><span><b>David R.</b><br/>Head of Growth · Forma</span><span>$8.1M ARR</span></div>
          </div>
          <div className="test">
            <div className="test-stars">★★★★★</div>
            <div className="test-q">"I ship a new landing page every Tuesday now. Used to take 3 weeks."</div>
            <div className="test-who"><span><b>Rachel K.</b><br/>CMO · Nook Home</span><span>$5.7M ARR</span></div>
          </div>
        </div>

        <div className="foot">Odyssey · we install the system, the system does the rest</div>
      </div>
    </div>
  );
}

// ============== TOP BAR ==============
function TopBar({ stage, submittedUrl, onReset }) {
  const cleanDomain = (u) => String(u || '').replace(/^https?:\/\//i, '').replace(/\/$/, '');
  const domain = cleanDomain(submittedUrl) || 'drinkolipop.com';
  return (
    <div className="topbar">
      <div className="wordmark">
        <div className="wordmark-dot"/>
        Odyssey<span style={{color:'var(--ink-3)',fontWeight:400,marginLeft:6}}>· preview</span>
      </div>
      <div className="topbar-right">
        {stage !== 'entry' && (
          <div className="pill" title={onReset ? 'Click to start over with a different URL' : ''} onClick={onReset} style={{cursor: onReset ? 'pointer' : 'default'}}>
            <span className="pill-dot"/> {domain}
          </div>
        )}
        <a href="#" className="btn-ghost" style={{display:'inline-flex',alignItems:'center',gap:6,padding:'9px 14px',fontSize:13,textDecoration:'none',color:'var(--ink-2)',border:'1px solid var(--line-2)',borderRadius:10}}>Book a call →</a>
      </div>
    </div>
  );
}

// ============== APP ==============
function App() {
  const [stage, setStage] = useState(() => localStorage.getItem('odyssey-stage') || 'entry');
  const [focus, setFocus] = useState(null);
  // Live-build state. previewId comes back from POST /api/generate; payload
  // is polled from GET /api/preview/:id and holds the extracted store +
  // generated assets as they land.
  const [previewId, setPreviewId] = useState(() => {
    try { return localStorage.getItem('odyssey:live-preview-id') || null; } catch { return null; }
  });
  const [submittedUrl, setSubmittedUrl] = useState(() => localStorage.getItem('odyssey-submitted-url') || '');
  const [payload, setPayload] = useState(null);

  useEffect(() => { localStorage.setItem('odyssey-stage', stage); }, [stage]);
  useEffect(() => { window.__setFocus = setFocus; }, []);

  // Re-skin the UI to the customer's extracted brand color once their
  // palette is live. Overrides the Odyssey cyan default by setting CSS
  // custom properties inline on <body> — the rest of the UI (topbar pill,
  // stage-glow, buttons, stats, stream, dashboard accents, finale) already
  // reads --coral / --coral-rgb / --plum, so this cascades everywhere.
  useEffect(() => {
    const brandStages = new Set(['extracting','generating','dashboard','finale']);
    const b = document.body;
    const inStage = brandStages.has(stage);
    const primary = payload && payload.store && payload.store.brand_primary;
    const accent = payload && payload.store && payload.store.brand_accent;

    if (!inStage || !primary) {
      // Default back to the cyan Odyssey palette between builds / on Entry.
      b.style.removeProperty('--coral');
      b.style.removeProperty('--coral-rgb');
      b.style.removeProperty('--coral-ink');
      b.style.removeProperty('--plum');
      b.removeAttribute('data-brand');
      return;
    }

    // Parse hex → "r, g, b" for rgba() compositing.
    const hexToRgb = (hex) => {
      const m = /^#?([0-9a-f]{6})$/i.exec(String(hex || ''));
      if (!m) return null;
      const n = parseInt(m[1], 16);
      return `${(n >> 16) & 255}, ${(n >> 8) & 255}, ${n & 255}`;
    };
    // Pick readable ink color against the brand primary (white for dark
    // brands, near-black for light brands). Basic luminance test.
    const inkFor = (hex) => {
      const rgb = hexToRgb(hex);
      if (!rgb) return '#ffffff';
      const [r, g, bl] = rgb.split(', ').map(Number);
      // Rec. 709 luma
      const L = (0.2126 * r + 0.7152 * g + 0.0722 * bl) / 255;
      return L > 0.62 ? '#0A0A0E' : '#ffffff';
    };

    const rgb = hexToRgb(primary);
    if (rgb) {
      b.style.setProperty('--coral', primary);
      b.style.setProperty('--coral-rgb', rgb);
      b.style.setProperty('--coral-ink', inkFor(primary));
    }
    if (accent) b.style.setProperty('--plum', accent);
    // data-brand stays set (informational) so Olipop fixtures still win
    // when we explicitly switch to them.
    b.setAttribute('data-brand', (payload && payload.store && payload.store.brand_code) || 'live');
  }, [stage, payload]);

  // Kicked off from Entry. Hits the deployed Worker, gets a previewId, fans
  // out all asset generators, and advances to extracting. Polling starts
  // once previewId is set.
  const handleStart = async ({ url, email }) => {
    if (!window.Odyssey || !window.Odyssey.startLiveBuild) {
      throw new Error('Odyssey live-build API not loaded');
    }
    const { id } = await window.Odyssey.startLiveBuild({ url, email });
    setPreviewId(id);
    setSubmittedUrl(url);
    try { localStorage.setItem('odyssey-submitted-url', url); } catch {}
    setPayload(null);
    setStage('extracting');
  };

  // Poll the Worker payload every 2.5s while we have a previewId. Keeps
  // payload fresh for all downstream stages (Extraction reads .store,
  // Generation reads .assets + .new_batch.ads, FocusOverlay reads
  // .assets.<kind>.html for instant iframe preview).
  useEffect(() => {
    if (!previewId) return;
    if (!window.OdysseyAPI) return;
    let cancelled = false;
    let timer = null;
    const tick = async () => {
      try {
        const p = await window.OdysseyAPI.getPayload(previewId);
        if (!cancelled) setPayload(p);
      } catch (e) {
        // transient — keep polling
      } finally {
        if (!cancelled) timer = setTimeout(tick, 2500);
      }
    };
    tick();
    return () => { cancelled = true; if (timer) clearTimeout(timer); };
  }, [previewId]);

  // If a previewId was persisted in localStorage across a reload, auto-resume
  // by re-firing the fan-out so the Worker keeps producing (Worker ops are
  // idempotent per-kind).
  useEffect(() => {
    if (!previewId || !window.OdysseyAPI) return;
    try { window.OdysseyAPI.fanOut(previewId); } catch {}
  }, [previewId]);

  const resetBuild = () => {
    try { localStorage.removeItem('odyssey:live-preview-id'); } catch {}
    try { localStorage.removeItem('odyssey-submitted-url'); } catch {}
    setPreviewId(null);
    setSubmittedUrl('');
    setPayload(null);
    setFocus(null);
    setStage('entry');
  };

  return (
    <>
      <div className="stage-glow"/>
      <div className="grain"/>
      <TopBar stage={stage} submittedUrl={submittedUrl} onReset={resetBuild}/>
      <div className="page">
        {stage === 'entry' && <Entry onStart={handleStart}/>}
        {stage === 'extracting' && <Extraction onDone={()=>setStage('generating')} payload={payload} submittedUrl={submittedUrl}/>}
        {stage === 'generating' && <Generation onDone={()=>setStage('dashboard')} payload={payload} submittedUrl={submittedUrl}/>}
        {stage === 'dashboard' && <Dashboard onFocus={setFocus} onFinish={()=>setStage('finale')} payload={payload} submittedUrl={submittedUrl}/>}
        {stage === 'finale' && <Finale/>}
      </div>
      {focus && <FocusOverlay id={focus} onClose={()=>setFocus(null)} previewId={previewId} payload={payload}/>}
      <div className="debug">
        {['entry','extracting','generating','dashboard','finale'].map(s => (
          <button key={s} className={stage===s?'active':''} onClick={()=>{setFocus(null);setStage(s);}}>{s}</button>
        ))}
        <button onClick={resetBuild} title="Clear preview and start over">reset</button>
      </div>
    </>
  );
}

ReactDOM.createRoot(document.getElementById('app')).render(<App/>);
