/* runmode.jsx — live at-the-table reference.
   Two layouts: "focus" (single calm column, progressive disclosure — default)
   and "board" (the original masonry). Depends on ui.jsx + data.js (window). */

// ── MarkdownText: renders body text as full markdown via marked ───────────────
function MarkdownText({ text, className, highlight, onCardLinkClick }) {
  if (!text) return null;
  let html = window.marked.parse(window.renderableMarkdown(text));
  if (highlight) {
    const esc = highlight.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
    // Pass HTML tags through unchanged; wrap bare text matches in <mark>
    html = html.replace(
      new RegExp(`(<[^>]*>)|(${esc})`, 'gi'),
      (m, tag, word) => tag ? tag : `<mark class="run-hl">${word}</mark>`
    );
  }
  return <div className={className} dangerouslySetInnerHTML={{ __html: html }}
    onClick={onCardLinkClick ? (e) => {
      const link = e.target.closest('.card-link');
      if (link) { e.preventDefault(); onCardLinkClick(link.dataset.cardTitle); }
    } : undefined} />;
}

// ── Hl: inline highlight for plain-text JSX nodes ────────────────────────────
function Hl({ text, q }) {
  if (!q || !text) return text || null;
  const esc = q.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
  const parts = text.split(new RegExp(`(${esc})`, 'gi'));
  if (parts.length === 1) return text;
  return parts.map((p, i) => i % 2 === 1 ? <mark key={i} className="run-hl">{p}</mark> : p);
}

const RUN_ORDER = ["combat", "secrets", "scenes", "npcs", "locations", "monsters", "items", "characters", "prepNotes", "notes"];
const RUN_META = {
  start: { icon: "flag", label: "Strong start", title: "Strong start" },
  combat: { icon: "sword", label: "Combat", title: "Combat" },
  secrets: { icon: "key", label: "Secrets", title: "Secrets" },
  scenes: { icon: "scroll", label: "Scenes", title: "Scenes" },
  npcs: { icon: "person", label: "NPCs", title: "NPCs" },
  locations: { icon: "map", label: "Locations", title: "Locations" },
  monsters: { icon: "skull", label: "Monsters", title: "Monsters" },
  items: { icon: "gem", label: "Rewards", title: "Rewards" },
  characters: { icon: "users", label: "Party", title: "The party" },
  prepNotes: { icon: "feather", label: "Prep notes", title: "Prep notes" },
  notes: { icon: "pen", label: "Notes", title: "Session notes" }
};

const DEFAULT_COMBAT = { active: false, round: 1, activeCombatantId: null, combatants: [] };

// ── Square thumbnail with size variant (S/M/L driven by imageSize on the card) ─
function RunItemImage({ src, size = "thumb" }) {
  if (!src) return null;
  return (
    <div className={`run-item-img run-item-img--${size}`}>
      <img src={src} alt="" />
    </div>
  );
}

// ── Click props that make a card open its detail modal (mouse + keyboard) ─────
function cardOpenProps(openDetail, stepId, item) {
  const open = () => openDetail(stepId, item);
  return {
    className: "run-clickable",
    role: "button",
    tabIndex: 0,
    onClick: open,
    onKeyDown: (e) => { if (e.key === "Enter" || e.key === " ") { e.preventDefault(); open(); } },
  };
}

