/* ============================================================
   KOMPONEN INTI — Interactive CV
   PhotoSlot, Panel info, 3 konsep interaksi, render konten.
   ============================================================ */
const { useState, useEffect, useRef, useCallback } = React;

/* ---------- Foto interaktif (drag & drop, tersimpan) ---------- */
/* ---------- Foto profil (terkunci — efek hover tetap, tapi tidak bisa diganti) ---------- */
function PhotoSlot({ accent, styleMode, defaultSrc }) {
  const [hover, setHover] = useState(false);
  const [intro, setIntro] = useState(true);
  const shown = defaultSrc || null;

  useEffect(() => {
    const id = setTimeout(() => setIntro(false), 1050);
    return () => clearTimeout(id);
  }, []);

  return (
    <div
      className={"photo" + (hover ? " is-hover" : "") + (shown ? " has-img" : "") + (intro ? " intro" : "")}
      data-ps={styleMode || "cutout"}
      onMouseEnter={() => setHover(true)}
      onMouseLeave={() => setHover(false)}
    >
      {shown ? (
        <React.Fragment>
          <img src={shown} alt="Foto Ian Septian" className="photo-img" />
          <div
            className="photo-shine"
            style={
              (styleMode || "cutout") !== "card"
                ? {
                    WebkitMaskImage: `url(${shown})`, maskImage: `url(${shown})`,
                    WebkitMaskSize: "contain", maskSize: "contain",
                    WebkitMaskPosition: "bottom center", maskPosition: "bottom center",
                    WebkitMaskRepeat: "no-repeat", maskRepeat: "no-repeat",
                  }
                : null
            }
          />
        </React.Fragment>
      ) : (
        <div className="photo-ph">
          <div className="photo-ph-stripes" />
          <div className="photo-ph-label">
            <span className="dot" style={{ background: accent }} />
            <span>FOTO PORTRAIT</span>
            <small className="photo-ph-size">assets/images/profile/me.png</small>
          </div>
        </div>
      )}
      {/* bingkai siku-siku — animasi buka dari tengah saat load */}
      <div className="photo-frame">
        <span className="bracket br-tl" style={{ borderColor: accent }} />
        <span className="bracket br-br" style={{ borderColor: accent }} />
        <span className="frame-line fl-top" style={{ background: accent }} />
        <span className="frame-line fl-left" style={{ background: accent }} />
        <span className="frame-line fl-bottom" style={{ background: accent }} />
        <span className="frame-line fl-right" style={{ background: accent }} />
      </div>
    </div>
  );
}

/* ---------- Teks latar bergerak (typewriter) ---------- */
function BigType({ words, motion }) {
  const [txt, setTxt] = useState((words && words[0]) || "");
  useEffect(() => {
    if (!words || !words.length) return;
    if (!motion) { setTxt(words[0]); return; }
    let w = 0, c = 0, deleting = false, timer;
    const tick = () => {
      const word = words[w % words.length];
      if (!deleting) {
        c++; setTxt(word.slice(0, c));
        if (c >= word.length) { deleting = true; timer = setTimeout(tick, 1500); return; }
        timer = setTimeout(tick, 95);
      } else {
        c--; setTxt(word.slice(0, Math.max(0, c)));
        if (c <= 0) { deleting = false; w++; timer = setTimeout(tick, 300); return; }
        timer = setTimeout(tick, 45);
      }
    };
    timer = setTimeout(tick, 600);
    return () => clearTimeout(timer);
  }, [motion, words]);
  return (
    <div className={"bgtype" + (motion ? " anim" : "")} aria-hidden="true">
      <span className="bgtype-text">{txt}</span>
      <span className="bgtype-caret" />
    </div>
  );
}

/* ---------- Pager (tombol ‹ halaman › ) ---------- */
function Pager({ page, total, accent, onPrev, onNext }) {
  if (total <= 1) return null;
  return (
    <div className="pf-pager" style={{ "--accent": accent }}>
      <button className="pf-pg-btn" onClick={onPrev} aria-label="Halaman sebelumnya">‹</button>
      <span className="pf-pg-count">{page + 1} / {total}</span>
      <button className="pf-pg-btn" onClick={onNext} aria-label="Halaman berikutnya">›</button>
    </div>
  );
}

