/* global React, ReactDOM */
const { useState, useEffect, useRef, useCallback } = React;

/* ============= TWEAK STATE ============= */
const TWEAK_DEFAULTS = /*EDITMODE-BEGIN*/{
  "dark": false
} /*EDITMODE-END*/;

/* ============= ICONS ============= */
const SendIcon = ({ size = 14 }) =>
<svg width={size} height={size} viewBox="0 0 16 14" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round">
    <path d="M14.5 1.5L7.5 8.5" />
    <path d="M14.5 1.5L10 13L7.5 8.5L3 6L14.5 1.5Z" />
  </svg>;

const ArrowRightIcon = ({ size = 18 }) =>
<svg width={size} height={size} viewBox="0 0 18 18" fill="none" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round" strokeLinejoin="round">
    <path d="M3.75 9h10.5" />
    <path d="M9 3.75L14.25 9 9 14.25" />
  </svg>;

const ArrowUpRightIcon = ({ size = 18 }) =>
<svg width={size} height={size} viewBox="0 0 18 18" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
    <path d="M5 13L13 5" />
    <path d="M6 5h7v7" />
  </svg>;

const ResumeIcon = ({ size = 22 }) =>
<svg width={size} height={size} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round" strokeLinejoin="round">
    <path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z" />
    <path d="M14 2v6h6" />
    <line x1="8" y1="13" x2="14" y2="13" />
    <line x1="8" y1="17" x2="13" y2="17" />
  </svg>;

const CloseIcon = ({ size = 18 }) =>
<svg width={size} height={size} viewBox="0 0 18 18" fill="none" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round" strokeLinejoin="round">
    <path d="M4 4l10 10M14 4L4 14" />
  </svg>;

const GoogleG = () =>
<svg width="32" height="32" viewBox="0 0 48 48">
    <path fill="#FFC107" d="M43.6 20.5h-1.9V20H24v8h11.3c-1.6 4.6-6 8-11.3 8-6.6 0-12-5.4-12-12s5.4-12 12-12c3.1 0 5.8 1.1 8 3l5.7-5.7C34 6.1 29.2 4 24 4 12.9 4 4 12.9 4 24s8.9 20 20 20 20-8.9 20-20c0-1.2-.1-2.3-.4-3.5z" />
    <path fill="#FF3D00" d="M6.3 14.7l6.6 4.8C14.6 16 18.9 13 24 13c3.1 0 5.8 1.1 8 3l5.7-5.7C34 6.1 29.2 4 24 4 16.3 4 9.6 8.4 6.3 14.7z" />
    <path fill="#4CAF50" d="M24 44c5.1 0 9.8-2 13.3-5.2l-6.1-5c-2 1.4-4.5 2.2-7.2 2.2-5.2 0-9.6-3.3-11.3-8l-6.5 5C9.5 39.5 16.2 44 24 44z" />
    <path fill="#1976D2" d="M43.6 20.5h-1.9V20H24v8h11.3c-.8 2.2-2.2 4.1-4.1 5.5l6.1 5C40.8 36 44 30.5 44 24c0-1.2-.1-2.3-.4-3.5z" />
  </svg>;

const IBMLogo = () =>
<svg width="40" height="16" viewBox="0 0 80 32" fill="currentColor">
    {[0, 1, 2, 3].map((row) =>
  <g key={row} transform={`translate(0,${row * 8})`}>
        {Array.from({ length: 8 }).map((_, col) =>
    <rect key={col} x={col * 10} y={0} width={5} height={3} />
    )}
      </g>
  )}
    <text x="0" y="28" fontFamily="monospace" fontWeight="900" fontSize="14" letterSpacing="2">IBM</text>
  </svg>;


const IBMMark = () =>
<img src="assets/ibm-logo.png" alt="IBM" style={{ width: 32, height: "auto", display: "block" }} />;


/* ============= LOGO ============= */
const LogoSVG = ({ height = 26, color = "#fff" }) =>
<svg viewBox="0 0 89 33" height={height} fill="none" xmlns="http://www.w3.org/2000/svg">
    <image href="assets/logo.svg" x="0" y="0" width="89" height="33" />
  </svg>;

// Simpler: just <img>
const Logo = ({ height = 26, invert = false }) =>
<img
  src="assets/logo.svg"
  alt="Rizqi Naridha"
  style={{
    height,
    width: "auto",
    filter: invert ? "invert(1) brightness(.2)" : "none"
  }} />;



/* ============= CUSTOM CURSOR ============= */
function CustomCursor() {
  const cursor = useRef(null);
  const aboutCursor = useRef(null);
  const footerCursor = useRef(null);
  useEffect(() => {
    const isFinePointer = window.matchMedia("(hover: hover) and (pointer: fine)").matches;
    if (!isFinePointer) return;
    document.body.classList.add("has-cursor");
    /* signal to the shared SiteFooter cursor that this page already renders
       the footer email pill, so it should stay dormant and avoid a duplicate */
    window.__portfolioCursorActive = true;

    let cx = -100,cy = -100,mx = -100,my = -100;
    let firstMove = true;
    let raf;
    const tick = () => {
      cx += (mx - cx) * 0.55;
      cy += (my - cy) * 0.55;
      const t = `translate(${cx}px, ${cy}px)`;
      if (cursor.current) cursor.current.style.transform = t;
      if (aboutCursor.current) aboutCursor.current.style.transform = t;
      if (footerCursor.current) footerCursor.current.style.transform = t;
      raf = requestAnimationFrame(tick);
    };
    raf = requestAnimationFrame(tick);

    const setVariant = (about, footer) => {
      cursor.current?.classList.toggle("is-suppressed", about || footer);
      aboutCursor.current?.classList.toggle("is-active", about);
      footerCursor.current?.classList.toggle("is-active", footer);
    };
    const onMove = (e) => {
      mx = e.clientX;my = e.clientY;
      if (firstMove) {cx = mx;cy = my;firstMove = false;}
      cursor.current?.classList.remove("is-hidden");
      const t = e.target;
      const overAbout = !!(t.closest && t.closest("#about"));
      const overFooter = !overAbout && !!(t.closest && t.closest(".site-footer"));
      setVariant(overAbout, overFooter);
    };
    const onEnter = () => cursor.current?.classList.remove("is-hidden");
    const onLeave = () => {
      cursor.current?.classList.add("is-hidden");
      setVariant(false, false);
    };

    window.addEventListener("mousemove", onMove);
    document.documentElement.addEventListener("mouseleave", onLeave);
    document.documentElement.addEventListener("mouseenter", onEnter);
    return () => {
      cancelAnimationFrame(raf);
      window.removeEventListener("mousemove", onMove);
      document.documentElement.removeEventListener("mouseleave", onLeave);
      document.documentElement.removeEventListener("mouseenter", onEnter);
      document.body.classList.remove("has-cursor");
      window.__portfolioCursorActive = false;
    };
  }, []);
  return (
    <React.Fragment>
      <div ref={cursor} className="cursor is-hidden" aria-hidden="true">
        <svg width="28" height="28" viewBox="0 0 28 28" fill="none" xmlns="http://www.w3.org/2000/svg">
          <path d="M25.8066 12.0084L16.336 15.6054L12.7397 25.0754L4.34001 3.60944L25.8066 12.0084Z" fill="currentColor" />
          <path d="M25.8066 12.0084L25.9487 12.3823L26.9186 12.0139L25.9524 11.6359L25.8066 12.0084ZM16.336 15.6054L16.1939 15.2315L16.0259 15.2953L15.962 15.4634L16.336 15.6054ZM12.7397 25.0754L12.3672 25.2211L12.7452 26.1874L13.1136 25.2174L12.7397 25.0754ZM4.34001 3.60944L4.48575 3.23693L3.63437 2.90382L3.96751 3.7552L4.34001 3.60944ZM25.8066 12.0084L25.6646 11.6345L16.1939 15.2315L16.336 15.6054L16.478 15.9793L25.9487 12.3823L25.8066 12.0084ZM16.336 15.6054L15.962 15.4634L12.3657 24.9334L12.7397 25.0754L13.1136 25.2174L16.7099 15.7474L16.336 15.6054ZM12.7397 25.0754L13.1122 24.9296L4.7125 3.46368L4.34001 3.60944L3.96751 3.7552L12.3672 25.2211L12.7397 25.0754ZM4.34001 3.60944L4.19426 3.98194L25.6609 12.3809L25.8066 12.0084L25.9524 11.6359L4.48575 3.23693L4.34001 3.60944Z" fill="#fff" />
        </svg>
      </div>
      <div ref={aboutCursor} className="cursor-about" aria-hidden="true">
        <div className="cursor-about-inner">
          <svg width="134" height="57" viewBox="0 0 134 57" fill="none" xmlns="http://www.w3.org/2000/svg">
            <path d="M17 35C17 22.8497 26.8497 13 39 13H112C124.15 13 134 22.8497 134 35V35C134 47.1503 124.15 57 112 57H39C26.8497 57 17 47.1503 17 35V35Z" fill="#9747FF"></path>
            <path d="M25.8066 12.0084L16.336 15.6054L12.7397 25.0754L4.34001 3.60944L25.8066 12.0084Z" fill="#9747FF"></path>
          </svg>
          <span className="cursor-about-text">Visitor&nbsp;<span role="img" aria-label="rock on">🤟</span></span>
        </div>
      </div>
      <div ref={footerCursor} className="cursor-about cursor-footer" aria-hidden="true">
        <div className="cursor-about-inner">
          <span className="cursor-about-text cursor-footer-text">reach.me@naridha.id&nbsp;<span role="img" aria-label="call me">🤙</span></span>
          <svg className="cursor-footer-tail" width="30" height="30" viewBox="0 0 30 30" fill="none" xmlns="http://www.w3.org/2000/svg">
            <path d="M25.8067 12.0084L16.336 15.6054L12.7397 25.0754L4.34003 3.60944L25.8067 12.0084Z" fill="#9747FF"></path>
          </svg>
        </div>
      </div>
    </React.Fragment>);

}

