// THE_YARD — UI components (HUD, terminal, container panel, debug, contact)

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

// ─────────── BOOT SEQUENCE ───────────
function BootSequence({ onDone }) {
  const lines = [
    "PODS-OS v4.2.1 (yard-edition) · build 26.04",
    "[ OK ] mounting /dev/yard",
    "[ OK ] loading 12 containers · 11 bays · 1 hidden",
    "[ OK ] booting operator terminal",
    "[ OK ] sodium lights online · floodlights @ 92%",
    "[ OK ] zone perimeter scanned",
    "[ ⏵ ] manifest ready · awaiting operator",
  ];
  const [shown, setShown] = useState(0);
  const [showPrompt, setShowPrompt] = useState(false);
  const btnRef = useRef(null);
  useEffect(() => {
    if (shown < lines.length) {
      const t = setTimeout(() => setShown(shown + 1), 140 + Math.random() * 110);
      return () => clearTimeout(t);
    } else {
      const t = setTimeout(() => setShowPrompt(true), 260);
      return () => clearTimeout(t);
    }
  }, [shown]);
  useEffect(() => {
    if (showPrompt && btnRef.current) btnRef.current.focus();
  }, [showPrompt]);
  useEffect(() => {
    const onKey = (e) => {
      if (e.key === "Enter" || e.key === " " || e.key === "Escape") {
        e.preventDefault();
        if (shown < lines.length) {
          // skip remaining boot lines
          setShown(lines.length);
          setShowPrompt(true);
        } else {
          onDone();
        }
      }
    };
    window.addEventListener("keydown", onKey);
    return () => window.removeEventListener("keydown", onKey);
  }, [shown, onDone]);
  return (
    <div className="boot" role="dialog" aria-label="Boot sequence">
      <div className="boot-brand">THE_YARD<span>// operator portfolio</span></div>
      <div className="boot-tagline">Interactive portfolio. Built through real projects, not just code.</div>
      <div className="boot-lines">
        {lines.slice(0, shown).map((l, i) => (
          <div key={i} className="boot-line" style={{ opacity: 1 }}>{l}</div>
        ))}
        {shown < lines.length && <span className="boot-cursor" />}
      </div>
      <div className="boot-prompt" style={{ opacity: showPrompt ? 1 : 0 }}>
        <span className="boot-prompt-text">operator · press to enter the yard</span>
        <button ref={btnRef} onClick={onDone} aria-label="Enter the yard">ENTER →</button>
        <div className="boot-skip">enter · space · esc to skip</div>
      </div>
    </div>
  );
}

function WebGLFallback() {
  const identity = window.YARD_DATA && window.YARD_DATA.identity;
  return (
    <div className="fallback-screen webgl-fallback" role="alert">
      <div className="fallback-card">
        <div className="fallback-kicker">THE_YARD · RENDER CHECK</div>
        <h1>WebGL is not available</h1>
        <p>
          This portfolio needs WebGL to render the 3D container yard. Try a current browser with hardware
          acceleration enabled, then reload the page.
        </p>
        {identity && (
          <p className="fallback-id">
            {identity.name} · {identity.role} · {identity.sub}
          </p>
        )}
        <button type="button" onClick={() => window.location.reload()}>Retry</button>
      </div>
    </div>
  );
}

// ─────────── HUD ───────────
// ─────────── AMBIENCE TOGGLE ───────────
// Tiny speaker icon in the HUD meta strip. Off by default — clicking it
// boots window.YARD_AUDIO which synthesizes the bed (hum + wind + buzz)
// plus randomized industrial events (horns, clinks, hiss, radio blips).
// The first click doubles as the user-gesture browsers require for audio,
// so the bed fades in immediately without a noisy attack.
function AmbienceToggle() {
  const [on, setOn] = useState(false);
  const toggle = useCallback(() => {
    const audio = window.YARD_AUDIO;
    if (!audio) return;
    if (audio.isPlaying()) { audio.stop(); setOn(false); }
    else { audio.start(); setOn(true); }
  }, []);
  return (
    <button
      type="button"
      className={"hud-audio" + (on ? " is-on" : "")}
      onClick={toggle}
      aria-label={on ? "Mute yard ambience" : "Enable yard ambience"}
      aria-pressed={on ? "true" : "false"}
      title={on ? "Ambience on. Click to mute." : "Yard is silent. Click for ambience."}
    >
      <svg className="ha-icon" viewBox="0 0 24 24" width="13" height="13" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true">
        <path d="M11 5L6 9H2v6h4l5 4V5z" />
        {on && <path d="M15.5 8.5a5 5 0 0 1 0 7M19 5a9 9 0 0 1 0 14" />}
        {!on && <path d="M22 9l-6 6M16 9l6 6" />}
      </svg>
      <span className="ha-k">{on ? "AMBIENCE ON" : "AMBIENCE"}</span>
    </button>
  );
}

function HUD({ nearest, openIndex, mode, walkTo, onCloseFocus, viewedIds }) {
  const data = window.YARD_DATA;
  const c = data.containers[nearest] || data.containers[0];
  const isFocus = mode === "focus";
  return (
    <div className="hud">
      <div className="hud-corner tl">
        <div className="operator-card">
          {data.identity.headshot && (
            <span className="op-portrait" aria-hidden="true">
              <SafeImage
                className="op-portrait-img"
                src={data.identity.headshot}
                alt=""
              />
              <span className="op-portrait-frame" />
            </span>
          )}
          <div className="op-card-body">
            <div className="op-id">
              <span className="op-id-tag">OPERATOR</span>
              <span className="op-id-num">001</span>
            </div>
            <div className="op-name">{data.identity.name}</div>
            <div className="op-role">{data.identity.role}</div>
            <div className="op-sub">
              <span className="op-dot" />
              {data.identity.sub || data.identity.location}
            </div>
          </div>
        </div>
      </div>
      <div className="hud-corner bl">
        <div className="hud-bl-stack">
          <div className="hud-bl-tag">{isFocus ? "FOCUSED ON" : "HOVERING"}</div>
          <div className="hud-bl-line">
            <span className="hud-bl-title">{c.title}</span>
            <span className="hud-bl-sep">·</span>
            <span className="hud-bl-summary">{c.summary}</span>
          </div>
        </div>
      </div>
      <div className="hud-corner br">
        <div className="hud-meta">
          <span className="hud-year-k">YEAR</span>
          <span className="hud-year-v">{c.year}</span>
        </div>
      </div>

      <BayGrid nearest={nearest} openIndex={openIndex} walkTo={walkTo} viewedIds={viewedIds} />

      <ControlsDock />
    </div>
  );
}

// ─────────── CONTROLS DOCK ───────────
// Two grouped panels — Navigation + Actions — replaces the long hint strip.
// On narrower screens it collapses to a short "Controls" toggle that opens
// a sheet with the same content.
function ControlsDock() {
  const [open, setOpen] = useState(false);
  return (
    <>
      <div className={"controls-hint" + (open ? " is-open" : "")} role="region" aria-label="Controls hint">
        <span className="ch-primary">Click a container to explore</span>
        <span className="ch-sep">·</span>
        <span className="ch-secondary">Drag to move &middot; Scroll to zoom</span>
        <button
          type="button"
          className="ch-toggle"
          onClick={() => setOpen(o => !o)}
          aria-label={open ? "Hide advanced controls" : "Show advanced controls"}
          aria-expanded={open ? "true" : "false"}
        >{open ? "Hide controls ▾" : "More controls ▴"}</button>
      </div>
      {open && (
        <div className="controls-dock open" role="region" aria-label="Advanced controls">
          <div className="cd-group">
            <span className="cd-label">Navigation</span>
            <span className="cd-key"><kbd>DRAG</kbd> pan</span>
            <span className="cd-key"><kbd>⇧</kbd>+<kbd>DRAG</kbd> orbit</span>
            <span className="cd-key"><kbd>SCROLL</kbd> zoom</span>
          </div>
          <div className="cd-divider" />
          <div className="cd-group">
            <span className="cd-label">Actions</span>
            <span className="cd-key"><kbd>CLICK</kbd> open</span>
            <span className="cd-key"><kbd>~</kbd> terminal</span>
            <span className="cd-key"><kbd>R</kbd> reset</span>
            <span className="cd-key"><kbd>ESC</kbd> close</span>
          </div>
          <div className="cd-divider" />
          <div className="cd-group cd-group-audio">
            <span className="cd-label">Ambience</span>
            <AmbienceToggle />
          </div>
        </div>
      )}
    </>
  );
}

function BayGrid({ nearest, openIndex, walkTo, viewedIds }) {
  const data = window.YARD_DATA;
  const viewed = viewedIds instanceof Set ? viewedIds : new Set(viewedIds || []);
  return (
    <div className="yard-map">
      <span className="map-label">YARD INDEX</span>
      <div className="bays">
        {data.containers.map((c, i) => {
          const cls = ["bay"];
          if (i === openIndex) cls.push("active");
          else if (i === nearest) cls.push("hovered");
          if (viewed.has(c.id)) cls.push("visited");
          return (
            <div
              key={c.id}
              className={cls.join(" ")}
              onClick={() => walkTo(i)}
              title={c.title}
            >
              <span className="bay-tip">{c.label} · {c.title}</span>
            </div>
          );
        })}
      </div>
    </div>
  );
}

// ─────────── CUSTOM CURSOR ───────────
function Cursor() {
  const ref = useRef(null);
  const [hover, setHover] = useState(false);
  useEffect(() => {
    const move = (e) => {
      if (ref.current) {
        ref.current.style.left = e.clientX + "px";
        ref.current.style.top = e.clientY + "px";
      }
      const t = e.target;
      const isInteractive = t && (
        t.closest("button, a, input, textarea, select, .yard-map .bay, .terminal-header, .debug-code .ln, .panel-close")
      );
      setHover(!!isInteractive);
    };
    window.addEventListener("mousemove", move);
    return () => window.removeEventListener("mousemove", move);
  }, []);
  return (
    <div className={"cursor" + (hover ? " hover" : "")} ref={ref}>
      <svg viewBox="0 0 24 24" fill="none" stroke="white" strokeWidth="1.5">
        <rect x="2" y="2" width="20" height="20" />
        <circle cx="12" cy="12" r="2" fill="white" />
      </svg>
    </div>
  );
}

// ─────────── TERMINAL ───────────
function Terminal({ expanded, setExpanded, onCommand, lines, inputRef }) {
  const [val, setVal] = useState("");
  const [history, setHistory] = useState([]);
  const [hIndex, setHIndex] = useState(-1);
  const outputRef = useRef(null);

  useEffect(() => {
    if (outputRef.current) outputRef.current.scrollTop = outputRef.current.scrollHeight;
  }, [lines]);

  const submit = (e) => {
    e.preventDefault();
    if (!val.trim()) return;
    onCommand(val);
    setHistory(h => [val, ...h]);
    setHIndex(-1);
    setVal("");
  };

  const onKey = (e) => {
    if (e.key === "ArrowUp") {
      e.preventDefault();
      if (history[hIndex + 1] !== undefined) {
        setHIndex(hIndex + 1);
        setVal(history[hIndex + 1]);
      }
    } else if (e.key === "ArrowDown") {
      e.preventDefault();
      if (hIndex > 0) {
        setHIndex(hIndex - 1);
        setVal(history[hIndex - 1]);
      } else {
        setHIndex(-1);
        setVal("");
      }
    }
  };

  return (
    <div className={"terminal-bar " + (expanded ? "expanded" : "collapsed")}>
      <div className="terminal-header" onClick={() => setExpanded(!expanded)}>
        <div className="lights"><span /><span /><span /></div>
        <div className="title">operator@yard:~ · type `help`</div>
        <div className="toggle">{expanded ? "▾ minimize" : "▴ expand"}</div>
      </div>
      {expanded && (
        <div className="terminal-body">
          <div className="terminal-output" ref={outputRef}>
            {lines.map((l, i) => (
              <div key={i} className={"term-line " + (l.kind || "out")}>{l.text}</div>
            ))}
          </div>
          <form className="terminal-input-row" onSubmit={submit}>
            <span className="prompt">$</span>
            <input
              ref={inputRef}
              autoFocus
              value={val}
              onChange={(e) => setVal(e.target.value)}
              onKeyDown={onKey}
              placeholder="ls / open mygui / whoami / help"
              spellCheck={false}
            />
          </form>
        </div>
      )}
    </div>
  );
}

// ─────────── DIVE FLASH OVERLAY ───────────
// Plays once when a container is clicked: doors open, scanlines, hololight,
// then the panel dock-slides into place from below.
function DiveFlash({ keyTick }) {
  if (!keyTick) return null;
  return (
    <div key={keyTick} className="dive-flash">
      <div className="dive-door dive-door-l" />
      <div className="dive-door dive-door-r" />
      <div className="dive-scan" />
      <div className="dive-vignette" />
      <div className="dive-corners">
        <span /><span /><span /><span />
      </div>
      <div className="dive-stamp">// ENTERING BAY</div>
    </div>
  );
}

// ─────────── CONTAINER PANEL ───────────
// Single scrollable case-study layout. Each container reads like a real
// product dashboard: HERO → METRICS → OVERVIEW → BUILT → SYSTEM → STACK →
// LINKS. Hero gets a per-project visual signature via data-theme.

// Per-bay hero art is pure CSS (themes in styles.css). Component just
// composes the layered overlay + content positioning.
// SafeImage — hides itself if the asset 404s, so missing files never render
// as broken-image glyphs. Loading is lazy and decoded async to keep the panel
// snappy when several visuals stack in the dossier.
function SafeImage({ src, alt, className, onMissing }) {
  const [ok, setOk] = useState(!!src);
  const [loaded, setLoaded] = useState(false);
  useEffect(() => {
    setOk(!!src);
    setLoaded(false);
  }, [src]);
  if (!src || !ok) return null;
  const cls = (className || "") + " safe-img" + (loaded ? " is-loaded" : "");
  return (
    <img
      className={cls}
      src={src}
      alt={alt}
      loading="lazy"
      decoding="async"
      onLoad={() => setLoaded(true)}
      onError={() => { setOk(false); if (onMissing) onMissing(); }}
    />
  );
}

function bayBanner(data) {
  if (data.image) return data.image;
  if (data.images && data.images[0]) return data.images[0];
  return null;
}

function CpHero({ data }) {
  const banner = bayBanner(data);
  const status = data.status;
  const statusClass = status ? "cp-status status-" + status.toLowerCase().replace(/\s+/g, "-") : "";
  return (
    <div
      className={"cp-hero" + (banner ? " has-image" : "")}
      data-theme={data.id}
      style={{ "--cp-accent": data.color }}
    >
      <SafeImage className="cp-hero-img" src={banner} alt={data.title + " · banner"} />
      {!banner && <div className="cp-hero-art" aria-hidden="true" />}
      <div className="cp-stencil-hero" aria-hidden="true">{data.stencil}</div>
      <div className="cp-hero-overlay" />
      <div className="cp-hero-content">
        <div className="cp-bay-line">
          <span className="cp-bay">{data.label}</span>
          <span className="cp-dot">·</span>
          <span className="cp-year">{data.year}</span>
          {status && <span className={"cp-status " + statusClass}>{status}</span>}
        </div>
        <h1 className="cp-title">{data.title}</h1>
        {data.summary && <p className="cp-tagline">{data.summary}</p>}
      </div>
    </div>
  );
}

// Highlight strip — auto-derives from data.stats; first card is always status
// when present. Empty when nothing useful.
function CpMetrics({ data }) {
  const items = [];
  if (data.status) items.push({ k: "STATUS", v: data.status, accent: "status" });
  if (data.stats) {
    for (const [k, v] of data.stats) {
      if (k === "STATUS") continue;
      items.push({ k, v });
    }
  }
  if (items.length === 0) return null;
  return (
    <div className="cp-metrics" role="list">
      {items.map((m, i) => (
        <div key={i} className={"cp-metric" + (m.accent ? " is-" + m.accent : "")} role="listitem">
          <span className="cp-metric-v">{m.v}</span>
          <span className="cp-metric-k">{m.k}</span>
        </div>
      ))}
    </div>
  );
}

