/* premium.jsx — single high-end portfolio. Modal + archive carousel + EN/PL + scroll motion. */
const { useState, useEffect, useRef, useCallback } = React;
const I18N = window.I18N,STACK = window.STACK,LINKS = window.LINKS,MAIL = window.MAIL;
const { useTweaks, TweaksPanel, TweakSection, TweakSlider, TweakRadio, TweakColor } = window;

/* Tweakable defaults — three controls that reshape the whole feel. */
const TWEAK_DEFAULTS = /*EDITMODE-BEGIN*/{
  "palette": ["#2b8bff", "#00e0ff", "#6a5cff"],
  "grit": 100,
  "atmo": "storm"
} /*EDITMODE-END*/;
const PALETTES = [
["#FF0000", "#ff1f8f", "#00d2ff"], // Thermal
["#c6ff00", "#9dff00", "#00ffa3"], // Acid
["#2b8bff", "#00e0ff", "#6a5cff"], // Ice
["#ff4400", "#ff7a00", "#ff145e"] // Inferno
];

/* ============================================================
   PROJECTS_DATA — single source of truth for every project.
   Swap the placeholder URLs with your own files later.
     mediaType : 'video' | 'image' | 'vimeo' | 'gallery'  → what the modal renders
               'vimeo'   → set vimeoId (e.g. '1199152904'), mediaUrl unused
               'gallery' → set gallery: ['img1.jpg','img2.jpg', ...], swipeable with arrows/dots
     group     : 'work'    → big "Selected Works" rows
               : 'archive' → horizontal Archive carousel
   ============================================================ */
