/* Studio 76 — Studio Bench. Upload a jewellery photo → AI spec, technical blueprint,
   front/side/top plate, and "see it worn" on a model (Instagram-ready).
   Talks only to blueprint.php (keys server-side). CAD export = coming soon. */

const WEAR = [
  { k: 'face',   label: 'Face' },
  { k: 'neck',   label: 'Neckline' },
  { k: 'ear',    label: 'Ear' },
  { k: 'finger', label: 'Hand' },
  { k: 'wrist',  label: 'Wrist' },
  { k: 'ankle',  label: 'Ankle' },
  { k: 'full',   label: 'Full body' },
];

const MODELS = [
  { k: 'woman', label: 'On her' },
  { k: 'man',   label: 'On him' },
];

/* aspect of a generated key, for the refine (perfect-it) pass */
function aspectFor(key) {
  if (key === 'views') return '3:2';
  if (key.slice(0, 4) === 'mag_') return '3:4';
  if (key.slice(0, 2) === 'w_') return '4:5';
  return '1:1';
}

const LOOKS = [
  { k: 'editorial',  label: 'Editorial' },
  { k: 'indian',     label: 'Indian' },
  { k: 'millennial', label: 'Millennial' },
  { k: 'glam',       label: 'Glam' },
  { k: 'studio',     label: 'Studio' },
  { k: 'goldenhour', label: 'Golden hour' },
];

const FORMS = [
  { k: 'ring',     label: 'As a ring' },
  { k: 'earrings', label: 'As earrings' },
  { k: 'necklace', label: 'As a necklace' },
  { k: 'bracelet', label: 'As a bracelet' },
  { k: 'pendant',  label: 'As a pendant' },
  { k: 'bangle',   label: 'As a bangle' },
];

async function benchApi(action, payload) {
  const r = await fetch('blueprint.php', { method: 'POST', headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify(Object.assign({ action }, payload)) });
  let j = {}; try { j = await r.json(); } catch (e) {}
  if (!r.ok || j.error) throw new Error(j.error || ('Something went wrong (' + r.status + ')'));
  return j;
}

function downscale(file, max) {
  return new Promise((res, rej) => {
    const fr = new FileReader();
    fr.onerror = () => rej(new Error('read'));
    fr.onload = () => {
      const img = new Image();
      img.onload = () => {
        const s = Math.min(1, max / Math.max(img.width, img.height));
        const w = Math.round(img.width * s), h = Math.round(img.height * s);
        const c = document.createElement('canvas'); c.width = w; c.height = h;
        c.getContext('2d').drawImage(img, 0, 0, w, h);
        res(c.toDataURL('image/jpeg', 0.85));
      };
      img.onerror = () => rej(new Error('img'));
      img.src = fr.result;
    };
    fr.readAsDataURL(file);
  });
}

function isUrl(x) { return typeof x === 'string' && x.slice(0, 5) === 'data:'; }

