// ── React components ──────────────────────────────────────────
// All visual building blocks. Exported to window.* so app.jsx
// and any future prototype can reference them without a bundler.
// ─────────────────────────────────────────────────────────────

// ── PhoneFrame ───────────────────────────────────────────────
function PhoneFrame({ children, width = 402, height = 874 }) {
  return (
    <div style={{
      width, height, borderRadius: 48, overflow: "hidden",
      position: "relative", background: "#000",
      boxShadow: "0 40px 80px rgba(0,0,0,0.18), 0 0 0 1px rgba(0,0,0,0.12)",
      fontFamily: "-apple-system, system-ui, sans-serif",
      WebkitFontSmoothing: "antialiased",
    }}>
      {/* dynamic island */}
      <div style={{
        position: "absolute", top: 11, left: "50%", transform: "translateX(-50%)",
        width: 126, height: 37, borderRadius: 24, background: "#000", zIndex: 50,
      }} />
      {/* status bar */}
      <div style={{
        position: "absolute", top: 0, left: 0, right: 0, height: 56, zIndex: 10,
        display: "flex", alignItems: "center", justifyContent: "space-between",
        padding: "21px 32px 0", color: "#000",
        fontFamily: '-apple-system, "SF Pro", system-ui',
        fontWeight: 590, fontSize: 17,
      }}>
        <span>9:41</span>
        <span style={{ display: "inline-flex", gap: 6, alignItems: "center" }}>
          <svg width="19" height="12" viewBox="0 0 19 12"><rect x="0" y="7.5" width="3.2" height="4.5" rx=".7" fill="currentColor"/><rect x="4.8" y="5" width="3.2" height="7" rx=".7" fill="currentColor"/><rect x="9.6" y="2.5" width="3.2" height="9.5" rx=".7" fill="currentColor"/><rect x="14.4" y="0" width="3.2" height="12" rx=".7" fill="currentColor"/></svg>
          <svg width="27" height="13" viewBox="0 0 27 13"><rect x=".5" y=".5" width="23" height="12" rx="3.5" stroke="currentColor" strokeOpacity=".35" fill="none"/><rect x="2" y="2" width="20" height="9" rx="2" fill="currentColor"/></svg>
        </span>
      </div>
      {/* scroll surface */}
      <div style={{ position: "absolute", inset: 0, overflow: "auto", background: "var(--paper)" }}>
        {children}
      </div>
      {/* home indicator */}
      <div style={{
        position: "absolute", bottom: 8, left: "50%", transform: "translateX(-50%)",
        width: 139, height: 5, borderRadius: 100,
        background: "rgba(0,0,0,0.25)", zIndex: 60, pointerEvents: "none",
      }} />
    </div>
  );
}

// ── ToggleIcon ────────────────────────────────────────────────
function ToggleIcon({ open }) {
  return (
    <svg width="16" height="16" viewBox="0 0 16 16" aria-hidden="true">
      <line x1="2" y1="8" x2="14" y2="8" stroke="currentColor" strokeWidth="1.6" />
      <line x1="8" y1="2" x2="8" y2="14" stroke="currentColor" strokeWidth="1.6"
            style={{
              transition: "transform 280ms cubic-bezier(.2,0,0,1), opacity 200ms",
              transformOrigin: "8px 8px",
              transform: open ? "rotate(90deg)" : "rotate(0deg)",
              opacity: open ? 0 : 1,
            }} />
    </svg>
  );
}

