/* Bundled components — concatenated to avoid in-browser Babel multi-fetch races */


/* ===== galaxy.jsx ===== */
/* ------------------------------------------------------------------
   Galaxy3D — Three.js cluster of app-icon sprites + starfield.
   Mounts into a full-bleed canvas behind the hero.
   Props: intensity (0..1.5), density ("stars"|"nebula"|"min"), accent (hex)
------------------------------------------------------------------- */
function Galaxy3D({ intensity = 1, density = "stars", accent = "#8B5CF6", paused = false }) {
  const mountRef = React.useRef(null);
  const stateRef = React.useRef({});

  // Build once: scene, icons, stars.
  React.useEffect(function () {
    const THREE = window.THREE;
    const mount = mountRef.current;
    if (!THREE || !mount) return;

    const W = () => mount.clientWidth;
    const H = () => mount.clientHeight;

    const scene = new THREE.Scene();
    const camera = new THREE.PerspectiveCamera(50, W() / H(), 0.1, 100);
    camera.position.set(0, 0, 15);

    const renderer = new THREE.WebGLRenderer({ antialias: true, alpha: true });
    renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
    renderer.setSize(W(), H());
    if ("outputColorSpace" in renderer && THREE.SRGBColorSpace) renderer.outputColorSpace = THREE.SRGBColorSpace;
    else if (THREE.sRGBEncoding) renderer.outputEncoding = THREE.sRGBEncoding;
    mount.appendChild(renderer.domElement);

    // ---- icon sprites on a fibonacci sphere ----
    const apps = window.PORTFOLIO.APPS;
    const iconGroup = new THREE.Group();
    scene.add(iconGroup);
    const loader = new THREE.TextureLoader();
    const sprites = [];
    const N = apps.length;
    const R = 6.4;

    function setCS(t) {
      if ("colorSpace" in t && THREE.SRGBColorSpace) t.colorSpace = THREE.SRGBColorSpace;
      else if (THREE.sRGBEncoding) t.encoding = THREE.sRGBEncoding;
      t.anisotropy = 4;
    }
    function roundRectPath(ctx, x, y, w, h, r) {
      ctx.beginPath();
      ctx.moveTo(x + r, y);
      ctx.arcTo(x + w, y, x + w, y + h, r);
      ctx.arcTo(x + w, y + h, x, y + h, r);
      ctx.arcTo(x, y + h, x, y, r);
      ctx.arcTo(x, y, x + w, y, r);
      ctx.closePath();
    }
    function makeTex(app) {
      const url = window.VISUALS.getIcon(app);
      if (!url) { const t = loader.load(window.VISUALS.iconDataUrl(app)); setCS(t); return t; }
      const sz = 256;
      const canvas = document.createElement("canvas");
      canvas.width = canvas.height = sz;
      const ctx = canvas.getContext("2d");
      const tex = new THREE.CanvasTexture(canvas);
      setCS(tex);
      const img = new Image();
      img.crossOrigin = "anonymous";
      img.onload = function () {
        ctx.clearRect(0, 0, sz, sz);
        ctx.save();
        roundRectPath(ctx, 0, 0, sz, sz, sz * 0.225);
        ctx.clip();
        const s = Math.max(sz / img.width, sz / img.height);
        const w = img.width * s, h = img.height * s;
        ctx.drawImage(img, (sz - w) / 2, (sz - h) / 2, w, h);
        ctx.restore();
        tex.needsUpdate = true;
      };
      img.src = url;
      return tex;
    }

    for (let i = 0; i < N; i++) {
      const y = 1 - (i / (N - 1)) * 2;
      const rad = Math.sqrt(1 - y * y);
      const theta = i * 2.399963; // golden angle
      const pos = new THREE.Vector3(Math.cos(theta) * rad, y, Math.sin(theta) * rad).multiplyScalar(R);

      const tex = makeTex(apps[i]);
      const mat = new THREE.SpriteMaterial({ map: tex, transparent: true, depthWrite: false });
      const sp = new THREE.Sprite(mat);
      sp.position.copy(pos);
      const s = 1.5;
      sp.scale.set(s, s, s);
      sp.userData.base = s;
      iconGroup.add(sp);
      sprites.push(sp);
    }

    // ---- starfield ----
    function makeStars(count, spread, size, color, opacity) {
      const geo = new THREE.BufferGeometry();
      const arr = new Float32Array(count * 3);
      for (let i = 0; i < count; i++) {
        const r = spread * (0.4 + Math.random() * 0.6);
        const t = Math.random() * Math.PI * 2;
        const p = Math.acos(2 * Math.random() - 1);
        arr[i * 3] = r * Math.sin(p) * Math.cos(t);
        arr[i * 3 + 1] = r * Math.sin(p) * Math.sin(t);
        arr[i * 3 + 2] = r * Math.cos(p);
      }
      geo.setAttribute("position", new THREE.BufferAttribute(arr, 3));
      const mat = new THREE.PointsMaterial({
        color: new THREE.Color(color), size: size, sizeAttenuation: true,
        transparent: true, opacity: opacity, depthWrite: false,
        blending: THREE.AdditiveBlending,
      });
      return new THREE.Points(geo, mat);
    }
    const starsFar = makeStars(1400, 34, 0.09, "#cdd6ff", 0.7);
    const starsNear = makeStars(500, 20, 0.16, accent, 0.55);
    scene.add(starsFar);
    scene.add(starsNear);

    // ---- interaction ----
    let rotX = 0.1, rotY = 0, targetRX = 0.1, targetRY = 0;
    let velY = 0.0016, velX = 0;
    let dragging = false, lastX = 0, lastY = 0;
    let mx = 0, my = 0;

    function onDown(e) {
      dragging = true;
      const p = e.touches ? e.touches[0] : e;
      lastX = p.clientX; lastY = p.clientY;
      velX = velY = 0;
    }
    function onMove(e) {
      const p = e.touches ? e.touches[0] : e;
      const rect = mount.getBoundingClientRect();
      mx = ((p.clientX - rect.left) / rect.width - 0.5);
      my = ((p.clientY - rect.top) / rect.height - 0.5);
      if (!dragging) return;
      const dx = p.clientX - lastX, dy = p.clientY - lastY;
      lastX = p.clientX; lastY = p.clientY;
      targetRY += dx * 0.005;
      targetRX += dy * 0.005;
      targetRX = Math.max(-0.9, Math.min(0.9, targetRX));
      velY = dx * 0.00025;
      velX = dy * 0.00025;
    }
    function onUp() { dragging = false; }

    const el = renderer.domElement;
    el.addEventListener("mousedown", onDown);
    el.addEventListener("touchstart", onDown, { passive: true });
    window.addEventListener("mousemove", onMove);
    window.addEventListener("touchmove", onMove, { passive: true });
    window.addEventListener("mouseup", onUp);
    window.addEventListener("touchend", onUp);

    function onResize() {
      camera.aspect = W() / H();
      camera.updateProjectionMatrix();
      renderer.setSize(W(), H());
    }
    window.addEventListener("resize", onResize);

    stateRef.current = {
      get intensity() { return stateRef.current._i; },
      get paused() { return stateRef.current._p; },
      starsNear, sprites, _i: intensity, _p: paused,
    };

    let raf;
    const clock = new THREE.Clock();
    function tick() {
      raf = requestAnimationFrame(tick);
      const dt = clock.getDelta();
      const I = stateRef.current._i;
      if (!stateRef.current._p) {
        if (!dragging) {
          targetRY += velY * I;
          targetRX += velX;
          velX *= 0.96;
        }
      }
      rotY += (targetRY - rotY) * 0.08;
      rotX += (targetRX - rotX) * 0.08;
      // parallax from mouse
      camera.position.x += (mx * 2.2 - camera.position.x) * 0.04;
      camera.position.y += (-my * 1.6 - camera.position.y) * 0.04;
      camera.lookAt(0, 0, 0);

      iconGroup.rotation.y = rotY;
      iconGroup.rotation.x = rotX;
      starsFar.rotation.y = rotY * 0.3;
      starsNear.rotation.y = -rotY * 0.18 + clock.elapsedTime * 0.01;

      // billboard already (sprites). subtle pulse on near side
      for (let i = 0; i < sprites.length; i++) {
        const sp = sprites[i];
        const wp = sp.getWorldPosition(new THREE.Vector3());
        const depth = (wp.z + R) / (2 * R); // 0 back .. 1 front
        const o = 0.35 + depth * 0.65;
        sp.material.opacity = o;
        const sc = sp.userData.base * (0.78 + depth * 0.32);
        sp.scale.set(sc, sc, sc);
      }
      renderer.render(scene, camera);
    }
    tick();

    return function cleanup() {
      cancelAnimationFrame(raf);
      el.removeEventListener("mousedown", onDown);
      el.removeEventListener("touchstart", onDown);
      window.removeEventListener("mousemove", onMove);
      window.removeEventListener("touchmove", onMove);
      window.removeEventListener("mouseup", onUp);
      window.removeEventListener("touchend", onUp);
      window.removeEventListener("resize", onResize);
      renderer.dispose();
      if (el.parentNode) el.parentNode.removeChild(el);
    };
  }, []);

  // react to prop changes
  React.useEffect(function () {
    if (stateRef.current) {
      stateRef.current._i = intensity;
      stateRef.current._p = paused;
      if (stateRef.current.starsNear) {
        stateRef.current.starsNear.material.color.set(accent);
      }
    }
  }, [intensity, paused, accent]);

  return React.createElement("div", { ref: mountRef, className: "galaxy-canvas" });
}