function Bench() {
  const [stage, setStage] = React.useState('idle');   // idle | analyzing | ready
  const [src, setSrc]     = React.useState(null);
  const [id, setId]       = React.useState(null);
  const [spec, setSpec]   = React.useState(null);
  const [out, setOut]     = React.useState({});         // {key: url|'loading'}
  const [look, setLook]   = React.useState('editorial');
  const [model, setModel] = React.useState('woman');
  const [shots, setShots] = React.useState([]);          // [{key, placeLabel, lookLabel}]
  const [morphs, setMorphs] = React.useState([]);        // variations + forms [{key, label}]
  const [fixes, setFixes] = React.useState([]);          // perfected takes [{key, label}]
  const [fixN, setFixN]   = React.useState(0);
  const [fixText, setFixText] = React.useState('');
  const [varN, setVarN]   = React.useState(0);
  const [varText, setVarText] = React.useState('');
  const [saving, setSaving]   = React.useState(false);
  const [savedN, setSavedN]   = React.useState('');
  const [savedUrl, setSavedUrl] = React.useState('');
  const [drag, setDrag]   = React.useState(false);
  const [err, setErr]     = React.useState('');
  const [urlText, setUrlText] = React.useState('');
  const [modalSrc, setModalSrc] = React.useState('');
  const [modalKey, setModalKey] = React.useState('');   // generated key behind the zoomed image ('' = not refinable)
  const [cad, setCad] = React.useState(null);   // {status, progress, glb, stl, obj}
  const [cadTex, setCadTex] = React.useState(null); // {status, progress, glb}
  const [cad3dView, setCad3dView] = React.useState('tex');
  const fileRef = React.useRef(null);
  const cadTaskRef = React.useRef(null);
  const texTaskRef = React.useRef(null);
  const savedSetRef = React.useRef(null);   // set id from the first save — re-saving UPDATES it (no duplicates)
  const savingRef = React.useRef(false);    // synchronous double-click guard (state updates are async)

  async function handleFile(file) {
    if (!file || !/^image\//.test(file.type)) { setErr('Please choose an image file.'); return; }
    setErr(''); setStage('analyzing'); setOut({}); setSpec(null); setId(null); setSavedUrl(''); savedSetRef.current = null;
    try {
      const dataUrl = await downscale(file, 1280);
      setSrc(dataUrl);
      const j = await benchApi('analyze', { image: dataUrl });
      setId(j.id); setSpec(j.spec); setStage('ready');
    } catch (e) { setErr(e.message || 'Could not read that image.'); setStage('idle'); }
  }

  async function handleUrl(url) {
    url = (url || '').trim();
    if (!/^https?:\/\//i.test(url)) { setErr('Paste a valid image URL.'); return; }
    setErr(''); setStage('analyzing'); setOut({}); setSpec(null); setId(null); setShots([]); setMorphs([]); setVarN(0); setSavedUrl(''); savedSetRef.current = null;
    setSrc(url);
    try { const j = await benchApi('analyze', { imageUrl: url }); setId(j.id); setSpec(j.spec); setStage('ready'); }
    catch (e) { setErr(e.message || 'Could not bring that image in.'); setStage('idle'); }
  }

  React.useEffect(() => {
    const u = new URLSearchParams(window.location.search).get('img');
    if (u) handleUrl(u);
  }, []);

  async function gen(key, action, payload) {
    if (!id) return;
    setOut(o => Object.assign({}, o, { [key]: 'loading' }));
    try { const j = await benchApi(action, Object.assign({ id }, payload)); setOut(o => Object.assign({}, o, { [key]: j.dataUrl })); }
    catch (e) { setOut(o => Object.assign({}, o, { [key]: null })); setErr(e.message || 'Generation failed.'); }
  }

  function genWear(place) {
    const w = WEAR.find(x => x.k === place), l = LOOKS.find(x => x.k === look), m = MODELS.find(x => x.k === model);
    const key = 'w_' + place + '_' + look + '_' + model;
    const lookLabel = l.label + (model === 'man' ? ' · Him' : '');
    if (!shots.find(s => s.key === key)) setShots(prev => [{ key, placeLabel: w.label, lookLabel }].concat(prev));
    gen(key, 'wear', { place, look, model });
  }

  function genMagazine() {
    const l = LOOKS.find(x => x.k === look);
    const key = 'mag_' + look + '_' + model;
    const lookLabel = l.label + (model === 'man' ? ' · Him' : '');
    if (!shots.find(s => s.key === key)) setShots(prev => [{ key, placeLabel: 'Magazine cover', lookLabel }].concat(prev));
    gen(key, 'magazine', { look, model });
  }

  const aspectsRef = React.useRef({});   // fix_N → aspect inherited from the render it perfected

  function genFix() {
    const srcKey = modalKey, base = out[srcKey];
    const text = fixText.trim();
    if (!srcKey || !isUrl(base) || !text) return;
    const aspect = aspectsRef.current[srcKey] || aspectFor(srcKey);
    const n = fixN + 1; setFixN(n);
    const key = 'fix_' + n;
    aspectsRef.current[key] = aspect;
    const label = '“' + (text.length > 42 ? text.slice(0, 42) + '…' : text) + '”';
    setFixes(prev => [{ key, label }].concat(prev));
    setFixText(''); setModalSrc(''); setModalKey('');
    setOut(o => Object.assign({}, o, { [key]: 'loading' }));
    benchApi('refine', { image: base, instructions: text, aspect })
      .then(j => setOut(o => Object.assign({}, o, { [key]: j.dataUrl })))
      .catch(e => { setOut(o => Object.assign({}, o, { [key]: null })); setErr(e.message || 'Refine failed.'); });
  }

  function genVar() {
    if (!id) return;
    const n = varN + 1; setVarN(n);
    const text = varText.trim();
    const key = 'var_' + n;
    const label = text ? ('“' + (text.length > 42 ? text.slice(0, 42) + '…' : text) + '”') : ('Variation ' + n);
    setMorphs(prev => [{ key, label }].concat(prev));
    gen(key, 'variation', { n, instructions: text });
    setVarText('');
  }
  function genForm(form) {
    const f = FORMS.find(x => x.k === form), key = 'form_' + form;
    if (!morphs.find(m => m.key === key)) setMorphs(prev => [{ key, label: f.label }].concat(prev));
    gen(key, 'reform', { form });
  }

  async function save() {
    if (!id || saving || savingRef.current) return;
    savingRef.current = true;
    setSaving(true); setSavedUrl(''); setSavedN(''); setErr('');
    try {
      const start = await benchApi('save_start', { id, spec, original: isUrl(src) ? src : '', setid: savedSetRef.current || undefined });
      savedSetRef.current = start.setid;
      const items = [];
      if (isUrl(out.blueprint)) items.push({ key: 'blueprint', label: 'Blueprint', url: out.blueprint });
      if (isUrl(out.views)) items.push({ key: 'views', label: 'Front · Side · Top', url: out.views });
      shots.forEach(s => { if (isUrl(out[s.key])) items.push({ key: s.key, label: s.placeLabel + ' · ' + s.lookLabel, url: out[s.key] }); });
      morphs.forEach(m => { if (isUrl(out[m.key])) items.push({ key: m.key, label: m.label, url: out[m.key] }); });
      fixes.forEach(f => { if (isUrl(out[f.key])) items.push({ key: f.key, label: 'Perfected · ' + f.label, url: out[f.key] }); });
      let n = 0;
      for (const it of items) { await benchApi('save_item', { setid: start.setid, key: it.key, label: it.label, dataUrl: it.url }); n++; setSavedN(n + '/' + items.length); }
      try {
        const k = 'foreve_sets', arr = JSON.parse(localStorage.getItem(k) || '[]').filter(s => s.id !== start.setid);
        arr.unshift({ id: start.setid, url: start.url, title: (spec && spec.piece_type) || 'Saved set', n: items.length, created: Date.now() });
        localStorage.setItem(k, JSON.stringify(arr.slice(0, 60)));
      } catch (e) {}
      const modelTask = texTaskRef.current || cadTaskRef.current;
      if (modelTask && cad && cad.glb) { try { await benchApi('save_model', { setid: start.setid, taskId: modelTask }); } catch (e) {} }
      setSavedUrl(start.url);
    } catch (e) { setErr(e.message || 'Save failed.'); }
    savingRef.current = false;
    setSaving(false);
  }

  function cadStart() {
    if (!id) return;
    setCad({ status: 'starting', progress: 0 });
    benchApi('cad_start', { id }).then(function (r) {
      const tid = r.taskId;
      cadTaskRef.current = tid;
      const poll = function () {
        benchApi('cad_status', { taskId: tid }).then(function (s) {
          setCad(s);
          if (s.status !== 'SUCCEEDED' && s.status !== 'FAILED') setTimeout(poll, 5000);
        }).catch(function () { setTimeout(poll, 6000); });
      };
      setTimeout(poll, 4000);
    }).catch(function (e) { setErr(e.message || '3D conversion failed.'); setCad(null); });
  }

  function texStart() {
    if (!cadTaskRef.current) return;
    setCadTex({ status: 'starting', progress: 0 });
    benchApi('cad_texture', { taskId: cadTaskRef.current, spec }).then(function (r) {
      const tt = r.texTaskId; texTaskRef.current = tt;
      const poll = function () {
        benchApi('cad_texture_status', { taskId: tt }).then(function (s) {
          setCadTex(s);
          if (s.glb) setCad3dView('tex');
          if (s.status !== 'SUCCEEDED' && s.status !== 'FAILED') setTimeout(poll, 5000);
        }).catch(function () { setTimeout(poll, 6000); });
      };
      setTimeout(poll, 4000);
    }).catch(function (e) { setErr(e.message || 'Texture pass failed.'); setCadTex(null); });
  }

  function reset() { setStage('idle'); setSrc(null); setId(null); setSpec(null); setOut({}); setShots([]); setMorphs([]); setFixes([]); setFixN(0); setFixText(''); setModalSrc(''); setModalKey(''); aspectsRef.current = {}; setVarN(0); setSaving(false); setSavedN(''); setSavedUrl(''); setCad(null); setCadTex(null); setCad3dView('tex'); texTaskRef.current = null; savedSetRef.current = null; savingRef.current = false; setErr(''); }

  const heroStone = spec && spec.hero_stone ? [spec.hero_stone.carat_estimate, spec.hero_stone.cut, spec.hero_stone.color, spec.hero_stone.type].filter(Boolean).join(' ') : '';
  const dims = spec && spec.estimated_dimensions_mm ? [spec.estimated_dimensions_mm.height, spec.estimated_dimensions_mm.width, spec.estimated_dimensions_mm.depth].filter(Boolean).join(' × ') : '';
  const accents = spec && spec.accent_stones && spec.accent_stones.length ? spec.accent_stones.map(a => [a.count, a.cut, a.type].filter(Boolean).join(' ')).filter(Boolean).join(', ') : '';

  const Row = ({ l, v }) => v ? <div className="bz-row"><span className="bz-k">{l}</span><span className="bz-v">{v}</span></div> : null;
  const Tile = ({ keyName, title, sub }) => {
    const v = out[keyName];
    return (
      <div className="bz-tile">
        <div className="bz-tile-frame">
          {v === 'loading'
            ? <div className="bz-skel"><span className="bz-spin"></span><span>{sub || 'Working…'}</span></div>
            : (typeof v === 'string')
              ? <img src={v} alt={title} style={{cursor:'zoom-in'}} onClick={() => { setModalSrc(v); setModalKey(keyName); setFixText(''); }} />
              : <div className="bz-empty"><span>{sub}</span></div>}
        </div>
        <div className="bz-tile-foot">
          <span className="bz-tile-t">{title}</span>
          {(typeof v === 'string' && v !== 'loading') && <a className="bz-dl" href={v} download={'studio76-' + keyName + '.png'}>Download</a>}
        </div>
      </div>
    );
  };

  return (
    <main className="bz">
      <header className="bz-head">
        <a className="bz-logo" href="/" style={{letterSpacing:'0.12em', textTransform:'uppercase'}}>Studio<span> 76</span></a>
        <span className="bz-headlinks"><a href="/mysets.html">My sets</a><span className="bz-eyebrow">✦ Studio Bench</span></span>
      </header>

      <section className="bz-hero">
        <h1 className="bz-title">From a photo to a <span className="bz-italic">blueprint.</span></h1>
        <p className="bz-sub">Upload any piece of jewellery — a photo, a screenshot, a memory you saved. Our bench reads it, drafts a
          technical blueprint, breaks down the spec, and even shows it <span className="bz-italic">worn on a model</span>, ready for the grid.</p>
      </section>

      {stage === 'idle' && (
        <div className={'bz-drop' + (drag ? ' over' : '')}
          onClick={() => fileRef.current && fileRef.current.click()}
          onDragOver={e => { e.preventDefault(); setDrag(true); }}
          onDragLeave={() => setDrag(false)}
          onDrop={e => { e.preventDefault(); setDrag(false); handleFile(e.dataTransfer.files[0]); }}>
          <div className="bz-drop-icon">
            <svg viewBox="0 0 24 24" width="34" height="34" fill="none"><path d="M12 16V5m0 0L8 9m4-4 4 4" stroke="currentColor" strokeWidth="1.6" strokeLinecap="round" strokeLinejoin="round"/><path d="M4 15v3a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2v-3" stroke="currentColor" strokeWidth="1.6" strokeLinecap="round"/></svg>
          </div>
          <p className="bz-drop-t">Drop a photo here, or <span>browse</span></p>
          <p className="bz-drop-s">JPG / PNG · a clear, well-lit shot works best</p>
          <input ref={fileRef} type="file" accept="image/*" hidden onChange={e => handleFile(e.target.files[0])} />
        </div>
      )}

      {stage === 'idle' && (
        <div className="bz-paste">
          <input className="bz-input" value={urlText} placeholder="…or paste an image URL (e.g. from Pinterest)"
            onChange={e => setUrlText(e.target.value)} onKeyDown={e => { if (e.key === 'Enter') handleUrl(urlText); }} />
          <button className="bz-btn sm" disabled={!urlText.trim()} onClick={() => handleUrl(urlText)}>Bring it in</button>
          <a className="bz-clip-link" href="/clip.html" target="_blank" rel="noopener">Get the Pinterest clipper ↗</a>
        </div>
      )}

      {err && <p className="bz-err">{err}</p>}

      {stage !== 'idle' && (
        <div className="bz-stage">
          {/* left: original + spec */}
          <aside className="bz-left">
            <div className="bz-orig bz-tile-frame">{src ? <img src={src} alt="Your piece" style={{cursor:'zoom-in'}} onClick={() => { setModalSrc(src); setModalKey(''); }} /> : <div className="bz-skel"><span className="bz-spin"></span></div>}</div>
            {spec ? (
              <div className="bz-spec">
                <span className="bz-spec-k">✦ Bench reading</span>
                <h2 className="bz-piece">{spec.piece_type || 'Jewellery piece'}</h2>
                <Row l="Metal" v={[spec.metal, spec.suggested_karat].filter(Boolean).join(' · ')} />
                <Row l="Stones" v={[spec.stone_count, spec.stone_layout].filter(Boolean).join(' · ')} />
                <Row l="Hero stone" v={heroStone} />
                <Row l="Accents" v={accents} />
                <Row l="Setting" v={spec.setting_style} />
                <Row l="Motif" v={spec.motif} />
                <Row l="Era" v={spec.era_style} />
                <Row l="Approx size" v={dims ? dims + ' mm' : ''} />
                <Row l="Sizing" v={spec.sizing} />
                <Row l="Est. metal" v={spec.estimated_metal_weight_g ? spec.estimated_metal_weight_g + ' g' : ''} />
                <Row l="Est. stones" v={spec.estimated_total_carats ? spec.estimated_total_carats + ' ct' : ''} />
                <Row l="Finish" v={spec.finish} />
                {spec.remake_notes && <p className="bz-notes">{spec.remake_notes}</p>}
                <button className="bz-reset" onClick={reset}>Try another photo</button>
              </div>
            ) : (
              <div className="bz-spec bz-spec-skel"><span className="bz-spin"></span><p>Reading your piece…</p></div>
            )}
          </aside>

          {/* right: generators */}
          <div className="bz-right">
            <div className="bz-saverow">
              <button className="bz-btn" disabled={!id || saving} onClick={save}>{saving ? ('Saving… ' + savedN) : (savedUrl ? 'Update this set' : 'Save this set')}</button>
              {savedUrl
                ? <a className="bz-saved" href={savedUrl} target="_blank" rel="noopener">View / share your set ↗</a>
                : <span className="bz-saverow-h">Stores the spec, blueprint &amp; every render at a shareable link.</span>}
            </div>
            <div className="bz-actions">
              <button className="bz-btn" disabled={!id || out.blueprint === 'loading'} onClick={() => gen('blueprint', 'draw')}>Draft blueprint</button>
              <button className="bz-btn" disabled={!id || out.views === 'loading'} onClick={() => gen('views', 'views')}>Front · Side · Top</button>
            </div>

            <div className="bz-grid">
              <Tile keyName="blueprint" title="Technical blueprint" sub="Tap “Draft blueprint”" />
              <Tile keyName="views" title="Orthographic plate" sub="Tap “Front · Side · Top”" />
            </div>

            <div className="bz-wear">
              <span className="bz-spec-k">✦ See it worn — Instagram-ready</span>
              <p className="bz-wear-h">Pick who wears it and a look, then tap a placement. Mix &amp; match to build a shoot.</p>
              <div className="bz-looks">
                {MODELS.map(m => (
                  <button key={m.k} className={'bz-look bz-who' + (model === m.k ? ' on' : '')} onClick={() => setModel(m.k)}>{m.label}</button>
                ))}
                <span className="bz-looksep"></span>
                {LOOKS.map(l => (
                  <button key={l.k} className={'bz-look' + (look === l.k ? ' on' : '')} onClick={() => setLook(l.k)}>{l.label}</button>
                ))}
              </div>
              <div className="bz-chips">
                {WEAR.map(w => {
                  const k = 'w_' + w.k + '_' + look + '_' + model;
                  return (
                    <button key={w.k} className="bz-chip" disabled={!id || out[k] === 'loading'} onClick={() => genWear(w.k)}>
                      {out[k] === 'loading' ? '…' : w.label}
                    </button>
                  );
                })}
                {(() => { const k = 'mag_' + look + '_' + model; return (
                  <button className="bz-chip bz-chip-mag" disabled={!id || out[k] === 'loading'} onClick={genMagazine}>
                    {out[k] === 'loading' ? '…' : '✦ Magazine cover'}
                  </button>
                ); })()}
              </div>
              {shots.length > 0 && (
                <div className="bz-grid">
                  {shots.map(s => <Tile key={s.key} keyName={s.key} title={s.placeLabel + ' · ' + s.lookLabel} sub="" />)}
                </div>
              )}
            </div>

            <div className="bz-wear">
              <span className="bz-spec-k">✦ Make it a set — variations &amp; forms</span>
              <p className="bz-wear-h">Tell us what to change, or just spin off a fresh take — then reimagine it as a matching ring, earrings, necklace…</p>
              <div className="bz-varbox">
                <input className="bz-input" value={varText} maxLength={400}
                  placeholder="e.g. change the stones to rubies, emerald-cut, softer rounded shape… (optional)"
                  onChange={e => setVarText(e.target.value)}
                  onKeyDown={e => { if (e.key === 'Enter') genVar(); }} />
                <button className="bz-btn sm" disabled={!id} onClick={genVar}>Create variation</button>
              </div>
              <div className="bz-chips">
                {FORMS.map(f => {
                  const k = 'form_' + f.k;
                  return <button key={f.k} className="bz-chip" disabled={!id || out[k] === 'loading'} onClick={() => genForm(f.k)}>{out[k] === 'loading' ? '…' : f.label}</button>;
                })}
              </div>
              {morphs.length > 0 && (
                <div className="bz-grid">
                  {morphs.map(m => <Tile key={m.key} keyName={m.key} title={m.label} sub="" />)}
                </div>
              )}
            </div>

            {fixes.length > 0 && (
              <div className="bz-wear">
                <span className="bz-spec-k">✦ Perfected takes</span>
                <p className="bz-wear-h">Corrected renders — zoom any image and tell the bench what to fix to make more.</p>
                <div className="bz-grid">
                  {fixes.map(f => <Tile key={f.key} keyName={f.key} title={f.label} sub="" />)}
                </div>
              </div>
            )}

            <div className="bz-cad3d">
              <span className="bz-spec-k">✦ Convert to 3D — spin it in real gold light</span>
              {!cad && (
                <React.Fragment>
                  <p className="bz-wear-h">Turn your piece into a rotatable 3D model — orbit it, zoom in, even view it in AR. A high-detail concept mesh from your blueprint; open-work &amp; cut-outs are finalised on CAD. A head start for your jeweller, not yet castable.</p>
                  <button className="bz-btn bz-cad-go" disabled={!id} onClick={cadStart}>✦ Convert to 3D</button>
                </React.Fragment>
              )}
              {cad && cad.status !== 'SUCCEEDED' && cad.status !== 'FAILED' && (
                <div className="bz-cad-stage loading">
                  <div className="bz-cad-orbit"><span></span><span></span><span></span></div>
                  <p>Sculpting your 3D model… {cad.progress || 0}%</p>
                  <div className="bz-cad-bar"><i style={{width: Math.max(4, cad.progress || 0) + '%'}}></i></div>
                </div>
              )}
              {cad && cad.status === 'FAILED' && <p className="bz-err" style={{color:'#f0b8a0'}}>3D conversion didn’t complete — <button className="bz-link" onClick={cadStart}>try again</button>.</p>}
              {cad && cad.glb && (
                <React.Fragment>
                  <div className="bz-cad-stage">
                    <model-viewer src={(cad3dView === 'tex' && cadTex && cadTex.glb) ? cadTex.glb : cad.glb} camera-controls="" auto-rotate="" auto-rotate-delay="0" rotation-per-second="22deg" shadow-intensity="1" exposure="1.15" environment-image="neutral" ar="" ar-modes="webxr scene-viewer quick-look" style={{width:'100%',height:'100%'}}></model-viewer>
                    <span className="bz-cad-badge">✦ 3D · drag to spin</span>
                    {cadTex && cadTex.glb && (
                      <div className="bz-cad-toggle">
                        <button className={cad3dView === 'geo' ? 'on' : ''} onClick={() => setCad3dView('geo')}>Geometry</button>
                        <button className={cad3dView === 'tex' ? 'on' : ''} onClick={() => setCad3dView('tex')}>Textured</button>
                      </div>
                    )}
                  </div>
                  <div className="bz-cad-dls">
                    <span>Export:</span>
                    <a className="bz-dl" href={(cad3dView === 'tex' && cadTex && cadTex.glb) ? cadTex.glb : cad.glb} download>.glb</a>
                    {cad.stl && <a className="bz-dl" href={cad.stl} download>.stl</a>}
                    {cad.obj && <a className="bz-dl" href={cad.obj} download>.obj</a>}
                    {!cadTex && <button className="bz-link" onClick={texStart}>✦ Add gold &amp; stones (+10 cr)</button>}
                    {cadTex && cadTex.status !== 'SUCCEEDED' && cadTex.status !== 'FAILED' && <span className="bz-cad-texing"><span className="bz-spin sm"></span>Texturing… {cadTex.progress || 0}%</span>}
                    {cadTex && cadTex.status === 'FAILED' && <button className="bz-link" onClick={texStart}>texture failed — retry</button>}
                  </div>
                </React.Fragment>
              )}
            </div>
          </div>
        </div>
      )}

      {modalSrc && (
        <div className="bz-modal" onClick={() => { setModalSrc(''); setModalKey(''); }}>
          <span className="bz-modal-x">×</span>
          <img src={modalSrc} alt="" onClick={e => e.stopPropagation()} />
          {modalKey && isUrl(out[modalKey]) && (
            <div className="bz-modal-fix" onClick={e => e.stopPropagation()}>
              <input className="bz-input" value={fixText} maxLength={400} autoFocus
                placeholder="Perfect it — e.g. make the chain thinner, warmer light, fix the clasp…"
                onChange={e => setFixText(e.target.value)}
                onKeyDown={e => { if (e.key === 'Enter') genFix(); }} />
              <button className="bz-btn sm" disabled={!fixText.trim()} onClick={genFix}>✦ Perfect it</button>
            </div>
          )}
        </div>
      )}
    </main>
  );
}

ReactDOM.createRoot(document.getElementById('root')).render(<Bench />);
