// MONOLITH v2 — focused single direction, all structural requests
const H = window.MASSIVAN_HELPERS;

function MassivanLogo({ size = 24, tone = 'dark' }) {
  // Placeholder wordmark — drop-in site logo lives in <img src="assets/logo.svg">
  // If that file doesn't exist, fall back to typeset wordmark.
  const [hasLogo, setHasLogo] = React.useState(true);
  return hasLogo ? (
    <img
      src="assets/logo.png"
      alt="Massivan"
      style={{ height: size, width: 'auto', display: 'block', filter: tone === 'light' ? 'invert(1)' : 'none' }}
      onError={() => setHasLogo(false)}
    />
  ) : (
    <span style={{ fontFamily: 'var(--display)', fontWeight: 800, fontSize: size * 0.8, letterSpacing: '0.02em', textTransform: 'uppercase' }}>Massivan</span>
  );
}

function BigLogo({ isMobile }) {
  const [hasLogo, setHasLogo] = React.useState(true);
  const dispRef = React.useRef(null);
  const turbRef = React.useRef(null);
  const rafRef  = React.useRef(null);

  // Drive the SVG displacement scale via rAF. On hover: ramp up, then ramp
  // down to a tiny ambient ripple. On hover-out: ease back to 0.
  // Also slowly animate the turbulence baseFrequency so the ripple "moves"
  // like water surface, not a frozen distortion.
  const onEnter = (e) => {
    if (rafRef.current) cancelAnimationFrame(rafRef.current);
    // Camera-flash burst at the cursor position
    const host = e.currentTarget;
    const rect = host.getBoundingClientRect();
    const x = e.clientX - rect.left;
    const y = e.clientY - rect.top;
    const flash = document.createElement('div');
    flash.className = 'logo-flash';
    flash.style.left = x + 'px';
    flash.style.top = y + 'px';
    host.appendChild(flash);
    // remove after animation completes
    setTimeout(() => flash.remove(), 700);
    const t0 = performance.now();
    const peak = 18;       // px of max distortion at impact
    const settle = 3;      // px of residual ambient ripple while hovering
    const tick = (now) => {
      const t = (now - t0) / 1000; // seconds
      // Splash curve: spike fast, decay over ~1s to the settle level
      const splash = peak * Math.exp(-t * 3.2);
      const scale = Math.max(splash, settle);
      // Slowly drift the noise pattern so the ripple isn't static
      const freq = 0.012 + 0.004 * Math.sin(t * 1.6);
      if (dispRef.current) dispRef.current.setAttribute('scale', scale.toFixed(2));
      if (turbRef.current) turbRef.current.setAttribute('baseFrequency', freq.toFixed(4));
      rafRef.current = requestAnimationFrame(tick);
    };
    rafRef.current = requestAnimationFrame(tick);
  };
  const onLeave = () => {
    if (rafRef.current) cancelAnimationFrame(rafRef.current);
    const t0 = performance.now();
    // Read current scale and decay smoothly back to 0
    const start = parseFloat(dispRef.current?.getAttribute('scale') || '0');
    const dur = 1400; // ms — slow ripple-settle
    const tick = (now) => {
      const k = Math.min(1, (now - t0) / dur);
      // ease-out cubic
      const ease = 1 - Math.pow(1 - k, 3);
      const scale = start * (1 - ease);
      if (dispRef.current) dispRef.current.setAttribute('scale', scale.toFixed(2));
      if (k < 1) rafRef.current = requestAnimationFrame(tick);
    };
    rafRef.current = requestAnimationFrame(tick);
  };
  // Hover-only ripple — no auto-pulse on the hero logo.
  React.useEffect(() => () => { if (rafRef.current) cancelAnimationFrame(rafRef.current); }, []);

  return (
    <div
      className="logo-glitch"
      onMouseEnter={onEnter}
      onMouseLeave={onLeave}
      style={{ position: 'relative', maxWidth: 1400, margin: '0 auto' }}
    >
      {/* SVG filter: turbulence noise displaces the logo image */}
      <svg width="0" height="0" style={{ position: 'absolute' }} aria-hidden="true">
        <filter id="massivan-ripple">
          <feTurbulence
            ref={turbRef}
            type="fractalNoise"
            baseFrequency="0.012"
            numOctaves="2"
            seed="2"
            result="noise"
          />
          <feDisplacementMap
            ref={dispRef}
            in="SourceGraphic"
            in2="noise"
            scale="0"
            xChannelSelector="R"
            yChannelSelector="G"
          />
        </filter>
      </svg>

      <div className="logo-stack">
        {hasLogo ? (
          <img
            src="assets/logo.png"
            alt="Massivan"
            onError={() => setHasLogo(false)}
            style={{ maxHeight: isMobile ? 180 : 340, objectFit: 'contain' }}
          />
        ) : (
          <h1 className="logo-fallback" style={{
            fontFamily: 'var(--display)', fontWeight: 800,
            fontSize: `clamp(88px, ${isMobile ? '22vw' : '15vw'}, 320px)`,
            lineHeight: 0.82, letterSpacing: '-0.04em', textTransform: 'uppercase',
            margin: 0, textAlign: 'center',
          }}>
            Massiv<span style={{ color: 'var(--accent)' }}>a</span>n
          </h1>
        )}
      </div>
      {!hasLogo && (
        <div style={{
          position: 'absolute', bottom: -10, right: 0,
          fontFamily: 'var(--mono)', fontSize: 10, letterSpacing: '0.2em', textTransform: 'uppercase', opacity: 0.35,
        }}>
          ↑ Drop assets/logo.png to replace
        </div>
      )}
    </div>
  );
}

// One-shot chromatic-split glitch on a heading. Mounts on every section change
// because each section's heading remounts when the user switches tabs.
function GlitchTitle({ children, style, as = 'h2' }) {
  const Tag = as;
  const ref = React.useRef(null);
  React.useEffect(() => {
    let timeoutId;
    const fire = () => {
      const el = ref.current;
      if (!el) return;
      el.classList.remove('auto-glitch');
      // force reflow so the animation can restart
      void el.offsetWidth;
      el.classList.add('auto-glitch');
      timeoutId = setTimeout(() => el?.classList.remove('auto-glitch'), 700);
    };
    fire();
    const intervalId = setInterval(fire, 5000);
    return () => { clearInterval(intervalId); clearTimeout(timeoutId); };
  }, []);
  return (
    <Tag ref={ref} className="glitch-title" style={style}>
      <span className="gt-ghost gt-r" aria-hidden="true">{children}</span>
      <span className="gt-ghost gt-c" aria-hidden="true">{children}</span>
      <span className="gt-main">{children}</span>
    </Tag>
  );
}