window.Galaxy3D = Galaxy3D;


/* ===== ui.jsx ===== */
/* Shared UI atoms: AppIcon, store buttons, platform badges, category pill, Reveal */

function AppIcon({ app, size = 64, radius = 22, glow = true }) {
  const c = window.VISUALS.catColors(app);
  const icon = window.VISUALS.getIcon(app);
  const ref = React.useRef(null);
  React.useEffect(function () {
    if (!icon && ref.current) ref.current.innerHTML = window.VISUALS.iconSvg(app, { radius: (radius / size) * 100 });
  }, [app, size, radius, icon]);
  const boxStyle = {
    width: size, height: size, borderRadius: radius,
    boxShadow: glow ? ("0 8px 26px -8px " + c[0] + "88") : "none",
  };
  if (icon) {
    return (
      <div className="app-icon" style={boxStyle}>
        <img src={icon} alt={app.name + " icon"} decoding="async"
          style={{ width: "100%", height: "100%", objectFit: "cover", display: "block" }} />
      </div>
    );
  }
  return React.createElement("div", { ref: ref, className: "app-icon", style: boxStyle });
}

function AppleMark({ s = 16 }) {
  return (
    <svg width={s} height={s} viewBox="0 0 24 24" fill="currentColor" aria-hidden="true">
      <path d="M17.05 12.6c-.03-2.6 2.12-3.85 2.22-3.91-1.21-1.77-3.1-2.01-3.77-2.04-1.6-.16-3.13.94-3.94.94-.82 0-2.07-.92-3.4-.9-1.75.03-3.36 1.02-4.26 2.58-1.82 3.16-.47 7.83 1.3 10.39.86 1.25 1.89 2.66 3.24 2.6 1.3-.05 1.79-.84 3.36-.84 1.56 0 2 .84 3.37.81 1.39-.02 2.27-1.27 3.12-2.53.98-1.45 1.39-2.86 1.41-2.93-.03-.01-2.71-1.04-2.74-4.13zM14.6 4.84c.72-.87 1.2-2.08 1.07-3.28-1.03.04-2.28.68-3.02 1.55-.66.77-1.24 2-1.08 3.18 1.15.09 2.32-.58 3.03-1.45z" />
    </svg>
  );
}