/* ---------- Portofolio (paginasi otomatis; lebih banyak saat mode lebar) ---------- */
const SHORTS_PER_PAGE = 6;        // mode normal: 3 kolom × 2 baris
const SHORTS_PER_PAGE_WIDE = 10;  // mode lebar: 5 kolom × 2 baris
const FEAT_PER_PAGE = 1;          // mode normal
const FEAT_PER_PAGE_WIDE = 2;     // mode lebar: 2 berdampingan
function PortfolioContent({ s, accent, onPlayVideo, expanded }) {
  const shorts = s.shorts || [];
  const featured = s.featured || [];
  const shPer = expanded ? SHORTS_PER_PAGE_WIDE : SHORTS_PER_PAGE;
  const ftPer = expanded ? FEAT_PER_PAGE_WIDE : FEAT_PER_PAGE;
  const [shPage, setShPage] = useState(0);
  const [ftPage, setFtPage] = useState(0);

  const shTotal = Math.max(1, Math.ceil(shorts.length / shPer));
  const ftTotal = Math.max(1, Math.ceil(featured.length / ftPer));
  // Jaga halaman tetap valid saat jumlah per-halaman berubah (toggle lebar).
  const shCur = Math.min(shPage, shTotal - 1);
  const ftCur = Math.min(ftPage, ftTotal - 1);
  const shStart = shCur * shPer;
  const ftStart = ftCur * ftPer;
  const shView = shorts.slice(shStart, shStart + shPer);
  const ftView = featured.slice(ftStart, ftStart + ftPer);

  return (
    <div className="sc">
      <p className="sc-lead">{s.body}</p>

      {shorts.length > 0 && (
        <React.Fragment>
          <div className="pf-subhead">
            <span className="pf-sub-label">Shorts</span>
            <Pager page={shCur} total={shTotal} accent={accent}
              onPrev={() => setShPage((p) => (Math.min(p, shTotal - 1) - 1 + shTotal) % shTotal)}
              onNext={() => setShPage((p) => (Math.min(p, shTotal - 1) + 1) % shTotal)} />
          </div>
          <div className={"pf-grid" + (expanded ? " pf-grid--wide" : "")}>
            {shView.map((v, i) => {
              const gi = shStart + i;
              return (
                <button key={gi} className="pf-card" onClick={() => onPlayVideo && onPlayVideo("shorts", gi)}
                  style={{ "--accent": accent }}>
                  <img className="pf-thumb" loading="lazy"
                    src={`https://i.ytimg.com/vi/${v.id}/hqdefault.jpg`} alt={v.title} />
                  <span className="pf-shade" />
                  <span className="pf-play" style={{ borderColor: accent }}>
                    <svg viewBox="0 0 24 24" fill={accent} aria-hidden="true"><path d="M8 6.5 18 12 8 17.5V6.5Z" /></svg>
                  </span>
                  <span className="pf-cap">{v.title}</span>
                </button>
              );
            })}
          </div>
        </React.Fragment>
      )}

      {featured.length > 0 && (
        <React.Fragment>
          <div className="pf-subhead pf-subhead--feat">
            <span className="pf-sub-label">Landscape</span>
            <Pager page={ftCur} total={ftTotal} accent={accent}
              onPrev={() => setFtPage((p) => (Math.min(p, ftTotal - 1) - 1 + ftTotal) % ftTotal)}
              onNext={() => setFtPage((p) => (Math.min(p, ftTotal - 1) + 1) % ftTotal)} />
          </div>
          <div className={"pf-feature-grid" + (expanded ? " pf-feature-grid--wide" : "")}>
            {ftView.map((v, i) => {
              const gi = ftStart + i;
              return (
                <button key={gi} className="pf-feature" onClick={() => onPlayVideo && onPlayVideo("featured", gi)}
                  style={{ "--accent": accent }}>
                  <img className="pf-thumb" loading="lazy"
                    src={`https://i.ytimg.com/vi/${v.id}/hqdefault.jpg`} alt={v.title} />
                  <span className="pf-shade" />
                  <span className="pf-play" style={{ borderColor: accent }}>
                    <svg viewBox="0 0 24 24" fill={accent} aria-hidden="true"><path d="M8 6.5 18 12 8 17.5V6.5Z" /></svg>
                  </span>
                  <span className="pf-cap">{v.title} <span className="pf-tag">landscape</span></span>
                </button>
              );
            })}
          </div>
        </React.Fragment>
      )}
    </div>
  );
}