const PROJECTS_DATA = [
// ============= SELECTED WORKS =============
{
  id: 'work-01', n: '01', group: 'work',
  title: 'Glow in the Dark — Skepta',
  description: {
    pl: 'Autorski teledysk do utworu "Glow in the Dark" brytyjsko-nigeryjskiego artysty Skepty, zrealizowany za oficjalną zgodą wytwórni Big Smoke Corporation w ramach pracy licencjackiej. Projekt oparty na strategii mixed media — animowany, trójwymiarowy awatar artysty (zaprojektowany i zanimowany w Blenderze) zderza się z surowym materiałem filmowym zarejestrowanym nocą w Poznaniu na iPhone. Estetyka czerpie z nurtu grime i motywu miejskiej inwigilacji, nawiązując do wczesnych teledysków Gorillaz. Cały proces — od modelowania i riggingu postaci, przez motion tracking, po compositing w After Effects i korekcję barwną w DaVinci Resolve — zrealizowano samodzielnie.',
    en: 'An original music video for "Glow in the Dark" by British-Nigerian artist Skepta, produced with official clearance from Big Smoke Corporation as part of a bachelor\'s thesis. Built on a mixed-media approach — a 3D animated avatar of the artist (designed and animated in Blender) collides with raw footage shot at night in Poznań on an iPhone. The aesthetic draws on grime and the theme of urban surveillance, referencing early Gorillaz music videos. The entire process — from character modelling and rigging, through motion tracking, to compositing in After Effects and colour grading in DaVinci Resolve — was carried out independently.'
  },
  tags: ['3D Animation', 'Music Video', 'Blender'],
  mediaType: 'gallery',
  gallery: [
    { type: 'vimeo', vimeoId: '1199152904' }, // teledysk
    { type: 'image', src: 'images/work-01-storyboard.jpg' }, // szkice storyboardu
    { type: 'image', src: 'images/work-01-concept-sketch.jpg' }, // szkic koncepcyjny awatara
    { type: 'image', src: 'images/work-01-model-compare.jpg' }, // wczesna wersja modelu vs finalna
    { type: 'image', src: 'images/work-01-rig.jpg' }, // rig postaci (Rigify) + render sceny
    { type: 'image', src: 'images/work-01-grading.jpg' } // przed/po korekcji barwnej
  ],
  thumb: 'images/work-01.jpg' // cover image shown on the homepage grid
},
{
  id: 'work-02', n: '02', group: 'work',
  title: 'OdkrywAI',
  description: {
    pl: 'Identyfikacja wizualna kampanii społecznej OdkrywAI, promującej inteligentne ortezy wspierające osoby z niepełnosprawnościami ruchowymi. Logo łączy sylwetkę człowieka w ruchu z literami nazwy. Identyfikacja obejmowała również oprawę graficzną towarzyszącego maratonu. Projekt zrealizowano w postaci pełnego systemu nośników: billboardu, plakatu przystankowego oraz materiałów motion — animacji sygnetu marki i spotu promującego szalik eventowy.',
    en: 'Visual identity for the OdkrywAI social campaign, promoting smart orthoses that support people with mobility disabilities. The logo combines a human figure in motion with the letters of the name. The identity also covered the graphic design of the accompanying marathon. The project was delivered as a full media system: a billboard, a bus-stop poster, and motion materials — a brand-mark animation and a promotional spot for the event scarf.'
  },
  tags: ['Visual Identity', 'Campaign', 'Print'],
  mediaType: 'gallery',
  gallery: [
    { type: 'image', src: 'images/work-02-billboard.jpg' }, // billboard mockup — slajd 1
    { type: 'image', src: 'images/work-02-zdj.png' }, // szalik — zbliżenia z boku
    { type: 'image', src: 'images/work-02-poster-car.jpg' }, // plakat na szybie samochodu
    { type: 'video', src: 'media/work-02-sygnet-anim.mp4' }, // animacja sygnetu (Behance render)
    { type: 'video', src: 'media/work-02-reklama-szalik.mp4' } // reklama szalika
  ],
  thumb: 'images/work-02-billboard.jpg' // ta sama grafika jako okładka na stronie głównej
},
{
  id: 'work-03', n: '03', group: 'work',
  title: 'Bittersweet Festival',
  description: {
    pl: 'Krótki spot animowany promujący festiwal Bittersweet, prezentujący trzech headlinerów — projekt powstały na styku języka wizualnego festiwalu i autorskiej estetyki, łączący spójną tożsamość marki z własnym podejściem do ruchu i kompozycji. Projekt uzupełnia seria trzech plakatów dedykowanych poszczególnym wykonawcom (oparta na częściowo fikcyjnym lineupie), utrzymana w tej samej kolorystyce i języku wizualnym festiwalu.',
    en: 'A short animated spot promoting the Bittersweet festival, featuring three headliners — a project built at the intersection of the festival\'s visual language and an original aesthetic, combining a coherent brand identity with a personal approach to motion and composition. The project is complemented by a series of three posters dedicated to individual performers (based on a partly fictional line-up), kept in the same colour palette and visual language as the festival.'
  },
  tags: ['Motion Graphics', 'Poster Design', 'Event Identity'],
  mediaType: 'gallery',
  gallery: [
    { type: 'video', src: 'media/work-03-concept-anim.mp4' }, // animacja koncepcyjna
    { type: 'image', src: 'images/work-03-posters.jpg' }, // 3 plakaty (Gorillaz / Radiohead / 21 Pilots)
    { type: 'video', src: 'media/work-03-comp1.mp4' } // druga animacja
  ],
  thumb: 'images/work-03-billboard.jpg' // okładka na stronie głównej
},
// ============= ARCHIVE CAROUSEL =============
{
  id: 'arc-01', n: '01', group: 'archive',
  title: 'I Lied — Poster Series',
  description: {
    pl: 'Seria trzech plakatów będących komentarzem do polityki Donalda Trumpa i zjawiska politycznego kłamstwa. Plakaty mają skłaniać do refleksji nad rozbieżnością między słowami a czynami — i nad tym, komu można ufać w przestrzeni publicznej. Jeden z plakatów z serii był prezentowany na wystawie Paste Up Warsaw w Warszawie.',
    en: 'A series of three posters commenting on Donald Trump\'s politics and the phenomenon of political lying. The posters are meant to prompt reflection on the gap between words and actions — and on who can be trusted in public space. One poster from the series was exhibited at Paste Up Warsaw in Warsaw.'
  },
  tags: ['Poster Design'],
  mediaType: 'gallery',
  gallery: [
    { type: 'image', src: 'images/arc-01-seria.png' }, // wszystkie trzy plakaty razem
    { type: 'image', src: 'images/arc-01-plakat-3.png' }, // "I will not do anything bad ever again"
    { type: 'image', src: 'images/arc-01-wystawa.png' }, // zdjęcie z wystawy Paste Up Warsaw
    { type: 'image', src: 'images/arc-01-plakat-2.png' }, // sylwetka na tle flagi
    { type: 'image', src: 'images/arc-01-plakat-1.png' } // "I LIED"
  ],
  thumb: 'images/arc-01-seria.png'
},
{
  id: 'arc-02', n: '02', group: 'archive',
  title: 'Album Cover Series',
  description: {
    pl: 'Seria autorskich projektów okładek albumowych inspirowanych twórczością artystów muzycznych, którymi się interesuję. Każda okładka powstawała jako niezależna interpretacja estetyki i charakteru danego artysty — bez zlecenia, z własnej inicjatywy.',
    en: 'A series of original album cover designs inspired by the work of musicians the author is interested in. Each cover was created as an independent interpretation of a given artist\'s aesthetic and character — unsolicited, entirely self-initiated.'
  },
  tags: ['Album Art'],
  mediaType: 'gallery',
  gallery: [
    { type: 'image', src: 'images/arc-02-seria.png' }, // wszystkie 8 okładek razem
    { type: 'video', src: 'media/arc-02-scianka.mp4' }, // ścianka z okładkami
    { type: 'image', src: 'images/arc-02-osobne-1.png' }, // 0PM BABI / Chmury / Stine Season / LILBOY
    { type: 'image', src: 'images/arc-02-osobne-2.png' } // More Chaos / [55] / Immortels / allsummalong
  ],
  thumb: 'images/arc-02-seria.png'
},
{
  id: 'arc-03', n: '03', group: 'archive',
  title: 'Lord Apex — Promo Materials',
  description: {
    pl: 'Materiały promocyjne (plakat trasy koncertowej, zaproszenie) zaprojektowane dla brytyjskiego rapera Lorda Apex — projekt własny, niezależny od artysty, zrealizowany częściowo w ramach zajęć na studiach.',
    en: 'Promotional materials (a tour poster, an invitation) designed for the British rapper Lord Apex — a personal project, independent of the artist, partly carried out as part of university coursework.'
  },
  tags: ['Identity'],
  mediaType: 'gallery',
  gallery: [
    { type: 'image', src: 'images/arc-03-sesja2.jpg' }, // sesja zdjęciowa (4 ujęcia, bez podpisów)
    { type: 'image', src: 'images/arc-03-promo.jpg' } // plakat trasy + zaproszenie na koncert
  ],
  thumb: 'images/arc-03-sesja2.jpg'
},
{
  id: 'arc-04', n: '04', group: 'archive',
  title: 'NoFear — Website Redesign',
  description: {
    pl: 'Projekt interfejsu strony zakupowej dla marki NoFear — koncepcyjne odświeżenie istniejącej witryny z zachowaniem oryginalnego języka wizualnego marki. Projekt zrealizowany w ramach zajęć akademickich.',
    en: 'Interface design for the NoFear e-commerce website — a conceptual refresh of the existing site while preserving the brand\'s original visual language. Project carried out as part of university coursework.'
  },
  tags: ['UI/UX'],
  mediaType: 'gallery',
  gallery: [
    { type: 'image', src: 'images/arc-04-laptop.jpg' }, // widok na laptopie (okładka)
    { type: 'video', src: 'media/arc-04-demo-1.mp4' }, // demo interakcji 1
    { type: 'video', src: 'media/arc-04-demo-2.mp4' } // demo interakcji 2
  ],
  thumb: 'images/arc-04-laptop.jpg'
},
{
  id: 'arc-05', n: '05', group: 'archive',
  title: 'Gr.up Uken — Poster Series',
  description: {
    pl: 'Trzy plakaty zrealizowane w ramach konkursu Gr.up\'a Uken, w którym każdy uczestnik miał jeden dzień na wykonanie projektu do wybranego tematu. Jeden z plakatów z serii był prezentowany na wystawie Paste Up Warsaw w Krakowie.',
    en: 'Three posters produced for the Gr.up Uken competition, in which each participant had one day to complete a project on a chosen theme. One poster from the series was exhibited at Paste Up Warsaw in Kraków.'
  },
  tags: ['Poster Design'],
  mediaType: 'gallery',
  gallery: [
    { type: 'image', src: 'images/arc-05-plakaty.jpg' } // cudaki / inny nie obcy / łamiszczęka
  ],
  thumb: 'images/arc-05-plakaty.jpg'
},
{
  id: 'arc-09', n: '06', group: 'archive',
  title: 'Excellent Expo — Website Showcase',
  description: {
    pl: 'Animacja typu showcase zrealizowana na zlecenie klienta (freelance), prezentująca odświeżoną stronę internetową firmy Excellent Expo.',
    en: 'A showcase animation produced for a client (freelance), presenting the redesigned website of Excellent Expo.'
  },
  tags: ['Motion Graphics'],
  mediaType: 'gallery',
  gallery: [
    { type: 'video', src: 'media/arc-09-excellent-expo.mp4', poster: 'images/arc-09-cover.jpg' }
  ],
  thumb: 'images/arc-09-cover.jpg' // klatka z logiem "Excellent Expo"
},
{
  id: 'arc-06', n: '07', group: 'archive',
  title: 'Study2Go — Brand Identity',
  description: {
    pl: 'Identyfikacja wizualna marki Study2Go — aplikacji edukacyjnej skierowanej do studentów i uczniów potrzebujących prostego i mobilnego rozwiązania do nauki. Projekt utrzymany w minimalistycznym wzornictwie, podkreślającym wartości marki: ruch, mobilność i efektywność.',
    en: 'Visual identity for the Study2Go brand — an educational app aimed at students who need a simple, mobile-friendly way to study. The project follows a minimalist design language that emphasises the brand\'s values: movement, mobility and efficiency.'
  },
  tags: ['Identity'],
  mediaType: 'gallery',
  gallery: [
    { type: 'image', src: 'images/arc-06-cover.jpg' }, // księga znaku — strona tytułowa
    { type: 'image', src: 'images/arc-06-siatka.png' } // znak graficzny, warianty, siatka konstrukcyjna
  ],
  thumb: 'images/arc-06-cover.jpg'
},
{
  id: 'arc-07', n: '08', group: 'archive',
  title: 'SMUK',
  description: {
    pl: 'Animacja postaci 2D — krótki loop przedstawiający postać o imieniu SMUK, zrealizowany w Adobe After Effects przy użyciu pluginu DUIK Angela, z zastosowaniem techniki paralaksy. Na bazie animacji stworzono również brand hero wykorzystany w ekranach aplikacji mobilnej. SMUK jest zawsze gotów stawić czoła codziennym wyzwaniom — jego długie ręce sprawdzają się w niespodziewanych sytuacjach, pomagając wszystkim dookoła, a sama postać udowadnia, że nawet w trudnych chwilach warto zachować lekkość i radość życia. Inspiracją były surrealistyczne światy znane z animacji "Potwory i Spółka" oraz "Adventure Time".',
    en: 'A 2D character animation — a short loop featuring a character named SMUK, created in Adobe After Effects using the DUIK Angela plugin, with a parallax technique. The animation was also developed into a brand hero used across mobile app screens. SMUK is always ready to face everyday challenges — his long arms come in handy in unexpected situations, helping everyone around him, and the character proves that even in tough moments it\'s worth keeping things light and joyful. The inspiration came from the surreal worlds of "Monsters, Inc." and "Adventure Time".'
  },
  tags: ['2D Animation'],
  mediaType: 'gallery',
  gallery: [
    { type: 'video', src: 'media/arc-07-demo.mp4', poster: 'images/arc-07-video-poster.jpg' }, // 1. animacja
    { type: 'image', src: 'images/arc-07-poses.png' }, // 2. SMUK — 3 kadry
    { type: 'image', src: 'images/arc-07-app-screens.jpg' }, // 3. ekrany aplikacji
    { type: 'image', src: 'images/arc-07-szkice.jpg' }, // 4. szkice koncepcyjne
    { type: 'image', src: 'images/arc-07-colors.jpg' }, // 5. warianty kolorystyczne postaci
    { type: 'image', src: 'media/arc-07-expr-ciekawy.gif' }, // 6. ekspresja — zaciekawiony
    { type: 'image', src: 'media/arc-07-expr-spokojny.gif' }, // ekspresja — spokojny
    { type: 'image', src: 'media/arc-07-expr-box.gif' } // ekspresja — zaskoczony (pudełko na głowie)
  ],
  thumb: 'images/arc-07-cover.jpg' // duży napis "SMUK" + postać
},
{
  id: 'arc-08', n: '09', group: 'archive',
  title: 'Spider-Man — Title Sequence',
  description: {
    pl: 'Autorski title sequence inspirowany filmem "Spider-Man: Across the Spider-Verse" — animacja prezentująca postacie i obsadę w stylu nawiązującym do estetyki produkcji. Projekt zrealizowany jako praca zaliczeniowa na przedmiocie "Animacja Graficzna", w całości w Adobe After Effects.',
    en: 'An original title sequence inspired by "Spider-Man: Across the Spider-Verse" — an animation presenting the characters and cast in a style referencing the film\'s aesthetic. Project completed as a coursework assignment for "Graphic Animation," made entirely in Adobe After Effects.'
  },
  tags: ['Motion Graphics'],
  mediaType: 'gallery',
  gallery: [
    { type: 'image', src: 'images/arc-08-cover.png' }, // sygnet pająka
    { type: 'video', src: 'media/arc-08-title-sequence.mp4' } // sekwencja tytułowa
  ],
  thumb: 'images/arc-08-cover.png'
}
];

