// James Dougall — Portfolio
// State machine: 'home' → 'expanding' (thumb grows from its rect to full viewport)
//               → 'detail' (project page)  →  'closing' → 'home'

const { useState, useEffect } = React;

const CONFIG = {
  "carouselDuration": 130,
  "fontChoice": "Space Grotesk",
  "bioName": "JAMES DOUGALL",
  "bioSubtitle": "Product Development Engineer — London",
  "bioBody": "James Dougall is a product development engineer at RHEON LABS, working across design, manufacturing, and impact testing.\n\nHis work spans CE-certified mountain bike armour for DHaRCO, the RHEON™ systems inside Ruroc's RG2 snow helmet and Ventete's inflatable AH-1, and the prototyping and test programmes behind them: injection moulding, EN 1621 certification testing, and production liaison from London to China.\n\nMEng Sports Engineering, University of Strathclyde, 2024.\n\nFor enquiries, reach out below.",
  "clientsLabel": "Worked with",
  "clientsDuration": 40
};

const LOGOS = [
  { src: 'Logos%20PNG/th.webp',                    alt: 'Adidas',     href: 'https://www.adidas.co.uk/rheon-clothing?msockid=1e5df16bd0556cfc080ee64ed14f6dd9' },
  { src: 'Logos%20PNG/Decathlon-Emblem.png',        alt: 'Decathlon',  href: 'https://www.decathlon.co.uk/p/men-s-trail-running-tights-kiprun-trail-run-900-ultra-black-x-rheon/360137/c1m8927689' },
  { src: 'Logos%20PNG/OIP.webp',                    alt: 'Ruroc',      href: 'https://www.ruroc.com/en_gb/what-is-rheon-technology' },
  { src: 'Logos%20PNG/DHarco.png',                  alt: 'DHaRCO',     href: 'https://dharco.com/pages/protection' },
  { src: 'Logos%20PNG/RHEON-LABS%C2%AE-Logo.png',  alt: 'Rheon Labs', href: 'https://rheonlabs.com/' },
  { src: 'Logos%20PNG/VENTETE_Logotype_Black.webp', alt: 'Ventete',   href: 'https://ventete.com/products/ah-1' },
  { src: 'Logos%20PNG/Scott.webp',                  alt: 'Scott Sport x University of Strathclyde', height: 72, href: 'https://www.scott-sports.com/gb/en/' },
  { src: 'Logos%20PNG/Peloton.webp',                alt: 'Peloton',     height: 72, href: 'https://www.onepeloton.com/en-GB/campaign/portfolio?utm_source=bing&utm_medium=search&utm_campaign=23527971344&utm_content=298729712819&utm_term=196575066910&utm_kwd=peloton&gclid=e18b6eb274b3108240fa0491aa8978e6&gclsrc=3p.ds&msclkid=e18b6eb274b3108240fa0491aa8978e6' },
];

function LogoTile({ logo }) {
  const img = (
    <img
      src={logo.src}
      alt={logo.alt}
      draggable={false}
      loading="eager"
      decoding="sync"
      style={{
        height: logo.height || 48,
        maxWidth: 160,
        objectFit: 'contain',
        filter: 'grayscale(100%)',
        opacity: 0.55,
        userSelect: 'none',
      }}
    />
  );
  return (
    <div style={{
      flex: '0 0 auto',
      padding: '0 40px',
      height: 80,
      display: 'flex',
      alignItems: 'center',
      justifyContent: 'center',
    }}>
      {logo.href
        ? <a href={logo.href} target="_blank" rel="noopener noreferrer"
             style={{ display: 'flex', lineHeight: 0 }}>{img}</a>
        : img}
    </div>
  );
}