/* ---------- Tombol kontak 3D (klik = langsung buka) ----------
   Ikon (ContactIcon) & iconKindFromType berasal dari cv-icons.jsx. */
function ContactButtons({ channels, accent }) {
  const list = channels || [];

  // Jenis ikon: pakai pilihan eksplisit (c.icon) jika ada, kalau tidak tebak dari teks.
  const kindOf = (c) => (c.icon ? c.icon : iconKindFromType(c.type));

  const urlFor = (c, kind) => {
    if (c.href) return c.href;
    const v = String(c.value || "").replace(/^@/, "").trim();
    if (kind === "instagram") return "https://instagram.com/" + v;
    if (kind === "tiktok") return "https://www.tiktok.com/@" + v;
    if (kind === "youtube") return "https://youtube.com/@" + v;
    if (kind === "facebook") return "https://facebook.com/" + v;
    if (kind === "x") return "https://x.com/" + v;
    if (kind === "linkedin") return "https://www.linkedin.com/in/" + v;
    if (kind === "telegram") return "https://t.me/" + v;
    if (kind === "whatsapp") return "https://wa.me/" + v.replace(/[^0-9]/g, "");
    if (kind === "email") return "mailto:" + String(c.value || "").trim();
    if (kind === "website") {
      const raw = String(c.value || "").trim();
      return /^https?:\/\//i.test(raw) ? raw : "https://" + raw;
    }
    return "";
  };
  const onClick = (c, kind) => {
    const url = urlFor(c, kind);
    if (!url) return;
    if (kind === "email") { window.location.href = url; return; }
    window.open(url, "_blank", "noopener,noreferrer");
  };

  return (
    <div className="c3d-list">
      {list.map((c, i) => {
        const kind = kindOf(c);
        return (
          <button key={i} type="button" className="c3d" style={{ "--accent": accent }}
            onClick={() => onClick(c, kind)}>
            <span className="c3d-ico"><ContactIcon kind={kind} /></span>
            <span className="c3d-main">
              <span className="c3d-type">{c.type}</span>
              <span className="c3d-val">{c.value}</span>
            </span>
            <span className="c3d-arr">{kind === "email" ? "✉" : "↗"}</span>
          </button>
        );
      })}
    </div>
  );
}

/* ---------- Konten tiap seksi ---------- */
function SectionContent({ id, data, accent, onPlayVideo, expanded }) {
  const s = data.sections[id];
  if (!s) return null;

  if (id === "tentang") {
    return (
      <div className="sc">
        <p className="sc-lead">{s.body}</p>
        <div className="sc-funfact">
          <span className="sc-funfact-tag" style={{ color: accent }}>fun fact</span>
          <p>{s.funfact}</p>
        </div>
        <ul className="sc-meta">
          <li><span>Peran</span>{data.role}</li>
          <li><span>Lokasi</span>{data.location}</li>
        </ul>
      </div>
    );
  }

  if (id === "pengalaman") {
    return (
      <div className="sc">
        <ol className="timeline">
          {s.items.map((it, i) => (
            <li key={i} className={"tl-item" + (it.current ? " is-now" : "")}>
              <span className="tl-dot" style={{ background: it.current ? accent : "transparent", borderColor: accent }} />
              <div className="tl-head">
                <span className="tl-period" style={{ color: accent }}>{it.period}</span>
                {it.current && <span className="tl-now">sekarang</span>}
              </div>
              <h4 className="tl-company">{it.company}</h4>
              <div className="tl-role">{it.role}</div>
              <p className="tl-note">{it.note}</p>
            </li>
          ))}
        </ol>
      </div>
    );
  }

  if (id === "pendidikan") {
    return (
      <div className="sc">
        {s.items.map((it, i) => (
          <div key={i} className="edu">
            <div className="edu-badge" style={{ borderColor: accent, color: accent }}>{it.period}</div>
            <div>
              <h4 className="edu-school">{it.school}</h4>
              <div className="edu-degree">{it.degree}</div>
              <p className="edu-note">{it.note}</p>
            </div>
          </div>
        ))}
      </div>
    );
  }

  if (id === "keahlian") {
    return (
      <div className="sc">
        <div className="skills">
          {s.items.map((it, i) => (
            <div key={i} className="skill">
              <div className="skill-no" style={{ color: accent }}>{String(i + 1).padStart(2, "0")}</div>
              <h4 className="skill-name">{it.name}</h4>
              <p className="skill-note">{it.note}</p>
            </div>
          ))}
        </div>
      </div>
    );
  }

  if (id === "portofolio") {
    return <PortfolioContent s={s} accent={accent} onPlayVideo={onPlayVideo} expanded={expanded} />;
  }

  if (id === "kontak") {
    return (
      <div className="sc">
        <p className="sc-lead">{s.body}</p>
        <ContactButtons channels={s.channels} accent={accent} />
      </div>
    );
  }
  return null;
}

