// mariusson — Artist Profile drawer with iPod-style Cover Flow
// Slides in from the right.  Cover Flow snaps smoothly back to center on
// release (with momentum), is drag/wheel/click navigable.
// Commission CTA lives HERE.
// Bio is rich: a written statement + exhibitions, studio status, contact strip.

function ArtistProfile({ artistName, highlightWork, catalogue, role, visible, onClose, onCommission, onPickWork }) {
  const [isMobile, setIsMobile] = React.useState(() =>
    typeof window !== 'undefined' && window.matchMedia('(max-width: 760px)').matches
  );
  React.useEffect(() => {
    if (typeof window === 'undefined') return;
    const mq = window.matchMedia('(max-width: 760px)');
    const fn = () => setIsMobile(mq.matches);
    if (mq.addEventListener) mq.addEventListener('change', fn); else mq.addListener(fn);
    return () => {
      if (mq.removeEventListener) mq.removeEventListener('change', fn); else mq.removeListener(fn);
    };
  }, []);
  const works = React.useMemo(() => catalogue.filter((w) => w.artist === artistName), [artistName, catalogue]);
  const startIdx = Math.max(0, works.findIndex((w) => w.id === highlightWork?.id));
  const [idx, setIdx] = React.useState(startIdx >= 0 ? startIdx : 0);
  const PAD_X = isMobile ? 16 : 32;

  const bio = React.useMemo(() => {
    const seed = hash(artistName);
    const r = rng(seed);
    // Real artists from data/catalogue.json carry an authoritative bio.
    // For anything else (uploads, unknown name) we fall back to the
    // deterministic-procedural bio so the panel still has something to say.
    const real = (window.AN_ARTISTS_BY_NAME || {})[artistName];

    const studios = ['Antwerp', 'Lisbon', 'Berlin', 'Paris', 'Rotterdam', 'Athens', 'Milan', 'Helsinki', 'Reykjavík', 'Oslo', 'Porto', 'Kraków', 'Copenhagen'];
    const subjects = ['light at the periphery', 'salt and erosion', 'industrial decay', 'the body in repose', 'corridors and thresholds', 'archive and memory', 'the unfinished gesture'];
    const verbs = ['examines', 'inhabits', 'returns to', 'unfolds', 'composes around'];
    const galleries = ['Voss', 'Linde', 'Marin', 'Hauser', 'Mendes', 'Albers'];
    const states = ['in residence', 'between exhibitions', 'preparing a solo', 'open to commissions', 'on sabbatical'];

    const studio = real?.region || studios[Math.floor(r() * studios.length)];
    const since = real?.born ? `${real.born}` : '20XX';
    const subject = subjects[Math.floor(r() * subjects.length)];
    const verb = verbs[Math.floor(r() * verbs.length)];
    const repr = r() > 0.4 ? 'Galerie ' + galleries[Math.floor(r() * galleries.length)] : null;
    const state = real
      ? (real.died ? `archive · ${real.died}` : 'living artist')
      : states[Math.floor(r() * states.length)];

    let statement;
    if (real) {
      statement = real.bio;
    } else {
      const statements = [
        `Working primarily from a single window in ${studio}, the practice ${verb} ${subject} as a slow accumulation of looking. Each work is a record of held attention.`,
        `Drawing on a long apprenticeship to material, the studio ${verb} ${subject} — interested less in resolution than in the conditions under which something becomes visible.`,
        `Based in ${studio}, the work ${verb} ${subject} across paint, paper, and image. The constant is duration: nothing is begun lightly, nothing finished cleanly.`,
      ];
      statement = statements[Math.floor(r() * statements.length)];
    }

    // Exhibitions — synthesised even for real masters, framed as posthumous.
    const venues = real
      ? ['Musée d\'Orsay, Paris', 'National Gallery, London', 'Stedelijk, Amsterdam', 'MoMA, New York', 'Tate Britain', 'Prado, Madrid', 'Kunsthistorisches, Vienna', 'Hermitage, St Petersburg', 'Uffizi, Florence']
      : ['Kunsthalle ' + studio, 'M HKA', 'Stedelijk', 'Le Plateau', 'Tate Etc.', 'Castello di Rivoli', 'Whitechapel', 'Bonniers Konsthall', 'Camden Art Centre', 'Casino Luxembourg'];
    const exhCount = real ? 5 : 4;
    const exh = Array.from({ length: exhCount }).map(() => {
      const yr = real ? 2010 + Math.floor(r() * 14) : 2018 + Math.floor(r() * 8);
      return {
        year: yr,
        venue: venues[Math.floor(r() * venues.length)],
        city: studios[Math.floor(r() * studios.length)],
        title: real
          ? ['Major Retrospective', 'Late Works', 'Drawing the Light', 'Selected Paintings', 'A Centenary', 'In the Studio'][Math.floor(r() * 6)]
          : ['Slow Look', 'Field Notes', 'Quiet Materials', 'After the Image', 'Salt Index', 'A Domestic Light'][Math.floor(r() * 6)],
        kind: r() > 0.6 ? 'Solo' : 'Group',
      };
    }).sort((a, b) => b.year - a.year);

    // Contact / social presence — synthetic. Real masters are obviously not
    // on Instagram, but the demo's commission/email plumbing still wants
    // *something* in these slots, so we render an estate-style handle.
    const handleBase = artistName.toLowerCase().replace(/[^a-z]/g, '').slice(0, 12) || 'studio';
    const estate = real ? `${handleBase}.estate` : handleBase;
    const websiteHosts = ['.studio', '.art', '.work', '.gallery', '.atelier'];
    const website = real
      ? `${estate}${websiteHosts[Math.floor(r() * websiteHosts.length)]}`
      : (r() > 0.25 ? `${handleBase}${websiteHosts[Math.floor(r() * websiteHosts.length)]}` : null);
    const socials = [];
    if (real || r() > 0.2)  socials.push({ kind: 'Instagram', handle: `@${estate}`, url: `instagram.com/${estate}` });
    if (!real && r() > 0.55) socials.push({ kind: 'X', handle: `@${handleBase}`, url: `x.com/${handleBase}` });
    if (r() > 0.6)          socials.push({ kind: 'Are.na', handle: `${handleBase}`, url: `are.na/${handleBase}` });
    const email = real
      ? `archive@${estate}.org`
      : (r() > 0.4 ? `studio@${handleBase}.${websiteHosts[Math.floor(r() * websiteHosts.length)].slice(1)}` : null);
    const acceptsDonations = real ? false : r() > 0.35;

    return {
      isReal: !!real, real,
      studio, since, subject, verb, repr, state,
      statement, exh, website, socials, email, acceptsDonations,
    };
  }, [artistName]);

  const current = works[idx];

  return (
    <div data-atlas-panel style={{
      position: 'absolute', inset: 0, zIndex: 50,
      background: visible ? 'rgba(14,14,12,0.45)' : 'rgba(14,14,12,0)',
      backdropFilter: visible ? 'blur(8px)' : 'blur(0px)',
      WebkitBackdropFilter: visible ? 'blur(8px)' : 'blur(0px)',
      transition: 'background 320ms var(--ease-out), backdrop-filter 320ms var(--ease-out), -webkit-backdrop-filter 320ms var(--ease-out)',
    }} onClick={onClose}>
      <div data-atlas-drawer onClick={(e) => e.stopPropagation()} style={{
        position: 'absolute', top: 0, right: 0, bottom: 0,
        width: 'min(880px, 94%)', background: BAU.paper,
        borderLeft: `1px solid ${BAU.ink}`,
        transform: visible ? 'translateX(0)' : 'translateX(100%)',
        transition: 'transform 420ms var(--ease-out)',
        display: 'flex', flexDirection: 'column',
        overflow: 'hidden',
      }}>
        {/* Bauhaus header banner */}
        <div style={{ display: 'flex', borderBottom: `1px solid ${BAU.ink}`, height: 8, flexShrink: 0 }}>
          <div style={{ background: BAU.red, flex: 3 }} />
          <div style={{ background: BAU.yellow, flex: 1 }} />
          <div style={{ background: BAU.blue, flex: 2 }} />
        </div>

        {/* Scrollable content area */}
        <div style={{ flex: 1, overflowY: 'auto', display: 'flex', flexDirection: 'column' }}>
          {/* Top: name + close */}
          <div style={{ padding: `${isMobile ? 16 : 24}px ${PAD_X}px 8px`, display: 'flex', justifyContent: 'space-between', alignItems: 'flex-start', gap: 12 }}>
            <div style={{ minWidth: 0, flex: 1 }}>
              <div style={{ fontFamily: 'var(--mono)', fontSize: 9.5, letterSpacing: '0.18em', color: BAU.graphite, textTransform: 'uppercase' }}>
                {bio.isReal
                  ? `ARTIST · ${bio.studio.toUpperCase()} · ${bio.real.born} — ${bio.real.died}`
                  : `ARTIST · ${bio.studio.toUpperCase()} · ACTIVE ${bio.since}`}
              </div>
              <h1 style={{
                fontFamily: 'var(--serif)', fontSize: isMobile ? 32 : 56, margin: '6px 0 0',
                lineHeight: 0.95, letterSpacing: '-0.02em', fontWeight: 400,
                overflowWrap: 'anywhere',
              }}>
                <span style={{ fontStyle: 'italic' }}>{artistName}</span>
                {highlightWork?.verified && (
                  <span style={{ display: 'inline-block', width: 14, height: 14, borderRadius: '50%', background: BAU.blue, marginLeft: 12, verticalAlign: 'middle' }} title="Verified" />
                )}
              </h1>
              <div style={{ marginTop: 10, fontFamily: 'var(--serif)', fontSize: isMobile ? 15 : 18, color: BAU.ink, fontStyle: 'italic', maxWidth: 640 }}>
                {bio.isReal
                  ? <>From the {bio.real.region} canon · died {bio.real.died}</>
                  : <>{bio.verb} {bio.subject}.{bio.repr && <span style={{ color: BAU.graphite }}> · Repr. {bio.repr}</span>}</>}
              </div>
              <div style={{ marginTop: 8, display: 'inline-flex', alignItems: 'center', gap: 8, fontFamily: 'var(--mono)', fontSize: 10, letterSpacing: '0.16em', color: BAU.graphite, textTransform: 'uppercase' }}>
                <span style={{ width: 8, height: 8, borderRadius: '50%', background: BAU.red, animation: 'an-pulse 2.4s ease-in-out infinite' }} />
                Studio status — {bio.state}
              </div>
            </div>
            <button onClick={onClose} style={{
              width: isMobile ? 36 : 30, height: isMobile ? 36 : 30,
              background: 'transparent', border: `1px solid ${BAU.ink}`,
              borderRadius: isMobile ? '50%' : 0,
              cursor: 'pointer', fontSize: isMobile ? 18 : 16, color: BAU.ink, lineHeight: 1, padding: 0,
              flexShrink: 0,
            }}>×</button>
          </div>

          {/* Stats strip — 4 cols on desktop, 2 cols on mobile so cells have room */}
          <div style={{
            display: 'grid',
            gridTemplateColumns: isMobile ? 'repeat(2, 1fr)' : 'repeat(4, 1fr)',
            gap: 0, margin: `16px ${PAD_X}px 0`,
            border: `1px solid ${BAU.rule}`, flexShrink: 0,
          }}>
            <Stat label="Works" value={works.length} accent={BAU.red} />
            <Stat label="Region" value={works[0]?.region || '—'} accent={BAU.yellow} />
            <Stat
              label={bio.isReal ? 'Lived' : 'Active'}
              value={bio.isReal ? `${bio.real.born}–${bio.real.died}` : bio.since}
              accent={BAU.blue} />
            <Stat label="Status" value={highlightWork?.verified ? 'Verified' : 'Listed'} accent={BAU.ink} />
          </div>

          {/* Cover Flow */}
          <div style={{ marginTop: 18 }}>
            <CoverFlow works={works} idx={idx} onIdx={setIdx} onOpen={onPickWork} />
          </div>

          {/* Selected work readout */}
          {current && (
            <div style={{
              padding: `6px ${PAD_X}px 10px`,
              display: isMobile ? 'flex' : 'grid',
              flexDirection: isMobile ? 'column' : undefined,
              gridTemplateColumns: isMobile ? undefined : '1.5fr auto',
              gap: isMobile ? 12 : 24,
              alignItems: isMobile ? 'stretch' : 'flex-end',
              flexShrink: 0,
            }}>
              <div>
                <div style={{ fontFamily: 'var(--mono)', fontSize: 9, letterSpacing: '0.16em', color: BAU.graphite, textTransform: 'uppercase' }}>
                  {current.id} · {current.year} · {current.method}
                </div>
                <h3 style={{ fontFamily: 'var(--serif)', fontSize: 24, margin: '4px 0 0', lineHeight: 1.05, fontWeight: 400 }}>
                  <span style={{ fontStyle: 'italic' }}>{current.title}</span>
                </h3>
                {current.price && (
                  <div style={{ marginTop: 6, fontFamily: 'var(--mono)', fontSize: 11, letterSpacing: '0.12em', color: BAU.ink, textTransform: 'uppercase' }}>
                    list · €{current.price.toLocaleString()}
                  </div>
                )}
              </div>
              <button onClick={() => onCommission({ work: current, bio })} style={{
                background: BAU.ink, color: BAU.paper, border: 'none',
                borderRadius: 'var(--r-pill)',
                padding: isMobile ? '12px 18px' : '14px 22px',
                fontFamily: 'var(--mono)', fontSize: 11, letterSpacing: '0.18em',
                textTransform: 'uppercase', cursor: 'pointer',
                display: 'inline-flex', alignItems: 'center', justifyContent: 'center', gap: 12,
                width: isMobile ? '100%' : undefined,
                minHeight: 44,
              }}>
                <span style={{ width: 9, height: 9, background: BAU.yellow }} />
                Commission similar
                <span>→</span>
              </button>
            </div>
          )}

          {/* ─── BIO BLOCK ─────────────────────────────────────── */}
          <div style={{ padding: `${isMobile ? 20 : 32}px ${PAD_X}px 16px`, borderTop: `1px solid ${BAU.rule}`, marginTop: 16 }}>
            <div style={{ display: 'grid', gridTemplateColumns: isMobile ? '1fr' : '1fr 2fr', gap: isMobile ? 12 : 32 }}>
              <div>
                <div style={{ fontFamily: 'var(--mono)', fontSize: 9.5, letterSpacing: '0.18em', color: BAU.graphite, textTransform: 'uppercase' }}>
                  Statement
                </div>
                <div style={{ marginTop: 6, fontFamily: 'var(--mono)', fontSize: 9.5, letterSpacing: '0.16em', color: BAU.ink, textTransform: 'uppercase' }}>
                  /{String(bio.exh.length).padStart(2, '0')} exh.
                </div>
              </div>
              <div>
                <p style={{ fontFamily: 'var(--serif)', fontSize: isMobile ? 16 : 20, lineHeight: 1.5, margin: 0, color: BAU.ink, fontStyle: 'italic', textWrap: 'pretty' }}>
                  {bio.statement}
                </p>
              </div>
            </div>
          </div>

          {/* ─── EXHIBITIONS ───────────────────────────────────── */}
          <div style={{ padding: `12px ${PAD_X}px 16px` }}>
            <div style={{ display: 'grid', gridTemplateColumns: isMobile ? '1fr' : '1fr 2fr', gap: isMobile ? 8 : 32 }}>
              <div>
                <div style={{ fontFamily: 'var(--mono)', fontSize: 9.5, letterSpacing: '0.18em', color: BAU.graphite, textTransform: 'uppercase' }}>
                  Selected Exhibitions
                </div>
              </div>
              <div style={{ display: 'flex', flexDirection: 'column' }}>
                {bio.exh.map((e, i) => (
                  <div key={i} style={{
                    display: 'grid',
                    gridTemplateColumns: isMobile ? '46px 1fr' : '52px 70px 1fr 60px',
                    gridTemplateRows: isMobile ? 'auto auto' : 'auto',
                    gap: isMobile ? '2px 10px' : 12, alignItems: 'baseline',
                    padding: '10px 0',
                    borderBottom: i < bio.exh.length - 1 ? `1px solid ${BAU.rule}` : 'none',
                  }}>
                    <span style={{ fontFamily: 'var(--mono)', fontSize: 10, letterSpacing: '0.16em', color: BAU.ink, textTransform: 'uppercase' }}>{e.year}</span>
                    <span style={{ fontFamily: 'var(--mono)', fontSize: 9, letterSpacing: '0.16em', color: e.kind === 'Solo' ? BAU.red : BAU.graphite, textTransform: 'uppercase' }}>{e.kind}</span>
                    <span style={{ fontFamily: 'var(--serif)', fontSize: 16, fontStyle: 'italic', color: BAU.ink }}>
                      <span style={{ color: BAU.graphite, fontStyle: 'normal', fontFamily: 'var(--sans)', fontSize: 12, marginRight: 6 }}>“{e.title}”</span>
                      {e.venue}
                    </span>
                    <span style={{ fontFamily: 'var(--mono)', fontSize: 9.5, letterSpacing: '0.14em', color: BAU.graphite, textTransform: 'uppercase', textAlign: 'right' }}>{e.city}</span>
                  </div>
                ))}
              </div>
            </div>
          </div>

          {/* ─── CONTACT STRIP ────────────────────────────────── */}
          <div style={{ padding: `12px ${PAD_X}px ${isMobile ? 20 : 32}px` }}>
            <div style={{ fontFamily: 'var(--mono)', fontSize: 9.5, letterSpacing: '0.18em', color: BAU.graphite, textTransform: 'uppercase', marginBottom: 10 }}>
              Reach the studio · {bio.studio}
            </div>
            <div style={{ display: 'flex', flexWrap: 'wrap', gap: 8 }}>
              {bio.website && (
                <ContactLink kind="Website" label={bio.website} href={`https://${bio.website}`} accent={BAU.red} />
              )}
              {bio.email && (
                <ContactLink kind="Email" label={bio.email} href={`mailto:${bio.email}`} accent={BAU.yellow} />
              )}
              {bio.socials.map((s) => (
                <ContactLink key={s.kind} kind={s.kind} label={s.handle} href={`https://${s.url}`} accent={BAU.blue} />
              ))}
              {bio.repr && (
                <ContactLink kind="Repr." label={bio.repr} href="#" accent={BAU.ink} />
              )}
              {bio.acceptsDonations && (
                <span style={{
                  display: 'inline-flex', alignItems: 'center', gap: 8,
                  padding: '8px 12px',
                  border: `1px dashed ${BAU.ink}`, background: BAU.paper2, color: BAU.ink,
                }}>
                  <span style={{ width: 6, height: 6, background: BAU.red, borderRadius: '50%' }} />
                  <span style={{ fontFamily: 'var(--mono)', fontSize: 9, letterSpacing: '0.16em', textTransform: 'uppercase', opacity: 0.7 }}>Donations</span>
                  <span style={{ fontFamily: 'var(--serif)', fontSize: 14, fontStyle: 'italic' }}>Open to support</span>
                </span>
              )}
            </div>
            <div style={{ fontFamily: 'var(--mono)', fontSize: 9, letterSpacing: '0.16em', color: BAU.graphite2, textTransform: 'uppercase', marginTop: 10 }}>
              ↳ Or send a brief through mariusson — use the commission button above.
            </div>
          </div>
        </div>
      </div>
    </div>
  );
}

