// ═══════════════════════════════════════
//   OSS — web app
//   Junji-Ito-light: cream, ink, spiral.
//   Prototype: React via CDN + Babel standalone.
// ═══════════════════════════════════════

const { useState, useEffect, createContext, useContext } = React;

// ── HASH ROUTER ─────────────────────────
function useHashRoute() {
  const [route, setRoute] = useState(() => location.hash.replace(/^#/, "") || "/");
  useEffect(() => {
    const onChange = () => setRoute(location.hash.replace(/^#/, "") || "/");
    window.addEventListener("hashchange", onChange);
    return () => window.removeEventListener("hashchange", onChange);
  }, []);
  return route;
}
function navigate(to) {
  location.hash = to.startsWith("#") ? to : "#" + to;
}

// ── API ─────────────────────────────────
const api = {
  async get(path) {
    const r = await fetch(path, { credentials: "include" });
    if (!r.ok) throw new Error(await r.text());
    return r.json();
  },
  async post(path, body) {
    const r = await fetch(path, {
      method: "POST",
      credentials: "include",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify(body || {}),
    });
    if (!r.ok) throw new Error(await r.text());
    return r.json();
  },
};

// ── FAMILY META (mirrors db seed) ──────
const FAMILY = {
  pappa: { name: "pappa",  kanji: "父", accent: "pappa" },
  mamma: { name: "mamma",  kanji: "母", accent: "mamma" },
  ninja: { name: "ninja",  kanji: "忍", accent: "ninja" },
  ava:   { name: "ava",    kanji: "星", accent: "ava"   },
};

// ── SPIRAL (PM character motif, also loader) ──
function Spiral({ size = 48, className = "" }) {
  return (
    <svg viewBox="0 0 100 100" width={size} height={size} className={className}>
      <path
        d="M50,50 m0,-2 a2,2 0 1,1 0,4 a4,4 0 1,1 0,-8 a6,6 0 1,1 0,12 a8,8 0 1,1 0,-16 a10,10 0 1,1 0,20 a12,12 0 1,1 0,-24 a14,14 0 1,1 0,28 a16,16 0 1,1 0,-32 a18,18 0 1,1 0,36 a20,20 0 1,1 0,-40 a22,22 0 1,1 0,44 a24,24 0 1,1 0,-48"
        fill="none" stroke="#2a2825" strokeWidth="1.2" strokeLinecap="round"
      />
    </svg>
  );
}

// ── MA (conflict character motif) — two overlapping circles ──
function MaGlyph({ size = 40 }) {
  return (
    <svg viewBox="0 0 100 60" width={size * 1.5} height={size}>
      <circle cx="38" cy="30" r="22" fill="none" stroke="#2a2825" strokeWidth="1.2" />
      <circle cx="62" cy="30" r="22" fill="none" stroke="#2a2825" strokeWidth="1.2" />
    </svg>
  );
}

// ── KANJI AVATAR ────────────────────────
function Avatar({ memberId, size = 44 }) {
  const m = FAMILY[memberId] || { kanji: "?", accent: "ink-100" };
  return (
    <div
      className={`kanji flex items-center justify-center rounded-full border border-ink-100`}
      style={{
        width: size, height: size,
        fontSize: size * 0.5,
        color: `var(--tw-${m.accent}, #2a2825)`,
      }}
    >
      <span className={`text-${m.accent}`}>{m.kanji}</span>
    </div>
  );
}

// ── AUTH CONTEXT ────────────────────────
const Me = createContext(null);
const useMe = () => useContext(Me);

function Shell({ children }) {
  return (
    <div className="min-h-screen">
      <div className="max-w-2xl mx-auto px-6 py-10">{children}</div>
    </div>
  );
}

// ═══ LOGIN ════════════════════════════
function Login({ onLogin }) {
  const [loading, setLoading] = useState(null);

  const pick = async (id) => {
    setLoading(id);
    try {
      const me = await api.post("/api/auth/dev-login", { memberId: id });
      onLogin(me);
    } catch (e) {
      alert("login failed: " + e.message);
      setLoading(null);
    }
  };

  return (
    <Shell>
      <div className="flex flex-col items-center text-center pt-12">
        <Spiral size={64} />
        <h1 className="serif text-5xl mt-8 text-ink-200">oss</h1>
        <p className="text-ink-50 mt-2 text-sm tracking-wide">the space between us</p>

        <div className="divider mt-12 text-xs tracking-widest uppercase">dev mode</div>
        <p className="text-ink-50 mt-4 text-sm max-w-sm">
          who are you today? switch any time — we're not live yet.
        </p>

        <div className="grid grid-cols-2 gap-4 mt-8 w-full max-w-md">
          {Object.entries(FAMILY).map(([id, m]) => (
            <button
              key={id}
              onClick={() => pick(id)}
              disabled={loading !== null}
              className="group flex flex-col items-center py-6 border border-ink-100/30 hover:border-ink-100 transition-all bg-cream-50 hover:bg-cream-200"
            >
              <span className={`kanji text-4xl mb-2 text-${m.accent}`}>{m.kanji}</span>
              <span className="text-ink-100 tracking-wide">{m.name}</span>
              {loading === id && <span className="mt-2 text-xs text-ink-50">...</span>}
            </button>
          ))}
        </div>

        <p className="mt-12 text-xs text-ink-50/70 max-w-sm italic">
          nothing you do here is shared with anyone else yet. this is a workshop.
        </p>
      </div>
    </Shell>
  );
}

// ═══ HOME — THE FEED ═══════════════════
function Home() {
  const me = useMe();
  const m = FAMILY[me.id];
  const [moods, setMoods] = useState([]);
  const [stream, setStream] = useState(null);
  const [shipped, setShipped] = useState([]);
  const [nonce, setNonce] = useState(0);
  const [undoPost, setUndoPost] = useState(null); // {id, expiresAt}

  useEffect(() => {
    api.get("/api/moods").then(setMoods).catch(() => setMoods([]));
    api.get("/api/feed/stream").then(setStream).catch(() => setStream([]));
    api.get("/api/version").then(setShipped).catch(() => setShipped([]));
  }, [me, nonce]);

  // Auto-expire the undo toast after 10s
  useEffect(() => {
    if (!undoPost) return;
    const ms = undoPost.expiresAt - Date.now();
    if (ms <= 0) { setUndoPost(null); return; }
    const t = setTimeout(() => setUndoPost(null), ms);
    return () => clearTimeout(t);
  }, [undoPost]);

  const onPosted = (res) => {
    setNonce(n => n + 1);
    if (res?.id) setUndoPost({ id: res.id, expiresAt: Date.now() + 10000 });
  };

  const undo = async () => {
    if (!undoPost) return;
    try { await fetch(`/api/posts/${undoPost.id}`, { method: "DELETE", credentials: "include" }); }
    catch {}
    setUndoPost(null);
    setNonce(n => n + 1);
  };

  const deletePost = async (id) => {
    try { await fetch(`/api/posts/${id}`, { method: "DELETE", credentials: "include" }); }
    catch {}
    setNonce(n => n + 1);
  };

  const hour = new Date().getHours();
  const timeOfDay =
    hour < 5 ? "still night" :
    hour < 12 ? "morning" :
    hour < 17 ? "afternoon" :
    hour < 22 ? "evening" : "late";

  return (
    <Shell>
      <Header />

      {/* Light greeting strip — keeps human warmth without dominating */}
      <section className="mt-8">
        <div className="flex items-baseline gap-3">
          <p className="serif text-2xl text-ink-200">hi {m.name}</p>
          <p className="text-ink-50 text-xs italic">· {timeOfDay} in bangkok</p>
        </div>
        <p className="text-[11px] text-ink-50/70 italic mt-0.5">oss — the space between us.</p>
      </section>

      {/* Version line — shows kids their ideas are in the app */}
      {shipped.length > 0 && (
        <p className="mt-2 text-[11px] text-ink-50/80 italic">
          latest build: {shipped.map((w, i) => {
            const who = FAMILY[w.member_id]?.name || w.member_id;
            const short = w.text.length > 40 ? w.text.slice(0, 38) + "…" : w.text;
            return (
              <span key={w.id}>
                {i > 0 ? " · " : ""}
                <span className="text-ink-100">{who}</span> asked for {short}
              </span>
            );
          })}
        </p>
      )}
      {shipped.length === 0 && (
        <p className="mt-2 text-[11px] text-ink-50/60 italic">
          nothing shipped yet — the spiral (🌀) is waiting for your first idea.
        </p>
      )}

      {/* Family mood strip — collapses when no one has checked in */}
      <section className="mt-6">
        <MoodRow moods={moods} />
      </section>

      {/* Invitation to shape the app — shown until something ships, suppressed when the welcome beat already covers this in the feed */}
      {shipped.length === 0 && !(stream && stream.some(it => it.synthetic || String(it.id).startsWith("welcome-"))) && (
        <a
          href="#/wishlist"
          className="mt-6 flex items-start gap-3 border border-ink-100/20 bg-cream-50 p-4 hover:border-ink-100 transition-all"
        >
          <div className="pt-1"><Spiral size={28} /></div>
          <div className="flex-1">
            <div className="serif text-lg text-ink-200">this app is still being built.</div>
            <div className="text-sm text-ink-50 italic mt-0.5">
              tap BUILD to tell the spiral what you'd add, change, or delete. your name stays on the idea.
            </div>
          </div>
          <div className="text-ink-50 pt-2">→</div>
        </a>
      )}

      {/* Unified composer */}
      <section className="mt-8">
        <Composer onPosted={onPosted} />
      </section>

      {/* Feed stream */}
      <section className="mt-10 mb-44 pb-[env(safe-area-inset-bottom)]">
        {stream === null && <div className="pt-10 flex justify-center"><Spiral /></div>}
        {stream && stream.length === 0 && (
          <div className="text-center mt-10 space-y-2">
            <p className="text-ink-50 italic text-sm">
              nothing yet. say something above — even just how the day feels.
            </p>
            <p className="text-ink-50/70 italic text-xs">
              or go to <a href="#/wishlist" className="underline underline-offset-4 hover:text-ink-200">BUILD</a> and shape this app into something you'd actually want.
            </p>
          </div>
        )}
        {stream && stream.length > 0 && <FeedStream items={stream} onDelete={deletePost} />}
      </section>

      {/* Undo toast */}
      {undoPost && (
        <div className="fixed bottom-20 left-1/2 -translate-x-1/2 bg-ink-200 text-cream-50 px-4 py-2 shadow-lg flex items-center gap-4 text-sm z-40">
          <span>sent.</span>
          <button onClick={undo} className="underline underline-offset-4 text-cream-50 hover:text-cream-200">undo</button>
        </div>
      )}

      <BottomBar />
    </Shell>
  );
}

function Header() {
  const me = useMe();
  const m = FAMILY[me.id];
  return (
    <header className="flex items-center justify-between">
      <div className="flex items-center gap-3">
        <Avatar memberId={me.id} />
        <div>
          <div className={`kanji text-sm text-${m.accent}`}>{m.kanji}</div>
          <div className="text-ink-200 tracking-wide">{m.name}</div>
        </div>
      </div>
      <button
        onClick={async () => {
          await api.post("/api/auth/logout");
          location.reload();
        }}
        className="text-xs text-ink-50 hover:text-ink-200 underline-offset-4 hover:underline"
      >
        switch
      </button>
    </header>
  );
}

function MoodRow({ moods }) {
  const byMember = Object.keys(FAMILY).map(id => {
    const latest = moods.find(x => x.member_id === id);
    return { id, latest };
  });

  // If nobody has checked in at all, collapse to one gentle line.
  if (byMember.every(x => !x.latest)) {
    return (
      <div className="border border-dashed border-ink-100/20 py-4 px-4 text-center bg-cream-50">
        <p className="text-sm text-ink-50 italic">
          no one's checked in today yet — you could go first.
        </p>
      </div>
    );
  }

  return (
    <div className="grid grid-cols-4 gap-2">
      {byMember.map(({ id, latest }) => {
        const m = FAMILY[id];
        const empty = !latest;
        return (
          <div
            key={id}
            className={`flex flex-col items-center py-5 border border-ink-100/10 bg-cream-50 ${empty ? "opacity-60" : ""}`}
          >
            <span className={`kanji text-xl text-${m.accent}`}>{m.kanji}</span>
            {latest ? (
              <>
                <span className="mt-2 text-xl">{latest.emoji}</span>
                <span className="mt-1 text-[10px] text-ink-50 tracking-wider uppercase">
                  {latest.label}
                </span>
              </>
            ) : (
              <span className="mt-3 text-[10px] text-ink-50/60 italic">
                no check-in
              </span>
            )}
          </div>
        );
      })}
    </div>
  );
}

function QuietRoom({ title, sub, to }) {
  return (
    <a
      href={"#" + to}
      className="block py-3 px-1 border-b border-ink-100/10 hover:border-ink-100/40 transition-all group"
    >
      <div className="serif text-lg text-ink-200 group-hover:text-ink-900">{title}</div>
      <div className="text-xs text-ink-50 mt-0.5">{sub}</div>
    </a>
  );
}

// ── CLIENT-SIDE IMAGE RESIZE ─────────────
async function fileToResizedDataUrl(file, maxEdge = 1600, quality = 0.85) {
  return new Promise((resolve, reject) => {
    const img = new Image();
    const url = URL.createObjectURL(file);
    img.onload = () => {
      const scale = Math.min(1, maxEdge / Math.max(img.width, img.height));
      const w = Math.round(img.width * scale);
      const h = Math.round(img.height * scale);
      const canvas = document.createElement("canvas");
      canvas.width = w; canvas.height = h;
      canvas.getContext("2d").drawImage(img, 0, 0, w, h);
      URL.revokeObjectURL(url);
      resolve(canvas.toDataURL("image/jpeg", quality));
    };
    img.onerror = (e) => { URL.revokeObjectURL(url); reject(e); };
    img.src = url;
  });
}

// ── SHARE MOMENT (home) ──────────────────
function ShareMoment({ onShared }) {
  const [busy, setBusy] = useState(false);
  const [caption, setCaption] = useState("");
  const [preview, setPreview] = useState(null);
  const [dataUrl, setDataUrl] = useState(null);
  const fileRef = React.useRef(null);

  const onPick = async (e) => {
    const file = e.target.files?.[0];
    if (!file) return;
    try {
      const du = await fileToResizedDataUrl(file);
      setDataUrl(du);
      setPreview(du);
    } catch (err) {
      alert("couldn't read that image");
    }
  };

  const share = async () => {
    if (!dataUrl || busy) return;
    setBusy(true);
    try {
      await api.post("/api/media", { dataUrl, caption: caption.trim() });
      setDataUrl(null);
      setPreview(null);
      setCaption("");
      if (fileRef.current) fileRef.current.value = "";
      onShared && onShared();
    } catch (e) {
      alert("upload failed: " + e.message);
    } finally {
      setBusy(false);
    }
  };

  return (
    <div className="border border-ink-100/20 bg-cream-50 p-4">
      <h3 className="text-[11px] tracking-widest uppercase text-ink-50 mb-3">a moment</h3>

      {!preview && (
        <label className="block cursor-pointer">
          <input
            ref={fileRef}
            type="file"
            accept="image/*"
            capture="environment"
            className="hidden"
            onChange={onPick}
          />
          <div className="border border-dashed border-ink-100/30 hover:border-ink-100/60 p-6 text-center text-sm text-ink-50">
            tap to add a photo or drawing
            <div className="text-[11px] text-ink-50/60 mt-1 italic">
              something you saw, made, or want to remember
            </div>
          </div>
        </label>
      )}

      {preview && (
        <>
          <img src={preview} alt="preview" className="w-full max-h-[380px] object-contain bg-cream-100 border border-ink-100/10" />
          <textarea
            rows={2}
            value={caption}
            onChange={(e) => setCaption(e.target.value)}
            placeholder="a line about it... (optional)"
            className="mt-3 w-full bg-cream-50 border border-ink-100/20 focus:border-ink-100 p-3 text-ink-200 placeholder-ink-50/60 focus:outline-none text-sm"
          />
          <div className="flex justify-between mt-3">
            <button
              onClick={() => { setPreview(null); setDataUrl(null); setCaption(""); if (fileRef.current) fileRef.current.value = ""; }}
              className="text-xs text-ink-50 hover:text-ink-200 underline-offset-4 hover:underline"
            >
              pick another
            </button>
            <button onClick={share} disabled={busy} className="btn-ink text-xs disabled:opacity-40">
              {busy ? "..." : "share it"}
            </button>
          </div>
        </>
      )}
    </div>
  );
}

// ── RECENT MOMENTS STRIP ─────────────────
function RecentMoments({ refresh }) {
  const [items, setItems] = useState([]);
  useEffect(() => {
    api.get("/api/media?limit=6").then(setItems).catch(() => setItems([]));
  }, [refresh]);

  if (!items.length) return null;

  return (
    <section className="mt-12">
      <div className="flex items-baseline justify-between mb-3">
        <h2 className="text-[11px] tracking-widest uppercase text-ink-50">recent moments</h2>
        <a href="#/gallery" className="text-xs text-ink-50 hover:text-ink-200">see all →</a>
      </div>
      <div className="grid grid-cols-3 gap-1">
        {items.slice(0, 6).map(it => (
          <a key={it.id} href="#/gallery" className="block aspect-square overflow-hidden bg-cream-200">
            <img src={it.image_url} className="w-full h-full object-cover hover:opacity-90 transition-opacity" />
          </a>
        ))}
      </div>
    </section>
  );
}

// ═══ COMPOSER (unified input) ═══════════
const MOOD_OPTIONS = [
  { emoji: "🌞", label: "bright",  kanji: "陽" },
  { emoji: "☁️", label: "foggy",   kanji: "霧" },
  { emoji: "⚡", label: "wired",   kanji: "雷" },
  { emoji: "🌊", label: "wavy",    kanji: "波" },
  { emoji: "🌙", label: "quiet",   kanji: "静" },
  { emoji: "🔥", label: "lit",     kanji: "火" },
  { emoji: "🌧️", label: "heavy",   kanji: "雨" },
  { emoji: "✨", label: "soft",    kanji: "柔" },
];

function Composer({ onPosted }) {
  const me = useMe();
  const [text, setText] = useState("");
  const [audience, setAudience] = useState("everyone");
  const [mood, setMood] = useState(null);
  const [dataUrl, setDataUrl] = useState(null);
  const [preview, setPreview] = useState(null);
  const [busy, setBusy] = useState(false);
  const [expanded, setExpanded] = useState(false);
  const fileRef = React.useRef(null);

  const pickPhoto = async (e) => {
    const file = e.target.files?.[0];
    if (!file) return;
    try {
      const du = await fileToResizedDataUrl(file);
      setDataUrl(du);
      setPreview(du);
      setExpanded(true);
    } catch { alert("couldn't read that image"); }
  };

  const reset = () => {
    setText(""); setMood(null); setDataUrl(null); setPreview(null);
    setAudience("everyone"); setExpanded(false);
    if (fileRef.current) fileRef.current.value = "";
  };

  const canSend = (text.trim() || dataUrl || mood) && !busy;

  const send = async () => {
    if (!canSend) return;
    setBusy(true);
    try {
      const res = await api.post("/api/posts", {
        text: text.trim(),
        dataUrl,
        mood,
        audience,
      });
      reset();
      onPosted && onPosted(res);
    } catch (e) {
      alert("couldn't post: " + e.message);
    } finally { setBusy(false); }
  };

  const audienceLabel = {
    everyone: "everyone",
    self:     "just me",
    pappa:    "pappa",
    mamma:    "mamma",
    ninja:    "ninja",
    ava:      "ava",
  };

  return (
    <div className="border border-ink-100/20 bg-cream-50">
      {/* Collapsed view — looks like a single line, expands on focus */}
      <div className="flex items-start gap-3 p-4">
        <Avatar memberId={me.id} size={36} />
        <textarea
          rows={expanded ? 3 : 1}
          value={text}
          onChange={(e) => setText(e.target.value)}
          onFocus={() => setExpanded(true)}
          placeholder="something on your mind..."
          className="flex-1 bg-transparent text-ink-200 placeholder-ink-50/60 focus:outline-none serif text-lg resize-none"
        />
      </div>

      {/* Preview image */}
      {preview && (
        <div className="px-4 pb-3">
          <div className="relative">
            <img src={preview} className="w-full max-h-[320px] object-contain bg-cream-100 border border-ink-100/10" />
            <button
              onClick={() => { setDataUrl(null); setPreview(null); if (fileRef.current) fileRef.current.value = ""; }}
              className="absolute top-2 right-2 bg-cream-50/90 border border-ink-100/40 text-xs px-2 py-1 text-ink-100 hover:bg-ink-100 hover:text-cream-50"
            >✕</button>
          </div>
        </div>
      )}

      {/* Mood picker row */}
      {expanded && (
        <div className="px-4 pb-3">
          <div className="flex flex-wrap gap-1.5">
            {MOOD_OPTIONS.map(opt => {
              const active = mood?.label === opt.label;
              return (
                <button
                  key={opt.label}
                  onClick={() => setMood(active ? null : opt)}
                  className={`text-sm px-2.5 py-1 border transition-all ${
                    active
                      ? "bg-ink-100 text-cream-50 border-ink-100"
                      : "border-ink-100/25 text-ink-100 hover:border-ink-100"
                  }`}
                  title={opt.label}
                >
                  <span className="mr-1">{opt.emoji}</span>
                  <span className="text-[11px] tracking-wide">{opt.label}</span>
                </button>
              );
            })}
          </div>
        </div>
      )}

      {/* Controls bar */}
      {expanded && (
        <div className="border-t border-ink-100/10 px-4 py-3 flex items-center justify-between gap-3">
          <div className="flex items-center gap-3">
            <label className="cursor-pointer text-ink-50 hover:text-ink-200 text-sm" title="add photo">
              <input ref={fileRef} type="file" accept="image/*" capture="environment" className="hidden" onChange={pickPhoto} />
              📷
            </label>
            <AudienceSelect value={audience} onChange={setAudience} />
          </div>
          <div className="flex items-center gap-3">
            <button onClick={reset} className="text-xs text-ink-50 hover:text-ink-200">cancel</button>
            <button
              onClick={send}
              disabled={!canSend}
              className="btn-ink text-xs tracking-wide disabled:opacity-40"
            >
              {busy ? "..." : audience === "self" ? "keep" : "send"}
            </button>
          </div>
        </div>
      )}
    </div>
  );
}

function AudienceSelect({ value, onChange }) {
  const me = useMe();
  const options = [
    { id: "everyone", label: "→ everyone" },
    { id: "self",     label: "→ just me (journal)" },
    ...Object.keys(FAMILY).filter(id => id !== me.id).map(id => ({ id, label: `→ ${FAMILY[id].name}` })),
  ];
  return (
    <select
      value={value}
      onChange={(e) => onChange(e.target.value)}
      className="bg-cream-50 border border-ink-100/20 text-xs text-ink-100 px-2 py-1 focus:outline-none focus:border-ink-100"
    >
      {options.map(o => <option key={o.id} value={o.id}>{o.label}</option>)}
    </select>
  );
}

// ═══ FEED STREAM ═══════════════════════
function FeedStream({ items, onDelete }) {
  // Group by day buckets
  const now = new Date();
  const startOfToday = new Date(now.getFullYear(), now.getMonth(), now.getDate()).getTime();
  const startOfYesterday = startOfToday - 86400000;
  const startOfWeek = startOfToday - 6 * 86400000;

  const buckets = { today: [], yesterday: [], week: [], earlier: [] };
  for (const it of items) {
    const t = new Date(it.timestamp + (it.timestamp.endsWith("Z") ? "" : "Z")).getTime();
    if (t >= startOfToday) buckets.today.push(it);
    else if (t >= startOfYesterday) buckets.yesterday.push(it);
    else if (t >= startOfWeek) buckets.week.push(it);
    else buckets.earlier.push(it);
  }

  return (
    <div className="space-y-8">
      {buckets.today.length > 0    && <BucketSection title="today"       items={buckets.today}     onDelete={onDelete} />}
      {buckets.yesterday.length > 0 && <BucketSection title="yesterday"  items={buckets.yesterday} onDelete={onDelete} />}
      {buckets.week.length > 0     && <BucketSection title="this week"   items={buckets.week}      onDelete={onDelete} />}
      {buckets.earlier.length > 0  && <BucketSection title="earlier"     items={buckets.earlier}   onDelete={onDelete} />}
    </div>
  );
}

function BucketSection({ title, items, onDelete }) {
  return (
    <div>
      <div className="divider text-[11px] tracking-widest uppercase mb-4"><span>{title}</span></div>
      <div className="space-y-4">
        {items.map(it => <FeedCard key={it.id} item={it} onDelete={onDelete} />)}
      </div>
    </div>
  );
}

function FeedCard({ item, onDelete }) {
  const me = useMe();
  const canDelete = !item.synthetic && !String(item.id).startsWith("welcome-") && item.from_id === me.id && onDelete;
  const handleDelete = () => {
    if (!canDelete) return;
    if (!confirm("delete this post? can't undo after this.")) return;
    onDelete(item.id);
  };

  // Character beats render differently: no avatar header, italic, spiral marker.
  if (item.type === "character_beat") {
    const t = new Date(item.timestamp + (item.timestamp.endsWith("Z") ? "" : "Z"));
    const timeStr = t.toLocaleTimeString([], { hour: "numeric", minute: "2-digit" }).toLowerCase();
    return (
      <article className="border border-ink-100/15 bg-cream-100/60 px-4 py-3 flex items-start gap-3">
        <div className="pt-0.5"><Spiral size={20} /></div>
        <div className="flex-1">
          <div className="text-ink-200 serif text-base leading-snug">{item.text}</div>
          <div className="text-[11px] text-ink-50/70 mt-1">{timeStr} · the app</div>
        </div>
      </article>
    );
  }

  const fam = FAMILY[item.from_id] || { name: item.from_id, kanji: "?", accent: "ink-100" };
  const isJournal = item.to_id === "self";
  const isTargeted = item.to_id && item.to_id !== "self";
  const targetFam = isTargeted ? FAMILY[item.to_id] : null;
  const isMe = item.from_id === me.id;

  const time = new Date(item.timestamp + (item.timestamp.endsWith("Z") ? "" : "Z"));
  const timeStr = time.toLocaleTimeString([], { hour: "numeric", minute: "2-digit" }).toLowerCase();

  return (
    <article className={`border bg-cream-50 ${isJournal ? "border-dashed border-ink-100/25" : "border-ink-100/15"}`}>
      <header className="flex items-center gap-3 px-4 pt-3">
        <Avatar memberId={item.from_id} size={32} />
        <div className="flex-1 min-w-0">
          <div className="text-sm text-ink-200 tracking-wide">
            {isMe ? "you" : fam.name}
            {isJournal && <span className="ml-2 text-[10px] text-ink-50/70 tracking-widest uppercase">· private</span>}
            {isTargeted && <span className="ml-2 text-xs text-ink-50">→ {targetFam?.name || item.to_id}</span>}
          </div>
          <div className="text-[11px] text-ink-50/70">{timeStr}</div>
        </div>
        {item.feeling_emoji && (
          <div className="text-sm flex items-center gap-1 text-ink-50">
            <span>{item.feeling_emoji}</span>
            <span className="text-[10px] tracking-wider uppercase">{item.feeling_label}</span>
          </div>
        )}
        {canDelete && (
          <button
            onClick={handleDelete}
            className="text-ink-50/60 hover:text-ink-200 text-xs ml-2"
            title="delete"
          >×</button>
        )}
      </header>

      {item.image_url && (
        <div className="mt-3">
          <img src={item.image_url} className="w-full max-h-[420px] object-contain bg-cream-100" />
        </div>
      )}

      {item.text && item.text !== "shared something" && (
        <div className={`px-4 py-3 ${item.image_url ? "" : "pt-2"} text-ink-200 serif text-lg leading-snug whitespace-pre-wrap`}>
          {item.text}
        </div>
      )}

      {!item.text && !item.image_url && item.feeling_emoji && (
        <div className="px-4 py-3 text-ink-50 text-sm italic">
          checked in — {item.feeling_label}
        </div>
      )}
    </article>
  );
}

// ═══ BOTTOM BAR — character rooms ══════
function BottomBar() {
  const items = [
    { to: "/",          glyph: "家", label: "home",     title: "home" },
    { to: "/navigator", glyph: "灯", label: "think",    title: "a place to think" },
    { to: "/bridge",    glyph: "橋", label: "words",    title: "help with words" },
    { to: "/ma",        glyph: "結", label: "untangle", title: "untangle something" },
    { to: "/wishlist",  glyph: "渦", label: "build",    title: "wishlist — build the app with the family" },
  ];
  const route = useHashRoute();
  return (
    <nav className="fixed bottom-0 left-0 right-0 bg-cream-100/95 backdrop-blur border-t border-ink-100/15 pb-[env(safe-area-inset-bottom)]">
      <div className="max-w-2xl mx-auto grid grid-cols-5">
        {items.map(it => {
          const active =
            route === it.to ||
            (it.to === "/" && (route === "" || route === "/")) ||
            (it.to === "/wishlist" && route === "/pm");
          return (
            <a
              key={it.to}
              href={"#" + it.to}
              title={it.title}
              className={`py-2.5 flex flex-col items-center gap-0.5 transition-colors ${
                active ? "text-ink-900" : "text-ink-50 hover:text-ink-100"
              }`}
            >
              <span className="text-lg kanji leading-none">{it.glyph}</span>
              <span className="text-[9px] tracking-widest uppercase">{it.label}</span>
            </a>
          );
        })}
      </div>
    </nav>
  );
}

// ── GALLERY PAGE ─────────────────────────
function Gallery() {
  const [items, setItems] = useState(null);
  useEffect(() => { api.get("/api/media?limit=60").then(setItems); }, []);

  return (
    <Shell>
      <Header />
      <div className="mt-10 flex items-baseline justify-between">
        <h1 className="serif text-3xl text-ink-200">moments</h1>
        <a href="#/" className="text-xs text-ink-50 hover:text-ink-200">← home</a>
      </div>
      <p className="text-sm text-ink-50 italic mt-1">what this family saw, made, remembered.</p>

      {items === null && (
        <div className="pt-16 flex justify-center"><Spiral /></div>
      )}
      {items && items.length === 0 && (
        <p className="mt-12 text-ink-50 italic">nothing yet. share a moment from home.</p>
      )}
      {items && items.length > 0 && (
        <div className="grid grid-cols-2 md:grid-cols-3 gap-1 mt-6">
          {items.map(it => {
            const fam = FAMILY[it.from_id];
            return (
              <div key={it.id} className="bg-cream-50 border border-ink-100/10">
                <img src={it.image_url} className="w-full aspect-square object-cover" />
                <div className="p-2 text-xs text-ink-50 flex items-center justify-between">
                  <span className={`kanji text-${fam?.accent || "ink-100"}`}>{fam?.kanji || "?"}</span>
                  <span className="truncate ml-2">{it.text && it.text !== "shared something" ? it.text : ""}</span>
                </div>
              </div>
            );
          })}
        </div>
      )}
    </Shell>
  );
}

// ═══ PM ROOM — kids (and anyone) shape the app ═══
function PMRoom() {
  const me = useMe();
  const [opener, setOpener] = useState("");
  const [turns, setTurns] = useState([]); // [{role:"user"|"pm", text}]
  const [input, setInput] = useState("");
  const [busy, setBusy] = useState(false);

  useEffect(() => {
    api.get("/api/pm/opener").then(r => setOpener(r.text)).catch(() => setOpener("tell me what to build."));
  }, []);

  const send = async () => {
    const text = input.trim();
    if (!text || busy) return;
    setBusy(true);
    setTurns(t => [...t, { role: "user", text }]);
    setInput("");
    try {
      const r = await api.post("/api/pm/wish", { text });
      setTurns(t => [...t, { role: "pm", text: r.reply }]);
    } catch (e) {
      setTurns(t => [...t, { role: "pm", text: "couldn't save that one. try again?" }]);
    } finally { setBusy(false); }
  };

  return (
    <Shell>
      <Header />
      <div className="mt-8 flex items-baseline justify-between">
        <div>
          <h1 className="serif text-3xl text-ink-200 flex items-center gap-3">
            <Spiral size={32} /> the spiral
          </h1>
          <p className="text-sm text-ink-50 italic mt-1">
            this is the room where ninja, ava, mamma, pappa shape the app.
          </p>
        </div>
        <a href="#/" className="text-xs text-ink-50 hover:text-ink-200">← home</a>
      </div>

      <div className="mt-2">
        <a href="#/wishlist" className="text-xs tracking-widest uppercase text-ink-50 hover:text-ink-200">
          see the wishlist →
        </a>
      </div>

      {/* Conversation */}
      <div className="mt-8 space-y-4">
        {opener && (
          <PMBubble role="pm" text={opener} />
        )}
        {turns.map((t, i) => <PMBubble key={i} role={t.role} text={t.text} />)}
      </div>

      {/* Composer */}
      <div className="mt-6 border border-ink-100/20 bg-cream-50 p-3 mb-44 pb-[env(safe-area-inset-bottom)]">
        <textarea
          rows={2}
          value={input}
          onChange={(e) => setInput(e.target.value)}
          placeholder="something this app should do, change, or stop doing..."
          className="w-full bg-transparent text-ink-200 placeholder-ink-50/60 focus:outline-none serif text-lg resize-none"
        />
        <div className="flex justify-between items-center mt-2">
          <p className="text-[11px] text-ink-50/70 italic">
            your name goes on the card. everyone will see it's your idea.
          </p>
          <button
            onClick={send}
            disabled={!input.trim() || busy}
            className="btn-ink text-xs tracking-wide disabled:opacity-40"
          >
            {busy ? "..." : "drop it in"}
          </button>
        </div>
      </div>

      <BottomBar />
    </Shell>
  );
}

function PMBubble({ role, text }) {
  if (role === "user") {
    const me = useMe();
    return (
      <div className="flex gap-3 justify-end">
        <div className="bg-cream-200 border border-ink-100/15 px-4 py-2 max-w-[80%] text-ink-200 serif text-lg">
          {text}
        </div>
        <Avatar memberId={me.id} size={32} />
      </div>
    );
  }
  return (
    <div className="flex gap-3">
      <div className="pt-1"><Spiral size={24} /></div>
      <div className="bg-cream-50 border border-ink-100/20 px-4 py-2 max-w-[80%] text-ink-200 serif text-lg whitespace-pre-wrap">
        {text}
      </div>
    </div>
  );
}

// ═══ WISHLIST — the board everyone can see ═══
const WISHLIST_COLUMNS = [
  { id: "new",      label: "ideas",     hint: "fresh from the family" },
  { id: "next",     label: "up next",   hint: "pappa picked these" },
  { id: "shipping", label: "building",  hint: "coming soon" },
  { id: "live",     label: "shipped",   hint: "in the app now" },
];

function Wishlist() {
  const me = useMe();
  const [items, setItems] = useState(null);
  const [nonce, setNonce] = useState(0);
  const isParent = me.role === "parent";

  useEffect(() => { api.get("/api/wishlist").then(setItems).catch(() => setItems([])); }, [nonce]);

  const upvote = async (id) => {
    try { await api.post(`/api/wishlist/${id}/upvote`); setNonce(n => n + 1); }
    catch {}
  };
  const move = async (id, status) => {
    try { await api.post(`/api/wishlist/${id}/status`, { status }); setNonce(n => n + 1); }
    catch {}
  };

  return (
    <Shell>
      <Header />
      <div className="mt-8 flex items-baseline justify-between">
        <div>
          <h1 className="serif text-3xl text-ink-200">the wishlist</h1>
          <p className="text-sm text-ink-50 italic mt-1">
            every idea the family has put in. your name stays on it.
          </p>
        </div>
        <a href="#/pm" className="text-xs text-ink-50 hover:text-ink-200">+ new idea →</a>
      </div>

      {items === null && <div className="mt-20 flex justify-center"><Spiral /></div>}
      {items && items.length === 0 && (
        <div className="mt-20 text-center">
          <Spiral size={40} className="mx-auto" />
          <p className="mt-4 text-ink-50 italic text-sm">no ideas yet. someone has to go first.</p>
          <a href="#/pm" className="btn-ink text-xs inline-block mt-4">open the spiral</a>
        </div>
      )}

      {items && items.length > 0 && (
        <div className="mt-8 space-y-8 mb-44 pb-[env(safe-area-inset-bottom)]">
          {WISHLIST_COLUMNS.map(col => {
            const rows = items.filter(x => x.status === col.id);
            if (rows.length === 0 && col.id !== "new") return null;
            return (
              <section key={col.id}>
                <div className="flex items-baseline justify-between mb-3">
                  <h2 className="text-[11px] tracking-widest uppercase text-ink-50">{col.label}</h2>
                  <span className="text-[11px] text-ink-50/60 italic">{col.hint}</span>
                </div>
                {rows.length === 0 && (
                  <p className="text-xs text-ink-50/60 italic pl-1">empty</p>
                )}
                <div className="space-y-2">
                  {rows.map(w => (
                    <WishCard
                      key={w.id}
                      wish={w}
                      isParent={isParent}
                      onUpvote={() => upvote(w.id)}
                      onMove={(s) => move(w.id, s)}
                    />
                  ))}
                </div>
              </section>
            );
          })}
        </div>
      )}

      <BottomBar />
    </Shell>
  );
}

function WishCard({ wish, isParent, onUpvote, onMove }) {
  const fam = FAMILY[wish.member_id] || { name: wish.member_id, kanji: "?", accent: "ink-100" };
  const statusIdx = WISHLIST_COLUMNS.findIndex(c => c.id === wish.status);
  const next = WISHLIST_COLUMNS[statusIdx + 1];
  const prev = WISHLIST_COLUMNS[statusIdx - 1];

  return (
    <article className="border border-ink-100/15 bg-cream-50 p-3 flex gap-3">
      <div className="flex flex-col items-center pt-0.5">
        <span className={`kanji text-lg text-${fam.accent}`}>{fam.kanji}</span>
        <button
          onClick={onUpvote}
          className="mt-2 text-xs text-ink-50 hover:text-ink-200 flex flex-col items-center"
          title="upvote"
        >
          <span>▲</span>
          <span className="text-[10px]">{wish.upvotes || 0}</span>
        </button>
      </div>
      <div className="flex-1 min-w-0">
        <div className="text-ink-200 serif text-lg leading-snug">{wish.text}</div>
        <div className="text-[11px] text-ink-50 mt-1">— {fam.name}</div>

        {/* Stage journey — visible to everyone, so the author sees motion */}
        {wish.status !== "dismissed" && (
          <div className="mt-2 flex items-center gap-1.5">
            {WISHLIST_COLUMNS.map((col, i) => (
              <div
                key={col.id}
                title={col.label}
                className={`h-1.5 flex-1 ${
                  i < statusIdx ? "bg-ink-100" :
                  i === statusIdx ? "bg-ink-200" :
                  "bg-ink-100/15"
                }`}
              />
            ))}
            <span className="text-[10px] text-ink-50/70 ml-1 tracking-widest uppercase whitespace-nowrap">
              {statusIdx + 1}/{WISHLIST_COLUMNS.length}
            </span>
          </div>
        )}

        {isParent && (
          <div className="mt-2 flex flex-wrap gap-2">
            {prev && (
              <button onClick={() => onMove(prev.id)} className="text-[10px] tracking-widest uppercase text-ink-50 hover:text-ink-200 border border-ink-100/20 px-2 py-0.5">
                ← {prev.label}
              </button>
            )}
            {next && (
              <button onClick={() => onMove(next.id)} className="text-[10px] tracking-widest uppercase text-ink-100 hover:bg-ink-100 hover:text-cream-50 border border-ink-100/40 px-2 py-0.5">
                {next.label} →
              </button>
            )}
            <button onClick={() => onMove("dismissed")} className="text-[10px] tracking-widest uppercase text-ink-50/70 hover:text-ink-200 px-2 py-0.5">
              skip
            </button>
          </div>
        )}
      </div>
    </article>
  );
}

// ═══ ROOT ═══════════════════════════════
// A room that isn't fully built yet — but still captures what the kid wants in it.
function RoomUnderConstruction({ title, sub, category }) {
  const [input, setInput] = useState("");
  const [busy, setBusy] = useState(false);
  const [sent, setSent] = useState(false);

  const send = async () => {
    const text = input.trim();
    if (!text || busy) return;
    setBusy(true);
    try {
      await api.post("/api/pm/wish", { text, category });
      setInput("");
      setSent(true);
    } catch { alert("couldn't save — try again?"); }
    finally { setBusy(false); }
  };

  return (
    <Shell>
      <Header />
      <div className="mt-10 flex items-baseline justify-between">
        <h1 className="serif text-3xl text-ink-200">{title}</h1>
        <a href="#/" className="text-xs text-ink-50 hover:text-ink-200">← home</a>
      </div>
      <p className="text-sm text-ink-50 italic mt-2">{sub}</p>

      <div className="mt-10 flex justify-center"><Spiral size={48} /></div>
      <p className="text-center text-xs text-ink-50/70 italic mt-4">
        this room isn't built yet. while you wait —<br/>
        tell the spiral what it should do, and your name goes on the card.
      </p>

      <div className="mt-8 border border-ink-100/20 bg-cream-50 p-3 mb-44 pb-[env(safe-area-inset-bottom)]">
        <textarea
          rows={3}
          value={input}
          onChange={(e) => setInput(e.target.value)}
          placeholder={`what would this room do for you?`}
          className="w-full bg-transparent text-ink-200 placeholder-ink-50/60 focus:outline-none serif text-lg resize-none"
        />
        <div className="flex justify-between items-center mt-2">
          {sent ? (
            <p className="text-xs text-ink-100 italic">logged. <a href="#/wishlist" className="underline underline-offset-4">see the wishlist →</a></p>
          ) : (
            <p className="text-[11px] text-ink-50/70 italic">goes to 渦 BUILD with your name.</p>
          )}
          <button onClick={send} disabled={!input.trim() || busy} className="btn-ink text-xs disabled:opacity-40">
            {busy ? "..." : "drop it in"}
          </button>
        </div>
      </div>

      <BottomBar />
    </Shell>
  );
}

function App() {
  const [me, setMe] = useState(undefined); // undefined = loading, null = not logged in
  const route = useHashRoute();

  useEffect(() => {
    api.get("/api/me").then(setMe).catch(() => setMe(null));
  }, []);

  if (me === undefined) {
    return (
      <div className="min-h-screen flex items-center justify-center">
        <Spiral size={48} />
      </div>
    );
  }

  if (me === null) return <Login onLogin={setMe} />;

  let page;
  if (route === "/gallery")        page = <Gallery />;
  else if (route === "/navigator") page = <RoomUnderConstruction title="a place to think" sub="a quiet space to work something out, just with you and the app." category="navigator" />;
  else if (route === "/bridge")    page = <RoomUnderConstruction title="help with words" sub="for finding the way in to a hard conversation with someone." category="bridge" />;
  else if (route === "/ma")        page = <RoomUnderConstruction title="untangle something" sub="when something's knotted between you and another person in the family." category="ma" />;
  else if (route === "/pm")        page = <PMRoom />;
  else if (route === "/wishlist")  page = <Wishlist />;
  else                             page = <Home />;

  return <Me.Provider value={me}>{page}</Me.Provider>;
}

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