/* ---------- Panel info (sheet melayang) ---------- */
function InfoPanel({ id, data, accent, onClose, onPlayVideo, expanded, onToggleExpand }) {
  const open = !!id;
  const s = id ? data.sections[id] : null;
  const canExpand = id === "portofolio";
  const wide = canExpand && expanded;
  // Mundur selangkah: kalau sedang full screen → kembali ke mode biasa dulu;
  // kalau sudah biasa → baru tutup panel.
  const dismiss = () => { if (wide) onToggleExpand(); else onClose(); };
  useEffect(() => {
    const onKey = (e) => { if (e.key === "Escape") dismiss(); };
    window.addEventListener("keydown", onKey);
    return () => window.removeEventListener("keydown", onKey);
  }, [wide, onToggleExpand, onClose]);

  return (
    <div className={"panel-root" + (open ? " is-open" : "") + (wide ? " is-wide" : "")} aria-hidden={!open}>
      <div className="panel-scrim" onClick={dismiss} />
      <aside className="panel" role="dialog" aria-modal="true">
        {s && (
          <React.Fragment>
            <div className="panel-watermark" style={{ color: accent }}><SecIcon id={id} /></div>
            {canExpand && (
              <button className="panel-expand" onClick={onToggleExpand}
                aria-label={wide ? "Perkecil panel" : "Perlebar panel"}
                title={wide ? "Perkecil" : "Perlebar"} style={{ "--accent": accent }}>
                {wide ? (
                  <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.8"
                    strokeLinecap="round" strokeLinejoin="round" aria-hidden="true">
                    <path d="M9 4v5H4M15 4v5h5M9 20v-5H4M15 20v-5h5" />
                  </svg>
                ) : (
                  <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.8"
                    strokeLinecap="round" strokeLinejoin="round" aria-hidden="true">
                    <path d="M4 9V4h5M20 9V4h-5M4 15v5h5M20 15v5h-5" />
                  </svg>
                )}
              </button>
            )}
            <button className="panel-close" onClick={onClose} aria-label="Tutup">
              <span /><span />
            </button>
            <div className="panel-inner">
              <div className="panel-kicker" style={{ color: accent }}>
                <span className="kick-line" style={{ background: accent }} />
                {s.kicker}
              </div>
              <h2 className="panel-title">{s.label}</h2>
              <SectionContent id={id} data={data} accent={accent} onPlayVideo={onPlayVideo} expanded={wide} />
            </div>
          </React.Fragment>
        )}
      </aside>
    </div>
  );
}

