// pages.jsx — Otherworld.dev page components

// ─── Shared bits ────────────────────────────────────────────────────────

function Brand({ size = 26 }) {
  return (
    <span className="nav-brand">
      <span className="mark" style={{ width: size, height: size }} aria-hidden="true"></span>
      <span>otherworld<span className="slash">/</span><span className="role">adam morgan</span></span>
    </span>
  );
}

function Nav({ route, go }) {
  const links = [
    { id: "home", label: "Index", path: "/" },
    { id: "markaloud", label: "Markaloud", path: "/markaloud" },
    { id: "work", label: "Work", path: "/work" },
    { id: "about", label: "About", path: "/about" },
    { id: "contact", label: "Contact", path: "/contact" },
  ];
  const [open, setOpen] = React.useState(false);
  const onLink = (id) => (e) => {
    e.preventDefault();
    setOpen(false);
    go(id);
  };
  return (
    <nav className={"nav" + (open ? " nav-open" : "")}>
      <div className="nav-inner">
        <a href="/" onClick={onLink("home")}>
          <Brand />
        </a>
        <div className="nav-links">
          {links.map(l => (
            <a key={l.id}
               href={l.path}
               className={route === l.id ? "active" : ""}
               onClick={onLink(l.id)}>
              {l.label}
            </a>
          ))}
        </div>
        <div className="nav-cta">
          <a className="btn btn-primary nav-cta-hire" href="/contact" onClick={onLink("contact")}>
            Hire me <span className="arr">→</span>
          </a>
          <button className="nav-toggle" aria-label={open ? "Close menu" : "Open menu"}
                  aria-expanded={open}
                  onClick={() => setOpen((v) => !v)}>
            <span></span><span></span><span></span>
          </button>
        </div>
      </div>
      <div className="nav-mobile-panel" role="menu">
        {links.map(l => (
          <a key={l.id} role="menuitem"
             href={l.path}
             className={route === l.id ? "active" : ""}
             onClick={onLink(l.id)}>
            <span>{l.label}</span>
            <span className="arr">→</span>
          </a>
        ))}
        <a role="menuitem" className="nav-mobile-hire" href="/contact" onClick={onLink("contact")}>
          Hire me <span className="arr">→</span>
        </a>
      </div>
    </nav>
  );
}

function Footer({ go }) {
  return (
    <footer className="footer">
      <div className="footer-inner" style={{ alignItems: "flex-start", paddingTop: 8, paddingBottom: 8 }}>
        <div style={{ display: "flex", flexDirection: "column", gap: 10 }}>
          <img src="assets/otherworld-wordmark.png" alt="otherworld.dev"
            className="logo-wordmark"
            style={{ height: 28, width: "auto", display: "block", opacity: 0.92 }}/>
          <div>© 2026 Otherworld Dev Ltd · Registered in England &amp; Wales · Swansea, UK.</div>
        </div>
        <div className="links">
          <a href="/markaloud" onClick={(e) => { e.preventDefault(); go("markaloud"); }}>Markaloud</a>
          <a href="/work" onClick={(e) => { e.preventDefault(); go("work"); }}>Work</a>
          <a href="/about" onClick={(e) => { e.preventDefault(); go("about"); }}>About</a>
          <a href="/contact" onClick={(e) => { e.preventDefault(); go("contact"); }}>Contact</a>
          <a href="https://github.com/otherworld-dev" target="_blank" rel="noreferrer">GitHub ↗</a>
          <a href="https://uk.linkedin.com/in/a-j-morgan" target="_blank" rel="noreferrer">LinkedIn ↗</a>
        </div>
      </div>
    </footer>
  );
}

// Random-but-stable waveform values
function makeWave(n, seed = 7) {
  const out = [];
  let s = seed;
  for (let i = 0; i < n; i++) {
    s = (s * 9301 + 49297) % 233280;
    const r = s / 233280;
    // shape so middle bars are taller
    const center = 1 - Math.abs(i - n / 2) / (n / 2);
    const h = 12 + r * 48 * (0.6 + 0.4 * center);
    out.push(Math.round(h));
  }
  return out;
}