function App() {
  const { tweaks, setTweaks, editMode } = window.useTweaks();
  const [playing, setPlaying] = React.useState(null);
  const [section, setSection] = React.useState('index');
  const [navOpen, setNavOpen] = React.useState(false);

  // Scroll to top on every section change so a new page never opens mid-scroll.
  React.useEffect(() => {
    window.scrollTo(0, 0);
  }, [section]);

  // Lock body scroll while the mobile menu is open
  React.useEffect(() => {
    if (!navOpen) return;
    const prev = document.body.style.overflow;
    document.body.style.overflow = 'hidden';
    return () => { document.body.style.overflow = prev; };
  }, [navOpen]);

  const [isMobile, setIsMobile] = React.useState(typeof window !== 'undefined' ? window.innerWidth < 760 : false);
  React.useEffect(() => {
    const on = () => setIsMobile(window.innerWidth < 760);
    window.addEventListener('resize', on);
    return () => window.removeEventListener('resize', on);
  }, []);

  const onPlay = (r) => setPlaying(r);

  return (
    <div className={tweaks.grain ? 'grain' : ''} style={{
      '--display': '"Archivo Narrow", "Oswald", Impact, sans-serif',
      '--body': '"Inter", system-ui, sans-serif',
      '--mono': '"JetBrains Mono", ui-monospace, monospace',
      '--bg': '#ece8dd',
      '--fg': '#0a0a0a',
      '--muted': '#5b5a52',
      '--accent': tweaks.accent || '#c6311a',
      '--line': '#0a0a0a',
      background: 'var(--bg)', color: 'var(--fg)', minHeight: '100vh',
      fontFamily: 'var(--body)', paddingBottom: 120,
    }}>
      {/* Top bar */}
      <header style={{
        display: 'flex', alignItems: 'center', justifyContent: 'space-between',
        padding: '14px 24px', borderBottom: '1px solid var(--line)',
        position: 'sticky', top: 0, background: 'var(--bg)', zIndex: 40,
        fontFamily: 'var(--mono)', fontSize: 11, letterSpacing: '0.15em', textTransform: 'uppercase',
      }}>
        <button onClick={() => setSection('index')} style={{ background: 'none', border: 'none', padding: 0, cursor: 'pointer', display: 'flex', alignItems: 'center', gap: 14, color: 'var(--fg)', font: 'inherit', letterSpacing: 'inherit', textTransform: 'inherit' }}>
          <MassivanLogo size={22} tone="dark"/>
          <span style={{ opacity: 0.5, fontFamily: 'var(--mono)', fontSize: 11, letterSpacing: '0.15em', textTransform: 'uppercase' }}>— Archive 14/26</span>
        </button>
        {!isMobile ? (
          <nav style={{ display: 'flex', gap: 20, alignItems: 'center' }}>
            {[
              ['index', '§01 Index'],
              ['releases', '§02 Catalogue'],
              ['labels', '§03 Labels'],
              ['credits', '§04 Credits'],
              ['bio', '§05 Bio'],
              ['shop', '§06 Shop'],
            ].map(([k, l]) => (
              <button key={k} onClick={() => setSection(k)} style={{
                background: 'none', border: 'none', padding: 0, cursor: 'pointer',
                fontFamily: 'inherit', fontSize: 'inherit', letterSpacing: 'inherit', textTransform: 'inherit',
                color: section === k ? 'var(--accent)' : 'var(--fg)',
                textDecoration: section === k ? 'line-through' : 'none',
              }}>{l}</button>
            ))}
            <a href="https://massivan.bandcamp.com" target="_blank" rel="noopener noreferrer" style={{ background: '#1da0c3', color: '#fff', border: '1px solid #1da0c3', padding: '6px 10px', fontFamily: 'inherit', fontSize: 'inherit', letterSpacing: 'inherit', textTransform: 'inherit', textDecoration: 'none' }}>
              ↗ Bandcamp
            </a>
          </nav>
        ) : (
          <button onClick={() => setNavOpen(v => !v)} style={{ background: 'none', border: '1px solid var(--fg)', padding: '6px 10px', fontFamily: 'inherit', fontSize: 'inherit', letterSpacing: 'inherit', textTransform: 'inherit', cursor: 'pointer', color: 'var(--fg)' }}>
            {navOpen ? 'Close' : 'Menu'}
          </button>
        )}
      </header>
      {isMobile && navOpen && (
        <>
          {/* dim backdrop — also catches taps to close */}
          <div onClick={() => setNavOpen(false)} style={{
            position: 'fixed', inset: 0, top: 51, zIndex: 39,
            background: 'rgba(10,10,10,0.4)',
          }}/>
          <nav style={{
            position: 'fixed', top: 51, left: 0, right: 0, zIndex: 41,
            display: 'flex', flexDirection: 'column',
            background: 'var(--bg)',
            borderBottom: '1px solid var(--line)',
            fontFamily: 'var(--mono)', fontSize: 12, letterSpacing: '0.15em', textTransform: 'uppercase',
            maxHeight: 'calc(100vh - 51px)', overflowY: 'auto',
          }}>
            {[['index','§01 Index'],['releases','§02 Catalogue'],['labels','§03 Labels'],['credits','§04 Credits'],['bio','§05 Bio'],['shop','§06 Shop']].map(([k, l]) => (
              <button key={k} onClick={() => { setSection(k); setNavOpen(false); }} style={{
                background: 'var(--bg)', border: 'none', borderTop: '1px solid var(--line)',
                padding: '14px 24px', textAlign: 'left', cursor: 'pointer',
                fontFamily: 'inherit', fontSize: 'inherit', letterSpacing: 'inherit', textTransform: 'inherit',
                color: section === k ? 'var(--accent)' : 'var(--fg)',
              }}>{l}</button>
            ))}
            <a href="https://massivan.bandcamp.com" target="_blank" rel="noopener noreferrer" style={{ background: 'var(--bg)', borderTop: '1px solid var(--line)', padding: '14px 24px', textDecoration: 'none', color: 'var(--fg)', fontFamily: 'inherit', fontSize: 'inherit', letterSpacing: 'inherit', textTransform: 'inherit' }}>
              ↗ Bandcamp
            </a>
          </nav>
        </>
      )}

      {section === 'index'    && <Hero isMobile={isMobile} onPlay={onPlay} goto={setSection} />}
      {section === 'releases' && <Catalogue isMobile={isMobile} onPlay={onPlay} />}
      {section === 'labels'   && <Labels isMobile={isMobile} onPlay={onPlay} />}
      {section === 'credits' && <Credits isMobile={isMobile} onPlay={onPlay} />}
      {section === 'bio'      && <Bio isMobile={isMobile} />}
      {section === 'shop'     && <Shop isMobile={isMobile} onPlay={onPlay} />}

      <Footer />

      <window.NowPlaying release={playing} onClose={() => setPlaying(null)} />

      {editMode && (
        <div id="tweak-panel">
          <h4>Tweaks</h4>
          <div className="tweak-row">
            <div className="tweak-row-label">Accent colour</div>
            <div className="tweak-btn-group">
              {[['#c6311a','Ember'],['#3a6ae0','Cobalt'],['#2e8b57','Moss'],['#8a3aa0','Violet'],['#0a0a0a','Ink']].map(([c, n]) => (
                <button key={c} onClick={() => setTweaks({ accent: c })}
                  className={"tweak-btn" + (tweaks.accent === c ? ' active' : '')}
                  style={{ background: tweaks.accent === c ? c : 'transparent', borderColor: c }}
                >{n}</button>
              ))}
            </div>
          </div>
          <div className="tweak-row">
            <div className="tweak-row-label">Paper grain</div>
            <div className="tweak-btn-group">
              <button className={"tweak-btn" + (tweaks.grain ? ' active' : '')} onClick={() => setTweaks({ grain: true })}>On</button>
              <button className={"tweak-btn" + (!tweaks.grain ? ' active' : '')} onClick={() => setTweaks({ grain: false })}>Off</button>
            </div>
          </div>
          <div className="tweak-row">
            <div className="tweak-row-label">Label badges in catalogue</div>
            <div className="tweak-btn-group">
              <button className={"tweak-btn" + (tweaks.labelBadges ? ' active' : '')} onClick={() => setTweaks({ labelBadges: true })}>Show</button>
              <button className={"tweak-btn" + (!tweaks.labelBadges ? ' active' : '')} onClick={() => setTweaks({ labelBadges: false })}>Hide</button>
            </div>
          </div>
        </div>
      )}
    </div>
  );
}