const WORKS = PROJECTS_DATA.filter((p) => p.group === 'work');
const ARCHIVE = PROJECTS_DATA.filter((p) => p.group === 'archive');

/* ---- scroll-reveal: robust rAF/scroll check (IntersectionObserver is unreliable
   inside sandboxed preview iframes — its callbacks may never fire). ---- */
const revealNodes = new Set();
function registerReveal(root) {
  if (!root) return;
  root.querySelectorAll('.reveal, .line-mask').forEach((n) => revealNodes.add(n));
  checkReveals();
}
function checkReveals() {
  const trigger = window.innerHeight * 0.9;
  revealNodes.forEach((n) => {
    if (!n.isConnected) {revealNodes.delete(n);return;}
    const r = n.getBoundingClientRect();
    if (r.top < trigger && r.bottom > 0) {n.classList.add('in');revealNodes.delete(n);}
  });
}
(function revealLoop() {
  let ticking = false;
  const onScroll = () => {if (!ticking) {ticking = true;requestAnimationFrame(() => {checkReveals();ticking = false;});}};
  window.addEventListener('scroll', onScroll, { passive: true });
  window.addEventListener('resize', onScroll);
  setInterval(checkReveals, 250);
})();
function useReveal() {
  const ref = useRef(null);
  useEffect(() => {
    registerReveal(ref.current);
    // a couple of delayed passes catch fonts/layout settling
    const a = setTimeout(checkReveals, 60),b = setTimeout(checkReveals, 400);
    return () => {clearTimeout(a);clearTimeout(b);};
  }, []);
  return ref;
}

