// SmileCam Pro — clinical photography + case module for TheDentist.ai.
// One module, two presets that ship under one brand:
//   mode='patient'   → "SmileCam"      — self-triage, own cases, share to dentist.
//   mode='clinician' → "SmileCam Pro"  — triage inbox + multi-case library +
//                                        guided capture + collage + carousel +
//                                        smile sim + chairside present + export.
//
// Local-first storage: cases + photos persist to localStorage (and optionally
// IndexedDB for blobs). When R2/Supabase env is bound server-side, /api/folio/*
// promotes them to durable storage.
//
// Window exports keep the existing names (Folio, FolioCase, FolioCapture,
// FolioConsent, SmileSim, Collage, Carousel, DentistPresent) so existing hash
// routes and deep links keep working. SmileCamPro / SmileCam aliases are added
// so other components can reference the product name directly.

const { useState: useFs, useEffect: useFe, useRef: useFr, useMemo: useFm } = React;

const SMILECAM_BRAND = {
  patient:   { word: 'smilecam',  accent: '',     subtitle: 'My photos, my tags, my consent. Share with a dentist when you call.' },
  clinician: { word: 'smilecam',  accent: 'pro',  subtitle: 'Triage inbox · guided capture · auto-tag · present chairside · export. Consent-aware.' },
};
function brand(mode) { return SMILECAM_BRAND[mode] || SMILECAM_BRAND.patient; }

// Build a "X, Y and Z" English list — used by the "Added tags: X, Y and Z."
// confirmation banner mirroring the dentalfolio Add-to-folio sheet.
function listJoin(arr) {
  if (!arr || !arr.length) return '';
  if (arr.length === 1) return arr[0];
  if (arr.length === 2) return arr[0] + ' and ' + arr[1];
  return arr.slice(0, -1).join(', ') + ' and ' + arr[arr.length - 1];
}

// ─── Storage helpers ──────────────────────────────────────────────────────────
const FOLIO_KEY = 'td_folio_cases';
function loadCases() {
  try { return JSON.parse(localStorage.getItem(FOLIO_KEY)) || []; } catch { return []; }
}
function saveCases(cases) {
  try { localStorage.setItem(FOLIO_KEY, JSON.stringify(cases)); } catch {}
}
function newCaseId() { return 'case_' + Math.random().toString(36).slice(2, 10); }
function newPhotoId() { return 'photo_' + Math.random().toString(36).slice(2, 10); }

function fmtDate(iso) {
  try {
    return new Date(iso).toLocaleDateString('en-US', { day:'numeric', month:'short', year:'numeric' });
  } catch { return iso; }
}

function getRole() { try { return localStorage.getItem('td_role') || 'patient'; } catch { return 'patient'; } }

// ─── Folio (case list) ────────────────────────────────────────────────────────
function Folio({ go, lang, setLang, state, set }) {
  const mode = (state && state.role) || getRole();
  const [cases, setCases] = useFs(loadCases);
  const [filterTag, setFilterTag] = useFs('all');
  const [tab, setTab] = useFs(mode === 'clinician' ? 'mycases' : 'mycases');
  const [inbox, setInbox] = useFs(null);

  useFe(() => {
    if (mode === 'clinician' && tab === 'inbox' && inbox == null) {
      fetch('/api/dentist/inbox-demo').then(r => r.json()).then(d => setInbox(d.cases || [])).catch(() => setInbox([]));
    }
  }, [mode, tab, inbox]);

  // Tag chip counts (mirrors dentalfolio "All Photos (21) / Attrition (1)")
  const tagCounts = useFm(() => {
    const counts = { all: 0 };
    cases.forEach(c => {
      counts.all += (c.photos || []).length;
      (c.tags || []).forEach(t => {
        counts[t] = (counts[t] || 0) + (c.photos || []).length;
      });
    });
    return counts;
  }, [cases]);

  const tagChips = useFm(() => {
    const list = [{ slug: 'all', label: 'All Photos', count: tagCounts.all }];
    Object.keys(tagCounts).filter(k => k !== 'all').forEach(slug => {
      const t = window.TagTaxonomyBySlug?.[slug];
      list.push({ slug, label: t?.label || slug, count: tagCounts[slug] });
    });
    return list;
  }, [tagCounts]);

  const visibleCases = useFm(() => {
    if (filterTag === 'all') return cases;
    return cases.filter(c => (c.tags || []).includes(filterTag));
  }, [cases, filterTag]);

  const newCase = () => {
    const c = {
      id: newCaseId(),
      title: mode === 'clinician' ? 'New patient case' : 'My smile folio',
      date: new Date().toISOString(),
      owner_role: mode,
      photos: [], tags: [], notes: '',
      layout: 'grid',
      consent: null,
    };
    const next = [c, ...cases];
    setCases(next); saveCases(next);
    set && set(s => ({ ...s, _folio_case_id: c.id }));
    go('folio-case');
  };

  return (
    <div className="app">
      <Masthead lang={lang} setLang={setLang} go={go}/>
      <main>
        <div className="container" style={{padding:'var(--s-6) 0'}}>
          <div style={{display:'flex',justifyContent:'space-between',alignItems:'flex-start',flexWrap:'wrap',gap:12,marginBottom:'var(--s-3)'}}>
            <div>
              <div className="t-eyebrow">{mode === 'clinician' ? 'SmileCam Pro · clinical' : 'SmileCam · patient'}</div>
              <h1 style={{fontFamily:'var(--font-display)',fontSize:38,fontWeight:500,margin:0,letterSpacing:'-0.01em'}}>
                {brand(mode).word}<span style={{color:'var(--seal)'}}>:</span>
                {brand(mode).accent && <span style={{color:'var(--ink-3)',fontWeight:400,fontSize:24,marginLeft:8}}>{brand(mode).accent}</span>}
              </h1>
              <p style={{color:'var(--ink-2)',margin:'4px 0 0',fontSize:14}}>{brand(mode).subtitle}</p>
            </div>
            <div style={{display:'flex',gap:8,flexWrap:'wrap'}}>
              <button className="btn btn-secondary btn-md" onClick={()=>go('folio-consent')}>Consent</button>
              <button className="btn btn-primary btn-md" onClick={newCase}>+ Add case</button>
            </div>
          </div>

          {mode === 'clinician' && (
            <div role="tablist" aria-label="Folio sections" style={{display:'flex',gap:0,borderBottom:'1px solid var(--ink-5)',marginBottom:'var(--s-4)'}}>
              {[['mycases','My cases'],['inbox', `Inbox${inbox ? ' ('+inbox.length+')':''}`]].map(([k,label]) => (
                <button key={k} role="tab" aria-selected={tab===k}
                  onClick={()=>setTab(k)}
                  style={{
                    padding:'10px 14px', fontSize:13, fontWeight:600, cursor:'pointer', border:0,
                    background: 'transparent', color: tab===k ? 'var(--ink-1)' : 'var(--ink-3)',
                    borderBottom: tab===k ? '2px solid var(--seal)' : '2px solid transparent',
                  }}>{label}</button>
              ))}
            </div>
          )}

          {tab === 'mycases' && (
            <>
              {/* Tag chip filter row (mirrors dentalfolio "present" view) */}
              {tagChips.length > 1 && (
                <div role="group" aria-label="Filter by tag" style={{display:'flex',gap:8,overflowX:'auto',padding:'0 0 var(--s-3)'}}>
                  {tagChips.map(c => (
                    <button key={c.slug} onClick={()=>setFilterTag(c.slug)}
                      aria-pressed={filterTag===c.slug}
                      style={{
                        whiteSpace:'nowrap', padding:'6px 12px', borderRadius:999, fontSize:12, fontWeight:500, cursor:'pointer',
                        background: filterTag===c.slug ? 'var(--paper)' : 'transparent',
                        border:`1px solid ${filterTag===c.slug ? 'var(--ink-1)' : 'var(--ink-5)'}`,
                        color: filterTag===c.slug ? 'var(--ink-1)' : 'var(--ink-2)',
                      }}>{c.label} ({c.count})</button>
                  ))}
                </div>
              )}

              {visibleCases.length === 0 && (
                <div style={{textAlign:'center',padding:'var(--s-8)',border:'1px dashed var(--ink-5)',borderRadius:14,color:'var(--ink-3)'}}>
                  <div style={{marginBottom:8,display:'flex',justifyContent:'center'}} aria-hidden="true"><Icon d={ICONS.camera} size={36} stroke={1.3}/></div>
                  <p style={{margin:0,fontSize:14}}>No cases yet. Add your first one to capture or import photos.</p>
                </div>
              )}

              <div style={{display:'grid',gridTemplateColumns:'repeat(auto-fill, minmax(220px, 1fr))',gap:'var(--s-3)'}}>
                {visibleCases.map(c => {
                  const hasGroup = (c.carousels && c.carousels.length) || (c.collages && c.collages.length);
                  return (
                  <button key={c.id} type="button"
                    onClick={()=>{ set && set(s=>({...s, _folio_case_id: c.id })); go('folio-case'); }}
                    style={{textAlign:'left',background:'var(--paper)',border:'1px solid var(--ink-5)',borderRadius:14,padding:'var(--s-3)',cursor:'pointer',position:'relative'}}>
                    {hasGroup && (
                      <span aria-label="Has carousel or collage" title="Has carousel or collage"
                        style={{position:'absolute',top:10,right:10,zIndex:2,width:22,height:22,background:'rgba(10,31,51,.78)',color:'#fff',display:'flex',alignItems:'center',justifyContent:'center',borderRadius:5,fontSize:12,fontWeight:700,boxShadow:'2px 2px 0 rgba(10,31,51,.5)'}}>▤</span>
                    )}
                    <div style={{ display:'grid', gridTemplateColumns:'repeat(3, 1fr)', gap:4, marginBottom:8 }}>
                      {(c.photos || []).slice(0, 6).map(p => (
                        <div key={p.id} style={{ aspectRatio:'1/1', background:`var(--bone) url(${p.url || p.local_blob || ''}) center/cover`, borderRadius:6, border:'1px solid var(--ink-5)' }}/>
                      ))}
                      {(c.photos || []).length === 0 && [0,1,2].map(i => (
                        <div key={i} style={{ aspectRatio:'1/1', background:'var(--bone)', borderRadius:6, border:'1px solid var(--ink-5)' }}/>
                      ))}
                    </div>
                    <div style={{fontWeight:600,fontSize:14,color:'var(--ink-1)'}}>{c.title}</div>
                    <div style={{fontSize:12,color:'var(--ink-3)',marginTop:2}}>{fmtDate(c.date)} · {(c.photos||[]).length} photos</div>
                    {(c.tags || []).length > 0 && (
                      <div style={{display:'flex',flexWrap:'wrap',gap:4,marginTop:8}}>
                        {(c.tags||[]).slice(0,3).map(t => (
                          <span key={t} style={{fontSize:11,padding:'2px 8px',background:'var(--ink-1)',color:'var(--paper)',borderRadius:999}}>
                            {window.TagTaxonomyBySlug?.[t]?.label || t}
                          </span>
                        ))}
                      </div>
                    )}
                  </button>
                  );
                })}
              </div>
            </>
          )}

          {tab === 'inbox' && mode === 'clinician' && <DentistInboxInline cases={inbox} go={go} set={set}/>}
        </div>
      </main>
      <Footer go={go}/>
    </div>
  );
}

