// screens-preview.jsx — PreviewScreen + PaidPreviewScreen
//
// PreviewScreen sits between ProcessingScreen and SubmittedScreen. It polls
// dashboard.gravida.nl/api/scan/<sessionId>/preview every 5 seconds while
// Rodin is generating, then drops in Google's <model-viewer> web component
// once the GLB URL is available, plus a "Reserve €35" CTA that opens a Mollie
// checkout in a browser tab.
//
// PaidPreviewScreen is shown when the Mollie checkout returns successfully
// (the deposit endpoint sets a return URL of /?paid=GRV-XXXX which the app
// detects on load).

const { useState: useP, useEffect: useEP, useRef: useRP } = React;

const PREVIEW_POLL_MS = 5000;
const DEPOSIT_AMOUNT_EUR = 10;
const TEST_PHASE = false;

// Lazy-load Google's model-viewer web component the first time the screen
// mounts. The script tag registers a <model-viewer> custom element globally.
function ensureModelViewerLoaded() {
  if (typeof window === 'undefined') return;
  if (window.__modelViewerLoaded) return;
  const s = document.createElement('script');
  s.type   = 'module';
  s.src    = 'https://unpkg.com/@google/model-viewer@4.0.0/dist/model-viewer.min.js';
  s.async  = true;
  document.head.appendChild(s);
  window.__modelViewerLoaded = true;
}

