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

// reveal-on-scroll helper
function useReveal(threshold = 0.2) {
  const ref = useRef(null);
  const [seen, setSeen] = useState(false);
  useEffect(() => {
    const el = ref.current;if (!el) return;
    const io = new IntersectionObserver((es) => {
      es.forEach((e) => {if (e.isIntersecting) setSeen(true);});
    }, { threshold });
    io.observe(el);
    return () => io.disconnect();
  }, []);
  return [ref, seen];
}

// count-up number
function CountUp({ to, suffix = "", dur = 1400 }) {
  const [v, setV] = useState(0);
  const [ref, seen] = useReveal(0.5);
  useEffect(() => {
    if (!seen) return;
    let raf, t0;
    const tick = (t) => {if (!t0) t0 = t;const p = Math.min((t - t0) / dur, 1);setV(Math.round(to * (1 - Math.pow(1 - p, 3))));if (p < 1) raf = requestAnimationFrame(tick);};
    raf = requestAnimationFrame(tick);
    return () => cancelAnimationFrame(raf);
  }, [seen]);
  return <span ref={ref}>{v.toLocaleString()}{suffix}</span>;
}

// ---------- 03 · WHAT I DO ----------
const CAPS = [
{ ic: "◎", t: "Brand Concept", d: "把 Brief 拆成清晰的创意方向", tags: ["INTERNATIONAL BRAND", "PITCH & STRATEGY"] },
{ ic: "❑", t: "Social Content", d: "把选题、视觉和平台语感做成内容系统", tags: ["SILICON ANGEL", "小红书"] },
{ ic: "⊞", t: "AIGC Workflow", d: "把 AI 接入策划、视觉和执行流程", tags: ["AI NATIVE WORKFLOW", "AIGC PLAYBOOK"] },
{ ic: "◷", t: "Campaign Planning", d: "把概念延展成可执行的传播方案", tags: ["INTERNATIONAL BRAND", "AIGC PLAYBOOK"] },
{ ic: "▤", t: "Digital Storytelling", d: "把信息包装成可被记住的故事", tags: ["THE TAROT DEMON", "SILICON ANGEL"] }];

function WhatIDo() {
  const [ref, seen] = useReveal(0.25);
  const secRef = useRef(null);
  const [gli, setGli] = useState(false);
  const [active, setActive] = useState(-1); // click-activated card (no scroll auto-switch)
  // brief glitch burst when it enters
  useEffect(() => {if (seen) {setGli(true);const t = setTimeout(() => setGli(false), 700);return () => clearTimeout(t);}}, [seen]);
  return (
    <section className="sec do-screen" id="skills" ref={secRef} data-screen-label="04 · SKILLS">
      <div className="do-matrix"><div className="grid3d" style={{ transform: "rotateX(60deg) translateY(-40px)" }} /></div>
      <div className="dot-grid" />
      <div className="sec-head" data-cine>
        <div className="eyebrow">03 · SKILL MAP</div>
        <div className={"h glitch-head" + (gli ? " glitching" : "")} data-t="SKILL MAP_">SKILL<span className="v">MAP_</span></div>
        <div className="zh">职业能力图谱 · 一套围绕创意、产品、视觉与执行展开的能力系统</div>
      </div>
      <div className={"skillmap-mount" + (seen ? " in" : "")} data-cine ref={ref}>
        {window.SkillMap ? <window.SkillMap /> : null}
      </div>
    </section>);

}