// ── Card detail modal: full image (tap for lightbox) + all of the card's text ─
function RunCardModal({ stepId, item, resolved, onResolve, onClose, onCardLinkClick }) {
  const [lightboxOpen, setLightboxOpen] = React.useState(false);
  const [blobUrl, setBlobUrl] = React.useState(null);
  const src = item.image;

  React.useEffect(() => {
    if (!lightboxOpen || !src) return;
    // Images are data URIs offline but become Supabase storage URLs once synced —
    // only the former needs (and can be) converted to a blob URL.
    const isData = src.startsWith("data:");
    const url = isData ? window.dataUriToBlobUrl(src) : src;
    setBlobUrl(url);
    return () => { if (isData) URL.revokeObjectURL(url); setBlobUrl(null); };
  }, [lightboxOpen, src]);

  React.useEffect(() => {
    if (!lightboxOpen) return;
    // Capture + stopPropagation so Escape closes the lightbox first, not the modal.
    const onKey = (e) => { if (e.key === 'Escape') { e.stopPropagation(); setLightboxOpen(false); } };
    document.addEventListener('keydown', onKey, true);
    return () => document.removeEventListener('keydown', onKey, true);
  }, [lightboxOpen]);

  const stepDef = (window.RPG.STEP_DEFS || []).find((s) => s.id === stepId) || { fields: [] };
  const fields = stepDef.fields.filter((f) => f.key !== "title");
  const title = item.title || item.body?.split('\n')[0]?.slice(0, 50) || "Untitled";

  return (
    <window.Modal title={title} onClose={onClose} width={620}>
      <div className="card-expand-body run-detail">
        {src && (
          <div className="card-expand-image run-detail-image" onClick={() => setLightboxOpen(true)} title="Tap to view full size">
            <img src={src} alt="" />
          </div>
        )}
        {fields.map((f) => {
          const val = item[f.key];
          if (f.kind === "aspects")
            return Array.isArray(val) && val.length
              ? <span key={f.key} className="run-aspects">{val.map((a, i) => <i key={i}>{a}</i>)}</span>
              : null;
          if (!val) return null;
          if (f.kind === "area")
            return <div key={f.key} className="run-detail-body"><MarkdownText text={val} onCardLinkClick={onCardLinkClick} /></div>;
          return <span key={f.key} className="run-tag run-detail-tag">{val}</span>;
        })}
        {onResolve && (
          <div className="run-resolve-row">
            <button type="button" className={`btn btn-sm${resolved ? " btn-accent" : ""}`} onClick={onResolve}>
              <window.Icon name="check" size={14} /> {resolved ? "Resolved — tap to undo" : "Mark resolved"}
            </button>
          </div>
        )}
      </div>
      {lightboxOpen && blobUrl && (
        <div className="img-lightbox" onClick={() => setLightboxOpen(false)}>
          <img src={blobUrl} alt="" className="img-lightbox-img" onClick={(e) => e.stopPropagation()} />
        </div>
      )}
    </window.Modal>
  );
}