function Waveform({ progress = 0.42, live = 0.46 }) {
  const bars = React.useMemo(() => makeWave(56, 5), []);
  return (
    <div className="wave" aria-hidden="true">
      {bars.map((h, i) => {
        const t = i / bars.length;
        let cls = "bar";
        if (t < progress) cls += " played";
        if (Math.abs(t - live) < 0.018) cls += " live";
        return <div key={i} className={cls} style={{ height: h }}></div>;
      })}
    </div>
  );
}

// ─── Markaloud product mock (audio + AI-structured feedback) ────────────

function MarkaloudMock() {
  return (
    <div className="mark-mock">
      <div className="mark-mock-head">
        <div className="icon">M</div>
        <div className="meta">
          <div className="t">Final Viva — Eleanor Hughes</div>
          <div className="s">PHIL3402 · Submitted Tue 14:22 · 12:48 long</div>
        </div>
        <div className="badge">Reviewing</div>
      </div>

      <Waveform progress={0.42} live={0.46} />
      <div className="mark-timeline">
        <span>00:00</span>
        <span>05:54</span>
        <span>12:48</span>
      </div>

      <div className="mark-comments">
        <div className="mark-comment">
          <div className="ts">05:54</div>
          <div className="body">
            <span className="tag">Strength</span>
            Clear framing of Kant's transcendental argument — pair this with a counter-example next time to score the top band.
          </div>
        </div>
        <div className="mark-comment">
          <div className="ts">07:21</div>
          <div className="body">
            <span className="tag">Action</span>
            Citation drift around minute 7. Re-listen and tighten attribution; rubric criterion B2.
          </div>
        </div>
        <div className="mark-comment">
          <div className="ts">11:02</div>
          <div className="body">
            <span className="tag">Next</span>
            Strong recovery in the conclusion. Suggest extending this thread into the written commentary.
          </div>
        </div>
      </div>

      <div className="mark-controls">
        <div style={{ display: "flex", alignItems: "center", gap: 12 }}>
          <button className="play" aria-label="Play">▶</button>
          <span>05:54 / 12:48</span>
        </div>
        <span>Rubric · QAA-aligned</span>
      </div>
    </div>
  );
}

// ─── Markaloud feature (three treatments) ───────────────────────────────