function PlayMark({ s = 15 }) {
  return (
    <svg width={s} height={s} viewBox="0 0 24 24" aria-hidden="true">
      <path d="M3.6 2.3 13.4 12 3.6 21.7c-.3-.2-.5-.6-.5-1.1V3.4c0-.5.2-.9.5-1.1z" fill="#5BE0A6" />
      <path d="M16.8 8.6 13.4 12l3.4 3.4 3.5-2c.9-.5.9-1.8 0-2.3l-3.5-2.5z" fill="#FFD24D" />
      <path d="M13.4 12 3.6 2.3c.1-.1.3-.1.5-.1.3 0 .6.1.9.3l8 4.6-0 .1z" fill="#46C2FF" />
      <path d="M13.4 12 5 16.9l-1.4.8 9.8-5.7z" fill="#FF6B6B" />
    </svg>
  );
}

function StoreButton({ kind, href, compact = false }) {
  if (!href) return null;
  const apple = kind === "ios";
  return (
    <a className={"store-btn" + (compact ? " compact" : "")} href={href} target="_blank" rel="noopener noreferrer">
      <span className="store-mark">{apple ? <AppleMark s={compact ? 15 : 19} /> : <PlayMark s={compact ? 14 : 18} />}</span>
      <span className="store-txt">
        <span className="store-top">{apple ? "Download on the" : "Get it on"}</span>
        <span className="store-name">{apple ? "App Store" : "Google Play"}</span>
      </span>
    </a>
  );
}