/* ============= SCROLL REVEAL ============= */
/* Keep ScrollTrigger positions accurate while late-loading images grow the
   page — otherwise scrubbed sections briefly show a stale (finished) state */
function useScrollTriggerImageRefresh() {
  useEffect(() => {
    if (!window.ScrollTrigger) return;
    let t;
    const refresh = () => {
      clearTimeout(t);
      t = setTimeout(() => window.ScrollTrigger.refresh(), 150);
    };
    const imgs = Array.from(document.querySelectorAll("img")).filter((i) => !i.complete);
    imgs.forEach((i) => i.addEventListener("load", refresh, { once: true }));
    window.addEventListener("load", refresh);
    return () => {
      clearTimeout(t);
      window.removeEventListener("load", refresh);
      imgs.forEach((i) => i.removeEventListener("load", refresh));
    };
  }, []);
}

function useScrollReveal() {
  useEffect(() => {
    const els = document.querySelectorAll(".reveal");
    const showInView = () => {
      const vh = window.innerHeight;
      els.forEach((el) => {
        if (el.classList.contains("is-in")) return;
        const r = el.getBoundingClientRect();
        if (r.top < vh - 60 && r.bottom > 0) el.classList.add("is-in");
      });
    };
    if (!("IntersectionObserver" in window)) {
      els.forEach((el) => el.classList.add("is-in"));
      return;
    }
    const io = new IntersectionObserver((entries) => {
      entries.forEach((e) => {
        if (e.isIntersecting) {
          e.target.classList.add("is-in");
          io.unobserve(e.target);
        }
      });
    }, { threshold: 0.05, rootMargin: "0px 0px -40px 0px" });
    els.forEach((el) => io.observe(el));
    // Fallback: also fire on scroll for environments where IO is flaky (e.g. screenshot tools)
    showInView();
    window.addEventListener("scroll", showInView, { passive: true });
    return () => {io.disconnect();window.removeEventListener("scroll", showInView);};
  }, []);
}

/* ============= LOADING SCREEN ============= */
function LoadingScreen({ onDone }) {
  const [pct, setPct] = useState(0);
  const [exiting, setExiting] = useState(false);
  const cursorRef = useRef(null);

  // Track actual loading progress: fonts + all images
  useEffect(() => {
    const imgs = Array.from(document.images);
    const fontPromise = document.fonts && document.fonts.ready || Promise.resolve();

    // Only above-the-fold images should BLOCK the loader. Below-the-fold
    // lazy images are deferred by Chrome while the loader covers the page
    // (their load event never fires), so waiting on them stalls the bar.
    const vh = window.innerHeight || 800;
    const isBlocking = (img) => {
      if (img.loading === "lazy") return false;          // never block on lazy imgs
      const r = img.getBoundingClientRect();
      return r.top < vh * 1.2;                             // roughly in/near first screen
    };

    // Each tracked promise contributes equally
    const tasks = [
    { p: fontPromise, weight: 1 },
    ...imgs.filter(isBlocking).map((img) => {
      if (img.complete && img.naturalWidth > 0) {
        return { p: Promise.resolve(), weight: 1, alreadyDone: true };
      }
      return {
        p: new Promise((res) => {
          const done = () => res();
          img.addEventListener('load', done, { once: true });
          img.addEventListener('error', done, { once: true });
        }),
        weight: 1
      };
    })];


    const totalWeight = tasks.reduce((s, t) => s + t.weight, 0);
    let loadedWeight = tasks.filter((t) => t.alreadyDone).reduce((s, t) => s + t.weight, 0);

    // Smooth percentage display — interpolate toward target
    let targetPct = loadedWeight / totalWeight * 100;
    let displayedPct = 0;
    let raf;
    const tick = () => {
      displayedPct += (targetPct - displayedPct) * 0.08;
      if (Math.abs(targetPct - displayedPct) < 0.5) displayedPct = targetPct;
      // Cap displayed value at 99% — only allow 100 when we're truly done
      setPct(Math.min(99, Math.floor(displayedPct)));
      if (displayedPct < 100 || targetPct < 100) raf = requestAnimationFrame(tick);
    };
    raf = requestAnimationFrame(tick);

    tasks.forEach((t) => {
      t.p.then(() => {
        loadedWeight += t.weight;
        // Cap real progress to 99% — only the "done" handler flips to 100
        targetPct = Math.min(99, loadedWeight / totalWeight * 100);
      });
    });

    // Ensure minimum visible duration (4s) so the screen registers
    const minTime = new Promise((r) => setTimeout(r, 4000));
    const allDone = Promise.all(tasks.map((t) => t.p));
    // Hard failsafe: never let the loader hang. If assets haven't resolved
    // within the cap, finish anyway (covers Chrome's deferred lazy images,
    // slow networks, or an asset that never fires load/error).
    const failsafe = new Promise((r) => setTimeout(r, 8000));
    Promise.all([Promise.race([allDone, failsafe]), minTime]).then(() => {
      targetPct = 99;
      // wait for displayed to catch up to 99, then exit
      const waitForDisplay = setInterval(() => {
        if (displayedPct >= 98.5) {
          clearInterval(waitForDisplay);
          setPct(99);
          setExiting(true);
          setTimeout(() => {
            cancelAnimationFrame(raf);
            onDone();
          }, 650);
        }
      }, 50);
    });

    return () => cancelAnimationFrame(raf);
  }, [onDone]);

  // Custom cursor: kite + Hello pill follows mouse with easing
  useEffect(() => {
    let mx = window.innerWidth / 2,my = window.innerHeight / 2;
    let cx = mx,cy = my;
    let raf;
    const tick = () => {
      cx += (mx - cx) * 0.18;
      cy += (my - cy) * 0.18;
      if (cursorRef.current) cursorRef.current.style.transform = `translate(${cx}px, ${cy}px)`;
      raf = requestAnimationFrame(tick);
    };
    raf = requestAnimationFrame(tick);
    const onMove = (e) => {mx = e.clientX;my = e.clientY;};
    window.addEventListener('mousemove', onMove);
    return () => {window.removeEventListener('mousemove', onMove);cancelAnimationFrame(raf);};
  }, []);

  // While loading, hide system cursor + suppress page scroll
  useEffect(() => {
    document.body.classList.add('is-loading');
    return () => document.body.classList.remove('is-loading');
  }, []);

  return (
    <div className={`loading-screen ${exiting ? 'is-exiting' : ''}`} aria-hidden={exiting}>
      <div className="loading-stage">
        <div className="loading-pct">
          <span className="loading-number">{String(pct).padStart(2, '0')}</span>
          <span className="loading-percent">%</span>
        </div>
        <div className="loading-bar-row">
          <div className="loading-divider" />
          <div className="loading-bar">
            <div className="loading-bar-fill" style={{ width: `${Math.min(99, pct)}%` }} />
          </div>
        </div>
      </div>
      <div ref={cursorRef} className="loading-cursor" aria-hidden="true">
        <svg className="loading-cursor-svg" width="134" height="57" viewBox="0 0 134 57" fill="none" xmlns="http://www.w3.org/2000/svg">
          <path d="M17 35C17 22.8497 26.8497 13 39 13H112C124.15 13 134 22.8497 134 35V35C134 47.1503 124.15 57 112 57H39C26.8497 57 17 47.1503 17 35V35Z" fill="#3D3D3D"></path>
          <path d="M25.8066 12.0084L16.336 15.6054L12.7397 25.0754L4.34001 3.60944L25.8066 12.0084Z" fill="#3D3D3D"></path>
        </svg>
        <span className="loading-cursor-text">Hello&nbsp;<span role="img" aria-label="wave">👋</span></span>
      </div>
    </div>);

}