// ── HERO ─────────────────────────────────────────────────────────────
function Hero({ isMobile, onPlay, goto }) {
  const data = window.MASSIVAN_DATA;
  const feature = data.releases.find(r => r.featured);
  const byFormat = {
    lp: data.releases.filter(r => r.format === 'lp').length,
    ep: data.releases.filter(r => r.format === 'ep').length,
    single: data.releases.filter(r => r.format === 'single').length,
  };
  return (
    <>
      <section style={{ padding: isMobile ? '40px 24px 24px' : '80px 40px 40px', borderBottom: '1px solid var(--line)' }}>
        <div style={{ fontFamily: 'var(--mono)', fontSize: 11, letterSpacing: '0.2em', textTransform: 'uppercase', marginBottom: 24, display: 'flex', justifyContent: 'space-between', maxWidth: 1400, margin: '0 auto 24px', flexWrap: 'wrap', gap: 8 }}>
          <span>Producer · Composer · Selector</span>
          <span>Formentera / Ibiza / Zürich · EST. 1997</span>
        </div>
        <BigLogo isMobile={isMobile}/>
        <div style={{
          display: 'grid', gridTemplateColumns: isMobile ? '1fr' : '2fr 1fr',
          gap: isMobile ? 24 : 60, marginTop: 40, maxWidth: 1400, marginInline: 'auto',
          paddingTop: 24, borderTop: '1px solid var(--line)',
        }}>
          <p style={{
            fontFamily: 'var(--display)', fontWeight: 400,
            fontSize: isMobile ? 26 : 38, lineHeight: 1.1, letterSpacing: '-0.015em',
            margin: 0, textWrap: 'balance',
          }}>
            A catalogue of slow-building music — chillout, nu-jazz, deep house and the occasional techno record — <span style={{ color: 'var(--accent)' }}>streamed in full and sold direct</span>, without compression, on Bandcamp.
          </p>
          <div style={{ fontFamily: 'var(--mono)', fontSize: 12, lineHeight: 1.7, letterSpacing: '0.04em' }}>
            <div style={{ borderTop: '1px solid var(--line)', paddingTop: 8, marginBottom: 16 }}>
              <div style={{ opacity: 0.5 }}>CATALOGUE</div>
              <div>{byFormat.lp} LPs · {byFormat.ep} EPs · {byFormat.single} singles</div>
            </div>
            <div style={{ borderTop: '1px solid var(--line)', paddingTop: 8, marginBottom: 16 }}>
              <div style={{ opacity: 0.5 }}>FORMATS</div>
              <div>MP3 320 / WAV 24-48 / FLAC</div>
            </div>
            <div style={{ borderTop: '1px solid var(--line)', paddingTop: 8 }}>
              <div style={{ opacity: 0.5 }}>LABELS</div>
              <div>Pmusica · Modest Electronica · ext.</div>
            </div>
          </div>
        </div>
      </section>

      {/* Portrait strip — small, just a mood-setter between the hero copy and the featured release */}
      <section style={{ padding: isMobile ? '24px 24px 0' : '32px 40px 0' }}>
        <div style={{ maxWidth: 1400, margin: '0 auto', display: 'grid', gridTemplateColumns: isMobile ? '1fr' : '220px 1fr', gap: isMobile ? 20 : 32, alignItems: 'end' }}>
          <figure style={{ margin: 0, position: 'relative' }}>
            <img src="assets/Profile_pic.jpeg" alt="Massivan portrait" style={{ width: '100%', maxWidth: isMobile ? 240 : 220, height: 'auto', display: 'block', filter: 'grayscale(0.15) contrast(1.05)', border: '1px solid var(--line)' }}/>
            <figcaption style={{ fontFamily: 'var(--mono)', fontSize: 9, letterSpacing: '0.2em', textTransform: 'uppercase', opacity: 0.55, marginTop: 6 }}>Fig. 01 — Portrait, Zürich · ph. Juventino Mateo Leon</figcaption>
          </figure>
          <div style={{ fontFamily: 'var(--mono)', fontSize: 11, letterSpacing: '0.18em', textTransform: 'uppercase', opacity: 0.65, lineHeight: 1.7, paddingBottom: isMobile ? 0 : 4 }}>
            <div style={{ borderTop: '1px solid var(--line)', paddingTop: 10 }}>Ivan Pezzini · b. 1971, Switzerland</div>
            <div>Producer, composer, selector — recording as Massivan since 1997.</div>
          </div>
        </div>
      </section>

      {/* Featured release */}
      <section style={{ padding: isMobile ? '32px 24px' : '60px 40px', borderBottom: '1px solid var(--line)', borderTop: '1px solid var(--line)', marginTop: isMobile ? 24 : 32 }}>
        <div style={{ maxWidth: 1400, margin: '0 auto' }}>
          <div style={{ fontFamily: 'var(--mono)', fontSize: 11, letterSpacing: '0.2em', textTransform: 'uppercase', marginBottom: 20, display: 'flex', justifyContent: 'space-between', flexWrap: 'wrap', gap: 8 }}>
            <span>→ Latest · {feature.catNo || `MSV—${String(feature.year).slice(-2)}`} · {H.formatLabel(feature.format)}</span>
            <span>{H.labelName(feature)}{feature.artist && feature.artist !== 'Massivan' ? ` · ${feature.artist}` : ''}</span>
          </div>
          <div style={{ display: 'grid', gridTemplateColumns: isMobile ? '1fr' : '1fr 1fr', gap: isMobile ? 24 : 48, alignItems: 'end' }}>
            <window.AlbumArt release={feature} size={isMobile ? 320 : 500} />
            <div>
              <h2 style={{ fontFamily: 'var(--display)', fontSize: isMobile ? 48 : 80, lineHeight: 0.9, margin: 0, fontWeight: 800, letterSpacing: '-0.03em', textTransform: 'uppercase' }}>{feature.title}</h2>
              <p style={{ fontSize: 16, lineHeight: 1.5, maxWidth: 480, marginTop: 20, textWrap: 'pretty' }}>{feature.blurb}</p>
              <div style={{ display: 'flex', gap: 12, marginTop: 24, flexWrap: 'wrap' }}>
              {H.hasBandcamp(feature) ? (
                <>
                  <button onClick={() => onPlay(feature)} style={btn(true)}>▶ Stream full LP</button>
                  <a href={feature.bandcamp} target="_blank" rel="noopener noreferrer" style={{ ...btn(false), textDecoration: 'none', background: '#1da0c3', color: '#fff', borderColor: '#1da0c3' }}>Buy on Bandcamp ↗</a>
                </>
              ) : (
                <>
                  <a href={H.spotifyLink(feature)} target="_blank" rel="noopener noreferrer" style={{ ...btn(true), textDecoration: 'none', background: '#1db954', color: '#000', borderColor: '#1db954' }}>▶ Listen on Spotify</a>
                  <span style={{ ...btn(false), opacity: 0.5, cursor: 'default' }}>Licensed to {H.labelName(feature)}</span>
                </>
              )}
              </div>
              {feature.bandcampEmbed && (
                <div style={{ marginTop: 20, border: '1px solid var(--line)' }}>
                  <window.BandcampEmbed release={feature} compact />
                </div>
              )}
              <div style={{ display: 'grid', gridTemplateColumns: 'repeat(3, 1fr)', gap: 0, marginTop: 32, borderTop: '1px solid var(--line)' }}>
                {[['Tracks', feature.tracks], ['Length', feature.duration], ['Year', feature.year]].filter(([, v]) => v != null).map(([k, v]) => (
                  <div key={k} style={{ padding: '12px 0', borderRight: '1px solid var(--line)', paddingRight: 12 }}>
                    <div style={{ fontFamily: 'var(--mono)', fontSize: 10, letterSpacing: '0.2em', opacity: 0.5, textTransform: 'uppercase' }}>{k}</div>
                    <div style={{ fontFamily: 'var(--display)', fontSize: 24, fontWeight: 700 }}>{v}</div>
                  </div>
                ))}
              </div>
            </div>
          </div>
        </div>
      </section>

      {/* Quick catalogue preview */}
      <section style={{ padding: isMobile ? '32px 24px' : '60px 40px' }}>
        <div style={{ maxWidth: 1400, margin: '0 auto' }}>
          <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'baseline', flexWrap: 'wrap', gap: 8 }}>
            <h3 style={{ fontFamily: 'var(--display)', fontSize: isMobile ? 32 : 48, margin: 0, fontWeight: 700, letterSpacing: '-0.02em', textTransform: 'uppercase' }}>Selected catalogue</h3>
            <button onClick={() => goto('releases')} style={{ background: 'none', border: 'none', fontFamily: 'var(--mono)', fontSize: 11, letterSpacing: '0.2em', textTransform: 'uppercase', cursor: 'pointer', color: 'var(--accent)' }}>View all →</button>
          </div>
          <div style={{ borderTop: '1px solid var(--line)', marginTop: 20 }}>
            {data.releases.slice(0, 6).map((r, i) => <CatalogueRow key={r.id} r={r} i={i} isMobile={isMobile} onPlay={onPlay} />)}
          </div>
        </div>
      </section>
    </>
  );
}

