// ====================================================================
// SKETCH — 手書きスクラップブック風
// Left sidebar, warm off-white (the original wireframe design)
// ====================================================================
const { useState: useStateS, useEffect: useEffectS, useMemo: useMemoS } = React;

// SK_POSTS / SK_TAGS は posts.js（build-posts-manifest.py で自動生成）から読み込む
const SK_POSTS = (typeof window !== "undefined" && window.SK_POSTS) || [];
const SK_TAGS = (typeof window !== "undefined" && window.SK_TAGS) || [];

function SkOctoLogo({ size = 36, animate = false }) {
  // 脱力系ハイテクタコ: 眠そうな半月目 + LEDほっぺ + 8本足のパチパチ波
  const legCount = 8;
  return (
    <svg width={size} height={size} viewBox="0 0 48 48" fill="none" className={animate ? "sk-logo-live" : ""}>
      {/* 頭 */}
      <circle cx="24" cy="19" r="11" stroke="currentColor" strokeWidth="2" fill="#fbf7ee" />
      {/* 目（点） */}
      <circle cx="20" cy="18" r="1.4" fill="currentColor" />
      <circle cx="28" cy="18" r="1.4" fill="currentColor" />
      {/* LEDほっぺ（アクセント色） */}
      <circle cx="15.5" cy="21.5" r="0.9" fill="var(--accent, #c0532a)" />
      <circle cx="32.5" cy="21.5" r="0.9" fill="var(--accent, #c0532a)" />
      {/* 8本足 */}
      {[...Array(legCount)].map((_, i) => (
        <path
          key={i}
          className="sk-logo-leg"
          style={{ animationDelay: `${i * 90}ms` }}
          d={`M ${10 + i*4} 29 q 1.5 3 0 6 q -1.5 3 0 6`}
          stroke="currentColor" strokeWidth="1.6" strokeLinecap="round" fill="none"
        />
      ))}
    </svg>
  );
}
function SkSquiggle({ w = 80 }) {
  return <svg width={w} height="8" viewBox="0 0 80 8"><path d="M0 4 Q 8 0 16 4 T 32 4 T 48 4 T 64 4 T 80 4" stroke="currentColor" strokeWidth="1.5" fill="none" strokeLinecap="round"/></svg>;
}
function SkPlaceholder({ label = "image", aspect = "4/3", className = "", subtle = false, img = null, alt = "" }) {
  if (img) {
    return (
      <div className={`sk-eyecatch-ph sk-eyecatch-has-img ${className}`} style={{ aspectRatio: aspect }}>
        <img src={img} alt={alt || label} loading="lazy" />
      </div>
    );
  }
  return (
    <div className={`sk-eyecatch-ph ${subtle ? "sk-eyecatch-subtle" : ""} ${className}`} style={{ aspectRatio: aspect }}>
      <div className="sk-eyecatch-corners"><i/><i/><i/><i/></div>
      <div className="sk-eyecatch-center">
        <div className="font-mono text-[11px] muted tracking-widest uppercase">{label}</div>
        <div className="font-hand text-[11px] muted mt-1">ここにアイキャッチが入ります</div>
      </div>
    </div>
  );
}
function SkKoma({ n, hint, img = null }) {
  // 画像がある場合は作画済みなのでコマ番号も hint も表示しない
  return (
    <div className="sk-manga-panel">
      {!img && <span className="sk-manga-num">{n}</span>}
      <div className="sk-manga-inner">
        {img ? (
          <img src={img} alt={hint || `panel ${n}`} className="sk-manga-img" loading="lazy" />
        ) : (
          <>
            <div className="sk-manga-sketch"></div>
            {hint && <div className="sk-manga-bubble">{hint}</div>}
          </>
        )}
      </div>
    </div>
  );
}

function SketchSidebar({ page, setPage, onTagClick }) {
  const nav = [
    { id: "home", label: "トップ", sub: "最新 & 注目バズ", icon: "⌂" },
    { id: "post", label: "記事詳細", sub: "4コマ + 本文", icon: "✎" },
    { id: "search", label: "セマンティック検索", sub: "キーワード + 意味", icon: "◎" },
    { id: "tags", label: "タグ & アーカイブ", sub: "時系列・一覧", icon: "#" },
    { id: "tools", label: "ツール", sub: "シミュレーター + 補助", icon: "⚙", href: "/tools/" },
  ];
  return (
    <aside className="sidebar-w paper-bg border-r-2 p-5 flex flex-col sticky self-start" style={{ borderColor: "var(--line)", top: 50, height: "calc(100vh - 50px)", overflow: "hidden" }}>
      <div className="flex items-center gap-3 mb-1 ink">
        <SkOctoLogo size={40} animate />
        <div>
          <div className="font-decol text-[22px] leading-none ink">octopachi</div>
          <div className="font-mono text-[10px] muted mt-1">AI / LLM / Agent 観測日誌</div>
        </div>
      </div>
      <div className="mt-2 mb-5 ink-2"><SkSquiggle /></div>
      <button onClick={() => setPage("search")} className="sketch-border-sm px-3 py-2 text-left text-sm mb-5 flex items-center justify-between">
        <span className="muted">キーワード or 意味で検索...</span>
        <kbd>⌘K</kbd>
      </button>
      <nav className="flex flex-col gap-1 mb-6">
        <div className="font-mono text-[10px] muted uppercase tracking-wider px-2 mb-1">navigate</div>
        {nav.map(n => {
          const inner = (
            <>
              <span className="font-mono text-[14px] mt-0.5" style={{ color: page === n.id ? "var(--accent)" : "var(--muted)" }}>{n.icon}</span>
              <span>
                <div className="font-hand text-[15px] leading-tight">{n.label}</div>
                <div className="font-mono text-[10px] muted">{n.sub}</div>
              </span>
            </>
          );
          const cls = `text-left px-3 py-2 rounded-md flex items-start gap-2 ${page === n.id ? "nav-pill-active" : ""}`;
          return n.href
            ? <a key={n.id} href={n.href} className={cls} style={{ textDecoration: "none", color: "inherit" }}>{inner}</a>
            : <button key={n.id} onClick={() => setPage(n.id)} className={cls}>{inner}</button>;
        })}
      </nav>
      <div className="mb-5">
        <div className="font-mono text-[10px] muted uppercase tracking-wider px-1 mb-2">popular tags</div>
        <div className="flex flex-wrap gap-1.5">
          {SK_TAGS.slice(0, 8).map(t => (
            <button key={t.name} onClick={() => onTagClick && onTagClick(t.name)} className="tag-chip text-[11px] cursor-pointer hover:bg-[var(--wash)]" type="button">
              {t.name}<span className="muted ml-1">{t.count}</span>
            </button>
          ))}
        </div>
      </div>
    </aside>
  );
}