function PlatformBadges({ app }) {
  return (
    <div className="plat-badges">
      {app.ios && <span className="plat ios">iOS</span>}
      {app.android && <span className="plat android">Android</span>}
    </div>
  );
}

function CategoryPill({ cat, active, onClick, count }) {
  const meta = window.PORTFOLIO.CATEGORIES[cat];
  const style = meta ? { "--c1": meta.g[0], "--c2": meta.g[1] } : {};
  return (
    <button className={"cat-pill" + (active ? " active" : "")} style={style} onClick={onClick}>
      {cat}{count != null && <span className="cat-count">{count}</span>}
    </button>
  );
}

function Reveal({ children, delay = 0, as = "div", className = "" }) {
  const ref = React.useRef(null);
  const [seen, setSeen] = React.useState(false);
  React.useEffect(function () {
    const el = ref.current;
    if (!el) return;
    const io = new IntersectionObserver(function (entries) {
      entries.forEach(function (e) { if (e.isIntersecting) { setSeen(true); io.disconnect(); } });
    }, { threshold: 0.12, rootMargin: "0px 0px -8% 0px" });
    io.observe(el);
    return function () { io.disconnect(); };
  }, []);
  return React.createElement(as, {
    ref: ref,
    className: "reveal " + (seen ? "in " : "") + className,
    style: { transitionDelay: delay + "ms" },
  }, children);
}

Object.assign(window, { AppIcon, StoreButton, PlatformBadges, CategoryPill, Reveal, AppleMark, PlayMark });


/* ===== hero.jsx ===== */
/* Hero — 3D galaxy backdrop, name, value line, store CTAs, proof stats */

function NavBar({ accent }) {
  const [solid, setSolid] = React.useState(false);
  React.useEffect(function () {
    const onScroll = () => setSolid(window.scrollY > 40);
    window.addEventListener("scroll", onScroll);
    return () => window.removeEventListener("scroll", onScroll);
  }, []);
  const links = [["Featured", "#featured"], ["Apps", "#apps"], ["Approach", "#approach"], ["About", "#about"]];
  return (
    <header className={"nav" + (solid ? " solid" : "")}>
      <a className="brand" href="#top">
        <span className="brand-name">Marwan Akhandaf</span>
      </a>
      <nav className="nav-links">
        {links.map(([l, h]) => <a key={h} href={h}>{l}</a>)}
      </nav>
      <a className="nav-cta" href="#contact">Get in touch</a>
    </header>
  );
}

function Hero({ t }) {
  const apps = window.PORTFOLIO.APPS;
  const links = window.PORTFOLIO.LINKS;
  const cats = Object.keys(window.PORTFOLIO.CATEGORIES).length;
  const stats = [
    { n: apps.length + "+", l: "Published apps" },
    { n: "2", l: "Platforms" },
    { n: cats, l: "Categories" },
    { n: "AI", l: "Core focus" },
  ];
  return (
    <section className="hero" id="top">
      <div className="galaxy-wrap">
        <Galaxy3D intensity={t.intensity} accent={t.accent} density={t.bg} paused={false} />
        <div className="galaxy-vignette" />
      </div>

      <div className="hero-inner">
        <div className="hero-eyebrow">
          <span className="pulse-dot" /> Mobile App Developer · iOS &amp; Android
        </div>
        <h1 className="hero-title">Marwan<br />Akhandaf</h1>
        <p className="hero-lead">
          I’m a computer science graduate and mobile app developer building iOS and Android
          apps since 2019. What started as fascination with putting software in people’s
          pockets has grown into <strong>{apps.length}+ published apps</strong> across the App Store
          and Google Play.
        </p>
        <div className="hero-cta">
          <StoreButton kind="ios" href={links.appleDev} />
          <StoreButton kind="android" href={links.androidDev} />
        </div>

        <div className="hero-stats">
          {stats.map((s, i) => (
            <div className="stat" key={i}>
              <div className="stat-n">{s.n}</div>
              <div className="stat-l">{s.l}</div>
            </div>
          ))}
        </div>
      </div>

      <a className="hero-scroll" href="#featured" aria-label="Scroll to work">
        <span>Drag the galaxy · scroll to explore</span>
        <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round"><path d="M12 5v14M6 13l6 6 6-6" /></svg>
      </a>
    </section>
  );
}