/* ---------- Unduh CV + preview saat hover ---------- */
function CvDownload({ accent, url, defaultPreview }) {
  const PKEY = "ian_cv_preview";
  const [preview, setPreview] = useState(null);
  const inputRef = useRef(null);
  const shownPrev = preview || defaultPreview || null;
  useEffect(() => {
    const load = () => { try { setPreview(localStorage.getItem(PKEY) || null); } catch (e) {} };
    load();
    const onPick = () => inputRef.current && inputRef.current.click();
    const onClear = () => { try { localStorage.removeItem(PKEY); } catch (e) {} setPreview(null); };
    window.addEventListener("cv-pick-preview", onPick);
    window.addEventListener("cv-clear-preview", onClear);
    return () => { window.removeEventListener("cv-pick-preview", onPick);
      window.removeEventListener("cv-clear-preview", onClear); };
  }, []);
  const handleFile = (file) => {
    if (!file || !file.type.startsWith("image/")) return;
    const r = new FileReader();
    r.onload = (e) => { const d = e.target.result; setPreview(d);
      try { localStorage.setItem(PKEY, d); } catch (err) {} };
    r.readAsDataURL(file);
  };
  return (
    <div className="cv-dl">
      <span className="cv-dl-label">Unduh CV Saya</span>
      <a
        className="cv-dl-link"
        href={url || undefined}
        target={url ? "_blank" : undefined}
        rel="noopener noreferrer"
        download
        aria-label="Unduh CV"
        onClick={(e) => { if (!url) e.preventDefault(); }}
        style={{ "--accent": accent }}
      >
        <svg className="cv-doc" viewBox="0 0 24 24" fill="none" stroke={accent}
          strokeWidth="1.7" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true">
          <path d="M14 3H7a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h10a2 2 0 0 0 2-2V8l-5-5Z" />
          <path d="M14 3v5h5" />
          <path d="M12 12v5" /><path d="m9.5 14.5 2.5 2.5 2.5-2.5" />
        </svg>
        <span className="cv-prev">
          {shownPrev ? (
            <img className="cv-prev-img" src={shownPrev} alt="Preview CV" />
          ) : (
            <span className="cv-prev-empty">Preview CV belum diatur<br /><small>atur di panel Tweaks</small></span>
          )}
          <span className="cv-prev-tip" />
        </span>
      </a>
      <input ref={inputRef} type="file" accept="image/*" style={{ display: "none" }}
        onChange={(e) => handleFile(e.target.files[0])} />
    </div>
  );
}

/* ---------- Lightbox player (YouTube Shorts 9:16) ---------- */
function VideoLightbox({ state, videos, land, accent, onClose, onNav }) {
  useEffect(() => {
    const onKey = (e) => {
      if (e.key === "Escape") onClose();
      else if (e.key === "ArrowRight") onNav(1);
      else if (e.key === "ArrowLeft") onNav(-1);
    };
    window.addEventListener("keydown", onKey);
    return () => window.removeEventListener("keydown", onKey);
  }, [onClose, onNav]);

  const open = state != null;
  const v = open ? videos[state] : null;
  return (
    <div className={"lb-root" + (open ? " is-open" : "")} aria-hidden={!open}>
      <div className="lb-scrim" onClick={onClose} />
      {v && (
        <React.Fragment>
          <button className="lb-nav lb-prev" onClick={() => onNav(-1)} aria-label="Sebelumnya">‹</button>
          <div className="lb-stage" role="dialog" aria-modal="true">
            <div className={"lb-frame" + (land ? " land" : "")}>
              <iframe
                key={state}
                src={`https://www.youtube.com/embed/${v.id}?autoplay=1&playsinline=1&rel=0&modestbranding=1`}
                title={v.title}
                frameBorder="0"
                allow="autoplay; encrypted-media; picture-in-picture"
                allowFullScreen
              />
            </div>
            <div className="lb-cap" style={{ borderColor: accent }}>
              <span className="lb-dot" style={{ background: accent }} />
              {v.title}
              <span className="lb-count">{state + 1} / {videos.length}</span>
            </div>
          </div>
          <button className="lb-nav lb-next" onClick={() => onNav(1)} aria-label="Berikutnya">›</button>
          <button className="lb-close" onClick={onClose} aria-label="Tutup"><span /><span /></button>
        </React.Fragment>
      )}
    </div>
  );
}