// ─── DentistInbox (inline list) ───────────────────────────────────────────────
function DentistInboxInline({ cases, go, set }) {
  if (cases == null) return <p style={{color:'var(--ink-3)'}}>Loading inbox…</p>;
  if (cases.length === 0) return <p style={{color:'var(--ink-3)'}}>No inbound triage requests right now.</p>;
  return (
    <>
      <div style={{padding:'10px 12px',background:'#FFF8E6',border:'1px solid #E6C77A',borderRadius:8,color:'#7A5500',fontSize:12,marginBottom:'var(--s-3)'}}>
        Demo — no authentication, no real PHI. Replace with /api/dentist/inbox when auth is wired.
      </div>
      <div style={{display:'grid',gap:'var(--s-3)'}}>
        {cases.map(c => (
          <div key={c.id} style={{background:'var(--paper)',border:'1px solid var(--ink-5)',borderRadius:12,padding:'var(--s-3)'}}>
            <div style={{display:'flex',justifyContent:'space-between',alignItems:'flex-start',gap:8,flexWrap:'wrap'}}>
              <div>
                <div style={{fontWeight:600,fontSize:14}}>{c.patient_initials} · ZIP {c.zip} · {c.insurance}</div>
                <div style={{fontSize:13,color:'var(--ink-2)',marginTop:4}}>{c.pain_summary}</div>
              </div>
              <Pill tone={c.urgency === 'emergency' ? 'seal' : c.urgency === 'urgent' ? 'sage' : 'ok'}>
                {c.urgency}
              </Pill>
            </div>
            <div style={{display:'flex',gap:8,flexWrap:'wrap',marginTop:'var(--s-2)'}}>
              <span style={{fontSize:12,color:'var(--ink-3)'}}>Pain {c.pain_level}/10</span>
              <span style={{fontSize:12,color:'var(--ink-3)'}}>{c.photos} photos</span>
              {(c.tags || []).map(t => (
                <span key={t} style={{fontSize:11,padding:'2px 8px',background:'var(--bone)',borderRadius:999,color:'var(--ink-2)'}}>{window.TagTaxonomyBySlug?.[t]?.label || t}</span>
              ))}
            </div>
            <div style={{display:'flex',gap:8,marginTop:'var(--s-3)'}}>
              <button className="btn btn-tertiary btn-sm" onClick={()=>{ try { const a=JSON.parse(localStorage.getItem('dentist_actions')||'{}'); a[c.id]='defer'; localStorage.setItem('dentist_actions', JSON.stringify(a)); } catch{} }}>Defer</button>
              <button className="btn btn-secondary btn-sm" onClick={()=>{ try { const a=JSON.parse(localStorage.getItem('dentist_actions')||'{}'); a[c.id]='route'; localStorage.setItem('dentist_actions', JSON.stringify(a)); } catch{} }}>Route to staff</button>
              <button className="btn btn-primary btn-sm" onClick={()=>{ try { const a=JSON.parse(localStorage.getItem('dentist_actions')||'{}'); a[c.id]='accept'; localStorage.setItem('dentist_actions', JSON.stringify(a)); } catch{}; set && set(s=>({...s, _folio_case_id: c.id})); go('folio-case'); }}>Accept &amp; review</button>
            </div>
          </div>
        ))}
      </div>
    </>
  );
}