Object.assign(window, { NavBar, Hero });


/* ===== featured.jsx ===== */
/* Featured apps — 6 headline cards */

function FeatCard({ app, i }) {
  const grad = window.VISUALS.gradientCss(app);
  const meta = window.PORTFOLIO.CATEGORIES[app.cat];
  return (
    <Reveal as="article" className="feat-card" delay={i * 70}>
      <div className="feat-media" style={{ backgroundImage: grad }}>
        <div className="feat-watermark" dangerouslySetInnerHTML={{
          __html: '<svg viewBox="0 0 24 24" fill="none" stroke="#fff" stroke-width="1.1" stroke-linecap="round" stroke-linejoin="round">' + window.PORTFOLIO.GLYPHS[app.glyph] + '</svg>'
        }} />
        <div className="feat-media-row">
          <AppIcon app={app} size={66} radius={18} glow={false} />
          <div className="feat-media-meta">
            <span className="feat-media-name">{app.short}</span>
            <span className="feat-media-cat">{app.cat}</span>
          </div>
        </div>
      </div>
      <div className="feat-body">
        <h3 className="feat-name">{app.name}</h3>
        <p className="feat-blurb">{app.blurb}</p>
        <div className="feat-foot">
          <PlatformBadges app={app} />
          <div className="feat-stores">
            <StoreButton kind="ios" href={app.ios} compact />
            <StoreButton kind="android" href={app.android} compact />
          </div>
        </div>
      </div>
    </Reveal>
  );
}

function Featured() {
  const apps = window.PORTFOLIO.APPS.filter((a) => a.featured);
  return (
    <section className="section featured" id="featured">
      <div className="section-head">
        <Reveal as="div" className="eyebrow">Featured work</Reveal>
        <Reveal as="h2" className="section-title" delay={60}>
          Apps that put AI to work
        </Reveal>
        <Reveal as="p" className="section-sub" delay={120}>
          A selection of recent builds — each one shipped end to end, from concept and
          UI to publishing and store optimization.
        </Reveal>
      </div>
      <div className="feat-grid">
        {apps.map((a, i) => <FeatCard key={a.name} app={a} i={i} />)}
      </div>
    </section>
  );
}

window.Featured = Featured;


/* ===== work.jsx ===== */
/* Approach — qualitative case studies + skills */

const CASES = [
  {
    short: "MelonNote", appName: "MelonNote — AI Note Taker", glyph: "note", cat: "Productivity",
    problem: "Notes from meetings and lectures pile up faster than anyone can organize them.",
    built: "An AI note taker that records, transcribes and restructures audio into clean, shareable notes — shipped on both iOS and Android.",
    features: ["Audio capture", "AI transcription", "Smart summaries", "Cross-platform"],
    result: "Raw recordings become readable, structured notes in seconds.",
    ios: "https://apps.apple.com/us/app/melonnote-ai-note-taker/id6754255911?uo=4",
    android: "https://play.google.com/store/apps/details?id=com.akhandaf.melonnote&hl=nl",
  },
  {
    short: "Zwintji", appName: "AI Calorie scanner — Zwintji", glyph: "flame", cat: "AI",
    problem: "Manual calorie logging is tedious enough that most people give up within a week.",
    built: "A camera-first scanner that estimates calories and macros from a single photo of a meal.",
    features: ["Camera-first", "AI food recognition", "Macro breakdown", "Daily log"],
    result: "Logging a meal becomes a photo instead of a form.",
    ios: "https://apps.apple.com/us/app/ai-calorie-scanner-zwintji/id6747187048?uo=4",
  },
  {
    short: "Baytee", appName: "AI Room Planner — Baytee", glyph: "home", cat: "AI",
    problem: "It's hard to picture a room redesign before committing real time or money.",
    built: "Snap a photo of a space and let generative AI restyle it into new looks instantly.",
    features: ["Photo input", "Generative restyle", "Multiple styles", "Share & save"],
    result: "Interior inspiration that's personal to your actual room.",
    ios: "https://apps.apple.com/us/app/ai-room-planner-baytee/id6743873862?uo=4",
  },
];