function ClientStrip({ label, logos, duration }) {
  const copies = logos.length ? Math.max(2, Math.ceil(8 / logos.length) * 2) : 0;
  const sequence = Array.from({ length: copies }, () => logos).flat();

  const [ready, setReady] = useState(false);
  useEffect(() => {
    if (!logos.length) { setReady(true); return; }
    let done = 0, cancelled = false;
    const finish = () => { if (!cancelled && ++done >= logos.length) setReady(true); };
    logos.forEach(l => {
      const im = new Image();
      im.onload = finish;
      im.onerror = finish;
      im.src = l.src;
    });
    return () => { cancelled = true; };
  }, [logos]);

  return (
    <div style={{ marginTop: 30 }}>
      <div style={{
        display: 'flex', alignItems: 'center', gap: 18,
        paddingBottom: 14,
        borderBottom: '1px solid var(--rule)'
      }}>
        <div style={{
          fontFamily: 'var(--mono)',
          fontSize: 10,
          letterSpacing: 2,
          color: 'var(--mute)',
          textTransform: 'uppercase',
          whiteSpace: 'nowrap'
        }}>
          {label}
        </div>
        <div style={{ flex: 1 }} />
        <div style={{
          fontFamily: 'var(--mono)', fontSize: 10, letterSpacing: 2, color: 'var(--mute)'
        }}>{String(logos.length).padStart(2, '0')}</div>
      </div>

      <div style={{
          marginTop: 22,
          position: 'relative',
          overflow: 'hidden',
          maskImage: 'linear-gradient(to right, transparent 0, #000 6%, #000 94%, transparent 100%)',
          WebkitMaskImage: 'linear-gradient(to right, transparent 0, #000 6%, #000 94%, transparent 100%)'
        }}>
        <div style={{
          display: 'flex',
          gap: 0,
          width: 'max-content',
          animation: (ready && !window.__rheonReduced) ? `clientsRTL ${duration}s linear infinite` : 'none',
          willChange: 'transform'
        }}>
          {sequence.map((logo, i) => <LogoTile key={`${logo.alt}-${i}`} logo={logo} />)}
        </div>
        <style>{`
          @keyframes clientsRTL {
            0%   { transform: translateX(0); }
            100% { transform: translateX(-50%); }
          }
        `}</style>
      </div>
    </div>
  );
}

const TERMS_UPDATED = 'June 2026';
const TERMS = [
  { h: 'Ownership', p: ['This website and its contents — including the design, layout, code, graphics, images, text, and interactive elements — are the work and property of James Dougall, except where third-party material is explicitly credited. All rights are reserved.'] },
  { h: 'Permitted use', p: ["You're welcome to view this site and share a link to it. You may reference or discuss the work shown here with appropriate credit to James Dougall."] },
  { h: 'Restrictions', p: ['Without prior written permission from James Dougall, you may not:'], list: [
    'Copy, reproduce, or republish any part of this site or its assets',
    'Use any content, designs, or code for commercial purposes',
    'Incorporate any material here into other products, projects, or assets, whether modified or unmodified',
    'Use any content to train, fine-tune, or develop machine learning or AI systems',
  ] },
  { h: 'Confidential and client work', p: ['Some projects shown here were produced in collaboration with clients or partners and may be subject to separate agreements. Nothing on this site transfers any rights in that work, and the presence of a project here does not grant any licence to reproduce or reuse it.'] },
  { h: 'Contact', p: ['For permissions or enquiries:'], email: 'Jamie.dougall@icloud.com' },
];