function Stat({ label, value, accent }) {
  return (
    <div style={{
      padding: '12px 14px',
      borderRight: `1px solid ${BAU.rule}`,
      position: 'relative',
    }}>
      <div style={{ position: 'absolute', top: 0, left: 0, width: 16, height: 3, background: accent }} />
      <div style={{ fontFamily: 'var(--mono)', fontSize: 8.5, letterSpacing: '0.18em', color: BAU.graphite, textTransform: 'uppercase' }}>{label}</div>
      <div style={{ fontFamily: 'var(--serif)', fontSize: 18, fontStyle: 'italic', color: BAU.ink, marginTop: 2 }}>{value}</div>
    </div>
  );
}

function ContactLink({ kind, label, href, accent }) {
  return (
    <a href={href} target="_blank" rel="noreferrer" onClick={(e) => { if (href === '#') e.preventDefault(); }} style={{
      display: 'inline-flex', alignItems: 'center', gap: 8,
      padding: '8px 12px',
      border: `1px solid ${BAU.ink}`,
      background: BAU.paper, color: BAU.ink,
      textDecoration: 'none',
      transition: 'background 0.2s var(--ease-out), color 0.2s var(--ease-out)',
    }}
    onMouseEnter={(e) => { e.currentTarget.style.background = BAU.ink; e.currentTarget.style.color = BAU.paper; }}
    onMouseLeave={(e) => { e.currentTarget.style.background = BAU.paper; e.currentTarget.style.color = BAU.ink; }}>
      <span style={{ width: 6, height: 6, background: accent, flexShrink: 0 }} />
      <span style={{ fontFamily: 'var(--mono)', fontSize: 9, letterSpacing: '0.16em', textTransform: 'uppercase', opacity: 0.7 }}>{kind}</span>
      <span style={{ fontFamily: 'var(--serif)', fontSize: 14, fontStyle: 'italic' }}>{label}</span>
      <span style={{ fontFamily: 'var(--mono)', fontSize: 11, opacity: 0.6 }}>↗</span>
    </a>
  );
}