/* ---------- Video background (drag & drop, tersimpan via IndexedDB) ---------- */
const VID_DB = "ian_cv_db", VID_STORE = "media", VID_KEY = "bgVideo";
function vidOpen() {
  return new Promise((res, rej) => {
    const r = indexedDB.open(VID_DB, 1);
    r.onupgradeneeded = () => r.result.createObjectStore(VID_STORE);
    r.onsuccess = () => res(r.result);
    r.onerror = () => rej(r.error);
  });
}
async function vidSave(blob) {
  const db = await vidOpen();
  return new Promise((res, rej) => {
    const tx = db.transaction(VID_STORE, "readwrite");
    tx.objectStore(VID_STORE).put(blob, VID_KEY);
    tx.oncomplete = () => res();
    tx.onerror = () => rej(tx.error);
  });
}
async function vidLoad() {
  const db = await vidOpen();
  return new Promise((res) => {
    const tx = db.transaction(VID_STORE, "readonly");
    const g = tx.objectStore(VID_STORE).get(VID_KEY);
    g.onsuccess = () => res(g.result || null);
    g.onerror = () => res(null);
  });
}

function VideoBg({ enabled, darkness, autoData }) {
  const [url, setUrl] = useState(null);
  const [drag, setDrag] = useState(false);
  const [suppressed, setSuppressed] = useState(false); // dimatikan otomatis krn koneksi
  const inputRef = useRef(null);
  const vidRef = useRef(null);
  const stallTimer = useRef(null);

  useEffect(() => {
    let live = true, objUrl;
    vidLoad().then((blob) => {
      if (live && blob) { objUrl = URL.createObjectURL(blob); setUrl(objUrl); }
    });
    return () => { live = false; if (objUrl) URL.revokeObjectURL(objUrl); };
  }, []);

  const handleFile = useCallback(async (file) => {
    if (!file || !file.type.startsWith("video/")) return;
    await vidSave(file);
    setUrl((old) => { if (old) URL.revokeObjectURL(old); return URL.createObjectURL(file); });
  }, []);

  useEffect(() => {
    const onPick = () => inputRef.current && inputRef.current.click();
    const onClear = async () => {
      try { const db = await vidOpen(); const tx = db.transaction(VID_STORE, "readwrite");
        tx.objectStore(VID_STORE).delete(VID_KEY); } catch (e) {}
      setUrl((old) => { if (old) URL.revokeObjectURL(old); return null; });
    };
    window.addEventListener("cv-pick-video", onPick);
    window.addEventListener("cv-clear-video", onClear);
    return () => { window.removeEventListener("cv-pick-video", onPick);
      window.removeEventListener("cv-clear-video", onClear); };
  }, []);

  // Auto-deteksi koneksi: matikan video saat hemat-data / koneksi lambat.
  useEffect(() => {
    if (!autoData) { setSuppressed(false); return; }
    const conn = navigator.connection || navigator.mozConnection || navigator.webkitConnection;
    const evalConn = () => {
      if (!conn) { setSuppressed(false); return; }
      const slow = conn.saveData === true ||
        ["slow-2g", "2g", "3g"].includes(conn.effectiveType);
      setSuppressed(slow);
    };
    evalConn();
    if (conn && conn.addEventListener) {
      conn.addEventListener("change", evalConn);
      return () => conn.removeEventListener("change", evalConn);
    }
  }, [autoData]);

  // Hemat baterai/CPU: jeda video saat tab tidak aktif.
  useEffect(() => {
    const v = vidRef.current;
    if (!v) return;
    const onVis = () => { if (document.hidden) v.pause(); else v.play().catch(() => {}); };
    document.addEventListener("visibilitychange", onVis);
    return () => document.removeEventListener("visibilitychange", onVis);
  }, [url, suppressed]);

  // Fallback saat buffering lama (>6s) → anggap koneksi tersendat.
  const onWaiting = () => {
    if (!autoData) return;
    clearTimeout(stallTimer.current);
    stallTimer.current = setTimeout(() => setSuppressed(true), 6000);
  };
  const onPlaying = () => { clearTimeout(stallTimer.current); };

  if (!enabled) return null;
  const dk = darkness == null ? 0.62 : darkness;
  const showVideo = url && !suppressed;
  return (
    <div className={"videobg" + (drag ? " is-drag" : "") + (url ? " has-vid" : " empty")}
      onDragOver={(e) => { e.preventDefault(); setDrag(true); }}
      onDragLeave={() => setDrag(false)}
      onDrop={(e) => { e.preventDefault(); setDrag(false); handleFile(e.dataTransfer.files[0]); }}
    >
      {showVideo ? (
        <video ref={vidRef} className="videobg-el" src={url} autoPlay muted loop playsInline
          preload="auto" onWaiting={onWaiting} onPlaying={onPlaying} onStalled={onWaiting} />
      ) : !url ? (
        <button className="videobg-drop" onClick={() => inputRef.current && inputRef.current.click()}>
          <span className="vbg-card">
            <span className="vbg-ico" aria-hidden="true">
              <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.6"
                strokeLinecap="round" strokeLinejoin="round">
                <rect x="2.5" y="5" width="14" height="14" rx="2.5" />
                <path d="m16.5 9.5 5-2.5v10l-5-2.5" />
              </svg>
            </span>
            <span className="vbg-t">Tarik video ke sini</span>
            <span className="vbg-s">video looping untuk latar · atau klik untuk pilih file</span>
          </span>
        </button>
      ) : null}
      <input ref={inputRef} type="file" accept="video/*" style={{ display: "none" }}
        onChange={(e) => handleFile(e.target.files[0])} />
      {url && <div className="videobg-scrim" style={{ opacity: suppressed ? 1 : dk }} />}
      {url && suppressed && (
        <div className="videobg-note">
          <span className="vbn-dot" />
          Video dijeda otomatis — koneksi terbatas / hemat data
        </div>
      )}
    </div>
  );
}

