/* ============================================================================
   app.jsx — CandyFactory Blog · sober editorial register
   Matches v2 parlor production: clean highlight blocks (not rotated stickers),
   single accent per screen, generous whitespace, quiet metadata.
   ============================================================================ */

const { useState, useEffect, useMemo, useRef } = React;

// ─── Hash routing ──────────────────────────────────────────────────────────
function parseHash() {
  const h = window.location.hash.replace(/^#/, '') || '/';
  const [path, qs] = h.split('?');
  const parts = path.split('/').filter(Boolean);
  const q = new URLSearchParams(qs || '');
  if (parts.length === 0) return { view: 'index' };
  if (parts[0] === 'post')   return { view: 'post',   slug: parts[1] };
  if (parts[0] === 'tag')    return { view: 'tag',    tag: parts[1] };
  if (parts[0] === 'author') return { view: 'author', author: parts[1] };
  if (parts[0] === 'search') return { view: 'search', q: q.get('q') || '', tag: q.get('tag') || '', author: q.get('author') || '' };
  return { view: 'index' };
}
function nav(href) { window.location.hash = href; window.scrollTo({ top: 0, behavior: 'instant' }); }

// ─── Per-slug PATH bootstrap (BON-1355 · T13) ──────────────────────────────
// A direct hit on /blog/<slug>/ is served the SPA shell via a vercel.json
// rewrite (approach (a)). The SPA is hash-routed, so without this a path hit
// would render the index. This mirrors the pure logic unit-tested in
// scripts/lib/blog-slug-route.mjs (kept in sync; the SPA loads via <script>
// tags, not ESM, so the logic is duplicated rather than imported).
const SLUG_RE = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
function slugFromPath(pathname) {
  if (typeof pathname !== 'string' || !pathname.startsWith('/blog/')) return null;
  const segments = pathname.slice('/blog/'.length).split('/').filter(Boolean);
  if (segments.length !== 1) return null;          // listing or nested route
  const seg = segments[0];
  if (seg.includes('.')) return null;              // a file (rss.xml), not a post
  if (!SLUG_RE.test(seg)) return null;             // malformed / unsafe
  return seg;
}
// If we landed on /blog/<slug>/ with no hash route yet, bootstrap the post view
// and mutate the canonical so the served shell honestly represents the slug
// (acceptance criterion 2; canonical-mutation-post-hydration pattern per BON-1351).
function applyPathSlugBootstrap() {
  if (window.location.hash) return;                // hash already drives routing
  const slug = slugFromPath(window.location.pathname);
  if (!slug) return;
  const canonical = `https://candyfactory.ai/blog/${slug}`;
  let link = document.querySelector('link[rel="canonical"]');
  if (!link) {
    link = document.createElement('link');
    link.setAttribute('rel', 'canonical');
    document.head.appendChild(link);
  }
  link.setAttribute('href', canonical);
  // Drive the hash router to the post without adding a history entry.
  window.history.replaceState(null, '', window.location.pathname + '#/post/' + slug);
}
applyPathSlugBootstrap();

// Map a tag color name → the actual --brand-* variable.
const BRAND_VAR = { pink: 'a', butter: 'd', mint: 'b', sky: 'c', cherry: 'hot', grape: 'pop' };
const brandColor = (color) => `var(--brand-${BRAND_VAR[color] || 'hot'})`;

// ─── Headline highlight (baked sober default; was the Tweaks color picker) ──
const HIGHLIGHT_COLOR = '#5fb8ff';

// ─── Top rail (parlor status strip — calmer than the homepage version) ─────
function Rail() {
  return (
    <div className="rail">
      <div className="lights"><span className="light r"></span><span className="light y"></span><span className="light g"></span></div>
      <span><b style={{color:'var(--ink)'}}>CANDYFACTORY</b> · THE PARLOR</span>
      <div className="sep"></div>
      <span>BLOG · <span className="hot">BATCH 01 SHIPPING · BONFIRE v0.9</span></span>
      <div className="sep"></div>
      <a className="badge" href="#/" style={{cursor:'pointer', textDecoration:'none', color:'var(--ink)', display:'inline-block'}}>LINE · ON</a>
    </div>
  );
}

// ─── Blog sub-rail ─────────────────────────────────────────────────────────
// ─── Highlight block — sober replacement for rotated sticker em ────────────
// Accepts a brand color name OR a hex code (so the Tweaks color picker can drive it).
const HEX_TO_NAME = {
  '#5fb8ff': 'sky',    '#ffd84d': 'butter', '#ff5fa2': 'pink',
  '#5fd4c4': 'mint',   '#a96bff': 'grape',  '#ff3b6b': 'cherry',
};
function Highlight({ children, color = 'sky', size }) {
  const name = HEX_TO_NAME[(color || '').toLowerCase()] || color || 'sky';
  const map = { sky: '', butter: 'butter', pink: 'pink', mint: 'mint', grape: 'grape', cherry: 'cherry' };
  return <em className={"hl " + (map[name] || '') + (size === 'small' ? ' small' : '')}>{children}</em>;
}

// ─── Index (blog home) ─────────────────────────────────────────────────────
function IndexView({ highlightColor }) {
  const featured = POSTS.find(p => p.featured) || POSTS[0];
  const rest = POSTS.filter(p => p !== featured);
  return (
    <main className="stage" data-screen-label="01 Blog Index">
      <header className="masthead">
        <div className="eyebrow"><span className="pulse"></span>PARLOR LOG · POSTED FROM THE FLOOR</div>
        <div className="row">
          <h1 className="title">
            Notes from<br/>
            the <Highlight color={highlightColor}>conveyor</Highlight>.
          </h1>
          <div>
            <p className="dek">
              Engineering deep-dives, release receipts, and the occasional essay on why the factory
              is <b>visible by design</b>. Posted from the floor — never marketing soup.
            </p>
          </div>
        </div>
        <div className="strap">
          <span><span className="dot">●</span> <b>{POSTS.length}</b> POSTS · <b>{Object.keys(TAGS).length}</b> CATEGORIES</span>
          <span className="muted">·</span>
          <span>SINCE 2025 · BATCH 0019</span>
          <span className="muted">·</span>
          <a onClick={() => nav('/search')}>full archive →</a>
        </div>
      </header>

      <div className="layout">
        <div>
          <FeatureCard post={featured} />
          <div className="post-list">
            {rest.map(p => <PostRow key={p.slug} post={p} />)}
          </div>
        </div>
        <aside className="side">
          <TagCloud />
          <SignupCard />
          <ArchiveList />
        </aside>
      </div>
    </main>
  );
}

function FeatureCard({ post }) {
  const t = TAGS[post.tag];
  const a = AUTHORS[post.author];
  return (
    <article className="feature" onClick={() => nav('/post/' + post.slug)} style={{cursor:'pointer'}}>
      <div className="label-tab">FEATURED BATCH</div>
      <div className="visual">
        <FeatureMark tag={post.tag}/>
      </div>
      <div className="copy">
        <div style={{
          display:'flex', gap:14, alignItems:'center', flexWrap:'wrap',
          fontFamily:'var(--mono)', fontSize:11, letterSpacing:'0.24em', textTransform:'uppercase',
          color:'var(--ink-soft)',
        }}>
          <span style={{
            color:'var(--ink)', borderBottom: `2px solid ${brandColor(t.color)}`,
            paddingBottom:1, fontWeight:700,
          }} onClick={e => { e.stopPropagation(); nav('/tag/' + t.id); }}>{t.label}</span>
          <span style={{opacity:0.4}}>·</span>
          <span>{post.date.day} {post.date.mo} {post.date.yr}</span>
          {post.numberDisplay && <span style={{opacity:0.4}}>·</span>}
          {post.numberDisplay && <span className="post-number">{post.numberDisplay}</span>}
        </div>
        <h2>{post.title.lead} <Highlight color="butter" size="small">{post.title.em}</Highlight></h2>
        <p className="dek" dangerouslySetInnerHTML={{__html: post.dek}}></p>
        <div className="feature-meta">
          <span className="by" onClick={e => { e.stopPropagation(); nav('/author/' + a.id); }} style={{cursor:'pointer'}}>
            BY <b>{a.name}</b>
          </span>
          <span style={{opacity:0.4}}>·</span>
          <span>{post.readMin} MIN</span>
          <span style={{flex:1}}></span>
          <span className="read">READ THE BATCH →</span>
        </div>
      </div>
    </article>
  );
}

// A clean candy mark per tag — used as the feature visual.
function FeatureMark({ tag }) {
  // The ONE canonical Bonfire flame — identical mark to the wordmark flame on
  // bonfire/index.html. Canon: Bonfire has exactly one flame icon, used for
  // every fire on the site. Paths/fills are byte-faithful to the canonical
  // mark (JSX camelCase attrs); currentColor stroke inherits the ink color.
  const flame = (
    <svg viewBox="0 0 58 68" width="164" height="192" aria-hidden="true" role="presentation">
      <path d="M29 4 C20 18 12 24 12 38 C12 52 19 62 29 62 C39 62 46 52 46 38 C46 30 41 24 36 18 C34 22 31 24 28 22 C30 16 30 10 29 4 Z" stroke="currentColor" strokeWidth="1.8" strokeLinejoin="round" fill="#ff3b6b"/>
      <path d="M29 32 C25 38 22 42 22 48 C22 54 25 58 29 58 C33 58 36 54 36 48 C36 44 33 40 30 36 C29 38 28 38 27 37 C28 35 28 33 29 32 Z" stroke="currentColor" strokeWidth="1.6" strokeLinejoin="round" fill="#ffd84d"/>
    </svg>
  );
  return flame;
}

function PostRow({ post, showTag = true }) {
  const t = TAGS[post.tag];
  const a = AUTHORS[post.author];
  return (
    <article className="post-row" onClick={() => nav('/post/' + post.slug)}>
      <div className="date">
        <span className="mo">{post.date.mo}</span>
        <span className="day">{post.date.day}</span>
        <span className="yr">{post.date.yr}</span>
      </div>
      <div>
        <h3>{post.title.lead} <Highlight color="butter" size="small">{post.title.em}</Highlight></h3>
        <p className="dek" dangerouslySetInnerHTML={{__html: post.dek}}></p>
        <div className="row-meta" style={{'--cat-color': brandColor(t.color)}}>
          {showTag && (
            <span className="cat" onClick={e => { e.stopPropagation(); nav('/tag/' + t.id); }}>
              {t.label}
            </span>
          )}
          {showTag && <span style={{opacity:0.4}}>·</span>}
          <span className="auth" onClick={e => { e.stopPropagation(); nav('/author/' + a.id); }}>
            BY <b>{a.name}</b>
          </span>
          {post.numberDisplay && <span style={{opacity:0.4}}>·</span>}
          {post.numberDisplay && <span className="post-number">{post.numberDisplay}</span>}
        </div>
      </div>
      <div className="rt">{post.readMin} min →</div>
    </article>
  );
}

function TagCloud() {
  return (
    <div className="card">
      <h4>Categories</h4>
      <div className="tag-cloud">
        {Object.values(TAGS).map(t => {
          const n = POSTS.filter(p => p.tag === t.id).length;
          return (
            <a key={t.id} className={"chip " + t.color} onClick={() => nav('/tag/' + t.id)}>
              {t.label}<span style={{opacity:0.7, marginLeft:5}}>{n}</span>
            </a>
          );
        })}
      </div>
    </div>
  );
}

// ─── Blog signup form (BON-1307 · T5) ───────────────────────────────────────
// Real subscribe form: POSTs { email, _trap } to the BON-1305 blog-subscribe
// endpoint, distinct from the homepage product waitlist. The submit/validation/
// state logic lives in blog/blog-signup.mjs and is exposed on window.BlogSignup
// so this Babel-scoped component can call it without ESM imports.
function BlogSignupForm() {
  const [email, setEmail] = useState('');
  const [trap, setTrap] = useState('');         // honeypot — humans never see it
  const [pending, setPending] = useState(false);
  const [status, setStatus] = useState(null);   // { state, message } | null

  const onSubmit = async (e) => {
    e.preventDefault();
    if (pending) return;
    const api = (typeof window !== 'undefined' && window.BlogSignup) || null;
    if (!api) {
      // Loud, contextful — never a silent dead button (Elegance Law).
      setStatus({ state: 'error', message: 'Server hiccup — try again.' });
      return;
    }
    const result = await api.submitBlogSignup({
      email,
      _trap: trap,
      onPending: setPending,
    });
    setStatus(result);
    if (result.state === 'success') setEmail('');
  };

  const done = status && status.state === 'success';

  return (
    <div className="card signup-card">
      <h4>Subscribe.</h4>
      <div className="sub-h">One email per post. No newsletter soup.</div>
      <p>No marketing funnel — just a note when a new post leaves the kitchen.</p>
      <form id="blog-signup" className="signup blog-signup" onSubmit={onSubmit} noValidate>
        <label htmlFor="blog-signup-email" className="sr-only">Email</label>
        <input
          id="blog-signup-email"
          name="email"
          type="email"
          autoComplete="email"
          required
          placeholder="you@somewhere"
          value={email}
          disabled={pending || done}
          onChange={(e) => setEmail(e.target.value)}
        />
        {/* Honeypot — bots fill this; humans never see it. */}
        <input
          name="_trap"
          type="text"
          tabIndex={-1}
          autoComplete="off"
          aria-hidden="true"
          className="hp-trap"
          value={trap}
          onChange={(e) => setTrap(e.target.value)}
        />
        <button type="submit" disabled={pending || done}>
          {done ? '🍭 SAVED' : pending ? '…' : 'Subscribe'}
        </button>
      </form>
      <p
        className={'status note' + (status ? ' status-' + status.state : '')}
        role="status"
        aria-live="polite"
      >
        {status
          ? '↳ ' + status.message
          : '↳ ~1 email per ship. unsubscribe in one click.'}
      </p>
    </div>
  );
}

// Backwards-compatible alias — the SignupCard slots throughout the SPA now
// render the real subscribe form.
function SignupCard() {
  return <BlogSignupForm />;
}

function ArchiveList() {
  const groups = useMemo(() => {
    const byYear = {};
    POSTS.forEach(p => { (byYear[p.date.yr] = byYear[p.date.yr] || []).push(p); });
    return byYear;
  }, []);
  return (
    <div className="card">
      <h4>Archive</h4>
      <ul className="archive">
        {Object.entries(groups).sort((a,b) => b[0].localeCompare(a[0])).map(([y, ps]) => (
          <li key={y} onClick={() => nav('/search')}>
            <span>{y}</span><b>{ps.length} posts</b>
          </li>
        ))}
        <li onClick={() => nav('/search')} style={{marginTop:10, color:'var(--brand-hot)', fontWeight:600, borderTop:'1.5px solid var(--ink-soft)', paddingTop:10}}>
          <span>→ full archive</span><b>{POSTS.length}</b>
        </li>
      </ul>
    </div>
  );
}

// ─── Post (reading) view ───────────────────────────────────────────────────
function PostView({ slug }) {
  const post = POSTS.find(p => p.slug === slug);
  const [progress, setProgress] = useState(0);
  const [active, setActive] = useState(0);
  const articleRef = useRef(null);

  useEffect(() => {
    const onScroll = () => {
      const doc = document.documentElement;
      const max = doc.scrollHeight - doc.clientHeight;
      setProgress(max <= 0 ? 100 : Math.min(100, Math.max(0, (window.scrollY / max) * 100)));
    };
    onScroll();
    window.addEventListener('scroll', onScroll, { passive: true });
    return () => window.removeEventListener('scroll', onScroll);
  }, [slug]);

  const headings = useMemo(() => post ? post.body.filter(b => b.type === 'h2').map((b, i) => ({ id: 's-' + i, text: b.text })) : [], [post]);
  useEffect(() => {
    if (!articleRef.current) return;
    const els = headings.map(h => document.getElementById(h.id)).filter(Boolean);
    if (els.length === 0) return;
    const obs = new IntersectionObserver((entries) => {
      let topMost = null;
      for (const e of entries) {
        if (e.isIntersecting && (!topMost || e.boundingClientRect.top < topMost.boundingClientRect.top)) topMost = e;
      }
      if (topMost) {
        const i = els.indexOf(topMost.target);
        if (i >= 0) setActive(i);
      }
    }, { rootMargin: '-80px 0px -65% 0px', threshold: [0, 1] });
    els.forEach(el => obs.observe(el));
    return () => obs.disconnect();
  }, [headings.length, slug]);

  if (!post) return (
    <main className="stage">
      <div className="empty">
        <div className="big">404 · burned batch</div>
        That batch isn't on the conveyor. <a className="hot" onClick={() => nav('/')} style={{cursor:'pointer', borderBottom:'1.5px solid currentColor'}}>back to the parlor →</a>
      </div>
    </main>
  );

  const t = TAGS[post.tag];
  const a = AUTHORS[post.author];
  const related = POSTS.filter(p => p.tag === post.tag && p.slug !== post.slug).slice(0, 2);
  const wordCount = post.body.reduce((n, b) => n + ((b.text || '') + ' ' + (b.es || '') + ' ' + (b.en || '')).trim().split(/\s+/).filter(Boolean).length, 0);

  return (
    <>
      <div className="read-prog"><i style={{width: progress + '%'}}></i></div>
      <main className="stage" data-screen-label="02 Post — Reading View">
        <article ref={articleRef}>
          <header className="post-hero">
            <div className="kicker">
              <span className="cat" style={{'--cat-color': brandColor(t.color)}}
                    onClick={() => nav('/tag/' + t.id)}>{t.label}</span>
              <span style={{opacity:0.4}}>·</span>
              <span>{post.date.day} {post.date.mo} {post.date.yr}</span>
              {post.numberDisplay && <span style={{opacity:0.4}}>·</span>}
              {post.numberDisplay && <span className="post-number">{post.numberDisplay}</span>}
            </div>
            <h1>{post.title.lead} <Highlight color="butter">{post.title.em}</Highlight></h1>
            <p className="dek" dangerouslySetInnerHTML={{__html: post.dek}}></p>
            <div className="byline">
              <span className="avatar" style={{background: a.avatarColor}} onClick={() => nav('/author/' + a.id)}>{a.initials}</span>
              <span>BY <span className="name" onClick={() => nav('/author/' + a.id)}>{a.name}</span></span>
              <span className="sep">·</span>
              <span>{post.readMin} MIN · {Math.round(wordCount)} WORDS</span>
            </div>
          </header>

          <div className="layout reading">
            <div>
              <div className="prose">
                {renderPostBody(post, headings)}
              </div>

              <div className="post-foot">
                <div className="tags">
                  <span className={"chip " + t.color} onClick={() => nav('/tag/' + t.id)}>{t.label}</span>
                  <span className="chip">Bonfire</span>
                  <span className="chip">checkpoints</span>
                  <span className="chip">fsync</span>
                </div>
                <div className="share">
                  <a>copy link</a><a>x / twitter</a><a>send to a friend</a>
                </div>
              </div>

              <WatermarkReceipt post={post} />

              {related.length > 0 && (
                <section style={{marginTop:72}}>
                  <p className="section-h">SAME SHELF</p>
                  <h2 className="section-title">More from {t.label.toLowerCase()}.</h2>
                  <div className="post-list">
                    {related.map(p => <PostRow key={p.slug} post={p} />)}
                  </div>
                </section>
              )}
            </div>

            <aside className="toc">
              <h4>On this page</h4>
              <ol>
                {headings.map((h, i) => (
                  <li key={h.id} className={i === active ? 'active' : ''} onClick={() => {
                    document.getElementById(h.id)?.scrollIntoView({ behavior: 'smooth', block: 'start' });
                  }}>{h.text}</li>
                ))}
              </ol>
              <div className="read-stats">
                <div className="row"><span>read</span><b>{Math.round(progress)}%</b></div>
                <div className="row"><span>min left</span><b>{Math.max(0, Math.round(post.readMin * (1 - progress/100)))}</b></div>
                <div className="row"><span>cost</span><b>$0.00</b></div>
              </div>
            </aside>
          </div>
        </article>
      </main>
    </>
  );
}

// ─── Digital watermark receipt (BON-1352 · ADR 0019) ────────────────────────
// Renders the verifiable publish-date proof on every post page. Reads the
// watermark bridge field seeded into data.js by scripts/watermark-post.mjs;
// the snapshot URL/timestamp originate from the blog-source adapter (ADR 0017).
function WatermarkReceipt({ post }) {
  const archive = (post.watermark && post.watermark.archive) || { snapshotUrl: null, snapshotAt: null };
  const claimed = post.date.day + ' ' + post.date.mo + ' ' + post.date.yr;
  return (
    <div className="watermark" role="note" aria-label="Publish-date proof">
      <div className="wm-row">
        <span className="wm-label">Published</span>
        <span className="wm-value">{claimed}</span>
      </div>
      <div className="wm-row">
        <span className="wm-label">Verified by</span>
        <span className="wm-value">
          Internet Archive · {archive.snapshotUrl
            ? <a className="wm-link" href={archive.snapshotUrl} target="_blank" rel="noopener">snapshot ↗</a>
            : <span className="wm-pending">snapshot pending</span>}
        </span>
      </div>
      <p className="wm-caption">
        This page was archived independently on the date it claims — the receipt is the proof.
      </p>
    </div>
  );
}

function renderPostBody(post, headings) {
  let hIdx = 0;
  return post.body.map((b, i) => {
    if (b.type === 'p')          return <p key={i} dangerouslySetInnerHTML={{__html: b.text}}></p>;
    if (b.type === 'h2') {
      const id = headings[hIdx++].id;
      return <h2 key={i} id={id} dangerouslySetInnerHTML={{__html: b.text}}></h2>;
    }
    if (b.type === 'h3')         return <h3 key={i} dangerouslySetInnerHTML={{__html: b.text}}></h3>;
    if (b.type === 'blockquote') return <blockquote key={i} dangerouslySetInnerHTML={{__html: b.text}}></blockquote>;
    if (b.type === 'ul')         return <ul key={i}>{b.items.map((it, j) => <li key={j} dangerouslySetInnerHTML={{__html: it}}></li>)}</ul>;
    if (b.type === 'ol')         return <ol key={i}>{b.items.map((it, j) => <li key={j} dangerouslySetInnerHTML={{__html: it}}></li>)}</ol>;
    if (b.type === 'callout') {
      return (
        <div key={i} className={"callout " + (b.flavor || '')}>
          <div className="icon">{b.flavor === 'warn' ? '!' : b.flavor === 'ok' ? '✓' : 'i'}</div>
          <div className="body"><b>{b.title}</b><span dangerouslySetInnerHTML={{__html: b.body}}></span></div>
        </div>
      );
    }
    if (b.type === 'code') {
      return (
        <pre key={i}>
          <span className="tag">{b.lang}</span>
          {b.lines.map((ln, j) => (
            <div key={j}>
              {ln.pink   && <span className="pink">{ln.pink}</span>}
              {ln.mint   && <span className="mint">{ln.mint}</span>}
              {ln.sky    && <span className="sky">{ln.sky}</span>}
              {ln.butter && <span className="butter">{ln.butter}</span>}
              {ln.dim    && <span className="dim">{ln.dim}</span>}
              {ln.rest}
            </div>
          ))}
        </pre>
      );
    }
    if (b.type === 'turn') {
      const who = b.speaker === 'anta' ? 'Anta' : 'Ishtar';
      return (
        <div key={i} className={"turn " + b.speaker}>
          <div className="who">{who}</div>
          <p className="en" lang="en" dangerouslySetInnerHTML={{__html: b.en}}></p>
          <p className="es" lang="es" dangerouslySetInnerHTML={{__html: b.es}}></p>
        </div>
      );
    }
    return null;
  });
}

// ─── Tag page ──────────────────────────────────────────────────────────────
function TagView({ tag, highlightColor }) {
  const t = TAGS[tag];
  const posts = POSTS.filter(p => p.tag === tag);
  if (!t) return <main className="stage"><div className="empty"><div className="big">unknown tag</div></div></main>;
  // map TAGS.color -> Highlight palette name
  const hlColor = ({ pink: 'pink', butter: 'butter', mint: 'mint', sky: 'sky', cherry: 'cherry', grape: 'grape' }[t.color]) || 'sky';
  return (
    <main className="stage" data-screen-label="03 Tag Page">
      <header className="tag-head">
        <div className="crumb-line">CATEGORY · <b>/{t.id}</b></div>
        <h1><Highlight color={hlColor}>{t.label}</Highlight></h1>
        <p className="dek">{t.desc}</p>
        <div className="count-row">
          <span><b>{posts.length}</b> POSTS</span>
          <span className="sep">·</span>
          <span>LATEST · {posts[0]?.date.mo} {posts[0]?.date.yr}</span>
          <span className="sep">·</span>
          <span>OLDEST · {posts[posts.length-1]?.date.mo} {posts[posts.length-1]?.date.yr}</span>
        </div>
      </header>

      <div className="tag-tabs">
        <span className="tab-lbl">also see</span>
        {Object.values(TAGS).filter(tt => tt.id !== t.id).map(tt => (
          <a key={tt.id} className={"chip " + tt.color} onClick={() => nav('/tag/' + tt.id)}>{tt.label}</a>
        ))}
      </div>

      <div className="layout">
        <div className="post-list">
          {posts.map(p => <PostRow key={p.slug} post={p} showTag={false} />)}
        </div>
        <aside className="side">
          <SignupCard />
          <div className="card">
            <h4>Other categories</h4>
            <div className="tag-cloud">
              {Object.values(TAGS).filter(tt => tt.id !== t.id).map(tt => (
                <a key={tt.id} className={"chip " + tt.color} onClick={() => nav('/tag/' + tt.id)}>{tt.label}</a>
              ))}
            </div>
          </div>
        </aside>
      </div>
    </main>
  );
}

// ─── Author page ───────────────────────────────────────────────────────────
function AuthorView({ author }) {
  const a = AUTHORS[author];
  const posts = POSTS.filter(p => p.author === author);
  if (!a) return <main className="stage"><div className="empty"><div className="big">unknown wrapper</div></div></main>;
  const wordCount = posts.reduce((n, p) => n + p.body.reduce((m, b) => m + ((b.text||'') + ' ' + (b.es||'') + ' ' + (b.en||'')).trim().split(/\s+/).filter(Boolean).length, 0), 0);
  return (
    <main className="stage" data-screen-label="04 Author Page">
      <header className="author-head">
        <div className="avatar-big" style={{background: a.avatarColor}}>{a.initials}</div>
        <div>
          <div className="eyebrow"><span className="pulse"></span>WRAPPER ON THE LINE</div>
          <h1>{a.name}</h1>
          <div className="role">{a.role}</div>
          <p className="bio">{a.bio}</p>
          <div className="links">
            {a.links.map((l, i) => <a key={i} className="chip">{l}</a>)}
          </div>
          <div className="stats">
            <span><b>{posts.length}</b>posts</span>
            <span><b>{Math.round(wordCount / 1000) || '<1'}K</b>words</span>
            <span><b>{Math.round(posts.reduce((n, p) => n + p.readMin, 0))}</b>min total</span>
            {a.id !== 'factory' && <span><b>{Math.floor(28 + posts.length * 4)}</b>tickets closed</span>}
          </div>
        </div>
      </header>

      <div className="layout">
        <div>
          <p className="section-h">POSTS</p>
          <h2 className="section-title">Everything {a.name.split(' ')[0]} has shipped.</h2>
          {posts.length === 0
            ? <div className="empty"><div className="big">no posts yet</div>Still in the kitchen. Check back next batch.</div>
            : <div className="post-list">{posts.map(p => <PostRow key={p.slug} post={p} />)}</div>
          }
        </div>
        <aside className="side">
          <div className="card">
            <h4>Other wrappers</h4>
            <div style={{display:'flex', flexDirection:'column', gap:0}}>
              {Object.values(AUTHORS).filter(aa => aa.id !== a.id).map((aa, idx, arr) => (
                <a key={aa.id} onClick={() => nav('/author/' + aa.id)} style={{
                  display:'flex', alignItems:'center', gap:12, cursor:'pointer',
                  padding:'12px 0',
                  borderBottom: idx < arr.length - 1 ? '1px dashed var(--ink-soft)' : 'none',
                  textDecoration:'none', color:'inherit',
                }}>
                  <span style={{
                    width:38, height:38, borderRadius:'50%', border:'2px solid var(--ink)',
                    background: aa.avatarColor, color:'var(--paper)',
                    display:'flex', alignItems:'center', justifyContent:'center',
                    fontFamily:'var(--display)', fontWeight:700, fontSize:14, flexShrink:0,
                  }}>{aa.initials}</span>
                  <div>
                    <div style={{fontFamily:'var(--display)', fontWeight:700, fontSize:15}}>{aa.name}</div>
                    <div style={{fontFamily:'var(--mono)', fontSize:10, letterSpacing:'0.18em', textTransform:'uppercase', color:'var(--ink-soft)'}}>{aa.role}</div>
                  </div>
                </a>
              ))}
            </div>
          </div>
          <SignupCard />
        </aside>
      </div>
    </main>
  );
}

// ─── Search / archive page ─────────────────────────────────────────────────
function SearchView({ initial, highlightColor }) {
  const [q, setQ] = useState(initial.q || '');
  const [tag, setTag] = useState(initial.tag || '');
  const [author, setAuthor] = useState(initial.author || '');

  const results = useMemo(() => {
    const ql = q.trim().toLowerCase();
    return POSTS.filter(p => {
      if (tag && p.tag !== tag) return false;
      if (author && p.author !== author) return false;
      if (!ql) return true;
      const hay = (p.title.lead + ' ' + p.title.em + ' ' + p.dek + ' ' + p.excerpt).toLowerCase();
      return hay.includes(ql);
    });
  }, [q, tag, author]);

  const grouped = useMemo(() => {
    const g = {};
    results.forEach(p => { (g[p.date.yr] = g[p.date.yr] || []).push(p); });
    return Object.entries(g).sort((a,b) => b[0].localeCompare(a[0]));
  }, [results]);

  return (
    <main className="stage" data-screen-label="05 Search / Archive">
      <header className="search-head">
        <div className="eyebrow"><span className="pulse"></span>FULL ARCHIVE · {POSTS.length} POSTS · SINCE 2025</div>
        <h1>Find a <Highlight color="mint">batch</Highlight>.</h1>

        <div className="search-box">
          <span className="icon">⌕</span>
          <input
            type="text"
            value={q}
            placeholder="search posts, deks, batch numbers…"
            onChange={e => setQ(e.target.value)}
            autoFocus
          />
          {(q || tag || author) && <button className="clr" onClick={() => { setQ(''); setTag(''); setAuthor(''); }}>clear</button>}
        </div>

        <div className="search-filters">
          <div className="group">
            <span className="lbl">category</span>
            <a className={"chip" + (tag === '' ? ' on' : '')} onClick={() => setTag('')}>any</a>
            {Object.values(TAGS).map(t => (
              <a key={t.id} className={"chip " + t.color + (tag === t.id ? ' on' : '')} onClick={() => setTag(t.id === tag ? '' : t.id)}>{t.label}</a>
            ))}
          </div>
          <div className="group">
            <span className="lbl">wrapper</span>
            <a className={"chip" + (author === '' ? ' on' : '')} onClick={() => setAuthor('')}>any</a>
            {Object.values(AUTHORS).map(a => (
              <a key={a.id} className={"chip" + (author === a.id ? ' on' : '')} onClick={() => setAuthor(a.id === author ? '' : a.id)}>
                <span style={{
                  display:'inline-block', width:9, height:9, borderRadius:'50%',
                  background: a.avatarColor, border:'1.5px solid currentColor',
                  marginRight:5, verticalAlign:'middle',
                }}></span>
                {a.name.split(' ')[0]}
              </a>
            ))}
          </div>
        </div>
      </header>

      <div className="search-results-stat">
        <span className="hit"><b>{results.length}</b></span> POSTS MATCH
        {q && <> · QUERY · "<b>{q}</b>"</>}
        {tag && <> · CATEGORY · <b>{TAGS[tag].label}</b></>}
        {author && <> · BY · <b>{AUTHORS[author].name}</b></>}
      </div>

      {results.length === 0 ? (
        <div className="empty">
          <div className="big">No batches matched.</div>
          The conveyor is empty for that query. Try a different word, or <a className="hot" onClick={() => { setQ(''); setTag(''); setAuthor(''); }} style={{cursor:'pointer', borderBottom:'1.5px solid currentColor'}}>clear all filters →</a>
        </div>
      ) : (
        grouped.map(([yr, ps]) => (
          <section key={yr} className="archive-year">
            <h2>{yr}<span className="count">{ps.length} POSTS</span></h2>
            <div className="post-list">
              {ps.map(p => <PostRowWithHighlight key={p.slug} post={p} q={q} />)}
            </div>
          </section>
        ))
      )}
    </main>
  );
}

function PostRowWithHighlight({ post, q }) {
  const t = TAGS[post.tag];
  const a = AUTHORS[post.author];
  const hl = (str) => {
    if (!q) return str;
    const re = new RegExp('(' + q.replace(/[.*+?^${}()|[\]\\]/g, '\\$&') + ')', 'gi');
    return str.replace(re, '<mark class="hit">$1</mark>');
  };
  return (
    <article className="post-row" onClick={() => nav('/post/' + post.slug)}>
      <div className="date">
        <span className="mo">{post.date.mo}</span>
        <span className="day">{post.date.day}</span>
        <span className="yr">{post.date.yr}</span>
      </div>
      <div>
        <h3 dangerouslySetInnerHTML={{__html:
          hl(post.title.lead)
          + ' <em class="hl butter small" style="transform:rotate(calc(-1deg * var(--sticker-rot)))">'
          + hl(post.title.em) + '</em>'
        }}></h3>
        <p className="dek" dangerouslySetInnerHTML={{__html: hl(post.dek)}}></p>
        <div className="row-meta" style={{'--cat-color': brandColor(t.color)}}>
          <span className="cat" onClick={e => { e.stopPropagation(); nav('/tag/' + t.id); }}>{t.label}</span>
          <span style={{opacity:0.4}}>·</span>
          <span className="auth" onClick={e => { e.stopPropagation(); nav('/author/' + a.id); }}>BY <b>{a.name}</b></span>
        </div>
      </div>
      <div className="rt">{post.readMin} min →</div>
    </article>
  );
}

// ─── Footer + ticker ──────────────────────────────────────────────────────
function Foot() {
  return (
    <footer className="parlor-foot">
      <span>© 2026 CANDYFACTORY · ALL CANDIES MADE WITH SUGAR &amp; SOFTWARE</span>
      <div className="links">
        <a onClick={() => nav('/')}>BLOG</a>
        <a>BONFIRE</a>
        <a>MIRROR</a>
        <a>DECK</a>
        <a>GITHUB ↗</a>
        <a>RSS</a>
        <a>HELLO@CANDYFACTORY.AI</a>
      </div>
    </footer>
  );
}

function Ticker() {
  const events = [
    'BLOG · 9 POSTS IN ARCHIVE',
    'BONFIRE v0.9 · SEALED JOURNALS',
    'MIRROR · CONTEXT-QUILT IN OVEN',
    'DECK · SLOT 4 OPEN',
    'NO FAKE NUMBERS',
    'VISIBLE MACHINERY',
    'NO BLACK-BOX MAGIC',
    'BATCH 0947 · 71%',
  ];
  return (
    <div className="ticker">
      <div className="ticker-track">
        {[...events, ...events, ...events].map((e, i) => (
          <span key={i}><span className="dot">●</span> {e}&nbsp;&nbsp;&nbsp;</span>
        ))}
      </div>
    </div>
  );
}

// ─── App root ─────────────────────────────────────────────────────────────
function App() {
  const [route, setRoute] = useState(parseHash());
  useEffect(() => {
    const onHash = () => setRoute(parseHash());
    window.addEventListener('hashchange', onHash);
    return () => window.removeEventListener('hashchange', onHash);
  }, []);

  useEffect(() => { if (window.applyTheme) window.applyTheme('cottoncandy'); }, []);
  useEffect(() => { document.body.dataset.density = 'cozy'; }, []);

  // ADR 0018 — on a post route, prefix document.title with the canonical post
  // number (#NNN). Off post routes, restore the default blog title.
  const DEFAULT_TITLE = 'The Blog · CandyFactory';
  useEffect(() => {
    if (route.view === 'post') {
      const post = POSTS.find(p => p.slug === route.slug);
      if (post) {
        const titleText = [post.title.lead, post.title.em].filter(Boolean).join(' ');
        const prefix = post.numberDisplay ? post.numberDisplay + ' · ' : '';
        document.title = prefix + titleText.trim() + ' · CandyFactory Blog';
        return;
      }
    }
    document.title = DEFAULT_TITLE;
  }, [route]);

  let view;
  if (route.view === 'index')        view = <IndexView  highlightColor={HIGHLIGHT_COLOR}/>;
  else if (route.view === 'post')    view = <PostView   slug={route.slug}/>;
  else if (route.view === 'tag')     view = <TagView    tag={route.tag} highlightColor={HIGHLIGHT_COLOR}/>;
  else if (route.view === 'author')  view = <AuthorView author={route.author}/>;
  else if (route.view === 'search')  view = <SearchView initial={route} highlightColor={HIGHLIGHT_COLOR} key={route.q + '|' + route.tag + '|' + route.author}/>;
  else                                view = <IndexView highlightColor={HIGHLIGHT_COLOR}/>;

  return (
    <>
      <Rail/>
      {view}
      <Ticker/>
      <Foot/>
    </>
  );
}

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