// ─── FolioCase (single case) ──────────────────────────────────────────────────
function FolioCase({ go, lang, setLang, state, set }) {
  const mode = (state && state.role) || getRole();
  const caseId = state && state._folio_case_id;
  const [cases, setCases] = useFs(loadCases);
  const caseObj = cases.find(c => c.id === caseId);
  const [layout, setLayout] = useFs(caseObj?.layout || 'grid');
  const [addTag, setAddTag] = useFs('');
  const [recentTags, setRecentTags] = useFs([]);  // for "Added tags: X, Y and Z." banner
  const [viewerIdx, setViewerIdx] = useFs(null);   // open per-photo modal
  const [showBuilder, setShowBuilder] = useFs(null); // 'collage' | 'carousel'

  // Auto-dismiss the "Added tags" banner
  useFe(() => {
    if (!recentTags.length) return;
    const t = setTimeout(()=>setRecentTags([]), 3500);
    return () => clearTimeout(t);
  }, [recentTags]);

  if (!caseObj) return (
    <div className="app"><Masthead lang={lang} setLang={setLang} go={go}/>
      <main><div className="container" style={{padding:'var(--s-8) 0'}}>
        <p>Case not found. <button className="btn btn-tertiary btn-sm" onClick={()=>go('folio')}>← Back to folio</button></p>
      </div></main><Footer go={go}/></div>
  );

  const update = (patch) => {
    const next = cases.map(c => c.id === caseObj.id ? { ...c, ...patch, updated: new Date().toISOString() } : c);
    setCases(next); saveCases(next);
  };

  const toggleTag = (slug) => {
    const tags = caseObj.tags || [];
    const adding = !tags.includes(slug);
    update({ tags: adding ? [...tags, slug] : tags.filter(t => t !== slug) });
    if (adding) {
      const label = window.TagTaxonomyBySlug?.[slug]?.label || slug;
      setRecentTags(rt => rt.includes(label) ? rt : [...rt, label]);
    }
  };

  const onAddTagFree = () => {
    const slug = addTag.trim().toLowerCase().replace(/[^a-z0-9]+/g, '-');
    if (!slug) return;
    if (!(caseObj.tags || []).includes(slug)) {
      update({ tags: [...(caseObj.tags || []), slug] });
      setRecentTags(rt => [...rt, addTag.trim()]);
    }
    setAddTag('');
  };

  const deletePhoto = (photoId) => {
    update({ photos: (caseObj.photos || []).filter(p => p.id !== photoId) });
    setViewerIdx(null);
  };

  return (
    <div className="app">
      <Masthead lang={lang} setLang={setLang} go={go}/>
      <main>
        <div className="container" style={{padding:'var(--s-6) 0'}}>
          <div style={{display:'flex',justifyContent:'space-between',alignItems:'center',marginBottom:'var(--s-3)'}}>
            <button className="bc-link" onClick={()=>go('folio')} style={{background:'none',border:0,cursor:'pointer',color:'var(--ink-2)',fontSize:14}}>← Back</button>
            <div style={{display:'flex',gap:6}}>
              <button className="btn btn-tertiary btn-sm">Edit</button>
              {mode === 'clinician' && <button className="btn btn-secondary btn-sm" onClick={()=>{ set && set(s=>({...s, _folio_case_id: caseObj.id})); go('dentist-present'); }}>Present chairside</button>}
              {mode === 'patient' && <button className="btn btn-secondary btn-sm">Share with dentist</button>}
            </div>
          </div>

          <h1 style={{fontFamily:'var(--font-display)',fontSize:30,fontWeight:500,margin:'0 0 4px'}}>{caseObj.title}</h1>
          <div style={{color:'var(--ink-3)',fontSize:13,marginBottom:'var(--s-3)'}}>{fmtDate(caseObj.date)} · {(caseObj.photos || []).length} photos · {caseObj.consent ? 'Consent on file' : 'No consent yet'}</div>

          <UploadIndicator/>

          {/* Add Photos / Collage / Carousel tiles (mirrors SmileCam Pro capture screen) */}
          <div style={{display:'grid',gridTemplateColumns:'repeat(3, 1fr)',gap:'var(--s-2)',marginBottom:'var(--s-4)'}}>
            <ActionTile label="Add Photos" icon="+" onClick={()=>{ set && set(s=>({...s, _folio_case_id: caseObj.id})); go('folio-capture'); }}/>
            <ActionTile label="Collage"    icon="▦" onClick={()=>setShowBuilder('collage')}/>
            <ActionTile label="Carousel"   icon="▤" onClick={()=>setShowBuilder('carousel')}/>
          </div>

          {/* Tag confirmation banner ("Added tags: X, Y and Z.") */}
          {recentTags.length > 0 && (
            <div role="status" aria-live="polite"
              style={{padding:'10px 14px',background:'#1A2F47',color:'var(--paper)',borderRadius:8,marginBottom:'var(--s-3)',fontSize:13,display:'flex',justifyContent:'space-between',alignItems:'center',gap:8}}>
              <span>Added tags: <strong>{listJoin(recentTags)}</strong>.</span>
              <button onClick={()=>setRecentTags([])} aria-label="Dismiss"
                style={{background:'transparent',color:'var(--paper)',border:0,fontSize:18,cursor:'pointer',opacity:.7,padding:0,lineHeight:1}}>×</button>
            </div>
          )}

          {/* Photo grid (always shown; tap any photo to open viewer) */}
          <div style={{display:'grid',gridTemplateColumns:'repeat(3, 1fr)',gap:8,marginBottom:'var(--s-4)'}}>
            {(caseObj.photos || []).map((p, i) => (
              <button key={p.id} type="button" onClick={()=>setViewerIdx(i)} aria-label={`Open photo ${i+1}`}
                style={{padding:0,aspectRatio:'1/1',background:`var(--bone) url(${p.url || p.local_blob}) center/cover`,borderRadius:6,border:'1px solid var(--ink-5)',position:'relative',cursor:'pointer'}}>
                <span style={{position:'absolute',bottom:4,left:4,fontSize:10,background:'rgba(10,31,51,.8)',color:'var(--paper)',padding:'2px 6px',borderRadius:4}}>{p.role || 'photo'}</span>
              </button>
            ))}
            {(caseObj.photos || []).length === 0 && (
              <div style={{gridColumn:'1 / -1',padding:'var(--s-6)',textAlign:'center',border:'1px dashed var(--ink-5)',borderRadius:10,color:'var(--ink-3)'}}>No photos yet — tap "Add Photos" above.</div>
            )}
          </div>

          {/* Optional inline builders (open from the action tiles) */}
          {showBuilder === 'collage' && window.Collage && (
            <BuilderSheet title="collage" onClose={()=>setShowBuilder(null)}>
              <window.Collage caseObj={caseObj} update={update}/>
            </BuilderSheet>
          )}
          {showBuilder === 'carousel' && window.Carousel && (
            <BuilderSheet title="carousel" onClose={()=>setShowBuilder(null)}>
              <window.Carousel caseObj={caseObj} update={update} onDone={()=>setShowBuilder(null)}/>
            </BuilderSheet>
          )}

          {/* Per-photo viewer modal */}
          {viewerIdx != null && (caseObj.photos || [])[viewerIdx] && (
            <PhotoViewer
              photos={caseObj.photos}
              startIdx={viewerIdx}
              onClose={()=>setViewerIdx(null)}
              onDelete={mode === 'patient' ? null : deletePhoto}
            />
          )}

          {/* Add New Tag (dentalfolio Add Tag screen) */}
          <div style={{marginBottom:'var(--s-4)'}}>
            <div className="t-eyebrow" style={{marginBottom:8}}>Add New Tag</div>
            <form onSubmit={(e)=>{e.preventDefault();onAddTagFree();}} style={{display:'flex',gap:8,marginBottom:'var(--s-3)'}}>
              <input type="text" value={addTag} onChange={(e)=>setAddTag(e.target.value)} placeholder="E.g. Whitening"
                style={{flex:1,padding:'8px 12px',border:'1px solid var(--ink-5)',borderRadius:8,fontSize:14}}/>
              <button type="submit" className="btn btn-secondary btn-sm">Add</button>
            </form>
            <div className="t-eyebrow" style={{marginBottom:8}}>Existing Tags</div>
            <div style={{display:'flex',flexWrap:'wrap',gap:6}}>
              {(window.TagTaxonomy || []).map(t => {
                const on = (caseObj.tags || []).includes(t.slug);
                return (
                  <button key={t.slug} type="button" onClick={()=>toggleTag(t.slug)}
                    style={{
                      fontSize:12, padding:'4px 10px', borderRadius:999,
                      background: on ? 'var(--ink-1)' : 'var(--paper)',
                      color: on ? 'var(--paper)' : 'var(--ink-1)',
                      border:`1px solid ${on ? 'var(--ink-1)' : 'var(--ink-5)'}`,
                      cursor:'pointer',
                    }}>{t.label} {on ? '×' : '+'}</button>
                );
              })}
            </div>
          </div>

          {/* AI Triage panel */}
          <section style={{padding:'var(--s-4)',background:'var(--bone)',borderRadius:12,marginBottom:'var(--s-4)'}}>
            <div className="t-eyebrow" style={{marginBottom:6}}>AI triage</div>
            <p style={{margin:'0 0 var(--s-2)',fontSize:14,color:'var(--ink-2)'}}>
              {mode === 'clinician'
                ? 'Vision-assisted summary of the case photos. Information, not diagnosis.'
                : 'A licensed dentist will confirm. Information only.'}
            </p>
            <button className="btn btn-secondary btn-sm" onClick={()=>go('snapshot')}>Run AI snapshot →</button>
            {mode === 'clinician' && <button className="btn btn-tertiary btn-sm" style={{marginLeft:8}} onClick={()=>go('smile-sim')}>Open smile simulator</button>}
          </section>

          <p className="t-fine">Information, not diagnosis. AI-assisted (AB 3030). Reviewed by the Editorial Council (SB 1120).</p>
        </div>
      </main>
      <Footer go={go}/>
    </div>
  );
}