/* ============= NAV ============= */
function Nav({ onReachOut }) {
  const [menuOpen, setMenuOpen] = useState(false);
  useEffect(() => {
    document.body.style.overflow = menuOpen ? "hidden" : "";
    return () => {document.body.style.overflow = "";};
  }, [menuOpen]);
  const scrollTo = (id) => (e) => {
    e.preventDefault();
    const wasOpen = menuOpen;
    setMenuOpen(false);
    const el = document.getElementById(id);
    if (el) setTimeout(() => el.scrollIntoView({ behavior: "smooth", block: "start" }), wasOpen ? 250 : 0);
  };
  return (
    <React.Fragment>
      <div className="nav-wrap">
        <nav className="nav">
          <a href="#top" className="nav-logo" onClick={scrollTo("top")} aria-label="Home">
            <Logo height={36} />
          </a>
          <div className="nav-links">
            <a href="#projects" className="nav-link" onClick={scrollTo("projects")}>Projects</a>
            <a href="#about" className="nav-link" onClick={scrollTo("about")}>About</a>
            <a href="#highlights" className="nav-link" onClick={scrollTo("highlights")}>Highlights</a>
            <a href="#journal" className="nav-link" onClick={scrollTo("journal")}>Journal</a>
          </div>
          <div className="nav-right">
            <button
              className={`nav-burger ${menuOpen ? "is-open" : ""}`}
              onClick={() => setMenuOpen((o) => !o)}
              aria-label={menuOpen ? "Close menu" : "Open menu"}
              aria-expanded={menuOpen}>
              
              <span className="nav-burger-lines" aria-hidden="true">
                <span></span>
                <span></span>
                <span></span>
              </span>
            </button>
            <button className="nav-cta" onClick={onReachOut} aria-label="Reach Out">
              <span className="nav-cta-label">Reach Out</span>
              <span className="icon"><SendIcon /></span>
            </button>
          </div>
        </nav>
      </div>
      <div className={`mobile-menu ${menuOpen ? "is-open" : ""}`} onClick={() => setMenuOpen(false)}>
        <div className="mobile-menu-inner" onClick={(e) => e.stopPropagation()}>
          <a href="#projects" onClick={scrollTo("projects")}>Projects</a>
          <a href="#about" onClick={scrollTo("about")}>About</a>
          <a href="#highlights" onClick={scrollTo("highlights")}>Highlights</a>
          <a href="#journal" onClick={scrollTo("journal")}>Journal</a>
          <button className="pill-cta mobile-menu-cta" onClick={() => {setMenuOpen(false);onReachOut();}}>
            Reach Out
            <span className="icon"><SendIcon /></span>
          </button>
        </div>
      </div>
    </React.Fragment>);

}

/* ============= HERO ============= */
function HeroGrid() {
  const canvasRef = useRef(null);
  useEffect(() => {
    const canvas = canvasRef.current;
    if (!canvas) return;
    const hero = canvas.parentElement;
    const ctx = canvas.getContext("2d");
    const GRID = 40;
    const PINK = "255,77,228";
    let w = 0,h = 0;
    const dpr = Math.min(window.devicePixelRatio || 1, 2);
    let tx = -9999,ty = -9999; // raw target
    let cx = -9999,cy = -9999; // eased position
    let glow = 0,glowTarget = 0; // overall fade in/out
    let raf = null;
    let visible = true;

    const resize = () => {
      const r = hero.getBoundingClientRect();
      w = r.width;h = r.height;
      canvas.width = Math.round(w * dpr);
      canvas.height = Math.round(h * dpr);
      canvas.style.width = w + "px";
      canvas.style.height = h + "px";
      ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
    };
    resize();
    const ro = new ResizeObserver(resize);
    ro.observe(hero);

    const onMove = (e) => {
      // suppress pointer tracking during the first-visit intro until it finishes
      // (the intro removes the .hero-intro class on completion)
      if (hero.classList.contains("hero-intro")) {glowTarget = 0;return;}
      const r = hero.getBoundingClientRect();
      const x = e.clientX - r.left;
      const y = e.clientY - r.top;
      tx = x;ty = y;
      const inside = x >= 0 && y >= 0 && x <= w && y <= h;
      glowTarget = inside ? 1 : 0;
      if (cx < -5000) {cx = x;cy = y;} // snap on first sight, no fly-in
      ensureRunning();
    };
    const onLeave = () => {glowTarget = 0;ensureRunning();};
    window.addEventListener("mousemove", onMove);
    hero.addEventListener("mouseleave", onLeave);

    const REACH = 60; // how far (px) the influence spreads to pick lines
    const HALF = 78; // half-length of each glowing line segment
    const drawLineGrad = (x0, y0, x1, y1, peak) => {
      const g = ctx.createLinearGradient(x0, y0, x1, y1);
      g.addColorStop(0, `rgba(${PINK},0)`);
      g.addColorStop(0.5, `rgba(${PINK},${peak})`);
      g.addColorStop(1, `rgba(${PINK},0)`);
      ctx.strokeStyle = g;
      ctx.shadowColor = `rgba(${PINK},${0.55 * peak})`;
      ctx.shadowBlur = 7;
      ctx.beginPath();
      ctx.moveTo(x0, y0);
      ctx.lineTo(x1, y1);
      ctx.stroke();
    };

    const draw = () => {
      ctx.clearRect(0, 0, w, h);
      if (glow > 0.01 && cx > -5000) {
        ctx.lineWidth = 1.1;
        ctx.lineCap = "round";
        const denom = REACH + GRID; // fade columns/rows beyond ~1.5 cells
        // vertical lines snapped to grid columns around cursor
        const c0 = Math.floor((cx - denom) / GRID);
        const c1 = Math.ceil((cx + denom) / GRID);
        for (let c = c0; c <= c1; c++) {
          const gx = c * GRID;
          const d = Math.abs(gx - cx);
          const peak = Math.max(0, 1 - d / denom) * glow;
          if (peak <= 0.01) continue;
          drawLineGrad(gx, cy - HALF, gx, cy + HALF, peak);
        }
        // horizontal lines snapped to grid rows around cursor
        const r0 = Math.floor((cy - denom) / GRID);
        const r1 = Math.ceil((cy + denom) / GRID);
        for (let r = r0; r <= r1; r++) {
          const gy = r * GRID;
          const d = Math.abs(gy - cy);
          const peak = Math.max(0, 1 - d / denom) * glow;
          if (peak <= 0.01) continue;
          drawLineGrad(cx - HALF, gy, cx + HALF, gy, peak);
        }
        ctx.shadowBlur = 0;
      }
    };

    const loop = () => {
      cx += (tx - cx) * 0.14;
      cy += (ty - cy) * 0.14;
      glow += (glowTarget - glow) * 0.08;
      draw();
      // suspend once the highlight has fully settled (faded out, no pending target)
      const settled = glow < 0.01 && glowTarget < 0.01 &&
      Math.abs(tx - cx) < 0.5 && Math.abs(ty - cy) < 0.5;
      if (settled) {
        raf = null;
        if (glow !== 0) {glow = 0;draw();} // clean final frame
        return;
      }
      raf = requestAnimationFrame(loop);
    };
    const ensureRunning = () => {
      if (raf == null && visible) raf = requestAnimationFrame(loop);
    };
    // don't spend frames on the grid while the hero is scrolled out of view
    const io = new IntersectionObserver((entries) => {
      visible = entries[0].isIntersecting;
      if (!visible) {if (raf != null) {cancelAnimationFrame(raf);raf = null;}} else {ensureRunning();}
    }, { threshold: 0 });
    io.observe(hero);
    ensureRunning();

    return () => {
      if (raf != null) cancelAnimationFrame(raf);
      io.disconnect();
      ro.disconnect();
      window.removeEventListener("mousemove", onMove);
      hero.removeEventListener("mouseleave", onLeave);
    };
  }, []);
  return <canvas ref={canvasRef} className="hero-cursor-canvas" aria-hidden="true" />;
}