// ── Combat Tracker ────────────────────────────────────────────────────────────
function CombatSection({ session, setSession, d }) {
  const uid = window.RPG.uid;
  const combat = (session.runtime && session.runtime.combat) || DEFAULT_COMBAT;

  const setCombat = (next) => setSession((sess) => {
    const prev = (sess.runtime && sess.runtime.combat) || DEFAULT_COMBAT;
    const nextVal = typeof next === "function" ? next(prev) : next;
    return { ...sess, runtime: { ...(sess.runtime || {}), combat: nextVal } };
  });

  const [showAdd, setShowAdd] = React.useState(false);
  const [addName, setAddName] = React.useState("");
  const [addInit, setAddInit] = React.useState("");
  const [addMaxHp, setAddMaxHp] = React.useState("");
  const [editingHpId, setEditingHpId] = React.useState(null);
  const [editingHpVal, setEditingHpVal] = React.useState("");

  const sorted = React.useMemo(() => [...(combat.combatants || [])].sort((a, b) => {
    if (a.defeated !== b.defeated) return a.defeated ? 1 : -1;
    return (b.initiative || 0) - (a.initiative || 0);
  }), [combat.combatants]);

  const living = sorted.filter((c) => !c.defeated);

  const setCombatant = (id, patch) => setCombat((prev) => ({
    ...prev,
    combatants: (prev.combatants || []).map((c) => c.id === id ? { ...c, ...patch } : c),
  }));

  const adjustHp = (id, delta) => setCombat((prev) => ({
    ...prev,
    combatants: (prev.combatants || []).map((c) => {
      if (c.id !== id) return c;
      const next = Math.max(0, c.maxHp != null ? Math.min(c.maxHp, (c.currentHp || 0) + delta) : (c.currentHp || 0) + delta);
      return { ...c, currentHp: next };
    }),
  }));

  const setHp = (id, hp) => setCombat((prev) => ({
    ...prev,
    combatants: (prev.combatants || []).map((c) => {
      if (c.id !== id) return c;
      const clamped = Math.max(0, c.maxHp != null ? Math.min(c.maxHp, hp) : hp);
      return { ...c, currentHp: clamped };
    }),
  }));

  const toggleDefeated = (id) => setCombat((prev) => ({
    ...prev,
    combatants: (prev.combatants || []).map((c) => c.id === id ? { ...c, defeated: !c.defeated } : c),
  }));

  const removeCombatant = (id) => setCombat((prev) => ({
    ...prev,
    combatants: (prev.combatants || []).filter((c) => c.id !== id),
    activeCombatantId: prev.activeCombatantId === id ? null : prev.activeCombatantId,
  }));

  const doAdd = () => {
    if (!addName.trim()) return;
    const maxHp = addMaxHp ? parseInt(addMaxHp) : null;
    setCombat((prev) => ({
      ...prev,
      combatants: [...(prev.combatants || []), {
        id: uid(), name: addName.trim(),
        initiative: parseInt(addInit) || 0,
        maxHp, currentHp: maxHp,
        defeated: false, sourceStepId: null, sourceId: null,
      }],
    }));
    setAddName(""); setAddInit(""); setAddMaxHp(""); setShowAdd(false);
  };

  const addFromPrep = (name, stepId, sourceId) => setCombat((prev) => ({
    ...prev,
    combatants: [...(prev.combatants || []), {
      id: uid(), name, initiative: 0,
      maxHp: null, currentHp: null,
      defeated: false, sourceStepId: stepId, sourceId,
    }],
  }));

  const nextTurn = () => {
    if (!living.length) return;
    const idx = living.findIndex((c) => c.id === combat.activeCombatantId);
    const nextIdx = idx + 1;
    const wraps = nextIdx >= living.length;
    setCombat((prev) => ({
      ...prev,
      round: wraps ? prev.round + 1 : prev.round,
      activeCombatantId: living[wraps ? 0 : nextIdx].id,
    }));
  };

  const startCombat = () => setCombat((prev) => ({
    ...prev, active: true,
    activeCombatantId: living.length ? living[0].id : null,
  }));

  const endCombat = () => setCombat(DEFAULT_COMBAT);

  const addedIds = new Set((combat.combatants || []).map((c) => c.sourceId).filter(Boolean));
  const prepCards = [
    ...(d.monsters || []).map((m) => ({ id: m.id, title: m.title, stepId: "monsters" })),
    ...(d.npcs || []).map((n) => ({ id: n.id, title: n.title, stepId: "npcs" })),
    ...(d.characters || []).map((c) => ({ id: c.id, title: c.title, stepId: "characters" })),
  ].filter((c) => !addedIds.has(c.id));

  const addForm = showAdd && (
    <form className="combat-add-form" onSubmit={(e) => { e.preventDefault(); doAdd(); }}>
      <input className="combat-form-name" placeholder="Name" value={addName} onChange={(e) => setAddName(e.target.value)} autoFocus />
      <input className="combat-form-num" type="number" placeholder="Init" value={addInit} onChange={(e) => setAddInit(e.target.value)} />
      <input className="combat-form-num" type="number" placeholder="Max HP" value={addMaxHp} onChange={(e) => setAddMaxHp(e.target.value)} />
      <button type="submit" className="btn btn-sm btn-accent">Add</button>
      <button type="button" className="btn btn-sm" onClick={() => { setShowAdd(false); setAddName(""); setAddInit(""); setAddMaxHp(""); }}>Cancel</button>
    </form>
  );

  const prepPicks = !showAdd && prepCards.length > 0 && (
    <div className="combat-prep-picks">
      <p className="combat-prep-label">Add from prep</p>
      <div className="combat-prep-list">
        {prepCards.map((c) => (
          <button key={c.id} className="combat-prep-pick" onClick={() => addFromPrep(c.title, c.stepId, c.id)}>
            <window.Icon name={c.stepId === "monsters" ? "skull" : c.stepId === "characters" ? "users" : "person"} size={12} /> {c.title}
          </button>
        ))}
      </div>
    </div>
  );

  if (!combat.active) {
    return (
      <div className="combat-idle">
        {sorted.length > 0 && (
          <ul className="combat-setup-list">
            {sorted.map((c) => (
              <li key={c.id} className="combat-setup-row">
                <span className="combat-setup-name">{c.name}</span>
                <input className="combat-form-num" type="number" placeholder="Init"
                  value={c.initiative || ""}
                  onChange={(e) => setCombatant(c.id, { initiative: parseInt(e.target.value) || 0 })} />
                <input className="combat-form-num" type="number" placeholder="HP"
                  value={c.maxHp != null ? c.maxHp : ""}
                  onChange={(e) => { const v = e.target.value ? parseInt(e.target.value) : null; setCombatant(c.id, { maxHp: v, currentHp: v }); }} />
                <button type="button" className="combat-remove-btn" onClick={() => removeCombatant(c.id)}><window.Icon name="x" size={13} /></button>
              </li>
            ))}
          </ul>
        )}
        {addForm}
        {prepPicks}
        <div className="combat-actions">
          {!showAdd && <button className="btn btn-sm" onClick={() => setShowAdd(true)}><window.Icon name="plus" size={14} /> Add combatant</button>}
          {sorted.length > 0 && !showAdd && (
            <button className="btn btn-sm btn-accent" onClick={startCombat}><window.Icon name="sword" size={14} /> Start combat</button>
          )}
        </div>
      </div>
    );
  }

  return (
    <div className="combat-active">
      <div className="combat-controls">
        <span className="combat-round">Round {combat.round}</span>
        <button className="btn btn-sm" onClick={nextTurn}><window.Icon name="play" size={13} /> Next turn</button>
        {!showAdd && <button className="btn btn-sm" onClick={() => setShowAdd(true)}><window.Icon name="plus" size={14} /> Add</button>}
        <button className="btn btn-sm combat-end-btn" onClick={endCombat}>End combat</button>
      </div>
      {addForm}
      {prepPicks}
      <ul className="combat-list">
        {sorted.map((c) => {
          const isActive = c.id === combat.activeCombatantId;
          const hasHp = c.maxHp != null;
          const isEditing = editingHpId === c.id;
          return (
            <li key={c.id} className={`combat-row${isActive ? " active" : ""}${c.defeated ? " defeated" : ""}`}>
              <span className="combat-init-badge">{c.initiative}</span>
              <span className="combat-name">
                {isActive && <window.Icon name="chevron" size={13} className="combat-turn-marker" />}
                {c.name}
              </span>
              <span className="combat-hp-group">
                {hasHp ? (
                  <>
                    <button className="combat-btn" onClick={() => adjustHp(c.id, -1)}>−</button>
                    {isEditing ? (
                      <input className="combat-hp-edit" type="number" value={editingHpVal}
                        onChange={(e) => setEditingHpVal(e.target.value)}
                        onBlur={() => { setHp(c.id, parseInt(editingHpVal) || 0); setEditingHpId(null); }}
                        onKeyDown={(e) => { if (e.key === "Enter") { setHp(c.id, parseInt(editingHpVal) || 0); setEditingHpId(null); } if (e.key === "Escape") setEditingHpId(null); }}
                        autoFocus />
                    ) : (
                      <span className="combat-hp" title="Click to edit" onClick={() => { setEditingHpId(c.id); setEditingHpVal(String(c.currentHp ?? 0)); }}>
                        {c.currentHp ?? "?"}<span className="combat-hp-max">/{c.maxHp}</span>
                      </span>
                    )}
                    <button className="combat-btn" onClick={() => adjustHp(c.id, 1)}>+</button>
                  </>
                ) : (
                  <span className="combat-hp-none">—</span>
                )}
              </span>
              <button className={`combat-btn combat-skull${c.defeated ? " is-defeated" : ""}`}
                onClick={() => toggleDefeated(c.id)} title={c.defeated ? "Restore" : "Defeat"}>☠</button>
            </li>
          );
        })}
      </ul>
    </div>
  );
}