function CaseCard({ c, i }) {
  const fakeApp = { cat: c.cat, glyph: c.glyph, short: c.short, name: c.appName };
  const grad = window.VISUALS.gradientCss(fakeApp);
  return (
    <Reveal as="article" className="case-card" delay={i * 90}>
      <div className="case-media" style={{ backgroundImage: grad }}>
        <div className="case-icon"><AppIcon app={fakeApp} size={76} radius={20} glow={false} /></div>
        <span className="case-num">{String(i + 1).padStart(2, "0")}</span>
      </div>
      <div className="case-body">
        <div className="case-tag">{c.cat}</div>
        <h3 className="case-name">{c.appName}</h3>
        <div className="case-block">
          <span className="case-label">The problem</span>
          <p>{c.problem}</p>
        </div>
        <div className="case-block">
          <span className="case-label">What I built</span>
          <p>{c.built}</p>
        </div>
        <div className="case-features">
          {c.features.map((f) => <span className="chip" key={f}>{f}</span>)}
        </div>
        <div className="case-result">
          <span className="case-label">Outcome</span>
          <p>{c.result}</p>
        </div>
        <div className="case-stores">
          <StoreButton kind="ios" href={c.ios} compact />
          <StoreButton kind="android" href={c.android} compact />
        </div>
      </div>
    </Reveal>
  );
}

function Approach() {
  return (
    <section className="section approach" id="approach">
      <div className="section-head">
        <Reveal as="div" className="eyebrow">How it comes together</Reveal>
        <Reveal as="h2" className="section-title" delay={60}>From a real problem to a shipped app</Reveal>
        <Reveal as="p" className="section-sub" delay={120}>
          A few builds, broken down — the problem they solve, what went into them, and the
          experience they deliver.
        </Reveal>
      </div>
      <div className="case-grid">
        {CASES.map((c, i) => <CaseCard key={c.short} c={c} i={i} />)}
      </div>
    </section>
  );
}

const SKILLS = [
  { t: "Platforms", items: ["iOS development", "Android development", "Cross-platform releases"] },
  { t: "Product & design", items: ["UI / UX design", "Product design", "Rapid prototyping"] },
  { t: "AI features", items: ["AI app workflows", "Camera & vision features", "Generative media"] },
  { t: "Launch & growth", items: ["App publishing", "Store optimization (ASO)", "Monetization"] },
];

function Skills() {
  return (
    <section className="section skills">
      <div className="section-head">
        <Reveal as="div" className="eyebrow">What I do</Reveal>
        <Reveal as="h2" className="section-title" delay={60}>End to end, idea to store</Reveal>
      </div>
      <div className="skills-grid">
        {SKILLS.map((s, i) => (
          <Reveal as="div" className="skill-col" delay={i * 80} key={s.t}>
            <h3 className="skill-t">{s.t}</h3>
            <ul>{s.items.map((it) => <li key={it}>{it}</li>)}</ul>
          </Reveal>
        ))}
      </div>
    </section>
  );
}

Object.assign(window, { Approach, Skills });


/* ===== directory.jsx ===== */
/* Full app directory — searchable + filterable */

function DirCard({ app }) {
  const c = window.VISUALS.catColors(app);
  return (
    <div className="dir-card" style={{ "--c1": c[0], "--c2": c[1] }}>
      <AppIcon app={app} size={50} radius={14} glow={false} />
      <div className="dir-info">
        <span className="dir-name">{app.name}</span>
        <span className="dir-cat">{app.cat}</span>
      </div>
      <div className="dir-links">
        {app.ios && (
          <a href={app.ios} target="_blank" rel="noopener noreferrer" className="dir-link" title="App Store"><AppleMark s={15} /></a>
        )}
        {app.android && (
          <a href={app.android} target="_blank" rel="noopener noreferrer" className="dir-link" title="Google Play"><PlayMark s={15} /></a>
        )}
      </div>
    </div>
  );
}