function HeroVideo() {
  const vidRef = useRef(null);
  useEffect(() => {
    /* moov atom is at file end + server ignores Range requests, so streaming
       fails — fetch whole file as a blob and play from an object URL */
    let url = null;
    let cancelled = false;
    fetch("assets/hero-video.mp4").
    then((r) => r.blob()).
    then((blob) => {
      if (cancelled) return;
      url = URL.createObjectURL(blob);
      const v = vidRef.current;
      if (v) {
        v.src = url;
        v.play().catch(() => {});
      }
    });
    return () => {
      cancelled = true;
      if (url) URL.revokeObjectURL(url);
    };
  }, []);
  return (
    <span className="hero-mock hero-video" aria-hidden="true">
      <video ref={vidRef} autoPlay muted loop playsInline></video>
    </span>);

}

function Hero({ onReachOut, intro, introStart }) {
  const root = useRef(null);

  /* First-visit intro: "People" & "Business" outlines wipe on then fill,
     the video container border draws then the frame fades in, and the rest
     of the hero fades up once those two finish. Runs once after the loading
     screen leaves; on revisits the normal scroll-reveal handles the hero. */
  useEffect(() => {
    if (!intro || !introStart) return;
    const el = root.current;
    const gsap = window.gsap;
    const words = el.querySelectorAll(".hero-word");
    const built = el.querySelector(".built");
    const eyebrow = el.querySelector(".hero-eyebrow");
    const bottom = el.querySelector(".hero-bottom");
    const desc = el.querySelector(".hero-bottom p");
    const cta = el.querySelector(".hero-bottom .pill-cta");
    const video = el.querySelector(".hero-video video");
    const videoBox = el.querySelector(".hero-video");
    /* generous clip box so tight line-height (.85) descenders (p, g) aren't cut */
    const OPEN = "inset(-35% 0% -60% 0)";
    const SHUT = "inset(-35% 100% -60% 0)";

    const finish = () => {
      fills.forEach((f) => f.remove());
      words.forEach((w) => {w.style.cssText = "";});
      [eyebrow, built].forEach((n) => {if (n) n.style.cssText = "";});
      if (desc) desc.style.cssText = "";
      if (cta) cta.style.cssText = "";
      if (video) {video.style.opacity = "";video.style.clipPath = "";}
      el.classList.remove("hero-intro");
    };

    /* filled overlay clone per word — lets the solid fill wipe in left→right
       over the drawn outline, instead of flashing the whole word at once */
    const fills = [];
    words.forEach((w) => {
      w.style.position = "relative";
      const f = w.cloneNode(true);
      f.className = "hero-word-fill";
      f.style.cssText = "position:absolute;left:0;top:0;color:var(--ink);-webkit-text-stroke:0;pointer-events:none;";
      w.appendChild(f);
      fills.push(f);
    });

    if (!gsap) {finish();return;}

    /* draw-on border overlay for the video container */
    const svgNS = "http://www.w3.org/2000/svg";
    const svg = document.createElementNS(svgNS, "svg");
    svg.setAttribute("viewBox", "0 0 100 100");
    svg.setAttribute("preserveAspectRatio", "none");
    svg.style.cssText = "position:absolute;inset:0;width:100%;height:100%;pointer-events:none;z-index:3;overflow:visible;";
    const rect = document.createElementNS(svgNS, "rect");
    rect.setAttribute("x", "1");rect.setAttribute("y", "1");
    rect.setAttribute("width", "98");rect.setAttribute("height", "98");
    rect.setAttribute("rx", "5");rect.setAttribute("fill", "none");
    rect.setAttribute("stroke", "var(--ink)");rect.setAttribute("stroke-width", "1");
    rect.setAttribute("vector-effect", "non-scaling-stroke");
    rect.setAttribute("stroke-dasharray", "400");
    svg.appendChild(rect);
    if (videoBox) videoBox.appendChild(svg);

    gsap.set(words, { color: "transparent", webkitTextStrokeWidth: "1.4px", webkitTextStrokeColor: "var(--ink)", clipPath: OPEN, opacity: 1 });
    gsap.set(fills, { clipPath: SHUT });
    gsap.set([eyebrow, built].filter(Boolean), { opacity: 0, y: 16 });
    if (desc) gsap.set(desc, { clipPath: "inset(0 100% 0 0)" });
    if (cta) gsap.set(cta, { opacity: 0, x: -44 });
    if (video) gsap.set(video, { opacity: 1, clipPath: "inset(0 100% 0 0)" });
    gsap.set(rect, { strokeDashoffset: 400 });

    const tl = gsap.timeline({ onComplete: finish });
    /* 1 + 2 in parallel over 2s: word outlines wipe on, video border draws */
    tl.fromTo(words, { clipPath: SHUT }, { clipPath: OPEN, duration: 2, ease: "power1.inOut", stagger: 0.12 }, 0);
    tl.to(rect, { strokeDashoffset: 0, duration: 2, ease: "power1.inOut" }, 0);
    /* fill wipes in left→right over each outline (same direction) */
    tl.to(fills, { clipPath: OPEN, duration: 0.7, ease: "power1.inOut", stagger: 0.12 }, 2);
    /* video frame wipes in left→right, same timing as the fill */
    if (video) tl.to(video, { clipPath: "inset(0 0% 0 0)", duration: 0.7, ease: "power1.inOut" }, 2);
    /* eyebrow + "Built for" fade up; description types in; button slides in
       left→right — all on the fill's 0.7s beat so nothing lingers */
    tl.to([eyebrow, built].filter(Boolean), { opacity: 1, y: 0, duration: 0.7, ease: "power2.out", stagger: 0.12 }, 2);
    if (desc) tl.to(desc, { clipPath: "inset(0 0% 0 0)", duration: 0.75, ease: "steps(30)" }, 2);
    if (cta) tl.to(cta, { opacity: 1, x: 0, duration: 0.7, ease: "power3.out" }, 2);
    tl.to(svg, { opacity: 0, duration: 0.4, ease: "none", onComplete: () => svg.remove() }, 2.5);

    return () => {tl.kill();svg.remove();fills.forEach((f) => f.remove());};
  }, [intro, introStart]);

  return (
    <section id="top" className={"hero" + (intro ? " hero-intro" : "")} data-screen-label="01 Hero" ref={root}>
      <div className="hero-grid" aria-hidden="true" />
      <HeroGrid />
      <div className="hero-grid-fade" aria-hidden="true" />
      <div className="container hero-inner">
        <div className="hero-eyebrow reveal">Design for</div>
        <h1 className="hero-title">
          <div className="line reveal delay-1">
            <span className="hero-word">People</span>
          </div>
          <div className="line right reveal delay-2">
            <span className="built">Built for</span>
            <HeroVideo />
          </div>
          <div className="line right reveal delay-3">
            <span className="hero-word">Business</span>
          </div>
        </h1>
        <div className="hero-bottom reveal delay-4">
          <p>
            Turning <i>business ideas</i> into experiences with real impacts through <i>user-centered</i> yet <i>tech-friendly</i> design.
          </p>
          <a href="#projects" className="pill-cta" onClick={(e) => {
            e.preventDefault();
            const target = document.getElementById("projects");
            if (!target) return;
            if (window.__lenis) {
              window.__lenis.scrollTo(target, { duration: 1.6 });
            } else if (window.gsap && window.ScrollToPlugin) {
              window.gsap.registerPlugin(window.ScrollToPlugin);
              window.gsap.to(window, { duration: 1.6, ease: "power2.inOut", scrollTo: { y: target, offsetY: 0 } });
            } else {
              target.scrollIntoView({ behavior: "smooth" });
            }
          }}>
            See Recent Work
            <span className="icon"><img src="assets/cta-arrow.png" alt="" width="24" height="24" style={{ width: "16px", height: "16px" }} /></span>
          </a>
        </div>
      </div>
    </section>);

}

/* ============= PROJECTS ============= */
const PROJECTS = [
{ id: "koolio", title: "Koolio", sub: "Apps for kid's achievement report", img: "assets/project-koolio.png" },
{ id: "styledoubler", title: "StyleDoubler", sub: "Apps micro animation", img: "assets/project-styledoubler.png" },
{ id: "kvliah", title: "Kvliah", sub: "Online learning platform", img: "assets/project-kvliah.png" },
{ id: "indiesel", title: "Indiesel", sub: "Startup business landing page", img: "assets/project-indiesel.png" }];