// ---------- 02 · ABOUT ME ----------
function AboutMe() {
  const [hRef, hSeen] = useReveal(0.3);
  const [bRef, bSeen] = useReveal(0.2);
  const [cRef, cSeen] = useReveal(0.2);
  const stageRef = useRef(null);

  // mouse parallax — smoothed via lerp; layers read --px / --py
  useEffect(() => {
    const el = stageRef.current;if (!el) return;
    if (window.matchMedia && window.matchMedia("(prefers-reduced-motion: reduce)").matches) return;
    let raf = 0,tx = 0,ty = 0,cx = 0,cy = 0;
    function loop() {
      cx += (tx - cx) * 0.08;cy += (ty - cy) * 0.08;
      el.style.setProperty("--px", cx.toFixed(4));
      el.style.setProperty("--py", cy.toFixed(4));
      if (Math.abs(tx - cx) > 0.0008 || Math.abs(ty - cy) > 0.0008) {raf = requestAnimationFrame(loop);} else {raf = 0;}
    }
    function onMove(e) {
      const r = el.getBoundingClientRect();
      tx = (e.clientX - r.left) / r.width - 0.5;
      ty = (e.clientY - r.top) / r.height - 0.5;
      if (!raf) raf = requestAnimationFrame(loop);
    }
    window.addEventListener("mousemove", onMove, { passive: true });
    return () => {window.removeEventListener("mousemove", onMove);if (raf) cancelAnimationFrame(raf);};
  }, []);

  const strengths = ["品牌策略", "内容运营", "AIGC 视觉", "产品企划", "社媒增长", "故事叙述"];
  const tools = ["Midjourney", "Stable Diffusion", "ComfyUI", "GPTs", "Coze", "Claude", "Notion", "Figma"];

  return (
    <section className="sec about-screen" id="about" ref={stageRef} data-screen-label="03 · CHARACTER PROFILE">
      {/* ── left · one big frosted glass panel ── */}
      <div className={"about-left sec-reveal" + (hSeen ? " in" : "")} data-cine ref={hRef}>
        <div className="eyebrow">01 · CHARACTER PROFILE</div>
        <h2 className="ap-title">角色档案</h2>
        <p className="lead">我叫谢安妮，一个用 AI、故事和网感，把抽象需求转化为可传播创意的人。</p>
        <p className="lead-en">I'm Annie Xie, a creative planner using AI and storytelling to turn ideas into shareable content.</p>

        <div className="ap-cards">
          <div className="ap-card">
            <span className="ap-ico"><svg viewBox="0 0 24 24"><circle cx="12" cy="8" r="4" /><path d="M4 21c0-4 3.5-6 8-6s8 2 8 6" /></svg></span>
            <div className="ap-cbody">
              <span className="ap-k">身份 · Identity</span>
              <span className="ap-v">创意策划 / AIGC 内容创作者 / 衍生品主理人</span>
            </div>
          </div>
          <div className="ap-card">
            <span className="ap-ico"><svg viewBox="0 0 24 24" className="fill"><path d="M12 2l2.4 6.2L21 9l-5 4.1L17.6 20 12 16.4 6.4 20 8 13.1 3 9l6.6-.8z" /></svg></span>
            <div className="ap-cbody">
              <span className="ap-k">擅长 · Strengths</span>
              <div className="ap-chips">{strengths.map((s) => <span className="ap-chip" key={s}>{s}</span>)}</div>
            </div>
          </div>
          <div className="ap-card">
            <span className="ap-ico"><svg viewBox="0 0 24 24"><path d="M14.7 6.3a4 4 0 00-5.4 5.4l-6 6 2 2 6-6a4 4 0 005.4-5.4l-2.5 2.5-2-2 2.5-2.5z" /></svg></span>
            <div className="ap-cbody">
              <span className="ap-k">工具 · Toolkit</span>
              <div className="ap-chips">{tools.map((s) => <span className="ap-chip mono" key={s}>{s}</span>)}</div>
            </div>
          </div>
        </div>

        <div className="ap-stats">
          <div className="ap-stat"><div className="n"><CountUp to={4} suffix="+" /></div><div className="l">年广告<small>策划经验</small></div></div>
          <div className="ap-stat"><div className="n"><CountUp to={7800} suffix="+" /></div><div className="l">小红书<small>粉丝关注</small></div></div>
          <div className="ap-stat"><div className="n"><CountUp to={1000} suffix="+" /></div><div className="l">GPTs<small>对话次数</small></div></div>
          <div className="ap-stat"><div className="n">∞</div><div className="l">创意<small>好奇心</small></div></div>
        </div>

        <div className="ap-foot">
          <a className="ap-cv" href="pdfs/Annie-CV-2026.pdf" download="Annie-CV-2026.pdf" target="_blank" rel="noopener" onClick={() => {try {window.playSelect && window.playSelect();} catch (e) {}}}>↓ DOWNLOAD CV.PDF</a>
          <a className="ap-cv ghost" href="pdfs/Annie-Portfolio-2026.pdf" download="Annie-Portfolio-2026.pdf" target="_blank" rel="noopener" onClick={() => {try {window.playSelect && window.playSelect();} catch (e) {}}}>↓ PORTFOLIO.PDF</a>
        </div>
        <div className="ap-pills">
          <span className="ap-pill"><i></i>OPEN TO WORK</span>
          <span className="ap-pill violet"><i></i>AIGC · ONLINE</span>
          <span className="ap-pill mute"><i></i>BASED IN CHINA</span>
        </div>
      </div>

      {/* ── right · layered creative scene (geometry · person · floating cards) ── */}
      <div className={"cosmos sec-reveal" + (cSeen ? " in" : "")} ref={cRef}>
        {/* layer 1 · background geometry */}
        <div className="cos-geo">
          <div className="shape cg-sphere"></div>
          <div className="shape cg-pink"></div>
          <div className="shape cg-star"></div>
          <div className="shape cg-tri"></div>
          <div className="shape cg-tri2"></div>
          <div className="cg-orbit">
            <svg viewBox="0 0 900 800" preserveAspectRatio="none" fill="none">
              <ellipse cx="560" cy="340" rx="320" ry="250" transform="rotate(-14 560 340)" stroke="rgba(255,255,255,.5)" strokeWidth="1.4"></ellipse>
              <ellipse cx="540" cy="430" rx="380" ry="170" transform="rotate(12 540 430)" stroke="rgba(255,255,255,.26)" strokeWidth="1.2"></ellipse>
            </svg>
          </div>
        </div>
        <div className="cos-twinkle" style={{ width: "12px", height: "12px", left: "52%", top: "18%", animationDelay: "0s" }}></div>
        <div className="cos-twinkle" style={{ width: "8px", height: "8px", left: "90%", top: "44%", animationDelay: "1.1s" }}></div>
        <div className="cos-twinkle" style={{ width: "9px", height: "9px", left: "62%", top: "64%", animationDelay: "2s" }}></div>

        {/* layer 2 · person (stable) */}
        <div className="cos-person"></div>

        {/* layer 3 · independent floating cards */}
        <div className="cos-card cc-gem" style={{ "--ci": ".25s" }}>
          <div className="fl"><div className="cci"><span className="star"></span><b>Gemini</b></div></div>
        </div>
        <div className="cos-card cc-gpt" style={{ "--ci": ".35s" }}>
          <div className="fl"><div className="hit"><img src="assets/layer-chatgpt.png" alt="ChatGPT" /></div></div>
        </div>
        <div className="cos-card cc-login" style={{ "--ci": ".45s" }}>
          <div className="fl"><div className="hit"><img src="assets/layer-login.png" alt="Login" /></div></div>
        </div>
        <div className="cos-card cc-price" style={{ "--ci": ".55s" }}>
          <div className="fl"><div className="hit"><div className="cci cci-price">
            <div className="cc-pl">Plus</div>
            <div className="cc-prow"><span className="cc-amt">$15</span><span className="cc-per">per editor / month<br />billed annually</span></div>
            <button className="cc-get">Get started</button>
          </div></div></div>
        </div>
      </div>

      {/* left readability scrim sits above the stage, below the panel */}
      <div className="about-scrim"></div>
    </section>);

}