// ── collectSlots ──────────────────────────────────────────────
// Flattens a media tree into a sequential list of image/upload slots.
function collectSlots(media, caseId) {
  const slots = [];

  // Flat array: [{ src, alt, caption }, ...] or ["slot-id", ...]
  if (Array.isArray(media) && media.length > 0 && !("before" in media[0]) && !("after" in media[0]) && !("label" in media[0])) {
    media.forEach((item, i) => {
      if (typeof item === "string") {
        slots.push({ id: item, src: null, alt: "", caption: "", phaseLabel: null, side: null });
      } else {
        slots.push({
          id: item.id || `${caseId}-${i}`,
          src: item.src || null,
          alt: item.alt || "",
          caption: item.caption || "",
          phaseLabel: null,
          side: null,
        });
      }
    });
    return slots;
  }

  const phases = Array.isArray(media)
    ? media
    : [{ label: null, before: media && media.before, after: media && media.after }];

  phases.forEach((phase, pi) => {
    const push = (item, baseId, side) => {
      if (item === null || item === undefined) return;
      if (Array.isArray(item)) {
        item.forEach((m, k) => push(m, `${baseId}-${k}`, side));
        return;
      }
      if (typeof item === "string") {
        slots.push({ id: item, src: null, alt: "", caption: "", phaseLabel: phase.label || null, side });
        return;
      }
      slots.push({
        id: item.id || baseId,
        src: item.src || null,
        alt: item.alt || "",
        caption: item.caption || "",
        phaseLabel: phase.label || null,
        side,
      });
    };
    if (phase.before !== undefined) push(phase.before, `${caseId}-p${pi}-before`, "Before");
    if (phase.after  !== undefined) push(phase.after,  `${caseId}-p${pi}-after`,  "After");
  });
  return slots;
}