function Projects() {
  const root = useRef(null);

  /* simple one-shot float-in: all cards rise together when section enters view */
  useEffect(() => {
    const gsap = window.gsap;
    if (!gsap || !window.ScrollTrigger) return;
    gsap.registerPlugin(window.ScrollTrigger);
    if (window.matchMedia("(prefers-reduced-motion: reduce)").matches) return;
    const cards = root.current.querySelectorAll(".ph-card");
    const tween = gsap.fromTo(cards,
    { y: 120 },
    {
      y: 0, duration: 0.9, ease: "power3.out",
      scrollTrigger: { trigger: root.current, start: "top 80%", toggleActions: "restart none none reset" }
    });
    return () => {tween.scrollTrigger && tween.scrollTrigger.kill();tween.kill();};
  }, []);

  return (
    <section id="projects" className="ph-section" data-screen-label="02 Project Highlight" ref={root}>
      <div className="container">
        <div className="ph-grid">
          {PROJECTS.map((p) =>
          <a key={p.id} href={p.id === "koolio" ? "koolio.html" : p.id === "styledoubler" ? "styledoubler.html" : p.id === "kvliah" ? "kvliah.html" : p.id === "indiesel" ? "indiesel.html" : `case-study.html?p=${p.id}`} className={`ph-card ph-card-${p.id}`}>
              <div className="ph-head">
                <span className="ph-title">{p.title}</span>
                <span className="ph-sub">{p.sub}</span>
              </div>
              <div className="ph-thumb">
                <img src={p.img} alt={p.title} loading="lazy" />
              </div>
            </a>
          )}
        </div>
      </div>
    </section>);
}

/* ============= HEY (About) ============= */
function Hey() {
  const root = useRef(null);

  /* scroll-scrubbed draw-on: HEY letter outlines trace themselves as the user
     scrolls, then fill in; pink measure overlay wipes in; pill pops last */
  useEffect(() => {
    const gsap = window.gsap;
    if (!gsap || !window.ScrollTrigger) return;
    gsap.registerPlugin(window.ScrollTrigger);
    if (window.matchMedia("(prefers-reduced-motion: reduce)").matches) return;
    const el = root.current;
    const allStatic = el.querySelectorAll(".hey-static path");
    /* last path in the group is the dotted connector arrow — excluded from the
       tracing, it simply fades in at the end */
    const letters = Array.from(allStatic).slice(0, -1);
    const arrow = allStatic[allStatic.length - 1];
    const pink = el.querySelector(".hey-pink");
    const pill = el.querySelector(".hey-pill");
    const paras = el.querySelectorAll(".hey-body p");
    const actions = el.querySelector(".hey-actions");

    /* prep letters for stroke tracing */
    letters.forEach((p) => {
      const len = p.getTotalLength();
      p.style.strokeDasharray = len;
      p.style.strokeDashoffset = len;
      p.style.stroke = "#3D3D3D";
      p.style.strokeWidth = "0.6";
      p.style.fillOpacity = "0";
    });
    arrow.style.opacity = "0";
    /* prep description paragraphs for a scroll-scrubbed type-in wipe */
    paras.forEach((p) => {p.style.clipPath = "inset(0 100% 0 0)";});

    const tl = gsap.timeline({
      scrollTrigger: {
        trigger: el,
        start: "top 88%",
        end: "center 35%",
        scrub: 1
      }
    });
    tl.
    to(letters, { strokeDashoffset: 0, duration: 2.2, ease: "none", stagger: 0.25 }).
    to(letters, { fillOpacity: 1, strokeWidth: 0, duration: 0.8, ease: "none" }, "-=0.5").
    fromTo(pink, { clipPath: "inset(0 100% 0 0)" }, { clipPath: "inset(0 0% 0 0)", duration: 1.4, ease: "none" }, "-=1").
    fromTo(pill, { scale: 0, transformOrigin: "40px 160px" }, { scale: 1, duration: 0.8, ease: "back.out(1.6)" }, "-=0.4").
    to(arrow, { opacity: 1, duration: 0.7, ease: "none" }, "<").
    to(paras, { clipPath: "inset(0 0% 0 0)", duration: 1.1, ease: "steps(34)", stagger: 0.4 }, "<").
    fromTo(actions, { y: 40, autoAlpha: 0 }, { y: 0, autoAlpha: 1, duration: 0.9, ease: "power2.out" }, "-=0.3");

    /* dwell: once fully drawn, pin the section briefly so the user has time
       to interact (hover the HEY graphic) before the page scrolls on */
    const pin = window.ScrollTrigger.create({
      trigger: el,
      start: "center center",
      end: "+=650",
      pin: true,
      anticipatePin: 1
    });
    return () => {pin.kill();tl.scrollTrigger && tl.scrollTrigger.kill();tl.kill();};
  }, []);

  return (
    <section id="about" className="hey" data-screen-label="03 About" ref={root}>
      <div className="container">
        <div className="hey-grid">
          <div className="hey-art reveal">
            <HeyGraphic />
          </div>
          <div className="hey-body reveal delay-2">
            <p>
              A <i>"Digital Product Designer"</i> who focused on <i>Strategic UX</i>, packed by <i>10+ years of experience</i> in professional design field.
            </p>
            <p>
              Few things which "describe" me:<br />
              <i>Graphic design</i> past experience, enthusiasm into <i>strategic business</i>, and <i>engineering educational</i> background.
            </p>
            <div className="hey-actions">
              <a className="btn" href="assets/rizqi-naridha-resume.pdf" target="_blank" rel="noopener noreferrer">
                <span>View Resume</span>
                <span className="icon"><ResumeIcon size={20} /></span>
              </a>
            </div>
          </div>
        </div>
      </div>
    </section>);

}

/* ============= CERTIFICATE ============= */
const CERT_CARDS = [
{ color: "#5234DD", logo: <GoogleG />, title: "UX Design Professional Certificate", issuer: "Google", date: "Feb 2022", url: "https://www.credly.com/badges/9de94fdf-d044-4834-8281-7737e39ee7a0?source=linked_in_profile" },
{ color: "#CF1C9C", logo: <IBMMark />, title: "Enterprise Design Thinking Co-Creator", issuer: "IBM", date: "October 2022", url: "https://www.credly.com/badges/ce535aa6-24a8-42e9-8402-79c7db9335e6/public_url" },
{ color: "#E6A900", logo: <GoogleG />, title: "Project Management Specialization", issuer: "Google", date: "May 2024", url: "https://www.credly.com/badges/ddea6768-099e-48b2-8ae6-8d602402f820/public_url" }];

/* the single certificate CTA adopts the active card's color as you scroll, so
   we use that color to resolve which card is active and open its credential URL */
function openActiveCert(e) {
  const a = e.currentTarget;
  const m = getComputedStyle(a).backgroundColor.match(/\d+/g);
  let best = null, bestD = Infinity;
  if (m) {
    const [r, g, b] = m.map(Number);
    CERT_CARDS.forEach((c) => {
      const h = c.color.replace("#", "");
      const cr = parseInt(h.slice(0, 2), 16), cg = parseInt(h.slice(2, 4), 16), cb = parseInt(h.slice(4, 6), 16);
      const d = (r - cr) ** 2 + (g - cg) ** 2 + (b - cb) ** 2;
      if (d < bestD) {bestD = d;best = c;}
    });
  }
  /* only act when the button clearly matches a card color (not the initial
     black circle or a mid-transition color) */
  if (best && best.url && bestD < 2500) {
    /* point the anchor at the badge and let the browser open it natively in a
       new tab. A stable per-badge target name means a repeat click reuses and
       focuses that same tab instead of opening a duplicate. */
    a.href = best.url;
    a.target = "cert-" + (best.issuer + "-" + best.title).toLowerCase().replace(/[^a-z0-9]+/g, "-");
    return; // let the default navigation proceed (opens/focuses the named tab)
  }
  e.preventDefault();
}

