/* global React */
const { useState, useEffect, useRef, useMemo, createContext, useContext } = React;

/* ============================================================
   IDENTITY CONTEXT
   Tells every view which operator is looking. Views use it to
   frame "waiting on you" vs "blocked on them".
   ============================================================ */
const IdentityContext = createContext('jarett');

// Which grant-state field belongs to which operator. Add new operators here
// and all identity-aware views pick them up.
const OPERATOR_MAP = {
  jarett: { mine: 'actions_on_you',    theirs: 'waiting_on_matty', otherName: 'matty' },
  matty:  { mine: 'waiting_on_matty',  theirs: 'actions_on_you',   otherName: 'jarett' },
};

/* ============================================================
   FILE PANEL — shared across grant-facing views
   Used by GrantStatusView and BlockersView. Holds the openFile
   state, body-class toggle (grant-expanded), Esc-key close,
   deep-link hash consumption, and the panel JSX itself.
   ============================================================ */

// Very light markdown-ish renderer — headings, lists, bold, paragraphs.
// Intentionally small: enough to make the preview read as a document,
// not a terminal dump. Not a full markdown parser.
function FilePreview({ body }) {
  if (!body) return null;
  const lines = body.split('\n');
  const out = [];
  let listBuf = null;

  const flushList = () => {
    if (!listBuf) return;
    out.push(<ul className="fp-list" key={`ul-${out.length}`}>{listBuf.map((li, i) => <li key={i}>{renderInline(li)}</li>)}</ul>);
    listBuf = null;
  };

  function renderInline(text) {
    // **bold** → <strong>; `code` stays plain
    const parts = text.split(/(\*\*[^*]+\*\*)/g);
    return parts.map((p, i) => {
      if (p.startsWith('**') && p.endsWith('**')) return <strong key={i}>{p.slice(2, -2)}</strong>;
      return <React.Fragment key={i}>{p}</React.Fragment>;
    });
  }

  lines.forEach((raw, idx) => {
    const line = raw; // preserve indentation for yaml
    const trimmed = raw.trim();

    if (!trimmed) { flushList(); return; }

    // Heading
    const h = trimmed.match(/^(#{1,4})\s+(.+)$/);
    if (h) {
      flushList();
      const level = h[1].length;
      out.push(React.createElement(`h${Math.min(level + 1, 6)}`, { className: `fp-h${level}`, key: idx }, h[2]));
      return;
    }

    // List item
    const li = trimmed.match(/^[-*]\s+(.+)$/) || trimmed.match(/^\d+\.\s+(.+)$/);
    if (li) {
      if (!listBuf) listBuf = [];
      listBuf.push(li[1]);
      return;
    }

    // YAML-ish line (key: value at indent 0-4)
    if (/^[a-z_][a-z0-9_]*:\s*/.test(line) || /^\s{2,}/.test(line) && !trimmed.startsWith('-')) {
      flushList();
      out.push(<pre className="fp-yaml" key={idx}>{line}</pre>);
      return;
    }

    flushList();
    out.push(<p className="fp-p" key={idx}>{renderInline(trimmed)}</p>);
  });
  flushList();
  return <div className="pb-grant-file-body">{out}</div>;
}

function FilePanel({ file, onClose }) {
  if (!file) return null;
  return (
    <aside className="pb-grant-file">
      <div className="pb-grant-file-head">
        <div>
          <div className="kicker">{file.kicker || 'file'}</div>
          <div className="ttl">{file.title}</div>
          <div className="pth">{file.path}</div>
        </div>
        <button className="close" onClick={onClose} title="close (esc)">close</button>
      </div>
      <FilePreview body={file.body} />
    </aside>
  );
}

// Build openFile descriptor for a grant child (component) or a receipt
function descForChild(c) {
  return {
    title: c.name.replace(/-/g, ' '),
    path: c.file || c.path,
    body: c.preview || c.intro,
    kicker: `component · ${c.state}`,
  };
}

function useGrantFilePanel(children) {
  const [openFile, setOpenFile] = useState(null);

  useEffect(() => {
    document.body.classList.toggle('grant-expanded', !!openFile);
    return () => document.body.classList.remove('grant-expanded');
  }, [openFile]);

  useEffect(() => {
    const onKey = (e) => { if (e.key === 'Escape' && openFile) setOpenFile(null); };
    window.addEventListener('keydown', onKey);
    return () => window.removeEventListener('keydown', onKey);
  }, [openFile]);

  // Deep-link #grant/<component-id> — consume on mount, leave hash in place
  // so refresh restores the same panel state.
  useEffect(() => {
    const m = window.location.hash.match(/grant\/([a-z0-9-]+)/i);
    if (!m) return;
    const c = (children || []).find(ch => ch.id === m[1]);
    if (c) setOpenFile(descForChild(c));
  }, []);

  const openChild = (c) => setOpenFile(descForChild(c));
  const openChildById = (id) => {
    const c = (children || []).find(ch => ch.id === id);
    if (c) setOpenFile(descForChild(c));
  };

  return { openFile, setOpenFile, openChild, openChildById };
}

/* ============================================================
   SIX PREBUILT VIEWS — each is an instant, in-aesthetic layout.
   ============================================================ */

/* ---------- 1. Grant status (primary demo) — wired to AMPLIFY_BC ---------- */
function GrantStatusView() {
  const G = window.AMPLIFY_BC;
  const identity = useContext(IdentityContext);
  const opMap = OPERATOR_MAP[identity] || OPERATOR_MAP.jarett;

  const DEADLINE = new Date('2026-04-29T16:00:00-07:00');
  const daysLeft = Math.max(0, Math.ceil((DEADLINE - new Date()) / 86400000));

  // Map child.state → ladder step class
  const stepClass = { signal: 'now', amber: 'block', idle: 'pending', voltage: 'done' };
  const stepMark  = { signal: '●',   amber: '!',     idle: '○',       voltage: '✓'    };

  const children = G.children || [];
  const done     = children.filter(c => c.state === 'signal' || c.state === 'voltage').length;
  // Waiting section: shows Matt's inputs (the list doesn't change by viewer);
  // only the framing does — Matt sees "waiting on you", Jarett sees "waiting on matty".
  const waiting  = G.waiting_on_matty || [];
  const sources  = (G.receipts || []).slice(0, 5);
  const amMatty  = identity === 'matty';
  const waitingLabel   = amMatty ? 'waiting on you' : 'waiting on matty';
  const waitingVerdict = amMatty ? 'inputs waiting on you' : 'inputs waiting on matt';

  const prettyName = (s) => s.replace(/-/g, ' ');
  const firstSentence = (s) => (s.split('. ')[0] || s).trim().replace(/\.$/, '') + '.';

  const { openFile, setOpenFile, openChild } = useGrantFilePanel(children);

  return (
    <div className={`pb-grant${openFile ? ' has-file' : ''}`}>
     <div className="pb-grant-main">
      <div className="v-verdict">
        <div className="k">verdict · right now</div>
        <div className="t">
          Amplify BC 2026/27 — <em>T-{daysLeft} days</em>. {done}/{children.length} components in signal,
          <em> {waiting.length} {waitingVerdict}</em> (target 04-21). Narrative locked, facts updating.
        </div>
      </div>

      <div className="v-section">components</div>
      <div className="v-ladder">
        {children.map((c) => {
          const top = c.targets && c.targets[0];
          const active = openFile && openFile.path === (c.file || c.path);
          return (
            <div
              className={`v-step clickable ${stepClass[c.state] || 'pending'}${active ? ' active' : ''}`}
              key={c.id}
              onClick={() => openChild(c)}
              role="button"
              tabIndex={0}
              onKeyDown={(e) => { if (e.key === 'Enter' || e.key === ' ') openChild(c); }}
            >
              <div className="mark">{stepMark[c.state] || '○'}</div>
              <div>
                <div className="lbl">{prettyName(c.name)}</div>
                <div className="sub">{firstSentence(c.intro)}</div>
              </div>
              <div className="tag">{top ? `${top.k} · ${top.v}` : c.state}</div>
            </div>
          );
        })}
      </div>

      {waiting.length > 0 && (
        <>
          <div className="v-section">{waitingLabel}</div>
          <div className="v-ladder">
            {waiting.map((w, i) => {
              const linked = children.find(c => c.id === w.componentId);
              const active = linked && openFile && openFile.path === (linked.file || linked.path);
              return (
                <div
                  className={`v-step clickable ${w.tone === 'voltage' ? 'block' : 'now'}${active ? ' active' : ''}`}
                  key={i}
                  onClick={() => linked && openChild(linked)}
                  role="button"
                  tabIndex={0}
                  onKeyDown={(e) => { if ((e.key === 'Enter' || e.key === ' ') && linked) openChild(linked); }}
                >
                  <div className="mark">!</div>
                  <div>
                    <div className="lbl">{w.k}</div>
                    <div className="sub">unblocks {w.unblocks} · drop-dead {w.dd}</div>
                  </div>
                  <div className="tag">{w.v}</div>
                </div>
              );
            })}
          </div>
        </>
      )}

      <div className="v-section">what I'd do next</div>
      <div className="v-quote">
        {G.next_move}
        <span className="src">— claude, reading the grant state</span>
      </div>

      <div className="v-citations">
        <span className="k">recent receipts</span>
        {sources.map(r => {
          const clickable = !!r.preview;
          const active = openFile && r.file && openFile.path === r.file;
          return (
            <span
              className={`n${clickable ? ' clickable' : ''}${active ? ' active' : ''}`}
              key={r.id}
              onClick={clickable ? () => setOpenFile({
                title: r.msg.split(' · ')[0],
                path: r.file,
                body: r.preview,
                kicker: `log · ${r.e}`,
              }) : undefined}
              role={clickable ? 'button' : undefined}
              tabIndex={clickable ? 0 : undefined}
            >
              {r.t} · {r.msg.split(' · ')[0]}
            </span>
          );
        })}
      </div>
     </div>
     <FilePanel file={openFile} onClose={() => setOpenFile(null)} />
    </div>
  );
}

/* ---------- 2. Timeline — "what happened with UV last 30 days" ---------- */
function UvTimelineView() {
  return (
    <>
      <div className="v-timeline">
        <div className="v-event done">
          <div className="when">feb 22 · 9 days ago</div>
          <div className="what">Brooklyn Steel went on sale — sold out in 11 minutes.</div>
          <div className="who">Ticketmaster → Matty → celebration in #uv-tour.</div>
        </div>
        <div className="v-event done">
          <div className="when">feb 27 · 5 days ago</div>
          <div className="what">Merch LLC structure finalized.</div>
          <div className="who">Legal cleared the splits with Plumb; Nadia filed.</div>
        </div>
        <div className="v-event stale">
          <div className="when">mar 01 · 3 days ago</div>
          <div className="what">Oversold: Brooklyn demand at ~2.4× capacity.</div>
          <div className="who">Agent pitched adding a second night. Unanswered.</div>
        </div>
        <div className="v-event now">
          <div className="when">today</div>
          <div className="what">UV agent following up on the second night.</div>
          <div className="who">"Need an answer by Friday or we lose the hold." — Jacob, 11:04am.</div>
        </div>
        <div className="v-event">
          <div className="when">mar 08 · in 5 days</div>
          <div className="what">Press day — Pitchfork + FADER.</div>
          <div className="who">Embargo lifts 10am ET. Matty briefed; Stel unavailable.</div>
        </div>
      </div>

      <div className="v-verdict" style={{marginTop: 28}}>
        <div className="k">one thing that needs you</div>
        <div className="t">Decide on the <em>second Brooklyn night</em> — the hold drops Friday.</div>
      </div>

      <div className="v-citations">
        <span className="k">sources</span>
        <span className="n">#uv-tour</span>
        <span className="n">Jacob · email mar 01</span>
        <span className="n">Nadia · LLC filing</span>
        <span className="n">Pitchfork pitch doc</span>
      </div>
    </>
  );
}

/* ---------- 3. Blocked on — wired to AMPLIFY_BC + identity-aware ---------- */
function BlockersView() {
  const G = window.AMPLIFY_BC;
  const identity = useContext(IdentityContext);
  const opMap = OPERATOR_MAP[identity] || OPERATOR_MAP.jarett;

  const children = G.children || [];
  const { openFile, setOpenFile, openChildById } = useGrantFilePanel(children);

  // "mine" = what I own. "theirs" = what the other operator owes me.
  const mine   = G[opMap.mine] || [];
  const theirs = G[opMap.theirs] || [];
  const parked = G.parked || [];
  const other  = opMap.otherName;

  const rowSub = (it) => it.sub || (it.unblocks ? `unblocks ${it.unblocks}` : '');
  const stepClass = { voltage: 'block', amber: 'now', now: 'now', idle: 'pending' };

  const rowClick = (item) => item.componentId ? () => openChildById(item.componentId) : undefined;
  const rowActive = (item) => item.componentId && openFile && children.find(c => c.id === item.componentId)?.file === openFile.path;

  return (
    <div className={`pb-grant${openFile ? ' has-file' : ''}`}>
     <div className="pb-grant-main">
      <div className="v-big">
        <em>{mine.length} things</em> waiting on you on the grant. {theirs.length} more are blocked on {other}.
      </div>

      <div className="v-section">decisions pending</div>
      <div className="v-ladder">
        {mine.map((a, i) => {
          const click = rowClick(a);
          return (
            <div
              className={`v-step ${stepClass[a.tone] || 'now'}${click ? ' clickable' : ''}${rowActive(a) ? ' active' : ''}`}
              key={i}
              onClick={click}
              role={click ? 'button' : undefined}
              tabIndex={click ? 0 : undefined}
              onKeyDown={(e) => { if (click && (e.key === 'Enter' || e.key === ' ')) click(); }}
            >
              <div className="mark">{i + 1}</div>
              <div>
                <div className="lbl">{a.k}</div>
                <div className="sub">{rowSub(a)}</div>
              </div>
              <div className="tag">drop-dead {a.dd}</div>
            </div>
          );
        })}
      </div>

      <div className="v-section">also blocked on {other}</div>
      <div className="v-ladder">
        {theirs.slice(0, 4).map((w, i) => {
          const click = rowClick(w);
          return (
            <div
              className={`v-step block${click ? ' clickable' : ''}${rowActive(w) ? ' active' : ''}`}
              key={i}
              onClick={click}
              role={click ? 'button' : undefined}
              tabIndex={click ? 0 : undefined}
              onKeyDown={(e) => { if (click && (e.key === 'Enter' || e.key === ' ')) click(); }}
            >
              <div className="mark">!</div>
              <div>
                <div className="lbl">{w.k}</div>
                <div className="sub">{w.unblocks ? `unblocks ${w.unblocks}` : w.sub} · drop-dead {w.dd}</div>
              </div>
              <div className="tag">{w.v || 'blocker'}</div>
            </div>
          );
        })}
      </div>

      <div className="v-section">parked for later</div>
      <dl className="v-kv">
        {parked.map((p, i) => (
          <React.Fragment key={i}>
            <dt>{p.k}</dt>
            <dd><em>{p.v}</em></dd>
          </React.Fragment>
        ))}
      </dl>

      <div className="v-citations">
        <span className="k">sources</span>
        <span className="n">amplify-bc-2026 · {opMap.mine}</span>
        <span className="n">amplify-bc-2026 · {opMap.theirs}</span>
        <span className="n">STATUS.md · next steps</span>
      </div>
     </div>
     <FilePanel file={openFile} onClose={() => setOpenFile(null)} />
    </div>
  );
}

/* ---------- 4. Sophia brand deal — side by side comparison ---------- */
function SophiaDealView() {
  return (
    <>
      <div className="v-verdict">
        <div className="k">claude's read</div>
        <div className="t">
          <em>Mercury is the cleaner deal</em> — less money up front, but the rights structure is honest.
          The Adidas term sheet has a reversion clause that quietly takes your publishing.
        </div>
      </div>

      <div className="v-section">mercury</div>
      <dl className="v-kv">
        <dt>Fee</dt><dd>$85k · paid on signing</dd>
        <dt>Term</dt><dd>6 months · then non-exclusive</dd>
        <dt>Usage</dt><dd>Instagram + one TVC, US only</dd>
        <dt>Rights</dt><dd><em>No publishing grab. Full reversion.</em></dd>
        <dt>Approval</dt><dd>Sophia has final cut</dd>
      </dl>

      <div className="v-section">adidas</div>
      <dl className="v-kv">
        <dt>Fee</dt><dd>$140k · 50/50 on sign + delivery</dd>
        <dt>Term</dt><dd>12 months · global</dd>
        <dt>Usage</dt><dd>All platforms, 18 months archival</dd>
        <dt>Rights</dt><dd><em>Sync + sub-publishing on the custom track.</em></dd>
        <dt>Approval</dt><dd>Brand team has final cut · 2-round revisions</dd>
      </dl>

      <div className="v-quote">
        You've told me before — fee is never the whole picture for Sophia. She hates being reshot
        and she hates losing her songs. Adidas wants both.
        <span className="src">— recalling your note, oct '25</span>
      </div>

      <div className="v-citations">
        <span className="k">sources</span>
        <span className="n">Mercury · term sheet v2</span>
        <span className="n">Adidas · term sheet v1</span>
        <span className="n">Legal · markup from Liv</span>
        <span className="n">Your notes · sophia</span>
      </div>
    </>
  );
}

/* ---------- 5. Money — Q1 so far ---------- */
function MoneyView() {
  return (
    <>
      <div className="v-big">
        Q1 net is <em>+$412k</em> — tour receipts carried, grant cash not yet counted.
      </div>

      <div className="v-section">money in</div>
      <div className="v-money">
        <div className="label">UV · tour gross (Jan–Feb)<em>Brooklyn, Phila, DC, Boston</em></div>
        <div className="amt in">+ $486,200</div>
        <div className="date">feb 28</div>
      </div>
      <div className="v-money">
        <div className="label">Sophia · sync placement<em>A24 film — "Shoreline"</em></div>
        <div className="amt in">+ $62,500</div>
        <div className="date">feb 12</div>
      </div>
      <div className="v-money">
        <div className="label">Merch · DTC<em>UV drop 02</em></div>
        <div className="amt in">+ $41,300</div>
        <div className="date">feb 24</div>
      </div>
      <div className="v-money">
        <div className="label">Publishing · Q4 catalog<em>from Kobalt</em></div>
        <div className="amt in">+ $28,900</div>
        <div className="date">jan 31</div>
      </div>

      <div className="v-section">money out</div>
      <div className="v-money">
        <div className="label">Tour · production<em>lighting, bus, crew</em></div>
        <div className="amt out">− $148,000</div>
        <div className="date">feb 28</div>
      </div>
      <div className="v-money">
        <div className="label">Legal<em>Liv · retainer + Sophia deal work</em></div>
        <div className="amt out">− $22,500</div>
        <div className="date">q1</div>
      </div>
      <div className="v-money">
        <div className="label">Grant · prep<em>consultant, writing, design</em></div>
        <div className="amt out">− $18,400</div>
        <div className="date">feb–mar</div>
      </div>
      <div className="v-money">
        <div className="label">Office + ops<em>rent, tools, Nadia's contract</em></div>
        <div className="amt out">− $17,900</div>
        <div className="date">q1</div>
      </div>

      <div className="v-verdict" style={{marginTop: 22}}>
        <div className="k">what I'd watch</div>
        <div className="t">
          Tour production is tracking <em>12% over</em> budget — mostly bus upgrades. Not alarming,
          but if you add Brooklyn night 2 it compounds.
        </div>
      </div>

      <div className="v-citations">
        <span className="k">sources</span>
        <span className="n">Ramp · feed</span>
        <span className="n">Nadia · Q1 close</span>
        <span className="n">Kobalt · statement</span>
      </div>
    </>
  );
}

/* ---------- 6. People · who's around the grant ---------- */
function PeopleView() {
  return (
    <>
      <div className="v-big">
        <em>Five people</em> touch the Warhol grant. Only two are blocking it.
      </div>

      <div className="v-section">inside</div>
      <div className="v-people">
        <div className="v-person"><div className="av" style={{background:'#FF6FB5'}}>YO</div><div><div className="nm">You</div><div className="rl">lead · owner</div></div></div>
        <div className="v-person"><div className="av" style={{background:'#00E08F'}}>MH</div><div><div className="nm">Matty</div><div className="rl">reviewer · co-sign</div></div></div>
        <div className="v-person"><div className="av" style={{background:'#4CC7FF'}}>ND</div><div><div className="nm">Nadia</div><div className="rl">budget · ops</div></div></div>
        <div className="v-person"><div className="av" style={{background:'#FF9A3C'}}>ST</div><div><div className="nm">Stel</div><div className="rl">artist · statement</div></div></div>
      </div>

      <div className="v-section">outside</div>
      <div className="v-people">
        <div className="v-person"><div className="av" style={{background:'#F6C8DD', color:'#000'}}>DH</div><div><div className="nm">Deana Haggag</div><div className="rl">letter of support</div></div></div>
        <div className="v-person"><div className="av" style={{background:'#F6C8DD', color:'#000'}}>LR</div><div><div className="nm">Legacy Russell</div><div className="rl">letter of support</div></div></div>
        <div className="v-person"><div className="av" style={{background:'#1f1f1f', color:'#fff'}}>WA</div><div><div className="nm">Warhol · Rachel B.</div><div className="rl">program officer</div></div></div>
      </div>

      <div className="v-section">what I know about them</div>
      <dl className="v-kv">
        <dt>Stel</dt>
        <dd><em>On tour bus</em> — responds best late night. You've ghost-written for her once before; she was fine with it.</dd>
        <dt>Nadia</dt>
        <dd>Two-round revision pattern on budgets. <em>Budget v3 is almost always final.</em></dd>
        <dt>Deana</dt>
        <dd>Last letter took 2 weeks. <em>Nudge in 4 days if no reply.</em></dd>
        <dt>Rachel (Warhol)</dt>
        <dd>Prefers a single submission email. <em>Do not send an update stream.</em></dd>
      </dl>

      <div className="v-citations">
        <span className="k">sources</span>
        <span className="n">Personal brain · notes</span>
        <span className="n">Past grants · 2023, 2024</span>
        <span className="n">Rachel · prior emails</span>
      </div>
    </>
  );
}

/* ============================================================
   ELECTRIC BRAIN — loading indicator
   Pink brain shape, yellow lightning bolts that flash on/off,
   brain bobs slightly. No background — sits on the black canvas.
   ============================================================ */
function ElectricBrain() {
  return (
    <svg className="ebr" viewBox="0 0 220 180" xmlns="http://www.w3.org/2000/svg" aria-hidden="true">
      {/* bolts — 6 around the brain, random flash offsets */}
      <g className="bolts">
        <path className="bolt b1" d="M40 46 L50 62 L44 62 L54 80" />
        <path className="bolt b2" d="M176 40 L168 56 L176 56 L166 74" />
        <path className="bolt b3" d="M22 108 L34 120 L26 122 L40 136" />
        <path className="bolt b4" d="M196 104 L186 118 L194 120 L184 136" />
        <path className="bolt b5" d="M68 22 L76 36 L70 36 L78 50" />
        <path className="bolt b6" d="M152 22 L146 36 L152 36 L144 50" />
      </g>

      {/* radiating ticks */}
      <g className="ticks">
        <line x1="30"  y1="90" x2="14"  y2="90" />
        <line x1="190" y1="90" x2="206" y2="90" />
        <line x1="110" y1="20" x2="110" y2="6" />
        <line x1="60"  y1="30" x2="50"  y2="22" />
        <line x1="160" y1="30" x2="170" y2="22" />
      </g>

      {/* brain — single path, two hemispheres + cerebellum hint */}
      <g className="brain">
        <path className="body"
          d="M110 46
             C 84 46, 60 58, 58 82
             C 46 88, 46 108, 60 114
             C 58 134, 78 146, 104 144
             L 104 150
             C 104 156, 112 156, 112 150
             L 112 144
             C 138 146, 158 134, 156 114
             C 170 108, 170 88, 158 82
             C 156 58, 134 46, 110 46 Z" />
        {/* center fold */}
        <path className="fold" d="M110 50 L110 144" />
        {/* wrinkles */}
        <path className="wrinkle" d="M72 78 Q 80 84 76 92 Q 70 100 80 108" />
        <path className="wrinkle" d="M88 68 Q 96 74 90 84 Q 84 94 94 102" />
        <path className="wrinkle" d="M148 78 Q 140 84 144 92 Q 150 100 140 108" />
        <path className="wrinkle" d="M132 68 Q 124 74 130 84 Q 136 94 126 102" />
        {/* cerebellum bump */}
        <path className="stem" d="M128 138 Q 138 142 140 132 Q 142 124 132 124 Z" />
      </g>
    </svg>
  );
}


/* ---------- 7. Tour now — wired to SOPHIA_TOUR (live clock + weather) ---------- */
function TourNowView() {
  const T = window.SOPHIA_TOUR || { shows: [] };
  const [now, setNow] = useState(() => new Date());
  const [weather, setWeather] = useState(null);
  const [wxErr, setWxErr] = useState(null);

  // Live clock tick (every 30s is plenty; avoids churn).
  useEffect(() => {
    const id = setInterval(() => setNow(new Date()), 30000);
    return () => clearInterval(id);
  }, []);

  // Find current or next show. "Current" = show date is today in the city's
  // local timezone. Otherwise "next" = earliest future date.
  const pick = useMemo(() => {
    const shows = (T.shows || []).slice().sort((a, b) => a.date.localeCompare(b.date));
    const today = new Date().toISOString().slice(0, 10);
    const current = shows.find(s => s.date === today);
    if (current) return { show: current, when: 'today' };
    const next = shows.find(s => s.date > today);
    if (next) {
      const days = Math.round((new Date(next.date) - new Date(today)) / 86400000);
      return { show: next, when: days === 1 ? 'tomorrow' : `in ${days} days` };
    }
    const last = shows[shows.length - 1];
    return last ? { show: last, when: 'tour wrapped' } : null;
  }, [T.shows]);

  // Fetch weather for the picked show's city. Open-Meteo, no key.
  useEffect(() => {
    if (!pick) return;
    setWeather(null); setWxErr(null);
    let cancelled = false;
    (async () => {
      try {
        const q = encodeURIComponent(pick.show.city.split(',')[0]);
        const geo = await fetch(`https://geocoding-api.open-meteo.com/v1/search?name=${q}&count=1`).then(r => r.json());
        const loc = geo?.results?.[0];
        if (!loc) throw new Error('geocode miss');
        const fc = await fetch(`https://api.open-meteo.com/v1/forecast?latitude=${loc.latitude}&longitude=${loc.longitude}&current=temperature_2m,weather_code&daily=temperature_2m_max,temperature_2m_min,weather_code&timezone=${encodeURIComponent(pick.show.tz || 'auto')}&forecast_days=1`).then(r => r.json());
        if (cancelled) return;
        setWeather({
          current_c: Math.round(fc.current?.temperature_2m),
          code: fc.current?.weather_code,
          hi_c: Math.round(fc.daily?.temperature_2m_max?.[0]),
          lo_c: Math.round(fc.daily?.temperature_2m_min?.[0]),
        });
      } catch (e) {
        if (!cancelled) setWxErr(e.message || 'weather fetch failed');
      }
    })();
    return () => { cancelled = true; };
  }, [pick?.show?.city, pick?.show?.date]);

  if (!pick) {
    return <div className="v-verdict"><div className="k">tour</div><div className="t">no shows on file.</div></div>;
  }

  const { show, when } = pick;
  const tz = show.tz || 'UTC';
  const localTime = new Intl.DateTimeFormat('en-GB', {
    hour: '2-digit', minute: '2-digit', timeZone: tz, hour12: false,
  }).format(now);
  const localDate = new Intl.DateTimeFormat('en-CA', {
    weekday: 'short', month: 'short', day: 'numeric', timeZone: tz,
  }).format(now);

  // Weather code → short phrase (WMO code subset)
  const wmoPhrase = (c) => {
    if (c == null) return '';
    if (c === 0) return 'clear';
    if (c <= 3) return 'partly cloudy';
    if (c <= 48) return 'fog';
    if (c <= 57) return 'drizzle';
    if (c <= 67) return 'rain';
    if (c <= 77) return 'snow';
    if (c <= 82) return 'rain showers';
    if (c <= 86) return 'snow showers';
    return 'storm';
  };

  // Day-of schedule — only meaningful when show is today.
  const dayof = (show.date === new Date().toISOString().slice(0, 10))
    ? (T.dayof_template || []).map(t => {
        // Anchor doors at 21:00 local (club default).
        const doorsUTC = new Date(`${show.date}T21:00:00`);
        const eventUTC = new Date(doorsUTC.getTime() + (t.offset_h || 0) * 3600000);
        const label = new Intl.DateTimeFormat('en-GB', {
          hour: '2-digit', minute: '2-digit', timeZone: tz, hour12: false,
        }).format(eventUTC);
        const passed = eventUTC <= now;
        return { ...t, label, time: label, passed };
      })
    : null;

  return (
    <div className="pb-grant">
     <div className="pb-grant-main">
      <div className="v-verdict">
        <div className="k">sophia · {when}</div>
        <div className="t">
          <em>{show.city}</em> · {show.venue} · {show.date}
        </div>
      </div>

      <div className="v-section">local in {show.city.split(',')[0]}</div>
      <div className="v-kv">
        <div><div className="k">time now</div><div className="v">{localTime}</div></div>
        <div><div className="k">date</div><div className="v">{localDate}</div></div>
        <div><div className="k">timezone</div><div className="v">{tz}</div></div>
        {weather && (
          <>
            <div><div className="k">right now</div><div className="v">{weather.current_c}°C · {wmoPhrase(weather.code)}</div></div>
            <div><div className="k">today hi/lo</div><div className="v">{weather.hi_c}° / {weather.lo_c}°</div></div>
          </>
        )}
        {!weather && !wxErr && <div><div className="k">weather</div><div className="v">loading…</div></div>}
        {wxErr && <div><div className="k">weather</div><div className="v">— ({wxErr})</div></div>}
      </div>

      {dayof && (
        <>
          <div className="v-section">day of — schedule (doors 21:00 assumed)</div>
          <div className="v-ladder">
            {dayof.map((d, i) => (
              <div key={i} className={`v-step ${d.passed ? 'done' : 'now'}`}>
                <div className="mark">{d.passed ? '✓' : '●'}</div>
                <div>
                  <div className="lbl">{d.label} · {d.time}</div>
                  <div className="sub">{show.city} local</div>
                </div>
                <div className="tag">{d.offset_h >= 0 ? `+${d.offset_h}h` : `${d.offset_h}h`}</div>
              </div>
            ))}
          </div>
        </>
      )}

      <div className="v-section">upcoming</div>
      <div className="v-ladder">
        {(T.shows || []).filter(s => s.date >= new Date().toISOString().slice(0, 10)).slice(0, 6).map((s, i) => (
          <div key={i} className="v-step pending">
            <div className="mark">○</div>
            <div>
              <div className="lbl">{s.city}</div>
              <div className="sub">{s.venue}</div>
            </div>
            <div className="tag">{s.date}</div>
          </div>
        ))}
      </div>

      <div className="v-citations">
        <span className="k">source</span>
        <span className="n">lab/punk-brain-f/tour-sophia.yml</span>
        <span className="n">weather · open-meteo.com</span>
      </div>
     </div>
    </div>
  );
}


const VIEWS = {
  grant: {
    id: 'grant',
    kicker: 'grant · amplify bc 2026/27',
    title: 'Where the grant is.',
    meta: ['status · in flight', 'due apr 29 · 4pm pt'],
    narration: <>Pulling the <span className="tint">amplify bc grant state</span> — sections, reference letters, budget, roster…</>,
    Component: GrantStatusView,
  },
  uv: {
    id: 'uv',
    kicker: 'uv · last 30 days',
    title: 'What moved, what stalled.',
    meta: ['timeline', 'mar 04'],
    narration: <>Reading the <span className="tint">uv timeline</span> — tour, merch, press…</>,
    Component: UvTimelineView,
  },
  blockers: {
    id: 'blockers',
    kicker: 'status · waiting on you',
    title: 'What needs a decision.',
    meta: ['grant-scoped · apr 29', 'jarett actions · matty inputs · parked'],
    narration: <>Scanning the grant for things <span className="tint">blocked on you</span> vs. <span className="tint">blocked on matty</span>…</>,
    Component: BlockersView,
  },
  sophia: {
    id: 'sophia',
    kicker: 'sophia · brand deal',
    title: 'Mercury vs. Adidas.',
    meta: ['two term sheets', 'decide thurs'],
    narration: <>Diffing the <span className="tint">term sheets</span> and pulling your notes on Sophia…</>,
    Component: SophiaDealView,
  },
  money: {
    id: 'money',
    kicker: 'q1 · money',
    title: 'In, out, where we are.',
    meta: ['ytd', 'mar 04'],
    narration: <>Adding up <span className="tint">q1 so far</span>…</>,
    Component: MoneyView,
  },
  people: {
    id: 'people',
    kicker: 'warhol grant · people',
    title: 'Who touches this.',
    meta: ['7 people', '2 blocking'],
    narration: <>Pulling the <span className="tint">people around the grant</span> and what I know about them…</>,
    Component: PeopleView,
  },
  calendar: {
    id: 'calendar',
    kicker: 'the week · on your plate',
    title: 'Your week.',
    meta: ['zoomable · day · week · month', 'today anchor'],
    narration: <>Laying out the <span className="tint">week</span>, folding the weeks after it down small…</>,
    Component: CalendarView,
  },
  tour: {
    id: 'tour',
    kicker: 'sophia · tour now',
    title: 'Where Sophia is · live.',
    meta: ['current/next show', 'local time · weather · day-of'],
    narration: <>Reading <span className="tint">sophia's tour</span> — current city, local time, weather, day-of schedule…</>,
    Component: TourNowView,
  },
};

/* ============================================================
   SUGGESTED PROMPTS (context-aware to the brain state)
   Each maps to a prebuilt view id.
   ============================================================ */
// Suggestions shown under the empty-state ask bar. Only surface views that
// are wired to real data — others exist in VIEWS but aren't exposed here so
// the first-impression surface doesn't lead into mock content.
const SUGGESTIONS = [
  { prompt: "Where are we at with the grant application?", view: 'grant',    nudge: 'due apr 29' },
  { prompt: "What's blocked on me right now?",             view: 'blockers', nudge: 'decisions + inputs' },
  { prompt: "Where is Sophia right now?",                  view: 'tour',     nudge: 'live · local time + weather' },
];

/* Quick scope pills — inline-right of the docked ask bar.
   Same scrub as SUGGESTIONS: only wired views. */
const QUICK_PILLS = [
  { lbl: 'the grant', prompt: "where are we with the grant?",    view: 'grant'    },
  { lbl: 'blockers',  prompt: "what's blocked on me right now?", view: 'blockers' },
  { lbl: 'sophia · tour', prompt: "where is sophia right now?",  view: 'tour'     },
];

/* ============================================================
   CLAUDE MARK — small character on the canvas
   ============================================================ */
function ClaudeMark({ thinking, whisper }) {
  return (
    <div className={`ak-claude ${thinking ? 'thinking' : ''}`}>
      <div className="mark" title="claude">
        <div className="ring"></div>
        <svg width="36" height="36" viewBox="0 0 36 36" fill="none">
          {/* A small asterisk-star glyph — Claude-ish */}
          <g stroke="#FF6FB5" strokeWidth="1.5" strokeLinecap="round">
            <line x1="18" y1="6" x2="18" y2="30" />
            <line x1="6" y1="18" x2="30" y2="18" />
            <line x1="9.5" y1="9.5" x2="26.5" y2="26.5" />
            <line x1="26.5" y1="9.5" x2="9.5" y2="26.5" />
          </g>
          <circle cx="18" cy="18" r="3" fill="#FF6FB5" />
        </svg>
      </div>
      <div className="whisper">{whisper}</div>
    </div>
  );
}

/* Default status blurb shown in Claude's whisper when idle */
const QuietBlurb = () => (
  <>
    <span className="tag">it's quiet</span>
    <span className="spark">Four things</span> want a decision from you this week —
    the Warhol grant, UV's second Brooklyn night, Sophia's brand deal,
    and the Q2 itinerary. Stel's been offline since Tuesday.
  </>
);

/* ============================================================
   LEFT RAIL — live/open browser of Punk Brain
   ============================================================ */

const IDENTITIES = {
  jarett: { initials: 'jh', name: 'jarett', role: 'founder · punk brain', homeTz: 'America/Vancouver' },
  matty:  { initials: 'mm', name: 'matty',  role: 'founder · punk brain', homeTz: 'America/Vancouver' },
};

// Identity-aware timezone. Matty is on tour with Sophia, so his "where am I
// right now" is the tz of the current/next show within a 7-day window. Falls
// back to homeTz if no show is close. Jarett stays at homeTz.
function operatorTz(identity) {
  const I = IDENTITIES[identity] || IDENTITIES.jarett;
  if (identity !== 'matty') return I.homeTz;
  const tour = (typeof window !== 'undefined' && window.SOPHIA_TOUR) || { shows: [] };
  const today = new Date().toISOString().slice(0, 10);
  const shows = (tour.shows || []).slice().sort((a, b) => a.date.localeCompare(b.date));
  const current = shows.find(s => s.date === today);
  if (current && current.tz) return current.tz;
  const next = shows.find(s => s.date >= today);
  if (next && next.tz) {
    const days = (new Date(next.date) - new Date(today)) / 86400000;
    if (days <= 7) return next.tz;
  }
  // Recently on tour? Within 3 days after the last show we still use its tz.
  const past = shows.slice().reverse().find(s => s.date < today);
  if (past && past.tz) {
    const daysSince = (new Date(today) - new Date(past.date)) / 86400000;
    if (daysSince <= 3) return past.tz;
  }
  return I.homeTz;
}

// Hour (0-23) in an operator's current local tz.
function operatorHour(identity, when = new Date()) {
  const tz = operatorTz(identity);
  return parseInt(new Intl.DateTimeFormat('en-US', {
    hour: 'numeric', hour12: false, timeZone: tz,
  }).format(when), 10);
}

// Layered live items — items wear the voice of an inner thought, not a badge.
// The `lbl` for items that reference grant state can be a function of identity;
// AskRail resolves these at render time.
const _numWord = (n) => ({ 1:'one', 2:'two', 3:'three', 4:'four', 5:'five', 6:'six', 7:'seven', 8:'eight', 9:'nine' }[n] || String(n));

// Scrubbed for first-impression demo — only wired items surface in the rail.
// UV and Different Gear layers are deliberately empty: they'll fill in as
// their views get wired to real state. Dropping them avoids Matt clicking
// into mock content (Brooklyn Steel, Stel, Nadia, the Warhol grant, etc.).
const RAIL_LAYERS = [
  {
    id: 'shared', name: 'punk brain', tone: 'pink',
    items: [
      {
        id: 'blockers',
        lbl: (identity) => {
          const G = window.AMPLIFY_BC || {};
          const field = (OPERATOR_MAP[identity] || OPERATOR_MAP.jarett).mine;
          const n = (G[field] || []).length;
          return `${_numWord(n)} things are waiting on you`;
        },
        meta: 'grant · t-9', open: true, tone: 'open',
      },
    ],
  },
];

const RAIL_GLYPHS = {
  blockers: '◆', money: '$', people: '◎',
  uv: '▶', 'uv-bk2': '▲', 'merch-03': '◇',
  sophia: '◉', grant: '★', q2: '▢', lua: '◆',
};

function greeting(identity, openCount) {
  const h = operatorHour(identity);
  const name = identity;
  let timeOfDay;
  if (h < 5) timeOfDay = 'still up,';
  else if (h < 11) timeOfDay = 'morning,';
  else if (h < 14) timeOfDay = 'middle of the day,';
  else if (h < 18) timeOfDay = 'afternoon,';
  else if (h < 22) timeOfDay = 'evening,';
  else timeOfDay = 'late,';
  const trail = openCount === 0
    ? "nothing pulling at you yet."
    : openCount === 1
    ? "one thing’s tugging."
    : `${openCount} things are tugging, softly.`;
  return { lead: `${timeOfDay} ${name}.`, trail };
}

function IdentityChip({ identity, onSwap, collapsed, openCount }) {
  const I = IDENTITIES[identity];
  const g = greeting(identity, openCount);
  return (
    <div className="ident" onClick={onSwap} title={`looking through ${I.name}’s eyes — click to swap`}>
      <div className="av">{I.initials}</div>
      <div className="info">
        <span className="lead">{g.lead}</span>
        <span className="trail">{g.trail}</span>
      </div>
      <div className="swap">⇄</div>
    </div>
  );
}

function AskRail({ collapsed, toggleCollapsed, identity, swapIdentity, openFile, activeFile, filter, setFilter, recents, reopenRecent, dismissRecent, chatOpen, toggleChat }) {
  const openCount = RAIL_LAYERS.reduce((n, L) => n + L.items.filter(i => i.open).length, 0);
  const totalCount = RAIL_LAYERS.reduce((n, L) => n + L.items.length, 0);
  const activeItem = RAIL_LAYERS.flatMap(L => L.items).find(i => i.id === activeFile);

  const askCopy = chatOpen
    ? 'never mind'
    : activeItem
    ? `ask about this…`
    : 'ask something…';

  return (
    <aside className={`ak-rail${collapsed ? ' collapsed' : ''}`}>
      <button className="collapse-btn" onClick={toggleCollapsed} title={collapsed ? 'expand' : 'collapse'}>
        {collapsed ? '›' : '‹'}
      </button>

      <IdentityChip identity={identity} onSwap={swapIdentity} collapsed={collapsed} openCount={openCount} />

      <div className="filter">
        <span className={`tab${filter === 'open' ? ' on' : ''}`} onClick={() => setFilter('open')}>
          <em>needs you</em> <span className="count">{openCount}</span>
        </span>
        <span className={`tab${filter === 'all' ? ' on' : ''}`} onClick={() => setFilter('all')}>
          <em>everything here</em> <span className="count">{totalCount}</span>
        </span>
      </div>

      <div className="scroll">
        {RAIL_LAYERS.map(L => {
          const visible = L.items.filter(i => filter === 'all' || i.open);
          if (visible.length === 0 && filter === 'open') return null;
          return (
            <React.Fragment key={L.id}>
              <div className={`layer ${L.tone}${visible.length === 0 ? ' mute' : ''}`}>
                <span className="name">{L.name}</span>
              </div>
              {visible.map(it => {
                const lbl = typeof it.lbl === 'function' ? it.lbl(identity) : it.lbl;
                return (
                  <div
                    key={it.id}
                    className={`item ${it.tone || ''}${it.open ? ' open' : ''}${activeFile === it.id ? ' active' : ''}`}
                    onClick={() => openFile(it.id)}
                    title={lbl}
                  >
                    <span className="dot"></span>
                    <span className="lbl">{lbl}</span>
                    <span className="meta">{it.meta}</span>
                  </div>
                );
              })}
            </React.Fragment>
          );
        })}
      </div>

      {recents.length > 0 && (
        <div className="recents">
          <div className="hdr">
            <span>still with you</span>
            <span className="hint">fades after a while</span>
          </div>
          {recents.map(r => (
            <div key={r.id} className="r" onClick={() => reopenRecent(r.id)} title={r.lbl}>
              <span className="g">{RAIL_GLYPHS[r.id] || '·'}</span>
              <span className="lbl">{r.lbl}</span>
              <span className="t">{r.ago}</span>
              <span className="x" onClick={(e) => { e.stopPropagation(); dismissRecent(r.id); }}>×</span>
            </div>
          ))}
        </div>
      )}
    </aside>
  );
}
const CITIES = [
  { key: 'LA',  label: 'los angeles', tz: 'America/Los_Angeles', weather: '52° clear' },
  { key: 'NY',  label: 'new york',    tz: 'America/New_York',    weather: '41° grey' },
  { key: 'LDN', label: 'london',      tz: 'Europe/London',       weather: '46° rain' },
  { key: 'BER', label: 'berlin',      tz: 'Europe/Berlin',       weather: '39° fog' },
];

function todFor(hour) {
  if (hour >= 5 && hour < 9) return 'dawn';
  if (hour >= 9 && hour < 17) return 'day';
  if (hour >= 17 && hour < 20) return 'dusk';
  return 'night';
}

function cityFeel(tod) {
  if (tod === 'dawn')  return 'waking up';
  if (tod === 'day')   return 'bright';
  if (tod === 'dusk')  return 'soft';
  return 'asleep';
}

function DateBar() {
  const identity = useContext(IdentityContext);
  const [now, setNow] = useState(new Date());
  const [open, setOpen] = useState(false);
  useEffect(() => {
    const i = setInterval(() => setNow(new Date()), 30_000);
    return () => clearInterval(i);
  }, []);

  const tz = operatorTz(identity);
  const dow = new Intl.DateTimeFormat('en-US', { weekday: 'long', timeZone: tz }).format(now).toLowerCase();
  const datePart = new Intl.DateTimeFormat('en-US', { month: 'long', day: 'numeric', timeZone: tz }).format(now).toLowerCase();
  const h = operatorHour(identity, now);
  let phase;
  if (h < 6) phase = 'early';
  else if (h < 11) phase = 'morning';
  else if (h < 14) phase = 'midday';
  else if (h < 18) phase = 'afternoon';
  else if (h < 22) phase = 'evening';
  else phase = 'late';

  return (
    <div className={`ak-timewidget${open ? ' open' : ''}`} onClick={() => setOpen(v => !v)}>
      <div className="glyph" title="the day, the cities">
        <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.6" strokeLinecap="round" strokeLinejoin="round">
          <circle cx="12" cy="12" r="9"/>
          <path d="M12 3v9l5 3"/>
        </svg>
      </div>
      <div className="ak-datebar">
        <div className="ak-date">
          <span className="dow">{dow}</span>
          <span className="sep">·</span>
          <span className="phase">{phase}</span>
          <span className="sep">·</span>
          <span className="day">{datePart}</span>
        </div>
        <div className="ak-cities">
          {CITIES.map(c => {
            const hh = parseInt(new Intl.DateTimeFormat('en-US', { hour: 'numeric', hour12: false, timeZone: c.tz }).format(now), 10);
            const timeStr = new Intl.DateTimeFormat('en-US', { hour: 'numeric', minute: '2-digit', hour12: true, timeZone: c.tz }).format(now).toLowerCase().replace(' ', '');
            const tod = todFor(hh);
            const feel = cityFeel(tod);
            return (
              <div key={c.key} className={`ak-city ${tod}`} title={`${c.label}, ${feel}`}>
                <span className="sun"></span>
                <span className="cty">{c.label}</span>
                <span className="time">{timeStr}</span>
              </div>
            );
          })}
        </div>
      </div>
    </div>
  );
}

/* ============================================================
   AMBIENT EDGE (removed — constellation deleted per request)
   ============================================================ */

/* ============================================================
   GENERATED CARD
   ============================================================ */
function Card({ view, depth, materializing }) {
  const V = VIEWS[view];
  if (!V) return null;
  const { Component } = V;
  return (
    <div className={`ak-card ${materializing ? 'materializing' : ''}`} data-depth={depth}>
      <div className="ak-card-head">
        <div>
          <div className="kicker">{V.kicker}</div>
          <h2>{V.title}</h2>
        </div>
        <div className="meta">
          {V.meta.map((m, i) => (
            <React.Fragment key={i}>
              {i > 0 && <span className="sep">/</span>}
              {m}
            </React.Fragment>
          ))}
        </div>
      </div>
      <div className="ak-card-body">
        <Component />
      </div>
    </div>
  );
}

/* ============================================================
   TWEAKS PANEL — palette + noise variants
   ============================================================ */
const DEFAULTS = /*EDITMODE-BEGIN*/{
  "palette": "pink",
  "noise": "balanced",
  "autoOpen": "grant"
}/*EDITMODE-END*/;

const PALETTES = [
  { id: 'pink',     label: 'pink',      sub: 'the usual warmth' },
  { id: 'green',    label: 'green',     sub: 'electric, alive' },
  { id: 'duo',      label: 'pink+green', sub: 'both, in tension' },
  { id: 'light',    label: 'light',     sub: 'white · pink · green · black' },
  { id: 'mono',     label: 'mono',      sub: 'black, white, gray' },
  { id: 'whiteout', label: 'whiteout',  sub: 'paper-bright' },
];

const NOISES = [
  { id: 'quiet',    label: 'quieter',   sub: 'less accent' },
  { id: 'balanced', label: 'balanced',  sub: 'as-is' },
  { id: 'loud',     label: 'noisier',   sub: 'more accent' },
];

function Tweaks({ show, tweaks, setTweaks }) {
  useEffect(() => {
    document.body.dataset.palette = tweaks.palette || 'pink';
    document.body.dataset.noise = tweaks.noise || 'balanced';
  }, [tweaks.palette, tweaks.noise]);

  if (!show) return null;
  return (
    <div className={`ak-tweaks ${show ? 'show' : ''}`}>
      <h3>Tweaks</h3>
      <div className="row">
        <div className="lbl">palette</div>
        <div className="opts opts-col">
          {PALETTES.map(p => (
            <button key={p.id}
              className={`opt ${tweaks.palette === p.id ? 'on' : ''}`}
              onClick={() => setTweaks({...tweaks, palette: p.id})}>
              <span className="swatches" data-palette={p.id}>
                <span className="sw a"></span>
                <span className="sw b"></span>
                <span className="sw c"></span>
              </span>
              <span className="lbl2">{p.label}</span>
              <span className="sub2">{p.sub}</span>
            </button>
          ))}
        </div>
      </div>
      <div className="row">
        <div className="lbl">accent</div>
        <div className="opts">
          {NOISES.map(n => (
            <button key={n.id}
              className={`opt ${tweaks.noise === n.id ? 'on' : ''}`}
              onClick={() => setTweaks({...tweaks, noise: n.id})}>{n.label}</button>
          ))}
        </div>
      </div>
    </div>
  );
}

/* ============================================================
   APP
   ============================================================ */
function App() {
  // Initial palette: URL ?palette= > localStorage (set by theme.js) > DEFAULTS.
  const [tweaks, setTweaks] = useState(() => {
    const qp = new URLSearchParams(window.location.search);
    let palette = qp.get('palette');
    if (!palette) {
      try { palette = localStorage.getItem('pb-palette'); } catch (e) {}
    }
    return {
      ...DEFAULTS,
      palette: palette || DEFAULTS.palette,
      noise:   qp.get('noise') || DEFAULTS.noise,
    };
  });

  // Listen for theme-toggle clicks from theme.js and sync React state.
  useEffect(() => {
    const onPaletteChange = (e) => {
      setTweaks(t => ({ ...t, palette: e.detail.palette }));
    };
    window.addEventListener('palette-change', onPaletteChange);
    return () => window.removeEventListener('palette-change', onPaletteChange);
  }, []);

  // Persist palette changes from the Tweaks panel back to localStorage so
  // theme.js (and other pages) see the same value.
  useEffect(() => {
    try { localStorage.setItem('pb-palette', tweaks.palette); } catch (e) {}
  }, [tweaks.palette]);
  const [showTweaks, setShowTweaks] = useState(false);
  const [stack, setStack] = useState([]);          // [viewId, ...] most recent first
  const [query, setQuery] = useState('');
  const [generating, setGenerating] = useState(null); // viewId currently crystallizing
  const [thinking, setThinking] = useState(false);
  const [justMaterialized, setJustMaterialized] = useState(null);
  const inputRef = useRef(null);

  // New sidebar/chat model
  const [railCollapsed, setRailCollapsed] = useState(false);
  // Identity: URL ?as=matty|jarett overrides default (jarett = repo owner)
  const [identity, setIdentity] = useState(() => {
    const qp = new URLSearchParams(window.location.search).get('as');
    return (qp && IDENTITIES[qp]) ? qp : 'jarett';
  });
  const [chatOpen, setChatOpen] = useState(true);   // spotlight visible by default
  const [railFilter, setRailFilter] = useState('open');
  const [recents, setRecents] = useState([]);       // [{id, lbl, ago, at}]

  const docked = stack.length > 0;
  const activeFile = stack[0] || null;

  /* mirror states on <body> for CSS rules */
  useEffect(() => {
    document.body.classList.toggle('ak-docked', docked);
    document.body.classList.toggle('file-open', docked);
  }, [docked]);
  useEffect(() => {
    document.body.classList.toggle('rail-collapsed', railCollapsed);
  }, [railCollapsed]);
  useEffect(() => {
    document.body.classList.toggle('chat-open', chatOpen);
    if (chatOpen || docked) setTimeout(() => inputRef.current?.focus(), 60);
  }, [chatOpen]);
  useEffect(() => {
    document.body.classList.toggle('generating', !!generating);
  }, [generating]);

  /* ⌘K toggles chat; Esc closes chat or pops file */
  useEffect(() => {
    const onKey = (e) => {
      if ((e.metaKey || e.ctrlKey) && e.key === 'k') {
        e.preventDefault();
        setChatOpen(v => !v);
        return;
      }
      if (e.key === 'Escape') {
        if (chatOpen) { setChatOpen(false); return; }
        if (docked) { setStack(s => s.slice(1)); }
      }
    };
    window.addEventListener('keydown', onKey);
    return () => window.removeEventListener('keydown', onKey);
  }, [docked, chatOpen]);

  /* Open a file from the rail (or chat). Demote current top to recents. */
  const openFile = (viewId) => {
    if (!VIEWS[viewId]) return;
    setStack(s => {
      const prev = s[0];
      if (prev && prev !== viewId) {
        const V = VIEWS[prev];
        setRecents(r => [
          { id: prev, lbl: V?.kicker || prev, ago: 'just now', at: Date.now() },
          ...r.filter(x => x.id !== prev && x.id !== viewId)
        ].slice(0, 8));
      }
      return [viewId, ...s.filter(v => v !== viewId)];
    });
    setJustMaterialized(viewId);
    setTimeout(() => setJustMaterialized(null), 800);
  };
  const reopenRecent = (id) => {
    setRecents(r => r.filter(x => x.id !== id));
    openFile(id);
  };
  const dismissRecent = (id) => setRecents(r => r.filter(x => x.id !== id));
  const swapIdentity = () => setIdentity(i => i === 'jarett' ? 'matty' : 'jarett');

  /* Prune recents every minute; update relative time labels */
  useEffect(() => {
    const tick = () => {
      setRecents(r => {
        const now = Date.now();
        return r
          .filter(x => now - x.at < 1000 * 60 * 60 * 4) // 4h window
          .map(x => {
            const m = Math.floor((now - x.at) / 60000);
            return { ...x, ago: m < 1 ? 'just now' : m < 60 ? `${m}m` : `${Math.floor(m/60)}h` };
          });
      });
    };
    const id = setInterval(tick, 30000);
    return () => clearInterval(id);
  }, []);

  /* Edit mode handshake */
  useEffect(() => {
    const onMsg = (e) => {
      if (e.data?.type === '__activate_edit_mode') setShowTweaks(true);
      if (e.data?.type === '__deactivate_edit_mode') setShowTweaks(false);
      // Drive a view from a parent frame (welcome page demo cycler).
      if (e.data?.type === '__run_view' && e.data.id && VIEWS[e.data.id]) {
        runView(e.data.id);
      }
    };
    window.addEventListener('message', onMsg);
    window.parent.postMessage({ type: '__edit_mode_available' }, '*');
    return () => window.removeEventListener('message', onMsg);
  }, []);

  // Allow landing page (Shared Brain.html) to open a specific file on load
  // via `#open=<id>` — lets the shared rail navigate between pages.
  // Also: `#grant/<component>` auto-opens the grant view + expands to that component.
  // We leave the grant-form hash in place so GrantStatusView can consume it.
  useEffect(() => {
    const h = window.location.hash;
    const grantMatch = h.match(/#grant\/([a-z0-9-]+)/i);
    if (grantMatch) {
      setTimeout(() => runView('grant'), 40);
      return;
    }
    const m = h.match(/open=([a-z0-9-]+)/i);
    if (!m) return;
    const id = m[1];
    if (!VIEWS[id]) return;
    history.replaceState(null, '', window.location.pathname + window.location.search);
    setTimeout(() => runView(id), 40);
  }, []);

  const runView = (viewId) => {
    setThinking(true);
    setGenerating(viewId);
    // brief narration, then snap
    setTimeout(() => {
      // demote previous to recents before pushing new
      const prev = stack[0];
      if (prev && prev !== viewId) {
        const V = VIEWS[prev];
        setRecents(r => [
          { id: prev, lbl: V?.kicker || prev, ago: 'just now', at: Date.now() },
          ...r.filter(x => x.id !== prev && x.id !== viewId)
        ].slice(0, 8));
      }
      setStack(s => [viewId, ...s.filter(v => v !== viewId)]);
      setGenerating(null);
      setThinking(false);
      setJustMaterialized(viewId);
      setQuery('');
      setChatOpen(false); // clear the blur; docked ask bar stays beneath
      setTimeout(() => setJustMaterialized(null), 800);
    }, 900);
  };

  const submit = (e) => {
    e?.preventDefault?.();
    const q = (query || '').toLowerCase();
    if (!q.trim()) return;
    // match against suggestions first
    let viewId = SUGGESTIONS.find(s => s.prompt.toLowerCase() === q)?.view;
    if (!viewId) {
      // loose keyword match for open typing
      // Only route to wired views. Everything else falls through to grant so
      // a stray query doesn't drop the viewer into mock content.
      if (q.includes('block') || q.includes('decision') || q.includes('waiting')) viewId = 'blockers';
      else viewId = 'grant';
    }
    runView(viewId);
  };

  const ambientShow = null;

  const whisper = useMemo(() => {
    if (thinking) return <><span className="tag">pulling threads…</span></>;
    return <QuietBlurb />;
  }, [thinking]);

  return (
    <IdentityContext.Provider value={identity}>
      <div className="ak-canvas" />

      {/* Left rail — live browser of the brain */}
      <AskRail
        collapsed={railCollapsed}
        toggleCollapsed={() => setRailCollapsed(v => !v)}
        identity={identity}
        swapIdentity={swapIdentity}
        openFile={(id) => { setChatOpen(false); runView(id); }}
        activeFile={activeFile}
        filter={railFilter}
        setFilter={setRailFilter}
        recents={recents}
        reopenRecent={(id) => { setChatOpen(false); reopenRecent(id); }}
        dismissRecent={dismissRecent}
        chatOpen={chatOpen}
        toggleChat={() => setChatOpen(v => !v)}
      />

      <DateBar />

      <ClaudeMark thinking={thinking} whisper={whisper} />

      {/* Empty-canvas invitation: nothing open, chat closed */}
      {!docked && !chatOpen && (() => {
        const G = window.AMPLIFY_BC || {};
        const opMap = OPERATOR_MAP[identity] || OPERATOR_MAP.jarett;
        const mineCount = (G[opMap.mine] || []).length;
        const countWord = _numWord(mineCount);
        const DEADLINE = new Date('2026-04-29T16:00:00-07:00');
        const daysLeft = Math.max(0, Math.ceil((DEADLINE - new Date()) / 86400000));
        const youLabel = identity === 'matty'
          ? `${countWord} grant inputs are waiting on you`
          : `${countWord} grant decisions are waiting on you`;
        return (
          <div className="ak-empty">
            <div className="quiet">it’s quiet in here.</div>
            <div className="ak-empty-frame">
              <span className="nm">{identity}</span>
              <span className="sep">·</span>
              <span className="msg"><em>{youLabel}</em></span>
              <span className="sep">·</span>
              <span className="tod">t-{daysLeft} · apr 29</span>
            </div>
            <h1 className="lead">
              what do you need to <span className="tint">see</span>?
            </h1>
            <button className="invite" onClick={() => setChatOpen(true)}>
              <span className="kbd">⌘K</span>
              <span className="t">to ask</span>
            </button>
            <div className="soft">or tap <em>the grant</em> or <em>blockers</em> below</div>
          </div>
        );
      })()}

      {/* stack of generated cards — click the backdrop (outside the card) to close */}
      {stack.length > 0 && (
        <div
          className="ak-stack"
          onClick={(e) => { if (e.target === e.currentTarget) setStack([]); }}
        >
          <div
            className="ak-card-area"
            onClick={(e) => { if (e.target === e.currentTarget) setStack([]); }}
          >
            {stack.slice(0, 4).map((vid, i) => (
              <Card key={vid + '-' + i} view={vid} depth={i} materializing={i === 0 && justMaterialized === vid} />
            ))}
          </div>
        </div>
      )}

      {/* stack meta */}
      {stack.length > 0 && (
        <div className="ak-stack-meta">
          <div className="ak-stack-count">
            <span><span className="n">{stack.length}</span> view{stack.length !== 1 ? 's' : ''} in stack</span>
            {stack.length > 1 && (
              <button onClick={() => setStack(s => [...s.slice(1), s[0]])}>flip</button>
            )}
            <button onClick={() => setStack([])}>clear</button>
          </div>
        </div>
      )}

      {/* spotlight / chat — centered is toggled by ⌘K; docked stays beneath any open view */}
      {(chatOpen || docked) && (
      <div className={`ak-spot-wrap ${docked ? 'docked' : 'center'}`}>
        <div className={`ak-spot${query.trim() ? ' typing' : ''}`}>
          <form className="ak-input" onSubmit={submit}>
            <span className="prefix">⌘K</span>
            <input
              ref={inputRef}
              value={query}
              onChange={e => setQuery(e.target.value)}
              placeholder={docked ? 'ask something else…' : 'where are we at with the grant application?'}
              autoFocus
            />
            <span className="kbd">↵</span>
          </form>

          {docked && (
            <div className="ak-pills" aria-hidden={!!query.trim()}>
              {QUICK_PILLS.map((p, i) => (
                <button key={i}
                  className="pill"
                  type="button"
                  onClick={() => { setQuery(p.prompt); setTimeout(() => runView(p.view), 40); }}>
                  {p.lbl}
                </button>
              ))}
            </div>
          )}

          {!docked && (
            <div className="ak-suggest">
              <div className="heading">try</div>
              {SUGGESTIONS.slice(0, 4).map((s, i) => (
                <button key={i} className="s" onClick={() => { setQuery(s.prompt); setTimeout(() => runView(s.view), 40); }}>
                  {s.prompt}
                  <span className="nudge">· {s.nudge}</span>
                </button>
              ))}
            </div>
          )}
        </div>
      </div>
      )}

      {/* chat toggle lives in the rail now */}

      {/* generating overlay */}
      {generating && VIEWS[generating] && (
        <div className="ak-generating">
          <div className="inner">
            <ElectricBrain />
            <div className="narration">{VIEWS[generating].narration}</div>
          </div>
        </div>
      )}

      {/* hint removed — ⌘K prefix in the input is enough */}

      <Tweaks show={showTweaks} tweaks={tweaks} setTweaks={setTweaks} />
    </IdentityContext.Provider>
  );
}

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