function ActionTile({ label, icon, onClick }) {
  return (
    <button type="button" onClick={onClick}
      style={{display:'flex',flexDirection:'column',alignItems:'center',justifyContent:'center',gap:6,
        padding:'var(--s-3)',background:'var(--paper)',border:'1px solid var(--ink-5)',borderRadius:12,
        cursor:'pointer'}}>
      <span aria-hidden="true" style={{fontSize:26,lineHeight:1,color:'var(--ink-2)'}}>{icon}</span>
      <span style={{fontSize:12,fontWeight:600,color:'var(--ink-2)'}}>{label}</span>
    </button>
  );
}

// Generic bottom-sheet wrapper that mirrors SmileCam Pro's collage / carousel
// sheet ("Cancel | title | Create / Done").
function BuilderSheet({ title, onClose, children }) {
  return (
    <div style={{margin:'var(--s-3) 0 var(--s-4)',background:'var(--paper)',border:'1px solid var(--ink-5)',borderRadius:14,overflow:'hidden'}}>
      <div style={{display:'flex',justifyContent:'space-between',alignItems:'center',padding:'12px 16px',borderBottom:'1px solid var(--ink-5)'}}>
        <button onClick={onClose} style={{background:'transparent',border:0,color:'var(--ink-2)',cursor:'pointer',fontSize:14}}>Cancel</button>
        <div style={{fontFamily:'var(--font-display)',fontSize:18,fontWeight:500}}>{title}</div>
        <button onClick={onClose} className="btn btn-primary btn-sm">Done</button>
      </div>
      <div style={{padding:'var(--s-3)'}}>{children}</div>
    </div>
  );
}

// Per-photo viewer modal — tap a thumbnail in the case to open. Edit/Delete
// icons at the bottom (delete is gated to clinician mode by FolioCase).
function PhotoViewer({ photos, startIdx, onClose, onDelete }) {
  const [idx, setIdx] = useFs(startIdx || 0);
  useFe(() => {
    const h = (e) => {
      if (e.key === 'Escape') onClose();
      if (e.key === 'ArrowRight') setIdx(i => Math.min(photos.length - 1, i + 1));
      if (e.key === 'ArrowLeft')  setIdx(i => Math.max(0, i - 1));
    };
    window.addEventListener('keydown', h);
    return () => window.removeEventListener('keydown', h);
  }, [photos]);
  const p = photos[idx];
  return (
    <div role="dialog" aria-modal="true" aria-label="Photo viewer"
      style={{position:'fixed',inset:0,background:'rgba(0,0,0,.92)',zIndex:9999,display:'flex',flexDirection:'column'}}
      onClick={(e)=>{ if (e.target === e.currentTarget) onClose(); }}>
      <div style={{display:'flex',justifyContent:'space-between',padding:'12px 16px',color:'#fff'}}>
        <span style={{fontSize:13,opacity:.7}}>{idx+1} / {photos.length}</span>
        <button onClick={onClose} aria-label="Close" style={{background:'transparent',border:0,color:'#fff',fontSize:24,cursor:'pointer',lineHeight:1,padding:0}}>×</button>
      </div>
      <div style={{flex:1,display:'flex',alignItems:'center',justifyContent:'center',padding:16,position:'relative'}}>
        {idx > 0 && (
          <button onClick={()=>setIdx(i=>i-1)} aria-label="Previous"
            style={{position:'absolute',left:8,top:'50%',transform:'translateY(-50%)',background:'rgba(255,255,255,.15)',color:'#fff',border:0,borderRadius:999,width:36,height:36,cursor:'pointer',fontSize:18}}>‹</button>
        )}
        <img src={p.url || p.local_blob} alt={p.role || 'photo'} style={{maxWidth:'100%',maxHeight:'100%',objectFit:'contain'}}/>
        {idx < photos.length - 1 && (
          <button onClick={()=>setIdx(i=>i+1)} aria-label="Next"
            style={{position:'absolute',right:8,top:'50%',transform:'translateY(-50%)',background:'rgba(255,255,255,.15)',color:'#fff',border:0,borderRadius:999,width:36,height:36,cursor:'pointer',fontSize:18}}>›</button>
        )}
      </div>
      <div style={{display:'flex',justifyContent:'space-between',padding:'12px 20px',color:'#fff'}}>
        <button aria-label="Edit" title="Edit tags on this photo"
          style={{background:'transparent',color:'#fff',border:0,cursor:'pointer',display:'flex',alignItems:'center'}}><Icon d={ICONS.edit} size={20}/></button>
        <span style={{fontSize:11,opacity:.6,alignSelf:'center'}}>{p.role || ''}</span>
        {onDelete ? (
          <button aria-label="Delete photo" title="Delete photo" onClick={()=>{ if (confirm('Delete this photo?')) onDelete(p.id); }}
            style={{background:'transparent',color:'#fff',border:0,cursor:'pointer',display:'flex',alignItems:'center'}}><Icon d={ICONS.trash} size={20}/></button>
        ) : <span/>}
      </div>
    </div>
  );
}

// Background-sync indicator — shown when one or more uploads are in flight.
// Mirrors SmileCam Pro's "Uploading 1 photo · Photos will sync in the
// background" pill. Reads a counter from window.__td_uploads.
function UploadIndicator() {
  const [n, setN] = useFs(() => (window.__td_uploads || 0));
  useFe(() => {
    const tick = () => setN(window.__td_uploads || 0);
    const i = setInterval(tick, 600);
    return () => clearInterval(i);
  }, []);
  if (!n) return null;
  return (
    <div role="status" aria-live="polite"
      style={{display:'flex',alignItems:'center',gap:10,padding:'10px 14px',background:'#0F2238',color:'var(--paper)',borderRadius:10,marginBottom:'var(--s-3)',fontSize:13}}>
      <span aria-hidden="true" style={{display:'inline-block',width:12,height:12,border:'2px solid var(--paper)',borderTopColor:'transparent',borderRadius:999,animation:'spin .7s linear infinite'}}/>
      <div>
        <div style={{fontWeight:600}}>{`Uploading ${n} photo${n>1?'s':''}`}</div>
        <div style={{fontSize:11,opacity:.7}}>Photos will sync in the background</div>
      </div>
    </div>
  );
}