function Certificate() {
  const root = useRef(null);
  useEffect(() => {
    const gsap = window.gsap;
    if (!gsap || !window.ScrollTrigger) return; // CSS default = final stacked state
    gsap.registerPlugin(window.ScrollTrigger);
    if (window.matchMedia("(prefers-reduced-motion: reduce)").matches) return;

    const el = root.current;
    const q = gsap.utils.selector(el);
    const stage = q(".cs-stage")[0];
    const cards = q(".cs-card");
    const stack = q(".cs-stack")[0];
    const cta = q(".cs-cta")[0];
    const mobile = window.matchMedia("(max-width: 720px)").matches;
    const STEP = mobile ? 56 : 66;

    /* collapsed (90°) cards form the deck themselves — positioned in the lower
       third of the stage, blue on top of the pile */
    const cardH = stack.offsetHeight;
    const stageH = stage.offsetHeight;
    const plateY = (i) => Math.round(stageH * 0.26) + i * 14;

    gsap.set(q(".cs-title"), { x: -140, opacity: 0 });
    /* GSAP must own the transform of elements whose CSS centering uses translate(),
       otherwise the parallax y tween wipes the -50%/-52% offsets */
    gsap.set(stack, { xPercent: -50, yPercent: -52, x: 0, y: 0 });
    gsap.set(q(".cs-outline"), { xPercent: -50, yPercent: -50, x: 0, y: 0 });
    gsap.set(cta, { x: 140, opacity: 0 });
    /* CTA starts as a black circle (arrow only); it expands into the colored
       pill when the first card floats */
    const ctaLabel = cta.querySelector("span");
    const ctaLabelW = ctaLabel.offsetWidth;
    gsap.set(ctaLabel, { width: 0, opacity: 0, overflow: "hidden", whiteSpace: "nowrap", display: "inline-block" });
    gsap.set(cta, { backgroundColor: "#111111", color: "#ffffff", gap: 0, paddingLeft: 15, paddingRight: 15 });
    /* arrow points DOWN while the button is a circle */
    const ctaArrow = cta.querySelector("svg");
    gsap.set(ctaArrow, { rotation: 90, transformOrigin: "50% 50%" });
    gsap.set(q(".cs-outline"), { opacity: 0 });
    gsap.set(cards, { rotationX: 90, transformPerspective: 1100, transformOrigin: "50% 50%" });
    cards.forEach((card, i) => {
      gsap.set(card, { y: plateY(i), zIndex: 3 - i }); // blue paints on top of the deck
      gsap.set(card.children, { opacity: 0 });
    });

    /* 1 — title, outline, and CTA slide in while the section approaches
       (before the pin engages), so there's no blank-screen gap */
    const intro = gsap.timeline({
      scrollTrigger: { trigger: el, start: "top 82%", end: "top top", scrub: 0.6 }
    });
    intro.to(q(".cs-outline"), { opacity: 1, duration: 0.7, ease: "none" }, 0).
    to(q(".cs-title"), { x: 0, opacity: 1, duration: 0.9, ease: "power2.out" }, 0).
    to(cta, { x: 0, opacity: 1, duration: 0.9, ease: "power2.out" }, 0);

    const tl = gsap.timeline({
      scrollTrigger: {
        trigger: el,
        start: "top top",
        end: "+=3200",
        scrub: 0.6,
        pin: true,
        anticipatePin: 1
      }
    });

    /* deck rises in at the start of the pin */
    tl.from(cards, { y: "+=60", autoAlpha: 0, stagger: 0.1, duration: 0.5, ease: "power2.out" }, 0);

    /* 2 & 3 — cards flip up from the deck in 3D, one by one.
       Final resting positions keep the whole revealed group vertically centered:
       front card sits at +i*STEP/2, earlier cards peek STEP apart above it. */
    cards.forEach((card, i) => {
      const at = 0.9 + i * 1.85; // pause between one card settling and the next rising
      tl.set(card, { zIndex: 10 + i }, at).
      to(card, { y: i * (STEP / 2), duration: 1.1, ease: "power3.out" }, at).
      to(card, { rotationX: 0, duration: 1.0, ease: "power2.inOut" }, at).
      to(card.children, { opacity: 1, duration: 0.35, ease: "none" }, at + 0.95);
      /* previously revealed cards drift up to peek behind the new front card */
      for (let p = 0; p < i; p++) {
        tl.to(cards[p], { y: "-=" + STEP / 2, duration: 1, ease: "power2.inOut" }, at);
      }
      /* CTA adopts the active card color */
      tl.to(cta, { backgroundColor: CERT_CARDS[i].color, color: "#ffffff", duration: 0.45, ease: "none" }, at + 0.35);
      if (i === 0) {
        /* black circle morphs into the pill; arrow rotates down → right */
        tl.to(ctaLabel, { width: ctaLabelW, opacity: 1, duration: 0.5, ease: "power2.out" }, at + 0.35).
        to(cta, { gap: 12, paddingLeft: 24, paddingRight: 24, duration: 0.5, ease: "power2.out" }, at + 0.35).
        to(ctaArrow, { rotation: 0, duration: 0.5, ease: "power2.out" }, at + 0.35);
      }
    });

    /* settle pause after the last card before the pin releases */
    tl.to({}, { duration: 1.0 });

    /* 4 — parallax exit after the pin releases */
    const st = tl.scrollTrigger;
    const parallax = [
    [stack, -150],
    [q(".cs-title")[0], -70],
    [cta, -70],
    [q(".cs-outline")[0], -30]].
    map(([target, dy]) =>
    gsap.fromTo(target, { y: 0 }, {
      y: dy, ease: "none",
      scrollTrigger: { trigger: stage, start: () => st.end, end: () => st.end + 800, scrub: 0.4 }
    })
    );

    const refresh = () => window.ScrollTrigger.refresh();
    window.addEventListener("load", refresh);
    const t = setTimeout(refresh, 5600); // after loading screen

    return () => {
      window.removeEventListener("load", refresh);
      clearTimeout(t);
      parallax.forEach((p) => p.scrollTrigger && p.scrollTrigger.kill());
      intro.scrollTrigger && intro.scrollTrigger.kill();
      intro.kill();
      tl.scrollTrigger && tl.scrollTrigger.kill();
      tl.kill();
    };
  }, []);

  return (
    <section id="certificate" ref={root} className="cs-section" data-screen-label="04 Certificate">
      <div className="cs-stage">
        <div className="cs-container">
          <div className="cs-outline" aria-hidden="true">Certificate</div>
          <h2 className="cs-title">Certificate</h2>
          <div className="cs-stack">
            {CERT_CARDS.map((c, i) =>
            <article className="cs-card" key={i} style={{ backgroundColor: c.color, zIndex: i + 1 }}>
                <div className="cs-card-logo">{c.logo}</div>
                <div className="cs-card-body">
                  <h3>{c.title}</h3>
                  <p>Issued by <strong>{c.issuer}</strong> on {c.date}</p>
                </div>
              </article>
            )}
          </div>
          <a className="cs-cta" href="#" onClick={openActiveCert}>
            <span>View Details</span>
            <svg width="19" height="16" viewBox="0 0 19 16" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
              <path d="M1.5 8h15" /><path d="M11 2.5L16.5 8L11 13.5" />
            </svg>
          </a>
        </div>
      </div>
    </section>);

}

/* ============= SHIPPED PRODUCTS ============= */
const SP_ICONS = [
{ src: "assets/shipped/icon-a.png", url: "https://apps.apple.com/id/app/rezerv-gym-wellness-sports/id1661249713" },
{ src: "assets/shipped/icon-b.png", url: "https://apps.apple.com/id/app/styledoubler-for-creators/id1590603600?l=id" },
{ src: "assets/shipped/icon-c.png", url: "https://bitcoinpostcards.net" },
{ src: "assets/shipped/icon-d.png", url: "https://apkpure.com/wallyst-free-hd-wallpapers-b/com.wallyst.wallyst" }];

/* open a URL in a new tab via a real anchor click — reliable new tab, keeps
   the current tab in place (unlike window.open with a custom name) */
function openInNewTab(url) {
  const a = document.createElement("a");
  a.href = url;
  a.target = "_blank";
  a.rel = "noopener noreferrer";
  document.body.appendChild(a);
  a.click();
  a.remove();
}