function ContactCell({ label, value, accent }) {
  return (
    <div style={{
      padding: '14px 16px', borderRight: `1px solid ${BAU.rule}`, position: 'relative',
    }}>
      <div style={{ position: 'absolute', top: 0, left: 0, width: 20, height: 4, background: accent }} />
      <div style={{ fontFamily: 'var(--mono)', fontSize: 9, letterSpacing: '0.18em', color: BAU.graphite, textTransform: 'uppercase' }}>{label}</div>
      <div style={{ fontFamily: 'var(--serif)', fontSize: 18, fontStyle: 'italic', color: BAU.ink, marginTop: 4 }}>{value}</div>
    </div>
  );
}

// ─── iPod-style Cover Flow with smooth-snap on release ─────
function CoverFlow({ works, idx, onIdx, onOpen }) {
  // virtualIdx is the rendered (continuous) position. While dragging it's a
  // float; when released it animates (CSS transition) to the snapped target.
  const [virtualIdx, setVirtualIdx] = React.useState(idx);
  const [isDragging, setDragging] = React.useState(false);
  // dragState carries `virt` — the LATEST virtualIdx from the most recent
  // pointermove. Reading React state from onUp's closure gave a stale value
  // (whatever virtualIdx was at drag start), which is why a long drag-and-
  // release used to snap back to the work nearest where the drag began.
  const dragStateRef = React.useRef(null);

  React.useEffect(() => {
    // External idx change — animate to it (transition is on because !isDragging)
    if (!isDragging) setVirtualIdx(idx);
  }, [idx, isDragging]);

  const clamp = (i) => Math.max(0, Math.min(works.length - 1, i));

  const onDown = (e) => {
    e.preventDefault();
    const x = e.clientX ?? e.touches?.[0]?.clientX ?? 0;
    dragStateRef.current = {
      startX: x, startVirt: virtualIdx,
      lastX: x, lastT: performance.now(), vel: 0,
      virt: virtualIdx,
    };
    setDragging(true);
  };
  const onMove = (e) => {
    if (!dragStateRef.current) return;
    const x = e.clientX ?? e.touches?.[0]?.clientX ?? 0;
    const ds = dragStateRef.current;
    const dx = x - ds.startX;
    const next = ds.startVirt - dx / 200;
    const now = performance.now();
    const dt = Math.max(8, now - ds.lastT);
    // Velocity in idx-units / second
    ds.vel = ((ds.lastX - x) / 200) / (dt / 1000);
    ds.lastX = x;
    ds.lastT = now;
    ds.virt = next;        // <— keep the live position in the ref
    setVirtualIdx(next);
  };
  const onUp = () => {
    const ds = dragStateRef.current;
    if (!ds) return;
    // Use the latest dragged-to position from the ref, not React closure.
    // This is the actual fix for "drag releases snap back to start".
    const liveVirt = ds.virt ?? ds.startVirt;
    const projected = liveVirt + ds.vel * 0.18;
    const target = clamp(Math.round(projected));
    dragStateRef.current = null;
    setDragging(false);
    setVirtualIdx(target); // CSS transition will smoothly snap
    onIdx(target);
  };

  // Functional update — under fast wheel input multiple events fire before
  // React commits state, so reading `idx` from closure gives a stale value
  // and successive ticks all snap back to the same starting work. Reading
  // `prev` per call accumulates correctly.
  const onWheel = (e) => {
    e.preventDefault();
    const dir = Math.sign(e.deltaY || e.deltaX);
    if (!dir) return;
    onIdx((prev) => Math.max(0, Math.min(works.length - 1, prev + dir)));
  };

  React.useEffect(() => {
    if (!isDragging) return;
    const m = (e) => onMove(e);
    const u = () => onUp();
    window.addEventListener('mousemove', m);
    window.addEventListener('mouseup', u);
    window.addEventListener('touchmove', m, { passive: false });
    window.addEventListener('touchend', u);
    return () => {
      window.removeEventListener('mousemove', m);
      window.removeEventListener('mouseup', u);
      window.removeEventListener('touchmove', m);
      window.removeEventListener('touchend', u);
    };
  }, [isDragging]);

  return (
    <div onMouseDown={onDown} onTouchStart={onDown} onWheel={onWheel}
      style={{
        position: 'relative', width: '100%', height: 280,
        perspective: 1400, perspectiveOrigin: 'center 60%',
        overflow: 'hidden',
        cursor: isDragging ? 'grabbing' : 'grab',
        userSelect: 'none',
      }}>
      <div style={{ position: 'absolute', top: '50%', left: '50%', transformStyle: 'preserve-3d' }}>
        {works.map((w, i) => {
          const offset = i - virtualIdx;
          const abs = Math.abs(offset);
          if (abs > 5) return null;
          // Smooth fan-out: tiles arc away as they leave center.
          const t = Math.max(-1, Math.min(1, offset / 1.2));
          const x = offset * 110;
          const rot = -t * 55;
          const z = -Math.abs(offset) * 100;
          const op = abs > 4 ? 0 : 1 - (abs * 0.12);
          const tileH = 200;
          const tileW = Math.round(tileH * (w.aspect || 1));
          return (
            <div key={w.id}
              onClick={(e) => { e.stopPropagation(); if (Math.round(virtualIdx) === i) onOpen?.(w); else onIdx(i); }}
              style={{
                position: 'absolute', left: 0, top: 0,
                transform: `translate(-50%, -50%) translate3d(${x}px, 0, ${z}px) rotateY(${rot}deg)`,
                transition: isDragging ? 'none' : 'transform 0.55s cubic-bezier(0.22, 1, 0.36, 1), opacity 0.4s',
                opacity: op,
                zIndex: 100 - Math.round(abs * 10),
                cursor: 'pointer',
              }}>
              <div style={{
                width: tileW, height: tileH,
                boxShadow: Math.round(virtualIdx) === i
                  ? '0 30px 60px rgba(0,0,0,0.28), 0 0 0 1px rgba(0,0,0,0.06)'
                  : '0 20px 40px rgba(0,0,0,0.22)',
                position: 'relative',
              }}>
                <ArtTile work={w} width={tileW} height={tileH} />
                <div style={{
                  position: 'absolute', top: '100%', left: 0, right: 0, height: tileH * 0.5,
                  transform: 'scaleY(-1)',
                  maskImage: 'linear-gradient(to bottom, rgba(0,0,0,0.35), transparent 80%)',
                  WebkitMaskImage: 'linear-gradient(to bottom, rgba(0,0,0,0.35), transparent 80%)',
                  pointerEvents: 'none',
                }}>
                  <ArtTile work={w} width={tileW} height={tileH} />
                </div>
              </div>
            </div>
          );
        })}
      </div>

      <div style={{ position: 'absolute', top: 12, left: 0, right: 0, display: 'flex', justifyContent: 'center', pointerEvents: 'none' }}>
        <span style={{ fontFamily: 'var(--mono)', fontSize: 10, letterSpacing: '0.18em', color: BAU.graphite, textTransform: 'uppercase' }}>
          {String(idx + 1).padStart(2, '0')} / {String(works.length).padStart(2, '0')}
        </span>
      </div>

      <button onClick={() => onIdx(clamp(idx - 1))} style={{
        position: 'absolute', left: 16, top: '50%', transform: 'translateY(-50%)',
        width: 36, height: 36, borderRadius: '50%',
        background: BAU.paper, border: `1px solid ${BAU.ink}`,
        cursor: 'pointer', fontFamily: 'var(--mono)', fontSize: 13, color: BAU.ink,
        display: 'flex', alignItems: 'center', justifyContent: 'center',
      }}>‹</button>
      <button onClick={() => onIdx(clamp(idx + 1))} style={{
        position: 'absolute', right: 16, top: '50%', transform: 'translateY(-50%)',
        width: 36, height: 36, borderRadius: '50%',
        background: BAU.paper, border: `1px solid ${BAU.ink}`,
        cursor: 'pointer', fontFamily: 'var(--mono)', fontSize: 13, color: BAU.ink,
        display: 'flex', alignItems: 'center', justifyContent: 'center',
      }}>›</button>
    </div>
  );
}

Object.assign(window, { ArtistProfile, CoverFlow, Stat, ContactCell, ContactLink });