// ─── FolioCapture (With templates / Freestyle / Import) ───────────────────────
const TEMPLATES = [
  { slug:'front_retracted',  label:'Front retracted',    hint:'Lips retracted; teeth together.' },
  { slug:'smile_relaxed',    label:'Smile relaxed',      hint:'Natural smile; show natural tooth display.' },
  { slug:'smile_wide',       label:'Smile wide',         hint:'Wide smile showing back teeth.' },
  { slug:'occlusal_upper',   label:'Occlusal upper',     hint:'Upper arch from below, looking up.' },
  { slug:'occlusal_lower',   label:'Occlusal lower',     hint:'Lower arch from above, looking down.' },
  { slug:'lateral_L',        label:'Lateral left',       hint:'Cheek retracted, left side teeth.' },
  { slug:'lateral_R',        label:'Lateral right',      hint:'Cheek retracted, right side teeth.' },
  { slug:'anterior_bite',    label:'Anterior bite',      hint:'Teeth in bite, anterior view.' },
  { slug:'profile',          label:'Profile',            hint:'Side profile of the face.' },
];

function FolioCapture({ go, lang, setLang, state, set }) {
  const [tab, setTab] = useFs('templates');
  const [busy, setBusy] = useFs(false);
  const [err, setErr] = useFs(null);
  const fileRef = useFr(null);
  const caseId = state && state._folio_case_id;

  const addPhotosFromFiles = async (files, role) => {
    setBusy(true); setErr(null);
    const cases = loadCases();
    const idx = cases.findIndex(c => c.id === caseId);
    if (idx === -1) { setErr('Case not found'); setBusy(false); return; }
    // Append local copies immediately so the UI feels instant; uploads run in
    // the background and the UploadIndicator on the case page shows progress.
    const added = [];
    for (const f of files) {
      const url = URL.createObjectURL(f);
      const photo = {
        id: newPhotoId(), role: role || 'other', capture_mode: tab,
        local_blob: url, captured: new Date().toISOString(), tags: [],
      };
      cases[idx].photos = [...(cases[idx].photos || []), photo];
      added.push({ photo, file: f });
    }
    saveCases(cases);
    setBusy(false);
    // Kick off background uploads — bump global counter so UploadIndicator shows.
    window.__td_uploads = (window.__td_uploads || 0) + added.length;
    (async () => {
      for (const { file } of added) {
        try {
          const fd = new FormData();
          fd.append('photo', file);
          fd.append('case_id', caseId);
          fd.append('role', role || 'other');
          await fetch('/api/folio/upload', { method:'POST', body: fd });
        } catch {/* keep local */}
        window.__td_uploads = Math.max(0, (window.__td_uploads || 1) - 1);
      }
    })();
    go('folio-case');
  };

  return (
    <div className="app">
      <Masthead lang={lang} setLang={setLang} go={go}/>
      <main>
        <div className="container" style={{padding:'var(--s-6) 0',maxWidth:720}}>
          <div style={{display:'flex',justifyContent:'space-between',alignItems:'center',marginBottom:'var(--s-3)'}}>
            <button className="bc-link" onClick={()=>go('folio-case')} style={{background:'none',border:0,cursor:'pointer',color:'var(--ink-2)',fontSize:14}}>Cancel</button>
            <div style={{fontFamily:'var(--font-display)',fontSize:18}}>add photos</div>
            <span/>
          </div>

          <div role="tablist" style={{display:'grid',gridTemplateColumns:'repeat(3, 1fr)',gap:0,background:'var(--paper)',border:'1px solid var(--ink-5)',borderRadius:12,overflow:'hidden',marginBottom:'var(--s-4)'}}>
            {[['templates','With templates',ICONS.grid],['freestyle','Freestyle',ICONS.camera],['import','Import',ICONS.download]].map(([k,label,icon]) => (
              <button key={k} role="tab" aria-selected={tab===k}
                onClick={()=>setTab(k)}
                style={{padding:'12px',fontSize:13,fontWeight:600,cursor:'pointer',border:0,
                  display:'flex',alignItems:'center',justifyContent:'center',gap:6,
                  background: tab===k ? 'var(--bone)' : 'transparent',
                  color: tab===k ? 'var(--ink-1)' : 'var(--ink-3)'}}>
                <Icon d={icon} size={16}/> {label}
              </button>
            ))}
          </div>

          {tab === 'templates' && (
            <div>
              <p style={{margin:'0 0 var(--s-3)',color:'var(--ink-2)',fontSize:14}}>Pick a guided pose. We'll auto-tag the photo with its view.</p>
              <div style={{display:'grid',gridTemplateColumns:'repeat(auto-fit, minmax(180px, 1fr))',gap:'var(--s-3)'}}>
                {TEMPLATES.map(t => (
                  <button key={t.slug} type="button"
                    onClick={()=>{ fileRef.current && (fileRef.current.dataset.role = t.slug); fileRef.current && fileRef.current.click(); }}
                    style={{textAlign:'left',padding:'var(--s-3)',background:'var(--paper)',border:'1px solid var(--ink-5)',borderRadius:12,cursor:'pointer'}}>
                    <div style={{fontWeight:600,fontSize:14}}>{t.label}</div>
                    <div style={{fontSize:12,color:'var(--ink-3)',marginTop:4}}>{t.hint}</div>
                  </button>
                ))}
              </div>
              <input ref={fileRef} type="file" accept="image/*" capture="environment" multiple
                onChange={(e)=>addPhotosFromFiles(Array.from(e.target.files || []), e.target.dataset.role || 'other')}
                style={{position:'absolute',width:1,height:1,opacity:0,pointerEvents:'none'}}/>
            </div>
          )}
          {tab === 'freestyle' && (
            <div>
              <p style={{margin:'0 0 var(--s-3)',color:'var(--ink-2)',fontSize:14}}>Open the camera — no template, no pose guidance.</p>
              <label className="btn btn-primary btn-md" style={{cursor:'pointer'}}>
                <input type="file" accept="image/*" capture="environment" multiple
                  onChange={(e)=>addPhotosFromFiles(Array.from(e.target.files || []), 'other')}
                  style={{position:'absolute',width:1,height:1,opacity:0,pointerEvents:'none'}}/>
                Open camera
              </label>
            </div>
          )}
          {tab === 'import' && (
            <div>
              <p style={{margin:'0 0 var(--s-3)',color:'var(--ink-2)',fontSize:14}}>Import from your library (multi-select).</p>
              <label className="btn btn-secondary btn-md" style={{cursor:'pointer'}}>
                <input type="file" accept="image/*" multiple
                  onChange={(e)=>addPhotosFromFiles(Array.from(e.target.files || []), 'imported')}
                  style={{position:'absolute',width:1,height:1,opacity:0,pointerEvents:'none'}}/>
                Choose photos
              </label>
            </div>
          )}
          {busy && <p style={{marginTop:'var(--s-3)',color:'var(--ink-3)',fontSize:13}}>Uploading…</p>}
          {err && <p role="alert" style={{marginTop:'var(--s-3)',color:'var(--seal)',fontSize:13}}>{err}</p>}
        </div>
      </main>
      <Footer go={go}/>
    </div>
  );
}