// ── CATALOGUE ROW ─────────────────────────────────────────────────────
function CatalogueRow({ r, i, isMobile, onPlay }) {
  const [hover, setHover] = React.useState(false);
  const labelKind = H.labelKind(r);
  const { tweaks } = window.useTweaks();
  const showBadges = tweaks.labelBadges !== false;
  return (
    <div
      onMouseEnter={() => setHover(true)} onMouseLeave={() => setHover(false)}
      onClick={() => onPlay(r)}
      style={{
        display: 'grid',
        gridTemplateColumns: isMobile ? '60px 1fr auto' : '48px 1.2fr 0.6fr 1fr 60px 80px 90px',
        gap: 14, padding: '16px 0',
        borderBottom: '1px solid var(--line)', cursor: 'pointer',
        background: hover ? 'var(--fg)' : 'transparent',
        color: hover ? 'var(--bg)' : 'var(--fg)',
        transition: 'background 80ms, color 80ms',
        alignItems: 'center',
        fontFamily: 'var(--mono)', fontSize: 12, letterSpacing: '0.04em',
      }}
    >
      <div style={{ opacity: 0.5, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>{r.catNo || `§${String(i+1).padStart(2,'0')}`}</div>
      <div style={{ minWidth: 0 }}>
        <div style={{ fontFamily: 'var(--display)', fontSize: isMobile ? 18 : 26, fontWeight: 700, letterSpacing: '-0.02em', textTransform: 'uppercase', lineHeight: 1.05, wordBreak: 'break-word' }}>
          {r.title}
        </div>
        {r.artist && r.artist !== 'Massivan' && (
          <div style={{ fontFamily: 'var(--mono)', fontSize: 10, letterSpacing: '0.15em', textTransform: 'uppercase', opacity: 0.65, marginTop: 4 }}>
            {r.artist}{r.producerCredit ? ` · ${r.producerCredit}` : ''}
          </div>
        )}
        {(!r.artist || r.artist === 'Massivan') && r.producerCredit && (
          <div style={{ fontFamily: 'var(--mono)', fontSize: 10, letterSpacing: '0.15em', textTransform: 'uppercase', opacity: 0.65, marginTop: 4 }}>
            {r.producerCredit}
          </div>
        )}
      </div>
      {!isMobile && <FormatBadge format={r.format} />}
      {!isMobile && showBadges && <LabelBadge release={r} kind={labelKind} inverted={hover} />}
      {!isMobile && !showBadges && <div></div>}
      {!isMobile && <div>{r.year}</div>}
      {!isMobile && <div>{r.duration || ''}</div>}
      <div style={{ textAlign: 'right', textTransform: 'uppercase', opacity: hover ? 1 : 0.7 }}>
        {hover ? (H.hasBandcamp(r) ? '▶ Play' : '↗ Spotify') : H.formatLabel(r.format).toUpperCase()}
      </div>
    </div>
  );
}

function FormatBadge({ format }) {
  const label = H.formatLabel(format);
  return (
    <span style={{
      fontFamily: 'var(--mono)', fontSize: 10, letterSpacing: '0.18em', textTransform: 'uppercase',
      border: '1px solid currentColor', padding: '3px 6px',
      justifySelf: 'start', opacity: 0.85,
    }}>{label}</span>
  );
}

function LabelBadge({ release, kind, inverted }) {
  const name = H.labelName(release);
  const dot = kind === 'external' ? 'transparent' : 'currentColor';
  return (
    <span style={{
      fontFamily: 'var(--mono)', fontSize: 10, letterSpacing: '0.14em', textTransform: 'uppercase',
      opacity: 0.85, display: 'flex', alignItems: 'center', gap: 6,
    }}>
      <span style={{ width: 7, height: 7, borderRadius: '50%', background: dot, border: '1px solid currentColor', display: 'inline-block' }}/>
      {name}
    </span>
  );
}

// ── CATALOGUE PAGE (full, with filters) ───────────────────────────────
function Catalogue({ isMobile, onPlay }) {
  const data = window.MASSIVAN_DATA;
  const [format, setFormat] = React.useState('all');
  const [labelFilter, setLabelFilter] = React.useState('all');
  const [sort, setSort] = React.useState('newest');

  const labelsOptions = React.useMemo(() => {
    const present = new Set(data.releases.map(r => r.label));
    // Explicit order: Pmusica, Modest Electronica, then the rest alphabetically.
    const own = ['pmusica', 'modest-electronica'].filter(k => present.has(k));
    const rest = [...present].filter(k => !own.includes(k)).sort((a, b) =>
      H.labelName({ label: a }).localeCompare(H.labelName({ label: b })));
    return [...own, ...rest];
  }, []);

  // Sort by release year; use catalogue # as a stable tiebreaker within a year.
  const catKey = (s) => (s || '').toLowerCase().split(/(\d+)/).map(p => /^\d+$/.test(p) ? p.padStart(8, '0') : p).join('');
  const yearCmp = (a, b) => {
    const dir = sort === 'newest' ? -1 : 1;
    if (a.year !== b.year) return (a.year - b.year) * dir;
    const ka = catKey(a.catNo), kb = catKey(b.catNo);
    return ka < kb ? -1 * dir : ka > kb ? 1 * dir : 0;
  };

  const filtered = data.releases
    .filter(r => format === 'all' || (format === 'single-ep' ? (r.format === 'single' || r.format === 'ep') : r.format === format))
    .filter(r => labelFilter === 'all' || r.label === labelFilter)
    .sort(yearCmp);

  const grouped = {
    lp: filtered.filter(r => r.format === 'lp'),
    singlesEps: filtered.filter(r => r.format === 'ep' || r.format === 'single'),
  };

  return (
    <section style={{ padding: isMobile ? '40px 24px' : '60px 40px', borderBottom: '1px solid var(--line)' }}>
      <div style={{ maxWidth: 1400, margin: '0 auto' }}>
        <div style={{ fontFamily: 'var(--mono)', fontSize: 11, letterSpacing: '0.2em', textTransform: 'uppercase', display: 'flex', justifyContent: 'space-between', marginBottom: 16, flexWrap: 'wrap', gap: 8 }}>
          <span>§02 · Catalogue</span>
          <span>{filtered.length} of {data.releases.length}</span>
        </div>
        <GlitchTitle style={{ fontFamily: 'var(--display)', fontSize: isMobile ? 56 : 108, lineHeight: 0.88, margin: 0, fontWeight: 800, letterSpacing: '-0.04em', textTransform: 'uppercase' }}>
          The full<br/>catalogue.
        </GlitchTitle>

        {/* Filters */}
        <div style={{ marginTop: 32, paddingTop: 16, borderTop: '1px solid var(--line)', display: 'flex', gap: 32, flexWrap: 'wrap', alignItems: 'center' }}>
          <FilterGroup label="Format" value={format} onChange={setFormat} options={[
            ['all', `All (${data.releases.length})`],
            ['lp', `LPs (${data.releases.filter(r=>r.format==='lp').length})`],
            ['single-ep', `Singles & EPs (${data.releases.filter(r=>r.format==='ep'||r.format==='single').length})`],
          ]}/>
          <FilterGroup label="Label" value={labelFilter} onChange={setLabelFilter} options={[
            ['all', 'All labels'],
            ...labelsOptions.map(l => [l, H.labelName({ label: l })]),
          ]}/>
          <FilterGroup label="Sort" value={sort} onChange={setSort} options={[
            ['newest', 'Latest first'],
            ['oldest', 'Earliest first'],
          ]}/>
        </div>

        {/* Grouped: LPs, then Singles & EPs combined */}
        <div style={{ marginTop: 48 }}>
          {[
            ['lp', 'Long-players', grouped.lp],
            ['singlesEps', 'Singles & EPs', grouped.singlesEps],
          ].map(([k, title, list]) => list.length > 0 && (
            <div key={k} style={{ marginBottom: 48 }}>
              <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'baseline', paddingBottom: 12, borderBottom: '2px solid var(--line)', marginBottom: 12 }}>
                <h3 style={{ fontFamily: 'var(--display)', fontSize: isMobile ? 32 : 44, margin: 0, fontWeight: 700, letterSpacing: '-0.02em', textTransform: 'uppercase' }}>
                  {title}
                </h3>
                <span style={{ fontFamily: 'var(--mono)', fontSize: 11, letterSpacing: '0.2em', textTransform: 'uppercase', opacity: 0.6 }}>{list.length} titles</span>
              </div>
              {list.map((r, i) => <CatalogueRow key={r.id} r={r} i={i} isMobile={isMobile} onPlay={onPlay}/>)}
            </div>
          ))}
          {filtered.length === 0 && (
            <div style={{ padding: 40, textAlign: 'center', fontFamily: 'var(--mono)', fontSize: 12, opacity: 0.6 }}>No releases match these filters.</div>
          )}
        </div>
      </div>
    </section>
  );
}

function FilterGroup({ label, value, onChange, options }) {
  return (
    <div>
      <div style={{ fontFamily: 'var(--mono)', fontSize: 10, letterSpacing: '0.2em', textTransform: 'uppercase', opacity: 0.5, marginBottom: 8 }}>{label}</div>
      <div style={{ display: 'flex', gap: 4, flexWrap: 'wrap' }}>
        {options.map(([v, l]) => (
          <button key={v} onClick={() => onChange(v)} style={{
            background: value === v ? 'var(--fg)' : 'transparent',
            color: value === v ? 'var(--bg)' : 'var(--fg)',
            border: '1px solid var(--line)', padding: '6px 10px',
            fontFamily: 'var(--mono)', fontSize: 11, letterSpacing: '0.1em', textTransform: 'uppercase',
            cursor: 'pointer',
          }}>{l}</button>
        ))}
      </div>
    </div>
  );
}