// Link icons — keyword-detected from label/href. All inline SVGs, ~200B each.
function CpLinkIcon({ label, href }) {
  const t = ((label || "") + " " + (href || "")).toLowerCase();
  const ix = { width: 14, height: 14, "aria-hidden": true };
  if (t.includes("github")) return (
    <svg {...ix} viewBox="0 0 24 24" fill="currentColor"><path d="M12 .5C5.65.5.5 5.65.5 12c0 5.08 3.29 9.39 7.86 10.92.57.1.78-.25.78-.55 0-.27-.01-1-.02-1.95-3.2.7-3.87-1.54-3.87-1.54-.52-1.33-1.27-1.69-1.27-1.69-1.04-.71.08-.7.08-.7 1.15.08 1.76 1.18 1.76 1.18 1.02 1.76 2.69 1.25 3.34.96.1-.74.4-1.25.72-1.54-2.55-.29-5.23-1.27-5.23-5.66 0-1.25.45-2.27 1.18-3.07-.12-.29-.51-1.46.11-3.05 0 0 .96-.31 3.15 1.18.91-.25 1.89-.38 2.86-.38.97 0 1.95.13 2.86.38 2.18-1.49 3.14-1.18 3.14-1.18.62 1.59.23 2.76.11 3.05.73.8 1.18 1.82 1.18 3.07 0 4.4-2.68 5.36-5.24 5.65.41.36.78 1.06.78 2.13 0 1.54-.01 2.78-.01 3.16 0 .31.21.66.79.55C20.21 21.39 23.5 17.08 23.5 12c0-6.35-5.15-11.5-11.5-11.5z"/></svg>
  );
  if (t.includes("app store") || t.includes("ios")) return (
    <svg {...ix} viewBox="0 0 24 24" fill="currentColor"><path d="M19.06 17.14c-.32.74-.7 1.42-1.15 2.05-.6.85-1.1 1.43-1.48 1.76-.6.55-1.24.83-1.93.85-.49 0-1.09-.14-1.78-.43-.69-.28-1.32-.42-1.9-.42-.6 0-1.25.14-1.94.42-.7.29-1.26.44-1.69.45-.66.03-1.31-.26-1.96-.86-.4-.36-.93-.96-1.56-1.81-.68-.91-1.24-1.97-1.68-3.18-.49-1.31-.74-2.57-.74-3.78 0-1.36.29-2.54.87-3.52.46-.79 1.07-1.41 1.83-1.87.76-.46 1.59-.7 2.48-.71.52 0 1.21.16 2.07.48.85.32 1.4.49 1.64.49.18 0 .79-.19 1.83-.57.98-.35 1.81-.5 2.49-.45 1.85.15 3.24.88 4.16 2.19-1.65 1-2.47 2.4-2.45 4.2.02 1.4.53 2.57 1.51 3.5.45.43.95.76 1.5.99-.12.34-.25.66-.39.97zM15.74.79c0 1.02-.37 1.97-1.12 2.84-.9 1.04-1.99 1.65-3.17 1.55-.02-.13-.02-.25-.02-.39 0-.97.43-2.02 1.18-2.86.38-.42.86-.77 1.45-1.05.59-.27 1.14-.43 1.66-.45.02.12.02.25.02.36z"/></svg>
  );
  if (t.includes("demo")) return (
    <svg {...ix} viewBox="0 0 24 24" fill="currentColor"><path d="M8 5v14l11-7z"/></svg>
  );
  if (t.includes("devpost") || t.includes("hackathon")) return (
    <svg {...ix} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2"><path d="M8 8 4 12l4 4M16 8l4 4-4 4M14 4l-4 16"/></svg>
  );
  return (
    <svg {...ix} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2"><path d="M14 3h7v7M21 3l-9 9M21 14v6h-6M3 10V3h7"/></svg>
  );
}
function CpSection({ index, label, accent, children }) {
  return (
    <section className="cp-section">
      <div className="cp-section-head">
        <span className="cp-section-num" style={{ color: accent || "var(--steel-300)" }}>
          {String(index).padStart(2, "0")}
        </span>
        <span className="cp-section-label">{label}</span>
        <span className="cp-section-rule" />
      </div>
      <div className="cp-section-body">{children}</div>
    </section>
  );
}

function cpAsArray(value) {
  if (!value) return [];
  return Array.isArray(value) ? value.filter(Boolean) : [value];
}

function cpCategory(data) {
  if (data.category) return data.category;
  if (data.type === "project") return "Project";
  if (data.type === "chapter") return "Chapter";
  return "Container";
}

// CpVisual — single project visual block under the header. Real image when
// data.images[0] is present, otherwise a themed CSS block so every panel feels
// like a product page (no broken image gaps). The block reuses the per-bay
// `data-theme` id so each project gets its own signature.
//
// When data.image is set but the file fails to load (preview not yet shipped),
// the visual gracefully falls back to the procedural art block instead of an
// empty rectangle — fixes the dead-space bug visible on Cortex / PODS / etc.
function CpVisual({ data }) {
  const banner = bayBanner(data);
  const [imgMissing, setImgMissing] = useState(false);
  const showArt = !banner || imgMissing;
  // Reset missing state when the data changes (panel-to-panel navigation)
  useEffect(() => { setImgMissing(false); }, [data && data.id, banner]);
  const stack = (data.stack || data.skills || []).slice(0, 4);
  const status = data.status || "ACTIVE";
  return (
    <figure
      className={"cp-visual cp-manifest" + (banner && !imgMissing ? " has-image" : "")}
      data-theme={data.id}
    >
      {/* PINNED MANIFEST treatment — corner tape marks + manifest ID strip so
          the visual reads as a tacked-up operational document, not a CSS
          screenshot card. Tape marks are CSS pseudos in styles.css. */}
      <span className="cp-manifest-tape cp-tape-tl" aria-hidden="true" />
      <span className="cp-manifest-tape cp-tape-tr" aria-hidden="true" />
      <div className="cp-manifest-strip" aria-hidden="true">
        <span className="cp-manifest-strip-k">MANIFEST</span>
        <span className="cp-manifest-strip-v">{data.label}</span>
        <span className="cp-manifest-strip-bars">||||||||</span>
        <span className="cp-manifest-strip-id">{(data.id || "").toUpperCase()}-{(data.year || "·")}</span>
      </div>
      <SafeImage
        className="cp-visual-img"
        src={imgMissing ? null : banner}
        alt={data.title + " · visual"}
        onMissing={() => setImgMissing(true)}
      />
      {showArt && (
        <div className="cp-visual-art" aria-hidden="true">
          <div className="cp-visual-grid" />
          <div className="cp-visual-scanlines" />
          <div className="cp-visual-stencil">{data.stencil}</div>
          <div className="cp-visual-content">
            <span className="cp-visual-tag">{data.label} · {status}</span>
            <span className="cp-visual-title">{data.title}</span>
            {stack.length > 0 && (
              <span className="cp-visual-stack">{stack.join(" · ")}</span>
            )}
          </div>
          {/* Operational system feed — embedded into the visual so the block
              reads as a live monitor view rather than a placeholder card.
              Falls back to derived metadata when no `evidence` is supplied. */}
          {(() => {
            const feed = (data.evidence && data.evidence.length)
              ? data.evidence.slice(0, 3)
              : [
                  `${(data.category || data.type || "").toUpperCase()} · ONLINE`,
                  `${(data.year || "·")} · ${(status || "ACTIVE")}`,
                  `OPERATOR · 001`,
                ].filter(Boolean);
            return (
              <div className="cp-visual-feed">
                <span className="cp-feed-pulse" />
                <span className="cp-feed-k">// SYSTEM FEED</span>
                {feed.map((line, i) => (
                  <span key={i} className="cp-feed-line">{line}</span>
                ))}
              </div>
            );
          })()}
          <div className="cp-visual-corners">
            <span /><span /><span /><span />
          </div>
        </div>
      )}
    </figure>
  );
}

function CpDossierHero({ data, category }) {
  const status = data.status || "ACTIVE";
  const statusClass = "cp-status status-" + status.toLowerCase().replace(/\s+/g, "-");
  const logo = data.logo;
  const signature = data.signature || "default";
  return (
    <header className="cp-dossier-hero" data-theme={data.id} data-signature={signature}>
      <div className="cp-dossier-motif" data-motif={data.id} aria-hidden="true" />
      {logo && (
        <div className="cp-dossier-wallmark" aria-hidden="true">
          <SafeImage src={logo} alt="" />
        </div>
      )}
      <div className="cp-signature" aria-hidden="true">
        <span className={"cp-sig cp-sig-" + signature} />
      </div>
      <div className="cp-dossier-copy">
        <div className="cp-bay-line">
          <span className="cp-bay">{data.label}</span>
          <span className="cp-year">{data.year}</span>
          <span className={statusClass}>{status}</span>
        </div>
        <div className="cp-dossier-titleRow">
          {logo && (
            <SafeImage className="cp-dossier-logo" src={logo} alt={data.title + " logo"} />
          )}
          <h1 className="cp-dossier-title">{data.title}</h1>
        </div>
        {data.identityTag && (
          <p className="cp-dossier-identity">{data.identityTag}</p>
        )}
        {data.summary && <p className="cp-dossier-summary">{data.summary}</p>}
        {Array.isArray(data.evidence) && data.evidence.length > 0 && (
          <ul className="cp-evidence cp-evidence-inline" aria-label="Operational artifacts">
            {data.evidence.slice(0, 3).map((entry, i) => (
              <li key={i} className="cp-evidence-row">
                <span className="cp-evidence-dot" aria-hidden="true" />
                <span className="cp-evidence-text">{entry}</span>
              </li>
            ))}
          </ul>
        )}
      </div>

      <div className="cp-dossier-plate" aria-hidden="true" data-status={(status || "").toLowerCase().replace(/\s+/g, "-")}>
        <div className="cp-plate-header">
          <span className="cp-plate-pulse" />
          <span className="cp-plate-label">CONTAINER ID</span>
          <span className="cp-plate-status">{status}</span>
        </div>
        <div className="cp-plate-body">
          <span className="cp-plate-stencil">{data.stencil}</span>
          <div className="cp-plate-meta">
            <div>
              <span className="cp-plate-meta-k">BAY</span>
              <span className="cp-plate-meta-v">{data.label}</span>
            </div>
            <div>
              <span className="cp-plate-meta-k">CODE</span>
              <span className="cp-plate-meta-v">{(data.id || "").toUpperCase()}-{data.year || "·"}</span>
            </div>
            <div>
              <span className="cp-plate-meta-k">CLASS</span>
              <span className="cp-plate-meta-v">{(category || "").toUpperCase()}</span>
            </div>
          </div>
        </div>
        <div className="cp-plate-footer">
          <span className="cp-plate-bars">||||||||||||</span>
          <span className="cp-plate-foot-id">OPR-001</span>
        </div>
      </div>
    </header>
  );
}