/* ---- parallax: shifts inner .par by element position ---- */
const parallaxNodes = [];
function registerParallax(node, strength) {if (node && !parallaxNodes.some((p) => p.node === node)) parallaxNodes.push({ node, strength });}
(function parallaxLoop() {
  let ticking = false;
  function update() {
    const vh = window.innerHeight;
    for (const { node, strength } of parallaxNodes) {
      if (!node.isConnected) continue;
      const par = node.querySelector('.par');if (!par) continue;
      const r = node.getBoundingClientRect();
      if (r.bottom < -200 || r.top > vh + 200) continue;
      const off = (r.top + r.height / 2 - vh / 2) / vh;
      par.style.transform = `translate3d(0, ${(-off * strength).toFixed(1)}px, 0)`;
    }
    ticking = false;
  }
  const onScroll = () => {if (!ticking) {ticking = true;requestAnimationFrame(update);}};
  window.addEventListener('scroll', onScroll, { passive: true });
  window.addEventListener('resize', onScroll);
  setInterval(update, 400); // catches newly-mounted nodes
})();

function Tags({ items }) {
  return <div className="tags">{items.map((t) => <span className="tag" key={t}>{t}</span>)}</div>;
}

/* big display line that slide-reveals from a mask */
function MaskLine({ children, className = '', style }) {
  return <span className={'line-mask ' + className} style={style}><span>{children}</span></span>;
}