/* ---------- Tanda tangan (SVG stroke-draw, mengikuti goresan) ---------- */
function Signature({ name, revealed }) {
  const ref = useRef(null);
  const [len, setLen] = useState(1000);
  useEffect(() => {
    const measure = () => {
      if (!ref.current) return;
      try {
        const l = ref.current.getComputedTextLength() * 2.7;
        if (l && isFinite(l)) setLen(l);
      } catch (e) {}
    };
    measure();
    if (document.fonts && document.fonts.ready) document.fonts.ready.then(measure);
  }, [name]);
  return (
    <svg className="sig" viewBox="0 0 260 112" preserveAspectRatio="xMaxYMax meet" aria-hidden="true">
      <text
        ref={ref}
        x="254"
        y="74"
        textAnchor="end"
        className="sig-text"
        style={{ strokeDasharray: len, strokeDashoffset: revealed ? 0 : len }}
      >
        {name}
      </text>
    </svg>
  );
}

/* ---------- Jabatan berganti (konveyor ke atas, tanpa jeda) ---------- */
function RotatingRole({ words, motion }) {
  const [i, setI] = useState(0);
  const [shift, setShift] = useState(false);
  useEffect(() => {
    if (!motion || !words || words.length < 2) return;
    const id = setInterval(() => setShift(true), 2000);
    return () => clearInterval(id);
  }, [motion, words]);
  if (!words || !words.length) return null;
  const next = (i + 1) % words.length;
  const onEnd = () => { setI(next); setShift(false); };
  return (
    <span className="role-rotator">
      <span className={"role-track" + (shift ? " go" : "")} onTransitionEnd={onEnd}>
        <span className="role-word">{words[i]}</span>
        <span className="role-word">{words[next]}</span>
      </span>
    </span>
  );
}