function MarkaloudFeature({ treatment, go }) {
  if (treatment === "manifesto") {
    return (
      <section className="feature">
        <div className="wrap feature-manifesto">
          <div className="t-eyebrow fade-in"><span className="dot"></span>Featured product · Markaloud</div>
          <h2 className="manifesto-quote fade-in" data-delay="1" style={{ marginTop: 22 }}>
            Marking a viva should take <span className="accent">minutes</span>, not hours —<br/>
            and feedback should feel like <span className="accent">coaching</span>,
            <span className="muted"> not a verdict.</span>
          </h2>
          <p className="t-body-lg fade-in" data-delay="2" style={{ maxWidth: 720, marginTop: 28 }}>
            Markaloud is an academic audio-marking tool. Educators upload a viva or oral assignment;
            our model timestamps strengths, flags rubric gaps, and drafts constructive comments students
            can actually act on.
          </p>
          <div className="manifesto-meta fade-in" data-delay="3">
            <div className="col"><div className="lbl">Status</div><div className="val">Closed beta · 2026</div></div>
            <div className="col"><div className="lbl">Sector</div><div className="val">UK higher education</div></div>
            <div className="col"><div className="lbl">Approach</div><div className="val">Constructive · rubric-aligned</div></div>
            <div className="col"><div className="lbl">Type</div><div className="val">SaaS · Web app</div></div>
          </div>
          <div className="feature-cta fade-in" data-delay="4" style={{ marginTop: 36 }}>
            <a className="btn btn-primary" href="/markaloud" onClick={(e) => { e.preventDefault(); go("markaloud"); }}>
              Read the case study <span className="arr">→</span>
            </a>
            <a className="btn btn-ghost" href="https://www.markaloud.cloud" target="_blank" rel="noreferrer">
              markaloud.cloud <span className="arr">↗</span>
            </a>
          </div>
        </div>
      </section>
    );
  }

  if (treatment === "split") {
    return (
      <section className="feature">
        <div className="wrap">
          <div className="feature-split">
            <div className="left fade-in">
              <span className="feature-tag">● Featured product</span>
              <h2 className="feature-title">
                Markaloud.<br/>
                <span className="accent">Audio marking,</span>{" "}
                without the dread.
              </h2>
              <p className="feature-sub" style={{ marginTop: 18 }}>
                Markers record once. Markaloud transcribes, timestamps, and structures
                constructive feedback against your rubric — so students get coaching, not just a grade.
              </p>
              <div className="feature-stats">
                <div className="stat"><div className="n">Hours<span style={{ fontSize: 16, color: "var(--fg-3)", marginLeft: 4 }}>saved</span></div><div className="l">per marker, per cohort</div></div>
                <div className="stat"><div className="n">Pilots</div><div className="l">UK higher-ed · 2026</div></div>
                <div className="stat"><div className="n">QAA</div><div className="l">Aligned rubrics</div></div>
              </div>
              <div className="feature-cta">
                <a className="btn btn-primary" href="/markaloud" onClick={(e) => { e.preventDefault(); go("markaloud"); }}>
                  Explore Markaloud <span className="arr">→</span>
                </a>
                <a className="btn btn-ghost" href="https://www.markaloud.cloud" target="_blank" rel="noreferrer">
                  Visit site <span className="arr">↗</span>
                </a>
              </div>
            </div>
            <div className="right fade-in" data-delay="2">
              <div style={{ width: "100%", maxWidth: 480 }}>
                <MarkaloudMock />
              </div>
            </div>
          </div>
        </div>
      </section>
    );
  }

  // spotlight (default)
  return (
    <section className="feature">
      <div className="wrap">
        <div className="feature-spotlight">
          <div className="feature-grid">
            <div className="fade-in">
              <span className="feature-tag">● Featured product · Live</span>
              <h2 className="feature-title">
                <span className="accent">Markaloud</span> turns viva
                recordings into constructive feedback.
              </h2>
              <p className="feature-sub">
                An academic audio-marking tool for assignments and vivas. Upload once;
                Markaloud timestamps the moments that matter and drafts feedback that's
                aligned with your rubric — and your students.
              </p>
              <div className="feature-stats">
                <div className="stat"><div className="n">Hours<span style={{ fontSize: 16, color: "var(--fg-3)", marginLeft: 4 }}>saved</span></div><div className="l">Per marker, per cohort</div></div>
                <div className="stat"><div className="n">Pilots</div><div className="l">UK higher-ed · 2026</div></div>
                <div className="stat"><div className="n">QAA</div><div className="l">Aligned rubrics</div></div>
              </div>
              <div className="feature-cta">
                <a className="btn btn-primary" href="/markaloud" onClick={(e) => { e.preventDefault(); go("markaloud"); }}>
                  Read the case study <span className="arr">→</span>
                </a>
                <a className="btn btn-ghost" href="https://www.markaloud.cloud" target="_blank" rel="noreferrer">
                  markaloud.cloud <span className="arr">↗</span>
                </a>
              </div>
            </div>
            <div className="fade-in" data-delay="2">
              <MarkaloudMock />
            </div>
          </div>
        </div>
      </div>
    </section>
  );
}

// ─── Hero ────────────────────────────────────────────────────────────────

function Hero({ go }) {
  return (
    <section className="hero">
      <div className="wrap hero-grid">
        <div>
          <div className="hero-eyebrow fade-in">
            <span className="pulse" aria-hidden="true"></span>
            Now <span style={{ color: "var(--fg-3)", marginLeft: 4 }}>— shipping Markaloud beta</span>
          </div>
          <h1 className="t-display fade-in" data-delay="1">
            Engineering<br/>
            for the messy,<br/>
            <span className="accent">physical</span> world.
            <span className="hero-cursor" aria-hidden="true"></span>
          </h1>
          <p className="hero-sub fade-in" data-delay="2">
            I'm <span style={{ color: "var(--fg)" }}>Adam Morgan</span>. I run
            <span style={{ color: "var(--fg)" }}> Otherworld Dev Ltd</span> — a
            software, AI &amp; robotics engineering practice. Currently Lecturer in
            Robotics at Swansea University; <span style={{ color: "var(--accent)" }}>going
            full-time independent from July 2026</span>.
          </p>
          <div className="hero-actions fade-in" data-delay="3">
            <a className="btn btn-primary" href="/markaloud" onClick={(e) => { e.preventDefault(); go("markaloud"); }}>
              See Markaloud <span className="arr">→</span>
            </a>
            <a className="btn btn-ghost" href="/work" onClick={(e) => { e.preventDefault(); go("work"); }}>
              Selected work
            </a>
          </div>
          <div className="hero-meta fade-in" data-delay="4">
            <div>
              <div className="lbl">Disciplines</div>
              <div className="val">Software · AI · Robotics · Hardware</div>
            </div>
            <div>
              <div className="lbl">Available</div>
              <div className="val" style={{ color: "var(--accent)" }}>● From July 2026</div>
            </div>
            <div>
              <div className="lbl">Research</div>
              <div className="val">5 published · 1 in progress</div>
            </div>
            <div>
              <div className="lbl">Based</div>
              <div className="val">Swansea, UK</div>
            </div>
          </div>
        </div>

        <div className="fade-in" data-delay="2">
          <div className="hero-vis">
            <div className="hero-vis-bar">
              <span className="dot live"></span>
              <span style={{ flex: 1 }}>arm-04 · pick + place · cycle 003</span>
              <span style={{ color: "var(--accent)" }}>● LIVE</span>
            </div>
            <div style={{ padding: 0 }}>
              <RobotArm />
            </div>
          </div>
        </div>
      </div>
    </section>
  );
}