// ── Lightbox ──────────────────────────────────────────────────
function Lightbox({ slots, startIdx, onClose }) {
  const [idx, setIdx] = React.useState(startIdx);
  const total = slots.length;
  const slot = slots[idx];

  // Pre-populate uploaded images from localStorage for all DropZone slots
  const [dzCache, setDzCache] = React.useState(() => {
    const cache = {};
    slots.forEach(s => {
      if (!s.src) {
        try { const v = localStorage.getItem("dz-v1:" + s.id); if (v) cache[s.id] = v; } catch {}
      }
    });
    return cache;
  });

  React.useEffect(() => {
    const onDzUpdate = (e) => setDzCache(prev => ({ ...prev, [e.detail.id]: e.detail.src }));
    window.addEventListener("dz-update", onDzUpdate);
    return () => window.removeEventListener("dz-update", onDzUpdate);
  }, []);

  // Lock body scroll while open
  React.useEffect(() => {
    const prev = document.body.style.overflow;
    document.body.style.overflow = "hidden";
    return () => { document.body.style.overflow = prev; };
  }, []);

  const prev = React.useCallback(() => setIdx(i => (i - 1 + total) % total), [total]);
  const next = React.useCallback(() => setIdx(i => (i + 1) % total), [total]);

  React.useEffect(() => {
    const onKey = (e) => {
      if (e.key === "Escape") onClose();
      if (e.key === "ArrowLeft") prev();
      if (e.key === "ArrowRight") next();
    };
    window.addEventListener("keydown", onKey);
    return () => window.removeEventListener("keydown", onKey);
  }, [onClose, prev, next]);

  const touchStart = React.useRef(null);
  const onTouchStart = (e) => { touchStart.current = e.touches[0].clientX; };
  const onTouchEnd = (e) => {
    if (touchStart.current === null) return;
    const dx = e.changedTouches[0].clientX - touchStart.current;
    if (Math.abs(dx) > 40) dx < 0 ? next() : prev();
    touchStart.current = null;
  };

  const effectiveSrc = slot.src || dzCache[slot.id] || null;
  const label = [slot.phaseLabel, slot.side, slot.caption].filter(Boolean).join(" · ");

  return ReactDOM.createPortal(
    <div
      style={{
        position: "fixed", inset: 0, zIndex: 9100,
        background: "rgba(14,12,10,0.93)",
        display: "flex", flexDirection: "column",
        alignItems: "center", justifyContent: "center",
        padding: "56px 60px 48px",
      }}
      onClick={onClose}
      onTouchStart={onTouchStart}
      onTouchEnd={onTouchEnd}
    >
      {/* Counter */}
      {total > 1 && (
        <div style={{
          position: "absolute", top: 18, left: "50%", transform: "translateX(-50%)",
          fontFamily: "Hanken Grotesk, sans-serif", fontSize: 12, letterSpacing: 0.8,
          color: "rgba(255,255,255,0.42)", whiteSpace: "nowrap",
        }}>
          {idx + 1} / {total}
        </div>
      )}

      {/* Close */}
      <button
        type="button"
        onClick={onClose}
        style={{
          all: "unset", cursor: "pointer",
          position: "absolute", top: 14, right: 18,
          color: "rgba(255,255,255,0.55)", fontSize: 20,
          width: 36, height: 36,
          display: "flex", alignItems: "center", justifyContent: "center",
          borderRadius: "50%", background: "rgba(255,255,255,0.08)",
        }}
        aria-label="關閉"
      >×</button>

      {/* Image / DropZone */}
      <div
        onClick={e => e.stopPropagation()}
        style={{
          maxWidth: "88vw", maxHeight: "74vh",
          display: "flex", alignItems: "center", justifyContent: "center",
        }}
      >
        {effectiveSrc ? (
          <img
            src={effectiveSrc}
            alt={slot.alt || ""}
            style={{ maxWidth: "88vw", maxHeight: "74vh", objectFit: "contain", borderRadius: 4, display: "block" }}
          />
        ) : (
          <div style={{ width: "min(60vw, 420px)" }}>
            <DropZone
              id={slot.id}
              placeholder="拖入截圖"
              imageStyle={{ display: "block", width: "100%", height: "auto", borderRadius: 4, border: "none" }}
              placeholderStyle={{ display: "block", width: "100%", height: 220, background: "rgba(255,255,255,0.06)", borderRadius: 6 }}
            />
          </div>
        )}
      </div>

      {/* Caption / label */}
      {label && (
        <div style={{
          marginTop: 16,
          fontFamily: "Hanken Grotesk, sans-serif", fontStyle: "italic",
          fontSize: 12.5, color: "rgba(255,255,255,0.45)",
          textAlign: "center", letterSpacing: 0.2,
        }}>
          {label}
        </div>
      )}

      {/* Prev / Next arrows */}
      {total > 1 && (
        <>
          <button
            type="button"
            onClick={e => { e.stopPropagation(); prev(); }}
            aria-label="上一張"
            style={{
              all: "unset", cursor: "pointer",
              position: "absolute", top: "50%", left: 14,
              transform: "translateY(-50%)",
              display: "flex", alignItems: "center", justifyContent: "center",
              width: 44, height: 44, borderRadius: "50%",
              background: "rgba(255,255,255,0.1)",
              color: "#fff", fontSize: 18,
            }}
          >←</button>
          <button
            type="button"
            onClick={e => { e.stopPropagation(); next(); }}
            aria-label="下一張"
            style={{
              all: "unset", cursor: "pointer",
              position: "absolute", top: "50%", right: 14,
              transform: "translateY(-50%)",
              display: "flex", alignItems: "center", justifyContent: "center",
              width: 44, height: 44, borderRadius: "50%",
              background: "rgba(255,255,255,0.1)",
              color: "#fff", fontSize: 18,
            }}
          >→</button>
        </>
      )}
    </div>,
    document.body
  );
}