// ── LABELS ────────────────────────────────────────────────────────────
function Labels({ isMobile, onPlay }) {
  const data = window.MASSIVAN_DATA;
  const ownLabels = Object.entries(window.MASSIVAN_LABELS).filter(([, l]) => l.type === 'own');
  // External labels: pull from the labels map (Pschent, Irma Dancefloor, Suicide Robot, etc.)
  // and any label that appears on a release but isn't in the map.
  const externalLabels = (() => {
    const fromMap = Object.entries(window.MASSIVAN_LABELS)
      .filter(([, l]) => l.type === 'external')
      .map(([id, l]) => ({ id, name: l.name }));
    const seen = new Set(fromMap.map(x => x.id));
    const fromReleases = [...new Set(data.releases.filter(r => H.labelKind(r) === 'external').map(r => r.label))]
      .filter(id => !seen.has(id))
      .map(id => ({ id, name: H.labelName({ label: id }) }));
    return [...fromMap, ...fromReleases].sort((a, b) => a.name.localeCompare(b.name));
  })();

  return (
    <section style={{ padding: isMobile ? '40px 24px' : '80px 40px', borderBottom: '1px solid var(--line)' }}>
      <div style={{ maxWidth: 1200, margin: '0 auto' }}>
        <div style={{ fontFamily: 'var(--mono)', fontSize: 11, letterSpacing: '0.2em', textTransform: 'uppercase', marginBottom: 16 }}>§03 · Labels</div>
        <GlitchTitle style={{ fontFamily: 'var(--display)', fontSize: isMobile ? 56 : 108, lineHeight: 0.88, margin: 0, fontWeight: 800, letterSpacing: '-0.04em', textTransform: 'uppercase' }}>
          Two houses.<br/>Some guests.
        </GlitchTitle>
        <p style={{ fontSize: 17, lineHeight: 1.5, maxWidth: 640, marginTop: 20, textWrap: 'pretty' }}>
          Most records live on one of two imprints I run — one built for house & techno, one for the quieter end of the spectrum. A handful of titles are signed to external labels.
        </p>

        <div style={{ marginTop: 48, display: 'grid', gridTemplateColumns: isMobile ? '1fr' : '1fr 1fr', gap: isMobile ? 20 : 24 }}>
          {ownLabels.map(([id, l]) => {
            const count = data.releases.filter(r => r.label === id).length;
            const logoSrc = id === 'pmusica' ? 'assets/logos/pmusica.jpg'
              : id === 'modest-electronica' ? 'assets/logos/modest.jpg'
              : null;
            return (
              <div key={id} style={{ border: '1px solid var(--line)', padding: isMobile ? 20 : 32 }}>
                <div style={{ display: 'flex', alignItems: 'flex-start', justifyContent: 'space-between', gap: 16 }}>
                  <div style={{ flex: 1, minWidth: 0 }}>
                    <div style={{ fontFamily: 'var(--mono)', fontSize: 10, letterSpacing: '0.25em', textTransform: 'uppercase', opacity: 0.5 }}>Own imprint</div>
                    <h3 style={{ fontFamily: 'var(--display)', fontSize: isMobile ? 32 : 44, margin: '8px 0 0', fontWeight: 800, letterSpacing: '-0.02em', textTransform: 'uppercase', lineHeight: 1 }}>
                      {l.name}
                    </h3>
                  </div>
                  {logoSrc && (
                    <img src={logoSrc} alt={l.name + ' logo'} style={{
                      width: isMobile ? 56 : 72, height: isMobile ? 56 : 72,
                      objectFit: 'contain', flexShrink: 0,
                      mixBlendMode: 'multiply',
                      padding: id === 'pmusica' ? (isMobile ? 10 : 14) : 0,
                    }}/>
                  )}
                </div>
                <p style={{ fontSize: 14, lineHeight: 1.5, marginTop: 12 }}>{l.desc}</p>
                <div style={{ fontFamily: 'var(--mono)', fontSize: 11, letterSpacing: '0.2em', textTransform: 'uppercase', opacity: 0.6, marginTop: 20, paddingTop: 12, borderTop: '1px solid var(--line)' }}>{count} releases</div>
                <div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fill, minmax(100px, 1fr))', gap: 8, marginTop: 12 }}>
                  {data.releases.filter(r => r.label === id).map(r => (
                    <button key={r.id} onClick={() => onPlay(r)} style={{ background: 'transparent', border: 'none', padding: 0, cursor: 'pointer', textAlign: 'left' }}>
                      <window.AlbumArt release={r} size={100}/>
                    </button>
                  ))}
                </div>
              </div>
            );
          })}
        </div>

        <div style={{ marginTop: 48, paddingTop: 24, borderTop: '1px solid var(--line)' }}>
          <div style={{ fontFamily: 'var(--mono)', fontSize: 10, letterSpacing: '0.25em', textTransform: 'uppercase', opacity: 0.5 }}>Signed to</div>
          <div style={{ display: 'grid', gridTemplateColumns: isMobile ? '1fr' : 'repeat(auto-fill, minmax(280px, 1fr))', gap: 12, marginTop: 12 }}>
            {externalLabels.map(l => {
              const releases = data.releases.filter(r => r.label === l.id);
              return (
                <div key={l.id} style={{ border: '1px solid var(--line)', padding: 18 }}>
                  <div style={{ fontFamily: 'var(--display)', fontSize: 22, fontWeight: 700, letterSpacing: '-0.01em', textTransform: 'uppercase', lineHeight: 1 }}>{l.name}</div>
                  <div style={{ fontFamily: 'var(--mono)', fontSize: 10, letterSpacing: '0.2em', textTransform: 'uppercase', opacity: 0.55, marginTop: 6 }}>
                    {releases.length} {releases.length === 1 ? 'release' : 'releases'} · licensed
                  </div>
                  <div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fill, minmax(80px, 1fr))', gap: 8, marginTop: 12 }}>
                    {releases.slice(0, 8).map(r => (
                      <button key={r.id} onClick={() => onPlay(r)} style={{ background: 'transparent', border: 'none', padding: 0, cursor: 'pointer', textAlign: 'left' }}>
                        <window.AlbumArt release={r} size={80}/>
                      </button>
                    ))}
                  </div>
                </div>
              );
            })}
          </div>
        </div>
      </div>
    </section>
  );
}