// ── shared body renderers (used by both layouts) ─────────────────────────────
function secretsBody(d, openDetail, q, onCardLinkClick) {
  const secrets = d.secrets || [];
  if (!secrets.length) return <p className="run-muted">No secrets prepped.</p>;
  const sorted = [...secrets].sort((a, b) => (a.resolved ? 1 : 0) - (b.resolved ? 1 : 0));
  return (
    <ul className="run-secrets">
      {sorted.map((s) => {
        const cp = cardOpenProps(openDetail, "secrets", s);
        return (
          <li key={s.id} id={`card-${s.id}`} {...cp} className={`${cp.className}${s.resolved ? " resolved" : ""}`}>
            <MarkdownText text={s.body} className="run-secret-body" highlight={q} onCardLinkClick={onCardLinkClick} />
            {s.image && <window.Icon name="image" size={12} className="run-img-hint" />}
          </li>
        );
      })}
    </ul>
  );
}

function scenesBody(d, openDetail, q, onCardLinkClick) {
  const scenes = d.scenes || [];
  if (!scenes.length) return <p className="run-muted">No scenes outlined.</p>;
  const sorted = [...scenes].sort((a, b) => (a.resolved ? 1 : 0) - (b.resolved ? 1 : 0));
  return (
    <ul className="run-scenes">
      {sorted.map((s) => {
        const cp = cardOpenProps(openDetail, "scenes", s);
        return (
          <li key={s.id} id={`card-${s.id}`} {...cp} className={`${cp.className}${s.resolved ? " resolved" : ""}`}>
            <div>
              <div className="run-item-header">
                <strong><Hl text={s.title || "Untitled scene"} q={q} /></strong>
                {s.image && <window.Icon name="image" size={12} className="run-img-hint" />}
              </div>
              {s.body && <MarkdownText text={s.body} className="run-scene-desc" highlight={q} onCardLinkClick={onCardLinkClick} />}
            </div>
          </li>
        );
      })}
    </ul>
  );
}