// ── AccordionItem ─────────────────────────────────────────────
function AccordionItem({ item, isFirst, open, onToggle }) {
  const bodyRef = React.useRef(null);
  const [maxH, setMaxH] = React.useState(open ? "none" : 0);

  React.useLayoutEffect(() => {
    const el = bodyRef.current;
    if (!el) return;
    if (open) {
      const h = el.scrollHeight;
      setMaxH(h);
      const t = setTimeout(() => setMaxH("none"), 360);
      return () => clearTimeout(t);
    } else {
      const h = el.scrollHeight;
      setMaxH(h);
      requestAnimationFrame(() => requestAnimationFrame(() => setMaxH(0)));
    }
  }, [open]);

  return (
    <article style={{
      borderTop: isFirst ? "1px solid var(--rule)" : "none",
      borderBottom: "1px solid var(--rule)",
      padding: "18px 0 22px 0",
      position: "relative",
      display: "grid",
      gridTemplateColumns: "44px 1fr",
      columnGap: 0,
    }}>
      {/* Left date gutter */}
      <div style={{
        fontFamily: "Hanken Grotesk, sans-serif",
        fontStyle: "italic", fontWeight: 400, fontSize: 10,
        lineHeight: 1.25, color: "var(--ink-3)",
        textAlign: "right", paddingTop: 4, paddingRight: 10,
        userSelect: "none", letterSpacing: 0.2,
      }}>
        <div>{item.year}</div>
        <div>{item.month}</div>
      </div>

      {/* Right content column */}
      <div style={{ minWidth: 0 }}>
        <button
          type="button"
          onClick={onToggle}
          aria-expanded={open}
          style={{
            all: "unset", cursor: "pointer",
            display: "grid",
            gridTemplateColumns: "16px 1fr",
            columnGap: 12, alignItems: "start", width: "100%",
          }}
        >
          <span style={{
            display: "inline-flex", alignItems: "center", justifyContent: "center",
            width: 16, height: 20, color: "var(--ink)",
          }}>
            <ToggleIcon open={open} />
          </span>
          <h3 style={{
            margin: 0, fontFamily: "var(--font-tc)", fontWeight: 700,
            fontSize: 16, lineHeight: 1.45, letterSpacing: 0,
            color: "var(--ink)", textWrap: "pretty",
          }}>
            {item.title}
          </h3>
        </button>

        <p style={{
          margin: "10px 0 0 28px", fontFamily: "var(--font-tc)",
          fontSize: 14, lineHeight: 1.65, color: "var(--ink-2)", textWrap: "pretty",
        }}>
          {item.brief}
        </p>

        <div
          ref={bodyRef}
          style={{
            maxHeight: maxH === "none" ? "none" : maxH,
            overflow: maxH === "none" ? "visible" : "hidden",
            transition: "max-height 320ms cubic-bezier(.2,0,0,1)",
            willChange: "max-height",
            marginLeft: 28,
          }}
          aria-hidden={!open}
        >
          <div style={{ paddingTop: 14 }}>
            {item.sections.map((sec, i) => (
              <section key={i} style={{ marginTop: i === 0 ? 0 : 16 }}>
                <p style={{
                  margin: "0 0 4px 0", fontFamily: "var(--font-tc)",
                  fontSize: 14, fontWeight: 500,
                  color: "var(--ink)", lineHeight: 1.6,
                }}>{sec.heading}</p>
                <ul style={{ margin: 0, paddingLeft: 18, listStyle: "disc" }}>
                  {sec.bullets.map((b, j) => (
                    <li key={j} style={{
                      marginTop: j === 0 ? 4 : 8, fontFamily: "var(--font-tc)",
                      fontSize: 14, lineHeight: 1.7,
                      color: "var(--ink-2)", textWrap: "pretty",
                    }}>
                      <strong style={{ fontWeight: 600, color: "var(--ink)" }}>{b.label}：</strong>{" "}{b.body}
                    </li>
                  ))}
                </ul>
              </section>
            ))}
            <MediaPair media={item.media} caseId={item.id} />
          </div>
        </div>
      </div>
    </article>
  );
}

// ── DropZone ──────────────────────────────────────────────────
// Accepts PNG / JPEG / WebP / AVIF / SVG / PDF (first page).
// Persists upload in localStorage by id.
const DZ_NS = "dz-v1";

async function loadPdfJs() {
  if (window.__pdfjs) return window.__pdfjs;
  const lib = await import("https://cdn.jsdelivr.net/npm/pdfjs-dist@4.0.379/build/pdf.min.mjs");
  lib.GlobalWorkerOptions.workerSrc = "https://cdn.jsdelivr.net/npm/pdfjs-dist@4.0.379/build/pdf.worker.min.mjs";
  window.__pdfjs = lib;
  return lib;
}