// ─── FolioConsent ─────────────────────────────────────────────────────────────
function FolioConsent({ go, lang, setLang, state, set }) {
  const [scopes, setScopes] = useFs({
    clinical_records: true,
    professional_communication: false,
    education_enabled: false, education_face: 'smile_only',
    marketing_enabled: false,  marketing_face: 'smile_only',
    chairside_presentation: false,
  });
  const [sigData, setSigData] = useFs(null);
  const [busy, setBusy] = useFs(false);
  const [msg, setMsg] = useFs(null);
  const sigRef = useFr(null);
  const drawing = useFr(false);

  const startDraw = (e) => {
    drawing.current = true;
    const ctx = sigRef.current.getContext('2d');
    const rect = sigRef.current.getBoundingClientRect();
    ctx.beginPath();
    ctx.moveTo(e.clientX - rect.left, e.clientY - rect.top);
  };
  const moveDraw = (e) => {
    if (!drawing.current) return;
    const ctx = sigRef.current.getContext('2d');
    const rect = sigRef.current.getBoundingClientRect();
    ctx.lineWidth = 2; ctx.lineCap = 'round'; ctx.strokeStyle = '#0A1F33';
    ctx.lineTo(e.clientX - rect.left, e.clientY - rect.top);
    ctx.stroke();
  };
  const endDraw = () => { drawing.current = false; setSigData(sigRef.current?.toDataURL('image/png')); };
  const clearSig = () => {
    const ctx = sigRef.current.getContext('2d');
    ctx.clearRect(0, 0, sigRef.current.width, sigRef.current.height);
    setSigData(null);
  };

  const submit = async () => {
    setBusy(true); setMsg(null);
    try {
      const resp = await fetch('/api/folio/consent', {
        method: 'POST', headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({
          case_id: state?._folio_case_id || null,
          scopes: {
            clinical_records: scopes.clinical_records,
            professional_communication: scopes.professional_communication,
            education: { enabled: scopes.education_enabled, face: scopes.education_face },
            marketing: { enabled: scopes.marketing_enabled, face: scopes.marketing_face },
            chairside_presentation: scopes.chairside_presentation,
          },
          signature_png_b64: sigData,
          signed_at: new Date().toISOString(),
        }),
      });
      const d = await resp.json().catch(()=>({}));
      if (!resp.ok || d.ok === false) { setMsg(d.error || `Couldn't save (${resp.status})`); }
      else {
        setMsg('Consent saved.');
        // patch local case
        if (state?._folio_case_id) {
          const cases = loadCases();
          const next = cases.map(c => c.id === state._folio_case_id ? { ...c, consent: { signed_at: new Date().toISOString(), scopes } } : c);
          saveCases(next);
        }
        setTimeout(()=>go('folio'), 700);
      }
    } catch (err) {
      setMsg('Network error. Try again.');
    } finally { setBusy(false); }
  };

  return (
    <div className="app">
      <Masthead lang={lang} setLang={setLang} go={go}/>
      <main>
        <div className="container" style={{padding:'var(--s-6) 0',maxWidth:680}}>
          <div style={{display:'flex',justifyContent:'space-between',alignItems:'center',marginBottom:'var(--s-3)'}}>
            <button className="bc-link" onClick={()=>go('folio')} style={{background:'none',border:0,cursor:'pointer',color:'var(--ink-2)',fontSize:14}}>Cancel</button>
            <div style={{fontFamily:'var(--font-display)',fontSize:18}}>consent</div>
            <button className="btn btn-primary btn-sm" onClick={submit} disabled={busy}>{busy ? 'Saving…' : 'OK'}</button>
          </div>

          <p style={{fontSize:14,color:'var(--ink-2)',margin:'0 0 var(--s-3)'}}>I consent to my photographs being used for the following purposes:</p>

          <div style={{display:'flex',flexDirection:'column',gap:'var(--s-2)'}}>
            <Check label="Use for clinical records (required)" value={true} locked/>
            <Check label="Use for professional communication" sub="(Shared confidentially with other professionals)"
              value={scopes.professional_communication} onChange={(v)=>setScopes(s=>({...s, professional_communication:v}))}/>
            <CheckGroup label="Use for education and teaching" sub="(May appear in dental lectures or journals, will be anonymised)"
              value={scopes.education_enabled} onChange={(v)=>setScopes(s=>({...s, education_enabled:v}))}
              radioValue={scopes.education_face} onRadio={(v)=>setScopes(s=>({...s, education_face:v}))}/>
            <CheckGroup label="Use for marketing and promotional purposes" sub="(May appear online or in print, will be anonymised)"
              value={scopes.marketing_enabled} onChange={(v)=>setScopes(s=>({...s, marketing_enabled:v}))}
              radioValue={scopes.marketing_face} onRadio={(v)=>setScopes(s=>({...s, marketing_face:v}))}/>
            <Check label="Use for presentation to other patients" sub="(Chair-side case presentation to help similar patients, will be anonymised)"
              value={scopes.chairside_presentation} onChange={(v)=>setScopes(s=>({...s, chairside_presentation:v}))}/>
          </div>

          <p style={{fontSize:13,color:'var(--ink-2)',marginTop:'var(--s-4)',lineHeight:1.55}}>
            I confirm that I have read and understood the information above. I have had the opportunity to ask questions about how my images will be used, and I give my consent for the uses indicated above.
          </p>

          <div style={{marginTop:'var(--s-3)'}}>
            <div className="t-eyebrow" style={{marginBottom:6}}>Patient Signature</div>
            <canvas ref={sigRef} width={620} height={140}
              style={{width:'100%',height:140,background:'var(--paper)',border:'1px solid var(--ink-5)',borderRadius:8,touchAction:'none'}}
              onPointerDown={startDraw} onPointerMove={moveDraw} onPointerUp={endDraw} onPointerLeave={endDraw}/>
            <button className="btn btn-tertiary btn-sm" onClick={clearSig} style={{marginTop:6}}>Clear</button>
          </div>

          {msg && <p style={{marginTop:'var(--s-3)',fontSize:13,color: msg.startsWith('Consent') ? 'var(--sage)' : 'var(--seal)'}}>{msg}</p>}
        </div>
      </main>
      <Footer go={go}/>
    </div>
  );
}

function Check({ label, sub, value, onChange, locked }) {
  return (
    <label style={{display:'flex',alignItems:'flex-start',gap:10,padding:'8px 10px',background:'var(--paper)',border:'1px solid var(--ink-5)',borderRadius:8,cursor: locked ? 'default':'pointer'}}>
      <input type="checkbox" checked={!!value} disabled={!!locked}
        onChange={(e)=>onChange && onChange(e.target.checked)}
        style={{marginTop:3}}/>
      <span>
        <span style={{fontSize:14,fontWeight:500,color:'var(--ink-1)'}}>{label}</span>
        {sub && <span style={{display:'block',fontSize:12,color:'var(--ink-3)',marginTop:2}}>{sub}</span>}
      </span>
    </label>
  );
}
function CheckGroup({ label, sub, value, onChange, radioValue, onRadio }) {
  return (
    <div style={{padding:'8px 10px',background:'var(--paper)',border:'1px solid var(--ink-5)',borderRadius:8}}>
      <label style={{display:'flex',alignItems:'flex-start',gap:10,cursor:'pointer'}}>
        <input type="checkbox" checked={!!value} onChange={(e)=>onChange(e.target.checked)} style={{marginTop:3}}/>
        <span>
          <span style={{fontSize:14,fontWeight:500,color:'var(--ink-1)'}}>{label}</span>
          {sub && <span style={{display:'block',fontSize:12,color:'var(--ink-3)',marginTop:2}}>{sub}</span>}
        </span>
      </label>
      {value && (
        <div style={{display:'flex',gap:16,marginTop:6,paddingLeft:28}}>
          {[['include_face','Including face'], ['smile_only', 'Only smile and teeth']].map(([k, lbl]) => (
            <label key={k} style={{display:'flex',gap:6,alignItems:'center',fontSize:13,cursor:'pointer'}}>
              <input type="radio" name={'rad_'+label} checked={radioValue === k} onChange={()=>onRadio(k)}/>
              {lbl}
            </label>
          ))}
        </div>
      )}
    </div>
  );
}