function Shipped() {
  const root = useRef(null);

  useEffect(() => {
    const gsap = window.gsap;
    if (!gsap || !window.ScrollTrigger) return; // CSS default = final aligned state
    gsap.registerPlugin(window.ScrollTrigger);
    if (window.matchMedia("(prefers-reduced-motion: reduce)").matches) return;

    const el = root.current;
    const q = gsap.utils.selector(el);
    const stage = q(".sp-stage")[0];
    const back = q(".sp-folder-back")[0];
    const front = q(".sp-folder-front")[0];
    const icons = q(".sp-icon");
    const title = q(".sp-title")[0];
    const outline = q(".sp-outline")[0];
    const mobile = window.matchMedia("(max-width: 720px)").matches;

    /* hover: each icon tilts 15° in a random direction — driven via the CSS
       `rotate` property from JS so it can't be clobbered by GSAP's transform */
    const hoverHandlers = [];
    icons.forEach((ic) => {
      const dir = (Math.random() < 0.5 ? -15 : 15) + "deg";
      const enter = () => {ic.style.rotate = dir;};
      const leave = () => {ic.style.rotate = "0deg";};
      ic.addEventListener("pointerenter", enter);
      ic.addEventListener("pointerleave", leave);
      hoverHandlers.push([ic, enter, leave]);
    });

    const stageH = stage.offsetHeight;
    const s = mobile ? 234 / 390 : 1;
    const folderH = 300 * s;
    /* CSS resting (final) positions — derived from the stylesheet constants
       (folder floats above the bottom edge; icons top: 28%)
       so re-runs and ScrollTrigger refreshes can't pollute the math */
    const folderRestTop = stageH / 2 + (mobile ? 18 : 7);
    const iconRestTop = stageH / 2 - (mobile ? 110 : 161);
    const iconW = mobile ? 60 : 100;
    /* start: folder vertically centered */
    const folderStartTop = Math.round((stageH - folderH) / 2);
    const dy = folderStartTop - folderRestTop;

    /* horizontal offsets relative to each icon's final (CSS) slot */
    const xF = mobile ? [-129, -43, 43, 129] : [-243, -81, 81, 243];
    const xI = mobile ? [-60, -21, 21, 60] : [-120, -45, 45, 120]; // buried in the folder
    const xP = mobile ? [-82, -30, 30, 82] : [-155, -55, 55, 155]; // popped at the mouth
    const rotI = [-18, 12, -8, 16];
    const rotP = [-14, 9, -7, 12];
    const popJ = [-8, -22, -4, -16];

    const iconBuriedY = folderStartTop + folderH * (mobile ? 0.30 : 0.45) - iconRestTop;
    gsap.set(title, { xPercent: -50, x: 0 });

    const tl = gsap.timeline({
      scrollTrigger: {
        trigger: el,
        start: "top top",
        end: "+=2600",
        scrub: 0.6,
        pin: true,
        anticipatePin: 1
      }
    });

    /* 1 — icons pop out of the folder mouth; outlined text fades in.
       fromTo + immediateRender pins the start values into the tweens themselves,
       so a ScrollTrigger refresh can never lose the initial state */
    tl.fromTo(outline, { opacity: 0 }, { opacity: 1, duration: 0.9, ease: "none", immediateRender: true }, 0);
    icons.forEach((ic, i) => {
      tl.fromTo(ic, {
        x: xI[i] - xF[i],
        y: iconBuriedY,
        rotation: rotI[i]
      }, {
        x: xP[i] - xF[i],
        y: folderStartTop - iconW * 0.62 + popJ[i] - iconRestTop,
        rotation: rotP[i],
        duration: 0.7, ease: "power2.out",
        immediateRender: true
      }, 0.15 + i * 0.12);
    });

    /* 2 — folder sinks to the bottom edge; icons rise into the aligned row;
       title drops in with a dissolve */
    const t2 = 1.35;
    tl.fromTo([back, front], { y: dy }, { y: 0, duration: 1.2, ease: "power2.inOut", immediateRender: true }, t2);
    icons.forEach((ic, i) => {
      tl.to(ic, { x: 0, y: 0, rotation: 0, duration: 1.2, ease: "power2.inOut" }, t2 + i * 0.06);
    });
    tl.fromTo(title, { y: -80, opacity: 0 }, { y: 0, opacity: 1, duration: 0.9, ease: "power2.out", immediateRender: true }, t2 + 0.5);

    /* 3 — settle pause before the pin releases */
    tl.to({}, { duration: 0.9 });

    /* 4 — parallax exit after the pin releases.
       Uses yPercent so it never shares the `y` channel the timeline owns —
       scrolling back up can't clobber the pinned initial state. */
    const st = tl.scrollTrigger;
    const parallax = [
    [[back, front], -140],
    [icons[0], -100],
    [icons[1], -80],
    [icons[2], -110],
    [icons[3], -90],
    [title, -50],
    [outline, -25]].
    map(([target, pdy]) => {
      const first = Array.isArray(target) ? target[0] : target;
      const elh = first.getBoundingClientRect().height || 100;
      return gsap.fromTo(target, { yPercent: 0 }, {
        yPercent: pdy / elh * 100, ease: "none", immediateRender: false,
        scrollTrigger: { trigger: stage, start: () => st.end, end: () => st.end + 800, scrub: 0.4 }
      });
    });

    const refresh = () => window.ScrollTrigger.refresh();
    window.addEventListener("load", refresh);
    const t = setTimeout(refresh, 5600);
    return () => {
      window.removeEventListener("load", refresh);
      clearTimeout(t);
      hoverHandlers.forEach(([ic, enter, leave]) => {
        ic.removeEventListener("pointerenter", enter);
        ic.removeEventListener("pointerleave", leave);
      });
      parallax.forEach((p) => p.scrollTrigger && p.scrollTrigger.kill());
      tl.scrollTrigger && tl.scrollTrigger.kill();
      tl.kill();
    };
  }, []);

  return (
    <section id="shipped" className="sp-section" ref={root} data-screen-label="05 Shipped">
      <div className="sp-stage">
        <div className="sp-outline" aria-hidden="true">Shipped Product</div>
        <h2 className="sp-title">Shipped Product</h2>
        {SP_ICONS.map((item, i) =>
        <img key={i} className={`sp-icon sp-i${i + 1}${item.url ? " sp-linked" : ""}`} src={item.src} alt="Shipped app icon"
        onClick={item.url ? () => openInNewTab(item.url) : undefined} />
        )}
        <svg className="sp-folder-back" viewBox="0 0 390 279.351" fill="#0087DA" xmlns="http://www.w3.org/2000/svg">
          <path d="M 111.023 0 C 116.397 0 121.55 2.134 125.35 5.934 L 147.857 28.441 L 369.74 28.441 C 380.929 28.441 390 37.512 390 48.701 L 390 259.091 C 390 270.28 380.929 279.351 369.74 279.351 L 20.26 279.351 C 9.071 279.351 0 270.28 0 259.091 L 0 20.26 C 0 9.071 9.071 0 20.26 0 L 111.023 0 Z" fillRule="nonzero" />
        </svg>
        <svg className="sp-folder-front" viewBox="0 0 390 250.909" fill="#4FC2F8" xmlns="http://www.w3.org/2000/svg">
          <path d="M 369.74 0 C 380.929 0 390 9.071 390 20.26 L 390 230.649 C 390 241.838 380.929 250.909 369.74 250.909 L 20.26 250.909 C 9.071 250.909 0 241.839 0 230.649 L 0 20.26 C 0 9.071 9.071 0 20.26 0 L 369.74 0 Z" fillRule="nonzero" />
        </svg>
      </div>
    </section>);

}

/* ============= HIGHLIGHTS ============= */
const HIGHLIGHTS = [
{
  img: "assets/hl-1.png",
  label: "MyRent property dashboard",
  title: "MyRent",
  desc: "The vehicle rental software dashboard prototype features a clean, intuitive interface for browsing vehicles, managing bookings, and tracking rentals.",
  cta: { label: "Try Prototype", href: "#" }
},
{
  img: "assets/hl-2.png",
  label: "Mobile app — onboarding",
  title: "Simple",
  desc: "A grocery e-commerce mobile concept focused on fast re-ordering and a friendly, low-friction checkout flow.",
  cta: { label: "Try Prototype", href: "#" }
},
{
  img: "assets/hl-3.png",
  label: "Mobile app — checkout",
  title: "Simple — Checkout",
  desc: "Streamlined checkout exploration: address, payment and review collapsed into a single confident screen.",
  cta: null
},
{
  img: "assets/hl-4.png",
  label: "Travel & booking concept",
  title: "Bromo",
  desc: "A travel landing page concept inviting users to explore guided trips, with bold editorial imagery and a calm booking path.",
  cta: { label: "Try Prototype", href: "#" }
},
{
  img: "assets/hl-5.png",
  label: "Editorial landing page",
  title: "Editorial",
  desc: "An editorial landing page study balancing large display type with generous whitespace and a quiet, confident palette.",
  cta: null
},
{
  img: "assets/hl-6.png",
  label: "iPad smart-home app",
  title: "Smart Room",
  desc: "An iPad smart-home control concept — room-by-room scenes, lighting and climate, all reachable within a thumb's reach.",
  cta: { label: "Try Prototype", href: "#" }
}];


const PlayIcon = ({ size = 14 }) =>
<svg width={size} height={size} viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg">
    <path d="M4 2.5L11 7L4 11.5V2.5Z" fill="currentColor" />
  </svg>;