function DropZone({ id, placeholder, imageStyle, placeholderStyle }) {
  const key = DZ_NS + ":" + id;
  const [src, setSrc] = React.useState(() => {
    try { return localStorage.getItem(key); } catch { return null; }
  });
  const [busy, setBusy] = React.useState(false);
  const [error, setError] = React.useState(null);
  const [over, setOver] = React.useState(false);
  const fileRef = React.useRef(null);

  const save = (dataUrl) => {
    setSrc(dataUrl);
    try {
      localStorage.setItem(key, dataUrl);
      window.dispatchEvent(new CustomEvent("dz-update", { detail: { id, src: dataUrl } }));
    } catch { setError("儲存失敗（檔案過大）"); }
  };
  const clear = (e) => {
    e && e.stopPropagation();
    setSrc(null); setError(null);
    try {
      localStorage.removeItem(key);
      window.dispatchEvent(new CustomEvent("dz-update", { detail: { id, src: null } }));
    } catch {}
  };

  const ingest = async (file) => {
    if (!file) return;
    setError(null); setBusy(true);
    try {
      const name = (file.name || "").toLowerCase();
      const type = file.type || "";
      if (type === "application/pdf" || name.endsWith(".pdf")) {
        const pdfjs = await loadPdfJs();
        const buf = await file.arrayBuffer();
        const pdf = await pdfjs.getDocument({ data: new Uint8Array(buf) }).promise;
        const page = await pdf.getPage(1);
        let viewport = page.getViewport({ scale: 1.5 });
        const MAX_W = 1400;
        if (viewport.width > MAX_W) {
          viewport = page.getViewport({ scale: 1.5 * (MAX_W / viewport.width) });
        }
        const canvas = document.createElement("canvas");
        canvas.width = viewport.width;
        canvas.height = viewport.height;
        await page.render({ canvasContext: canvas.getContext("2d"), viewport }).promise;
        save(canvas.toDataURL("image/png"));
      } else if (type === "image/svg+xml" || name.endsWith(".svg")) {
        const text = await file.text();
        save("data:image/svg+xml," + encodeURIComponent(text));
      } else if (type.startsWith("image/")) {
        const reader = new FileReader();
        await new Promise((resolve, reject) => {
          reader.onload = () => { save(reader.result); resolve(); };
          reader.onerror = reject;
          reader.readAsDataURL(file);
        });
      } else {
        setError("不支援的格式");
      }
    } catch (e) {
      setError("處理失敗：" + (e.message || String(e)));
    } finally { setBusy(false); }
  };

  const onDrop = async (e) => {
    e.preventDefault();
    setOver(false);
    const f = e.dataTransfer.files && e.dataTransfer.files[0];
    await ingest(f);
  };

  if (src) {
    return (
      <div style={{ position: "relative" }}>
        <img src={src} alt="" style={imageStyle} />
        <button
          type="button"
          onClick={clear}
          style={{
            position: "absolute", top: 6, right: 6,
            border: "none", background: "rgba(0,0,0,0.55)", color: "white",
            fontFamily: "Hanken Grotesk, sans-serif",
            fontSize: 11, letterSpacing: 0.4, textTransform: "uppercase",
            padding: "4px 8px", borderRadius: 2, cursor: "pointer", opacity: 0.85,
          }}
          title="移除"
        >Remove</button>
      </div>
    );
  }

  return (
    <div
      onClick={() => fileRef.current && fileRef.current.click()}
      onDragOver={(e) => { e.preventDefault(); setOver(true); }}
      onDragLeave={() => setOver(false)}
      onDrop={onDrop}
      style={{
        ...placeholderStyle,
        cursor: "pointer",
        border: over ? "1.5px dashed var(--ink)" : "1px dashed var(--rule-soft)",
        background: over ? "var(--slot-2)" : placeholderStyle.background,
        display: "flex", flexDirection: "column",
        alignItems: "center", justifyContent: "center",
        gap: 4, padding: 12, textAlign: "center",
        color: "var(--ink-3)",
        transition: "background 120ms, border-color 120ms",
      }}
    >
      <input
        ref={fileRef}
        type="file"
        accept="image/png,image/jpeg,image/webp,image/avif,image/svg+xml,application/pdf,.svg,.pdf"
        hidden
        onChange={(e) => ingest(e.target.files && e.target.files[0])}
      />
      <span style={{ fontFamily: "var(--font-tc)", fontSize: 12, fontWeight: 500 }}>
        {busy ? "處理中…" : placeholder}
      </span>
      <span style={{ fontFamily: "Hanken Grotesk, sans-serif", fontSize: 10, letterSpacing: 0.4, opacity: 0.65 }}>
        PNG · JPEG · WebP · SVG · PDF
      </span>
      {error && (
        <span style={{ fontSize: 10, color: "#a23", marginTop: 4, maxWidth: "100%" }}>{error}</span>
      )}
    </div>
  );
}