function SketchHome({ setPage, setPostId, selectedTag, clearTag, onTagClick }) {
  const visiblePosts = selectedTag ? SK_POSTS.filter(p => (p.tags || []).includes(selectedTag)) : SK_POSTS;
  const hero = visiblePosts[0];
  if (!hero) {
    return (
      <div className="page-enter">
        <section className="sketch-border p-8 relative text-center" style={{ maxWidth: 640, margin: "0 auto" }}>
          <span className="tape" style={{ top: -10, left: "50%", transform: "translateX(-50%) rotate(-2deg)" }}></span>
          <div className="font-mono text-[11px] muted uppercase tracking-widest mb-4">system: idle</div>
          <h1 className="font-decol text-3xl ink mb-3">booting octopachi…</h1>
          <p className="ink-2 text-[14px] leading-[2] mb-6">
            まもなく最初の観測記録が届きます。<br/>
            X バズ → 記憶参照 → SEO記事 → 4コマ の自動パイプラインが記事を運んでくる予定です。
          </p>
          <div className="font-mono text-[10px] muted">
            pipeline: <span className="ink">buzz-memory-blog v5</span>
          </div>
        </section>
      </div>
    );
  }
  return (
    <div className="page-enter">
      {selectedTag && (
        <div className="sketch-border-sm p-3 mb-5 flex items-center gap-3">
          <span className="font-hand text-[13px] ink">「<span className="ink" style={{ color: "var(--accent)" }}>#{selectedTag}</span>」で絞り込み中（{visiblePosts.length} 件）</span>
          <button onClick={clearTag} className="ml-auto sketch-border-sm px-3 py-1 text-[11px] font-mono">絞り込みを解除 ✕</button>
        </div>
      )}
      <section className="grid grid-cols-1 lg:grid-cols-3 gap-5">
        <div className="lg:col-span-2 sketch-border p-5 relative">
          <span className="tape" style={{ top: -10, left: 40 }}></span>
          <div className="flex items-center gap-2 mb-3">
            <span className="font-mono text-[11px] muted">{hero.date} · {hero.readMin}min{hero.updatedAt ? ` · 更新 ${hero.updatedAt}` : ""}</span>
          </div>
          <h1 className="font-decol text-3xl md:text-4xl leading-snug ink mb-3">{hero.title}</h1>
          <p className="ink-2 text-[15px] leading-relaxed mb-4">{hero.excerpt}</p>
          <div className="grid grid-cols-4 gap-2 mb-4 sk-manga-preview">{hero.komaHint.map((k, i) => <SkKoma key={i} n={i+1} hint={k} img={hero.panels?.[i]} />)}</div>
          <div className="flex items-center justify-between">
            <div className="flex gap-1.5">{hero.tags.map(t => (
              <button key={t} onClick={(e) => { e.stopPropagation(); onTagClick && onTagClick(t); }} className="tag-chip cursor-pointer hover:bg-[var(--wash)]" type="button">#{t}</button>
            ))}</div>
            <button onClick={() => { setPostId(hero.id); setPage("post"); }} className="sketch-border-sm px-4 py-1.5 font-hand text-sm">つづきを読む →</button>
          </div>
        </div>
        <div className="flex flex-col gap-3">
          <div className="sketch-border-dashed p-3">
            <div className="font-hand text-[13px] ink-2 mb-1">📨 週イチのまとめ</div>
            <div className="font-mono text-[10px] muted mb-2">毎週日曜 23:00 配信 / 無料</div>
            <div className="flex gap-2">
              <input className="sketch-border-sm flex-1 px-2 py-1 text-[12px] bg-transparent" placeholder="you@example.com" />
              <button className="sketch-border-sm px-2 py-1 text-[12px]">購読</button>
            </div>
          </div>
        </div>
      </section>

      <section className="mt-10">
        <div className="flex items-end justify-between mb-4">
          <div>
            <h2 className="font-decol text-2xl ink"><span className="squiggle">さいきんの観測</span></h2>
            <div className="font-mono text-[11px] muted mt-1">recent — {SK_POSTS.length} posts</div>
          </div>
        </div>
        <div className="grid grid-cols-1 md:grid-cols-2 gap-5">
          {visiblePosts.map(p => (
            <div key={p.id} onClick={() => { setPostId(p.id); setPage("post"); }} className="sketch-border-sm p-4 text-left relative cursor-pointer" role="button" tabIndex={0} onKeyDown={(e) => { if (e.key === "Enter") { setPostId(p.id); setPage("post"); } }}>
              <div className="flex gap-3">
                <div className="w-20 shrink-0"><SkPlaceholder label="thumb" aspect="1/1" img={p.eyecatch} alt={p.title} /></div>
                <div className="flex-1 min-w-0">
                  <div className="font-mono text-[10px] muted mb-1">{p.date} · {p.readMin}min{p.updatedAt ? ` · 更新 ${p.updatedAt}` : ""}</div>
                  <div className="font-hand ink leading-snug text-[16px]">{p.title}</div>
                  <div className="ink-2 text-[12px] mt-1 line-clamp-2">{p.excerpt}</div>
                  <div className="mt-2 flex flex-wrap gap-1">
                    {p.tags.map(t => (
                      <button key={t} onClick={(e) => { e.stopPropagation(); onTagClick && onTagClick(t); }} className="tag-chip text-[10px] cursor-pointer hover:bg-[var(--wash)]" type="button">#{t}</button>
                    ))}
                  </div>
                </div>
              </div>
            </div>
          ))}
        </div>
      </section>
    </div>
  );
}