// ---------- 05 · BADGE CORONATION (scroll-scrubbed video) + CONTACT ----------
function BadgeCoronation() {
  const ref = useRef(null);
  const vidRef = useRef(null);
  const stickyRef = useRef(null);
  const quoteRef = useRef(null);
  const contactRef = useRef(null);
  const dimRef = useRef(null);
  const fillRef = useRef(null);
  const endAtRef = useRef(0); // timestamp when the video reaches its frozen final frame

  useEffect(() => {
    let raf = 0,dur = 0,ready = false,curr = 0;
    const v = vidRef.current;
    const onMeta = () => {dur = v.duration || 0;ready = true;try {v.pause();} catch (e) {}};
    if (v) {
      v.addEventListener("loadedmetadata", onMeta);
      if (v.readyState >= 1) onMeta();
      const kick = v.play();if (kick && kick.then) kick.then(() => {try {v.pause();v.currentTime = 0;} catch (e) {}}).catch(() => {});
    }
    const easeOut = (k) => 1 - Math.pow(1 - Math.min(Math.max(k, 0), 1), 3);

    const HOLD = 0.3; // seconds to wait on the frozen final frame before the glass panel appears

    function loop() {
      const el = ref.current;
      if (el) {
        const vh = window.innerHeight || 1;
        const total = el.offsetHeight - vh;
        const top = el.getBoundingClientRect().top;
        const p = Math.min(Math.max(-top / Math.max(total, 1), 0), 1); // 0..1 across section

        // scroll → video time:  t = start + p*(end-start), start=0
        const D = dur || 12;
        if (ready && dur) {
          const tgt = p * (dur - 0.04);
          curr += (tgt - curr) * 0.14; // lerp scrub
          if (Math.abs(tgt - curr) > 0.004) {try {v.currentTime = curr;} catch (e) {}}
        }
        const t = ready ? curr : p * D; // effective video time

        // detect the frozen final frame (scrolled to bottom): start a real-time 2s hold
        const atEnd = p > 0.985;
        if (atEnd) {if (!endAtRef.current) endAtRef.current = performance.now();} else
        {endAtRef.current = 0;}
        const held = endAtRef.current ? (performance.now() - endAtRef.current) / 1000 : 0;
        const post = easeOut((held - HOLD) / 0.9); // 0 until 2s elapsed, then ease 0→1

        // QUOTE: appears at 9s and now HOLDS through the freeze — only fades once the panel starts
        const qIn = easeOut((t - 9) / 0.7);
        const qe = qIn * (1 - post);
        if (quoteRef.current) {
          quoteRef.current.style.opacity = qe.toFixed(3);
          quoteRef.current.style.transform = `translateY(${(1 - qe) * 18}px)`;
        }

        // CONTACT panel (centered) + background dim — delayed 2s after the video freezes
        const ce = post;
        if (dimRef.current) dimRef.current.style.opacity = (ce * 0.78).toFixed(3);
        if (contactRef.current) {
          contactRef.current.style.opacity = ce.toFixed(3);
          contactRef.current.style.transform = `translate(-50%,-50%) translateY(${(1 - ce) * 28}px)`;
          contactRef.current.style.filter = `blur(${(1 - ce) * 8}px)`;
          contactRef.current.classList.toggle("on", ce > 0.5);
        }
        if (fillRef.current) fillRef.current.style.width = (p * 100).toFixed(1) + "%";
      }
      raf = requestAnimationFrame(loop);
    }
    raf = requestAnimationFrame(loop);
    return () => {if (raf) cancelAnimationFrame(raf);if (v) v.removeEventListener("loadedmetadata", onMeta);};
  }, []);

  const talk = () => {try {window.playSelect && window.playSelect();} catch (e) {}if (window.__openChat) window.__openChat();};

  return (
    <section className="coro" ref={ref} id="coronation">
      <div className="coro-sticky" ref={stickyRef}>
        <video className="coro-vid" ref={vidRef} muted playsInline preload="auto">
          <source src="assets/badge.mp4" type="video/mp4" />
        </video>
        <div className="coro-grade" />
        <div className="coro-scan" />
        <div className="coro-dim" ref={dimRef} />

        <div className="coro-eyebrow">05 · BADGE CORONATION · 加冕</div>

        <div className="coro-quote" ref={quoteRef}>
          <div className="cn">工牌只是工牌，直到想象将它加冕为冠。</div>
          <div className="en">A badge is just a badge, until imagination turns it into a crown.</div>
        </div>

        {/* bottom glass contact panel — replaces the old retro dialog */}
        <div className="coro-contact" ref={contactRef}>
          <div className="ey">06 · CONTACT</div>
          <div className="h">一起做点 <span className="w">weird,</span><br />有用又难忘的东西。</div>
          <div className="desc">如果你在找一个能把 AI、内容、审美和策略放进同一套系统里的人，从这里联系我。</div>
          <div className="rows">
            <a href="mailto:xan97@qq.com"><span className="k">EMAIL</span><span className="vv">xan97@qq.com</span><span className="ar">↗</span></a>
            <a href="#"><span className="k">小红书</span><span className="vv">@硅的天使</span><span className="ar">↗</span></a>
            <a href="pdfs/Annie-CV-2026.pdf" download="Annie-CV-2026.pdf" target="_blank" rel="noopener"><span className="k">简历 CV</span><span className="vv">Download CV ↓</span><span className="ar">↓</span></a>
            <a href="pdfs/Annie-Portfolio-2026.pdf" download="Annie-Portfolio-2026.pdf" target="_blank" rel="noopener"><span className="k">作品集</span><span className="vv">Download Portfolio ↓</span><span className="ar">↓</span></a>
          </div>
          <button className="cbtn" onClick={talk}>► TALK TO ANNIE <span className="c">↗</span></button>
        </div>

        <div className="coro-prog">
          <i><span className="fill" ref={fillRef} /></i>
        </div>
      </div>
    </section>);

}