// ── CREDITS / REMIXES / PRODUCTION ────────────────────────────────────
function Credits({ isMobile, onPlay }) {
  const data = window.MASSIVAN_DATA;
  const collabs = data.releases.filter(r => /feat\.|w\/|Calmstreet|Bea Luna|Dan Sonic|Olson|Scheherazade|Anna Rea|Angela Compagnino/i.test(r.title));
  // Remixes BY Massivan — tracks by other artists that Massivan reworked.
  // Distinct from records that feature remixes OF Massivan (which live in
  // the main catalogue alongside their parent release).
  const remixesByMassivan = [
    { artist: 'Amana Melomé',     title: 'Mama Jama',                          variant: 'Massivan Rmx' },
    { artist: 'Charles Shillings', title: 'On Top',                            variant: 'Massivan Rmx' },
    { artist: 'Charles Shillings', title: 'Runway Girl',                       variant: 'Massivan Rmx' },
    { artist: 'Chris Zippel',      title: 'Glacier',                           variant: 'Massivan Rmx' },
    { artist: 'Deladap',           title: 'Kaj Tu Salas',                      variant: 'Massivan Rmx' },
    { artist: 'Engelspost',        title: 'Wind',                              variant: 'Massivan Rmx' },
    { artist: 'Ganga',             title: 'When I Close My Eyes',              variant: 'Massivan Uptempo Rmx' },
    { artist: 'Kenneth Bager',     title: 'Fragment Two',                      variant: 'Organic Massivan Rmx' },
    { artist: 'Kinski',            title: 'Zambesi',                           variant: 'Massivan Rmx feat. Bea Luna' },
    { artist: 'Negghead',          title: 'Shifting Sands',                    variant: 'Massivan Rmx' },
    { artist: 'Solarmoon',         title: 'Dibidibi',                          variant: 'Massivan Rmx' },
  ];
  const sideProjects = [
    { year: '2002', what: 'Calmstreet', role: "Duo with Jens Andersson · 'Third Wave' LP", label: 'Mole Listening Pearls' },
    { year: '2011', what: "Bea Luna — 'Formentera'", role: 'Co-producer with Wolfgang Haffner (drums)', label: 'Pmusica' },
    { year: '2000–2003', what: "Josef's Ruhe", role: 'Trio with Jens Andersson & Michael K. · progressive house vinyl', label: 'Modest Records' },
    { year: '2000–2003', what: 'And Diven', role: 'Duo with Jens Andersson', label: 'Modest Records' },
  ];
  // Compilation appearances — Massivan tracks/remixes featured on
  // various-artist compilations. Source: Discogs (24 entries, 2007–2023).
  const compilations = [
    { track: 'Lovely Day',                                  album: 'Unwind 2 — Global Grooves Vol.2', label: 'Com.Pact Records', year: 2007 },
    { track: 'Daydream',                                    album: 'Pescadores Island — Formosa Del Mar', label: 'High Note Records', year: 2007 },
    { track: 'One Of Those Days (2007 Re-Edit)',            album: 'Ibiza Global Radio Moods Vol. 2', label: 'Irma Records', year: 2007 },
    { track: 'You Have To Care About Love',                 album: 'Beijing 2008', label: 'High Note Records', year: 2008 },
    { track: 'Daydream',                                    album: 'Mobasound', label: 'Mo.Ba Sound Records', year: 2008 },
    { track: '2 B @ 1 With The World (Melodias Para Tu Sofa Edit)', album: '[City:Music Cocktail] Vol. 5', label: 'Magic Records', year: 2009 },
    { track: '2 B @ 1 With The World (Melodias Para Tu Sofa Edit)', album: 'Primavera (Selected By Jose Maria Ramon)', label: 'Pschent / Wagram', year: 2009 },
    { track: '2 B @ 1 With The World',                      album: 'Hôtel Costes 12 (Stéphane Pompougnac)', label: 'Pschent', year: 2009 },
    { track: 'Na Watoto (Wolfgang Haffner Remix)',          album: 'Café Ibiza Vol. 13', label: 'Globe', year: 2009 },
    { track: 'Glacier (Massivan Remix)',                    album: 'Blue Bar Formentera: Connected', label: 'CKP Records', year: 2009 },
    { track: 'Daydream',                                    album: 'La Nuit Vol. 2 — The Finest Of Chill House Lounge', label: 'Ministry Of Sound', year: 2009 },
    { track: "Getaway (Olson's Mary Uhana Rmx)",            album: 'Klassik Lounge Nightflight Vol. 03', label: 'Lemongrassmusic', year: 2009 },
    { track: '2 B @ 1 With The World (Melodias Para Tu Sofa Edit)', album: 'Summer Remixes 2009', label: 'Pschent', year: 2009 },
    { track: 'Mutabor',                                     album: 'Cultura Sonica Presenta 003 / Verde', label: "S'hort Records", year: 2009 },
    { track: '4 Generations',                               album: 'Buddha-Bar XII (Ravin)', label: 'George V / Wagram', year: 2010 },
    { track: "Mutabor (Kinski's Daktari Child Rework)",     album: 'Lounge Edition', label: 'Sony Music', year: 2010 },
    { track: 'Mercury',                                     album: 'Pschent Sampler Midem 2010', label: 'Pschent', year: 2010 },
    { track: 'Toi Et Moi',                                  album: 'Paris Fashion District 4', label: 'Cool D:vision', year: 2011 },
    { track: 'Blue Love (Deep Mix)',                        album: 'Pschent Sampler Midem 2011', label: 'Pschent', year: 2011 },
    { track: 'So Long',                                     album: 'Nastaw Się Na Chill Out 7', label: 'Magic Records', year: 2012 },
    { track: 'Blue Love',                                   album: 'Chillout After Midnight 4', label: 'Magnetic / Magic Records', year: 2012 },
    { track: "Wide (Razoof & Emanuel's Lasso Dub)",         album: 'Wonnemeyer — No Stress', label: 'High Music', year: 2012 },
    { track: 'Satisfied (DJ Umbi Remix)',                   album: 'Lounge Stompin 03 By Vykvet', label: 'Stomp House Records', year: 2020 },
    { track: 'Space Echoes',                                album: 'After Hour Vibes 2023', label: 'Kigen Future House Sounds', year: 2023 },
  ];

  return (
    <section style={{ padding: isMobile ? '40px 24px' : '80px 40px', borderBottom: '1px solid var(--line)' }}>
      <div style={{ maxWidth: 1200, margin: '0 auto' }}>
        <div style={{ fontFamily: 'var(--mono)', fontSize: 11, letterSpacing: '0.2em', textTransform: 'uppercase', marginBottom: 16 }}>§04 · Credits</div>
        <GlitchTitle style={{ fontFamily: 'var(--display)', fontSize: isMobile ? 56 : 108, lineHeight: 0.88, margin: 0, fontWeight: 800, letterSpacing: '-0.04em', textTransform: 'uppercase' }}>
          Remixes &amp;<br/>production.
        </GlitchTitle>
        <p style={{ fontSize: 17, lineHeight: 1.5, maxWidth: 640, marginTop: 20, textWrap: 'pretty' }}>
          Work under other names, records produced for other artists, and mix/compilation work — threaded through the main discography.
        </p>

        <div style={{ marginTop: 48, display: 'grid', gridTemplateColumns: isMobile ? '1fr' : '1fr 1fr', gap: 32 }}>
          <div>
            <div style={{ fontFamily: 'var(--mono)', fontSize: 10, letterSpacing: '0.25em', textTransform: 'uppercase', opacity: 0.5, marginBottom: 12 }}>Monikers &amp; side projects</div>
            {sideProjects.map((p, i) => (
              <div key={i} style={{ paddingBottom: 12, marginBottom: 12, borderBottom: '1px solid var(--line)' }}>
                <div style={{ fontFamily: 'var(--mono)', fontSize: 11, letterSpacing: '0.15em', opacity: 0.55, textTransform: 'uppercase' }}>{p.year} · {p.label}</div>
                <div style={{ fontFamily: 'var(--display)', fontSize: 22, fontWeight: 700, letterSpacing: '-0.01em', textTransform: 'uppercase', marginTop: 4 }}>{p.what}</div>
                <div style={{ fontSize: 13, opacity: 0.8, marginTop: 2 }}>{p.role}</div>
              </div>
            ))}
          </div>

          <div>
            <div style={{ fontFamily: 'var(--mono)', fontSize: 10, letterSpacing: '0.25em', textTransform: 'uppercase', opacity: 0.5, marginBottom: 12, display: 'flex', justifyContent: 'space-between', flexWrap: 'wrap', gap: 8 }}>
              <span>Compilations &amp; DJ work</span>
              <span style={{ opacity: 0.7 }}>Selection · {compilations.length} of many</span>
            </div>
            {/* Header row (desktop only) */}
            {!isMobile && (
              <div style={{
                display: 'grid',
                gridTemplateColumns: '60px 1.4fr 1.4fr 1fr',
                gap: 16,
                padding: '8px 0',
                borderBottom: '1px solid var(--line)',
                fontFamily: 'var(--mono)', fontSize: 9, letterSpacing: '0.2em', textTransform: 'uppercase', opacity: 0.4,
              }}>
                <span>Year</span>
                <span>Track</span>
                <span>Compilation</span>
                <span>Label</span>
              </div>
            )}
            {compilations.map((c, i) => (
              <div key={i} style={{
                display: 'grid',
                gridTemplateColumns: isMobile ? '1fr' : '60px 1.4fr 1.4fr 1fr',
                gap: isMobile ? 4 : 16,
                padding: isMobile ? '14px 0' : '12px 0',
                borderBottom: '1px solid var(--line)',
                fontSize: 14, alignItems: 'baseline',
              }}>
                <span style={{ fontFamily: 'var(--mono)', fontSize: 11, letterSpacing: '0.15em', opacity: 0.55 }}>{c.year}</span>
                <span style={{ fontWeight: 500 }}>{c.track}</span>
                <span style={{ fontStyle: 'italic', opacity: 0.85 }}>{c.album}</span>
                <span style={{ fontFamily: 'var(--mono)', fontSize: 11, letterSpacing: '0.05em', opacity: 0.65, textTransform: 'uppercase' }}>{c.label}</span>
              </div>
            ))}
          </div>
        </div>

        <div style={{ marginTop: 48 }}>
          <div style={{ fontFamily: 'var(--mono)', fontSize: 10, letterSpacing: '0.25em', textTransform: 'uppercase', opacity: 0.5, marginBottom: 12, display: 'flex', justifyContent: 'space-between', flexWrap: 'wrap', gap: 8 }}>
            <span>Remixes by Massivan</span>
            <span style={{ opacity: 0.7 }}>Selection · {remixesByMassivan.length} of many</span>
          </div>
          {!isMobile && (
            <div style={{
              display: 'grid',
              gridTemplateColumns: '1fr 1.2fr 1.2fr',
              gap: 16,
              padding: '8px 0',
              borderBottom: '1px solid var(--line)',
              fontFamily: 'var(--mono)', fontSize: 9, letterSpacing: '0.2em', textTransform: 'uppercase', opacity: 0.4,
            }}>
              <span>Artist</span>
              <span>Track</span>
              <span>Version</span>
            </div>
          )}
          {remixesByMassivan.map((r, i) => (
            <div key={i} style={{
              display: 'grid',
              gridTemplateColumns: isMobile ? '1fr' : '1fr 1.2fr 1.2fr',
              gap: isMobile ? 4 : 16,
              padding: isMobile ? '14px 0' : '12px 0',
              borderBottom: '1px solid var(--line)',
              fontSize: 14, alignItems: 'baseline',
            }}>
              <span style={{ fontFamily: 'var(--display)', fontSize: 16, fontWeight: 700, letterSpacing: '-0.01em', textTransform: 'uppercase' }}>{r.artist}</span>
              <span style={{ fontStyle: 'italic' }}>{r.title}</span>
              <span style={{ fontFamily: 'var(--mono)', fontSize: 11, letterSpacing: '0.05em', opacity: 0.65, textTransform: 'uppercase' }}>{r.variant}</span>
            </div>
          ))}
        </div>

        <div style={{ marginTop: 48 }}>
          <div style={{ fontFamily: 'var(--mono)', fontSize: 10, letterSpacing: '0.25em', textTransform: 'uppercase', opacity: 0.5, marginBottom: 12 }}>Collaborations &amp; features ({collabs.length})</div>
          <div style={{ display: 'grid', gridTemplateColumns: isMobile ? '1fr' : '1fr 1fr', gap: 8 }}>
            {collabs.map(r => (
              <button key={r.id} onClick={() => onPlay(r)} style={{ background: 'none', border: 'none', padding: '10px 0', borderBottom: '1px solid var(--line)', cursor: 'pointer', textAlign: 'left', display: 'flex', justifyContent: 'space-between', gap: 12, alignItems: 'baseline', color: 'var(--fg)', font: 'inherit' }}>
                <span style={{ fontFamily: 'var(--display)', fontSize: 16, fontWeight: 600, letterSpacing: '-0.01em' }}>{r.title}</span>
                <span style={{ fontFamily: 'var(--mono)', fontSize: 10, letterSpacing: '0.15em', opacity: 0.55, textTransform: 'uppercase', whiteSpace: 'nowrap' }}>{r.year} · {H.labelName(r)}</span>
              </button>
            ))}
          </div>
        </div>
      </div>
    </section>
  );
}