function PreviewScreen({ ctx }) {
  const [reserving, setReserving] = useP(false);
  const [reserveErr, setReserveErr] = useP(null);
  const [notifAsked, setNotifAsked] = useP(false);
  const [codeOpen, setCodeOpen] = useP(false);
  const [codeValue, setCodeValue] = useP('');
  const [codeRedeeming, setCodeRedeeming] = useP(false);
  const [codeErr, setCodeErr] = useP(null);
  const modelViewerRef = useRP(null);

  // The sessionId was stashed by ProcessingScreen on success, on ctx.capture.
  const sessionId = ctx?.capture?.sessionId || null;
  const contact   = ctx?.capture?.contact || {};

  // App polls the preview status at the App level (so the poller survives
  // navigation to Atelier when the customer taps "Notify me & keep
  // browsing"). PreviewScreen just reads the latest from ctx.previewState.
  const state = ctx?.previewState || { state: sessionId ? 'loading' : 'unavailable' };

  useEP(() => { ensureModelViewerLoaded(); }, []);

  // Re-skin Rodin's neutral GLB with a single bronze PBR material so the
  // preview matches the brand finish (one of Atelier Gravida's signature
  // colour-ways, "Bronze-Look Resin"). The base GLB from Rodin ships with a
  // light-grey PBR; we wipe every texture and force a warm metallic bronze.
  // Re-runs whenever the GLB URL changes (i.e. a new scan finished).
  useEP(() => {
    const mv = modelViewerRef.current;
    if (!mv) return;
    const applyBronze = () => {
      try {
        const mats = mv.model && mv.model.materials;
        if (!mats || !mats.length) return;
        for (const m of mats) {
          // Warm bronze: brown-gold base, ~90% metallic, semi-polished.
          m.pbrMetallicRoughness.setBaseColorFactor([0.72, 0.50, 0.29, 1.0]);
          m.pbrMetallicRoughness.setMetallicFactor(0.9);
          m.pbrMetallicRoughness.setRoughnessFactor(0.35);
          // Strip every texture so the flat colour wins over Rodin's neutral
          // bake. Wrapped in try/catch because not every material has every
          // texture slot and model-viewer throws on null setters.
          try { m.pbrMetallicRoughness.baseColorTexture.setTexture(null); } catch {}
          try { m.pbrMetallicRoughness.metallicRoughnessTexture.setTexture(null); } catch {}
          try { m.normalTexture.setTexture(null); } catch {}
          try { m.emissiveTexture.setTexture(null); } catch {}
          try { m.occlusionTexture.setTexture(null); } catch {}
          try { m.setEmissiveFactor([0, 0, 0]); } catch {}
        }
      } catch (err) { console.warn('bronze override failed:', err); }
    };
    mv.addEventListener('load', applyBronze);
    // If a model is already loaded (component re-mount, src unchanged), apply now.
    if (mv.model) applyBronze();
    return () => mv.removeEventListener('load', applyBronze);
  }, [state.glb_url]);

  // 'unavailable' = backend skipped Rodin (no API key, or pre-feature scan).
  // Bounce the customer straight to the Atelier collection. useEP must live
  // at top-level (not behind a render-time `if`) so the hook order stays
  // stable across renders.
  useEP(() => {
    if (state.state === 'unavailable') ctx.go('atelier');
  }, [state.state]);

  // Tap "Notify me & keep browsing": request notification permission (so
  // App can ping when status flips to ready), then navigate to the Atelier
  // collection. The App-level poller keeps running in the background.
  const notifyAndBrowse = async () => {
    if (typeof Notification !== 'undefined' && Notification.permission === 'default') {
      try { await Notification.requestPermission(); } catch (e) {}
    }
    setNotifAsked(true);
    ctx.go('atelier');
  };

  const reserve = async () => {
    if (!sessionId) return;
    setReserveErr(null); setReserving(true);
    try {
      const cfg = (typeof window !== 'undefined' && window.GRAVIDA_API_CONFIG) || {};
      const base  = (cfg.base  || '').replace(/\/$/, '');
      const token = cfg.token || '';
      const res = await fetch(`${base}/api/scan/${encodeURIComponent(sessionId)}/deposit`, {
        method: 'POST',
        headers: {
          'Content-Type': 'application/json',
          ...(token ? { 'X-Scan-App-Token': token } : {}),
        },
        body: JSON.stringify({ email: contact.email, first_name: contact.first_name }),
      });
      const data = await res.json();
      if (!res.ok || !data.checkout_url) throw new Error(data.error || 'Could not start payment');
      window.location.href = data.checkout_url;
    } catch (err) {
      setReserveErr((err && err.message) || 'Payment could not start');
      setReserving(false);
    }
  };

  const redeemCode = async () => {
    const code = codeValue.trim().toUpperCase();
    if (!code) { setCodeErr('Vul een code in'); return; }
    if (!sessionId) return;
    setCodeErr(null); setCodeRedeeming(true);
    try {
      const cfg = (typeof window !== 'undefined' && window.GRAVIDA_API_CONFIG) || {};
      const base  = (cfg.base  || '').replace(/\/$/, '');
      const token = cfg.token || '';
      const res = await fetch(`${base}/api/scan/${encodeURIComponent(sessionId)}/redeem-code`, {
        method: 'POST',
        headers: {
          'Content-Type': 'application/json',
          ...(token ? { 'X-Scan-App-Token': token } : {}),
        },
        body: JSON.stringify({ code }),
      });
      const data = await res.json().catch(() => ({}));
      if (!res.ok) throw new Error(data.error || 'Code kon niet worden ingewisseld');
      // ok: true or ok: true + already: true — both mean we can continue
      ctx.go('paid');
    } catch (err) {
      setCodeErr((err && err.message) || 'Code kon niet worden ingewisseld');
      setCodeRedeeming(false);
    }
  };

  // ── Render variants ──
  if (state.state === 'unavailable') {
    // The useEffect above kicked off ctx.go('atelier'); render nothing
    // while the navigation takes effect.
    return null;
  }

  if (state.state === 'failed') {
    return (
      <div className="scr scr--paper">
        <TopBar onBack={() => ctx.back()} onHelp={ctx.onHelp} />
        <div className="pad flex1 col center" style={{ textAlign: 'center' }}>
          <div className="center" style={{ width: 76, height: 76, borderRadius: 999, background: 'color-mix(in oklch, var(--rose), transparent 85%)', color: 'var(--rose-deep)', marginBottom: 20 }}>
            <IcRetake size={32} />
          </div>
          <span className="eyebrow">A small detour</span>
          <h1 className="title" style={{ marginTop: 8 }}>The preview will take a bit longer.</h1>
          <p className="lede" style={{ marginTop: 12 }}>
            Our sculptors will pick this one up by hand. You'll get an email
            within a day with your first look. In the meantime, have a wander
            through the Collection.
          </p>
        </div>
        <div className="pad" style={{ paddingTop: 0 }}>
          <button className="btn btn--accent" onClick={() => ctx.go('atelier')}>
            See the Collection <IcArrow size={18} />
          </button>
        </div>
      </div>
    );
  }

  if (state.state !== 'ready') {
    // queued / generating / loading.
    // Replicate Hunyuan3D currently runs ~3 min wall-clock on average. We
    // tell the customer that upfront so 3 min does not feel like the app
    // crashed, and we surface a stage label + progress bar that moves
    // throughout, so they can SEE something is happening.
    const seconds       = state.elapsed_seconds ?? 0;
    const expectedTotal = 200;                                   // ~3:20, slightly longer than average so we don't end up at 100% and stuck
    const progress      = Math.min(0.97, seconds / expectedTotal);
    const stage = (() => {
      if (state.state === 'queued')        return 'In the queue, picking up your photos';
      if (seconds < 25)                    return 'Looking at your photos';
      if (seconds < 55)                    return 'Removing the background';
      if (seconds < 130)                   return 'Sculpting your form';
      if (seconds < 200)                   return 'Adding the finishing touches';
      return 'Almost ready, just a moment';
    })();
    const mmss = (s) => `${Math.floor(s / 60)}:${String(Math.max(0, Math.floor(s % 60))).padStart(2, '0')}`;

    return (
      <div className="scr scr--paper">
        <TopBar onHelp={ctx.onHelp} />
        <div className="pad flex1 col" style={{ justifyContent: 'space-between' }}>
          <div className="grow center col">
            <div style={{ position: 'relative', width: 132, height: 132, marginBottom: 26 }}>
              {[0, 1].map((i) => (
                <div key={i} style={{ position: 'absolute', inset: 0, borderRadius: 999, border: '2px solid var(--gold-soft)', animation: `pulsering 2.4s ease-out ${i * 1.2}s infinite` }} />
              ))}
              <div className="center" style={{ position: 'absolute', inset: 0, borderRadius: 999, background: 'linear-gradient(135deg, var(--gold), var(--rose))', color: '#fff' }}>
                <IcSparkle size={44} />
              </div>
            </div>
            <span className="eyebrow">Sculpting your form</span>
            <h1 className="title" style={{ textAlign: 'center', marginTop: 8 }}>
              A first glimpse, almost ready.
            </h1>
            <p className="lede" style={{ textAlign: 'center', marginTop: 12, maxWidth: 320 }}>
              This usually takes about three minutes. You can keep this open
              or come back later, we will remember where you were.
            </p>

            {/* Stage label, changes every 25-75 seconds so the customer sees
                progress even when the numeric counter would otherwise feel
                static. */}
            <p style={{ fontSize: 13.5, fontWeight: 600, color: 'var(--gold)', marginTop: 18, textAlign: 'center' }}>
              {stage}...
            </p>

            {/* Soft progress bar. Caps at 97% so it never feels "done" until
                the actual mesh is ready. */}
            <div style={{ width: 240, maxWidth: '78%', height: 6, borderRadius: 999, background: 'var(--hairline)', overflow: 'hidden', marginTop: 10 }}>
              <div style={{
                width: `${Math.round(progress * 100)}%`, height: '100%',
                background: 'linear-gradient(90deg, var(--rose), var(--gold))',
                transition: 'width .8s ease',
              }} />
            </div>

            <p style={{ fontSize: 12.5, color: 'var(--ink-mute)', marginTop: 8, fontFamily: 'ui-monospace, Menlo, monospace' }}>
              {mmss(seconds)} of about {mmss(expectedTotal)}
            </p>
          </div>
          <button className="btn btn--ghost" onClick={notifyAndBrowse}>
            {notifAsked ? 'Browsing the Collection while we sculpt' : 'Notify me & keep browsing'}
          </button>
        </div>
      </div>
    );
  }

  // state.state === 'ready'
  return (
    <div className="scr scr--paper">
      <TopBar onBack={() => ctx.back()} onHelp={ctx.onHelp} />
      <div style={{ flex: 1, minHeight: 0, overflowY: 'auto' }}>
        <div className="pad" style={{ paddingBottom: 10 }}>
          <span className="eyebrow">Your first look</span>
          <h1 className="title" style={{ marginTop: 8 }}>A rough AI sketch of your form.</h1>
          <p className="lede" style={{ fontSize: 14.5, marginTop: 10 }}>
            Read the note below before you rotate. This is the AI's raw first pass,
            not a preview of the final sculpture.
          </p>
        </div>

        {/* Prominent pose disclaimer — shown BEFORE the viewer so the customer
            reads it before seeing a potentially odd-looking model. The AI sketch
            tier (Rodin Sketch) often produces wide stances, shifted weight, or
            a generic face. None of this carries through to the final piece. */}
        <div className="pad" style={{ paddingTop: 0, paddingBottom: 12 }}>
          <div style={{
            borderRadius: 14, padding: '14px 16px',
            background: 'color-mix(in oklch, var(--gold), transparent 86%)',
            border: '1px solid color-mix(in oklch, var(--gold), transparent 60%)',
            display: 'flex', gap: 11,
          }}>
            <span style={{ color: 'var(--gold-deep, #b08030)', flex: '0 0 auto', marginTop: 2 }}><IcLeaf size={20} /></span>
            <div>
              <p style={{ margin: '0 0 5px', fontSize: 13.5, fontWeight: 700, color: 'var(--ink)' }}>
                This is not what your sculpture will look like.
              </p>
              <p style={{ margin: 0, fontSize: 13, lineHeight: 1.55, color: 'var(--ink-soft)' }}>
                The AI often places the feet too wide, shifts the weight to one side,
                or makes the face unrecognisable. The hands are rough and fine detail
                is missing. All of this is normal and none of it carries through to
                the final piece. Our sculptors correct the pose, refine every line,
                and work directly from your reference photos.
              </p>
            </div>
          </div>
        </div>

        {/* 3D viewer — metallic bronze finish applied in the load hook above. */}
        <div style={{ height: 380, margin: '0 16px', borderRadius: 18, overflow: 'hidden', background: 'radial-gradient(80% 70% at 50% 40%, #f4efe4 0%, #e9e3d6 100%)', border: '1px solid var(--hairline)' }}>
          {React.createElement('model-viewer', {
            ref: modelViewerRef,
            src: state.glb_url,
            alt: 'Your 3D preview, in bronze',
            'camera-controls': true,
            'auto-rotate': true,
            'auto-rotate-delay': '1500',
            'shadow-intensity': '1.1',
            'shadow-softness': '0.85',
            'environment-image': 'neutral',
            exposure: '0.95',
            'tone-mapping': 'aces',
            style: { width: '100%', height: '100%', backgroundColor: 'transparent' },
          })}
        </div>
        <p style={{ textAlign: 'center', fontSize: 12, color: 'var(--ink-mute)', margin: '8px 20px 0' }}>
          Rough AI sketch only. Drag to rotate.
        </p>

        {/* What the customer controls on the final preview. Specifically
            calls out the default smoothing under the belly so they know
            privacy is handled, and that they can override it. */}
        <div className="pad" style={{ paddingTop: 12, paddingBottom: 4 }}>
          <div className="card" style={{ background: 'color-mix(in oklch, var(--rose), transparent 88%)', borderColor: 'color-mix(in oklch, var(--rose), transparent 64%)' }}>
            <div style={{ display: 'flex', gap: 11 }}>
              <span style={{ color: 'var(--rose-deep)', flex: '0 0 auto', marginTop: 1 }}><IcEye size={19} /></span>
              <p style={{ margin: 0, fontSize: 13, lineHeight: 1.5, color: 'var(--ink-soft)' }}>
                <b>You decide what stays and what we soften.</b> By default we gently
                smooth the area below the belly so no intimate detail is shown,
                and any visible cellulite is minimised, unless you tell us
                otherwise.
              </p>
            </div>
          </div>
        </div>

        {/* How digital sculpting works — the 3-step ladder. Each step is its
            own payment in the live version; clearly hand-drawn, up to 14 days.
            The digital model is a standalone product the customer keeps. */}
        <div className="pad" style={{ paddingTop: 12, paddingBottom: 4 }}>
          <div className="card">
            <p style={{ margin: '0 0 4px', fontWeight: 700, fontSize: 15, color: 'var(--ink)' }}>
              How your digital sculpture works
            </p>
            <p style={{ margin: '0 0 14px', fontSize: 12.5, lineHeight: 1.5, color: 'var(--ink-mute)' }}>
              What you see now is the rough AI preview, made automatically from your photos.
              Turning it into a refined sculpture is two gentle steps.
            </p>

            {[
              ['1', 'Atelier preview', '€10',
               'We generate your model in high resolution. Marit personally reviews it and sends you a preview with our hand-finishing suggestions. You decide what stays and what we soften — no commitment yet.'],
              ['2', 'Your print-ready model', '€49',
               'Once you approve the edits, our sculptors refine every line by hand. You receive a print-ready file — yours to keep. Order a sculpture from our atelier any time, or print it yourself. (Use the credit code from step 1 for €10 off.)'],
            ].map(([n, title, price, body]) => (
              <div key={n} style={{ display: 'flex', gap: 11, marginBottom: 12 }}>
                <span className="center" style={{
                  width: 24, height: 24, flex: '0 0 auto', borderRadius: 999,
                  background: 'var(--ink)', color: 'var(--paper)', fontSize: 12.5, fontWeight: 700,
                }}>{n}</span>
                <div>
                  <div style={{ display: 'flex', alignItems: 'baseline', gap: 8 }}>
                    <b style={{ fontSize: 13.5, color: 'var(--ink)' }}>{title}</b>
                    <span className="serif-num" style={{ fontSize: 13.5, color: 'var(--gold)' }}>{price}</span>
                  </div>
                  <p style={{ margin: '2px 0 0', fontSize: 12.5, lineHeight: 1.5, color: 'var(--ink-soft)' }}>{body}</p>
                </div>
              </div>
            ))}

            <p style={{ margin: '4px 0 0', fontSize: 12.5, lineHeight: 1.5, color: 'var(--ink-soft)' }}>
              Your digital model is yours to keep. You can order a sculpture from our atelier
              whenever you like, now or years from now, in any of our finishes. Sculptures are
              crafted separately, see the Collection.
            </p>
          </div>
        </div>
      </div>

      {/* Sticky CTA. During the test phase the button registers interest only
          and never charges. In the live version this is step 1 (€35) of the
          digital sculpting ladder. */}
      <div style={{ flex: '0 0 auto', padding: '12px 20px 14px', borderTop: '1px solid var(--hairline)', background: 'var(--paper)' }}>
          <>
            <p style={{ margin: '0 0 10px', fontSize: 12.5, color: 'var(--ink-mute)', textAlign: 'center' }}>
              Get your atelier preview for <b style={{ color: 'var(--ink)' }}>€{DEPOSIT_AMOUNT_EUR}</b>.
              Includes our editing suggestions and a €10 credit towards the final model.
            </p>
            {codeOpen ? (
              <div style={{ marginBottom: 10 }}>
                <div style={{ display: 'flex', gap: 8 }}>
                  <input
                    type="text"
                    placeholder="Your code"
                    value={codeValue}
                    onChange={(e) => { setCodeValue(e.target.value.toUpperCase()); setCodeErr(null); }}
                    onKeyDown={(e) => e.key === 'Enter' && redeemCode()}
                    style={{
                      flex: 1, padding: '10px 12px', borderRadius: 8,
                      border: `1px solid ${codeErr ? 'var(--rose-deep)' : 'var(--hairline)'}`,
                      fontSize: 14, fontFamily: 'inherit', background: 'var(--paper)', color: 'var(--ink)',
                      outline: 'none',
                    }}
                  />
                  <button className="btn btn--accent" onClick={redeemCode} disabled={codeRedeeming}
                    style={{ flex: '0 0 auto', padding: '0 16px', opacity: codeRedeeming ? .6 : 1 }}>
                    {codeRedeeming ? '...' : 'Apply'}
                  </button>
                </div>
                {codeErr && <p style={{ margin: '6px 0 0', fontSize: 12.5, color: 'var(--rose-deep)' }}>{codeErr}</p>}
                <button onClick={() => { setCodeOpen(false); setCodeValue(''); setCodeErr(null); }}
                  style={{ all: 'unset', cursor: 'pointer', display: 'block', margin: '8px auto 0', fontSize: 12.5, color: 'var(--ink-mute)' }}>
                  Cancel
                </button>
              </div>
            ) : (
              <button className="linkbtn" style={{ display: 'block', margin: '0 auto 8px' }} onClick={() => setCodeOpen(true)}>
                Have a code?
              </button>
            )}
            {reserveErr && (
              <p style={{ margin: '0 0 8px', fontSize: 12.5, color: 'var(--rose-deep)', textAlign: 'center' }}>{reserveErr}</p>
            )}
            <button className="btn btn--accent" disabled={reserving}
              onClick={reserve}
              style={reserving ? { opacity: .6, pointerEvents: 'none' } : null}>
              {reserving ? 'Opening checkout...' : `Get your preview — €${DEPOSIT_AMOUNT_EUR}`}
              <IcArrow size={18} />
            </button>
            <button className="linkbtn" style={{ display: 'block', margin: '10px auto 0' }} onClick={() => ctx.go('atelier')}>
              Just browse the Collection first
            </button>
          </>
      </div>
    </div>
  );
}