// ─── Work cards (used on home + work page) ───────────────────────────────

const WORKS = [
  {
    n: "01",
    ttl: "Markaloud",
    desc: "The featured product — academic audio-marking for vivas and oral assessments. Whisper-based transcription, rubric-aligned feedback, time-pinned comments.",
    tags: ["AI", "SaaS", "Featured"],
    href: "/markaloud",
    vis: (
      <svg viewBox="0 0 200 120" width="100%" height="100%" preserveAspectRatio="xMidYMid slice">
        {Array.from({ length: 32 }).map((_, i) => {
          const h = 12 + ((i * 13) % 70);
          const lit = i > 10 && i < 18;
          return <rect key={i} x={10 + i * 6} y={(120 - h) / 2} width="3" height={h}
                       fill={lit ? "var(--accent)" : "var(--fg-4)"} rx="1.5"/>;
        })}
        <line x1="10" y1="60" x2="200" y2="60" stroke="rgba(34,211,196,0.18)"/>
        <text x="100" y="106" textAnchor="middle" fontFamily="monospace" fontSize="7" fill="var(--fg-3)" letterSpacing="0.12em">05:54 · STRENGTH</text>
      </svg>
    ),
  },
  {
    n: "02",
    ttl: "Shopping List",
        desc: "A shared shopping list for Nextcloud — Vue 3 + TypeScript + PHP. Real-time sync, offline mode, ingredient parsing, auto-area detection.",
    tags: ["Software", "Vue", "Nextcloud"],
    href: "https://github.com/otherworld-dev/Shopping-List",
    vis: (
      <svg viewBox="0 0 200 120" width="100%" height="100%" preserveAspectRatio="xMidYMid slice">
        {/* Cards: each row = a shopping item with a category color bar */}
        <rect width="200" height="120" fill="#0a0b0e"/>
        {[
          { y: 14, w: 130, c: "var(--accent)",  chk: true,  cat: "PRODUCE" },
          { y: 32, w: 110, c: "var(--accent)",  chk: true,  cat: "PRODUCE" },
          { y: 50, w: 144, c: "#a78bfa",        chk: false, cat: "DAIRY"   },
          { y: 68, w: 122, c: "#a78bfa",        chk: false, cat: "DAIRY"   },
          { y: 86, w: 136, c: "#f59e0b",        chk: false, cat: "BAKERY"  },
        ].map((row, i) => (
          <g key={i}>
            <rect x="14" y={row.y} width={row.w} height="14" rx="2"
              fill="rgba(255,255,255,0.04)" stroke="var(--line)" strokeWidth="0.6"/>
            <rect x="14" y={row.y} width="3" height="14" fill={row.c}/>
            {/* Checkbox */}
            <rect x="22" y={row.y + 4} width="6" height="6" rx="1"
              fill={row.chk ? row.c : "none"}
              stroke="var(--fg-4)" strokeWidth="0.6"/>
            {row.chk && (
              <path d={`M${23.5} ${row.y + 7.5} l${1.5} ${1.5} l${3} -${3}`}
                stroke="#0a0b0e" strokeWidth="1.2" fill="none"/>
            )}
            {/* Item name + qty */}
            <rect x="32" y={row.y + 4} width={row.w - 36} height="2.2"
              fill={row.chk ? "var(--fg-4)" : "var(--fg-1)"}/>
            <rect x="32" y={row.y + 8} width={(row.w - 36) * 0.55} height="1.5"
              fill="var(--fg-4)"/>
          </g>
        ))}
        <text x="186" y="113" textAnchor="end"
          fontFamily="monospace" fontSize="7" fill="var(--fg-4)" letterSpacing="0.12em">
          5 ITEMS · 3 AREAS
        </text>
      </svg>
    ),
  },
  {
    n: "03",
    ttl: "Bifrost",
    desc: "PyQt5 control GUI for a 6-axis robot arm — forward & inverse kinematics, 3D visualisation, sequence recorder. Talks RepRapFirmware over USB serial.",
    tags: ["Robotics", "Python", "PyQt5"],
    href: "https://github.com/otherworld-dev/Bifrost",
    vis: (
      <svg viewBox="0 0 200 120" width="100%" height="100%" preserveAspectRatio="xMidYMid slice">
        <defs>
          <pattern id="bf-g" width="10" height="10" patternUnits="userSpaceOnUse">
            <path d="M0 10 L10 10 M10 0 L10 10" stroke="rgba(255,255,255,0.05)"/>
          </pattern>
        </defs>
        <rect width="200" height="120" fill="url(#bf-g)"/>
        <line x1="20" y1="100" x2="180" y2="100" stroke="rgba(34,211,196,0.35)" strokeDasharray="2 4"/>
        <g stroke="var(--fg-2)" strokeWidth="3" strokeLinecap="round" fill="none">
          <line x1="40" y1="100" x2="40" y2="78"/>
          <line x1="40" y1="78"  x2="78" y2="50"/>
          <line x1="78" y1="50"  x2="118" y2="44"/>
          <line x1="118" y1="44" x2="142" y2="58"/>
          <line x1="142" y1="58" x2="156" y2="50"/>
        </g>
        {[[40,100],[40,78],[78,50],[118,44],[142,58],[156,50]].map(([x,y], i) => (
          <circle key={i} cx={x} cy={y} r="3.5" fill={i === 5 ? "var(--accent)" : "#1d1f23"} stroke="var(--accent)" strokeWidth="1"/>
        ))}
        <circle cx="156" cy="50" r="9" fill="none" stroke="var(--accent)" strokeDasharray="2 3"/>
        <text x="156" y="38" textAnchor="middle" fontFamily="monospace" fontSize="8" fill="var(--accent)">TCP</text>
        <rect x="28" y="100" width="24" height="6" fill="#1d1f23" stroke="var(--line)"/>
      </svg>
    ),
  },
  {
    n: "04",
    ttl: "RSIPI",
    desc: "Real-time KUKA control over the Robot Sensor Interface protocol — Python at 250 Hz, UDP/XML, trajectory generation, safety limits. MIT-licensed library.",
    tags: ["Robotics", "Python", "Real-time"],
    href: "https://github.com/otherworld-dev/rsi-pi",
    vis: (
      <svg viewBox="0 0 200 120" width="100%" height="100%" preserveAspectRatio="xMidYMid slice">
        <rect x="14" y="44" width="42" height="32" rx="3" fill="#1d1f23" stroke="var(--line-2)"/>
        <text x="35" y="64" textAnchor="middle" fontFamily="monospace" fontSize="9" fill="var(--fg-2)">PC</text>
        <rect x="144" y="40" width="42" height="40" rx="3" fill="#1d1f23" stroke="var(--line-2)"/>
        <text x="165" y="62" textAnchor="middle" fontFamily="monospace" fontSize="9" fill="var(--fg-2)">KR</text>
        <text x="165" y="73" textAnchor="middle" fontFamily="monospace" fontSize="6.5" fill="var(--fg-4)">CTRL</text>
        <line x1="56" y1="54" x2="144" y2="54" stroke="var(--accent)" strokeWidth="1.2"/>
        <polygon points="144,54 138,51 138,57" fill="var(--accent)"/>
        <text x="100" y="50" textAnchor="middle" fontFamily="monospace" fontSize="7" fill="var(--accent)">RKorr</text>
        <line x1="144" y1="68" x2="56" y2="68" stroke="var(--fg-2)" strokeWidth="1.2"/>
        <polygon points="56,68 62,65 62,71" fill="var(--fg-2)"/>
        <text x="100" y="78" textAnchor="middle" fontFamily="monospace" fontSize="7" fill="var(--fg-2)">RIst · IPOC</text>
        <text x="100" y="100" textAnchor="middle" fontFamily="monospace" fontSize="8" fill="var(--fg-3)" letterSpacing="0.1em">4ms · 250Hz</text>
        {[80,90,100,110,120].map((x, i) => (
          <line key={i} x1={x} y1="105" x2={x} y2="110" stroke="var(--fg-4)" strokeWidth="0.8"/>
        ))}
      </svg>
    ),
  },
  {
    n: "05",
    ttl: "Drone tracker",
    desc: "Real-time drone detection & tracking with a custom YOLOv11n model and a global-shutter USB camera. Two-state FSM: scanning → tracking, with offset logging.",
    tags: ["AI", "Computer Vision", "Python"],
    href: "https://github.com/otherworld-dev/Drone-Tracking",
    vis: (
      <svg viewBox="0 0 200 120" width="100%" height="100%" preserveAspectRatio="xMidYMid slice">
        <rect x="10" y="10" width="180" height="100" fill="#0a0b0e" stroke="var(--line-2)"/>
        <line x1="10" y1="60" x2="190" y2="60" stroke="rgba(34,211,196,0.35)" strokeDasharray="1 3"/>
        <line x1="100" y1="10" x2="100" y2="110" stroke="rgba(34,211,196,0.35)" strokeDasharray="1 3"/>
        <circle cx="100" cy="60" r="3" fill="none" stroke="var(--accent)"/>
        <rect x="124" y="34" width="40" height="26" fill="none" stroke="#ef4444" strokeWidth="1.4"/>
        <g stroke="var(--fg-2)" strokeWidth="1.2" fill="none">
          <line x1="138" y1="47" x2="150" y2="47"/>
          <line x1="144" y1="40" x2="144" y2="54"/>
          <circle cx="138" cy="40" r="2"/>
          <circle cx="150" cy="40" r="2"/>
          <circle cx="138" cy="54" r="2"/>
          <circle cx="150" cy="54" r="2"/>
        </g>
        <text x="124" y="30" fontFamily="monospace" fontSize="7" fill="#ef4444">drone 0.89</text>
        <line x1="100" y1="60" x2="144" y2="47" stroke="#7dd3fc" strokeWidth="1" strokeDasharray="2 2"/>
        <circle cx="144" cy="47" r="1.6" fill="#ef4444"/>
        <text x="14" y="106" fontFamily="monospace" fontSize="7" fill="#ef4444" letterSpacing="0.12em">● TRACKING</text>
      </svg>
    ),
  },
  {
    n: "06",
    ttl: "ThorRR",
    desc: "An open-source 6-DOF printable robot arm, forked from Thor and redesigned around RepRapFirmware + the Octopus Pro board. Stretches to 625 mm, ≤ £350 BOM.",
    tags: ["Hardware", "3D Print", "Open Source"],
    href: "https://github.com/otherworld-dev/ThorRR",
    vis: (
      <svg viewBox="0 0 200 120" width="100%" height="100%" preserveAspectRatio="xMidYMid slice">
        {[
          { y: 96, w: 70, label: "BASE" },
          { y: 80, w: 56, label: "J1" },
          { y: 66, w: 44, label: "J2" },
          { y: 52, w: 38, label: "J3" },
          { y: 38, w: 30, label: "WRIST" },
          { y: 24, w: 22, label: "TOOL" },
        ].map((s, i) => (
          <g key={i}>
            <rect x={100 - s.w/2} y={s.y - 5} width={s.w} height="9" rx="1.5"
              fill={i === 5 ? "var(--accent)" : "#1d1f23"}
              stroke="var(--line-2)" opacity={i === 5 ? 0.95 : 1}/>
            <text x="170" y={s.y + 2} fontFamily="monospace" fontSize="6.5"
              fill="var(--fg-4)" letterSpacing="0.1em">{s.label}</text>
          </g>
        ))}
        <line x1="100" y1="100" x2="100" y2="20" stroke="rgba(34,211,196,0.25)" strokeDasharray="2 2"/>
        <text x="30" y="113" fontFamily="monospace" fontSize="7" fill="var(--fg-3)" letterSpacing="0.1em">625 mm · 6-DOF</text>
      </svg>
    ),
  },
  {
    n: "07",
    ttl: "DrillCam",
    desc: "High-speed camera app for monitoring drilling — 309 fps on a global-shutter OV9281, lossless FFV1 capture, FFT vibration analysis, sub-pixel motion tracking.",
    tags: ["AI", "Imaging", "Raspberry Pi"],
    href: "https://github.com/otherworld-dev/Drill-Cam",
    vis: (
      <svg viewBox="0 0 200 120" width="100%" height="100%" preserveAspectRatio="xMidYMid slice">
        <rect x="10" y="10" width="180" height="60" fill="#0a0b0e" stroke="var(--line-2)"/>
        {Array.from({ length: 38 }).map((_, i) => {
          const x = 14 + i * 4.6;
          const phase = i * 0.62;
          const yMid = 40;
          const a = 16 * (0.5 + 0.5 * Math.sin(i * 0.18));
          const y1 = yMid - a * Math.sin(phase);
          const y2 = yMid - a * Math.sin(phase + 0.62);
          return <line key={i} x1={x} y1={y1} x2={x + 4.6} y2={y2}
            stroke={i > 8 && i < 22 ? "var(--accent)" : "var(--fg-2)"} strokeWidth="1.2"/>;
        })}
        <line x1="14" y1="40" x2="186" y2="40" stroke="rgba(255,255,255,0.05)"/>
        <text x="16" y="22" fontFamily="monospace" fontSize="7" fill="var(--fg-4)" letterSpacing="0.1em">VIBRATION · 187Hz</text>
        <rect x="10" y="80" width="50" height="28" rx="2" fill="#1d1f23" stroke="var(--line-2)"/>
        <circle cx="35" cy="94" r="8" fill="none" stroke="var(--accent)" strokeWidth="1.2"/>
        <circle cx="35" cy="94" r="3" fill="var(--accent)"/>
        <rect x="70" y="80" width="120" height="28" rx="2" fill="none" stroke="var(--line-2)"/>
        <text x="80" y="92" fontFamily="monospace" fontSize="9" fill="var(--fg-1)">309 fps</text>
        <text x="80" y="103" fontFamily="monospace" fontSize="7" fill="var(--fg-4)" letterSpacing="0.08em">OV9281 · global shutter</text>
      </svg>
    ),
  },
];