// ---------- 06 · CONTACT ----------
function Contact() {
  const [ref, seen] = useReveal(0.25);
  const [dlg, setDlg] = useState(false);
  const links = [
  { k: "EMAIL", v: "xan97@qq.com", href: "mailto:xan97@qq.com" },
  { k: "小红书", v: "@硅的天使", href: "#" },
  { k: "简历 CV", v: "Download CV ↓", href: "pdfs/Annie-CV-2026.pdf", file: "Annie-CV-2026.pdf" },
  { k: "作品集", v: "Download Portfolio ↓", href: "pdfs/Annie-Portfolio-2026.pdf", file: "Annie-Portfolio-2026.pdf" }];

  const openDlg = () => {try {window.playSelect && window.playSelect();} catch (e) {}setDlg(true);};
  return (
    <section className="sec contact-screen" id="contact">
      <div className={"contact-card sec-reveal" + (seen ? " in" : "")} ref={ref}>
        <div className="winbar">
          <span>CONTACT_REQUEST.exe</span>
          <span className="st"><span style={{ width: 8, height: 8, background: "var(--violet-2)", borderRadius: "50%", boxShadow: "0 0 6px var(--violet-2)" }} /> OPEN TO WORK</span>
        </div>
        <div className="contact-body">
          <div className="eyebrow" style={{ fontFamily: "IBM Plex Mono", fontSize: 10, letterSpacing: ".3em", color: "var(--violet-2)" }}>06 · CONTACT</div>
          <div className="h" style={{ marginTop: 8 }}>Let's build something <span style={{ color: "var(--violet)" }}>weird,</span> useful &amp; memorable.</div>
          <p className="tagline">如果你正在寻找一个能把 AI、内容、审美和策略放进同一套系统里的人，可以从这里联系我。</p>
          <p className="tagline-en">If you're looking for someone who can connect AI, content, aesthetics and strategy into one creative system, let's talk.</p>
          <div className="contact-links">
            {links.map((l) =>
            <a key={l.k} href={l.href} {...(l.file ? { download: l.file, target: "_blank", rel: "noopener" } : {})}><span className="k">{l.k}</span><span className="vv">{l.v}</span><span className="ar">{l.file ? "↓" : "↗"}</span></a>
            )}
          </div>
          <div className="contact-foot">
            <button className="send" onClick={openDlg}>► TALK TO ANNIE <span style={{ display: "inline-flex", width: 18, height: 18, alignItems: "center", justifyContent: "center", background: "#fff", color: "var(--violet)", borderRadius: "50%" }}>↗</span></button>
            <span className="copy">RESPONSE MODE · HUMAN / AI-ASSISTED · © 2026 谢安妮 ANNIE XIE</span>
          </div>
        </div>
      </div>

      {/* retro OS dialog */}
      <div className={"retro-mask" + (dlg ? " open" : "")} onClick={(e) => {if (e.target.classList.contains("retro-mask")) setDlg(false);}}>
        <div className="retro-win" role="dialog" aria-label="New business">
          <div className="retro-tb"><span>◇</span> New Business — annie_os.exe <button className="x" onClick={() => setDlg(false)} aria-label="Close">×</button></div>
          <div className="retro-body">
            <div className="ic" />
            <div className="msg"><b>嗨，我是 Annie。</b><br />留下你的项目 / 想法 / 一句话都行，我会尽快回你。</div>
            <label>FROM</label>
            <input type="text" defaultValue="your name / company" />
            <label>MESSAGE</label>
            <textarea rows="3" defaultValue="想聊一个 weird, useful & memorable 的项目…" />
            <div className="retro-actions">
              <button className="retro-btn" onClick={() => setDlg(false)}>Cancel</button>
              <button className="retro-btn primary" onClick={() => {try {window.playSelect && window.playSelect();} catch (e) {}location.href = "mailto:xan97@qq.com";}}>Send ►</button>
            </div>
          </div>
        </div>
      </div>
    </section>);

}