function TermsModal({ open, onClose }) {
  if (!open) return null;
  return (
    <div onClick={onClose} style={{
      position: 'fixed', inset: 0, background: 'rgba(10,10,10,0.45)', zIndex: 210,
      display: 'flex', alignItems: 'center', justifyContent: 'center', padding: 48,
    }}>
      <div onClick={(e) => e.stopPropagation()} style={{
        background: 'var(--paper)', padding: '52px 64px 48px', width: 660, maxWidth: '100%',
        maxHeight: '82%', overflowY: 'auto', overscrollBehavior: 'contain', position: 'relative',
        animation: 'contactIn 200ms cubic-bezier(.2,.7,.2,1) both',
      }} onWheel={(e) => e.stopPropagation()}>
        <button onClick={onClose} aria-label="Close" style={{
          position: 'absolute', top: 20, right: 24, background: 'none', border: 'none',
          cursor: 'pointer', padding: 0, fontFamily: 'var(--mono)', fontSize: 22,
          letterSpacing: 2, color: 'var(--mute)',
        }}>✕</button>

        <div style={{ fontFamily: 'var(--mono)', fontSize: 10, letterSpacing: 3, color: 'var(--mute)' }}>
          TERMS OF USE
        </div>
        <div style={{ fontFamily: 'var(--mono)', fontSize: 10, letterSpacing: 2, color: 'var(--mute)', marginTop: 8, marginBottom: 32 }}>
          LAST UPDATED — {TERMS_UPDATED.toUpperCase()}
        </div>

        {TERMS.map((sec, i) => (
          <section key={i} style={{ marginBottom: 28 }}>
            <h3 style={{
              fontFamily: 'var(--display)', fontStyle: 'italic', fontWeight: 500,
              fontSize: 22, lineHeight: 1.1, margin: '0 0 12px',
            }}>{sec.h}</h3>
            {sec.p.map((para, j) => (
              <p key={j} style={{
                fontFamily: 'var(--serif)', fontSize: 15, lineHeight: 1.6,
                color: 'var(--ink)', margin: '0 0 10px',
              }}>{para}</p>
            ))}
            {sec.list && (
              <ul style={{
                margin: '6px 0 0', paddingLeft: 20,
                fontFamily: 'var(--serif)', fontSize: 15, lineHeight: 1.6, color: 'var(--ink)',
              }}>
                {sec.list.map((li, j) => <li key={j} style={{ marginBottom: 6 }}>{li}</li>)}
              </ul>
            )}
            {sec.email && (
              <a href={`mailto:${sec.email}`} style={{
                fontFamily: 'var(--display)', fontStyle: 'italic', fontSize: 18,
                color: 'var(--ink)', textDecoration: 'none',
                borderBottom: '1px solid var(--rule)', paddingBottom: 2,
              }}>{sec.email}</a>
            )}
          </section>
        ))}
      </div>
    </div>
  );
}

// Stacked single-column home for small screens. Rendered in natural document
// flow (the scale-to-fit canvas is switched off below 760px), so real px / vw
// units are safe here — they resolve against the actual viewport.
function MobileProjectCard({ project, onSelect }) {
  const { thumbEmbed, video, image } = project;
  return (
    <button type="button" onClick={() => onSelect(project, null)}
      style={{
        appearance: 'none', background: 'transparent', border: 'none', padding: 0,
        textAlign: 'left', cursor: 'pointer', color: 'var(--ink)', fontFamily: 'inherit', width: '100%'
      }}>
      <div style={{ position: 'relative', width: '100%', aspectRatio: '16 / 9', background: 'var(--grey-1)', overflow: 'hidden' }}>
        {thumbEmbed
          ? <iframe src={thumbEmbed} title={project.title} scrolling="no"
              style={{ position: 'absolute', inset: 0, width: '100%', height: '100%', border: 'none', display: 'block', pointerEvents: 'none' }} />
          : video
          ? <video src={video} autoPlay muted loop playsInline
              style={{ position: 'absolute', inset: 0, width: '100%', height: '100%', objectFit: 'cover' }} />
          : image
          ? <img src={image} alt={project.title}
              style={{ position: 'absolute', inset: 0, width: '100%', height: '100%', objectFit: 'cover' }} />
          : <div style={{ position: 'absolute', inset: 0, pointerEvents: 'none' }}>
              <ThumbPlaceholder no={project.no} title={project.title} />
            </div>}
      </div>
      <div style={{ display: 'flex', alignItems: 'baseline', gap: 10, marginTop: 12 }}>
        <span style={{ fontFamily: 'var(--mono)', fontSize: 10, letterSpacing: 2, color: 'var(--mute)' }}>{project.no}</span>
        <span style={{ fontFamily: 'var(--display)', fontStyle: 'italic', fontSize: 22, fontWeight: 500, lineHeight: 1.1, flex: 1 }}>{project.title}</span>
      </div>
      <div style={{ marginTop: 5, fontFamily: 'var(--mono)', fontSize: 10, letterSpacing: 2, color: 'var(--mute)', textTransform: 'uppercase' }}>{project.kind} — {project.year}</div>
    </button>
  );
}