/* ---------------------------------- NAV ---------------------------------- */
function Nav({ lang, setLang, t }) {
  return (
    <nav className="nav">
      <span className="brand"><b>barto</b>folio</span>
      <div className="navlinks">
        <a href="#works"><span className="n">01</span>{t.nav.works}</a>
        <a href="#archive"><span className="n">02</span>{lang === 'pl' ? 'Archiwum' : 'Archive'}</a>
        <a href="#about"><span className="n">03</span>{t.nav.about}</a>
        <a href="#contact"><span className="n">04</span>{t.nav.contact}</a>
      </div>
      <span className="spacer"></span>
      <div className="lang" role="group" aria-label="Language">
        <button data-on={lang === 'en' ? '1' : '0'} onClick={() => setLang('en')}>EN</button>
        <button data-on={lang === 'pl' ? '1' : '0'} onClick={() => setLang('pl')}>PL</button>
      </div>
    </nav>);

}

/* ---------------------------------- HERO ---------------------------------- */
function Hero({ t }) {
  const ref = useReveal();
  return (
    <header className="hero" ref={ref} style={{ gap: "14px" }}>
      <div className="hero-top kicker reveal">
        <span>{t.hero.loc}</span>
        <span className="live">{t.hero.live}</span>
        <span>©2026</span>
      </div>
      <h1 className="hero-name display">
        <MaskLine className="ln">BARTOSZ</MaskLine>
        <MaskLine className="ln outline" style={{ transitionDelay: '.12s' }}>SYDOW</MaskLine>
      </h1>
      <p className="hero-sub reveal" style={{ transitionDelay: '.25s' }}>
        <span className="bar">alias:</span><span className="alias">bartofolio</span>
        <span className="bar">|</span><span>{t.hero.role}</span>
      </p>
      <div className="hero-scroll reveal" style={{ transitionDelay: '.35s' }}><span className="line"></span><span>{t.hero.scroll}</span></div>
    </header>);

}

/* 16:9 VHS thumb — clickable */
function VHSThumb({ slot, label, idx, onOpen, src }) {
  const wrapRef = useRef(null);
  useEffect(() => {registerParallax(wrapRef.current, 64);}, []);
  return (
    <div className="vhs" ref={wrapRef} onClick={onOpen} role="button" tabIndex={0}
    onKeyDown={(e) => {if (e.key === 'Enter' || e.key === ' ') {e.preventDefault();onOpen();}}}>
      <div className="par">
        <image-slot id={slot} src={src} placeholder="Drop project image"></image-slot>
        <div className="vhs-skin"><span className="ph"><b>{idx}</b> &nbsp;//&nbsp; {label}</span></div>
        <div className="vhs-static"></div>
        <div className="vhs-bar"></div>
      </div>
    </div>);

}

/* ------------------------------ SELECTED WORKS ------------------------------ */
function Works({ t, onOpen }) {
  const ref = useReveal();
  return (
    <section className="sec" id="works" ref={ref}>
      <div className="sec-head" style={{ width: "1121px" }}>
        <span className="sec-num reveal">{t.works.num}</span>
        <h2 className="sec-title display"><MaskLine>{t.works.title}</MaskLine></h2>
        <span className="kicker sec-kicker reveal" style={{ transitionDelay: '.15s', width: "141px" }}>{t.works.kicker}</span>
      </div>
      <div className="works">
        {WORKS.map((w, i) =>
        <article className="work reveal" key={w.n} style={{ transitionDelay: i * 100 + 'ms' }}>
            <div className="work-media">
              <VHSThumb slot={w.id} label={w.title} idx={w.n} onOpen={() => onOpen(w)} src={w.thumb} />
            </div>
            <div className="work-meta">
              <span className="w-idx">{w.n} — {String(WORKS.length).padStart(2, '0')}</span>
              <h3 className="w-title">{w.title}</h3>
              <Tags items={w.tags} />
              <button className="cta" onClick={() => onOpen(w)} style={{ background: 'none', border: 0, padding: 0, cursor: 'pointer', fontFamily: 'var(--font-mono)' }}>
                {t.works.cta} <span className="arr">→</span>
              </button>
            </div>
          </article>
        )}
      </div>
    </section>);

}