function PaidPreviewScreen({ ctx }) {
  // The app boots into this screen with `?paid=pending&session=<uuid>` after
  // Mollie redirects back. The Mollie webhook usually lands within 2-5 seconds,
  // so we poll the deposit-status endpoint until either the coupon code shows
  // up (state='paid') or we time out after ~30 seconds (fallback to email-only).
  const sessionId = (ctx?.capture?.sessionId) || null;
  const [couponCode, setCouponCode] = useP((ctx?.capture?.couponCode && ctx.capture.couponCode !== 'pending') ? ctx.capture.couponCode : null);
  const [polling, setPolling] = useP(!couponCode && !!sessionId);
  const [timedOut, setTimedOut] = useP(false);

  useEP(() => {
    if (!sessionId || couponCode) return;
    const cfg = (typeof window !== 'undefined' && window.GRAVIDA_API_CONFIG) || {};
    const base  = (cfg.base  || '').replace(/\/$/, '');
    const token = cfg.token || '';
    let tries = 0;
    const MAX_TRIES = 15;
    const id = setInterval(async () => {
      tries++;
      try {
        const res = await fetch(`${base}/api/scan/${encodeURIComponent(sessionId)}/deposit/status`, {
          headers: token ? { 'X-Scan-App-Token': token } : {},
        });
        if (res.ok) {
          const data = await res.json();
          if (data.state === 'paid' && data.coupon_code) {
            setCouponCode(data.coupon_code);
            if (ctx && ctx.capture) ctx.capture.couponCode = data.coupon_code;
            setPolling(false);
            clearInterval(id);
            return;
          }
        }
      } catch (e) {}
      if (tries >= MAX_TRIES) {
        setTimedOut(true);
        setPolling(false);
        clearInterval(id);
      }
    }, 2000);
    return () => clearInterval(id);
  }, [sessionId]);

  return (
    <div className="scr scr--light">
      <TopBar onHelp={ctx.onHelp} />
      <div className="pad flex1 col" style={{ justifyContent: 'space-between' }}>
        <div className="grow center col" style={{ textAlign: 'center' }}>
          <div className="center" style={{ width: 76, height: 76, borderRadius: 999, background: 'var(--ink)', color: 'var(--rose)', marginBottom: 22 }}>
            <IcCheck size={40} />
          </div>
          <span className="eyebrow">Reserved</span>
          <h1 className="display display--sm" style={{ marginTop: 10 }}>Your sculpture is on its way.</h1>
          <p className="lede" style={{ marginTop: 12, maxWidth: 320 }}>
            Thank you. Marit will personally send you your high-resolution atelier
            preview with our editing suggestions within a day.
          </p>

          {couponCode && (
            <div className="card" style={{ marginTop: 20, padding: 16, background: 'var(--paper)' }}>
              <p style={{ margin: 0, fontSize: 11.5, fontWeight: 700, letterSpacing: '.06em', textTransform: 'uppercase', color: 'var(--gold)' }}>
                Your €10 credit code
              </p>
              <p style={{ margin: '8px 0 0', fontFamily: 'ui-monospace, Menlo, monospace', fontSize: 22, fontWeight: 700, color: 'var(--ink)', letterSpacing: '.04em' }}>
                {couponCode}
              </p>
              <p style={{ margin: '8px 0 0', fontSize: 12.5, color: 'var(--ink-mute)' }}>
                Also sent to your email. Use it when you order your final model — brings the price from €59 to €49.
              </p>
            </div>
          )}
          {!couponCode && polling && (
            <div className="card" style={{ marginTop: 20, padding: 14, background: 'var(--paper)' }}>
              <div style={{ display: 'flex', alignItems: 'center', gap: 10, justifyContent: 'center' }}>
                <div style={{ width: 14, height: 14, borderRadius: 999, border: '2px solid var(--gold)', borderTopColor: 'transparent', animation: 'spin360 .8s linear infinite' }} />
                <p style={{ margin: 0, fontSize: 13, color: 'var(--ink-mute)' }}>Generating your credit code...</p>
              </div>
            </div>
          )}
          {!couponCode && timedOut && (
            <div className="card" style={{ marginTop: 20, padding: 14, background: 'var(--paper)' }}>
              <p style={{ margin: 0, fontSize: 13, color: 'var(--ink-mute)', textAlign: 'center' }}>
                Your credit code is on its way by email within a few minutes.
              </p>
            </div>
          )}

          <p style={{ marginTop: 22, fontSize: 13, color: 'var(--ink-mute)' }}>
            Laila will email you within a day with the next steps.
          </p>
        </div>
        <button className="btn btn--accent" onClick={() => ctx.go('atelier')}>
          See materials &amp; finishes <IcArrow size={18} />
        </button>
      </div>
    </div>
  );
}

Object.assign(window, { PreviewScreen, PaidPreviewScreen });