// ── BIO ───────────────────────────────────────────────────────────────
function Bio({ isMobile }) {
  const data = window.MASSIVAN_DATA;
  const bioParas = data.artist.bio;
  // Splice the two portrait images into the bio prose so they sit between paragraphs
  // rather than all clumping at the top. The hand-on-head image goes full-bleed below.
  const splitA = Math.min(1, bioParas.length - 1);
  const splitB = Math.min(3, bioParas.length - 1);
  return (
    <section style={{ padding: isMobile ? '40px 24px' : '80px 40px', borderBottom: '1px solid var(--line)' }}>
      <div style={{ maxWidth: 1100, margin: '0 auto' }}>
        <div style={{ fontFamily: 'var(--mono)', fontSize: 11, letterSpacing: '0.2em', textTransform: 'uppercase', marginBottom: 24 }}>§05 · Bio</div>
        <GlitchTitle style={{ fontFamily: 'var(--display)', fontSize: isMobile ? 48 : 88, lineHeight: 0.9, margin: 0, fontWeight: 800, letterSpacing: '-0.03em', textTransform: 'uppercase' }}>
          From a violin<br/>in Switzerland<br/>to a sunset<br/>in Ibiza.
        </GlitchTitle>
        <div style={{ display: 'grid', gridTemplateColumns: isMobile ? '1fr' : '1fr 2fr', gap: isMobile ? 24 : 48, marginTop: 48, paddingTop: 32, borderTop: '1px solid var(--line)' }}>
          <div style={{ fontFamily: 'var(--mono)', fontSize: 12, letterSpacing: '0.04em', lineHeight: 1.7 }}>
            <figure style={{ margin: '0 0 20px' }}>
              <img src="assets/_0022end_02.JPG" alt="Massivan, arms crossed" style={{ width: '100%', height: 'auto', display: 'block', border: '1px solid var(--line)', filter: 'grayscale(0.1) contrast(1.05)' }}/>
              <figcaption style={{ fontSize: 9, letterSpacing: '0.2em', opacity: 0.55, marginTop: 6, textTransform: 'uppercase' }}>Fig. 02 — Studio, Zürich · ph. Juventino Mateo Leon</figcaption>
            </figure>
            <div>Formentera / Ibiza / Zürich</div>
            <div>1997—present</div>
            <div style={{ marginTop: 16, opacity: 0.5 }}>Photography</div>
            <div style={{ textTransform: 'none', letterSpacing: '0.04em' }}>Juventino Mateo Leon <span style={{ opacity: 0.5 }}>(@juventinomateoleon)</span></div>
            <div style={{ textTransform: 'none', letterSpacing: '0.04em' }}>Ulrich Ambach <span style={{ opacity: 0.5 }}>(@uc_graphic)</span></div>
            <div style={{ marginTop: 16, opacity: 0.5 }}>Past residencies</div>
            <div>Labyrinth Club</div>
            <div>Spidergalaxy</div>
            <div>Blue Bar, Formentera</div>
            <div>Gorgeous @ Space Ibiza</div>
            <div style={{ marginTop: 16, opacity: 0.5 }}>Press & booking</div>
            <div>webmaster@massivan.com</div>
          </div>
          <div style={{ fontSize: 17, lineHeight: 1.6, textWrap: 'pretty' }}>
            {bioParas.map((p, i) => (
              <React.Fragment key={i}>
                <p style={{ margin: '0 0 20px' }}>{p}</p>
                {i === splitA && (
                  <figure style={{ margin: '32px 0', float: isMobile ? 'none' : 'right', width: isMobile ? '100%' : 280, marginLeft: isMobile ? 0 : 32, marginRight: 0 }}>
                    <img src="assets/CUT.jpg" alt="Massivan portrait, fragmented" style={{ width: '100%', height: 'auto', display: 'block', border: '1px solid var(--line)' }}/>
                    <figcaption style={{ fontFamily: 'var(--mono)', fontSize: 9, letterSpacing: '0.2em', opacity: 0.55, marginTop: 6, textTransform: 'uppercase' }}>Fig. 03 — 'Cut' (2019) · ph. Juventino Mateo Leon</figcaption>
                  </figure>
                )}
              </React.Fragment>
            ))}
            <div style={{ clear: 'both' }}></div>
          </div>
        </div>

        {/* Closing image — sits inside the column, modest size */}
        <figure style={{ margin: isMobile ? '40px 0 0' : '64px 0 0', maxWidth: 560, position: 'relative' }}>
          <img src="assets/massivan_gold.jpg" alt="Hand on head, gold tones" style={{ width: '100%', height: 'auto', maxHeight: 360, objectFit: 'cover', display: 'block', border: '1px solid var(--line)' }}/>
          <figcaption style={{ fontFamily: 'var(--mono)', fontSize: 10, letterSpacing: '0.25em', textTransform: 'uppercase', opacity: 0.6, marginTop: 8 }}>Fig. 04 — Pmusica · Calmstreet era · 2002 · ph. Ulrich Ambach</figcaption>
        </figure>
      </div>
    </section>
  );
}