function HighlightDrawer({ item, onClose }) {
  const [closing, setClosing] = useState(false);
  const close = useCallback(() => {
    setClosing(true);
    setTimeout(onClose, 380);
  }, [onClose]);

  useEffect(() => {
    if (!item) return;
    setClosing(false);
    document.body.style.overflow = "hidden";
    const onKey = (e) => {if (e.key === "Escape") close();};
    window.addEventListener("keydown", onKey);
    return () => {
      document.body.style.overflow = "";
      window.removeEventListener("keydown", onKey);
    };
  }, [item, close]);

  if (!item) return null;
  return (
    <div className={`hl-drawer-backdrop ${closing ? "is-closing" : ""}`} onClick={close}>
      <div className={`hl-drawer ${closing ? "is-closing" : ""}`} onClick={(e) => e.stopPropagation()} style={{ padding: "2px" }}>
        <div className="hl-drawer-inner" style={{ padding: "24px" }}>
          <div className="hl-drawer-img">
            <img src={item.img} alt={item.label} />
          </div>
          <div className="hl-drawer-side">
            <button className="hl-drawer-close" onClick={close} aria-label="Close">
              <CloseIcon size={16} />
            </button>
            <div className="hl-drawer-body" style={{ padding: "0px 40px 0px 0px" }}>
              <h3 className="hl-drawer-title">{item.title}</h3>
              <p className="hl-drawer-desc">{item.desc}</p>
              {item.cta &&
              <a className="btn hl-drawer-cta" href={item.cta.href} onClick={(e) => e.preventDefault()}>
                  <span>{item.cta.label}</span>
                  <span className="icon"><PlayIcon size={14} /></span>
                </a>
              }
            </div>
          </div>
        </div>
      </div>
    </div>);
}

function Highlights() {
  const [active, setActive] = useState(null);
  const root = useRef(null);

  /* Entrance: title types in, then each card's image fills in left→right.
     Plays once, automatically, when the section enters view (no scrub). */
  useEffect(() => {
    const gsap = window.gsap;
    if (!gsap || !window.ScrollTrigger) return;
    gsap.registerPlugin(window.ScrollTrigger);
    if (window.matchMedia("(prefers-reduced-motion: reduce)").matches) return;
    const cards = Array.from(root.current.querySelectorAll(".hl"));
    const title = root.current.querySelector(".highlights-h");
    if (title) gsap.set(title, { clipPath: "inset(-15% 100% -30% 0)" });
    cards.forEach((card) => gsap.set(card.querySelector("img"), { clipPath: "inset(0 100% 0 0)" }));

    const tl = gsap.timeline({
      scrollTrigger: { trigger: root.current, start: "top 78%", once: true }
    });
    if (title) tl.to(title, { clipPath: "inset(-15% 0% -30% 0)", duration: 0.7, ease: "steps(20)" }, 0);
    cards.forEach((card, i) => {
      tl.to(card.querySelector("img"), { clipPath: "inset(0 0% 0 0)", duration: 0.6, ease: "power1.inOut" }, 0.3 + i * 0.14);
    });
    return () => {tl.scrollTrigger && tl.scrollTrigger.kill();tl.kill();};
  }, []);

  return (
    <section id="highlights" className="highlights" data-screen-label="06 Highlights" ref={root}>
      <div className="container">
        <h2 className="highlights-h">Highlights</h2>
        <div className="hl-grid">
          {/* Row 1 — big left, 2 small right */}
          <div className="hl-row">
            <div className="hl hl-big" onClick={() => setActive(HIGHLIGHTS[0])}>
              <img src={HIGHLIGHTS[0].img} alt={HIGHLIGHTS[0].label} loading="lazy" />
            </div>
            <div className="hl-col">
              <div className="hl hl-small" onClick={() => setActive(HIGHLIGHTS[1])}>
                <img src={HIGHLIGHTS[1].img} alt={HIGHLIGHTS[1].label} loading="lazy" />
              </div>
              <div className="hl hl-small" onClick={() => setActive(HIGHLIGHTS[2])}>
                <img src={HIGHLIGHTS[2].img} alt={HIGHLIGHTS[2].label} loading="lazy" />
              </div>
            </div>
          </div>
          {/* Row 2 — 2 small left, big right */}
          <div className="hl-row">
            <div className="hl-col">
              <div className="hl hl-small" onClick={() => setActive(HIGHLIGHTS[3])}>
                <img src={HIGHLIGHTS[3].img} alt={HIGHLIGHTS[3].label} loading="lazy" />
              </div>
              <div className="hl hl-small" onClick={() => setActive(HIGHLIGHTS[4])}>
                <img src={HIGHLIGHTS[4].img} alt={HIGHLIGHTS[4].label} loading="lazy" />
              </div>
            </div>
            <div className="hl hl-big" onClick={() => setActive(HIGHLIGHTS[5])}>
              <img src={HIGHLIGHTS[5].img} alt={HIGHLIGHTS[5].label} loading="lazy" />
            </div>
          </div>
        </div>
      </div>
      <HighlightDrawer item={active} onClose={() => setActive(null)} />
    </section>);

}

/* ============= FOOTER ============= */
function Footer({ onReachOut }) {
  return (
    <footer id="journal" className="footer" data-screen-label="07 Footer">
      <div className="footer-inner">
        <div className="footer-brand">
          <Logo height={80} invert />
          <p>Independent product designer building human, strategic, business-driven digital experiences since 2016.</p>
          <a href="#" className="footer-mail" onClick={(e) => {e.preventDefault();onReachOut();}}>rnaridha@gmail.com

          </a>
        </div>
        <div className="footer-cols">
          <div className="footer-col">
            <h4>Sitemap</h4>
            <ul>
              <li><a href="#projects">Projects</a></li>
              <li><a href="#highlights">Highlights</a></li>
              <li><a href="#about">About</a></li>
              <li><a href="#journal">Journal</a></li>
            </ul>
          </div>
          <div className="footer-col">
            <h4>Elsewhere</h4>
            <ul>
              <li><a href="#" onClick={(e) => e.preventDefault()}>Dribbble</a></li>
              <li><a href="#" onClick={(e) => e.preventDefault()}>Behance</a></li>
              <li><a href="#" onClick={(e) => e.preventDefault()}>LinkedIn</a></li>
              <li><a href="#" onClick={(e) => e.preventDefault()}>Read.cv</a></li>
            </ul>
          </div>
        </div>
      </div>
      <div className="footer-bottom">
        <span>© {new Date().getFullYear()} Rizqi Naridha · All rights reserved.</span>
        <span>Made with care · Plus Jakarta Sans</span>
      </div>
    </footer>);

}

/* ============= REACH OUT MODAL ============= */
/* The contact modal now lives in assets/site-footer.jsx (window.ReachOutModal),
   shared across every page. Open it anywhere via window.openReachOut(). */

/* ============= TWEAKS ============= */
function Tweaks() {
  const { TweaksPanel, useTweaks, TweakToggle, TweakSection } = window;
  if (!TweaksPanel) return null;
  const [t, setTweak] = useTweaks(TWEAK_DEFAULTS);
  useEffect(() => {
    document.documentElement.setAttribute("data-theme", t.dark ? "dark" : "light");
  }, [t.dark]);
  return (
    <TweaksPanel title="Tweaks">
      <TweakSection label="Appearance">
        <TweakToggle label="Dark mode" value={t.dark} onChange={(v) => setTweak("dark", v)} />
      </TweakSection>
    </TweaksPanel>);

}

/* ============= APP ============= */
function App() {
  const startedLoadingRef = useRef(false);
  const [loading, setLoading] = useState(() => {
    if (typeof sessionStorage === 'undefined') return true;
    const first = !sessionStorage.getItem('porto:loaded');
    startedLoadingRef.current = first;
    return first;
  });
  useScrollReveal();
  useScrollTriggerImageRefresh();
  useEffect(() => {
    if (!loading && typeof sessionStorage !== 'undefined') {
      sessionStorage.setItem('porto:loaded', '1');
    }
  }, [loading]);
  return (
    <React.Fragment>
      {loading && <LoadingScreen onDone={() => setLoading(false)} />}
      {!loading && <CustomCursor />}
      <Nav onReachOut={() => window.openReachOut?.()} />
      <Hero onReachOut={() => window.openReachOut?.()} intro={startedLoadingRef.current} introStart={!loading} />
      <Projects />
      <Hey />
      <Certificate />
      <Shipped />
      <Highlights />
      {window.SiteFooter ? <window.SiteFooter /> : <Footer onReachOut={() => window.openReachOut?.()} />}
      <Tweaks />
    </React.Fragment>);

}

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