// ---------- 02 · TIMELINE / 履历 — Creative System version log ----------
// reverse-chronological · latest version first · scroll-driven active stage
const VERSIONS = [
{
  v: "v05", num: "05", yr: "2025–NOW", title: "AIGC CREATIVE OPERATOR",
  role: "AIGC 账号 × 衍生品 × 个人创意系统",
  line: "从 AI 视觉内容出发，延展到账号运营、衍生品企划、GPT 原型与个人作品集系统，开始搭建自己的 AIGC 创意闭环。",
  tags: ["AIGC", "Social Media", "Merchandising", "GPTs", "Creative OS", "Prototype Lab"] },
{
  v: "v04", num: "04", yr: "2023–2025", title: "SENIOR PLANNER MODE",
  role: "广告公司 Senior Planner 阶段",
  line: "独立参与品牌 campaign、IMC 方案、社媒内容、AIGC 视觉、视频脚本与复杂 brief 转译，形成从策略到创意表达的完整提案能力。",
  tags: ["Creative Planning", "Campaign Strategy", "Proposal Writing", "AIGC Visual", "Brand Content", "Complex Brief"] },
{
  v: "v03", num: "03", yr: "2021–2023", title: "CONTENT SYSTEM BUILDING",
  role: "内容策划与社媒运营阶段",
  line: "从日常内容运营、公众号、小红书、活动文案与品牌传播支持中，建立对内容节奏、用户反馈和渠道表达的理解。",
  tags: ["Social Content", "Copywriting", "Channel Operation", "Content Planning", "User Feedback"] },
{
  v: "v02", num: "02", yr: "2020–2021", title: "OPERATIONS GROUNDING",
  role: "泛娱乐与赛事运营起点",
  line: "从泛娱乐、赛事与运营执行中进入内容行业，理解活动节奏、用户参与、执行协同与现场反馈。",
  tags: ["Operations", "Esports", "Event Support", "Community", "Execution"] },
{
  v: "v01", num: "01", yr: "2016–2020", title: "FOUNDATION LAYER",
  role: "工商管理背景与基础能力",
  line: "工商管理背景提供了对商业、组织、用户与运营的基础理解，也成为后来进入品牌策划和内容运营的底层视角。",
  tags: ["Business Management", "Marketing Basics", "Organization", "User Understanding"] }];