function WorkGrid({ limit }) {
  const items = limit ? WORKS.slice(0, limit) : WORKS;
  const [stats, setStats] = React.useState({});
  React.useEffect(() => {
    let cancelled = false;
    const ghRepos = items
      .map(w => w.href || "")
      .filter(href => href.startsWith("https://github.com/"))
      .map(href => href.replace("https://github.com/", "").replace(/\/$/, ""));
    if (!ghRepos.length) return;
    Promise.all(ghRepos.map(async repo => {
      try {
        const r = await fetch(`https://api.github.com/repos/${repo}`);
        if (!r.ok) return null;
        const data = await r.json();
        return [repo, {
          stars: data.stargazers_count,
          lang: data.language,
          updated: data.pushed_at,
        }];
      } catch { return null; }
    })).then(results => {
      if (cancelled) return;
      const map = {};
      for (const r of results) if (r) map[r[0]] = r[1];
      setStats(map);
    });
    return () => { cancelled = true; };
  }, []);

  function formatAge(iso) {
    if (!iso) return null;
    const d = Math.max(0, (Date.now() - new Date(iso).getTime()) / 86400000);
    if (d < 1) return "today";
    if (d < 2) return "yesterday";
    if (d < 30) return `${Math.round(d)}d ago`;
    if (d < 365) return `${Math.round(d / 30)}mo ago`;
    return `${Math.round(d / 365)}y ago`;
  }

  return (
    <div className="work-grid">
      {items.map((w, i) => {
        const isExternal = w.href && w.href.startsWith("http");
        const isGh = w.href && w.href.startsWith("https://github.com/");
        const repoKey = isGh ? w.href.replace("https://github.com/", "").replace(/\/$/, "") : null;
        const s = repoKey && stats[repoKey];
        const inner = (
          <React.Fragment>
            <div className="num" style={{ display: "flex", justifyContent: "space-between", alignItems: "baseline", gap: 8, flexWrap: "wrap" }}>
              <span>{w.n} / {WORKS.length.toString().padStart(2, "0")}</span>
              <span style={{ display: "flex", gap: 8, alignItems: "center" }}>
                {w.badge && (
                  <span style={{
                    fontFamily: "var(--font-mono)", fontSize: 9.5,
                    color: "var(--accent)", letterSpacing: "0.08em",
                    textTransform: "uppercase",
                    padding: "2px 7px", borderRadius: 3,
                    background: "var(--accent-soft)",
                    whiteSpace: "nowrap",
                  }}>{w.badge}</span>
                )}
                {s && (
                  <span style={{ color: "var(--fg-3)", fontFamily: "var(--font-mono)", fontSize: 10.5 }}>
                    ★ {s.stars}
                    {s.updated ? <span style={{ marginLeft: 8 }}>· {formatAge(s.updated)}</span> : null}
                  </span>
                )}
              </span>
            </div>
            <div className="vis">{w.vis}</div>
            <div className="ttl">
              {w.ttl}
              <span style={{
                marginLeft: 8, fontFamily: "var(--font-mono)",
                fontSize: 11, color: "var(--fg-4)", fontWeight: 400, letterSpacing: "0.04em",
              }}>
                {isExternal ? "↗" : "→"}
              </span>
            </div>
            <div className="desc">{w.desc}</div>
            <div className="tags">{w.tags.map(t => <span className="tag" key={t}>{t}</span>)}</div>
          </React.Fragment>
        );
        const className = "work-card fade-in";
        const delay = Math.min(i + 1, 5);
        const style = { textDecoration: "none", color: "inherit", cursor: "pointer" };
        if (w.href) {
          return (
            <a key={w.n} className={className} data-delay={delay} href={w.href}
               target={isExternal ? "_blank" : undefined}
               rel={isExternal ? "noreferrer" : undefined}
               style={style}>
              {inner}
            </a>
          );
        }
        return <div className="work-card fade-in" key={w.n} data-delay={delay}>{inner}</div>;
      })}
    </div>
  );
}