function MobileHome({ t, projects, onSelect, onContact, onTerms }) {
  return (
    <div style={{ padding: '22px 20px 48px', maxWidth: 760, margin: '0 auto' }}>
      <div style={{
        display: 'flex', justifyContent: 'space-between',
        fontFamily: 'var(--mono)', fontSize: 10, letterSpacing: 2, color: 'var(--mute)'
      }}>
        <span>JD — PORTFOLIO 2026</span>
        <span>INDEX / 01–{String(projects.length).padStart(2, '0')}</span>
      </div>

      <h1 style={{
        fontFamily: 'var(--serif)', fontWeight: 700,
        fontSize: 'clamp(40px, 13vw, 84px)', lineHeight: 0.95, letterSpacing: '-0.01em',
        textWrap: 'balance', margin: '26px 0 14px'
      }}>{t.bioName}</h1>

      <div style={{
        fontFamily: 'var(--display)', fontStyle: 'italic', fontSize: 17,
        color: 'var(--mute)', marginBottom: 20, lineHeight: 1.3
      }}>{t.bioSubtitle}</div>

      <div style={{
        fontFamily: 'var(--serif)', fontSize: 16, lineHeight: 1.55,
        color: 'var(--ink)', whiteSpace: 'pre-wrap', textWrap: 'pretty'
      }}>{t.bioBody}</div>

      <ClientStrip label={t.clientsLabel} logos={LOGOS} duration={t.clientsDuration} />

      <button onClick={onContact} style={{
        marginTop: 24, background: 'none', border: 'none', cursor: 'pointer',
        padding: '0 0 4px', fontFamily: 'var(--mono)', fontSize: 11, letterSpacing: 2,
        color: 'var(--ink)', borderBottom: '1px solid var(--ink)'
      }}>CONTACT</button>

      <div style={{
        marginTop: 44, marginBottom: 18, display: 'flex', justifyContent: 'space-between',
        alignItems: 'baseline', borderBottom: '1px solid var(--rule)', paddingBottom: 12
      }}>
        <span style={{ fontFamily: 'var(--mono)', fontSize: 10, letterSpacing: 2, color: 'var(--mute)', textTransform: 'uppercase' }}>Selected work</span>
        <span style={{ fontFamily: 'var(--mono)', fontSize: 10, letterSpacing: 2, color: 'var(--mute)' }}>{String(projects.length).padStart(2, '0')}</span>
      </div>

      <div style={{ display: 'flex', flexDirection: 'column', gap: 36 }}>
        {projects.map(p => <MobileProjectCard key={p.id} project={p} onSelect={onSelect} />)}
      </div>

      <div style={{
        marginTop: 48, paddingTop: 20, borderTop: '1px solid var(--rule)',
        fontFamily: 'var(--mono)', fontSize: 10, letterSpacing: 2, color: 'var(--mute)',
        display: 'flex', flexWrap: 'wrap', gap: '8px 14px', alignItems: 'center'
      }}>
        <span>© {new Date().getFullYear()} James Dougall.</span>
        <button className="terms-link" onClick={onTerms}>Terms of Use</button>
        <span style={{ width: '100%' }}>BORN IN SCOTLAND — BASED IN LONDON</span>
      </div>
    </div>
  );
}