// ── MediaThumbnail ────────────────────────────────────────────
function MediaThumbnail({ slot, dzCache, onClick }) {
  const src = slot.src || (dzCache && dzCache[slot.id]) || null;
  const parts = [slot.phaseLabel, slot.side, slot.caption].filter(Boolean);
  const label = parts.join(" · ") || (src ? "圖片" : "—");

  return (
    <button
      type="button"
      onClick={onClick}
      style={{
        all: "unset", cursor: "pointer", boxSizing: "border-box",
        width: "100%",
        display: "flex", alignItems: "center", gap: 8,
        padding: "5px 10px 5px 6px",
        borderRadius: 4,
        border: src ? "1px solid var(--rule-soft)" : "1px dashed var(--rule-soft)",
        background: "var(--paper-2)",
        transition: "background 120ms",
      }}
      onMouseEnter={e => { e.currentTarget.style.background = "var(--slot)"; }}
      onMouseLeave={e => { e.currentTarget.style.background = "var(--paper-2)"; }}
    >
      {/* Micro-thumbnail */}
      <span style={{
        width: 32, height: 22, borderRadius: 2,
        overflow: "hidden", flexShrink: 0,
        display: "flex", alignItems: "center", justifyContent: "center",
        background: src ? "#1a1814" : "rgba(17,17,17,0.06)",
      }}>
        {src
          ? <img src={src} alt="" style={{ width: "100%", height: "100%", objectFit: "cover", display: "block" }} />
          : <svg width="9" height="9" viewBox="0 0 9 9" fill="none">
              <line x1="1" y1="4.5" x2="8" y2="4.5" stroke="var(--ink-3)" strokeWidth="1" />
              <line x1="4.5" y1="1" x2="4.5" y2="8" stroke="var(--ink-3)" strokeWidth="1" />
            </svg>
        }
      </span>
      <span style={{
        fontFamily: "Hanken Grotesk, sans-serif",
        fontStyle: "italic",
        fontSize: 12, letterSpacing: 0.1, lineHeight: 1,
        color: src ? "var(--ink-2)" : "var(--ink-3)",
        overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap",
      }}>
        {label}
      </span>
    </button>
  );
}

// ── MediaPair ─────────────────────────────────────────────────
function MediaPair({ media, caseId }) {
  const [lightboxIdx, setLightboxIdx] = React.useState(null);
  const slots = React.useMemo(() => collectSlots(media, caseId), [caseId]);

  const [dzCache, setDzCache] = React.useState(() => {
    const cache = {};
    slots.forEach(s => {
      if (!s.src) {
        try { const v = localStorage.getItem("dz-v1:" + s.id); if (v) cache[s.id] = v; } catch {}
      }
    });
    return cache;
  });

  React.useEffect(() => {
    const onDzUpdate = (e) => setDzCache(prev => ({ ...prev, [e.detail.id]: e.detail.src }));
    window.addEventListener("dz-update", onDzUpdate);
    return () => window.removeEventListener("dz-update", onDzUpdate);
  }, []);

  if (!slots.length) return null;

  return (
    <div style={{ marginTop: 18 }}>
      <div style={{ display: "flex", flexDirection: "column", gap: 6 }}>
        {slots.map((slot, i) => (
          <MediaThumbnail
            key={slot.id}
            slot={slot}
            dzCache={dzCache}
            onClick={() => setLightboxIdx(i)}
          />
        ))}
      </div>
      {lightboxIdx !== null && (
        <Lightbox
          slots={slots}
          startIdx={lightboxIdx}
          onClose={() => setLightboxIdx(null)}
        />
      )}
    </div>
  );
}