/* --------------------------------- ARCHIVE CAROUSEL --------------------------------- */
function ArchiveCard({ item, onOpen }) {
  const ref = useRef(null);
  useEffect(() => {registerParallax(ref.current, 34);}, []);
  return (
    <div className="arch-card" ref={ref} onClick={onOpen} role="button" tabIndex={0}
    onKeyDown={(e) => {if (e.key === 'Enter') onOpen();}}>
      <div className="par">
        <image-slot id={item.id} src={item.thumb} placeholder="Drop image"></image-slot>
        <div className="a-skin"><span className="t">{item.tags[0]}</span><span className="n">{item.n}</span></div>
        <div className="a-static"></div>
      </div>
    </div>);

}

function Archive({ t, onOpen }) {
  const ref = useReveal();
  const railRef = useRef(null);
  // drag-to-scroll
  useEffect(() => {
    const el = railRef.current;if (!el) return;
    let down = false,startX = 0,startScroll = 0,moved = 0;
    const onDown = (e) => {down = true;moved = 0;startX = e.pageX;startScroll = el.scrollLeft;el.classList.add('dragging');};
    const onMove = (e) => {if (!down) return;const dx = e.pageX - startX;moved = Math.abs(dx);el.scrollLeft = startScroll - dx;};
    const onUp = () => {down = false;el.classList.remove('dragging');};
    el.addEventListener('mousedown', onDown);
    window.addEventListener('mousemove', onMove);
    window.addEventListener('mouseup', onUp);
    // block click after a drag
    el.addEventListener('click', (e) => {if (moved > 6) {e.stopPropagation();e.preventDefault();}}, true);
    return () => {el.removeEventListener('mousedown', onDown);window.removeEventListener('mousemove', onMove);window.removeEventListener('mouseup', onUp);};
  }, []);
  return (
    <section className="sec arch" id="archive" ref={ref}>
      <div className="sec-head" style={{ width: "1378px" }}>
        <span className="sec-num reveal">{t.archive.num}</span>
        <h2 className="sec-title display"><MaskLine>{t.archive.title}</MaskLine></h2>
        <span className="kicker sec-kicker reveal" style={{ transitionDelay: '.15s' }}>{t.archive.kicker}</span>
      </div>
      <div className="reveal">
        <div className="arch-rail" ref={railRef} style={{ lineHeight: "0.45" }}>
          {ARCHIVE.map((a) =>
          <ArchiveCard key={a.id} item={a} onOpen={() => onOpen(a)} />
          )}
        </div>
        <div className="arch-hint"><span className="ln"></span>{t.archive.hint}</div>
      </div>
    </section>);

}

/* --------------------------------- ABOUT --------------------------------- */
function About({ t }) {
  const ref = useReveal();
  const a = t.about;
  return (
    <section className="sec" id="about" ref={ref}>
      <div className="sec-head" style={{ width: "1291px" }}>
        <span className="sec-num reveal">{a.num === '02' ? '03' : a.num}</span>
        <h2 className="sec-title display"><MaskLine>{a.title}</MaskLine></h2>
        <span className="kicker sec-kicker reveal" style={{ transitionDelay: '.15s' }}>{a.kicker}</span>
      </div>
      <div className="about-grid">
        <div className="mag reveal">
          <div className="frame">
            <image-slot id="portrait" src="images/portrait.jpg" placeholder="Drop portrait" style={{ borderRadius: "0px" }}></image-slot>
            <div className="duo"></div><div className="duo2"></div>
            <div className="mag-skin">[ portrait — 3:4 ]</div>
            <span className="issue">ISSUE 01</span><span className="barcode"></span>
          </div>
        </div>
        <div>
          <p className="bio reveal"><span className="first">{a.bio.slice(0, 34)}</span>{a.bio.slice(34)}</p>
          <div className="about-block reveal">
            <h4>{a.eduH}</h4>
            {a.edu.map((e, i) =>
            <div className="tl-item" key={i}>
                <span className="yr">{e.yr}</span>
                <span><span className="org">{e.org}</span><span className="role">{e.role}</span></span>
              </div>
            )}
          </div>
          <div className="about-block reveal">
            <h4>{a.expH}</h4>
            {a.exp.map((e, i) =>
            <div className="tl-item one" key={i}>
                <span><span className="org">{e.org}</span><span className="role">{e.role}</span></span>
              </div>
            )}
          </div>
        </div>
      </div>
    </section>);

}

/* --------------------------------- STACK --------------------------------- */
function Stack({ t }) {
  const ref = useReveal();
  return (
    <section className="sec" id="stack" ref={ref}>
      <div className="sec-head">
        <span className="sec-num reveal">04</span>
        <h2 className="sec-title display"><MaskLine>{t.stack.title}</MaskLine></h2>
        <span className="kicker sec-kicker reveal" style={{ transitionDelay: '.15s' }}>{t.stack.kicker}</span>
      </div>
      <div className="stack-grid">
        {STACK.map((g, i) =>
        <div className="stack-col reveal" key={i} style={{ transitionDelay: i * 100 + 'ms' }}>
            <h4>{g.h} <span className="n">[{String(g.items.length).padStart(2, '0')}]</span></h4>
            <div className="chips">{g.items.map((it) => <span className="chip" key={it}>{it}</span>)}</div>
          </div>
        )}
      </div>
    </section>);

}