/* ---------- Ikon SVG per bagian (ikut currentColor) ---------- */
const ICON_PATHS = {
  // Tentang Saya — sosok orang
  tentang: <React.Fragment><circle cx="12" cy="8" r="3.4" /><path d="M5.5 20c0-3.6 2.9-6 6.5-6s6.5 2.4 6.5 6" /></React.Fragment>,
  // Pengalaman — koper kerja
  pengalaman: <React.Fragment><rect x="3.5" y="7.5" width="17" height="12" rx="2" /><path d="M9 7.5V6a2 2 0 0 1 2-2h2a2 2 0 0 1 2 2v1.5" /><path d="M3.5 12.5h17" /></React.Fragment>,
  // Pendidikan — topi wisuda
  pendidikan: <React.Fragment><path d="M12 4 2.5 8.5 12 13l9.5-4.5L12 4Z" /><path d="M6.5 10.8v4.2c0 1.4 2.5 2.8 5.5 2.8s5.5-1.4 5.5-2.8v-4.2" /><path d="M21.5 8.5v5" /></React.Fragment>,
  // Keahlian — sparkle (kreativitas / craft)
  keahlian: <React.Fragment><path d="M12 3c.4 3.7 1.8 5.1 5.5 5.5-3.7.4-5.1 1.8-5.5 5.5-.4-3.7-1.8-5.1-5.5-5.5C10.2 8.1 11.6 6.7 12 3Z" /><path d="M18.5 14c.2 1.8.9 2.5 2.7 2.7-1.8.2-2.5.9-2.7 2.7-.2-1.8-.9-2.5-2.7-2.7 1.8-.2 2.5-.9 2.7-2.7Z" /></React.Fragment>,
  // Kontak — balon chat
  kontak: <React.Fragment><path d="M4 5.5h16a1.5 1.5 0 0 1 1.5 1.5v8a1.5 1.5 0 0 1-1.5 1.5H9l-4.5 3.5V16.5A1.5 1.5 0 0 1 3 15V7a1.5 1.5 0 0 1 1-1.5Z" /></React.Fragment>,
  // Portofolio — layar play
  portofolio: <React.Fragment><rect x="3" y="5" width="18" height="14" rx="2.2" /><path d="m10.5 9.3 4.2 2.7-4.2 2.7V9.3Z" /></React.Fragment>,
};
function SecIcon({ id }) {
  return (
    <svg className="sec-icon" viewBox="0 0 24 24" fill="none"
      stroke="currentColor" strokeWidth="1.6" strokeLinecap="round" strokeLinejoin="round"
      aria-hidden="true">
      {ICON_PATHS[id]}
    </svg>
  );
}

/* ---------- Konsep 3: Menu melayang ---------- */
function calcAge(iso) {
  const b = new Date(iso);
  const n = new Date();
  let a = n.getFullYear() - b.getFullYear();
  const m = n.getMonth() - b.getMonth();
  if (m < 0 || (m === 0 && n.getDate() < b.getDate())) a--;
  return a;
}

function FloatingMenu({ data, accent, active, onPick }) {
  return (
    <nav className="fmenu">
      {data.nodes.map((id) => {
        const s = data.sections[id];
        return (
          <button
            key={id}
            className={"fmenu-item" + (active === id ? " is-active" : "")}
            onClick={() => onPick(id)}
            style={{ "--accent": accent }}
          >
            <span className="fm-no" style={{ color: accent }}><SecIcon id={id} /></span>
            <span className="fm-label">{s.label}</span>
            <span className="fm-arrow" style={{ color: accent }}>→</span>
          </button>
        );
      })}

      <div className="fmenu-foot">
        <div className="ff-id">
          <span className="ff-name">{data.name}</span>
          <span className="ff-div" style={{ background: accent }} />
          <span className="ff-exp" style={{ color: accent }}>{data.experience}</span>
        </div>
        <div className="ff-tools">
          {data.tools.map((tl) => (
            <span key={tl.mono} className="tool-chip" title={tl.name} style={{ "--accent": accent }}>
              {tl.img ? (
                <img
                  className="tool-logo"
                  src={tl.img}
                  alt={tl.name}
                  onError={(e) => {
                    // Jika PNG belum ada, tampilkan singkatan huruf sebagai cadangan.
                    e.currentTarget.style.display = "none";
                    e.currentTarget.parentNode.classList.add("is-fallback");
                  }}
                />
              ) : null}
              <span className="tool-mono">{tl.mono}</span>
            </span>
          ))}
        </div>
      </div>
    </nav>
  );
}

Object.assign(window, {
  PhotoSlot, SectionContent, InfoPanel, BigType, SecIcon, RotatingRole, VideoBg, VideoLightbox, CvDownload,
  FloatingMenu,
});