// ─── SmileSim ─────────────────────────────────────────────────────────────────
function SmileSim({ go, lang, setLang, state }) {
  const [intent, setIntent] = useFs('whitening');
  const [data, setData] = useFs(null);
  const [busy, setBusy] = useFs(false);
  const [divider, setDivider] = useFs(50);
  const containerRef = useFr(null);

  const run = async () => {
    setBusy(true);
    try {
      const resp = await fetch('/api/smile-sim', {
        method:'POST', headers:{'Content-Type':'application/json'},
        body: JSON.stringify({ intent }),
      });
      const d = await resp.json();
      if (resp.ok) setData(d);
    } catch {}
    setBusy(false);
  };

  const onDrag = (e) => {
    if (e.buttons !== 1 && e.type !== 'pointermove') return;
    const rect = containerRef.current.getBoundingClientRect();
    const pct = Math.max(0, Math.min(100, ((e.clientX - rect.left) / rect.width) * 100));
    setDivider(pct);
  };

  return (
    <div className="app">
      <Masthead lang={lang} setLang={setLang} go={go}/>
      <main>
        <div className="container" style={{padding:'var(--s-6) 0',maxWidth:640}}>
          <div style={{display:'flex',justifyContent:'space-between',alignItems:'center',marginBottom:'var(--s-2)'}}>
            <div className="t-eyebrow">Smile simulator</div>
            <button className="btn btn-tertiary btn-sm" onClick={()=>go('folio')}>← Back</button>
          </div>
          <h1 style={{fontFamily:'var(--font-display)',fontSize:28,fontWeight:500,margin:'0 0 var(--s-3)'}}>generate in seconds — support patient conversations</h1>
          <div style={{display:'flex',gap:8,marginBottom:'var(--s-3)'}}>
            {['whitening','veneers','alignment'].map(k => (
              <button key={k} className={`btn ${intent===k?'btn-primary':'btn-tertiary'} btn-sm`} onClick={()=>setIntent(k)}>{k}</button>
            ))}
            <button className="btn btn-secondary btn-sm" onClick={run} disabled={busy}>{busy ? 'Generating…' : 'Generate'}</button>
          </div>
          <div ref={containerRef} onPointerDown={onDrag} onPointerMove={onDrag}
            style={{position:'relative',background:'#000',borderRadius:14,overflow:'hidden',aspectRatio:'3/4',width:'100%',cursor:'ew-resize',touchAction:'none'}}>
            <span style={{position:'absolute',top:8,left:10,color:'#fff',fontSize:12,fontWeight:600,zIndex:3}}>Before</span>
            <span style={{position:'absolute',top:8,right:10,color:'#fff',fontSize:12,fontWeight:600,zIndex:3}}>After</span>
            {data && (
              <>
                <img src={data.before_url} alt="Before" style={{position:'absolute',inset:0,width:'100%',height:'100%',objectFit:'cover'}}/>
                <div style={{position:'absolute',inset:0,width:`${divider}%`,overflow:'hidden'}}>
                  <img src={data.after_url} alt="After" style={{position:'absolute',inset:0,width:`${100/(divider/100)}%`,maxWidth:'none',height:'100%',objectFit:'cover'}}/>
                </div>
                <div style={{position:'absolute',top:0,bottom:0,left:`${divider}%`,width:2,background:'#fff'}}/>
                <span style={{position:'absolute',left:`${divider}%`,top:'50%',transform:'translate(-50%, -50%)',background:'#fff',width:28,height:28,borderRadius:999,display:'flex',alignItems:'center',justifyContent:'center',fontSize:12,fontWeight:700,color:'#000'}}>&lt;&gt;</span>
                <span style={{position:'absolute',bottom:8,right:10,color:'#fff',fontSize:11,opacity:.85,fontStyle:'italic'}}>AI Generated</span>
              </>
            )}
            {!data && <div style={{position:'absolute',inset:0,display:'flex',alignItems:'center',justifyContent:'center',color:'#fff',fontSize:14}}>Pick an intent and tap Generate.</div>}
          </div>
          {data && (
            <>
              <p style={{marginTop:'var(--s-3)',color:'var(--ink-2)',fontSize:14,lineHeight:1.6}}>{data.narration}</p>
              <p className="t-fine" style={{color:'var(--seal)'}}>{data.disclaimer}</p>
            </>
          )}
        </div>
      </main>
      <Footer go={go}/>
    </div>
  );
}

// ─── Collage builder (dentalfolio "collage" screen) ───────────────────────────
function Collage({ caseObj, update }) {
  const [ratio, setRatio] = useFs('1:1');
  const [spacing, setSpacing] = useFs(8);
  const [corners, setCorners] = useFs(8);
  const [border, setBorder] = useFs(2);
  const [bg, setBg] = useFs('#000000');
  const [borderColor, setBorderColor] = useFs('#000000');
  const photos = (caseObj.photos || []).slice(0, 2);

  const ratios = { '1:1':[1,1], '16:9':[16,9], '9:16':[9,16], '3:4':[3,4], '4:5':[4,5], 'Custom':[1,1] };
  const [rw, rh] = ratios[ratio] || [1,1];

  return (
    <div style={{padding:'var(--s-3)',background:'var(--paper)',border:'1px solid var(--ink-5)',borderRadius:12,marginBottom:'var(--s-4)'}}>
      <p style={{textAlign:'center',color:'var(--ink-3)',fontSize:12,margin:'0 0 var(--s-3)'}}>Drag to pan · Pinch to zoom · Rotate with two fingers</p>
      <div style={{background: bg,border:`${border}px solid ${borderColor}`,borderRadius: corners,padding: spacing,
        display:'grid',gridTemplateRows:'1fr 1fr',gap: spacing,aspectRatio:`${rw}/${rh}`,marginBottom:'var(--s-3)'}}>
        {photos.map(p => (
          <div key={p.id} style={{background:`#000 url(${p.url || p.local_blob}) center/cover`,borderRadius: corners/2}}/>
        ))}
        {photos.length === 0 && [0,1].map(i => (
          <div key={i} style={{background:'#222',borderRadius: corners/2,display:'flex',alignItems:'center',justifyContent:'center',color:'#888',fontSize:12}}>Add photos to the case</div>
        ))}
      </div>
      <div style={{display:'flex',gap:6,flexWrap:'wrap',marginBottom:'var(--s-3)'}}>
        {Object.keys(ratios).map(r => (
          <button key={r} className={`btn ${ratio===r?'btn-primary':'btn-tertiary'} btn-sm`} onClick={()=>setRatio(r)}>{r}</button>
        ))}
      </div>
      <div style={{display:'grid',gridTemplateColumns:'repeat(3, 1fr)',gap:'var(--s-2)',marginBottom:'var(--s-3)'}}>
        <Slider label="Spacing" value={spacing} min={0} max={32} onChange={setSpacing}/>
        <Slider label="Corners" value={corners} min={0} max={32} onChange={setCorners}/>
        <Slider label="Borders" value={border} min={0} max={16} onChange={setBorder}/>
      </div>
      <div style={{display:'flex',gap:'var(--s-3)',flexWrap:'wrap'}}>
        <ColorPicker label="Background" value={bg} onChange={setBg}/>
        <ColorPicker label="Border" value={borderColor} onChange={setBorderColor}/>
      </div>
    </div>
  );
}
function Slider({ label, value, min, max, onChange }) {
  return (
    <label style={{display:'flex',flexDirection:'column',gap:4,fontSize:11,color:'var(--ink-3)',fontWeight:600}}>
      <span>{label}</span>
      <input type="range" min={min} max={max} value={value} onChange={(e)=>onChange(+e.target.value)}/>
    </label>
  );
}
function ColorPicker({ label, value, onChange }) {
  return (
    <label style={{display:'flex',alignItems:'center',gap:6,fontSize:12,color:'var(--ink-3)',fontWeight:600,cursor:'pointer'}}>
      <span style={{width:20,height:20,borderRadius:999,background:value,border:'1px solid var(--ink-5)'}}/>
      <input type="color" value={value} onChange={(e)=>onChange(e.target.value)} style={{position:'absolute',width:1,height:1,opacity:0}}/>
      {label}
    </label>
  );
}