/* --------------------------------- FOOTER --------------------------------- */
function Footer({ t }) {
  const unit = (k) =>
  <span className="unit" key={k}>
      <span>LET'S&nbsp;</span><span className="hollow">CREATE&nbsp;&nbsp;</span>
    </span>;

  return (
    <footer className="footer" id="contact">
      <div className="marquee"><div className="track">{unit('a')}{unit('b')}{unit('c')}{unit('d')}</div></div>
      <div className="contact">
        <span className="kicker" style={{ display: 'block', marginBottom: '2vw' }}>{t.footer.avail}</span>
        <a className="big-mail" href={'mailto:' + MAIL}>{MAIL}</a>
        <div className="links">
          {LINKS.map((l) =>
          <a href={l.href} target="_blank" rel="noopener" key={l.n}><span className="n">{l.n}</span>{l.label} ↗</a>
          )}
        </div>
      </div>
      <div className="colophon">
        <span>BARTOFOLIO © </span><span>{t.footer.built}</span><span>Poznań, PL</span>
      </div>
    </footer>);

}

/* --------------------------------- PROJECT MODAL --------------------------------- */
function ProjectModal({ project, onClose, t, lang }) {
  const [show, setShow] = useState(false);
  const [data, setData] = useState(project);
  const [galIdx, setGalIdx] = useState(0);
  // keep content during exit transition
  useEffect(() => {
    if (project) {setData(project);setGalIdx(0);requestAnimationFrame(() => setShow(true));} else
    {setShow(false);}
  }, [project]);
  useEffect(() => {
    const onKey = (e) => {
      if (e.key === 'Escape') onClose();
      if (data && data.mediaType === 'gallery' && data.gallery && data.gallery.length > 1) {
        if (e.key === 'ArrowRight') setGalIdx((i) => (i + 1) % data.gallery.length);
        if (e.key === 'ArrowLeft') setGalIdx((i) => (i - 1 + data.gallery.length) % data.gallery.length);
      }
    };
    window.addEventListener('keydown', onKey);return () => window.removeEventListener('keydown', onKey);
  }, [onClose, data]);
  useEffect(() => {document.body.style.overflow = project ? 'hidden' : '';return () => {document.body.style.overflow = '';};}, [project]);
  if (!data) return null;
  const m = t.modal;
  return (
    <div className="modal-scrim" data-open={show ? '1' : '0'} onClick={onClose}>
      <div className="modal" onClick={(e) => e.stopPropagation()} role="dialog" aria-modal="true">
        <button className="modal-x" onClick={onClose} aria-label={m.close}>✕</button>
        <div className="modal-media" data-type={data.mediaType}>
          {data.mediaType === 'gallery' ?
          <React.Fragment>
            {(() => {
              const item = data.gallery[galIdx];
              // item can be a plain string (= image path) or an object { type, src/vimeoId }
              const it = typeof item === 'string' ? { type: 'image', src: item } : item;
              if (it.type === 'vimeo') return <iframe key={galIdx} className="modal-media-el" src={`https://player.vimeo.com/video/${it.vimeoId}`}
                frameBorder="0" allow="autoplay; fullscreen; picture-in-picture" allowFullScreen title={data.title}></iframe>;
              if (it.type === 'video') return <video key={galIdx} className="modal-media-el" src={it.src} poster={it.poster || undefined}
                controls playsInline preload="metadata"></video>;
              return <img key={galIdx} className="modal-media-el" src={it.src} alt={data.title + ' — ' + (galIdx + 1)} />;
            })()}
            {data.gallery.length > 1 &&
            <React.Fragment>
              <button className="modal-gal-nav prev" aria-label="Previous item"
              onClick={(e) => {e.stopPropagation();setGalIdx((i) => (i - 1 + data.gallery.length) % data.gallery.length);}}>‹</button>
              <button className="modal-gal-nav next" aria-label="Next item"
              onClick={(e) => {e.stopPropagation();setGalIdx((i) => (i + 1) % data.gallery.length);}}>›</button>
              <div className="modal-gal-dots" onClick={(e) => e.stopPropagation()}>
                {data.gallery.map((_, i) =>
                <span key={i} className={'dot' + (i === galIdx ? ' on' : '')} onClick={() => setGalIdx(i)}></span>
                )}
              </div>
              <span className="modal-gal-count">{galIdx + 1} / {data.gallery.length}</span>
            </React.Fragment>}
          </React.Fragment> :
          data.mediaType === 'vimeo' ?
          <iframe className="modal-media-el" src={`https://player.vimeo.com/video/${data.vimeoId}`}
          frameBorder="0" allow="autoplay; fullscreen; picture-in-picture" allowFullScreen
          title={data.title}></iframe> :
          data.mediaType === 'video' ?
          <video className="modal-media-el" src={data.mediaUrl || undefined} poster={data.poster || undefined}
          controls playsInline preload="metadata"></video> :
          data.mediaUrl ?
          <img className="modal-media-el" src={data.mediaUrl} alt={data.title} /> :
          <div className="modal-media-el modal-media-empty"></div>}
          {!data.mediaUrl && data.mediaType !== 'vimeo' && data.mediaType !== 'gallery' &&
          <span className="m-ph">
            {data.mediaType === 'video' && <span className="play"></span>}
            {m.media} · {data.mediaType === 'video' ? 'VIDEO' : 'IMAGE'}
          </span>}
        </div>
        <div className="modal-body">
          <div>
            <span className="modal-tag">{m.tag} — {data.n}</span>
            <h3 className="modal-title">{data.title}</h3>
            <p className="modal-txt">{(data.description && (data.description[lang] || data.description.pl)) || m.body}</p>
            <div className="modal-tags"><Tags items={data.tags} /></div>
          </div>
          <div className="modal-side">
            <div className="row"><span className="k">{t.modal.tag}</span><span className="v">{data.n} / —</span></div>
            {data.tags.map((tg, i) =>
            <div className="row" key={i}><span className="k">{i === 0 ? 'Discipline' : 'Tag'}</span><span className="v">{tg}</span></div>
            )}
            <div className="row"><span className="k">Year</span><span className="v">—</span></div>
          </div>
        </div>
      </div>
    </div>);

}