function Directory() {
  const allApps = window.PORTFOLIO.APPS;
  const cats = window.PORTFOLIO.CATEGORIES;
  const [q, setQ] = React.useState("");
  const [cat, setCat] = React.useState("All");

  const counts = React.useMemo(function () {
    const m = {};
    allApps.forEach((a) => { m[a.cat] = (m[a.cat] || 0) + 1; });
    return m;
  }, [allApps]);

  const filtered = allApps.filter(function (a) {
    const okCat = cat === "All" || a.cat === cat;
    const okQ = !q || (a.name + " " + a.cat + " " + a.blurb).toLowerCase().includes(q.toLowerCase());
    return okCat && okQ;
  });

  return (
    <section className="section directory" id="apps">
      <div className="section-head">
        <Reveal as="div" className="eyebrow">Full catalog</Reveal>
        <Reveal as="h2" className="section-title" delay={60}>Every app, in one place</Reveal>
        <Reveal as="p" className="section-sub" delay={120}>
          {allApps.length} published apps across {Object.keys(cats).length} categories. Search by name or
          filter by what they do.
        </Reveal>
      </div>

      <div className="dir-controls">
        <div className="dir-search">
          <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round"><circle cx="11" cy="11" r="7" /><path d="M21 21l-4-4" /></svg>
          <input
            type="text" placeholder="Search apps…" value={q}
            onChange={(e) => setQ(e.target.value)}
          />
          {q && <button className="dir-clear" onClick={() => setQ("")} aria-label="Clear">×</button>}
        </div>
        <div className="dir-pills">
          <CategoryPill cat="All" active={cat === "All"} onClick={() => setCat("All")} count={allApps.length} />
          {Object.keys(cats).map((k) => (
            <CategoryPill key={k} cat={k} active={cat === k} onClick={() => setCat(k)} count={counts[k] || 0} />
          ))}
        </div>
      </div>

      <div className="dir-grid">
        {filtered.map((a) => <DirCard key={a.name} app={a} />)}
      </div>
      {filtered.length === 0 && <p className="dir-empty">No apps match “{q}”.</p>}
    </section>
  );
}

window.Directory = Directory;


/* ===== about.jsx ===== */
/* About + Contact + Footer */

function About() {
  const apps = window.PORTFOLIO.APPS;
  return (
    <section className="section about" id="about">
      <div className="about-grid">
        <Reveal as="div" className="about-portrait-wrap">
          <div className="about-portrait">
            <img src="img/marwan.jpg" alt="Marwan Akhandaf" />
          </div>
          <div className="about-portrait-glow" />
        </Reveal>
        <div className="about-text">
          <Reveal as="div" className="eyebrow">About</Reveal>
          <Reveal as="h2" className="section-title" delay={50}>
            I ship small, useful apps — fast.
          </Reveal>
          <Reveal as="p" className="about-p" delay={110}>
            I’m a mobile app developer with a computer science background, focused on building
            useful products from idea to release. I started creating apps in 2019 because I was
            fascinated by how software could live in someone’s pocket and become part of their
            everyday routine.
          </Reveal>
          <Reveal as="p" className="about-p" delay={160}>
            Since then, I’ve published <strong>{apps.length}+ apps</strong> across the App Store and
            Google Play, covering AI tools, productivity, education, health, utilities and
            lifestyle apps. My work is not just about generating ideas. I care about the full
            engineering and product process: designing clear interfaces, building reliable app
            experiences, publishing, store optimization, iteration and monetization.
          </Reveal>
          <Reveal as="p" className="about-p" delay={210}>
            My focus is simple: build apps that solve a specific problem well, feel good to use,
            and are polished enough for real users.
          </Reveal>
          <Reveal as="div" className="about-facts" delay={260}>
            <div><span className="af-n">{apps.length}+</span><span className="af-l">apps shipped</span></div>
            <div><span className="af-n">iOS</span><span className="af-l">+ Android</span></div>
            <div><span className="af-n">AI</span><span className="af-l">first focus</span></div>
          </Reveal>
        </div>
      </div>
    </section>
  );
}