// ── WebShell ──────────────────────────────────────────────────
function WebShell({ children, maxWidth = 720 }) {
  return (
    <div style={{ minHeight: "100vh", width: "100%", background: "var(--paper)", fontFamily: "var(--font-tc)" }}>
      <div style={{
        maxWidth,
        margin: "0 auto",
        padding: "clamp(40px, 6vw, 88px) clamp(20px, 4vw, 32px) clamp(60px, 8vw, 120px)",
      }}>
        {children}
      </div>
    </div>
  );
}

// ── HeroSection ───────────────────────────────────────────────
function HeroSection({ isWeb }) {
  return (
    <div style={{
      padding: isWeb ? "0 0 32px" : "10px 18px 28px",
      borderBottom: "1px solid var(--rule-soft)",
    }}>
      <h2 style={{
        fontFamily: "var(--font-tc)", fontWeight: 700,
        fontSize: isWeb ? 32 : 28,
        lineHeight: 1.25, letterSpacing: -0.3,
        color: "var(--ink)", margin: "0 0 8px",
      }}>
        顏楷杰
      </h2>
      <p style={{
        fontFamily: "Hanken Grotesk, sans-serif",
        fontSize: 11, letterSpacing: 1.4, textTransform: "uppercase",
        color: "var(--ink-3)", fontWeight: 500, margin: "0 0 14px",
      }}>
        Digital Banking Product Manager
      </p>
      <p style={{
        fontSize: isWeb ? 14 : 13.5,
        lineHeight: 1.6, color: "var(--ink-2)",
        margin: 0, maxWidth: 480,
      }}>
        專注開戶旅程優化，在法遵、風控與用戶體驗之間找到成長的平衡點。
      </p>
    </div>
  );
}

// ── PageBody ──────────────────────────────────────────────────
// items prop: array of case study objects (from data.js)
function PageBody({ items, openId, setOpenId, isWeb }) {
  const headerLabelStyle = {
    fontFamily: "Hanken Grotesk, sans-serif",
    fontSize: 11, letterSpacing: 1.4, textTransform: "uppercase",
    color: "var(--ink-3)", fontWeight: 500,
  };
  return (
    <React.Fragment>
      <HeroSection isWeb={isWeb} />

      <div style={{ padding: isWeb ? "28px 0 0" : "20px 18px 0" }}>
        <div style={{ marginBottom: 4 }}>
          <div style={headerLabelStyle}>Selected Work</div>
        </div>
      </div>

      <div style={{ padding: isWeb ? 0 : "0 18px" }}>
        {items.map((it, i) => (
          <AccordionItem
            key={it.id}
            item={it}
            isFirst={i === 0}
            open={openId === it.id}
            onToggle={() => setOpenId(openId === it.id ? null : it.id)}
          />
        ))}
      </div>

      <footer style={{
        padding: isWeb ? "32px 0 0" : "26px 24px 12px",
        fontFamily: "Hanken Grotesk, sans-serif",
        fontSize: 11, letterSpacing: 1.4, textTransform: "uppercase",
        color: "var(--ink-3)", display: "flex", justifyContent: "space-between",
      }}>
        <span>© Portfolio</span>
        <span>v 1.0</span>
      </footer>
    </React.Fragment>
  );
}

Object.assign(window, {
  PhoneFrame, ToggleIcon, AccordionItem,
  DropZone, MediaPair, WebShell, HeroSection, PageBody,
});