function refList(items, render) {
  if (!items || !items.length) return <p className="run-muted">Nothing here yet.</p>;
  return <ul className="run-ref">{items.map(render)}</ul>;
}

function bodyFor(id, d, toggle, session, setSession, ctx, openDetail, q, onCardLinkClick) {
  switch (id) {
    case "combat": return <CombatSection session={session} setSession={setSession} d={d} />;
    case "start": {
      const st = (d.start && d.start[0]) || {};
      return st.body
        ? <div className="run-start-text"><MarkdownText text={st.body} highlight={q} onCardLinkClick={onCardLinkClick} /></div>
        : <p className="run-muted">No strong start written — jump back to prep to set the opening scene.</p>;
    }
    case "secrets": return secretsBody(d, openDetail, q, onCardLinkClick);
    case "scenes": return scenesBody(d, openDetail, q, onCardLinkClick);
    case "npcs": return refList(d.npcs, (n) =>
      <li key={n.id} id={`card-${n.id}`} {...cardOpenProps(openDetail, "npcs", n)}>
        <div className="run-item-body">
          <div className="run-item-header">
            <strong><Hl text={n.title} q={q} /></strong>
            {n.sub && <span className="run-tag"><Hl text={n.sub} q={q} /></span>}
            {n.image && <window.Icon name="image" size={12} className="run-img-hint" />}
          </div>
          {(n.image || n.body) && (
            <div className="run-item-details">
              {n.image && <RunItemImage src={n.image} size={n.imageSize} />}
              {n.body && <MarkdownText text={n.body} className="run-item-desc" highlight={q} onCardLinkClick={onCardLinkClick} />}
            </div>
          )}
        </div>
      </li>);
    case "locations": return refList(d.locations, (l) =>
      <li key={l.id} id={`card-${l.id}`} {...cardOpenProps(openDetail, "locations", l)}>
        <div className="run-item-body">
          <div className="run-item-header">
            <strong><Hl text={l.title} q={q} /></strong>
            {l.image && <window.Icon name="image" size={12} className="run-img-hint" />}
          </div>
          {Array.isArray(l.aspects) && l.aspects.length > 0 &&
            <span className="run-aspects">{l.aspects.map((a, i) => <i key={i}><Hl text={a} q={q} /></i>)}</span>}
          {(l.image || l.body) && (
            <div className="run-item-details">
              {l.image && <RunItemImage src={l.image} size={l.imageSize} />}
              {l.body && <MarkdownText text={l.body} className="run-item-desc" highlight={q} onCardLinkClick={onCardLinkClick} />}
            </div>
          )}
        </div>
      </li>);
    case "monsters": return refList(d.monsters, (m) =>
      <li key={m.id} id={`card-${m.id}`} {...cardOpenProps(openDetail, "monsters", m)}>
        <div className="run-item-body">
          <div className="run-item-header">
            <strong><Hl text={m.title} q={q} /></strong>
            {m.image && <window.Icon name="image" size={12} className="run-img-hint" />}
          </div>
          {m.sub && <span className="run-tag"><Hl text={m.sub} q={q} /></span>}
          {m.body && (
            <div className="run-item-details">
              <MarkdownText text={m.body} className="run-item-desc" highlight={q} onCardLinkClick={onCardLinkClick} />
            </div>
          )}
        </div>
      </li>);
    case "items": return refList(d.items, (it) =>
      <li key={it.id} id={`card-${it.id}`} {...cardOpenProps(openDetail, "items", it)}>
        <div className="run-item-body">
          <div className="run-item-header">
            <strong><Hl text={it.title} q={q} /></strong>
            {it.image && <window.Icon name="image" size={12} className="run-img-hint" />}
          </div>
          {(it.image || it.body) && (
            <div className="run-item-details">
              {it.image && <RunItemImage src={it.image} size={it.imageSize} />}
              {it.body && <MarkdownText text={it.body} className="run-item-desc" highlight={q} onCardLinkClick={onCardLinkClick} />}
            </div>
          )}
        </div>
      </li>);
    case "characters": return refList(d.characters, (c) =>
      <li key={c.id} id={`card-${c.id}`} {...cardOpenProps(openDetail, "characters", c)}>
        <div className="run-item-body">
          <div className="run-item-header">
            <strong><Hl text={c.title} q={q} /></strong>
            {c.sub && <span className="run-tag"><Hl text={c.sub} q={q} /></span>}
            {c.image && <window.Icon name="image" size={12} className="run-img-hint" />}
          </div>
          {(c.image || c.body) && (
            <div className="run-item-details">
              {c.image && <RunItemImage src={c.image} size={c.imageSize} />}
              {c.body && <MarkdownText text={c.body} className="run-item-desc" highlight={q} onCardLinkClick={onCardLinkClick} />}
            </div>
          )}
        </div>
      </li>);
    case "prepNotes": return (
      <window.NotesSections
        sections={session.prepNotes}
        onChange={(v) => setSession((sess) => ({ ...sess, prepNotes: typeof v === "function" ? v(sess.prepNotes) : v }))}
        ctx={ctx}
        highlight={q}
        bodyPlaceholder="Ideas, research threads, and anything that didn't fit the eight steps…" />);
    case "notes": return (
      <window.NotesSections
        sections={session.notes}
        onChange={(v) => setSession((sess) => ({ ...sess, notes: typeof v === "function" ? v(sess.notes) : v }))}
        ctx={ctx}
        highlight={q}
        bodyPlaceholder="Jot what happened, dropped clues, dangling threads for next time…" />);
    default: return null;
  }
}