function Timeline() {
  const [open, setOpen] = useState(0); // default: v05 expanded
  const [ready, setReady] = useState(false);
  const secRef = useRef(null);

  // one-time entrance when the section first reaches the viewport
  useEffect(() => {
    const el = secRef.current;if (!el) return;
    const io = new IntersectionObserver((es) => {es.forEach((e) => {if (e.isIntersecting) setReady(true);});}, { threshold: 0.12 });
    io.observe(el);
    return () => io.disconnect();
  }, []);

  // scroll only browses — expand is controlled purely by click (accordion)
  const toggle = (i) => {
    try {window.playSelect && window.playSelect();} catch (e) {}
    setOpen((prev) => prev === i ? -1 : i);
  };

  return (
    <section className="vt-sec sec" id="timeline" ref={secRef} data-screen-label="02 · TIMELINE">
      <div className={"vt-stage" + (ready ? " ready" : "")}>
        <div className="vt-grid-bg" />
        <header className="vt-head">
          <div className="vt-eyebrow">02 · TIMELINE / 履历</div>
          <h2 className="vt-title"><span className="ho">JOURNEY</span> 履历</h2>
          <p className="vt-sub">CREATIVE SYSTEM — 一份版本演进记录 · 从 v05 倒序回看 · 点击展开</p>
        </header>

        <div className="vt-list">
          {VERSIONS.map((r, i) => {
            const isOpen = i === open;
            return (
              <div className={"vt-row" + (isOpen ? " is-open" : "")} key={r.v}>
                <button className="vt-row-head" onClick={() => toggle(i)} aria-expanded={isOpen}>
                  <span className="vt-head-inner">
                    <span className="vt-ver">{r.v}</span>
                    <span className="vt-yr">{r.yr}</span>
                    <span className="vt-name">{r.title}</span>
                    <span className="vt-plus" aria-hidden="true">{isOpen ? "–" : "+"}</span>
                  </span>
                </button>
                <div className="vt-row-body">
                  <div className="vt-body-inner">
                    <div className="vt-role">{r.role}</div>
                    <p className="vt-line">{r.line}</p>
                    <div className="vt-tags">{r.tags.map((t) => <span key={t}>{t}</span>)}</div>
                  </div>
                </div>
                {isOpen && <div className="vt-bignum" key={"n" + i} aria-hidden="true">{r.num}</div>}
              </div>);

          })}
        </div>
      </div>
    </section>);

}

Object.assign(window, { WhatIDo, AboutMe, Timeline, BadgeCoronation, Contact });