// CpActionBar — primary external routes pulled up above the fold of the
// dossier. Recruiters and operators scanning a project should be able to
// jump to the live site, repo, or app store without scrolling past the
// hero. Industrial chips, not generic SaaS buttons — they sit on the same
// design language as the gantry signage and operator console.
function CpActionBar({ data }) {
  // Project-specific links first (live site, app store, repo, demo).
  const projectLinks = [
    ...(Array.isArray(data.links) ? data.links : []),
    ...(Array.isArray(data.externalLinks) ? data.externalLinks : []),
  ].filter(pair => Array.isArray(pair) && pair[1]);
  // Per-bay resume override or fall through to the operator-level resume.
  const resumeHref =
    data.resumeLink ||
    (window.YARD_DATA && window.YARD_DATA.identity && window.YARD_DATA.identity.routes &&
     window.YARD_DATA.identity.routes.resume && window.YARD_DATA.identity.routes.resume.href) || null;

  // De-dupe by href.
  const seen = new Set();
  const items = [];
  for (const pair of projectLinks) {
    const href = pair[1];
    if (!href || seen.has(href)) continue;
    seen.add(href);
    items.push(pair);
  }
  if (resumeHref && !seen.has(resumeHref)) {
    items.push(["Resume", resumeHref]);
    seen.add(resumeHref);
  }

  const categorize = (label, href) => {
    const t = ((label || "") + " " + (href || "")).toLowerCase();
    if (t.includes("github"))                                   return { kind: "repo",    short: "GITHUB" };
    if (t.includes("app store") || t.includes("apps.apple"))    return { kind: "app",     short: "APP STORE" };
    if (t.includes("devpost") || t.includes("hackathon"))       return { kind: "demo",    short: "DEVPOST" };
    if (t.includes("linkedin"))                                 return { kind: "link",    short: "LINKEDIN" };
    if (t.includes("resume") || t.endsWith(".pdf"))             return { kind: "resume",  short: "RESUME" };
    if (t.includes("demo"))                                     return { kind: "demo",    short: "DEMO" };
    if (t.includes("website") || /^https?:\/\//.test(href))     return { kind: "site",    short: "LIVE SITE" };
    return { kind: "link", short: (label || "OPEN").toUpperCase() };
  };

  // Dispatch (contact) is always available — opens BAY-11 via the global
  // scene handle. Stays a button so it never shows up in tab focus as an
  // empty anchor on dossiers loaded before the scene mounts.
  const openContact = () => {
    const scene = window.YARD_SCENE;
    if (scene && typeof scene.openContact === "function") scene.openContact();
  };
  return (
    <nav className="cp-action-bar" aria-label="External routes">
      <span className="cp-action-tag">// ROUTES</span>
      <div className="cp-action-chips">
        {items.map(([label, href], i) => {
          const meta = categorize(label, href);
          return (
            <a
              key={i}
              className={"cp-action-chip cp-chip-" + meta.kind}
              href={href}
              target={href.startsWith("/") ? "_self" : "_blank"}
              rel="noreferrer"
              title={label}
            >
              <span className="cp-chip-icon"><CpLinkIcon label={label} href={href} /></span>
              <span className="cp-chip-label">{meta.short}</span>
              <span className="cp-chip-arrow" aria-hidden="true">↗</span>
            </a>
          );
        })}
        <button
          type="button"
          className="cp-action-chip cp-chip-contact"
          onClick={openContact}
          title="Open dispatch line (BAY-11)"
        >
          <span className="cp-chip-icon"><CpLinkIcon label="contact" href="mailto:" /></span>
          <span className="cp-chip-label">CONTACT</span>
          <span className="cp-chip-arrow" aria-hidden="true">→</span>
        </button>
      </div>
    </nav>
  );
}

function CpBulletList({ items, variant }) {
  if (!items.length) return null;
  return (
    <ul className={"cp-list" + (variant === "checks" ? " cp-list-checks" : "")}>
      {items.map((item, i) => (
        <li key={i}>
          {variant === "checks" ? <span className="cp-check">›</span> : <span className="cp-bullet" />}
          <span>{item}</span>
        </li>
      ))}
    </ul>
  );
}

function CpImpactList({ items }) {
  if (!items.length) return null;
  return (
    <div className="cp-impact-grid">
      {items.map((item, i) => (
        <div key={i} className="cp-impact-card">
          <span className="cp-impact-k">OUTCOME {String(i + 1).padStart(2, "0")}</span>
          <span className="cp-impact-v">{item}</span>
        </div>
      ))}
    </div>
  );
}

// CpQuickScan — 5-second answer for recruiters scanning fast. Sits between
// the cinematic visual block and the deep body so a hurried reader gets
// role, year, top stack, and primary outcome without scrolling. Intentionally
// quiet styling — earns its place by being first, not loud.
function CpQuickScan({ data, category }) {
  // Pull a "role" from stats: prefer ROLE, fall back to TYPE/PLATFORM, then category.
  const stats = Array.isArray(data.stats) ? data.stats : [];
  const findStat = (k) => {
    const row = stats.find(([key]) => (key || "").toUpperCase() === k);
    return row ? row[1] : null;
  };
  const role = findStat("ROLE") || findStat("TYPE") || findStat("PLATFORM") || category;
  const stack = (data.stack || data.skills || []).slice(0, 4);
  const outcome = Array.isArray(data.impact) && data.impact.length
    ? data.impact[0]
    : Array.isArray(data.overview) && data.overview.length
    ? data.overview[0]
    : null;
  // Render only when there's enough signal to be useful — a chapter with
  // no role / stack / outcome would render an empty strip.
  const hasContent = role || stack.length || outcome;
  if (!hasContent) return null;
  return (
    <section className="cp-quickscan" aria-label="Quick scan">
      <div className="cp-qs-row">
        {role && (
          <div className="cp-qs-cell">
            <span className="cp-qs-k">ROLE</span>
            <span className="cp-qs-v">{role}</span>
          </div>
        )}
        {data.year && (
          <div className="cp-qs-cell cp-qs-cell-narrow">
            <span className="cp-qs-k">YEAR</span>
            <span className="cp-qs-v">{data.year}</span>
          </div>
        )}
        {stack.length > 0 && (
          <div className="cp-qs-cell cp-qs-cell-stack">
            <span className="cp-qs-k">KEY SYSTEMS</span>
            <span className="cp-qs-v cp-qs-v-stack">{stack.join(" · ")}</span>
          </div>
        )}
      </div>
      {outcome && (
        <div className="cp-qs-outcome">
          <span className="cp-qs-k">OUTCOME</span>
          <span className="cp-qs-out-v">{outcome}</span>
        </div>
      )}
    </section>
  );
}

function CpManifestAside({ data, category }) {
  const status = data.status || "ACTIVE";
  const rows = [
    ["BAY", data.label],
    ["TYPE", category],
    ["YEAR", data.year],
    ["STATUS", status],
    ...(data.stats || []),
  ];
  const uniqueRows = rows.filter(([k], i) => rows.findIndex(([kk]) => kk === k) === i).slice(0, 7);

  // Architecture bullets — short, terse system descriptors. Pulled from
  // features (already terse) or derived from stack tags as a fallback so
  // every pod surfaces some architectural shape, even chapter bays.
  const arch = (Array.isArray(data.features) && data.features.length
    ? data.features
    : (Array.isArray(data.stack) ? data.stack : [])
  ).slice(0, 4);

  // Timeline — synthesize a tiny deployment timeline from the project's
  // status + year + stats. Falls back gracefully when fields are missing.
  const timeline = buildPodTimeline(data);

  return (
    <aside className="cp-aside" aria-label="Container manifest details">
      <div className="cp-aside-card cp-intel-card">
        <div className="cp-aside-title">
          <span className="cp-intel-glyph" />
          Manifest
        </div>
        <div className="cp-manifest">
          {uniqueRows.map(([k, v]) => (
            <div key={k + v} className="cp-manifest-row">
              <span className="cp-manifest-k">{k}</span>
              <span className="cp-manifest-v">{v}</span>
            </div>
          ))}
        </div>
      </div>

      {arch.length > 0 && (
        <div className="cp-aside-card cp-intel-card">
          <div className="cp-aside-title">
            <span className="cp-intel-glyph" />
            Architecture
          </div>
          <ul className="cp-arch-list">
            {arch.map((line, i) => (
              <li key={i} className="cp-arch-row">
                <span className="cp-arch-hash">{`#${String(i + 1).padStart(2, "0")}`}</span>
                <span className="cp-arch-line">{line}</span>
              </li>
            ))}
          </ul>
        </div>
      )}

      <CpDiagram data={data} />

      {timeline.length > 0 && (
        <div className="cp-aside-card cp-intel-card">
          <div className="cp-aside-title">
            <span className="cp-intel-glyph" />
            Timeline
          </div>
          <ol className="cp-timeline-list">
            {timeline.map((entry, i) => (
              <li key={i} className={"cp-timeline-row" + (entry.now ? " is-now" : "")}>
                <span className="cp-timeline-node" />
                <span className="cp-timeline-tag">{entry.tag}</span>
                <span className="cp-timeline-text">{entry.text}</span>
              </li>
            ))}
          </ol>
        </div>
      )}
    </aside>
  );
}

// CpDiagram — small per-signature SVG that gives each pod a custom-built
// "system map" feel without leaning into stock illustrations. Every glyph
// reads as an operational schematic: nodes, flow, branches, pulse. CSS
// drives the slow animations so the diagram feels observed, not authored.
function CpDiagram({ data }) {
  if (!data) return null;
  const sig = data.signature || "default";
  const map = cpDiagramFor(sig);
  if (!map) return null;
  return (
    <div className="cp-aside-card cp-intel-card cp-diagram-card">
      <div className="cp-aside-title">
        <span className="cp-intel-glyph" />
        System Map
        <span className="cp-diagram-tag">{map.tag}</span>
      </div>
      <div className="cp-diagram-body" data-sig={sig} aria-hidden="true">
        {map.svg}
      </div>
    </div>
  );
}

function cpDiagramFor(sig) {
  // Each SVG uses viewBox 0 0 200 120 so the body card can scale them
  // cleanly. Strokes use currentColor (themed via cp-accent on the body)
  // and animations live in the stylesheet (cp-diagram-*).
  switch (sig) {
    case "neural": {
      // Cortex — sparse knowledge graph: a central node with peripheral
      // nodes connected by thin lines that pulse along their length.
      const nodes = [
        [100, 60, 8], [40, 32, 4.5], [160, 30, 5], [30, 88, 4],
        [170, 92, 4.5], [108, 18, 3.5], [70, 70, 3.5], [134, 76, 4],
      ];
      const links = [
        [0,1],[0,2],[0,3],[0,4],[0,5],[0,6],[0,7],[1,5],[2,5],[1,6],[2,7],
      ];
      return {
        tag: "graph",
        svg: (
          <svg className="cp-svg cp-svg-neural" viewBox="0 0 200 120">
            {links.map(([a, b], i) => (
              <line key={i}
                    x1={nodes[a][0]} y1={nodes[a][1]}
                    x2={nodes[b][0]} y2={nodes[b][1]}
                    className="cp-svg-link"
                    style={{ animationDelay: (i * 220) + "ms" }} />
            ))}
            {nodes.map(([x, y, r], i) => (
              <circle key={i} cx={x} cy={y} r={r}
                      className={"cp-svg-node" + (i === 0 ? " is-core" : "")}
                      style={{ animationDelay: (i * 320) + "ms" }} />
            ))}
          </svg>
        ),
      };
    }
    case "dispatch": {
      // PODS — left-to-right dispatch flow: three stops connected by an
      // arrow track. A small "load" indicator drifts along the track.
      return {
        tag: "flow",
        svg: (
          <svg className="cp-svg cp-svg-dispatch" viewBox="0 0 200 120">
            <line x1="20" y1="60" x2="180" y2="60" className="cp-svg-track" />
            <line x1="20" y1="60" x2="180" y2="60" className="cp-svg-track-dash" />
            {[30, 100, 170].map((x, i) => (
              <g key={i}>
                <rect x={x - 10} y="48" width="20" height="24" className="cp-svg-stop" />
                <text x={x} y="92" className="cp-svg-stop-label">
                  {["ORIG", "TRANSIT", "DELV"][i]}
                </text>
              </g>
            ))}
            <circle cx="20" cy="60" r="4" className="cp-svg-shuttle" />
          </svg>
        ),
      };
    }
    case "scanlines": {
      // MyGUI — stacked horizontal scanlines drifting at different speeds,
      // mimicking a live feed monitor.
      const rows = [22, 36, 50, 64, 78, 92];
      return {
        tag: "feed",
        svg: (
          <svg className="cp-svg cp-svg-scan" viewBox="0 0 200 120">
            {rows.map((y, i) => (
              <line key={i} x1="20" y1={y} x2="180" y2={y}
                    className="cp-svg-scanline"
                    style={{
                      animationDelay: (i * 280) + "ms",
                      animationDuration: (3.6 + i * 0.4) + "s",
                    }} />
            ))}
          </svg>
        ),
      };
    }
    case "network": {
      // SEC — radial hub-and-spoke with a slow rotation hint.
      const center = [100, 60];
      const spokes = [
        [40, 30], [160, 30], [30, 65], [170, 60], [50, 92], [148, 96], [100, 18], [100, 102],
      ];
      return {
        tag: "network",
        svg: (
          <svg className="cp-svg cp-svg-network" viewBox="0 0 200 120">
            {spokes.map(([x, y], i) => (
              <line key={"l" + i} x1={center[0]} y1={center[1]} x2={x} y2={y}
                    className="cp-svg-link"
                    style={{ animationDelay: (i * 180) + "ms" }} />
            ))}
            {spokes.map(([x, y], i) => (
              <circle key={"n" + i} cx={x} cy={y} r="3.5" className="cp-svg-node"
                      style={{ animationDelay: (i * 260) + "ms" }} />
            ))}
            <circle cx={center[0]} cy={center[1]} r="6.5" className="cp-svg-node is-core" />
          </svg>
        ),
      };
    }
    case "branching": {
      // YUTH — decision tree from a single root.
      const lines = [
        ["20,60", "70,60"],
        ["70,60", "120,32"],
        ["70,60", "120,60"],
        ["70,60", "120,88"],
        ["120,32", "170,22"],
        ["120,32", "170,42"],
        ["120,60", "170,60"],
        ["120,88", "170,80"],
        ["120,88", "170,98"],
      ];
      const leafs = [[170, 22], [170, 42], [170, 60], [170, 80], [170, 98]];
      return {
        tag: "tree",
        svg: (
          <svg className="cp-svg cp-svg-tree" viewBox="0 0 200 120">
            {lines.map((pts, i) => {
              const [a, b] = pts;
              const [x1, y1] = a.split(",").map(Number);
              const [x2, y2] = b.split(",").map(Number);
              return <line key={i} x1={x1} y1={y1} x2={x2} y2={y2}
                           className="cp-svg-link"
                           style={{ animationDelay: (i * 140) + "ms" }} />;
            })}
            <circle cx="20" cy="60" r="5" className="cp-svg-node is-core" />
            {[[70, 60], [120, 32], [120, 60], [120, 88]].map(([x, y], i) => (
              <circle key={"j" + i} cx={x} cy={y} r="3.5" className="cp-svg-node"
                      style={{ animationDelay: (i * 220) + "ms" }} />
            ))}
            {leafs.map(([x, y], i) => (
              <rect key={"l" + i} x={x - 3.5} y={y - 3.5} width="7" height="7"
                    className="cp-svg-leaf"
                    style={{ animationDelay: (i * 240) + "ms" }} />
            ))}
          </svg>
        ),
      };
    }
    case "pulse": {
      // Potentia — single wave pulse oscillating across the panel.
      return {
        tag: "pulse",
        svg: (
          <svg className="cp-svg cp-svg-pulse" viewBox="0 0 200 120">
            <line x1="0" y1="60" x2="200" y2="60" className="cp-svg-axis" />
            <path className="cp-svg-wave"
                  d="M0 60 Q 20 60 30 60 L 50 60 L 56 30 L 64 90 L 72 60 L 100 60 L 108 38 L 116 82 L 124 60 L 200 60" />
            <circle cx="0" cy="60" r="3" className="cp-svg-pulse-dot" />
          </svg>
        ),
      };
    }
    case "academic": {
      // Guelph — progression timeline marks (small ticks along an axis).
      const ticks = [30, 60, 90, 120, 150, 180];
      return {
        tag: "progression",
        svg: (
          <svg className="cp-svg cp-svg-academic" viewBox="0 0 200 120">
            <line x1="20" y1="60" x2="180" y2="60" className="cp-svg-axis" />
            {ticks.map((x, i) => (
              <g key={i}>
                <line x1={x} y1="50" x2={x} y2="70" className="cp-svg-tick"
                      style={{ animationDelay: (i * 220) + "ms" }} />
                {i % 2 === 0 && (
                  <text x={x} y="86" className="cp-svg-tick-label">{["'23","'24","'25","'26"][Math.floor(i/2)] || ""}</text>
                )}
              </g>
            ))}
            <circle cx={ticks[ticks.length - 2]} cy="60" r="4.5" className="cp-svg-node is-core" />
          </svg>
        ),
      };
    }
    case "yard":
    case "default":
    case "manifest":
    case "warning": {
      // Intro / Contact / Lab — compact zone map of the yard itself.
      // Same diagram for all "world-level" bays — reinforces that they
      // describe the world, not a single project.
      return {
        tag: "yard",
        svg: (
          <svg className="cp-svg cp-svg-yard" viewBox="0 0 200 120">
            <rect x="10"  y="22" width="36" height="76" className="cp-svg-zone" style={{ animationDelay: "0ms"  }} />
            <rect x="62"  y="22" width="56" height="76" className="cp-svg-zone is-build" style={{ animationDelay: "120ms" }} />
            <rect x="134" y="22" width="22" height="76" className="cp-svg-zone" style={{ animationDelay: "240ms" }} />
            <rect x="172" y="22" width="18" height="76" className="cp-svg-zone" style={{ animationDelay: "360ms" }} />
            <line x1="46" y1="60" x2="62" y2="60" className="cp-svg-track" />
            <line x1="118" y1="60" x2="134" y2="60" className="cp-svg-track" />
            <line x1="156" y1="60" x2="172" y2="60" className="cp-svg-track" />
            <text x="28"  y="110" className="cp-svg-zone-label">ORIG</text>
            <text x="90"  y="110" className="cp-svg-zone-label">BUILD</text>
            <text x="145" y="110" className="cp-svg-zone-label">N&amp;N</text>
            <text x="181" y="110" className="cp-svg-zone-label">OPS</text>
          </svg>
        ),
      };
    }
    default:
      return null;
  }
}

// Synthesizes a small deployment timeline strip from existing pod fields. We
// deliberately keep this terse and lore-consistent (concept → ship → operate)
// rather than inventing dates that don't exist in data.
function buildPodTimeline(data) {
  if (!data) return [];
  const year = data.year || "·";
  const status = (data.status || "ACTIVE").toUpperCase();
  const type = (data.type || "chapter");
  // Map per-pod milestones — keeps them lore-consistent without inventing
  // fake dates. Each entry: { tag, text, now? }.
  const id = data.id;
  if (id === "cortex") return [
    { tag: "Q1", text: "WebGL canvas r&d" },
    { tag: "Q2", text: "retrieval pipeline ship" },
    { tag: "Q3", text: "agent layer · providers" },
    { tag: "NOW", text: "private beta · iterating", now: true },
  ];
  if (id === "mygui") return [
    { tag: "'24 Q3", text: "co-founded · scoping SMEs" },
    { tag: "'24 Q4", text: "chatbot architecture" },
    { tag: "'25 Q1", text: "voice + multilingual" },
    { tag: "NOW", text: "analytics + integrations", now: true },
  ];
  if (id === "pods") return [
    { tag: "'26 Q1", text: "offer accepted" },
    { tag: "'26 Q2", text: "pre-onboarding" },
    { tag: "SUMMER", text: "IT intern · on-site", now: true },
    { tag: "TBD", text: "team assignment" },
  ];
  if (id === "sec") return [
    { tag: "'24 Q3", text: "president · chapter formed" },
    { tag: "'24 Q4", text: "first founder events" },
    { tag: "'25 Q1", text: "speaker nights · workshops" },
    { tag: "NOW", text: "building the operating team", now: true },
  ];
  if (id === "yuth") return [
    { tag: "HOUR 0", text: "team formed · scoping" },
    { tag: "HOUR 12", text: "resource model" },
    { tag: "HOUR 24", text: "frontend + integration" },
    { tag: "HOUR 36", text: "live pitch · finalist", now: true },
  ];
  if (id === "potentia") return [
    { tag: "'24 Q2", text: "concept + build" },
    { tag: "'24 Q3", text: "TestFlight · iterate" },
    { tag: "'24 Q4", text: "App Store · live" },
    { tag: "NOW", text: "journaling + trends", now: true },
  ];
  if (id === "scholarscope") return [
    { tag: "'25 Q1", text: "scope + data layer" },
    { tag: "'25 Q2", text: "search interface" },
    { tag: "'25 Q3", text: "deploy on Vercel" },
    { tag: "NOW", text: "operating · iterating", now: true },
  ];
  if (id === "algoviz") return [
    { tag: "'25 Q2", text: "canvas + animation loop" },
    { tag: "'25 Q3", text: "step controls + speed" },
    { tag: "'25 Q4", text: "sorting set complete" },
    { tag: "NOW", text: "live · educational use", now: true },
  ];
  if (id === "guelph") return [
    { tag: "'23", text: "enrolled · BSc CS" },
    { tag: "'24", text: "SEC · MyGUI" },
    { tag: "'25", text: "Cortex · ScholarScope · AlgoViz" },
    { tag: "'26", text: "graduating · PODS", now: true },
  ];
  if (id === "intro") return [
    { tag: "v0.1", text: "yard skeleton · boot" },
    { tag: "v1.0", text: "minimap · dossiers" },
    { tag: "v2.0", text: "tour · operator dossier" },
    { tag: "NOW", text: "operational pass · live", now: true },
  ];
  // Fallback for unknown / debug / contact — keep it short and honest.
  return [
    { tag: year, text: status, now: true },
  ];
}

// CpOpsConsole — dense operational widget that anchors the panel between
// the visual block and the long-form sections. Fills the dead space the
// procedural-art card used to occupy with: live signal cells, a system
// feed mimicking a terminal tail, and a deployment chip strip. Every value
// comes from existing data fields — no fabricated metrics.
function CpOpsConsole({ data, category }) {
  if (!data) return null;
  const status = data.status || "ACTIVE";
  const stats = Array.isArray(data.stats) ? data.stats : [];
  const evidence = Array.isArray(data.evidence) ? data.evidence : [];
  const stack = (data.stack || data.skills || []).slice(0, 8);

  // Build up to 6 signal cells from status / year / stats. We dedupe by key
  // so a stats row with a STATUS entry doesn't double up the badge.
  const seen = new Set();
  const cells = [];
  const pushCell = (k, v) => {
    const key = (k || "").toUpperCase();
    if (!key || !v || seen.has(key)) return;
    seen.add(key);
    cells.push({ k: key, v });
  };
  pushCell("STATUS", status);
  pushCell("YEAR", data.year);
  pushCell("CLASS", (category || "").toUpperCase());
  stats.forEach(([k, v]) => pushCell(k, v));

  const finalCells = cells.slice(0, 6);

  // Build a longer source pool of feed lines so the tail can rotate without
  // ever feeling repetitive. We dedupe — every visible line is sourced from
  // existing data; nothing fabricated.
  const sourceLines = useMemo(() => {
    const acc = [];
    evidence.forEach(line => acc.push(line));
    acc.push(`${(category || data.type || "").toUpperCase()} · ${status}`);
    acc.push(`${data.label} · ${data.year || "·"}`);
    acc.push(`OPR-001 · CLEARED`);
    if (Array.isArray(data.stack) && data.stack.length) {
      acc.push(`STACK · ${data.stack.slice(0, 3).join(" / ").toUpperCase()}`);
    }
    if (Array.isArray(data.stats)) {
      data.stats.forEach(([k, v]) => acc.push(`${(k || "").toUpperCase()} · ${v}`));
    }
    return Array.from(new Set(acc.filter(Boolean)));
  }, [data && data.id]);

  const VISIBLE = 4;
  const [feedHead, setFeedHead] = useState(0);
  const [ticking, setTicking] = useState(false);

  // Reset on pod switch
  useEffect(() => { setFeedHead(0); setTicking(false); }, [data && data.id]);

  // Rotate the feed window on a wide interval so the tail feels alive
  // without ever stealing focus. Respects prefers-reduced-motion.
  useEffect(() => {
    if (!sourceLines || sourceLines.length <= VISIBLE) return;
    const mq = window.matchMedia && window.matchMedia("(prefers-reduced-motion: reduce)");
    if (mq && mq.matches) return;
    const id = window.setInterval(() => {
      setTicking(true);
      window.setTimeout(() => {
        setFeedHead(h => (h + 1) % sourceLines.length);
        setTicking(false);
      }, 240);
    }, 3600);
    return () => window.clearInterval(id);
  }, [sourceLines]);

  const visible = [];
  for (let i = 0; i < Math.min(VISIBLE, sourceLines.length); i++) {
    visible.push(sourceLines[(feedHead + i) % sourceLines.length]);
  }

  // Stable, deterministic time codes — each visible row gets a tick code
  // that drifts forward as the feed rotates, so the tail reads like a
  // live operations log rather than static text.
  const timeFor = (offset) => {
    const base = (data.id || "x").split("").reduce((s, c) => s + c.charCodeAt(0), 0);
    const seed = base + (feedHead + offset);
    const h = (seed * 7) % 24;
    const m = (seed * 31 + offset * 13) % 60;
    return `${String(h).padStart(2, "0")}:${String(m).padStart(2, "0")}`;
  };

  return (
    <section
      className={"cp-ops-console" + (ticking ? " is-ticking" : "")}
      data-theme={data.id}
      aria-label="Operational console"
    >
      <span className="cp-ops-sweep" aria-hidden="true" />
      <div className="cp-ops-grid">
        <div className="cp-ops-block cp-ops-block-signal">
          <div className="cp-ops-head">
            <span className="cp-ops-pulse" />
            <span className="cp-ops-head-k">// LIVE SIGNAL</span>
            <span className="cp-ops-head-meta">{data.label}</span>
          </div>
          <div className="cp-ops-cells">
            {finalCells.map((c, i) => (
              <div key={c.k + i} className="cp-ops-cell" style={{ animationDelay: (i * 60) + "ms" }}>
                <span className="cp-ops-cell-k">{c.k}</span>
                <span className="cp-ops-cell-v">{c.v}</span>
              </div>
            ))}
          </div>
        </div>

        <div className="cp-ops-block cp-ops-block-feed">
          <div className="cp-ops-head">
            <span className="cp-ops-pulse" />
            <span className="cp-ops-head-k">// SYSTEM FEED</span>
            <span className="cp-ops-head-meta cp-ops-head-bars">▰ ▰ ▰ ▰ ▱ ▱</span>
          </div>
          <ul className="cp-ops-feed-list">
            {visible.map((line, i) => (
              <li
                key={`${feedHead}-${i}`}
                className={"cp-ops-feed-row" + (i === visible.length - 1 ? " is-newest" : "")}
              >
                <span className="cp-ops-feed-time">{timeFor(i)}</span>
                <span className="cp-ops-feed-arrow">›</span>
                <span className="cp-ops-feed-line">{line}</span>
              </li>
            ))}
            <li className="cp-ops-feed-row cp-ops-feed-cursor">
              <span className="cp-ops-feed-time">{timeFor(visible.length)}</span>
              <span className="cp-ops-feed-arrow">›</span>
              <span className="cp-ops-feed-line">
                <span className="cp-ops-cursor" aria-hidden="true">▋</span>
              </span>
            </li>
          </ul>
        </div>

        {stack.length > 0 && (
          <div className="cp-ops-block cp-ops-block-stack">
            <div className="cp-ops-head">
              <span className="cp-ops-head-k">// DEPLOYMENT</span>
              <span className="cp-ops-head-meta">{stack.length} systems</span>
            </div>
            <div className="cp-ops-chips">
              {stack.map((t, i) => (
                <span key={t + i} className="cp-ops-chip">
                  <span className="cp-ops-chip-dot" />
                  <span className="cp-ops-chip-label">{t}</span>
                </span>
              ))}
            </div>
          </div>
        )}
      </div>
    </section>
  );
}

function ContainerPanel({ data, onClose, onPrev, onNext, prevLabel, nextLabel, position, total }) {
  if (!data) return null;
  // Mobile swipe — track horizontal swipes inside the panel scroll area and
  // route them to prev/next. Vertical scrolling is preserved.
  const swipeRef = useRef({ x: 0, y: 0, locked: null });
  const onTouchStart = (e) => {
    if (!e.touches || e.touches.length !== 1) return;
    swipeRef.current = { x: e.touches[0].clientX, y: e.touches[0].clientY, locked: null };
  };
  const onTouchMove = (e) => {
    if (!e.touches || e.touches.length !== 1) return;
    const dx = e.touches[0].clientX - swipeRef.current.x;
    const dy = e.touches[0].clientY - swipeRef.current.y;
    if (swipeRef.current.locked === null && (Math.abs(dx) > 8 || Math.abs(dy) > 8)) {
      swipeRef.current.locked = Math.abs(dx) > Math.abs(dy) ? "x" : "y";
    }
  };
  const onTouchEnd = (e) => {
    const t = (e.changedTouches && e.changedTouches[0]) || null;
    if (!t || swipeRef.current.locked !== "x") return;
    const dx = t.clientX - swipeRef.current.x;
    const SWIPE = 60;
    if (dx <= -SWIPE && onNext) onNext();
    else if (dx >= SWIPE && onPrev) onPrev();
  };

  const category = cpCategory(data);
  const overview = Array.isArray(data.overview)
    ? data.overview
    : data.overview ? [data.overview]
    : (data.body || []);
  const whatItIs = cpAsArray(data.what || overview);
  const did = cpAsArray(data.did || data.built);
  const signals = cpAsArray(data.features);
  const skills = cpAsArray(data.skills || data.stack);
  const impact = cpAsArray(data.impact || data.outcome);
  const todos = cpAsArray(data.todos);

  const why = data.why || null;

  const sections = [];
  let n = 1;
  if (why) {
    sections.push(
      <CpSection key="why" index={n++} label="MISSION RATIONALE" accent="var(--pods-orange)">
        <p className="cp-paragraph cp-why">{why}</p>
      </CpSection>
    );
  }
  if (whatItIs.length) {
    sections.push(
      <CpSection key="what" index={n++} label="MISSION OVERVIEW" accent="var(--blueprint)">
        {whatItIs.map((p, i) => <p key={i} className="cp-paragraph">{p}</p>)}
      </CpSection>
    );
  }
  if (did.length) {
    sections.push(
      <CpSection key="built" index={n++} label="BUILD LOG" accent="var(--terminal-green)">
        <CpBulletList items={did} />
      </CpSection>
    );
  }
  if (skills.length) {
    sections.push(
      <CpSection key="stack" index={n++} label="SYSTEMS USED" accent="var(--terminal-amber)">
        <div className="cp-tags">
          {skills.map((t, i) => <span key={i} className="cp-tag">{t}</span>)}
        </div>
      </CpSection>
    );
  }
  if (impact.length) {
    sections.push(
      <CpSection key="impact" index={n++} label="FIELD RESULT" accent="var(--blueprint)">
        <CpImpactList items={impact} />
      </CpSection>
    );
  }
  if (todos.length) {
    sections.push(
      <CpSection key="todos" index={n++} label="INTEL PENDING" accent="var(--warning)">
        <ul className="cp-list cp-list-todo">
          {todos.map((item, i) => (
            <li key={i}><span className="cp-check">!</span><span>{item}</span></li>
          ))}
        </ul>
      </CpSection>
    );
  }
  const linkList = [
    ...(Array.isArray(data.links) ? data.links : []),
    ...(Array.isArray(data.externalLinks) ? data.externalLinks : []),
  ].filter(pair => Array.isArray(pair) && pair[1]);
  if (data.resumeLink) linkList.push(["Resume", data.resumeLink]);
  if (linkList.length) {
    sections.push(
      <CpSection key="links" index={n++} label="ROUTES" accent="var(--terminal-green)">
        <div className="cp-link-grid">
          {linkList.map(([label, href], i) => (
            <a key={i} className="cp-link-card" href={href} target="_blank" rel="noreferrer">
              <span className="cp-link-icon"><CpLinkIcon label={label} href={href} /></span>
              <span className="cp-link-label">{label}</span>
              <span className="cp-link-arrow">↗</span>
            </a>
          ))}
        </div>
      </CpSection>
    );
  }

  const aux = data.images && data.images[1];

  return (
    <div className="panel panel-deploy open">
      <div className="panel-bg" onClick={onClose} aria-label="Close panel" />
      <div className="panel-deploy-frame" aria-hidden="true">
        <span className="pdf-corner pdf-tl" />
        <span className="pdf-corner pdf-tr" />
        <span className="pdf-corner pdf-bl" />
        <span className="pdf-corner pdf-br" />
        <span className="pdf-tag">// CONTAINER MANIFEST · DEPLOYED</span>
      </div>
      <div
        className={"panel-card cp-card-v2 cp-theme-" + data.id}
        style={{ "--cp-accent": data.color }}
      >
        <div className="panel-strip" />
        <div className="cp-nav">
          {onPrev && (
            <button type="button" className="cp-nav-btn cp-nav-prev" onClick={onPrev} aria-label={prevLabel ? `Previous bay · ${prevLabel}` : "Previous bay"}>
              <span className="cp-nav-arrow">‹</span>
              <span className="cp-nav-meta">
                <span className="cp-nav-k">PREV</span>
                {prevLabel && <span className="cp-nav-v">{prevLabel}</span>}
              </span>
            </button>
          )}
          {onNext && (
            <button type="button" className="cp-nav-btn cp-nav-next" onClick={onNext} aria-label={nextLabel ? `Next bay · ${nextLabel}` : "Next bay"}>
              <span className="cp-nav-meta">
                <span className="cp-nav-k">NEXT</span>
                {nextLabel && <span className="cp-nav-v">{nextLabel}</span>}
              </span>
              <span className="cp-nav-arrow">›</span>
            </button>
          )}
          <button type="button" className="cp-nav-back" onClick={onClose} aria-label="Back to yard">
            <span className="cp-nav-arrow">↩</span>
            <span className="cp-nav-k">BACK TO YARD</span>
          </button>
          {position && total && (
            <span className="cp-nav-position" aria-label={`Container ${position} of ${total}`}>
              <span className="cp-pos-cur">{String(position).padStart(2, "0")}</span>
              <span className="cp-pos-sep">/</span>
              <span className="cp-pos-total">{String(total).padStart(2, "0")}</span>
            </span>
          )}
        </div>
        <button type="button" className="panel-close cp-close-fixed" onClick={onClose} aria-label="close">×</button>

        <div
          className="cp-scroll"
          onTouchStart={onTouchStart}
          onTouchMove={onTouchMove}
          onTouchEnd={onTouchEnd}
        >
          <CpDossierHero data={data} category={category} />
          <CpActionBar data={data} />
          <CpVisual data={data} />
          <CpOpsConsole data={data} category={category} />
          <CpQuickScan data={data} category={category} />
          <CpMetrics data={data} />

          <div className="cp-dossier-grid">
            <div className="cp-sections">
              {sections}
              {aux && (
                <figure className="cp-image-aux">
                  <SafeImage src={aux} alt={data.title + " · detail"} />
                </figure>
              )}
            </div>
            <CpManifestAside data={data} category={category} />
          </div>

          {/* Operator margin — quiet, hand-logged feel. Surfaces the
              project's humanNote at the foot of the dossier so the page
              closes on a personal note rather than a system stamp. */}
          {data.humanNote && (
            <aside className="cp-operator-margin" aria-label="Operator note">
              <span className="com-stamp">OPERATOR · 001 · LOG</span>
              <p className="com-text">{data.humanNote}</p>
              <span className="com-sig">// A.A.</span>
            </aside>
          )}

          <footer className="cp-footer">
            <span className="cp-footer-tag">// END · {data.label}</span>
            <span className="cp-footer-meta">FILED {data.year || "·"} · OPERATOR-001</span>
            <span className="cp-footer-pulse"><span className="op-dot" /> MANIFEST CLOSED</span>
          </footer>
        </div>
      </div>
    </div>
  );
}

// ─────────── DEBUG SCENE ───────────
function DebugScene({ onClose, onSolved, solved }) {
  const lines = [
    { n: 1,  t: "function shipContainer(bay, manifest) {", bug: false },
    { n: 2,  t: "  const door = bay.frontDoor;", bug: false },
    { n: 3,  t: "  if (!manifest) throw new Error('empty manifest');", bug: false },
    { n: 4,  t: "  door.unlock();", bug: false },
    { n: 5,  t: "  const cargo = manifest.items;", bug: false },
    { n: 6,  t: "  for (let i = 0; i <= cargo.length; i++) {", bug: true },  // off-by-one
    { n: 7,  t: "    bay.load(cargo[i]);", bug: false },
    { n: 8,  t: "  }", bug: false },
    { n: 9,  t: "  door.seal();", bug: false },
    { n: 10, t: "  return bay.dispatch();", bug: false },
    { n: 11, t: "}", bug: false },
  ];
  const [picked, setPicked] = useState(null);
  const [result, setResult] = useState(null);
  const pick = (n) => {
    setPicked(n);
    if (n === 6) {
      setResult({ ok: true, msg: "FIXED · off-by-one. cargo[cargo.length] is undefined; the loop should stop at i < cargo.length." });
      setTimeout(() => onSolved(), 1500);
    } else {
      setResult({ ok: false, msg: "Not that line. The failure only happens on the last cargo item." });
    }
  };

  const openContactDirect = useCallback(() => {
    const scene = window.YARD_SCENE;
    if (!scene) return;
    const idx = (window.YARD_DATA.containers || []).findIndex(c => c.id === "contact");
    if (idx >= 0) scene.focusBay(idx);
  }, []);

  return (
    <div className="panel open debug-scene">
      <div className="panel-bg" onClick={onClose} />
      <div className="panel-card">
        <div className="panel-strip" />
        <div className="panel-head">
          <div className="panel-stencil" style={{color:"#FF4444"}}>!!</div>
          <div className="panel-title">
            <div className="label" style={{color:"#FF4444"}}>BAY-10 · LAB · R&D · 02:14 AM</div>
            <h1>LAB</h1>
            <div className="summary">Operator R&D bay. Optional field test. Résumé and contact are always in BAY-11.</div>
          </div>
          <button className="panel-close" onClick={onClose} aria-label="close">×</button>
        </div>
        <div className="debug-grid">
          <div className="debug-code">
            {lines.map(l => {
              const cls = ["ln"];
              if (l.bug) cls.push("bug");
              if (picked === l.n) cls.push("picked", l.n === 6 ? "right" : "wrong");
              return (
                <div key={l.n} className={cls.join(" ")} onClick={() => !solved && pick(l.n)}>
                  <span className="num">{l.n}</span>
                  <span>{l.t}</span>
                </div>
              );
            })}
          </div>
          <div className="debug-side">
            <div className="debug-prompt">▸ stack trace</div>
            <div className="stack-trace">
              <div><span className="e">TypeError</span>: <span className="m">Cannot read properties of undefined (reading 'weight')</span></div>
              <div><span className="at">  at bay.load</span> (yard.js:7:14)</div>
              <div><span className="at">  at shipContainer</span> (yard.js:7:5)</div>
              <div><span className="at">  at dispatch.queue</span> (yard.js:1:1)</div>
              <div><span className="at">  …throws on last cargo only</span></div>
            </div>
            <div className="debug-prompt">▸ task</div>
            <div style={{fontSize:12, color:"var(--steel-200)", lineHeight:1.6}}>
              Pick the line that broke prod. Optional puzzle. Contact and résumé are always open in BAY-11.
            </div>
            {solved ? (
              <div className="debug-result ok">resolved · bonus stamp on BAY-11</div>
            ) : result && (
              <div className={"debug-result " + (result.ok ? "ok" : "bad")}>{result.msg}</div>
            )}
            <button
              type="button"
              className="debug-skip"
              onClick={openContactDirect}
              aria-label="Skip puzzle and open BAY-11 contact"
            >
              Skip puzzle · open BAY-11 →
            </button>
          </div>
        </div>
      </div>
    </div>
  );
}

// ─────────── CONTACT (manifest) ───────────
function ContactRoutes() {
  const routes = (window.YARD_DATA.identity && window.YARD_DATA.identity.routes) || {};
  const order = ["email", "linkedin", "github", "resume"];
  const items = order.map(k => ({ key: k, ...routes[k] })).filter(r => r && r.label);
  if (!items.length) return null;
  return (
    <div className="contact-routes">
      {items.map(r => {
        const live = !!r.href;
        const Tag = live ? "a" : "div";
        const props = live
          ? { href: r.href, target: r.href.startsWith("mailto:") ? undefined : "_blank", rel: "noreferrer" }
          : { "aria-disabled": "true" };
        return (
          <Tag key={r.key} className={"contact-route" + (live ? "" : " is-pending")} {...props}>
            <span className="cr-icon"><CpLinkIcon label={r.key} href={r.href || ""} /></span>
            <span className="cr-meta">
              <span className="cr-label">{r.label.toUpperCase()}</span>
              <span className="cr-value">{r.value || (live ? r.href : "Detail pending")}</span>
            </span>
            <span className="cr-arrow">{live ? "↗" : "·"}</span>
          </Tag>
        );
      })}
    </div>
  );
}

function ContactPanel({ unlocked = true, debugBonus = false, onClose }) {
  // Dispatch phases — honest four-state machine. No "queued", no fake
  // "shipped": the UI only ever claims what Web3Forms actually confirmed.
  //   idle    → form ready
  //   sending → POST in flight
  //   success → Web3Forms returned success:true (delivery confirmed)
  //   error   → anything else; visitor gets Retry + an explicit mail route
  const [phase, setPhase] = useState("idle");
  const [errorMsg, setErrorMsg] = useState(null);
  const [form, setForm] = useState({ from: "", company: "", role: "Recruiter", message: "", botcheck: false });

  const dispatch = (window.YARD_DATA && window.YARD_DATA.identity && window.YARD_DATA.identity.dispatch) || {};
  const inbox = dispatch.inbox || "anthonyabdulnour@gmail.com";

  // audioCue — fire-and-forget. YARD_AUDIO no-ops unless the visitor has
  // opted into ambient sound, so this is safe regardless of toggle state.
  const audioCue = (name) => {
    try {
      const A = window.YARD_AUDIO;
      if (A && typeof A[name] === "function") A[name]();
    } catch (e) { /* silent */ }
  };

  // Manual mail route — built ONLY when the visitor explicitly clicks the
  // error-state button. Never auto-fired: a failed dispatch is reported
  // honestly, it does not silently hijack the page into a mail client.
  const openMailClient = () => {
    const to = dispatch.mailtoBackup || inbox;
    const subject = dispatch.subject || "THE_YARD · Incoming Dispatch";
    const body = [
      `From: ${form.from || "(no email)"}`,
      `Company: ${form.company || "—"}`,
      `Role: ${form.role || "—"}`,
      "",
      "Signal:",
      form.message || "(blank)",
      "",
      "// manual route — direct dispatch failed",
    ].join("\n");
    window.location.href =
      `mailto:${to}?subject=${encodeURIComponent(subject)}&body=${encodeURIComponent(body)}`;
  };

  const onSubmit = async (e) => {
    e.preventDefault();
    if (phase === "sending") return;
    // Honeypot — a real visitor can neither see nor tab to this field. A
    // bot that checks it gets a silent no-op: no request, no signal back.
    if (form.botcheck) return;

    const endpoint = dispatch.endpoint;
    const accessKey = dispatch.accessKey;
    // Misconfiguration guard — surface an honest failure rather than a fake
    // success if the relay was never wired.
    if (!endpoint || !accessKey) {
      setErrorMsg("dispatch relay not configured");
      setPhase("error");
      return;
    }

    setErrorMsg(null);
    setPhase("sending");
    try {
      const resp = await fetch(endpoint, {
        method: "POST",
        headers: { "Content-Type": "application/json", "Accept": "application/json" },
        body: JSON.stringify({
          access_key: accessKey,
          subject: dispatch.subject || "THE_YARD · Incoming Dispatch",
          from_name: form.from,
          name: form.from,
          email: form.from,
          company: form.company || "—",
          role: form.role || "—",
          message: form.message,
          botcheck: "",
        }),
      });
      // Web3Forms answers { success: true } on confirmed delivery, and a
      // non-2xx or { success: false } on any failure (bad key, ratelimit,
      // spam block, validation). Delivery is trusted ONLY on success:true —
      // a bare 200 is never enough to claim the message landed.
      let json = null;
      try { json = await resp.json(); } catch (_) { /* non-JSON → failure below */ }
      if (!resp.ok || !json || json.success !== true) {
        throw new Error((json && json.message) || `relay responded ${resp.status}`);
      }
      setPhase("success");
      audioCue("beep");
    } catch (err) {
      // Honest failure — no auto-mailto, no fake confirmation. The visitor
      // sees a real error state with explicit Retry / mail-client options.
      setErrorMsg((err && err.message) || "network error — transmission did not reach the relay");
      setPhase("error");
    }
  };

  const sending = phase === "sending";
  const success = phase === "success";
  const errored = phase === "error";

  return (
    <div className="panel open">
      <div className="panel-bg" onClick={onClose} />
      <div className="panel-card">
        <div className="panel-strip" />
        <div className="panel-head">
          <div className="panel-stencil">→→</div>
          <div className="panel-title">
            <div className="label">BAY-11 · OUTBOUND{debugBonus ? " · DEBUG CLEARED" : ""}</div>
            <h1>DISPATCH</h1>
            <div className="summary">Direct routes below. Submit posts straight to the operator inbox — real delivery, confirmed, no fake queue.</div>
          </div>
          <button className="panel-close" onClick={onClose} aria-label="close">×</button>
        </div>
        <div className="contact-routes-wrap">
          <h3 className="contact-routes-title">DIRECT ROUTES</h3>
          <ContactRoutes />
        </div>
        <form className="manifest" onSubmit={onSubmit} autoComplete="on">
          <div>
            <h3>CONTACT ROUTE</h3>
            <label htmlFor="contact-from">your email</label>
            <input id="contact-from" required type="email" autoComplete="email" maxLength={120} value={form.from} onChange={e=>setForm({...form,from:e.target.value.trim()})} placeholder="you@company.com"/>
            <label htmlFor="contact-company">company / org</label>
            <input id="contact-company" autoComplete="organization" maxLength={120} value={form.company} onChange={e=>setForm({...form,company:e.target.value})} placeholder="optional"/>
            <label htmlFor="contact-role">role</label>
            <select id="contact-role" value={form.role} onChange={e=>setForm({...form,role:e.target.value})}>
              <option>Recruiter</option>
              <option>Founder</option>
              <option>Engineer</option>
              <option>Friend / Curious</option>
              <option>Other</option>
            </select>
          </div>
          <div>
            <h3>SIGNAL</h3>
            <label htmlFor="contact-message">what should we build or solve?</label>
            <textarea id="contact-message" required maxLength={5000} value={form.message} onChange={e=>setForm({...form,message:e.target.value})} placeholder="what is the problem, role, or team?"/>

            {/* Honeypot — hidden bot trap. Real visitors never see or touch it. */}
            <input
              type="checkbox"
              name="botcheck"
              className="dispatch-botcheck"
              tabIndex={-1}
              autoComplete="off"
              aria-hidden="true"
              style={{ display: "none" }}
              checked={form.botcheck}
              onChange={e=>setForm({...form,botcheck:e.target.checked})}
            />

            {/* Routing readout — phase-aware, honest at every state. */}
            <div className={"dispatch-route-note drn-" + phase} aria-live="polite">
              <span className="drn-tag">
                {success ? "// DISPATCH RECEIVED" : errored ? "// DISPATCH FAILED" : "// ROUTING"}
              </span>
              <span className="drn-line">
                {success
                  ? "Dispatch received."
                  : errored
                  ? "Dispatch failed."
                  : <>Submit routes directly → <span className="drn-target">{inbox}</span></>}
              </span>
              <span className="drn-sub">
                {success
                  ? <>Route confirmed → <span className="drn-target">{inbox}</span>. Reply within 48h.</>
                  : errored
                  ? "Transmission did not reach the operator inbox."
                  : sending
                  ? "Routing transmission through the relay…"
                  : "Real delivery via Web3Forms. Reply within 48h."}
              </span>
              {errored && errorMsg && <span className="drn-err">relay: {errorMsg}</span>}
            </div>

            {/* Primary action — send · routing · received · retry. */}
            {success ? (
              <div className="ship-btn is-success" role="status">DISPATCH RECEIVED ✓</div>
            ) : (
              <button
                className={"ship-btn" + (sending ? " is-sending" : "") + (errored ? " is-retry" : "")}
                type="submit"
                disabled={sending}
              >
                {sending ? "ROUTING…" : errored ? "RETRY DISPATCH" : "SEND DISPATCH →"}
              </button>
            )}

            {/* Error-only fallback — explicit, click-only, never auto-fired. */}
            {errored && (
              <button type="button" className="ship-btn ship-btn-alt" onClick={openMailClient}>
                OPEN MAIL CLIENT
              </button>
            )}
          </div>
        </form>
      </div>
    </div>
  );
}

// ─────────── HIDDEN BAY (konami) ───────────
function HiddenBay({ onClose }) {
  return (
    <div className="panel open">
      <div className="panel-bg" onClick={onClose} />
      <div className="panel-card" style={{height:"min(520px,72vh)", width:"min(720px,86vw)"}}>
        <div className="panel-strip" style={{background:"repeating-linear-gradient(-45deg,#6DFF8C 0 16px, #0a0d10 16px 32px)"}}/>
        <div className="panel-head">
          <div className="panel-stencil" style={{color:"#6DFF8C"}}>??</div>
          <div className="panel-title">
            <div className="label" style={{color:"#6DFF8C"}}>BAY-XX · HIDDEN</div>
            <h1>YOU FOUND IT</h1>
            <div className="summary">↑ ↑ ↓ ↓ ← → ← → B A. Nice.</div>
          </div>
          <button className="panel-close" onClick={onClose} aria-label="close">×</button>
        </div>
        <div style={{padding:32, fontFamily:"var(--font-mono)", fontSize:13, lineHeight:1.7, color:"var(--steel-100)"}}>
          <p style={{color:"var(--terminal-green)", fontSize:14, marginBottom:14}}>// secret_bay.txt</p>
          <p>if you're reading this, you scrolled past the obvious.</p>
          <p>that's the kind of person i want to work with.</p>
          <p style={{margin:"14px 0", color:"var(--steel-300)"}}>·</p>
          <p>three things i actually believe:</p>
          <p style={{paddingLeft:14}}>1. shipping &gt; perfecting.</p>
          <p style={{paddingLeft:14}}>2. tools should disappear.</p>
          <p style={{paddingLeft:14}}>3. small teams beat big ones if the loop is tight.</p>
          <p style={{margin:"18px 0 6px", color:"var(--blueprint)"}}>// reward</p>
          <p>tell me you found the konami bay and i'll respond first.</p>
        </div>
      </div>
    </div>
  );
}

// ─────────── MINI MAP ───────────
// Top-down 2D map of the yard. Shows zones, containers, current camera focus.
// Click anywhere = fly camera there. Click a bay marker = focus.
function MiniMap({ scene, onFocusBay, nearest, openIndex, filter, viewedIds }) {
  const data = window.YARD_DATA;
  const zones = scene ? scene.getZones() : [];
  const [cam, setCam] = useState({ tx: 0, tz: 0, az: 0 });
  // Hover state — independent of scene `nearest` so visitors can scan the map
  // freely without forcing a camera move. The readout strip at the bottom of
  // the minimap mirrors whichever element is hovered (zone or bay).
  const [hoverZone, setHoverZone] = useState(null);
  const [hoverBay, setHoverBay] = useState(null);

  // poll camera info @ 30hz for crosshair
  useEffect(() => {
    if (!scene) return;
    let raf;
    const tick = () => {
      const info = scene.getCameraInfo();
      setCam({ tx: info.tx, tz: info.tz, az: info.azimuth });
      raf = requestAnimationFrame(tick);
    };
    tick();
    return () => cancelAnimationFrame(raf);
  }, [scene]);

  // Debounced readout — the minimap markers have small gaps between them, so
  // without a grace period the bottom strip would snap to the route-progress
  // fallback whenever the cursor crossed a gap. Holding the last readout
  // ~220ms keeps movement between markers reading as smooth and stable; only
  // a real, settled exit reverts the strip to route progress.
  const [shownHover, setShownHover] = useState(null);
  useEffect(() => {
    let live = null;
    if (hoverBay != null) {
      const c = data.containers[hoverBay];
      if (c) live = { kind: "BAY", label: c.label.replace("BAY-", "") + " · " + c.title, color: c.color, sub: c.identityTag || c.type };
    } else if (hoverZone) {
      const z = zones.find(zz => zz.id === hoverZone);
      if (z) live = { kind: "ZONE", label: z.title, color: z.color, sub: z.sub };
    }
    if (live) { setShownHover(live); return; }
    const t = setTimeout(() => setShownHover(null), 220);
    return () => clearTimeout(t);
  }, [hoverBay, hoverZone]);

  // World→map coordinate system
  // World x: -90..120 (origins to ops). Map width 240, padding 12.
  const W = 240, H = 130, padX = 12, padY = 16;
  const worldMinX = -90, worldMaxX = 120;
  const worldMinZ = -36, worldMaxZ = 36;
  const sx = (W - padX * 2) / (worldMaxX - worldMinX);
  const sz = (H - padY * 2) / (worldMaxZ - worldMinZ);
  const w2sx = (x) => padX + (x - worldMinX) * sx;
  const w2sz = (z) => padY + (z - worldMinZ) * sz;

  const onClickMap = (e) => {
    const rect = e.currentTarget.getBoundingClientRect();
    const px = e.clientX - rect.left;
    const py = e.clientY - rect.top;
    const wx = worldMinX + (px - padX) / sx;
    const wz = worldMinZ + (py - padY) / sz;
    if (scene) scene.flyTo(wx, wz);
  };

  // Filter dimming logic
  const matches = (c, i) => {
    if (!filter || filter === "all") return true;
    if (filter.startsWith("zone:")) {
      const zid = filter.slice(5);
      const z = zones.find(zz => zz.id === zid);
      return z && z.ids.includes(c.id);
    }
    if (filter.startsWith("type:")) {
      return c.type === filter.slice(5);
    }
    return true;
  };

  return (
    <div className="minimap" onWheel={(e) => e.stopPropagation()}>
      <div className="minimap-header">
        <span className="mm-title">YARD MAP</span>
        <span className="mm-coords">[{Math.round(cam.tx)}, {Math.round(cam.tz)}]</span>
      </div>
      <svg
        className="minimap-svg"
        viewBox={`0 0 ${W} ${H}`}
        onClick={onClickMap}
        style={{ width: W, height: H }}
      >
        {/* base */}
        <rect x="0" y="0" width={W} height={H} fill="#0e1620" />
        <rect x="2" y="2" width={W-4} height={H-4} fill="none" stroke="#3d5670" strokeWidth="1" />

        {/* central road */}
        <rect
          x={w2sx(-105)} y={w2sz(-4)}
          width={w2sx(120) - w2sx(-105)} height={w2sz(4) - w2sz(-4)}
          fill="#1a2332"
        />

        {/* zone pads */}
        {zones.map(z => {
          const ids = z.ids;
          let minX = Infinity, maxX = -Infinity, minZ = Infinity, maxZ = -Infinity;
          ids.forEach((id, idx) => {
            const layout = ({
              origins: [[0,-10],[0,0],[0,10]],
              build:   [[-10,-9],[10,-9],[-10,9],[10,9]],
              now:     [[0,0]],
              ops:     [[0,-7.5],[0,7.5]],
            })[z.id];
            const [dx, dz] = layout[idx] || [0,0];
            const x = z.anchor.x + dx, zz = z.anchor.z + dz;
            minX = Math.min(minX, x - 6);
            maxX = Math.max(maxX, x + 6);
            minZ = Math.min(minZ, zz - 2.5);
            maxZ = Math.max(maxZ, zz + 2.5);
          });
          const dim = filter && filter.startsWith("zone:") && filter.slice(5) !== z.id;
          const isHovered = hoverZone === z.id;
          return (
            <g
              key={z.id}
              style={{ opacity: dim ? 0.25 : 1, cursor: dim ? "default" : "pointer", pointerEvents: dim ? "none" : "auto" }}
              onMouseEnter={() => setHoverZone(z.id)}
              onMouseLeave={() => setHoverZone(prev => prev === z.id ? null : prev)}
            >
              <rect
                x={w2sx(minX-3)} y={w2sz(minZ-3)}
                width={w2sx(maxX+3) - w2sx(minX-3)}
                height={w2sz(maxZ+3) - w2sz(minZ-3)}
                fill={z.color}
                fillOpacity={isHovered ? 0.28 : 0.12}
                stroke={z.color}
                strokeWidth={isHovered ? 1.6 : 1}
              />
              <text
                x={w2sx(z.anchor.x)}
                y={w2sz(minZ-3) - 2}
                fill={z.color}
                fontSize={isHovered ? 7.4 : 6.5}
                fontFamily="Anton, Impact, sans-serif"
                textAnchor="middle"
              >
                {z.title}
              </text>
            </g>
          );
        })}

        {/* recommended route — glowing polyline through the operator's
            preferred path. Visited segments turn green, the next segment
            pulses amber, future segments stay dim. The route lives in the
            minimap because that's where path-thinking is contextual; the
            yard itself stays uncluttered. */}
        {(() => {
          const routeIds = ["intro", "mygui", "cortex", "potentia", "pods", "contact"];
          const pts = routeIds.map(id => {
            const idx = data.containers.findIndex(c => c.id === id);
            if (idx < 0 || !scene) return null;
            const p = scene.getContainerPos(idx);
            if (!p) return null;
            return { idx, id, x: w2sx(p.x), y: w2sz(p.z) };
          }).filter(Boolean);
          if (!pts.length) return null;
          const visited = viewedIds instanceof Set ? viewedIds : new Set(viewedIds || []);
          // Find the first unvisited route stop — that's the "next" highlight.
          const nextK = pts.findIndex(p => !visited.has(p.id));
          return (
            <g aria-hidden="true" style={{ pointerEvents: "none" }}>
              {pts.slice(0, -1).map((p, k) => {
                const q = pts[k + 1];
                const segDone = visited.has(p.id) && visited.has(q.id);
                const segNext = k === Math.max(0, nextK - 1) && !segDone;
                const color = segDone ? "#6DFF8C" : segNext ? "#FFB627" : "#3d5670";
                const dash = segNext ? "2 2" : segDone ? undefined : "1 2";
                return (
                  <line
                    key={p.id + q.id}
                    x1={p.x} y1={p.y} x2={q.x} y2={q.y}
                    stroke={color}
                    strokeWidth={segDone ? 1.2 : segNext ? 1.3 : 0.8}
                    strokeDasharray={dash}
                    opacity={segDone ? 0.7 : segNext ? 0.78 : 0.35}
                  >
                    {segNext && (
                      <animate attributeName="stroke-dashoffset" from="0" to="-8" dur="2.4s" repeatCount="indefinite" />
                    )}
                  </line>
                );
              })}
              {/* step numbers — small chips above each route stop. Calmer
                  pulse on the next stop (slower + tighter radius range) so
                  the route guides without competing with the world. */}
              {pts.map((p, k) => {
                const isNext = k === nextK;
                const isDone = visited.has(p.id);
                const fill = isDone ? "#6DFF8C" : isNext ? "#FFB627" : "#5b7185";
                return (
                  <g key={"step-" + p.id}>
                    <circle
                      cx={p.x} cy={p.y - 5.6}
                      r="2.4"
                      fill={fill}
                      opacity={isDone ? 0.9 : isNext ? 0.95 : 0.55}
                    >
                      {isNext && (
                        <animate attributeName="r" values="2.4;2.9;2.4" dur="2.6s" repeatCount="indefinite" />
                      )}
                    </circle>
                    <text
                      x={p.x} y={p.y - 4.9}
                      fontSize="2.6"
                      fontFamily="JetBrains Mono, monospace"
                      fontWeight="bold"
                      textAnchor="middle"
                      fill="#0a0d10"
                    >{k + 1}</text>
                  </g>
                );
              })}
            </g>
          );
        })()}

        {/* containers */}
        {data.containers.map((c, i) => {
          const pos = scene ? scene.getContainerPos(i) : null;
          if (!pos) return null;
          const m = matches(c, i);
          const isOpen = i === openIndex;
          const isHover = i === nearest;
          const isVisited = viewedIds && viewedIds.has && viewedIds.has(c.id);
          const isMmHover = hoverBay === i;
          return (
            <g key={c.id}
               style={{ cursor: "pointer", opacity: m ? 1 : 0.18 }}
               onClick={(e) => { e.stopPropagation(); onFocusBay(i); }}
               onMouseEnter={(e) => { e.stopPropagation(); setHoverBay(i); }}
               onMouseLeave={() => setHoverBay(prev => prev === i ? null : prev)}
            >
              <rect
                x={w2sx(pos.x) - 5} y={w2sz(pos.z) - 2.5}
                width="10" height="5"
                fill={c.color}
                stroke={isOpen ? "#fff" : isMmHover || isHover ? c.color : "#0a0d10"}
                strokeWidth={isOpen ? 1.5 : isMmHover || isHover ? 1.5 : 0.5}
              />
              {isMmHover && (
                <rect
                  x={w2sx(pos.x) - 6} y={w2sz(pos.z) - 3.5}
                  width="12" height="7"
                  fill="none"
                  stroke="#FFB627"
                  strokeWidth="0.6"
                  opacity="0.75"
                />
              )}
              <text
                x={w2sx(pos.x)} y={w2sz(pos.z) + 1.5}
                fontSize="3.5"
                fontFamily="JetBrains Mono, monospace"
                fontWeight="bold"
                textAnchor="middle"
                fill="rgba(255,255,255,.9)"
              >{c.label.replace("BAY-","")}</text>
              {isVisited && !isOpen && (
                <circle
                  cx={w2sx(pos.x) + 5}
                  cy={w2sz(pos.z) - 2.5}
                  r="1.4"
                  fill="#6DFF8C"
                  opacity="0.85"
                />
              )}
            </g>
          );
        })}

        {/* camera position crosshair */}
        <g transform={`translate(${w2sx(cam.tx)}, ${w2sz(cam.tz)})`} style={{ pointerEvents: "none" }}>
          <circle r="2" fill="#FFB627" />
          <circle r="6" fill="none" stroke="#FFB627" strokeWidth="0.6" strokeDasharray="2 1" opacity="0.6" />
          <line x1="0" y1="0"
            x2={Math.sin(cam.az) * 9} y2={Math.cos(cam.az) * 9}
            stroke="#FFB627" strokeWidth="1" opacity="0.7" />
        </g>
      </svg>
      <div className="minimap-status">
        {shownHover ? (
          <div className="minimap-readout" style={{ "--mr-color": shownHover.color }} aria-live="polite">
            <span className="mmr-dot" />
            <span className="mmr-k">{shownHover.kind}</span>
            <span className="mmr-v">{shownHover.label}</span>
            {shownHover.sub && <span className="mmr-sub">{shownHover.sub}</span>}
          </div>
        ) : (() => {
          const routeIds = ["intro", "mygui", "cortex", "potentia", "pods", "contact"];
          const visited = viewedIds instanceof Set ? viewedIds : new Set(viewedIds || []);
          const done = routeIds.filter(id => visited.has(id)).length;
          const pct = Math.round((done / routeIds.length) * 100);
          return (
            <div className="minimap-route">
              <div className="mr-row">
                <span className="mr-k">RECOMMENDED ROUTE</span>
                <span className="mr-v">{done}/{routeIds.length}</span>
              </div>
              <div className="mr-track" aria-hidden="true">
                <div className="mr-fill" style={{ width: pct + "%" }} />
              </div>
            </div>
          );
        })()}
      </div>
      <div className="minimap-foot">click to fly · click a bay to focus</div>
    </div>
  );
}

// ─────────── FILTER BAR ───────────
function FilterBar({ filter, setFilter, scene }) {
  const zones = scene ? scene.getZones() : [];
  const filters = [
    { id: "all", label: "ALL", color: "#cdd5e0" },
    ...zones.map(z => ({ id: "zone:" + z.id, label: z.title, color: z.color })),
    { id: "type:project", label: "PROJECTS", color: "#5BA8D9" },
    { id: "type:chapter", label: "CHAPTERS", color: "#FFB627" },
  ];
  const isActive = filter && filter !== "all";
  return (
    <div className={"filter-bar" + (isActive ? " is-active" : "")}>
      <span className="fb-label">SHOWCASE</span>
      {filters.map(f => (
        <button
          key={f.id}
          className={"fb-pill" + (filter === f.id ? " active" : "")}
          style={{ "--pill-color": f.color }}
          aria-label={f.id === "all" ? "Reset showcase" : `Showcase ${f.label}`}
          onClick={() => setFilter(f.id)}
        >{f.label}</button>
      ))}
    </div>
  );
}

// ─────────── BOT PROMPT ───────────
// World-anchored speech bubble that appears once the dispatch bot has rolled
// into position. Tracks the bot's projected screen position so it reads as the
// bot speaking. Holds back until the bot is parked AND a small post-arrival
// pause has elapsed — gives the visitor a beat to notice the bot itself first.
function BotPrompt({ scene, onOpen }) {
  const [visible, setVisible] = useState(false);
  const [dismissed, setDismissed] = useState(false);
  const [clicked, setClicked] = useState(false);
  const [pos, setPos] = useState(null);

  // Wait for the bot to reach idle, then a short beat, then reveal the bubble.
  // Polls cheaply on a timeout instead of an rAF until the bot is ready.
  useEffect(() => {
    if (!scene || !scene.isBotReady) return;
    let revealTimer = null;
    const interval = setInterval(() => {
      if (scene.isBotReady()) {
        clearInterval(interval);
        // Short beat after park so the visitor registers the bot itself
        // before the bubble pops — kept tight enough to feel responsive.
        revealTimer = setTimeout(() => setVisible(true), 550);
      }
    }, 300);
    return () => {
      clearInterval(interval);
      if (revealTimer) clearTimeout(revealTimer);
    };
  }, [scene]);

  // Once visible, follow the bot's screen position each frame.
  useEffect(() => {
    if (!visible || !scene || !scene.getBotScreenPos || dismissed) return;
    let raf;
    const tick = () => {
      const p = scene.getBotScreenPos();
      if (p) setPos(p);
      raf = requestAnimationFrame(tick);
    };
    tick();
    return () => cancelAnimationFrame(raf);
  }, [visible, scene, dismissed]);

  const handleOpen = useCallback(() => {
    setClicked(true);
    onOpen && onOpen();
    // Brief flash before the focus camera carries the user to BAY-11.
    setTimeout(() => setClicked(false), 280);
  }, [onOpen]);

  if (!visible || dismissed || !pos || !pos.visible) return null;

  // Clamp so the bubble never falls off-screen and never crowds the mobile
  // filter bar (top:100px on small screens). Bubble translates -100% so
  // top must leave room above for the bubble height (~140px).
  const isNarrow = window.innerWidth <= 720;
  const minTop = isNarrow ? 200 : 130;
  const minLeft = isNarrow ? 120 : 150;
  const maxRight = isNarrow ? 24 : 30;
  const left = Math.max(minLeft, Math.min(window.innerWidth - maxRight, pos.x));
  const top  = Math.max(minTop,  Math.min(window.innerHeight - 110, pos.y));

  return (
    <div
      className={"bot-prompt" + (clicked ? " clicked" : "")}
      style={{ left, top }}
    >
      <button
        type="button"
        className="bp-cta-btn"
        onClick={handleOpen}
        aria-label="Open BAY-11 dispatch"
      >
        <div className="bp-tag"><span className="bp-pulse" /> DISPATCH LINE OPEN</div>
        <div className="bp-line">manifest ready.</div>
        <div className="bp-sub">awaiting operator input.</div>
        <div className="bp-cta">route to BAY-11 <span>→</span></div>
      </button>
      <button
        type="button"
        className="bp-dismiss"
        aria-label="dismiss prompt"
        onClick={(e) => { e.stopPropagation(); setDismissed(true); }}
      >×</button>
    </div>
  );
}

// (S21 retired OperatorBriefing in favor of MissionBriefing.
//  S23 removed the dead component entirely.)

// ─────────── QUICK ACTIONS (top-right cluster) ───────────
// Three-button cluster: Projects / Resume / Dispatch. Sits below the live HUD
// meta row so it does not crowd the operator status. Hidden in focus mode so
// it never overlaps the open container panel.
function QuickActions({ onResume, onTour, onContact, mode }) {
  if (mode === "focus") return null;
  const routes = (window.YARD_DATA && window.YARD_DATA.identity && window.YARD_DATA.identity.routes) || {};
  const linkedinHref = routes.linkedin && routes.linkedin.href;
  // One unified top-right action row — tour, resume, LinkedIn, then a
  // visually-dominant DISPATCH primary, all on a single baseline. (LIVE /
  // OVERVIEW / BAY-XX / AMBIENCE were removed earlier; ambience lives in the
  // controls dock, GitHub in the dispatch direct-routes.)
  return (
    <div className="quick-actions" role="navigation" aria-label="Operator actions">
      {onTour && (
        <button type="button" className="qa-btn qa-tour" onClick={onTour} title="Start guided walkthrough">
          <svg className="qa-icon" viewBox="0 0 24 24" width="12" height="12" fill="none" stroke="currentColor" strokeWidth="2" aria-hidden="true">
            <path d="M3 6l9-3 9 3-9 3-9-3z" />
            <path d="M3 12l9 3 9-3M3 18l9 3 9-3" />
          </svg>
          <span className="qa-k">START TOUR</span>
        </button>
      )}
      <button type="button" className="qa-btn" onClick={onResume} title="Open resume">
        <svg className="qa-icon" viewBox="0 0 24 24" width="12" height="12" fill="none" stroke="currentColor" strokeWidth="2" aria-hidden="true">
          <path d="M14 3H7a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h10a2 2 0 0 0 2-2V8z" />
          <path d="M14 3v5h5" />
        </svg>
        <span className="qa-k">RESUME</span>
      </button>
      {linkedinHref && (
        <a className="qa-btn" href={linkedinHref} target="_blank" rel="noreferrer" title="LinkedIn · anthonyabdulnour">
          <svg className="qa-icon" viewBox="0 0 24 24" width="12" height="12" fill="currentColor" aria-hidden="true">
            <path d="M4.98 3.5C4.98 4.88 3.87 6 2.5 6S0 4.88 0 3.5 1.12 1 2.5 1s2.48 1.12 2.48 2.5zM.22 8h4.56v14H.22V8zM8 8h4.36v1.92h.06c.61-1.16 2.1-2.38 4.32-2.38 4.62 0 5.47 3.04 5.47 6.99V22h-4.55v-6.34c0-1.51-.03-3.45-2.1-3.45-2.1 0-2.42 1.64-2.42 3.34V22H8V8z" />
          </svg>
          <span className="qa-k">LINKEDIN</span>
        </a>
      )}
      {onContact && (
        <button type="button" className="qa-dispatch" onClick={onContact} title="Open BAY-11 · DISPATCH — send a message">
          <span className="qad-arrow" aria-hidden="true">→</span>
          <span className="qad-k">DISPATCH LINE</span>
          <span className="qad-bay" aria-hidden="true">BAY-11</span>
        </button>
      )}
    </div>
  );
}

// ─────────── SIGNATURE CTA (idle dispatch hail) ───────────
// Lightweight, in-world prompt that appears once after the visitor has been
// browsing for a while. Tone matches the yard ("dispatch is open"). Shows at
// most once per session — dismissal persists in localStorage.
function SignatureCTA({ onResume, onDispatch, onClose, variant }) {
  const [closing, setClosing] = useState(false);
  // Brief mount delay so the card lands as a deliberate beat after the
  // user has been browsing — never a sudden popup.
  const [shown, setShown] = useState(false);
  useEffect(() => {
    const t = setTimeout(() => setShown(true), 180);
    return () => clearTimeout(t);
  }, []);
  const isCompletion = variant === "completion";
  // Completion shows a quiet vignette behind the card so the moment reads as
  // "this is the time to act." Conversion variant keeps the existing tone —
  // no backdrop dim, just the card.
  const close = useCallback(() => {
    setClosing(true);
    try {
      const key = isCompletion ? "yard.completion.dismissed" : "yard.signature.dismissed";
      localStorage.setItem(key, "1");
    } catch (_) {}
    setTimeout(() => onClose && onClose(), 240);
  }, [onClose, isCompletion]);
  const handle = (fn) => () => { if (fn) fn(); close(); };
  // Completion lands after every project has been opened — a different beat
  // from the normal conversion CTA so the "you've finished the tour" moment
  // reads distinct from the routine prompt.
  const tag = isCompletion ? "// END OF MANIFEST" : "DISPATCH · LINE OPEN";
  const line = isCompletion ? "You've seen the yard." : "Want to work together?";
  const sub = isCompletion ? "Let's build something real." : "One way to reach Anthony. A message or the résumé.";
  return (
    <div
      className={"signature-cta" + (shown ? " shown" : "") + (closing ? " closing" : "") + (isCompletion ? " is-completion" : "")}
      role="dialog"
      aria-label={isCompletion ? "Tour complete" : "Dispatch hail"}
    >
      {isCompletion && <div className="sc-vignette" aria-hidden="true" />}
      <div className="sc-card">
        <div className="sc-tag"><span className="sc-pulse" /> {tag}</div>
        <div className="sc-line">{line}</div>
        <div className="sc-sub">{sub}</div>
        <div className="sc-actions">
          <button type="button" className="sc-btn primary" onClick={handle(onDispatch)}>DISPATCH →</button>
          <button type="button" className="sc-btn" onClick={handle(onResume)}>RÉSUMÉ</button>
        </div>
        <button type="button" className="sc-x" aria-label="dismiss" onClick={close}>×</button>
      </div>
    </div>
  );
}

// ─────────── FILTER TOAST ───────────
// Brief in-world acknowledgement when a filter fires. Confirms the operation
// in plain language ("Showing: BUILD projects") so picking a filter feels
// like a system response, not just UI state.
function FilterToast({ filter, scene }) {
  const [text, setText] = useState(null);
  const tickRef = useRef(0);

  useEffect(() => {
    if (!filter || filter === "all") {
      setText(null);
      return;
    }
    let label = null;
    if (filter.startsWith("zone:")) {
      const zid = filter.slice(5);
      const zones = scene && scene.getZones ? scene.getZones() : [];
      const z = zones.find(zz => zz.id === zid);
      if (z) label = `Showing: ${z.title} bays`;
    } else if (filter.startsWith("type:")) {
      const t = filter.slice(5);
      const map = { project: "PROJECTS", chapter: "CHAPTERS", contact: "DISPATCH" };
      label = `Showing: ${map[t] || t.toUpperCase()}`;
    }
    if (!label) return;
    setText(label);
    const tick = ++tickRef.current;
    const t = setTimeout(() => {
      if (tick === tickRef.current) setText(null);
    }, 2400);
    return () => clearTimeout(t);
  }, [filter, scene]);

  if (!text) return null;
  return (
    <div className="filter-toast" role="status" aria-live="polite">
      <span className="ft-pulse" />
      <span className="ft-text">{text}</span>
    </div>
  );
}

// ─────────── GUIDED WALKTHROUGH ───────────
// A 5-step on-rails tour. Each step has a copy block and an enter() hook that
// moves the camera (or opens a panel). Visitor can Next / Back / Skip; the
// final step opens BAY-11 directly. Designed to make the portfolio legible to
// a stranger who clicks a single button.
// `move` is the explicit movement type for each step, so positioning never
// accidentally triggers a container dive:
//   overview — settle the camera to home (no bay opens)
//   focus    — pan/frame a zone (camera only, no dive)
//   open     — open a bay; reserved for the final DISPATCH step
const TOUR_STEPS = [
  {
    id: "intro",
    move: "overview",
    tag: "01 · OPERATOR",
    title: "WHO I AM",
    body: "Anthony Abdulnour. Software developer working across AI tooling, web platforms, and iOS. Currently finishing CS at Guelph and joining PODS as an IT intern in Summer 2026.",
    narration: "// operator profile",
  },
  {
    id: "origins",
    move: "focus",
    tag: "02 · ORIGINS",
    title: "WHERE IT STARTED",
    body: "CS at Guelph for the fundamentals. On the side, building the campus chapter of Startup Ecosystem Canada: founder events, speakers, partners, and the team that runs it.",
    narration: "// origins zone",
    zone: "origins",
    pulseIds: ["guelph", "sec"],
  },
  {
    id: "build",
    move: "focus",
    tag: "03 · BUILD",
    title: "WHAT I SHIP",
    body: "MyGUI: co-founded 24/7 multilingual voice assistant for SMEs and higher-ed. Cortex: solo AI knowledge OS. YUTH: Hack Canada finalist. Potentia: iOS Life Score app on the App Store. ScholarScope + AlgoViz: deployed utilities.",
    narration: "// build floor",
    zone: "build",
    pulseIds: ["mygui", "cortex", "potentia", "yuth", "scholarscope", "algoviz"],
  },
  {
    id: "now",
    move: "focus",
    tag: "04 · NOW & NEXT",
    title: "WHAT IS NEXT",
    body: "Joining PODS Summer 2026 as an IT Intern. The yard metaphor isn't a costume; software tied to physical operations is the kind of system I want to build well.",
    narration: "// dispatch lane",
    zone: "now",
    pulseIds: ["pods"],
  },
  {
    id: "contact",
    move: "open",
    tag: "05 · DISPATCH",
    title: "GET IN TOUCH",
    body: "Résumé, email, LinkedIn, GitHub, all open, no gate. The dossier button below gives you the full operator file; the resume button opens the PDF.",
    narration: "// dispatch line open",
    open: "contact",
  },
];

function TourMode({ scene, onResume, onDossier, onClose, onStepChange }) {
  const [step, setStep] = useState(0);
  const [closing, setClosing] = useState(false);
  // Timestamp of the last accepted step change — see navAllowed().
  const navLockRef = useRef(0);
  const total = TOUR_STEPS.length;
  const current = TOUR_STEPS[step];
  // Notify host of step changes so it can light up signature moments
  // (BuildReveal toast, FinalMoment vignette) without owning tour state.
  useEffect(() => {
    if (onStepChange && current) onStepChange(current.id, step);
  }, [step, current, onStepChange]);

  // Run the step's side effect once per step. Each beat coordinates a camera
  // move, a zone-light activation, and a sequential pulse over the projects
  // that matter at this step — so the tour reads as a directed beat rather
  // than a tooltip + camera shove. Operator can advance early; the cues are
  // fire-and-forget.
  useEffect(() => {
    if (!scene || !current) return;
    // Every timer scheduled below is collected here so the cleanup clears
    // ALL of them. A single reused `pulseTimer` var was leaking the earlier
    // pulse timers into the next step — stale "wrong-feel" pulses firing a
    // step late. Clearing the whole array on step change / unmount kills that.
    const timers = [];
    try {
      // Movement is driven by the step's explicit `move` type so a focus or
      // overview step can never trip a container dive — only the final
      // "open" step opens BAY-11; "focus" pans a zone; "overview" settles home.
      if (current.move === "open" && current.open && scene.openContact) {
        scene.openContact();
      } else if (current.move === "focus" && current.zone && scene.focusZone) {
        scene.focusZone(current.zone);
      } else if (scene.resetView && scene.getMode && scene.getMode() === "overview") {
        scene.resetView();
      }
      if (current.zone && scene.flickerZone) scene.flickerZone(current.zone);
      // Sequential container pulses — staggered so the eye reads them as a
      // sweep across the bays at this step. ~280ms gap between pulses.
      const ids = Array.isArray(current.pulseIds) ? current.pulseIds : [];
      const containers = (window.YARD_DATA && window.YARD_DATA.containers) || [];
      ids.forEach((id, i) => {
        const idx = containers.findIndex(c => c.id === id);
        if (idx < 0) return;
        timers.push(setTimeout(() => {
          if (scene.pulseContainer) scene.pulseContainer(idx);
        }, 240 + i * 320));
      });
      // S23 signature shot — only at the BUILD step. Fires AFTER the
      // sequential pulses so the moment reads as a payoff: each bay lights,
      // then all four lift in unison. Single beat. No other step gets it.
      if (current.id === "build" && scene.playBuildReveal) {
        const after = 240 + ids.length * 320 + 220;
        timers.push(setTimeout(() => scene.playBuildReveal(), after));
      }
    } catch (_) { /* tour should never crash the page */ }
    return () => { timers.forEach(t => clearTimeout(t)); };
  }, [step, scene, current]);

  const close = useCallback(() => {
    if (closing) return;
    setClosing(true);
    try { localStorage.setItem("yard.tour.seen", "1"); } catch (_) {}
    // Fade the card out before unmounting so Finish / Skip reads as a settle,
    // not an abrupt cut. The camera and the final step's context (the open
    // DISPATCH bay) are deliberately left exactly as the tour left them.
    setTimeout(() => { if (onClose) onClose(); }, 260);
  }, [closing, onClose]);

  // Small guard so a stray double-click or key-repeat can't skip a step or
  // double-fire the per-step movement effect: at most one step change per
  // ~240ms (a single camera-pan beat), and none at all while closing.
  const navAllowed = () => {
    const now = Date.now();
    if (closing || now - navLockRef.current < 240) return false;
    navLockRef.current = now;
    return true;
  };
  const next = () => {
    if (!navAllowed()) return;
    if (step >= total - 1) { close(); return; }
    setStep(s => s + 1);
  };
  const back = () => {
    if (!navAllowed()) return;
    if (step > 0) setStep(s => s - 1);
  };

  // Keyboard support: ←/→/Esc. Avoids hijacking when focus is in a form input.
  useEffect(() => {
    const onKey = (e) => {
      const t = document.activeElement;
      if (t && (t.tagName === "INPUT" || t.tagName === "TEXTAREA")) return;
      if (e.key === "ArrowRight") { e.preventDefault(); next(); }
      else if (e.key === "ArrowLeft") { e.preventDefault(); back(); }
      else if (e.key === "Escape") { e.preventDefault(); close(); }
    };
    window.addEventListener("keydown", onKey);
    return () => window.removeEventListener("keydown", onKey);
  });

  if (!current) return null;
  const isFinal = step === total - 1;
  return (
    <div className={"tour" + (closing ? " is-closing" : "")} role="dialog" aria-label="Guided walkthrough">
      <div className="tour-vignette" aria-hidden="true" />
      <div className="tour-card">
        <div className="tour-head">
          <span className="tour-tag">{current.tag}</span>
          <span className="tour-progress">
            <span className="tour-pos">{String(step + 1).padStart(2, "0")}</span>
            <span className="tour-sep">/</span>
            <span className="tour-total">{String(total).padStart(2, "0")}</span>
          </span>
          <button type="button" className="tour-x" aria-label="Skip tour" onClick={close}>×</button>
        </div>
        {current.narration && (
          <div className="tour-narration"><span className="tour-pulse" />{current.narration}</div>
        )}
        <h2 className="tour-title">{current.title}</h2>
        <p className="tour-body">{current.body}</p>
        <div className="tour-rail" aria-hidden="true">
          {TOUR_STEPS.map((s, i) => (
            <span key={s.id} className={"tour-pip" + (i === step ? " is-active" : i < step ? " is-done" : "")} />
          ))}
        </div>
        <div className="tour-actions">
          <button
            type="button"
            className="tour-btn tour-back"
            onClick={back}
            disabled={step === 0}
            aria-label="Previous step"
          >‹ Back</button>
          <button
            type="button"
            className="tour-btn tour-skip"
            onClick={close}
            aria-label="Skip the tour"
          >Skip tour</button>
          {isFinal && onDossier && (
            <button
              type="button"
              className="tour-btn tour-dossier"
              onClick={onDossier}
              aria-label="View operator dossier"
            >Operator dossier ↗</button>
          )}
          {isFinal && onResume && (
            <button
              type="button"
              className="tour-btn tour-resume"
              onClick={onResume}
              aria-label="Retrieve operator file PDF"
            >Operator file ↗</button>
          )}
          <button
            type="button"
            className="tour-btn tour-next primary"
            onClick={next}
            aria-label={isFinal ? "Finish tour" : "Next step"}
          >{isFinal ? "Finish ✓" : "Next ›"}</button>
        </div>
      </div>
    </div>
  );
}

// ─────────── MISSION BRIEFING ───────────
// Premium cinematic onboarding shown on first visit (and re-summonable from
// the QuickAccessDock). Replaces the previous OperatorBriefing modal feel
// with a full-screen "mission control" panel: ambient backdrop, large
// typography, three intentional actions. Dismisses with a soft camera-settle.
function MissionBriefing({ identity, onTour, onExplore, onResume, onDossier }) {
  const [closing, setClosing] = useState(false);
  const dismiss = useCallback((handler) => () => {
    setClosing(true);
    // Session-only dismissal — no localStorage. The briefing returns on every
    // fresh load, so first-time visitors always get oriented; closing it just
    // hides it for this page session.
    setTimeout(() => { if (handler) handler(); }, 240);
  }, []);
  // Esc closes (without firing a primary action).
  useEffect(() => {
    const onKey = (e) => {
      if (e.key === "Escape") dismiss(onExplore)();
    };
    window.addEventListener("keydown", onKey);
    return () => window.removeEventListener("keydown", onKey);
  });
  return (
    <div className={"mission-briefing" + (closing ? " is-closing" : "")} role="dialog" aria-label="Welcome to The Yard">
      <div className="mb-stage" aria-hidden="true">
        <div className="mb-stage-grid" />
        <div className="mb-stage-glow" />
        <div className="mb-stage-rail" />
      </div>
      <div className="mb-card">
        <div className="mb-meta">
          <span className="mb-tag"><span className="mb-pulse" /> MISSION CONTROL</span>
          <span className="mb-id">{identity ? identity.name : "OPERATOR-001"}</span>
        </div>
        <h1 className="mb-title">WELCOME TO THE YARD</h1>
        <p className="mb-sub">
          Anthony Abdulnour&rsquo;s portfolio &mdash; an interactive yard you walk
          through. Every container is a project, role, or chapter; open the ones
          that interest you.
        </p>
        <p className="mb-sub mb-sub-quiet">
          The guided walkthrough is the fastest way through if you&rsquo;re short on time.
        </p>
        <div className="mb-actions">
          {/* PRIMARY — the intended first experience. Visually dominant,
              auto-focused, sequenced so the eye lands here first. */}
          <button
            type="button"
            className="mb-btn primary mb-btn-primary"
            onClick={dismiss(onTour)}
            autoFocus
            aria-label="Start guided walkthrough"
          >
            <span className="mb-btn-hint">RECOMMENDED · 5 STEPS</span>
            <span className="mb-btn-row">
              <span className="mb-btn-k">START GUIDED WALKTHROUGH</span>
              <span className="mb-btn-arrow">→</span>
            </span>
          </button>
          {/* SECONDARY — equal-weight alternative for self-directed visitors. */}
          <button
            type="button"
            className="mb-btn secondary mb-btn-secondary"
            onClick={dismiss(onExplore)}
            aria-label="Explore freely"
          >
            <span className="mb-btn-k">EXPLORE FREELY</span>
          </button>
          {/* TERTIARY — direct paths for visitors who just want the
              facts. Compact row, quieter typography. */}
          <div className="mb-tertiary">
            {onDossier && (
              <button
                type="button"
                className="mb-link"
                onClick={dismiss(onDossier)}
                aria-label="View operator dossier"
              >OPERATOR DOSSIER</button>
            )}
            {onDossier && onResume && <span className="mb-link-sep" aria-hidden="true">·</span>}
            {onResume && (
              <button
                type="button"
                className="mb-link"
                onClick={dismiss(onResume)}
                aria-label="Retrieve operator file PDF"
              >OPERATOR FILE ↗</button>
            )}
          </div>
        </div>
        <p className="mb-foot">Desktop recommended for full experience · ESC to skip</p>
      </div>
    </div>
  );
}

// ─────────── RESUME ACCESS BUTTON ───────────
// Wraps the resume open action with a brief archival access beat so the
// download lands as "operator file retrieved" rather than a generic PDF
// click. The ~700ms delay before the new tab opens is deliberate: long
// enough to register the stamp, short enough not to feel slow.
function ResumeAccessButton({ onResume, variant = "primary", label }) {
  const [phase, setPhase] = useState("idle"); // idle | accessing
  const onClick = useCallback(() => {
    if (phase === "accessing") return;
    setPhase("accessing");
    window.setTimeout(() => {
      try { onResume && onResume(); } catch (_) {}
      window.setTimeout(() => setPhase("idle"), 600);
    }, 720);
  }, [phase, onResume]);
  return (
    <button
      type="button"
      className={"dossier-btn resume-access-btn " + variant + (phase === "accessing" ? " is-accessing" : "")}
      onClick={onClick}
      aria-label={phase === "accessing" ? "Retrieving operator file" : "Retrieve operator file PDF"}
    >
      {phase === "accessing" ? (
        <>
          <span className="db-k">
            <span className="rab-pulse" aria-hidden="true" />
            RETRIEVING OPERATOR FILE
          </span>
          <span className="db-arrow rab-arrow">···</span>
        </>
      ) : (
        <>
          <span className="db-k">{label || "RETRIEVE OPERATOR FILE"}</span>
          <span className="db-arrow">↗</span>
        </>
      )}
    </button>
  );
}

// ─────────── OPERATOR DOSSIER ───────────
// Stylized resume preview surfaced from MissionBriefing / QuickAccessDock /
// tour finale. The point: don't dump a PDF link — frame Anthony as an
// "operator" with mission, deployments, systems, skills, and projects. Then
// give the user the choice: keep reading here, jump to the PDF, or open a
// project deep-dive.
function OperatorDossier({ onClose, onResume, onProject, onContact }) {
  const data = window.YARD_DATA || {};
  const identity = data.identity || {};
  const op = identity.operator || {};
  const projects = (data.containers || []).filter(c => c.type === "project").slice(0, 6);
  // Esc + backdrop close.
  useEffect(() => {
    const onKey = (e) => { if (e.key === "Escape") onClose && onClose(); };
    window.addEventListener("keydown", onKey);
    return () => window.removeEventListener("keydown", onKey);
  }, [onClose]);
  return (
    <div className="panel panel-dock open dossier-panel" role="dialog" aria-label="Operator dossier">
      <div className="panel-bg" onClick={onClose} />
      <div className="panel-card dossier-card">
        <div className="panel-strip" />
        <button type="button" className="panel-close cp-close-fixed" onClick={onClose} aria-label="close">×</button>
        <div className="dossier-scroll">
          <header className="dossier-hero">
            <div className="dossier-tag">
              <span className="op-dot" />
              <span className="dt-label">OPERATOR FILE</span>
              <span className="dt-sep">·</span>
              <span className="dt-class">CLASSIFICATION: PUBLIC</span>
              <span className="dt-sep">·</span>
              <span className="dt-id">FILE-ANT-001</span>
            </div>
            <div className="dossier-row">
              <div className="dossier-portrait-wrap">
                {identity.headshot && (
                  <div className="dossier-portrait" aria-hidden="true">
                    <SafeImage
                      className="dossier-portrait-img"
                      src={identity.headshot}
                      alt={(identity.name || "Operator") + " headshot"}
                    />
                    <span className="dossier-portrait-frame" />
                  </div>
                )}
                <div className="dossier-name">
                  <div className="dossier-callsign">{op.callsign || "OPERATOR-001"}</div>
                  <h1 className="dossier-h1">{identity.name || "ANTHONY ABDULNOUR"}</h1>
                  <div className="dossier-role">{identity.role || "Software Developer"} · {identity.sub || ""}</div>
                </div>
              </div>
              <div className="dossier-mission">
                <span className="dossier-mission-k">CURRENT MISSION</span>
                <span className="dossier-mission-v">{op.missionStatus || "ACTIVE"}</span>
              </div>
            </div>
            {identity.headline && <p className="dossier-headline">{identity.headline}</p>}
            <div className="dossier-record">
              <span className="dossier-record-k">MISSION RECORD</span>
              <span className="dossier-record-v">
                {(op.deploymentHistory || []).length} deployments
                {op.systems ? ` · ${op.systems.length} systems` : ""}
                {op.skills ? ` · ${op.skills.length} stack entries` : ""}
              </span>
            </div>
          </header>

          {op.currentMission && (
            <section className="dossier-section">
              <h2 className="dossier-h2"><span className="dossier-num">01</span> CURRENT OPERATIONS</h2>
              <p className="dossier-p">{op.currentMission}</p>
            </section>
          )}

          {op.systems && op.systems.length > 0 && (
            <section className="dossier-section">
              <h2 className="dossier-h2"><span className="dossier-num">02</span> SYSTEMS</h2>
              <ul className="dossier-chips">
                {op.systems.map((s, i) => <li key={i} className="dossier-chip">{s}</li>)}
              </ul>
            </section>
          )}

          {projects.length > 0 && (
            <section className="dossier-section">
              <h2 className="dossier-h2"><span className="dossier-num">03</span> ACTIVE PROJECTS</h2>
              <ul className="dossier-projects">
                {projects.map(p => (
                  <li key={p.id} className="dossier-project" onClick={() => onProject && onProject(p.id)}>
                    <span className="dossier-project-bay" style={{ color: p.color }}>{p.label}</span>
                    <div className="dossier-project-body">
                      <span className="dossier-project-title">{p.title}</span>
                      {p.identityTag && <span className="dossier-project-tag">{p.identityTag}</span>}
                    </div>
                    {p.status && <span className="dossier-project-status">{p.status}</span>}
                    <span className="dossier-project-arrow">↗</span>
                  </li>
                ))}
              </ul>
            </section>
          )}

          {op.deploymentHistory && op.deploymentHistory.length > 0 && (
            <section className="dossier-section">
              <h2 className="dossier-h2"><span className="dossier-num">04</span> DEPLOYMENT HISTORY</h2>
              <ol className="dossier-timeline">
                {op.deploymentHistory.map((entry, i) => {
                  const prev = i > 0 ? op.deploymentHistory[i - 1] : null;
                  const newYear = !prev || prev.year !== entry.year;
                  return (
                    <li key={i} className="dossier-tl-row">
                      <span className={"dossier-tl-year" + (newYear ? "" : " is-cont")}>
                        {newYear ? entry.year : ""}
                      </span>
                      <span className="dossier-tl-rail" aria-hidden="true" />
                      <span className="dossier-tl-body">
                        <span className="dossier-tl-title">{entry.title}</span>
                        {entry.note && <span className="dossier-tl-note">{entry.note}</span>}
                      </span>
                    </li>
                  );
                })}
              </ol>
            </section>
          )}

          {op.deployments && op.deployments.length > 0 && (
            <section className="dossier-section">
              <h2 className="dossier-h2"><span className="dossier-num">05</span> FIELD PRESENCE</h2>
              <ul className="dossier-deploy">
                {op.deployments.map(([place, note], i) => (
                  <li key={i} className="dossier-deploy-row">
                    <span className="dossier-deploy-k">{place}</span>
                    <span className="dossier-deploy-v">{note}</span>
                  </li>
                ))}
              </ul>
            </section>
          )}

          {op.skills && op.skills.length > 0 && (
            <section className="dossier-section">
              <h2 className="dossier-h2"><span className="dossier-num">06</span> SYSTEMS · STACK</h2>
              <ul className="dossier-chips">
                {op.skills.map((s, i) => <li key={i} className="dossier-chip dossier-chip-skill">{s}</li>)}
              </ul>
            </section>
          )}

          {Array.isArray(identity.otherBuilds) && identity.otherBuilds.length > 0 && (
            <section className="dossier-section">
              <h2 className="dossier-h2"><span className="dossier-num">07</span> OTHER BUILDS · ARCHIVE</h2>
              <p className="dossier-archive-note">
                Side projects and earlier builds. Not full bays in the yard, surfaced here for completeness.
              </p>
              <ul className="dossier-archive">
                {identity.otherBuilds.map((b, i) => (
                  <li key={i} className="dossier-archive-row">
                    <span className="dossier-archive-logo-wrap" aria-hidden="true">
                      <SafeImage className="dossier-archive-logo" src={b.logo} alt={b.name + " logo"} />
                    </span>
                    <span className="dossier-archive-body">
                      <span className="dossier-archive-name">{b.name}</span>
                      {b.note && <span className="dossier-archive-note-row">{b.note}</span>}
                    </span>
                  </li>
                ))}
              </ul>
            </section>
          )}

          <footer className="dossier-actions">
            {onResume && (
              <ResumeAccessButton onResume={onResume} variant="primary" />
            )}
            {onContact && (
              <button type="button" className="dossier-btn" onClick={onContact}>
                <span className="db-k">OPEN DISPATCH</span>
                <span className="db-arrow">→</span>
              </button>
            )}
          </footer>
        </div>
      </div>
    </div>
  );
}

// ─────────── QUICK ACCESS DOCK ───────────
// Persistent compact dock for visitors who won't explore the yard deeply.
// Sits unobtrusively at the bottom-right and never blocks the world. The
// dock is the official answer to "I just want the résumé / contact" — every
// action here completes the visit in ≤1 click.
function QuickAccessDock({ onTour, onResume, onDossier, onContact, mode }) {
  const [open, setOpen] = useState(false);
  // Auto-close when entering focus mode so the dock doesn't crowd the panel.
  useEffect(() => {
    if (mode === "focus") setOpen(false);
  }, [mode]);
  const routes = (window.YARD_DATA && window.YARD_DATA.identity && window.YARD_DATA.identity.routes) || {};
  const linkedinHref = routes.linkedin && routes.linkedin.href;
  const githubHref = routes.github && routes.github.href;
  const emailHref = routes.email && routes.email.href;
  return (
    <div className={"quick-dock" + (open ? " is-open" : "")} role="navigation" aria-label="Quick access">
      <button
        type="button"
        className="qd-toggle"
        aria-label={open ? "Close quick access" : "Open quick access"}
        aria-expanded={open}
        onClick={() => setOpen(o => !o)}
      >
        <span className="qd-toggle-k">{open ? "CLOSE" : "QUICK ACCESS"}</span>
        <span className="qd-toggle-arrow">{open ? "▾" : "▴"}</span>
      </button>
      {open && (
        <div className="qd-tray">
          <div className="qd-group">
            <span className="qd-group-k">PORTFOLIO</span>
            {onTour && (
              <button type="button" className="qd-item" onClick={() => { setOpen(false); onTour(); }}>
                <span className="qd-item-k">Start tour</span>
                <span className="qd-item-v">5 steps · narrated</span>
              </button>
            )}
            {onDossier && (
              <button type="button" className="qd-item" onClick={() => { setOpen(false); onDossier(); }}>
                <span className="qd-item-k">Operator dossier</span>
                <span className="qd-item-v">Mission · projects · stack</span>
              </button>
            )}
          </div>
          <div className="qd-group">
            <span className="qd-group-k">DIRECT</span>
            {onResume && (
              <button type="button" className="qd-item primary" onClick={() => { setOpen(false); onResume(); }}>
                <span className="qd-item-k">Résumé PDF</span>
                <span className="qd-item-v">Open in new tab</span>
              </button>
            )}
            {onContact && (
              <button type="button" className="qd-item" onClick={() => { setOpen(false); onContact(); }}>
                <span className="qd-item-k">Contact manifest</span>
                <span className="qd-item-v">BAY-11 · always open</span>
              </button>
            )}
            {emailHref && (
              <a className="qd-item" href={emailHref}>
                <span className="qd-item-k">Email</span>
                <span className="qd-item-v">anthonyabdulnour@gmail.com</span>
              </a>
            )}
            {linkedinHref && (
              <a className="qd-item" href={linkedinHref} target="_blank" rel="noreferrer">
                <span className="qd-item-k">LinkedIn</span>
                <span className="qd-item-v">/in/anthonyabdulnour</span>
              </a>
            )}
            {githubHref && (
              <a className="qd-item" href={githubHref} target="_blank" rel="noreferrer">
                <span className="qd-item-k">GitHub</span>
                <span className="qd-item-v">/Aabdulnour</span>
              </a>
            )}
          </div>
        </div>
      )}
    </div>
  );
}

// ─────────── ZONE CHIP (active zone HUD) ───────────
// Small steel-blue chip pinned bottom-left of the HUD that names the zone
// the camera is currently focused on. Reads off the nearest container's zone
// membership so it stays accurate as the visitor pans around.
function ZoneChip({ scene, nearest }) {
  const data = window.YARD_DATA || {};
  const c = data.containers && data.containers[nearest];
  const zones = scene && scene.getZones ? scene.getZones() : [];
  const zone = c ? zones.find(z => z.ids.includes(c.id)) : null;
  if (!zone) return null;
  return (
    <div className="zone-chip" role="status" aria-live="polite" style={{ "--zc-color": zone.color }}>
      <span className="zc-dot" />
      <span className="zc-k">ZONE</span>
      <span className="zc-v">{zone.title}</span>
      <span className="zc-sub">{zone.sub}</span>
    </div>
  );
}

// ─────────── HOVER TOOLTIP ───────────
// World-anchored "Open BAY-XX · TITLE" tag that follows the nearest container's
// projected screen position. Provides a clear hover affordance without
// cluttering the HUD. Only renders in overview when not over UI chrome.
function HoverTooltip({ scene, nearest, mode, openIndex }) {
  const [pos, setPos] = useState(null);

  useEffect(() => {
    if (!scene || !scene.getContainerScreenPos) { setPos(null); return; }
    if (mode !== "overview") { setPos(null); return; }
    if (typeof nearest !== "number" || nearest < 0) { setPos(null); return; }
    if (openIndex === nearest) { setPos(null); return; }
    let raf;
    const tick = () => {
      const p = scene.getContainerScreenPos(nearest);
      if (p && p.visible) setPos(p); else setPos(null);
      raf = requestAnimationFrame(tick);
    };
    tick();
    return () => cancelAnimationFrame(raf);
  }, [scene, nearest, mode, openIndex]);

  if (!pos) return null;
  const data = (window.YARD_DATA && window.YARD_DATA.containers[nearest]) || null;
  if (!data) return null;
  // Lift the tip further above the container top so it never floats over the
  // bay number painted on an adjacent container at similar screen-y. Bumped
  // from -18 to -32 (plus the CSS -100% transform) so the tooltip clears the
  // container body in the projected view from the default camera.
  const left = Math.max(120, Math.min(window.innerWidth - 120, pos.x));
  const top = Math.max(56, Math.min(window.innerHeight - 96, pos.y - 32));
  return (
    <div className="hover-tip" style={{ left, top }} role="tooltip" aria-live="polite">
      <span className="ht-arrow">▾</span>
      <span className="ht-bay">{data.label}</span>
      <span className="ht-sep">·</span>
      <span className="ht-title">{data.title}</span>
      <span className="ht-cta">OPEN ↗</span>
    </div>
  );
}

// ─────────── BUILD REVEAL TOAST ───────────
// Industrial dispatch notification that fires once during the tour's BUILD
// step. Reinforces the "this is the heart of the world" beat without
// modifying the 3D scene itself. Auto-dismisses after ~3.6s.
function BuildRevealToast({ active }) {
  const [shown, setShown] = useState(false);
  useEffect(() => {
    if (!active) { setShown(false); return; }
    // Slight delay so the toast lands after the camera-settle, not in the middle
    // of the move. ~3.6s total presence keeps it from competing with the panel.
    const t1 = setTimeout(() => setShown(true), 480);
    const t2 = setTimeout(() => setShown(false), 4100);
    return () => { clearTimeout(t1); clearTimeout(t2); };
  }, [active]);
  if (!active) return null;
  return (
    <div className={"build-reveal" + (shown ? " is-shown" : "")} role="status" aria-live="polite">
      <div className="br-frame">
        <span className="br-tag"><span className="br-pulse" /> BUILD FLOOR</span>
        <span className="br-line">SHIPPED PRODUCTS</span>
        <span className="br-sub">Four projects · click any container to open</span>
      </div>
    </div>
  );
}

// ─────────── ROUTE COMPLETE BEAT ───────────
// Earned, one-time celebration when the visitor has opened every stop on
// the recommended route (intro → mygui → cortex → potentia → pods →
// contact). Full-bleed vignette with a centered "ROUTE COMPLETE" frame, a
// DISPATCH cue, and a soft auto-fade. Dismissal persists in localStorage.
function RouteCompleteBeat({ onDispatch, onClose }) {
  const [shown, setShown] = useState(false);
  useEffect(() => {
    const t1 = setTimeout(() => setShown(true), 60);
    // Slight auto-hint: pulse the call-to-action after a couple seconds so
    // a hesitant visitor still gets a nudge toward DISPATCH.
    return () => clearTimeout(t1);
  }, []);
  useEffect(() => {
    const onKey = (e) => { if (e.key === "Escape") onClose && onClose(); };
    window.addEventListener("keydown", onKey);
    return () => window.removeEventListener("keydown", onKey);
  }, [onClose]);
  return (
    <div className={"route-beat" + (shown ? " is-shown" : "")} role="dialog" aria-label="Route complete">
      <div className="rb-vignette" aria-hidden="true" />
      <div className="rb-card">
        <div className="rb-tag">
          <span className="rb-pulse" /> OPERATOR · ROUTE COMPLETE
        </div>
        <div className="rb-headline">
          You've walked the yard.
        </div>
        <div className="rb-sub">
          Six stops cleared · the dispatch line is open.
        </div>
        <div className="rb-actions">
          <button type="button" className="rb-btn primary" onClick={onDispatch}>
            <span className="rb-btn-k">DISPATCH</span>
            <span className="rb-btn-arrow">→</span>
          </button>
          <button type="button" className="rb-btn" onClick={onClose}>
            <span className="rb-btn-k">KEEP EXPLORING</span>
          </button>
        </div>
        <div className="rb-foot">
          <span className="rb-tick" />
          <span className="rb-tick" />
          <span className="rb-tick" />
          <span className="rb-tick" />
          <span className="rb-tick" />
          <span className="rb-tick" />
          <span className="rb-foot-k">// MANIFEST CLEARED · 06/06</span>
        </div>
      </div>
    </div>
  );
}

// ─────────── FINAL MOMENT VIGNETTE ───────────
// A warm vignette + tagline that softens the world for the tour's final step
// ("Let's build something real."). World stays visible — just a calmer
// atmosphere. Detached from the panel so the panel can dock independently.
function FinalMomentVignette({ active }) {
  // Keep the vignette mounted briefly after it deactivates so it can fade out
  // instead of popping — a hard darkening-to-clear unmount was the abrupt
  // "cut" felt on tour finish. The camera and the open bay are never touched.
  const [render, setRender] = useState(active);
  const [closing, setClosing] = useState(false);
  useEffect(() => {
    if (active) { setRender(true); setClosing(false); return; }
    if (!render) return;
    setClosing(true);
    const t = setTimeout(() => { setRender(false); setClosing(false); }, 340);
    return () => clearTimeout(t);
  }, [active, render]);
  if (!render) return null;
  return (
    <div className={"final-moment" + (closing ? " is-closing" : "")} role="presentation">
      <div className="fm-vignette" />
      <div className="fm-line">
        <span className="fm-narration">// dispatch line open</span>
        <span className="fm-tagline">Let&rsquo;s build something real.</span>
      </div>
    </div>
  );
}

// expose
Object.assign(window, {
  BootSequence, WebGLFallback, HUD, ControlsDock, Cursor, Terminal,
  ContainerPanel, DebugScene, ContactPanel, HiddenBay, MiniMap, FilterBar,
  BotPrompt, QuickActions, SignatureCTA,
  FilterToast, TourMode, HoverTooltip,
  MissionBriefing, OperatorDossier, QuickAccessDock, ZoneChip,
  BuildRevealToast, FinalMomentVignette, RouteCompleteBeat,
});