function Contact() {
  const links = window.PORTFOLIO.LINKS;
  return (
    <section className="section contact" id="contact">
      <Reveal as="div" className="contact-card">
        <div className="contact-orb" />
        <div className="eyebrow">Contact</div>
        <h2 className="contact-title">Have an app idea?<br />Let's build it.</h2>
        <p className="contact-sub">
          Open to freelance builds, collaborations and new app concepts — for iOS, Android, or both.
        </p>
        <div className="contact-actions">
          <a className="btn-primary" href={"mailto:" + links.email}>
            <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"><rect x="3" y="5" width="18" height="14" rx="2" /><path d="m3 7 9 6 9-6" /></svg>
            {links.email}
          </a>
          <a className="btn-ghost" href={links.linkedin} target="_blank" rel="noopener noreferrer">
            <svg width="18" height="18" viewBox="0 0 24 24" fill="currentColor"><path d="M20.45 20.45h-3.56v-5.57c0-1.33-.02-3.04-1.85-3.04-1.85 0-2.13 1.45-2.13 2.94v5.67H9.35V9h3.42v1.56h.05c.48-.9 1.64-1.85 3.37-1.85 3.6 0 4.27 2.37 4.27 5.46v6.28zM5.34 7.43a2.07 2.07 0 1 1 0-4.14 2.07 2.07 0 0 1 0 4.14zM7.12 20.45H3.55V9h3.57v11.45zM22.22 0H1.77C.79 0 0 .77 0 1.73v20.54C0 23.22.79 24 1.77 24h20.45c.98 0 1.78-.78 1.78-1.73V1.73C24 .77 23.2 0 22.22 0z" /></svg>
            LinkedIn
          </a>
        </div>
      </Reveal>
    </section>
  );
}

function Footer() {
  const links = window.PORTFOLIO.LINKS;
  return (
    <footer className="footer">
      <div className="footer-inner">
        <div className="footer-brand">
          <div>
            <div className="footer-name">Marwan Akhandaf</div>
            <div className="footer-role">Mobile App Developer</div>
          </div>
        </div>
        <div className="footer-cols">
          <div className="footer-col">
            <span className="footer-h">Stores</span>
            <a href={links.appleDev} target="_blank" rel="noopener noreferrer">App Store profile</a>
            <a href={links.androidDev} target="_blank" rel="noopener noreferrer">Google Play</a>
          </div>
          <div className="footer-col">
            <span className="footer-h">Connect</span>
            <a href={"mailto:" + links.email}>Email</a>
            <a href={links.linkedin} target="_blank" rel="noopener noreferrer">LinkedIn</a>
          </div>
          <div className="footer-col">
            <span className="footer-h">Explore</span>
            <a href="#featured">Featured</a>
            <a href="#apps">All apps</a>
          </div>
        </div>
      </div>
      <div className="footer-base">
        <span>© 2026 Marwan Akhandaf. All rights reserved.</span>
        <span>Built app-first · iOS &amp; Android</span>
      </div>
    </footer>
  );
}

Object.assign(window, { About, Contact, Footer });


/* ===== main.jsx ===== */
/* App root — assembles sections, wires tweaks */

const TWEAK_DEFAULTS = /*EDITMODE-BEGIN*/{
  "accent": "#8B5CF6",
  "intensity": 1,
  "motion": true
}/*EDITMODE-END*/;

function App() {
  const [t, setTweak] = useTweaks(TWEAK_DEFAULTS);
  const effIntensity = t.motion ? t.intensity : 0;

  return (
    <div className="page" style={{ "--accent": t.accent }}>
      <NavBar accent={t.accent} />
      <Hero t={{ accent: t.accent, intensity: effIntensity, bg: "stars" }} />
      <main>
        <Featured />
        <Approach />
        <Directory />
        <Skills />
        <About />
        <Contact />
      </main>
      <Footer />

      <TweaksPanel>
        <TweakSection label="Look" />
        <TweakColor label="Accent" value={t.accent}
          options={["#8B5CF6", "#22D3EE", "#34D399", "#F59E0B", "#FB7185"]}
          onChange={(v) => setTweak("accent", v)} />
        <TweakSection label="3D galaxy" />
        <TweakSlider label="Motion intensity" value={t.intensity} min={0} max={1.6} step={0.1}
          onChange={(v) => setTweak("intensity", v)} />
        <TweakToggle label="Animate galaxy" value={t.motion}
          onChange={(v) => setTweak("motion", v)} />
      </TweaksPanel>
    </div>
  );
}

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