function countFor(id, d, session) {
  if (id === "combat") {
    const combatants = session && session.runtime && session.runtime.combat && session.runtime.combat.combatants;
    return combatants && combatants.length ? combatants.length : null;
  }
  if (id === "start" || id === "notes" || id === "prepNotes") return null;
  if (id === "secrets") { const s = d.secrets || []; return s.length || null; }
  if (id === "scenes") { const s = d.scenes || []; return s.length || null; }
  return (d[id] || []).length;
}

// normalises all text fields of a card into one lowercase string for matching
function textOf(item) {
  return [item.title, item.sub, item.body, ...(Array.isArray(item.aspects) ? item.aspects : [])]
    .filter(Boolean).join(" ").toLowerCase();
}

// ── collapsible section (focus layout) ───────────────────────────────────────
function FocusSection({ id, meta, count, open, onToggle, innerRef, children, active }) {
  return (
    <section ref={innerRef} className={`focus-sec${open ? " open" : ""}${active ? " run-accent" : ""}`}>
      <button type="button" className="focus-sec-hd" onClick={() => onToggle(id)} aria-expanded={open}>
        <window.Icon name={meta.icon} size={18} className="focus-sec-icon" />
        <h3>{meta.title}</h3>
        {count != null && <span className="run-count">{count}</span>}
        <window.Icon name="chevron" size={16} className="focus-chev" />
      </button>
      {open && <div className="focus-sec-body">{children}</div>}
    </section>
  );
}

// ── board panel (board layout) ───────────────────────────────────────────────
function RunPanel({ id, meta, count, wide, innerRef, children, active }) {
  return (
    <section ref={innerRef} className={`run-panel${active ? " run-accent" : ""}${wide ? " run-wide" : ""}`}>
      <header className="run-panel-hd">
        <window.Icon name={meta.icon} size={17} />
        <h3>{meta.title}</h3>
        {count != null && <span className="run-count">{count}</span>}
      </header>
      <div>{children}</div>
    </section>
  );
}