// ─── Carousel ─────────────────────────────────────────────────────────────────
// Two modes:
//   builder  (update + onDone props)  → tap-to-order picker matching SmileCam
//                                       Pro's "carousel" creation screen.
//   viewer   (default)                → swipe through previously saved carousel
//                                       photos. Shown chairside.
function Carousel({ caseObj, update, onDone }) {
  const photos = caseObj.photos || [];
  const isBuilder = typeof update === 'function';
  const [order, setOrder] = useFs([]);             // builder: array of photo ids in tap order
  const [idx, setIdx]     = useFs(0);              // viewer: current index
  const [busy, setBusy]   = useFs(false);
  const touch = useFr({ x:0, t:0 });

  // Builder mode
  if (isBuilder) {
    const toggle = (pid) => {
      setOrder(o => o.includes(pid) ? o.filter(x => x !== pid) : [...o, pid]);
    };
    const create = () => {
      if (order.length < 2) return;
      setBusy(true);
      const carousels = [...(caseObj.carousels || []), {
        id: 'car_' + Math.random().toString(36).slice(2, 9),
        order, created: new Date().toISOString(),
      }];
      update({ carousels, layout: 'carousel' });
      setBusy(false);
      onDone && onDone();
    };
    if (!photos.length) return <p style={{color:'var(--ink-3)'}}>Add photos to the case first.</p>;
    return (
      <div>
        <p style={{margin:'0 0 var(--s-3)',color:'var(--ink-2)',fontSize:13,lineHeight:1.5}}>
          Select at least two photos to include in the carousel.
          <strong> The order you tap them determines the swipe order.</strong>
        </p>
        <div style={{display:'grid',gridTemplateColumns:'repeat(3, 1fr)',gap:6,marginBottom:'var(--s-3)'}}>
          {photos.map(p => {
            const pos = order.indexOf(p.id);
            const selected = pos !== -1;
            return (
              <button key={p.id} type="button" onClick={()=>toggle(p.id)}
                aria-pressed={selected}
                style={{position:'relative',padding:0,aspectRatio:'1/1',background:`var(--bone) url(${p.url || p.local_blob}) center/cover`,
                        borderRadius:6,border:`2px solid ${selected ? 'var(--seal)' : 'var(--ink-5)'}`,cursor:'pointer'}}>
                {selected && (
                  <span style={{position:'absolute',top:6,right:6,width:22,height:22,borderRadius:999,background:'var(--seal)',color:'#fff',
                                display:'flex',alignItems:'center',justifyContent:'center',fontSize:12,fontWeight:700}}>{pos+1}</span>
                )}
              </button>
            );
          })}
        </div>
        <div style={{display:'flex',justifyContent:'space-between',alignItems:'center',gap:8}}>
          <span style={{fontSize:12,color:'var(--ink-3)'}}>{order.length} selected</span>
          <div style={{display:'flex',gap:6}}>
            <button className="btn btn-tertiary btn-sm" onClick={()=>setOrder([])}>Clear</button>
            <button className="btn btn-primary btn-sm" onClick={create} disabled={order.length < 2 || busy}>
              {busy ? 'Creating…' : `Create carousel (${order.length})`}
            </button>
          </div>
        </div>
      </div>
    );
  }

  // Viewer mode — show the most recent saved carousel, or fall back to all photos in order.
  const saved = (caseObj.carousels || []).slice(-1)[0];
  const seq = saved
    ? saved.order.map(pid => photos.find(p => p.id === pid)).filter(Boolean)
    : photos;
  if (!seq.length) return <div style={{padding:'var(--s-6)',textAlign:'center',color:'var(--ink-3)'}}>No carousel yet — tap Carousel above to build one.</div>;
  const p = seq[Math.min(idx, seq.length - 1)];
  const onTouchStart = (e) => { touch.current = { x: e.touches[0].clientX, t: Date.now() }; };
  const onTouchEnd = (e) => {
    const dx = (e.changedTouches[0].clientX - touch.current.x);
    if (Math.abs(dx) > 40) setIdx(i => Math.max(0, Math.min(seq.length - 1, i + (dx < 0 ? 1 : -1))));
  };
  return (
    <div style={{background:'#000',borderRadius:14,padding:'var(--s-3)',marginBottom:'var(--s-4)'}}
         onTouchStart={onTouchStart} onTouchEnd={onTouchEnd}>
      <div style={{display:'flex',justifyContent:'space-between',color:'#fff',fontSize:11,opacity:.7,marginBottom:6}}>
        <span>{idx+1} / {seq.length}</span>
        <span>swipe to advance</span>
      </div>
      <div style={{background:`#000 url(${p.url || p.local_blob}) center/contain no-repeat`,aspectRatio:'4/5',borderRadius:10}}/>
      <div style={{display:'flex',justifyContent:'center',gap:6,marginTop:'var(--s-2)'}}>
        {seq.map((_, i) => (
          <button key={i} onClick={()=>setIdx(i)} aria-label={`Go to photo ${i+1}`}
            style={{width:6,height:6,borderRadius:999,border:0,padding:0,cursor:'pointer',
                    background: i===idx ? '#fff' : 'rgba(255,255,255,.4)'}}/>
        ))}
      </div>
    </div>
  );
}

// ─── DentistPresent (full-screen chairside) ───────────────────────────────────
function DentistPresent({ go, state }) {
  const caseId = state?._folio_case_id;
  const cases = loadCases();
  const caseObj = cases.find(c => c.id === caseId);
  const [idx, setIdx] = useFs(0);
  useFe(() => {
    const h = (e) => { if (e.key === 'Escape') go('folio-case'); if (e.key === 'ArrowRight') setIdx(i => Math.min((caseObj?.photos?.length || 1) - 1, i + 1)); if (e.key === 'ArrowLeft') setIdx(i => Math.max(0, i - 1)); };
    window.addEventListener('keydown', h); return () => window.removeEventListener('keydown', h);
  }, [caseObj]);
  if (!caseObj) return <div style={{padding:40}}>Case not found.</div>;
  const p = (caseObj.photos || [])[idx];
  return (
    <div style={{position:'fixed',inset:0,background:'#000',display:'flex',flexDirection:'column',zIndex:9999}}>
      <div style={{display:'flex',justifyContent:'space-between',padding:'10px 16px',color:'#fff'}}>
        <span style={{fontFamily:'var(--font-display)',fontSize:14,opacity:.7}}>{caseObj.title} · chairside</span>
        <button onClick={()=>go('folio-case')} style={{background:'transparent',color:'#fff',border:'1px solid rgba(255,255,255,.3)',padding:'4px 10px',borderRadius:6,cursor:'pointer'}}>Esc · Exit</button>
      </div>
      <div style={{flex:1,display:'flex',alignItems:'center',justifyContent:'center',padding:20}}>
        {p ? <img src={p.url || p.local_blob} alt={p.role} style={{maxWidth:'100%',maxHeight:'100%',objectFit:'contain'}}/> : <span style={{color:'#888'}}>No photos in this case yet.</span>}
      </div>
      <div style={{display:'flex',justifyContent:'center',gap:12,padding:'10px 16px',color:'#fff'}}>
        <button onClick={()=>setIdx(i=>Math.max(0,i-1))} style={{background:'transparent',color:'#fff',border:'1px solid rgba(255,255,255,.3)',padding:'6px 14px',borderRadius:6,cursor:'pointer'}}>← Prev</button>
        <span style={{opacity:.6,alignSelf:'center'}}>{(caseObj.photos || []).length ? `${idx+1} / ${caseObj.photos.length}` : ''}</span>
        <button onClick={()=>setIdx(i=>Math.min((caseObj.photos||[]).length-1,i+1))} style={{background:'transparent',color:'#fff',border:'1px solid rgba(255,255,255,.3)',padding:'6px 14px',borderRadius:6,cursor:'pointer'}}>Next →</button>
      </div>
    </div>
  );
}

// Window exports. The Folio* names are kept so existing hash routes and deep
// links continue to work; SmileCamPro / SmileCam are added so other components
// can reference the product name directly.
Object.assign(window, {
  Folio, FolioCase, FolioCapture, FolioConsent, SmileSim, Collage, Carousel, DentistPresent,
  SmileCamPro: Folio, SmileCam: Folio,
  SmileCamCase: FolioCase, SmileCamCapture: FolioCapture, SmileCamConsent: FolioConsent,
});