// ── SHOP ──────────────────────────────────────────────────────────────
// Storefront is now Bandcamp. Each release links out to its Bandcamp page,
// where the full catalogue, the discography bundle, and high-res downloads
// (MP3 320 / WAV / FLAC) are sold directly by Bandcamp.
function Shop({ isMobile, onPlay }) {
  // Bandcamp-sellable releases, plus any spotify-only release explicitly pinned
  // into the shop (Family With 3 Hearts on Pschent, etc — they get a Spotify
  // CTA card instead of a Bandcamp one).
  const all         = window.MASSIVAN_DATA.releases;
  const sellable    = all.filter(r => H.hasBandcamp(r) || r.pinToShop);
  const spotifyOnly = all.filter(r => r.spotifyOnly && !r.pinToShop);
  const [filter, setFilter] = React.useState('all');
  const filtered = sellable.filter(r => filter === 'all' || r.format === filter);

  return (
    <section style={{ padding: isMobile ? '40px 24px' : '80px 40px', borderBottom: '1px solid var(--line)' }}>
      <div style={{ maxWidth: 1100, margin: '0 auto' }}>
        <div style={{ fontFamily: 'var(--mono)', fontSize: 11, letterSpacing: '0.2em', textTransform: 'uppercase', marginBottom: 16, display: 'flex', justifyContent: 'space-between', flexWrap: 'wrap', gap: 8 }}>
          <span>§06 · Shop · Sold on Bandcamp</span>
          <a href="https://massivan.bandcamp.com" target="_blank" rel="noopener noreferrer" style={{ color: 'var(--accent)', textDecoration: 'none' }}>↗ massivan.bandcamp.com</a>
        </div>
        <GlitchTitle style={{ fontFamily: 'var(--display)', fontSize: isMobile ? 56 : 108, lineHeight: 0.88, margin: 0, fontWeight: 800, letterSpacing: '-0.04em', textTransform: 'uppercase' }}>
          Buy the records.
        </GlitchTitle>
        <p style={{ fontSize: 16, maxWidth: 600, marginTop: 16, textWrap: 'pretty' }}>
          The full Pmusica and Modest Electronica catalogue lives on Bandcamp — stream every track, then take it home in MP3 320, lossless WAV or FLAC. The discography bundle is over there too.
        </p>

        {/* Big primary CTA — drives traffic straight to the storefront */}
        <a href="https://massivan.bandcamp.com" target="_blank" rel="noopener noreferrer" style={{
          display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: 16,
          marginTop: 32, padding: isMobile ? '20px' : '28px 32px',
          background: '#1da0c3', color: '#fff', textDecoration: 'none',
          fontFamily: 'var(--mono)', textTransform: 'uppercase', letterSpacing: '0.15em',
          flexWrap: 'wrap',
        }}>
          <div>
            <div style={{ fontSize: 10, letterSpacing: '0.25em', opacity: 0.85 }}>The full storefront</div>
            <div style={{ fontFamily: 'var(--display)', fontSize: isMobile ? 28 : 40, fontWeight: 800, letterSpacing: '-0.02em', marginTop: 4 }}>massivan.bandcamp.com →</div>
          </div>
          <div style={{ fontSize: 11, opacity: 0.85 }}>{sellable.filter(r => H.hasBandcamp(r)).length} releases · MP3 / WAV / FLAC · pay-what-you-want options</div>
        </a>

        <div style={{ marginTop: 40 }}>
          <FilterGroup label="Filter" value={filter} onChange={setFilter} options={[
            ['all', `All (${sellable.length})`],
            ['lp', `LPs (${sellable.filter(r=>r.format==='lp').length})`],
            ['ep', `EPs (${sellable.filter(r=>r.format==='ep').length})`],
            ['single', `Singles (${sellable.filter(r=>r.format==='single').length})`],
          ]}/>
        </div>

        <div style={{ marginTop: 32 }}>
          {filtered.map((r, i) => (
            <div key={r.id} style={{
              display: 'grid',
              gridTemplateColumns: isMobile ? '1fr' : '140px 1fr auto',
              gap: isMobile ? 16 : 28,
              padding: '20px 0',
              borderTop: i === 0 ? '1px solid var(--line)' : 'none',
              borderBottom: '1px solid var(--line)',
              alignItems: 'center',
            }}>
              <button onClick={() => onPlay(r)} style={{ background: 'none', border: 'none', padding: 0, cursor: 'pointer' }}>
                <window.AlbumArt release={r} size={140}/>
              </button>
              <div>
                <div style={{ fontFamily: 'var(--mono)', fontSize: 10, letterSpacing: '0.2em', textTransform: 'uppercase', opacity: 0.6, display: 'flex', gap: 12, flexWrap: 'wrap' }}>
                  <span>{r.catNo || `MSV—${String(r.year).slice(-2)}`} · {H.formatLabel(r.format)}</span>
                  <span>· {H.labelName(r)} · {r.year}</span>
                </div>
                <div style={{ fontFamily: 'var(--display)', fontSize: 26, fontWeight: 800, letterSpacing: '-0.02em', textTransform: 'uppercase', lineHeight: 1, marginTop: 4 }}>{r.title}</div>
                {r.artist && r.artist !== 'Massivan' && (
                  <div style={{ fontFamily: 'var(--mono)', fontSize: 11, letterSpacing: '0.15em', textTransform: 'uppercase', opacity: 0.8, marginTop: 4 }}>
                    {r.artist}{r.producerCredit ? ` · ${r.producerCredit.toLowerCase()}` : ''}
                  </div>
                )}
                <div style={{ fontSize: 13, opacity: 0.7, marginTop: 6 }}>{[r.tracks ? `${r.tracks} tracks` : null, r.duration, r.genre].filter(Boolean).join(' · ')}</div>
              </div>
              <div style={{ display: 'flex', flexDirection: isMobile ? 'row' : 'column', gap: 8, alignItems: 'stretch' }}>
                {H.hasBandcamp(r) ? (
                  <>
                    <button onClick={() => onPlay(r)} style={{ ...btn(false), fontSize: 11, padding: '10px 14px', whiteSpace: 'nowrap' }}>▶ Preview</button>
                    <a href={r.bandcamp} target="_blank" rel="noopener noreferrer" style={{ ...btn(true), background: '#1da0c3', borderColor: '#1da0c3', color: '#fff', textDecoration: 'none', fontSize: 11, padding: '10px 14px', whiteSpace: 'nowrap', textAlign: 'center' }}>Buy on Bandcamp ↗</a>
                  </>
                ) : (
                  <>
                    <a href={H.spotifyLink(r)} target="_blank" rel="noopener noreferrer" style={{ ...btn(true), background: '#1db954', borderColor: '#1db954', color: '#000', textDecoration: 'none', fontSize: 11, padding: '10px 14px', whiteSpace: 'nowrap', textAlign: 'center' }}>▶ Listen on Spotify</a>
                    <span style={{ fontFamily: 'var(--mono)', fontSize: 9, letterSpacing: '0.18em', textTransform: 'uppercase', opacity: 0.55, textAlign: isMobile ? 'left' : 'center', whiteSpace: 'normal' }}>Licensed to {H.labelName(r)}</span>
                  </>
                )}
              </div>
            </div>
          ))}
        </div>

        {spotifyOnly.length > 0 && (
          <div style={{ marginTop: 64 }}>
            <div style={{ fontFamily: 'var(--mono)', fontSize: 11, letterSpacing: '0.2em', textTransform: 'uppercase', opacity: 0.7, marginBottom: 8, display: 'flex', justifyContent: 'space-between', flexWrap: 'wrap', gap: 8 }}>
              <span>Not on Bandcamp · Spotify only</span>
              <span style={{ opacity: 0.6 }}>{spotifyOnly.length} titles</span>
            </div>
            <p style={{ fontSize: 14, maxWidth: 600, opacity: 0.75, marginTop: 0 }}>
              These records are either licensed to external labels or by signed artists. They aren't on the Bandcamp page — follow the links to hear them.
            </p>
            <div style={{ marginTop: 20, display: 'grid', gridTemplateColumns: isMobile ? '1fr' : '1fr 1fr', gap: 0, borderTop: '1px solid var(--line)' }}>
              {spotifyOnly.map(r => (
                <a key={r.id} href={H.spotifyLink(r)} target="_blank" rel="noopener noreferrer"
                   style={{
                     display: 'grid', gridTemplateColumns: '80px 1fr auto', gap: 16,
                     padding: '14px 0', borderBottom: '1px solid var(--line)',
                     textDecoration: 'none', color: 'inherit', alignItems: 'center',
                   }}>
                  <window.AlbumArt release={r} size={80}/>
                  <div>
                    <div style={{ fontFamily: 'var(--mono)', fontSize: 10, letterSpacing: '0.15em', textTransform: 'uppercase', opacity: 0.55 }}>
                      {r.catNo || r.year} · {H.formatLabel(r.format)} · {H.labelName(r)}
                    </div>
                    <div style={{ fontFamily: 'var(--display)', fontSize: 20, fontWeight: 700, letterSpacing: '-0.01em', lineHeight: 1.1, marginTop: 2 }}>{r.title}</div>
                    {r.artist && r.artist !== 'Massivan' && (
                      <div style={{ fontFamily: 'var(--mono)', fontSize: 10, letterSpacing: '0.15em', textTransform: 'uppercase', opacity: 0.65, marginTop: 2 }}>{r.artist}</div>
                    )}
                  </div>
                  <div style={{ background: '#1db954', color: '#000', padding: '6px 10px', fontFamily: 'var(--mono)', fontSize: 10, letterSpacing: '0.15em', textTransform: 'uppercase', fontWeight: 700, whiteSpace: 'nowrap' }}>↗ Spotify</div>
                </a>
              ))}
            </div>
          </div>
        )}
      </div>
    </section>
  );
}

function Footer() {
  return (
    <footer style={{ padding: '40px 24px', borderTop: '1px solid var(--line)', fontFamily: 'var(--mono)', fontSize: 11, letterSpacing: '0.15em', textTransform: 'uppercase', display: 'flex', justifyContent: 'space-between', flexWrap: 'wrap', gap: 16 }}>
      <span>© Massivan 1997—2026 · Formentera / Ibiza / Zürich</span>
      <span>webmaster@massivan.com · Imprint</span>
    </footer>
  );
}

function btn(primary) {
  return {
    background: primary ? 'var(--fg)' : 'transparent',
    color: primary ? 'var(--bg)' : 'var(--fg)',
    border: '1px solid var(--fg)',
    padding: '12px 20px',
    fontFamily: 'var(--mono)', fontSize: 12,
    letterSpacing: '0.15em', textTransform: 'uppercase',
    cursor: 'pointer',
  };
}

window.App = App;