function RunMode({ session, setSession, ctx, layout = "focus" }) {
  const d = session.data;
  const setStep = (stepId, items) => setSession({ ...session, data: { ...d, [stepId]: items } });
  const toggle = (stepId, id, key) =>
    setStep(stepId, (d[stepId] || []).map((it) => it.id === id ? { ...it, [key]: !it[key] } : it));

  const start = d.start && d.start[0] || {};
  const refs = React.useRef({});
  const [open, setOpen] = React.useState({ start: true, secrets: true, scenes: true });
  const [pendingJump, setPendingJump] = React.useState(null);
  const [pendingCardJump, setPendingCardJump] = React.useState(null);
  const [detail, setDetail] = React.useState(null);
  const [activeSection, setActiveSection] = React.useState(null);
  const openDetail = React.useCallback((stepId, item) => setDetail({ stepId, item }), []);
  const onToggle = (id) => setOpen((o) => ({ ...o, [id]: !o[id] }));

  // Card index for [[link]] navigation
  const runCardIndex = React.useMemo(() => window.RPG.buildCardIndex(session.data || {}), [session]);

  // ── search / filter ──────────────────────────────────────────────────────────
  const [query, setQuery] = React.useState("");
  const savedOpenRef = React.useRef(null);

  const q = query.trim().toLowerCase();

  const filteredD = q ? {
    ...d,
    secrets:    (d.secrets    || []).filter(item => textOf(item).includes(q)),
    scenes:     (d.scenes     || []).filter(item => textOf(item).includes(q)),
    npcs:       (d.npcs       || []).filter(item => textOf(item).includes(q)),
    locations:  (d.locations  || []).filter(item => textOf(item).includes(q)),
    monsters:   (d.monsters   || []).filter(item => textOf(item).includes(q)),
    items:      (d.items      || []).filter(item => textOf(item).includes(q)),
    characters: (d.characters || []).filter(item => textOf(item).includes(q)),
  } : d;

  const notesMatch = (arr) =>
    (arr || []).some(s => [s.title, s.body].filter(Boolean).join(" ").toLowerCase().includes(q));

  const hasMatch = (id) => {
    if (!q) return true;
    if (id === "combat") return false;
    if (id === "start") { const st = (d.start && d.start[0]) || {}; return !!(st.body && st.body.toLowerCase().includes(q)); }
    if (id === "prepNotes") return notesMatch(session.prepNotes);
    if (id === "notes") return notesMatch(session.notes);
    return (filteredD[id] || []).length > 0;
  };

  const allIds = ["start", ...RUN_ORDER];
  const visibleIds = q ? allIds.filter(hasMatch) : allIds;

  React.useEffect(() => {
    if (!q) {
      if (savedOpenRef.current !== null) { setOpen(savedOpenRef.current); savedOpenRef.current = null; }
      return;
    }
    setOpen(prev => {
      if (savedOpenRef.current === null) savedOpenRef.current = prev;
      const next = {};
      allIds.forEach(id => { next[id] = hasMatch(id); });
      return next;
    });
  }, [q]);

  React.useEffect(() => { setQuery(""); }, [session.id]);
  // ─────────────────────────────────────────────────────────────────────────────

  const scrollToSection = React.useCallback((id) => {
    const el = refs.current[id];
    if (!el) return;
    let sc = el.parentElement;
    while (sc) {
      const oy = getComputedStyle(sc).overflowY;
      if ((oy === "auto" || oy === "scroll") && sc.scrollHeight > sc.clientHeight + 1) break;
      sc = sc.parentElement;
    }
    const wrap = el.closest(".run-wrap");
    const topbar = wrap && wrap.parentElement ? wrap.parentElement.querySelector(".topbar") : null;
    const nav = wrap ? wrap.querySelector(".run-jump") : null;
    const offset = (topbar ? topbar.offsetHeight : 0) + (nav ? nav.offsetHeight : 0) + 12;
    const reduce = window.matchMedia("(prefers-reduced-motion: reduce)").matches;
    const behavior = reduce ? "auto" : "smooth";
    if (sc) {
      const top = el.getBoundingClientRect().top - sc.getBoundingClientRect().top + sc.scrollTop - offset;
      sc.scrollTo({ top: Math.max(0, top), behavior });
    } else {
      window.scrollTo({ top: Math.max(0, el.getBoundingClientRect().top + window.scrollY - offset), behavior });
    }
  }, []);

  React.useEffect(() => {
    if (!pendingJump) return;
    const tm = setTimeout(() => { scrollToSection(pendingJump); setPendingJump(null); }, 60);
    return () => clearTimeout(tm);
  }, [pendingJump, open, scrollToSection]);

  const jump = (id) => {
    if (layout === "focus") setOpen((o) => ({ ...o, [id]: true }));
    setPendingJump(id);
    setActiveSection(id);
  };

  React.useEffect(() => {
    if (!pendingCardJump) return;
    const tm = setTimeout(() => {
      const el = document.getElementById(`card-${pendingCardJump.cardId}`);
      if (el) {
        el.scrollIntoView({ behavior: 'smooth', block: 'center' });
        el.classList.add('card-highlight');
        setTimeout(() => el.classList.remove('card-highlight'), 1500);
      }
      setPendingCardJump(null);
    }, 200);
    return () => clearTimeout(tm);
  }, [pendingCardJump, open]);

  const onCardLinkClick = (title) => {
    const entry = runCardIndex.get(title.toLowerCase().trim());
    if (!entry) return;
    jump(entry.stepId);
    setPendingCardJump({ stepId: entry.stepId, cardId: entry.cardId });
  };

  const startBlock =
    <section className="run-start" ref={(el) => refs.current.start = el} style={{ padding: "20px" }}>
      <span className="run-start-eyebrow"><window.Icon name="flag" size={14} /> Strong start</span>
      <div className="run-start-text" style={{ fontSize: "16px" }}>
        {start.body ? <MarkdownText text={start.body} onCardLinkClick={onCardLinkClick} /> : <span className="run-muted">No strong start written — jump back to prep to set the opening scene.</span>}
      </div>
    </section>;

  return (
    <div className="run-wrap">
      <nav className="run-jump">
        <div className="run-search">
          <window.Icon name="search" size={14} className="run-search-icon" />
          <input
            type="search"
            className="run-search-input"
            placeholder="Filter sections…"
            value={query}
            onChange={e => setQuery(e.target.value)}
            aria-label="Search run mode"
          />
          {query && (
            <button type="button" className="run-search-clear" onClick={() => setQuery("")} aria-label="Clear search">
              <window.Icon name="x" size={14} />
            </button>
          )}
        </div>
        <div className="run-jump-inner">
          {["start", ...RUN_ORDER].map((id) =>
            <button key={id} type="button" onClick={() => jump(id)}>
              <window.Icon name={RUN_META[id].icon} size={14} /> {RUN_META[id].label}
            </button>
          )}
        </div>
      </nav>

      {layout === "board" ?
        <div className="run">
          {startBlock}
          <div className="run-grid">
            {RUN_ORDER.map((id) =>
              <RunPanel key={id} id={id} meta={RUN_META[id]} count={countFor(id, filteredD, session)}
                wide={id === "secrets" || id === "notes"} innerRef={(el) => refs.current[id] = el}
                active={id === activeSection}>
                {bodyFor(id, filteredD, toggle, session, setSession, ctx, openDetail, q, onCardLinkClick)}
              </RunPanel>
            )}
          </div>
        </div> :

        <div className="run run-focus">
          {visibleIds.map((id) =>
            <FocusSection key={id} id={id} meta={RUN_META[id]} count={countFor(id, filteredD, session)}
              open={!!open[id]} onToggle={onToggle} innerRef={(el) => refs.current[id] = el}
              active={id === activeSection}>
              {bodyFor(id, filteredD, toggle, session, setSession, ctx, openDetail, q, onCardLinkClick)}
            </FocusSection>
          )}
        </div>
      }

      {detail && (() => {
        const liveItem = (d[detail.stepId] || []).find(it => it.id === detail.item.id) || detail.item;
        const resolvable = detail.stepId === "secrets" || detail.stepId === "scenes";
        return (
          <RunCardModal
            stepId={detail.stepId}
            item={liveItem}
            resolved={!!liveItem.resolved}
            onResolve={resolvable ? () => toggle(detail.stepId, liveItem.id, "resolved") : undefined}
            onClose={() => setDetail(null)}
            onCardLinkClick={onCardLinkClick}
          />
        );
      })()}
    </div>
  );
}

Object.assign(window, { RunMode, RUN_ORDER, RUN_META });