// ─── Clients ─────────────────────────────────────────────────────────────

const CLIENTS = ["Thor Community", "ThorRR", "Bifrost", "Nextcloud", "KUKA RSI", "OV9281"];

function Clients() {
  return (
    <div className="clients">
      {CLIENTS.map((c, i) => (
        <div className="client-cell" key={c}>{c.toUpperCase()}</div>
      ))}
    </div>
  );
}

// ─── Contact CTA (home strip) ────────────────────────────────────────────

function ContactCTA({ go }) {
  return (
    <section className="contact-cta">
      <div className="wrap contact-grid">
        <div>
          <div className="t-eyebrow fade-in"><span className="dot"></span>Open to work</div>
          <h2 className="t-h1 fade-in" data-delay="1" style={{ marginTop: 22 }}>
            Have a hard, weird,<br/>
            <span className="accent">cross-discipline</span> problem?
          </h2>
          <p className="t-body-lg fade-in" data-delay="2" style={{ marginTop: 22, maxWidth: 560 }}>
            I take on a small number of consulting and build engagements each year.
            If yours bridges hardware, software, and people — I'd love to hear about it.
          </p>
          <div className="fade-in" data-delay="3" style={{ marginTop: 28, display: "flex", gap: 10 }}>
            <a className="btn btn-primary" href="/contact" onClick={(e) => { e.preventDefault(); go("contact"); }}>
              Start a conversation <span className="arr">→</span>
            </a>
            <a className="btn btn-ghost" href="mailto:contact@otherworld.dev">contact@otherworld.dev</a>
          </div>
        </div>
        <div className="contact-side fade-in" data-delay="3">
          <div className="contact-row"><span>Engagement</span><span className="v">Build · Advise · Diligence</span></div>
          <div className="contact-row"><span>Disciplines</span><span className="v">Software · AI · Robotics</span></div>
          <div className="contact-row"><span>Response</span><span className="v">Within 48 hours</span></div>
          <div className="contact-row"><span>Based</span><span className="v">Swansea, UK · remote / hybrid</span></div>
        </div>
      </div>
    </section>
  );
}

// Export to window for cross-file access
Object.assign(window, {
  Nav, Footer, Hero, MarkaloudFeature, MarkaloudMock, WorkGrid,
  Clients, ContactCTA, Brand, WORKS, CLIENTS, Waveform,
});