/* ---------------------------------- APP ---------------------------------- */
function App() {
  const [lang, setLang] = useState(() => {try {return localStorage.getItem('bf-lang') || 'pl';} catch {return 'pl';}});
  const [project, setProject] = useState(null);
  const [tw, setTweak] = useTweaks(TWEAK_DEFAULTS);
  // apply tweaks → CSS variables + atmosphere attribute
  useEffect(() => {
    const root = document.documentElement;
    const pal = tw.palette || TWEAK_DEFAULTS.palette;
    root.style.setProperty('--acc', pal[0]);
    root.style.setProperty('--streak-1', pal[1]);
    root.style.setProperty('--streak-2', pal[2]);
    root.style.setProperty('--streak-3', pal[1]);
    const g = (tw.grit ?? 55) / 100;
    root.style.setProperty('--grain-op', (0.03 + g * 0.19).toFixed(3));
    root.style.setProperty('--scan-op', (0.12 + g * 0.58).toFixed(3));
    root.style.setProperty('--static-op', (0.12 + g * 0.48).toFixed(3));
    root.dataset.atmo = tw.atmo || 'alive';
  }, [tw]);
  useEffect(() => {try {localStorage.setItem('bf-lang', lang);} catch {}document.documentElement.lang = lang;}, [lang]);
  // scroll-driven background: tie orb/streak movement + scale + rotation to scroll
  useEffect(() => {
    const thermal = document.querySelector('.thermal');
    if (!thermal) return;
    if (window.matchMedia('(prefers-reduced-motion: reduce)').matches) return;
    let ticking = false;
    const onScroll = () => {
      if (ticking) return;ticking = true;
      requestAnimationFrame(() => {
        const max = document.documentElement.scrollHeight - window.innerHeight;
        const p = max > 0 ? Math.min(1, window.scrollY / max) : 0;
        thermal.style.transform =
        `translate3d(0, ${(-p * 8).toFixed(2)}vh, 0) scale(${(1 + p * 0.2).toFixed(3)}) rotate(${(p * 2.5).toFixed(2)}deg)`;
        ticking = false;
      });
    };
    window.addEventListener('scroll', onScroll, { passive: true });
    onScroll();
    return () => window.removeEventListener('scroll', onScroll);
  }, []);
  const t = I18N[lang];
  const open = useCallback((p) => setProject(p), []);
  const close = useCallback(() => setProject(null), []);
  return (
    <React.Fragment>
      <Nav lang={lang} setLang={setLang} t={t} />
      <main className="wrap">
        <Hero t={t} />
        <Works t={t} onOpen={open} />
        <Archive t={t} onOpen={open} />
        <About t={t} />
        <Stack t={t} />
        <Footer t={t} />
      </main>
      <ProjectModal project={project} onClose={close} t={t} lang={lang} />
      <TweaksPanel>
        <TweakSection label="Identity" />
        <TweakColor label="Palette" value={tw.palette} options={PALETTES}
        onChange={(v) => setTweak('palette', v)} />
        <TweakSection label="Texture" />
        <TweakSlider label="Analog grit" value={tw.grit} min={0} max={100} step={1} unit="%"
        onChange={(v) => setTweak('grit', v)} />
        <TweakSection label="Motion" />
        <TweakRadio label="Atmosphere" value={tw.atmo} options={['calm', 'alive', 'storm']}
        onChange={(v) => setTweak('atmo', v)} />
      </TweaksPanel>
    </React.Fragment>);

}

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