function App() {
  const t = CONFIG;

  const [view, setView] = useState('home'); // home | expanding | detail | closing
  const [selected, setSelected] = useState(null);
  const [originRect, setOriginRect] = useState(null);
  const [gridOpen, setGridOpen] = useState(false);
  const [contactOpen, setContactOpen] = useState(false);
  const [termsOpen, setTermsOpen] = useState(false);
  const [isMobile, setIsMobile] = useState(() => !!(typeof window !== 'undefined' && window.__rheonMobile));

  const viewRef = React.useRef(view);
  viewRef.current = view;

  useEffect(() => {
    const onVp = (e) => setIsMobile(!!(e.detail && e.detail.mobile));
    window.addEventListener('rheon:viewport', onVp);
    return () => window.removeEventListener('rheon:viewport', onVp);
  }, []);

  const closeNow = (animate) => {
    if (isMobile || !animate) {
      setView('home');
      setSelected(null);
      setOriginRect(null);
      return;
    }
    setView('closing');
    setTimeout(() => {
      setView('home');
      setSelected(null);
      setOriginRect(null);
    }, 360);
  };

  // Deep link: #p02 opens that project directly (no expand animation).
  useEffect(() => {
    const proj = PROJECTS.find(p => '#' + p.id === location.hash);
    if (proj) {
      setSelected(proj);
      setOriginRect(null);
      setView('detail');
    }
  }, []);

  // Back/forward buttons open or close the detail page instead of leaving the site.
  useEffect(() => {
    const onPop = () => {
      const proj = PROJECTS.find(p => '#' + p.id === location.hash);
      if (proj) {
        setGridOpen(false);
        setSelected(proj);
        setOriginRect(null);
        setView('detail');
      } else if (viewRef.current !== 'home') {
        closeNow(true);
      }
    };
    window.addEventListener('popstate', onPop);
    return () => window.removeEventListener('popstate', onPop);
  }, [isMobile]);

  useEffect(() => {
    const onKey = (e) => {
      if (e.key !== 'Escape') return;
      if (contactOpen) setContactOpen(false);
      else if (termsOpen) setTermsOpen(false);
      else if (gridOpen) setGridOpen(false);
      else if (viewRef.current === 'detail') handleClose();
    };
    window.addEventListener('keydown', onKey);
    return () => window.removeEventListener('keydown', onKey);
  }, [contactOpen, termsOpen, gridOpen, isMobile]);

  useEffect(() => {
    document.documentElement.style.setProperty(
      '--serif',
      `'${t.fontChoice}', Georgia, serif`
    );
  }, [t.fontChoice]);

  const handleSelect = (project, el) => {
    if (location.hash !== '#' + project.id) {
      history.pushState({ jd: project.id }, '', '#' + project.id);
    }
    if (isMobile) {
      setSelected(project);
      setView('detail');
      window.scrollTo(0, 0);
      return;
    }
    if (!el) {
      setSelected(project);
      setOriginRect(null);
      setView('detail');
      return;
    }
    const r = el.getBoundingClientRect();
    // #root has a CSS transform, so position:fixed children are positioned in
    // #root's own 1440×900 space, not the viewport. Convert accordingly.
    const rootRect = document.getElementById('root').getBoundingClientRect();
    const s = rootRect.width / 1440;
    setOriginRect({
      x: (r.left - rootRect.left) / s,
      y: (r.top  - rootRect.top)  / s,
      w: r.width  / s,
      h: r.height / s,
    });
    setSelected(project);
    setView('expanding');
    setTimeout(() => setView('detail'), 620);
  };

  const handleClose = () => {
    // With a pushed hash entry, go back and let popstate run the close, so the
    // browser back button and the ✕ stay in sync. A direct deep-link visit has
    // no entry to go back to — clear the hash in place instead.
    if (location.hash && history.state && history.state.jd) {
      history.back();
      return;
    }
    if (location.hash) history.replaceState(null, '', location.pathname + location.search);
    closeNow(true);
  };

  return (
    <>
      {/* ──────────── HOME (mobile reflow) ──────────── */}
      {isMobile &&
        <MobileHome
          t={t}
          projects={PROJECTS}
          onSelect={handleSelect}
          onContact={() => setContactOpen(o => !o)}
          onTerms={() => setTermsOpen(true)} />
      }

      {/* ──────────── HOME (desktop canvas) ──────────── */}
      {/* Unmounted while the detail page is open — otherwise the carousel's
          autoplaying videos and the lab-cover iframes keep decoding/animating
          behind the fully-opaque overlay for as long as the user stays there. */}
      {!isMobile && view !== 'detail' &&
      <main className="home" style={{
        position: 'absolute', inset: 0,
        display: 'grid',
        gridTemplateColumns: 'minmax(420px, 58%) 1fr',
        background: 'var(--paper)'
      }}>
        {/* LEFT — bio column */}
        <section style={{
          position: 'relative',
          padding: '52px 86px 40px 101px',
          display: 'flex', flexDirection: 'column',
          justifyContent: 'space-between'
        }}>
          <div style={{
            fontFamily: 'var(--mono)',
            fontSize: 11, letterSpacing: 2,
            color: 'var(--mute)',
            display: 'flex', justifyContent: 'space-between'
          }}>
            <span>JD — PORTFOLIO 2026</span>
            <span>INDEX / 01–{String(PROJECTS.length).padStart(2, '0')}</span>
          </div>

          <div style={{ flex: 1, display: 'flex', flexDirection: 'column', justifyContent: 'center', maxWidth: 680 }}>
            <h1 style={{
              fontFamily: 'var(--serif)',
              fontWeight: 700,
              fontSize: '96px',
              lineHeight: 0.92,
              letterSpacing: '-0.01em',
              textWrap: 'balance',
              margin: '10px 0px'
            }}>
              {t.bioName}
            </h1>

            <div style={{
              fontFamily: 'var(--display)',
              fontStyle: 'italic',
              fontSize: '20px',
              color: 'var(--mute)',
              marginBottom: 24,
              lineHeight: 1.3
            }}>
              {t.bioSubtitle}
            </div>

            <div style={{
              fontFamily: 'var(--serif)',
              fontSize: '15px',
              lineHeight: 1.5,
              color: 'var(--ink)',
              whiteSpace: 'pre-wrap',
              textWrap: 'pretty',
              maxWidth: '58ch'
            }}>
              {t.bioBody}
            </div>

            <ClientStrip
              label={t.clientsLabel}
              logos={LOGOS}
              duration={t.clientsDuration} />

            <div style={{ marginTop: 22 }}>
              <button
                className="hover-fade"
                onClick={() => setContactOpen(o => !o)}
                style={{
                  background: 'none', border: 'none', cursor: 'pointer', padding: 0,
                  fontFamily: 'var(--mono)', fontSize: 11, letterSpacing: 2,
                  color: 'var(--ink)',
                  paddingBottom: 4,
                }}>
                CONTACT
              </button>
            </div>
          </div>

          <div>
            <div style={{
              fontFamily: 'var(--mono)', fontSize: 11, letterSpacing: 2, color: 'var(--mute)',
              display: 'flex', justifyContent: 'space-between'
            }}>
              <span>BORN IN SCOTLAND — BASED IN LONDON</span>
              <span>HOVER ▸ CLICK</span>
            </div>
            <div style={{
              marginTop: 10,
              fontFamily: 'var(--mono)', fontSize: 11, letterSpacing: 2, color: 'var(--mute)',
              display: 'flex', alignItems: 'center', gap: 14
            }}>
              <span>© {new Date().getFullYear()} James Dougall. All rights reserved.</span>
              <button className="terms-link" onClick={() => setTermsOpen(true)}>
                Terms of Use
              </button>
            </div>
          </div>
        </section>

        {/* RIGHT — rotating thumbnail carousel */}
        <section style={{ position: 'relative' }}>
          <Carousel
            projects={PROJECTS}
            duration={t.carouselDuration}
            onSelect={handleSelect}
            paused={view !== 'home' || gridOpen}
            lockWheel={contactOpen || termsOpen} />
        </section>
      </main>
      }

      {/* GRID VIEW toggle — fixed top-right, outside the carousel */}
      {!isMobile && view === 'home' && !gridOpen &&
        <GridButton onClick={() => setGridOpen(true)} />
      }

      {/* ──────────── EXPANDING THUMB (transition) ──────────── */}
      {!isMobile && (view === 'expanding' || view === 'closing' || view === 'detail') && originRect && selected &&
        <div
          aria-hidden
          style={{
            position: 'fixed',
            top: originRect.y, left: originRect.x,
            width: originRect.w, height: originRect.h,
            background: 'var(--grey-5)',
            zIndex: 80,
            overflow: 'hidden',
            transformOrigin: 'top left',
            pointerEvents: view === 'detail' ? 'none' : undefined,
            animation: view === 'expanding' ?
              'expandThumb 620ms cubic-bezier(.6,.05,.2,1) forwards' :
              view === 'detail' ?
              'fadeCover 380ms ease forwards' :
              'collapseThumb 360ms cubic-bezier(.6,.05,.2,1) forwards'
          }}>
          <div style={{ position: 'absolute', inset: 0 }}>
            {selected.video
              ? <video src={selected.video} autoPlay muted loop playsInline
                  style={{ width: '100%', height: '100%', objectFit: 'cover' }} />
              : selected.image
                ? <img src={selected.image} alt={selected.title} style={{ width: '100%', height: '100%', objectFit: 'cover' }} />
                : <ThumbPlaceholder no={selected.no} title={selected.title} />
            }
          </div>
          <div style={{
            position: 'absolute', inset: 0,
            background: 'rgba(10,10,10,0.62)',
            display: 'flex', alignItems: 'center', justifyContent: 'flex-start',
            padding: '0 28px'
          }}>
            <div style={{ color: '#fafafa' }}>
              <div style={{ fontFamily: 'var(--mono)', fontSize: 11, letterSpacing: 2, opacity: 0.75, marginBottom: 8 }}>
                {selected.no} — {selected.kind.toUpperCase()}
              </div>
              <div style={{
                fontFamily: 'var(--display)', fontStyle: 'italic',
                fontSize: 34, lineHeight: 1.05, fontWeight: 500
              }}>{selected.title}</div>
            </div>
          </div>
        </div>
      }

      {/* ──────────── GRID VIEW ──────────── */}
      {!isMobile &&
        <GridView
          open={gridOpen}
          projects={PROJECTS}
          onClose={() => setGridOpen(false)}
          onSelect={(project, el) => {
            setGridOpen(false);
            setTimeout(() => handleSelect(project, el), 60);
          }} />
      }

      {/* ──────────── DETAIL ──────────── */}
      {view === 'detail' && selected &&
        <Detail project={selected} mobile={isMobile} onClose={handleClose} onTerms={() => setTermsOpen(true)} />
      }

      {/* ──────────── TERMS MODAL ──────────── */}
      <TermsModal open={termsOpen} onClose={() => setTermsOpen(false)} />

      {/* ──────────── CONTACT MODAL ──────────── */}
      {contactOpen &&
        <div
          onClick={() => setContactOpen(false)}
          style={{
            position: 'fixed', inset: 0,
            background: 'transparent',
            zIndex: 200,
            display: 'flex', alignItems: 'center', justifyContent: 'center',
          }}>
          <div
            onClick={(e) => e.stopPropagation()}
            style={{
              background: 'var(--paper)',
              padding: isMobile ? '48px 28px 40px' : '52px 64px 48px',
              minWidth: isMobile ? 0 : 460,
              width: isMobile ? '92vw' : 'auto',
              maxWidth: isMobile ? 420 : undefined,
              position: 'relative',
              display: 'flex', flexDirection: 'column', gap: 44,
              animation: 'contactIn 200ms cubic-bezier(.2,.7,.2,1) both',
            }}>
            <button
              onClick={() => setContactOpen(false)}
              aria-label="Close"
              style={{
                position: 'absolute', top: 20, right: 24,
                background: 'none', border: 'none', cursor: 'pointer', padding: 0,
                fontFamily: 'var(--mono)', fontSize: 22, letterSpacing: 2,
                color: 'var(--mute)',
              }}>
              ✕
            </button>
            <div style={{ fontFamily: 'var(--mono)', fontSize: 10, letterSpacing: 3, color: 'var(--mute)' }}>
              CONTACT
            </div>
            <div style={{ display: 'flex', flexDirection: 'column', gap: 10 }}>
              <div style={{ fontFamily: 'var(--mono)', fontSize: 10, letterSpacing: 2, color: 'var(--mute)' }}>
                LINKEDIN
              </div>
              <a
                href="https://www.linkedin.com/in/j-dougall"
                target="_blank"
                rel="noopener noreferrer"
                style={{
                  fontFamily: 'var(--display)', fontStyle: 'italic',
                  fontSize: 26, lineHeight: 1.2,
                  color: 'var(--ink)', textDecoration: 'none',
                  borderBottom: '1px solid var(--rule)',
                  paddingBottom: 4,
                }}>
                linkedin.com/in/j-dougall
              </a>
            </div>
            <div style={{ display: 'flex', flexDirection: 'column', gap: 10 }}>
              <div style={{ fontFamily: 'var(--mono)', fontSize: 10, letterSpacing: 2, color: 'var(--mute)' }}>
                EMAIL
              </div>
              <a
                href="mailto:jamie.dougall@icloud.com"
                style={{
                  fontFamily: 'var(--display)', fontStyle: 'italic',
                  fontSize: 26, lineHeight: 1.2,
                  color: 'var(--ink)', textDecoration: 'none',
                  borderBottom: '1px solid var(--rule)',
                  paddingBottom: 4,
                }}>
                jamie.dougall@icloud.com
              </a>
            </div>
          </div>
        </div>
      }

      {/* ──────────── EXPAND KEYFRAMES + CONTACT ANIMATION ──────────── */}
      <style>{`
        @keyframes contactIn {
          0%   { opacity: 0; transform: translateY(8px); }
          100% { opacity: 1; transform: translateY(0); }
        }
        @keyframes expandThumb {
          0%   { transform: translate(0,0); width: ${originRect ? originRect.w : 0}px; height: ${originRect ? originRect.h : 0}px; }
          100% { transform: translate(${originRect ? -originRect.x : 0}px, ${originRect ? -originRect.y : 0}px); width: 1440px; height: 900px; }
        }
        @keyframes collapseThumb {
          0%   { transform: translate(${originRect ? -originRect.x : 0}px, ${originRect ? -originRect.y : 0}px); width: 1440px; height: 900px; opacity:1; }
          100% { transform: translate(0,0); width: ${originRect ? originRect.w : 0}px; height: ${originRect ? originRect.h : 0}px; opacity:0; }
        }
        @keyframes fadeCover {
          0%   { transform: translate(${originRect ? -originRect.x : 0}px, ${originRect ? -originRect.y : 0}px); width: 1440px; height: 900px; opacity: 1; }
          100% { transform: translate(${originRect ? -originRect.x : 0}px, ${originRect ? -originRect.y : 0}px); width: 1440px; height: 900px; opacity: 0; }
        }
      `}</style>

    </>
  );
}

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