function SketchPost({ postId, setPage, setPostId, onTagClick }) {
  const post = SK_POSTS.find(p => p.id === postId) || SK_POSTS[0];
  // 記事が1件もない場合は home に誘導
  if (!post) {
    return (
      <div className="page-enter sketch-border p-8 text-center" style={{ maxWidth: 640, margin: "40px auto" }}>
        <div className="font-mono text-[11px] muted uppercase tracking-widest mb-3">404 / empty</div>
        <p className="ink-2 text-[14px] leading-[2] mb-4">表示できる記事がまだありません。</p>
        <button onClick={() => setPage("home")} className="sketch-border-sm px-4 py-1.5 font-hand text-sm">← トップへ</button>
      </div>
    );
  }
  const koma = post.komaHint || ["panel 1","panel 2","panel 3","panel 4"];
  const panels = post.panels || [null, null, null, null];

  // 4コマ画像の native aspect から最適レイアウトを自動選択する。
  //   aspect >= 1.8  → tall   (横長ストリップを縦積み)
  //   aspect <  0.6  → 1x4    (縦長ポートレートを横並び)
  //   それ以外       → 2x2    (ほぼ正方形の4コマ)
  // Tweaks パネル（edit mode）を触った場合はそちらが最後に data-koma を書くので自動値を上書きする。
  React.useEffect(() => {
    const a = post.panelAspect;
    if (!a) return;
    const layout = a >= 1.8 ? "tall" : a < 0.6 ? "1x4" : "2x2";
    const prev = document.documentElement.getAttribute("data-koma");
    document.documentElement.setAttribute("data-koma", layout);
    // 各記事固有のパネル aspect を CSS 変数で渡す（shell.jsx 側がフォールバック 928/288 で受ける）
    document.documentElement.style.setProperty("--panel-aspect", String(a));
    return () => {
      const t = window.__octopachiTweaks;
      if (t && t.komaLayout) document.documentElement.setAttribute("data-koma", t.komaLayout);
      else if (prev) document.documentElement.setAttribute("data-koma", prev);
      document.documentElement.style.removeProperty("--panel-aspect");
    };
  }, [post.id, post.panelAspect]);

  // body (draft.md) と 見出しリストを非同期で取得
  const [bodyHtml, setBodyHtml] = useStateS("");
  const [bodyHeadings, setBodyHeadings] = useStateS([]);

  useEffectS(() => {
    if (!post.content || !window.marked) { setBodyHtml(""); setBodyHeadings([]); return; }
    let cancelled = false;
    fetch(post.content).then(r => r.ok ? r.text() : "").then(md => {
      if (cancelled) return;
      // YAMLフロントマター削除 + 先頭H1（タイトル）削除（post.title と重複するため）
      const stripped = md
        .replace(/^---[\s\S]*?\n---\s*\n/, "")   // frontmatter
        .replace(/^\s*#\s+.+\n+/, "");            // 先頭H1（改行含む先行空白を許可）
      const html = window.marked.parse(stripped);
      // H2にIDを付与してTOC候補を抽出 + mermaid code block を div 化
      const tmp = document.createElement("div");
      tmp.innerHTML = html;
      // mermaid: <pre><code class="language-mermaid"> → <div class="mermaid">
      tmp.querySelectorAll("pre > code.language-mermaid").forEach((codeEl, i) => {
        const pre = codeEl.parentElement;
        const div = document.createElement("div");
        div.className = "mermaid";
        div.textContent = codeEl.textContent;
        div.id = `mermaid-${i}`;
        pre.replaceWith(div);
      });
      const h2s = Array.from(tmp.querySelectorAll("h2"));
      const headings = h2s.map((h, i) => {
        const id = `sec-md-${i}`;
        h.id = id;
        h.style.scrollMarginTop = "140px";
        return { id, label: h.textContent.trim() };
      });
      setBodyHtml(tmp.innerHTML);
      setBodyHeadings(headings);
    }).catch(() => { if (!cancelled) { setBodyHtml(""); setBodyHeadings([]); } });
    return () => { cancelled = true; };
  }, [post.content]);

  // mermaid 後処理（bodyHtml 更新後にレンダリング）
  useEffectS(() => {
    if (!bodyHtml || !window.mermaid) return;
    try {
      window.mermaid.initialize({ startOnLoad: false, theme: "neutral", securityLevel: "loose" });
      const nodes = document.querySelectorAll(".sk-article-body .mermaid");
      if (nodes.length) window.mermaid.run({ nodes: Array.from(nodes) });
    } catch (e) { /* noop */ }
  }, [bodyHtml]);

  // TOC は Markdown H2 のみ（構造アンカー eyecatch/koma は TOC に出さない）
  const sections = React.useMemo(() => [
    ...bodyHeadings,
  ], [bodyHeadings]);

  const [pinned, setPinned] = useStateS(false);
  const [activeId, setActiveId] = useStateS("sec-eyecatch");
  const [progress, setProgress] = useStateS(0);
  const [scrolling, setScrolling] = useStateS(false);
  const articleRef = React.useRef(null);
  const heroRef = React.useRef(null);
  const idleTimerRef = React.useRef(null);

  // Scroll effects: pin header after hero leaves, track active section, progress bar
  useEffectS(() => {
    const scroller = document.querySelector(".sketch-scroll") || window;
    const getScrollTop = () => (scroller === window ? window.scrollY : scroller.scrollTop);
    const onScroll = () => {
      const top = getScrollTop();
      // pin threshold = hero bottom
      if (heroRef.current) {
        const rect = heroRef.current.getBoundingClientRect();
        setPinned(rect.bottom < 80);
      }
      // progress
      if (articleRef.current) {
        const ar = articleRef.current;
        const total = ar.scrollHeight - (scroller === window ? window.innerHeight : scroller.clientHeight);
        const passed = scroller === window ? window.scrollY - ar.offsetTop : scroller.scrollTop - ar.offsetTop;
        setProgress(Math.max(0, Math.min(1, passed / Math.max(total, 1))));
      }
      // active section = last section whose top is above 140px
      if (sections.length) {
        let current = sections[0].id;
        for (const s of sections) {
          const el = document.getElementById(s.id);
          if (el && el.getBoundingClientRect().top < 180) current = s.id;
        }
        setActiveId(current);
      }
      // activity flag — true while scroll events fire, decays after 350ms idle
      setScrolling(true);
      if (idleTimerRef.current) clearTimeout(idleTimerRef.current);
      idleTimerRef.current = setTimeout(() => setScrolling(false), 350);
    };
    scroller.addEventListener("scroll", onScroll, { passive: true });
    onScroll();
    return () => scroller.removeEventListener("scroll", onScroll);
  }, [postId, bodyHeadings]);

  const jumpTo = (id) => {
    const el = document.getElementById(id);
    if (!el) return;
    const scroller = document.querySelector(".sketch-scroll");
    const offset = 140;
    if (scroller) {
      const rect = el.getBoundingClientRect();
      const srect = scroller.getBoundingClientRect();
      scroller.scrollBy({ top: rect.top - srect.top - offset, behavior: "smooth" });
    } else {
      window.scrollTo({ top: el.getBoundingClientRect().top + window.scrollY - offset, behavior: "smooth" });
    }
  };

  return (
    <article ref={articleRef} className="page-enter relative">
      {/* Pinned compact header */}
      <div className={`sketch-pin-header ${pinned ? "pinned" : ""}`}>
        <div className="sketch-pin-inner">
          <button onClick={() => setPage("home")} className="font-mono text-[11px] muted shrink-0">← 戻る</button>
          <div className="font-decol text-[15px] ink truncate flex-1">{post.title}</div>
          <div className="hidden md:flex items-center gap-2 shrink-0">
            {post.tags.slice(0,2).map(t => (
              <button key={t} onClick={() => onTagClick && onTagClick(t)} className="tag-chip text-[10px] cursor-pointer hover:bg-[var(--wash)]" type="button">#{t}</button>
            ))}
            <span className="font-mono text-[10px] muted">{post.readMin}min</span>
          </div>
        </div>
        <div className="sketch-progress"><div style={{ width: `${progress*100}%` }} /></div>
      </div>

      <div className="post-layout">
        {/* LEFT — TOC */}
        <aside className="post-toc">
          <div className="post-toc-inner">
            <div className="font-mono text-[10px] muted uppercase tracking-wider mb-2">contents</div>
            <ol className="space-y-1">
              {sections.map((s, i) => (
                <li key={s.id}>
                  <button onClick={() => jumpTo(s.id)} className={`toc-link ${activeId === s.id ? "toc-active" : ""}`}>
                    <span className="toc-num">{String(i+1).padStart(2,"0")}</span>
                    <span className="toc-label">{s.label}</span>
                  </button>
                </li>
              ))}
            </ol>
            {post.relatedTools && post.relatedTools.length > 0 && (
              <div className="mt-5 pt-4 border-t border-dashed" style={{ borderColor: "var(--line)" }}>
                <div className="font-mono text-[10px] muted uppercase tracking-wider mb-2">tools</div>
                <ul className="space-y-1">
                  {post.relatedTools.map((t, i) => (
                    <li key={i}>
                      <a href={t.href} className="toc-link" style={{ display: "flex", alignItems: "center", gap: 6, textDecoration: "none", color: "var(--accent)" }}>
                        <span className="font-mono text-[10px]">⚙</span>
                        <span className="toc-label">{t.label}</span>
                        <span className="font-mono text-[10px] muted ml-auto">↗</span>
                      </a>
                    </li>
                  ))}
                </ul>
              </div>
            )}
            <div className="mt-5 pt-4 border-t border-dashed" style={{ borderColor: "var(--line)" }}>
              <div className="flex items-center gap-2 mb-2">
                <div className={`sk-reading-octo ${scrolling ? "is-typing" : ""}`}>
                  <SkOctoLogo size={22} animate />
                </div>
                <div className="font-mono text-[10px] muted uppercase tracking-wider">reading</div>
              </div>
              <div className="font-mono text-[11px] ink-2">{Math.round(progress*100)}% · {post.readMin}min total</div>
              <div className="sketch-bar mt-2"><div style={{ width: `${progress*100}%` }} /></div>
            </div>
          </div>
        </aside>

        {/* CENTER — article body */}
        <div className="post-body">
          <button onClick={() => setPage("home")} className="font-mono text-[11px] muted mb-4">← トップへ戻る</button>

          <div ref={heroRef}>
            <div className="flex items-center gap-2 mb-2 flex-wrap">
              {post.tags.map(t => (
                <button key={t} onClick={() => onTagClick && onTagClick(t)} className="tag-chip cursor-pointer hover:bg-[var(--wash)]" type="button">#{t}</button>
              ))}
              <span className="font-mono text-[11px] muted ml-auto">{post.date} · {post.readMin}min{post.updatedAt ? ` · 更新 ${post.updatedAt}` : ""}</span>
            </div>
            <h1 className="font-decol text-[42px] leading-[1.2] ink mb-4">{post.title}</h1>
            <p className="ink-2 text-[16px] leading-relaxed mb-6 border-l-2 pl-4" style={{ borderColor: "var(--accent)" }}>{post.excerpt}</p>
          </div>

          {/* EYECATCH HERO — full-bleed, taped on, subtle rotation */}
          <figure id="sec-eyecatch" className="sk-eyecatch-hero mb-10" style={{ scrollMarginTop: 140 }}>
            <span className="tape" style={{ top: -12, left: "15%", width: 110, transform: "rotate(-3deg)" }}></span>
            <span className="tape" style={{ top: -12, right: "12%", width: 90, transform: "rotate(4deg)" }}></span>
            <SkPlaceholder label="eyecatch · 16:9" aspect="16/9" img={post.eyecatch} alt={post.title} />
          </figure>


          {/* 4-KOMA — single manga page */}
          <figure id="sec-koma" className="sk-manga-page mb-14" style={{ scrollMarginTop: 140 }}>
            <div className="sk-manga-header">
              <div className="font-mono text-[10px] muted tracking-widest uppercase">no.{String(SK_POSTS.indexOf(post)+1).padStart(3,"0")}</div>
            </div>
            <div className="sk-manga-grid">
              {koma.map((h, i) => <SkKoma key={i} n={i+1} hint={h} img={panels[i]} />)}
            </div>
          </figure>

          {bodyHtml ? (
            <div className="sk-article-body" dangerouslySetInnerHTML={{ __html: bodyHtml }} />
          ) : (
            <div className="sk-article-body">
              <p className="ink-2 text-[15px] leading-[2]">本文を読み込み中...（draft.md が存在しない場合はここが空のままになります）</p>
            </div>
          )}

        </div>
      </div>
    </article>
  );
}

function SketchSearch({ setPage, setPostId, onTagClick }) {
  const [q, setQ] = useStateS("");
  const [uiMode, setUiMode] = useStateS("A");
  const results = useMemoS(() => {
    const tokens = q.trim().toLowerCase().split(/\s+/).filter(Boolean);
    const score = (p) => {
      if (tokens.length === 0) return 0.5;
      const text = (p.title + " " + (p.excerpt || "") + " " + (p.tags || []).join(" ")).toLowerCase();
      let hits = 0;
      for (const t of tokens) if (text.includes(t)) hits++;
      return hits / tokens.length;
    };
    return SK_POSTS
      .map((p, i) => ({ ...p, sim: score(p), x: 25 + ((i * 17) % 50), y: 20 + ((i * 23) % 55) }))
      .filter(p => tokens.length === 0 || p.sim > 0)
      .sort((a, b) => b.sim - a.sim);
  }, [q]);

  return (
    <div className="page-enter">
      <div className="mb-5">
        <div className="font-mono text-[11px] muted mb-1">semantic search · キーワードと意味の両対応</div>
        <h1 className="font-decol text-3xl ink"><span className="squiggle">意味で探す</span></h1>
      </div>
      <div className="sketch-border p-4 mb-5">
        <div className="flex items-center gap-3">
          <span className="font-mono text-[14px] muted">◎</span>
          <input value={q} onChange={e => setQ(e.target.value)} placeholder="キーワードや文章で探す" className="flex-1 bg-transparent border-none outline-none font-hand text-[18px] ink" />
          <button onClick={() => setQ(q)} className="sketch-border-sm px-3 py-1 text-[12px] font-hand">検索</button>
        </div>
        <div className="font-mono text-[10px] muted mt-2">{q.trim() ? `${results.length} 件ヒット` : `すべての記事 ${SK_POSTS.length} 件を表示中（入力すると絞り込みます）`}</div>
      </div>
      <div className="flex items-center gap-6 mb-4 font-hand text-[14px] ink-2 border-b pb-2" style={{ borderColor: "var(--line)" }}>
        <button onClick={() => setUiMode("A")} className={uiMode === "A" ? "tab-active" : ""}>A. スコアバー表示</button>
        <button onClick={() => setUiMode("B")} className={uiMode === "B" ? "tab-active" : ""}>B. ベクトル空間マップ</button>
        <button onClick={() => setUiMode("C")} className={uiMode === "C" ? "tab-active" : ""}>C. クエリ拡張</button>
      </div>
      {uiMode === "A" && (
        <div className="space-y-3">
          {results.map((r, i) => (
            <button key={r.id} onClick={() => { setPostId(r.id); setPage("post"); }} className="sketch-border-sm p-4 text-left w-full">
              <div className="flex items-start gap-4">
                <div className="font-mono text-[11px] muted w-6">{String(i+1).padStart(2,"0")}</div>
                <div className="flex-1 min-w-0">
                  <div className="font-hand text-[17px] ink mb-1">{r.title}</div>
                  <div className="ink-2 text-[13px] line-clamp-2 mb-2">{r.excerpt}</div>
                </div>
                <div className="w-32 shrink-0">
                  <div className="font-mono text-[10px] muted mb-1 flex justify-between"><span>sim</span><span className="ink-2">{r.sim.toFixed(3)}</span></div>
                  <div className="h-2 w-full bg-[var(--bg-2)] rounded-full overflow-hidden"><div className="h-full" style={{ width: `${r.sim*100}%`, background: "var(--accent)" }} /></div>
                </div>
              </div>
            </button>
          ))}
        </div>
      )}
      {uiMode === "B" && (
        <div className="grid grid-cols-1 md:grid-cols-3 gap-4">
          <div className="md:col-span-2 sketch-border p-4 relative" style={{ height: 480 }}>
            <div className="font-mono text-[11px] muted mb-1">vector space · 2D projection (mock)</div>
            <div className="absolute inset-10 border border-dashed" style={{ borderColor: "var(--muted)" }}>
              <div className="absolute" style={{ left: "50%", top: "50%" }}>
                <div className="w-3 h-3 rounded-full" style={{ background: "var(--accent)", transform: "translate(-50%,-50%)" }}></div>
              </div>
              {results.map(r => (
                <div key={r.id} onClick={() => { setPostId(r.id); setPage("post"); }} className="absolute cursor-pointer" style={{ left: `${r.x}%`, top: `${r.y}%` }}>
                  <div className="w-2.5 h-2.5 rounded-full border-2" style={{ background: "var(--paper)", borderColor: "var(--ink)", transform: `translate(-50%,-50%) scale(${0.8+r.sim})` }}></div>
                </div>
              ))}
            </div>
          </div>
          <div className="space-y-2">
            {results.map(r => (
              <button key={r.id} onClick={() => { setPostId(r.id); setPage("post"); }} className="sketch-border-sm p-2 text-left w-full">
                <div className="font-hand text-[13px] ink line-clamp-2">{r.title}</div>
                <div className="font-mono text-[10px] muted mt-1">sim {r.sim.toFixed(3)}</div>
              </button>
            ))}
          </div>
        </div>
      )}
      {uiMode === "C" && (
        <div className="grid grid-cols-1 md:grid-cols-3 gap-5">
          <div className="md:col-span-1 space-y-4">
            <div className="sketch-border-sm p-3">
              <div className="font-mono text-[11px] muted mb-2">query expansion</div>
              <div className="flex flex-wrap gap-1.5">
                {["自己書き換え","auto-PR","self-improving","CI loop"].map(e => <span key={e} className="tag-chip">+ {e}</span>)}
              </div>
            </div>
            <div className="sketch-border-dashed p-3">
              <div className="font-hand text-[13px] ink-2">🤖 このクエリで下書き生成 →</div>
            </div>
          </div>
          <div className="md:col-span-2 space-y-3">
            {results.map(r => (
              <button key={r.id} onClick={() => { setPostId(r.id); setPage("post"); }} className="sketch-border-sm p-3 text-left w-full">
                <div className="font-mono text-[10px] muted mb-1">{r.date} · sim {r.sim.toFixed(3)}</div>
                <div className="font-hand text-[16px] ink">{r.title}</div>
              </button>
            ))}
          </div>
        </div>
      )}
    </div>
  );
}

function SketchTags({ setPage, setPostId, onTagClick }) {
  return (
    <div className="page-enter">
      <div className="mb-5">
        <h1 className="font-decol text-3xl ink"><span className="squiggle">タグ & アーカイブ</span></h1>
      </div>
      <div className="sketch-border p-5 mb-6">
        <div className="font-mono text-[11px] muted mb-3">tag cloud — クリックで絞り込み</div>
        <div className="flex flex-wrap gap-x-3 gap-y-2 items-baseline">
          {SK_TAGS.map(t => (
            <button key={t.name} onClick={() => onTagClick && onTagClick(t.name)} className="font-hand ink-2 hover:opacity-70 cursor-pointer" style={{ fontSize: `${14 + Math.min(t.count, 30)*0.7}px` }} type="button">
              #{t.name}<span className="font-mono text-[10px] muted ml-0.5">{t.count}</span>
            </button>
          ))}
        </div>
      </div>
      <div className="grid grid-cols-1 md:grid-cols-3 gap-5">
        <div className="md:col-span-2">
          <div className="font-mono text-[11px] muted mb-2">timeline</div>
          <div className="relative pl-6 border-l-2 space-y-3" style={{ borderColor: "var(--line)" }}>
            <div className="font-decol text-lg ink -ml-8 flex items-center gap-2">
              <div className="w-4 h-4 rounded-full" style={{ background: "var(--paper)", border: "2px solid var(--line)" }}></div>
              2026-04
            </div>
            {SK_POSTS.map(p => (
              <button key={p.id} onClick={() => { setPostId(p.id); setPage("post"); }} className="sketch-border-sm p-2 text-left w-full flex gap-3">
                <div className="font-mono text-[10px] muted mt-1 w-12">{p.date.slice(5)}</div>
                <div className="font-hand text-[14px] ink">{p.title}</div>
              </button>
            ))}
          </div>
        </div>
        <div>
          <div className="font-mono text-[11px] muted mb-2">calendar · 2026-04</div>
          <div className="sketch-border-sm p-3">
            <div className="grid grid-cols-7 gap-1 text-center font-mono text-[9px] muted mb-1">
              {["月","火","水","木","金","土","日"].map(d => <div key={d}>{d}</div>)}
            </div>
            <div className="grid grid-cols-7 gap-1">
              {[...Array(30)].map((_, i) => {
                const dateStr = `2026-04-${String(i+1).padStart(2,"0")}`;
                const has = SK_POSTS.some(p => p.date === dateStr);
                return (
                  <div key={i} className="aspect-square flex items-center justify-center text-[10px]" style={{ border: has ? "1.5px solid var(--line)" : "1px dashed transparent", borderRadius: "6px 4px", background: has ? "var(--wash)" : "transparent", color: has ? "var(--ink)" : "var(--muted)" }}>{i+1}</div>
                );
              })}
            </div>
          </div>
        </div>
      </div>
    </div>
  );
}

function SketchApp() {
  // 初期ステートを URL ハッシュから読む:
  //   #/post/<slug>   → page="post", postId=<slug>
  //   #/search        → page="search"
  //   #/tags          → page="tags"
  //   それ以外        → page="home"
  // 外部リンク（X/SNS）からの deep link を実現しつつ、戻るボタンも機能させる。
  const parseHash = () => {
    if (typeof window === "undefined") return { page: "home", postId: null };
    const h = (window.location.hash || "").replace(/^#\/?/, "");
    if (h.startsWith("post/")) {
      const slug = h.slice("post/".length).replace(/\/.*$/, "");
      return { page: "post", postId: slug };
    }
    if (h === "search" || h === "tags" || h === "home") return { page: h, postId: null };
    return { page: "home", postId: null };
  };
  const initial = parseHash();
  const [page, setPage] = useStateS(initial.page);
  const [postId, setPostId] = useStateS(initial.postId || "hermes-agent");
  const [selectedTag, setSelectedTag] = useStateS(null);

  const onTagClick = (tag) => {
    setSelectedTag(tag);
    setPage("home");
    if (typeof window !== "undefined") window.scrollTo(0, 0);
  };
  const clearTag = () => setSelectedTag(null);

  // page/postId → URL ハッシュに反映
  React.useEffect(() => {
    const target =
      page === "post" && postId ? `#/post/${postId}` :
      page === "home" ? "" :
      `#/${page}`;
    if ((window.location.hash || "") !== target) {
      // replaceState で hashchange イベント発火を避け、ナビゲーション履歴も汚さない
      const url = window.location.pathname + window.location.search + target;
      window.history.replaceState(null, "", url);
    }
  }, [page, postId]);

  // ブラウザの戻る/進むボタン対応
  React.useEffect(() => {
    const onPop = () => {
      const s = parseHash();
      setPage(s.page);
      if (s.postId) setPostId(s.postId);
    };
    window.addEventListener("popstate", onPop);
    window.addEventListener("hashchange", onPop);
    return () => {
      window.removeEventListener("popstate", onPop);
      window.removeEventListener("hashchange", onPop);
    };
  }, []);

  return (
    <div data-screen-label={`sketch/${page}`}>
      <style>{`
        :root {
          --bg: #f7f2e8; --bg-2: #efe8d6; --ink: #1f1a14; --ink-2: #3a332a;
          --muted: #7a6f5e; --line: #2a241c; --paper: #fbf7ee;
          --accent: #c0532a; --accent-2: #3b6b46; --accent-3: #c99a2e;
          --wash: rgba(192,83,42,0.12);
        }
        body { background: var(--bg); color: var(--ink); font-family: 'Klee One','Zen Kurenaido',sans-serif; }
        .paper-bg { background: var(--bg); background-image: radial-gradient(rgba(0,0,0,0.05) 1px, transparent 1px), radial-gradient(rgba(0,0,0,0.03) 1px, transparent 1px); background-size: 22px 22px, 11px 11px; background-position: 0 0, 11px 11px; }
        .sketch-border { border: 2px solid var(--line); border-radius: 14px 10px 16px 8px / 10px 14px 8px 16px; background: var(--paper); box-shadow: 3px 4px 0 var(--line); }
        .sketch-border-sm { border: 1.5px solid var(--line); border-radius: 10px 14px 8px 12px / 14px 10px 12px 8px; background: var(--paper); }
        .sketch-border-dashed { border: 2px dashed var(--line); border-radius: 12px 8px 14px 10px / 8px 12px 10px 14px; background: var(--paper); }
        .ink { color: var(--ink); } .ink-2 { color: var(--ink-2); } .muted { color: var(--muted); } .bg-paper { background: var(--paper); }
        .font-decol { font-family: 'Kaisei Decol', serif; }
        .font-hand { font-family: 'Klee One','Zen Kurenaido',sans-serif; }
        .font-mono { font-family: 'JetBrains Mono', monospace; }
        .squiggle { background-image: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 40 6'><path d='M0 3 Q 5 0 10 3 T 20 3 T 30 3 T 40 3' fill='none' stroke='%232a241c' stroke-width='1.4'/></svg>"); background-repeat: repeat-x; background-position: 0 100%; background-size: 40px 6px; padding-bottom: 4px; }
        .placeholder-box { background: repeating-linear-gradient(135deg, transparent 0, transparent 8px, rgba(0,0,0,0.06) 8px, rgba(0,0,0,0.06) 9px); }
        .tag-chip { display: inline-flex; align-items: center; gap: 4px; padding: 2px 10px; border: 1.5px solid var(--line); border-radius: 999px; background: var(--paper); font-size: 12px; font-family: 'JetBrains Mono', monospace; }
        .koma { background: var(--paper); border: 2px solid var(--line); border-radius: 6px 4px 6px 4px; aspect-ratio: 1/1; position: relative; overflow: hidden; }
        .grid-dot { background-image: radial-gradient(var(--muted) 0.8px, transparent 0.8px); background-size: 10px 10px; }
        .sidebar-w { width: 240px; min-width: 240px; }
        .main-max { max-width: 1080px; }

        /* ===== MOBILE: collapse sidebar, tighten padding ===== */
        @media (max-width: 820px) {
          .sidebar-w { display: none !important; }
          main.flex-1 { padding: 16px !important; }
          .sk-article-body { font-size: 15px; }
          .sk-article-body h2 { font-size: 1.3rem !important; }
          .sk-article-body h3 { font-size: 1.1rem !important; }
          .sk-article-body pre { font-size: 11px; }
          .sk-article-body figure.sk-md-figure { padding: 10px 10px 8px; }
        }
        /* Mobile top bar: minimal logo + home button when sidebar is hidden */
        .sk-mobile-bar { display: none; }
        @media (max-width: 820px) {
          .sk-mobile-bar {
            display: flex; align-items: center; justify-content: space-between;
            padding: 10px 14px; margin: -16px -16px 12px;
            border-bottom: 2px solid var(--line);
            background: var(--bg);
            position: sticky; top: 0; z-index: 50;
          }
          .sk-mobile-bar .brand { display: flex; align-items: center; gap: 8px; }
          .sk-mobile-bar .brand-name {
            font-family: 'Kaisei Decol', serif; font-size: 18px; color: var(--ink);
          }
          .sk-mobile-bar .nav-links { display: flex; gap: 10px; font-family: 'JetBrains Mono', monospace; font-size: 11px; color: var(--muted); }
          .sk-mobile-bar .nav-links button { padding: 4px 8px; border-radius: 4px; }
          .sk-mobile-bar .nav-links button.active { background: var(--wash); color: var(--ink); }
        }
        .tab-active { color: var(--ink); position: relative; }
        .tab-active::after { content: ""; position: absolute; left: 0; right: 0; bottom: -6px; height: 4px; background-image: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 40 6'><path d='M0 3 Q 5 0 10 3 T 20 3 T 30 3 T 40 3' fill='none' stroke='%23c0532a' stroke-width='2'/></svg>"); background-repeat: repeat-x; background-size: 40px 6px; }
        kbd { font-family: 'JetBrains Mono', monospace; font-size: 11px; border: 1.5px solid var(--line); border-radius: 4px; padding: 1px 5px; background: var(--paper); box-shadow: 1px 1px 0 var(--line); }
        .tape { position: absolute; width: 84px; height: 22px; background: rgba(201,154,46,0.55); border: 1px solid rgba(0,0,0,0.15); transform: rotate(-4deg); }
        .nav-pill-active { background: var(--wash); border-left: 3px solid var(--accent); color: var(--ink); }

        /* logo live: legs パチパチ wave */
        .sk-logo-live .sk-logo-leg { transform-origin: top center; transform-box: fill-box; animation: skLogoLeg 1.1s ease-in-out infinite; }
        @keyframes skLogoLeg {
          0%,100% { transform: translateY(0) rotate(-1deg); }
          50%     { transform: translateY(-1px) rotate(2deg); }
        }
        /* reading octo — types faster while scrolling */
        .sk-reading-octo { line-height: 0; }
        .sk-reading-octo.is-typing .sk-logo-leg { animation-duration: 0.22s; animation-timing-function: linear; }
        .sk-reading-octo.is-typing svg { animation: skOctoBob 0.3s ease-in-out infinite; }
        @keyframes skOctoBob {
          0%,100% { transform: translateY(0); }
          50%     { transform: translateY(-1.5px); }
        }
        .page-enter { animation: pageIn 220ms ease-out; }
        @keyframes pageIn { from { opacity: 0; transform: translateY(6px); } to { opacity: 1; transform: translateY(0); } }

        /* ===== POST LAYOUT: 2-col (TOC | body) — related cards pane removed 2026-04-23 ===== */
        .post-layout {
          display: grid;
          grid-template-columns: 220px minmax(0, 1fr);
          gap: 40px;
          align-items: start;
          position: relative;
        }
        @media (max-width: 1100px) {
          .post-layout { grid-template-columns: 200px minmax(0, 1fr); gap: 28px; }
        }
        @media (max-width: 820px) {
          .post-layout { grid-template-columns: 1fr; gap: 0; }
          .post-toc { display: none; }
        }
        .post-body { max-width: none; min-width: 0; }

        /* TOC sticky left */
        .post-toc { position: sticky; top: 110px; align-self: start; max-height: calc(100vh - 140px); overflow-y: auto; }
        .post-toc-inner { padding-right: 10px; border-right: 1px dashed var(--line); }
        .toc-link {
          width: 100%;
          display: flex; align-items: baseline; gap: 8px;
          text-align: left; padding: 6px 8px;
          border-left: 2px solid transparent;
          transition: background 0.12s, border-color 0.12s;
          border-radius: 2px;
        }
        .toc-link:hover { background: rgba(0,0,0,0.04); }
        .toc-num { font-family: 'JetBrains Mono', monospace; font-size: 10px; color: var(--muted); letter-spacing: 0.08em; }
        .toc-label { font-family: 'Kaisei Decol', serif; font-size: 14px; color: var(--ink-2); line-height: 1.35; }
        .toc-active { border-left-color: var(--accent); background: var(--wash); }
        .toc-active .toc-label { color: var(--ink); }
        .toc-active .toc-num { color: var(--accent); }
        .sketch-bar { height: 3px; background: rgba(0,0,0,0.08); position: relative; }
        .sketch-bar > div { height: 100%; background: var(--accent); transition: width 0.1s; }

        /* Pinned header */
        .sketch-pin-header {
          position: sticky; top: 0;
          margin: -32px -32px 24px;
          padding: 0 32px;
          background: transparent;
          z-index: 50;
          pointer-events: none;
          transition: background 0.2s, box-shadow 0.2s, border-color 0.2s;
          border-bottom: 1px solid transparent;
        }
        .sketch-pin-header.pinned {
          background: rgba(247,242,232,0.92);
          backdrop-filter: blur(6px);
          border-bottom-color: var(--line);
          box-shadow: 0 2px 8px rgba(0,0,0,0.04);
          pointer-events: auto;
        }
        .sketch-pin-inner {
          max-width: 1400px; margin: 0 auto;
          display: flex; align-items: center; gap: 14px;
          height: 0; overflow: hidden;
          transition: height 0.22s ease;
        }
        .sketch-pin-header.pinned .sketch-pin-inner { height: 52px; }
        .sketch-progress { height: 2px; background: transparent; position: relative; }
        .sketch-pin-header.pinned .sketch-progress { background: rgba(0,0,0,0.06); }
        .sketch-progress > div { height: 100%; background: var(--accent); transition: width 0.1s; }

        /* ===== EYECATCH & IMAGE PLACEHOLDERS ===== */
        .sk-eyecatch-ph {
          position: relative;
          background: var(--paper);
          border: 2px solid var(--line);
          border-radius: 6px 10px 4px 8px / 8px 4px 10px 6px;
          box-shadow: 3px 4px 0 var(--line);
          overflow: hidden;
          background-image:
            repeating-linear-gradient(45deg, transparent 0 12px, rgba(42,36,28,0.04) 12px 13px),
            radial-gradient(var(--muted) 0.6px, transparent 0.6px);
          background-size: auto, 14px 14px;
        }
        .sk-eyecatch-subtle { box-shadow: none; border-width: 1.5px; }
        .sk-eyecatch-has-img { background-image: none; }
        .sk-eyecatch-has-img img { width: 100%; height: 100%; object-fit: cover; display: block; }
        .sk-manga-img { position: absolute; inset: 0; width: 100%; height: 100%; object-fit: cover; display: block; }

        /* Markdown本文 — draft.md レンダリング用（デフォルトはモノ寄せ） */
        .sk-article-body { color: var(--ink); font-family: 'JetBrains Mono', 'Noto Sans JP', monospace; }
        .sk-article-body h2 {
          font-family: 'Kaisei Decol', 'Noto Serif JP', serif;
          font-size: 1.5rem; font-weight: 700; color: var(--ink);
          margin: 2.5rem 0 1rem;
          padding-bottom: 0.4rem;
          border-bottom: 2px dashed var(--line);
        }
        .sk-article-body h3 {
          font-family: 'Kaisei Decol', 'Noto Serif JP', serif;
          font-size: 1.2rem; font-weight: 600; color: var(--ink);
          margin: 1.6rem 0 0.6rem;
        }
        .sk-article-body p {
          color: var(--ink-2); font-size: 15px; line-height: 2; margin: 0.8rem 0;
        }
        .sk-article-body ul, .sk-article-body ol {
          color: var(--ink-2); font-size: 15px; line-height: 1.9;
          margin: 0.8rem 0 0.8rem 1.2rem;
        }
        .sk-article-body li { margin: 0.25rem 0; }
        .sk-article-body strong { color: var(--ink); font-weight: 700; }
        .sk-article-body a { color: var(--accent); text-decoration: underline; }
        .sk-article-body a:hover { color: var(--accent-dark, var(--accent)); }
        .sk-article-body hr {
          border: none; border-top: 1.5px dashed var(--line);
          margin: 2rem 0;
        }
        .sk-article-body blockquote {
          border-left: 3px solid var(--accent);
          padding: 0.4rem 0 0.4rem 1rem;
          margin: 1rem 0;
          background: var(--wash, rgba(0,0,0,0.02));
          color: var(--ink-2); font-style: italic;
        }
        .sk-article-body pre {
          background: var(--paper, #fdfaf1);
          border: 1.5px solid var(--line);
          border-radius: 4px;
          padding: 12px;
          font-family: 'JetBrains Mono', monospace;
          font-size: 12px;
          line-height: 1.6;
          overflow-x: auto;
          margin: 1rem 0;
        }
        .sk-article-body :not(pre) > code {
          background: var(--bg-muted, rgba(0,0,0,0.05));
          padding: 1px 6px;
          border-radius: 3px;
          font-family: 'JetBrains Mono', monospace;
          font-size: 0.85em;
        }
        .sk-article-body table {
          width: 100%;
          border-collapse: collapse;
          margin: 1rem 0;
          font-size: 14px;
        }
        .sk-article-body th, .sk-article-body td {
          border: 1.5px solid var(--line);
          padding: 8px 12px;
          text-align: left;
        }
        .sk-article-body th {
          background: var(--wash, rgba(0,0,0,0.04));
          font-weight: 700;
        }
        .sk-article-body img {
          max-width: 100%; height: auto; border-radius: 4px;
          margin: 1rem 0;
        }
        /* セクション内図解 figure — kawaii フレーム */
        .sk-article-body figure.sk-md-figure {
          margin: 2rem auto;
          max-width: 640px;
          position: relative;
          background: var(--paper, #fbf7ee);
          border: 2px solid var(--line, #2a241c);
          border-radius: 10px 6px 12px 4px / 6px 12px 4px 10px;
          padding: 14px 14px 10px;
          box-shadow: 3px 4px 0 var(--line, #2a241c);
        }
        .sk-article-body figure.sk-md-figure img {
          width: 100%; height: auto;
          border-radius: 4px;
          margin: 0 0 8px;
          display: block;
        }
        .sk-article-body figure.sk-md-figure figcaption {
          font-family: 'JetBrains Mono', monospace;
          font-size: 11px;
          color: var(--muted, #7a6f5e);
          text-align: center;
          letter-spacing: 0.02em;
          padding-top: 4px;
          border-top: 1px dashed var(--line, #2a241c);
        }
        /* compare figure — 同一プロンプトの左右並び比較用 */
        .sk-article-body figure.sk-compare-figure {
          max-width: 780px;
        }
        .sk-article-body figure.sk-compare-figure .sk-compare-pair {
          display: grid;
          grid-template-columns: 1fr 1fr;
          gap: 10px;
          margin-bottom: 8px;
        }
        .sk-article-body figure.sk-compare-figure .sk-compare-half {
          display: flex;
          flex-direction: column;
          gap: 4px;
        }
        .sk-article-body figure.sk-compare-figure .sk-compare-half img {
          width: 100%; height: auto;
          border-radius: 4px;
          margin: 0;
          display: block;
          border: 1px solid var(--line, #2a241c);
        }
        .sk-article-body figure.sk-compare-figure .sk-compare-half figcaption {
          font-family: 'JetBrains Mono', monospace;
          font-size: 11px;
          color: var(--terracotta, #c0532a);
          text-align: center;
          padding: 4px 0 0;
          border-top: none;
          letter-spacing: 0.04em;
          font-weight: 600;
        }
        .sk-article-body figure.sk-compare-figure .sk-compare-caption {
          font-family: 'JetBrains Mono', monospace;
          font-size: 11px;
          color: var(--muted, #7a6f5e);
          text-align: center;
          padding-top: 8px;
          border-top: 1px dashed var(--line, #2a241c);
          letter-spacing: 0.02em;
        }
        @media (max-width: 560px) {
          .sk-article-body figure.sk-compare-figure .sk-compare-pair {
            grid-template-columns: 1fr;
          }
        }
        /* mermaid */
        .sk-article-body .mermaid {
          margin: 1.5rem auto;
          padding: 12px;
          background: var(--paper, #fbf7ee);
          border: 1.5px solid var(--line, #2a241c);
          border-radius: 6px;
          text-align: center;
          overflow-x: auto;
        }
        .sk-article-body .mermaid svg { max-width: 100%; height: auto; }
        .sk-eyecatch-center {
          position: absolute; inset: 0;
          display: flex; flex-direction: column; align-items: center; justify-content: center;
          gap: 2px;
        }
        .sk-eyecatch-corners i {
          position: absolute; width: 14px; height: 14px; border: 1.5px solid var(--ink-2);
        }
        .sk-eyecatch-corners i:nth-child(1) { top: 8px; left: 8px; border-right: none; border-bottom: none; }
        .sk-eyecatch-corners i:nth-child(2) { top: 8px; right: 8px; border-left: none; border-bottom: none; }
        .sk-eyecatch-corners i:nth-child(3) { bottom: 8px; left: 8px; border-right: none; border-top: none; }
        .sk-eyecatch-corners i:nth-child(4) { bottom: 8px; right: 8px; border-left: none; border-top: none; }

        .sk-eyecatch-hero {
          position: relative;
          max-width: 780px;
          margin: 0 auto;
        }
        .sk-eyecatch-hero .sk-eyecatch-ph { box-shadow: 4px 6px 0 var(--line); }

        /* LEDE with drop-cap */
        .sk-lede { max-width: 680px; }
        .sk-dropcap {
          float: left;
          font-family: 'Kaisei Decol', serif;
          font-size: 62px; line-height: 0.85;
          padding: 6px 10px 0 0;
          color: var(--accent);
        }

        /* ===== 4-KOMA MANGA PAGE (single sheet) ===== */
        .sk-manga-page {
          position: relative;
          max-width: 640px;
          margin: 0 auto;
          padding: 28px 28px 20px;
          background: #fdfaf1;
          border: 1px solid var(--line);
          box-shadow: 0 1px 0 rgba(0,0,0,0.08), 2px 3px 0 rgba(0,0,0,0.05);
        }
        .sk-manga-page::before {
          content: "";
          position: absolute; inset: 8px;
          border: 1px solid rgba(42,36,28,0.15);
          pointer-events: none;
        }
        .sk-manga-header {
          text-align: center;
          padding-bottom: 14px;
          margin-bottom: 18px;
          border-bottom: 1.5px solid var(--line);
        }
        .sk-manga-grid {
          display: grid;
          grid-template-columns: 1fr;
          gap: 0;
          border: 2px solid var(--ink);
        }
        .sk-manga-panel {
          position: relative;
          aspect-ratio: 928 / 288;
          background: #fff;
          overflow: hidden;
        }
        /* thin manga gutters between vertically stacked panels */
        .sk-manga-panel:nth-child(1),
        .sk-manga-panel:nth-child(2),
        .sk-manga-panel:nth-child(3) { border-bottom: 2px solid var(--ink); }
        /* home hero: compact square tiles (no landscape stretch) */
        .sk-manga-preview .sk-manga-panel {
          aspect-ratio: 1 / 1;
          border: 1px solid var(--line);
        }
        .sk-manga-num {
          position: absolute; top: 6px; left: 8px;
          font-family: 'Kaisei Decol', serif;
          font-size: 12px;
          color: var(--ink-2);
          z-index: 2;
        }
        .sk-manga-inner { position: absolute; inset: 0; padding: 18px 16px 12px; display: flex; flex-direction: column; }
        .sk-manga-sketch {
          flex: 1;
          background-image:
            radial-gradient(rgba(0,0,0,0.12) 0.8px, transparent 0.8px);
          background-size: 8px 8px;
          opacity: 0.6;
          border-radius: 2px;
          margin-bottom: 8px;
        }
        .sk-manga-bubble {
          align-self: center;
          max-width: 90%;
          font-family: 'Klee One', sans-serif;
          font-size: 12px;
          line-height: 1.4;
          text-align: center;
          color: var(--ink);
          background: #fff;
          border: 1.5px solid var(--ink);
          border-radius: 18px;
          padding: 4px 10px;
          position: relative;
        }
        .sk-manga-footer { text-align: right; margin-top: 10px; }
      `}</style>
      <div className="flex min-h-screen paper-bg">
        <SketchSidebar page={page} setPage={setPage} onTagClick={onTagClick} />
        <main className="flex-1 p-8">
          {/* Mobile-only top bar (sidebar is hidden at <=820px) */}
          <div className="sk-mobile-bar">
            <div className="brand">
              <SkOctoLogo size={24} animate />
              <div className="brand-name">octopachi</div>
            </div>
            <div className="nav-links">
              <button onClick={() => setPage("home")} className={page === "home" ? "active" : ""}>トップ</button>
              <button onClick={() => setPage("search")} className={page === "search" ? "active" : ""}>検索</button>
              <button onClick={() => setPage("tags")} className={page === "tags" ? "active" : ""}>タグ</button>
            </div>
          </div>
          <div className="flex items-center justify-between mb-6">
            <div className="font-mono text-[11px] muted"><span className="ink-2">octopachi</span> / {page}</div>
          </div>
          <div className="main-max mx-auto" key={page}>
            {page === "home" && <SketchHome setPage={setPage} setPostId={setPostId} selectedTag={selectedTag} clearTag={clearTag} onTagClick={onTagClick} />}
            {page === "post" && <SketchPost postId={postId} setPage={setPage} setPostId={setPostId} onTagClick={onTagClick} />}
            {page === "search" && <SketchSearch setPage={setPage} setPostId={setPostId} onTagClick={onTagClick} />}
            {page === "tags" && <SketchTags setPage={setPage} setPostId={setPostId} onTagClick={onTagClick} />}
          </div>
        </main>
      </div>
    </div>
  );
}

window.SketchApp = SketchApp;
