/* ============================================================
   lib.jsx — shared primitives
   ============================================================ */
const { useState, useEffect, useRef, useLayoutEffect, useCallback } = React;

/* ---- measure a DOM node's pixel size ---- */
function useSize() {
  const ref = useRef(null);
  const [size, setSize] = useState({ w: 0, h: 0 });
  useLayoutEffect(() => {
    if (!ref.current) return;
    const el = ref.current;
    const ro = new ResizeObserver(() => {
      setSize({ w: el.clientWidth, h: el.clientHeight });
    });
    ro.observe(el);
    setSize({ w: el.clientWidth, h: el.clientHeight });
    return () => ro.disconnect();
  }, []);
  return [ref, size];
}

/* ---- run a timed sequence of steps, cancelable ---- */
function useTimeline() {
  const timers = useRef([]);
  const clear = useCallback(() => {
    timers.current.forEach(clearTimeout);
    timers.current = [];
  }, []);
  const at = useCallback((ms, fn) => {
    timers.current.push(setTimeout(fn, ms));
  }, []);
  useEffect(() => clear, [clear]);
  return { at, clear };
}

/* ============================================================
   useScrub — scrubbable virtual-clock timeline
   Each screen declares its animation as a list of keyframes:
     { t: ms, label, apply }   (apply = idempotent state setter)
   plus a reset() that restores the initial state.
   The hook plays them on a virtual clock you can drag/seek to
   any moment; CSS transitions interpolate between keyframes so a
   scrub feels continuous. Returns a controller for <Scrubber>.
   ============================================================ */
function useScrub(spec) {
  const framesRef = useRef([]);
  const resetRef = useRef(() => {});
  framesRef.current = (spec.frames || []).slice().sort((a, b) => a.t - b.t);
  resetRef.current = spec.reset || (() => {});
  const lastT = framesRef.current.length ? framesRef.current[framesRef.current.length - 1].t : 0;
  const duration = spec.duration || lastT + 700;

  const [time, setTime] = useState(0);
  /* 到屏即暂停（当接入 shell 的 bus 时）：等「点击」自动播完整屏，或「方向键」逐拍推进。
     未接 bus 的旧用法保持原自动播放行为。显式传 autoplay 优先。 */
  const autoplayDefault = spec.autoplay !== undefined ? spec.autoplay : !spec.bus;
  const [playing, setPlaying] = useState(autoplayDefault !== false);
  const [rate, setRate] = useState(1);
  const [open, setOpen] = useState(true);
  const timeRef = useRef(0);
  const appliedRef = useRef(-1);   // index of last-applied frame (-1 = only reset)

  /* recompute applied frames for time tt — idempotent, handles backward seeks */
  const applyAt = useCallback((tt) => {
    const frames = framesRef.current;
    let target = -1;
    for (let i = 0; i < frames.length; i++) { if (frames[i].t <= tt) target = i; else break; }
    if (target === appliedRef.current) return;
    if (target < appliedRef.current) {
      resetRef.current();
      for (let i = 0; i <= target; i++) frames[i].apply();
    } else {
      for (let i = appliedRef.current + 1; i <= target; i++) frames[i].apply();
    }
    appliedRef.current = target;
  }, []);

  const setT = useCallback((tt) => {
    tt = Math.max(0, Math.min(duration, tt));
    timeRef.current = tt; setTime(tt); applyAt(tt);
  }, [duration, applyAt]);

  /* on mount: establish the initial (reset) state once.
     倒着进来的屏直接跳到末态（呈现完整结果），方便从后往前回看；正着进来则停在起始态等推进。
     autoStart 屏例外：正着进来时把首段（到第一个停顿点）直接呈现，免去一次「空拍」推进。 */
  useEffect(() => {
    resetRef.current();
    if (!framesRef.current.length) return;
    if (spec.bus && spec.bus.entryDir === "back") {
      setPlaying(false);
      setT(duration);
    } else if (spec.autoStart) {
      const frames = framesRef.current;
      let firstStop = frames.findIndex((f) => f.stop);
      if (firstStop < 0) firstStop = frames.length - 1;
      setT(frames[firstStop].t);
    }
  }, []);

  /* playback loop (rAF, virtual clock) */
  useEffect(() => {
    if (!playing) return;
    let raf, prev = null;
    const loop = (ts) => {
      if (prev == null) prev = ts;
      const dt = (ts - prev) * rate; prev = ts;
      const nt = timeRef.current + dt;
      if (nt >= duration) { setT(duration); setPlaying(false); return; }
      setT(nt);
      raf = requestAnimationFrame(loop);
    };
    raf = requestAnimationFrame(loop);
    return () => cancelAnimationFrame(raf);
  }, [playing, rate, duration, setT]);

  const rewind0 = useCallback(() => {
    resetRef.current(); appliedRef.current = -1; timeRef.current = 0; setTime(0);
  }, []);
  const play = useCallback(() => { if (timeRef.current >= duration) rewind0(); setPlaying(true); }, [duration, rewind0]);
  const pause = useCallback(() => setPlaying(false), []);
  const toggle = useCallback(() => setPlaying((p) => { if (!p && timeRef.current >= duration) rewind0(); return !p; }), [duration, rewind0]);
  const seekNorm = useCallback((p) => { setPlaying(false); setT(p * duration); }, [setT, duration]);
  const seek = useCallback((tt) => { setPlaying(false); setT(tt); }, [setT]);
  const replay = useCallback(() => { rewind0(); setPlaying(true); }, [rewind0]);

  /* —— 逐拍推进（方向键）/ 自动播完（点击）—— 供 shell 通过 bus 驱动 —— */
  const playingRef = useRef(false);
  playingRef.current = playing;
  const atEnd = () => appliedRef.current >= framesRef.current.length - 1;
  /* 「段」边界：标了 stop:true 的帧即一个停顿点；末帧永远是停顿点。
     未标任何 stop 的屏 → 整屏一段（按一下整屏全呈现）。 */
  const stopList = () => {
    const frames = framesRef.current;
    if (!frames.length) return [];
    const arr = frames.map((f, i) => (f.stop ? i : -1)).filter((i) => i >= 0);
    if (!arr.length || arr[arr.length - 1] !== frames.length - 1) arr.push(frames.length - 1);
    return arr;
  };
  /* 方向键右：跳到下一个停顿点，把中间帧一次性应用（整段立刻全呈现）；
     已过末段返回 false，让 shell 翻到下一屏 */
  const stepNext = useCallback(() => {
    setPlaying(false);
    const stops = stopList();
    const next = stops.find((i) => i > appliedRef.current);
    if (next === undefined) return false;
    setT(framesRef.current[next].t);
    return true;
  }, [setT]);
  /* 方向键左：回到上一个停顿点；已在首段则退到起始态，再按才翻到上一屏 */
  const stepPrev = useCallback(() => {
    setPlaying(false);
    const stops = stopList();
    let prev;
    for (let k = stops.length - 1; k >= 0; k--) { if (stops[k] < appliedRef.current) { prev = stops[k]; break; } }
    if (prev === undefined) {
      if (appliedRef.current >= 0) { rewind0(); return true; }
      return false;
    }
    setT(framesRef.current[prev].t);
    return true;
  }, [setT, rewind0]);
  /* 点击：
     · 分拍逐步屏（autoStart）—— 与方向键右一致，直接跳到下一个停顿点，不实时空播留白；
     · 普通动画屏 —— 未播完则自动播放剩余动画（播放中再点一下＝跳到结尾）。
     所有拍已出返回 false，让 shell 翻屏。 */
  const tap = useCallback(() => {
    if (atEnd()) return false;
    if (spec.autoStart) return stepNext();
    if (playingRef.current) { setPlaying(false); setT(duration); }
    else { setPlaying(true); }
    return true;
  }, [setT, duration, stepNext, spec.autoStart]);

  /* 把步进控制器注册给 shell（仅 active 屏拿到真 bus；leaving 屏用 noopBus 不会注册） */
  const ctlRef = useRef(null);
  ctlRef.current = { stepNext, stepPrev, tap, atEnd };
  useEffect(() => {
    const bus = spec.bus;
    if (!bus || !bus.registerStepper) return;
    bus.registerStepper({
      next: () => ctlRef.current.stepNext(),
      prev: () => ctlRef.current.stepPrev(),
      tap: () => ctlRef.current.tap(),
      atEnd: () => ctlRef.current.atEnd(),
    });
    return () => { if (bus && bus.registerStepper) bus.registerStepper(null); };
  }, []);

  return {
    time, duration, playing, rate, setRate, open, setOpen,
    frames: framesRef.current, progress: duration ? time / duration : 0,
    play, pause, toggle, seekNorm, seek, replay,
    stepNext, stepPrev, tap,
  };
}

/* ---- global debug-mode flag (shared across all scenes) ---- */
function setDebugMode(on) {
  try { localStorage.setItem("ap_debug", on ? "1" : "0"); } catch (e) {}
  window.dispatchEvent(new CustomEvent("ap-debug", { detail: !!on }));
}
function useDebugMode() {
  const [on, setOn] = useState(() => {
    try { return localStorage.getItem("ap_debug") === "1"; } catch (e) { return false; }
  });
  useEffect(() => {
    const h = (e) => setOn(e.detail);
    window.addEventListener("ap-debug", h);
    return () => window.removeEventListener("ap-debug", h);
  }, []);
  return on;
}

/* ---- 开发者总开关（presentation 安全阀）----
   默认锁住：投资人看到的版本里，调试按钮 / 文案编辑入口 / 进度跳转一律不出现。
   唤出方式：网址加 ?dev=1（或 ?dev=0 锁回），或在页面上依次敲 d-e-v 三个键切换。
   锁回时一并退出调试，确保干净。 */
function devUnlocked() {
  try { return localStorage.getItem("ap_dev") === "1"; } catch (e) { return false; }
}
function setDevUnlocked(on) {
  try { localStorage.setItem("ap_dev", on ? "1" : "0"); } catch (e) {}
  if (!on) setDebugMode(false);   // 锁回 → 顺手退出调试，避免残留
  window.dispatchEvent(new CustomEvent("ap-dev", { detail: !!on }));
}
function useDevUnlocked() {
  const [on, setOn] = useState(devUnlocked);
  useEffect(() => {
    const h = (e) => setOn(e.detail);
    window.addEventListener("ap-dev", h);
    return () => window.removeEventListener("ap-dev", h);
  }, []);
  return on;
}

/* ---- Scrubber: draggable timeline debug panel (use with useScrub) ---- */
/* Renders ONLY in debug mode; in normal mode the scene autoplays untouched. */
function Scrubber({ ctl, label }) {
  const debug = useDebugMode();
  const dev = useDevUnlocked();
  const trackRef = useRef(null);
  const dragRef = useRef(false);
  const [pos, setPos] = useState(null);          // {x,y} when moved, else default CSS slot
  const moveRef = useRef(null);                  // panel-drag offset

  const normFromEvent = (e) => {
    const r = trackRef.current.getBoundingClientRect();
    return Math.max(0, Math.min(1, (e.clientX - r.left) / r.width));
  };

  useEffect(() => {
    const onMove = (e) => {
      if (dragRef.current) { ctl.seekNorm(normFromEvent(e)); }
      else if (moveRef.current) {
        setPos({ x: e.clientX - moveRef.current.dx, y: e.clientY - moveRef.current.dy });
      }
    };
    const onUp = () => { dragRef.current = false; moveRef.current = null; };
    window.addEventListener("pointermove", onMove);
    window.addEventListener("pointerup", onUp);
    return () => { window.removeEventListener("pointermove", onMove); window.removeEventListener("pointerup", onUp); };
  }, [ctl]);

  if (!debug || !dev) return null;   // normal / presentation mode: no panel, scene autoplays untouched

  const startScrub = (e) => { dragRef.current = true; ctl.seekNorm(normFromEvent(e)); };
  const startMove = (e) => {
    if (e.target.closest("button")) return;
    const el = e.currentTarget.parentElement.getBoundingClientRect();
    moveRef.current = { dx: e.clientX - el.left, dy: e.clientY - el.top };
    setPos({ x: el.left, y: el.top });
  };

  /* which keyframe is "current" (last one whose t <= time) */
  let active = -1;
  for (let i = 0; i < ctl.frames.length; i++) { if (ctl.frames[i].t <= ctl.time + 1) active = i; else break; }

  const style = pos ? { left: pos.x, top: pos.y, right: "auto", bottom: "auto" } : null;

  return (
    <div className={"sb-debug no-advance" + (ctl.open ? "" : " min")} style={style}>
      <div className="sb-head" onPointerDown={startMove}>
        <button className="sb-play" onClick={ctl.toggle} aria-label={ctl.playing ? "暂停" : "播放"}>
          {ctl.playing ? "❚❚" : "▶"}
        </button>
        <span className="sb-title">调试 · {label}</span>
        <span className="sb-time mono">{(ctl.time / 1000).toFixed(1)}s</span>
        <button className="sb-x" onClick={() => ctl.setOpen(!ctl.open)}>{ctl.open ? "–" : "+"}</button>
      </div>
      {ctl.open && (
        <div className="sb-body">
          <div className="sb-track" ref={trackRef} onPointerDown={startScrub}>
            <div className="sb-fill" style={{ width: ctl.progress * 100 + "%" }} />
            {ctl.frames.map((f, i) => (
              <span key={i} className={"sb-tick" + (i === active ? " on" : "")}
                    style={{ left: (f.t / ctl.duration) * 100 + "%" }} />
            ))}
            <div className="sb-handle" style={{ left: ctl.progress * 100 + "%" }} />
          </div>
          {ctl.frames.some((f) => f.label) && (
            <div className="sb-steps">
              {ctl.frames.map((f, i) => f.label && (
                <button key={i} className={"sb-step" + (i === active ? " on" : "")}
                        onClick={() => ctl.seek(f.t)}>{f.label}</button>
              ))}
            </div>
          )}
          <div className="sb-row">
            <span className="sb-lbl">速度</span>
            {[0.25, 0.5, 1, 2].map((r) => (
              <button key={r} className={"sb-rate" + (ctl.rate === r ? " on" : "")} onClick={() => ctl.setRate(r)}>{r}×</button>
            ))}
            <button className="sb-replay" onClick={ctl.replay}><Icon name="RotateCcw" size={13} /> 重播</button>
          </div>
        </div>
      )}
    </div>
  );
}

/* ============================================================
   Brand tiles — real product logos
   ============================================================ */
/* full-color official logos — rendered as-is on a clean light tile */
const LOGO = {
  claude: "claude.svg", anthropic: "anthropic.svg", openai: "openai.svg", gemini: "gemini.svg",
  deepseek: "deepseek.svg", qwen: "qwen.svg", kimi: "kimi.svg", notion: "notion.svg",
  slack: "slack.svg", discord: "discord.svg", telegram: "telegram.svg", gmail: "gmail.svg",
  gcal: "googlecalendar.svg", reddit: "reddit.svg", linear: "linear.svg", copilot: "githubcopilot.svg",
  openrouter: "openrouter.svg", x: "x.svg", perplexity: "perplexity.svg", grok: "grok.svg",
  hackernews: "ycombinator.svg", apple: "apple.svg", feishu: "feishu.webp",
  /* iMessage —— 官方 app-icon（自带绿色渐变圆角底 + 白色气泡），整图满铺，不走刷白蒙版 */
  imessage: "imessage.svg",
  /* 本机可路由的 Agent / 本地 LLM 运行时（真实官方 logo，取自 svgl 开源 logo 库） */
  cursor: "cursor.svg", codex: "codex.svg", claudecode: "claude.svg",
  windsurf: "windsurf.svg", ollama: "ollama.svg", zed: "zed.svg",
  hermes: "hermes.png", openclaw: "openclaw.png", manus: "manus-icon.png",
  /* 垂直 Agent —— 真实产品官方 app-icon（Rogo · 面向金融的垂直 Agent） */
  rogo: "rogo.png",
};
/* full-bleed raster app-icons —— fill the tile edge-to-edge, clipped to its rounding */
const LOGO_BLEED = { openclaw: true, hermes: true, imessage: true };
const LOGO_SCALE = {
  feishu: 0.72,
  rogo: 0.92,
  manus: 0.74,
  hermes: 1,
  openclaw: 1,
  imessage: 1,
  gmail: 0.68,
  gcal: 0.72,
  githubcopilot: 0.7,
  copilot: 0.7,
  perplexity: 0.72,
  slack: 0.7,
};
/* monochrome brand marks — rendered white on the brand-color tile (real app-icon look) */
const TILE = {
  wechat:   { src: "wechat.svg",   bg: "#07c160" },
  arxiv:    { src: "arxiv.svg",    bg: "#b31b1b" },
  /* 本机 Agent —— 统一用本机执行体图标，深色底与第三幕选中态保持一致 */
  localagent: { lucide: "MonitorSmartphone", bg: "#1f2329" },
  /* AgentPilot 自身 —— 「本机 Agent」就是 AgentPilot 在本地的执行体，用产品自有标记（slate 主色），区别于第三方 logo */
  agentpilot: { lucide: "Navigation2", bg: "#5b6478" },
};
/* generic sources — Lucide line icons on a neutral tile（统一走 Icon 组件，按 Lucide 名渲染） */
const GLYPHS = {
  web:    { color: "var(--accent)", lucide: "Globe" },
  folder: { color: "var(--warn)",  lucide: "Folder" },
  server: { color: "var(--accent)", lucide: "Server" },
  cpu:    { color: "var(--ink)",   lucide: "Cpu" },
  doc:    { color: "var(--ink-3)", lucide: "FileText" },
  news:   { color: "var(--pos)",   lucide: "Newspaper" },
  chart:  { color: "var(--neg)",   lucide: "TrendingUp" },
};
const NAMES = {
  claude: "Claude", anthropic: "Anthropic", openai: "OpenAI", gemini: "Gemini",
  deepseek: "DeepSeek", qwen: "通义千问", kimi: "Kimi", notion: "Notion",
  slack: "Slack", discord: "Discord", telegram: "Telegram", gmail: "邮箱",
  gcal: "日历", reddit: "Reddit", linear: "Linear", copilot: "Copilot",
  openrouter: "OpenRouter", x: "X", perplexity: "Perplexity", grok: "Grok",
  hackernews: "Hacker News", apple: "桌面端", wechat: "微信", imessage: "iMessage",
  arxiv: "arXiv", feishu: "飞书",
  cursor: "Cursor", codex: "Codex", claudecode: "Claude Code",
  windsurf: "Windsurf", ollama: "Ollama", zed: "Zed",
  hermes: "Hermes Agent", openclaw: "OpenClaw", manus: "Manus",
  rogo: "Rogo", localagent: "本机 Agent", agentpilot: "本机 Agent",
  web: "公开网络", folder: "本地文件夹", server: "远端 Runner", cpu: "本机 Token",
  doc: "文档", news: "新闻源", chart: "持仓监控",
};

function BrandTile({ brand, size = 52, radius = 14, soft = true, glow = false }) {
  const s = { width: size, height: size, borderRadius: radius };
  /* full-color official logo */
  if (LOGO[brand]) {
    const logoSize = size * (LOGO_SCALE[brand] || 0.62);
    return (
      <span className={"btile logo" + (glow ? " glow" : "")} style={{ ...s }}>
        <img className={LOGO_BLEED[brand] ? "bleed" : ""}
             src={"assets/logos/" + LOGO[brand]} alt={NAMES[brand] || brand}
             style={{ width: logoSize, height: logoSize }} />
      </span>
    );
  }
  /* brand-color tile + white mark */
  if (TILE[brand]) {
    const t = TILE[brand];
    return (
      <span className={"btile" + (glow ? " glow" : "")} style={{ ...s, background: t.bg, color: "#fff" }}>
        {t.src
          ? <img className="inv" src={"assets/logos/" + t.src} alt={NAMES[brand] || brand}
                 style={{ width: size * 0.56, height: size * 0.56 }} />
          : t.lucide
          ? <Icon name={t.lucide} size={size * 0.56} color="#fff" />
          : <span style={{ fontSize: size * 0.5, fontWeight: 700, letterSpacing: "-.02em" }}>{t.mark}</span>}
      </span>
    );
  }
  /* generic Lucide line glyph */
  const g = GLYPHS[brand] || GLYPHS.doc;
  return (
    <span className={"btile" + (glow ? " glow" : "")} style={{ ...s, background: soft ? "#f3f4f7" : "#fff" }}>
      <Icon name={g.lucide} size={size * 0.56} color={g.color} />
    </span>
  );
}
function brandName(brand) { return NAMES[brand] || brand; }

/* ============================================================
   Phone frame with WeChat-style chat
   ============================================================ */
function Phone({ entry = "wechat", onEntry, entries = ["wechat", "slack", "feishu", "apple"], children, title, footer, bodyRef }) {
  return (
    <div className="phone">
      <div className="phone-notch" />
      <div className="phone-screen">
        <div className="chat-head">
          <span className="chat-back"><Icon name="ChevronLeft" size={20} /></span>
          <div className="chat-title">
            <span className="ct-name">{title || brandName(entry)}</span>
            <span className="ct-sub">AgentPilot</span>
          </div>
          <span className="chat-dots">···</span>
        </div>
        {entries && (
          <div className="entry-switch">
            {entries.map((e) => (
              <button key={e}
                className={"entry-chip" + (e === entry ? " on" : "")}
                onClick={() => onEntry && onEntry(e)}>
                <BrandTile brand={e} size={22} radius={6} soft={false} />
                <span>{brandName(e)}</span>
              </button>
            ))}
          </div>
        )}
        <div className="chat-body" ref={bodyRef}>{children}</div>
        {footer && <div className="chat-input">{footer}</div>}
      </div>
    </div>
  );
}

function ChatBubble({ side = "in", children, delay = 0, brand }) {
  return (
    <div className={"bubble-row " + side} style={{ animationDelay: delay + "ms" }}>
      <div className={"bubble " + side + (brand === "wechat" ? " wx" : "")}>{children}</div>
    </div>
  );
}

/* a richer delivery card that lands in the chat */
function DeliveryCard({ title, lines = [], sediment, delay = 0 }) {
  return (
    <div className="bubble-row in" style={{ animationDelay: delay + "ms" }}>
      <div className="deliver-card">
        <div className="dc-title">{title}</div>
        <div className="dc-lines">
          {lines.map((l, i) => (
            <div className="dc-line" key={i}>
              <span className="dc-tick"><Icon name="Check" size={14} stroke={2.6} /></span>
              <span>{l}</span>
            </div>
          ))}
        </div>
        {sediment && (
          <div className="dc-sediment">
            <BrandTile brand={sediment.brand} size={26} radius={7} />
            <span>{sediment.label}</span>
            <span className="dc-arrow">↘</span>
          </div>
        )}
      </div>
    </div>
  );
}

/* ============================================================
   Small UI atoms
   ============================================================ */
/* DoneTick —— 全站唯一的「完成」徽章：绿色实心圆 + 白色对勾。
   任何表示「完成 / 已送达 / 已选中 / 通过」的圆形勾选都必须走这里，
   只通过 size 适配上下文，样式（圆形 · 实心 --pos · 白勾 · 60% 占比）全站一致。
   不传 size 时由 CSS 控制（默认 18px，可在具体场景用 .x .done-tick 覆盖、含响应式）。 */
function DoneTick({ size, className = "" }) {
  const style = size != null ? { width: size, height: size } : undefined;
  return (
    <span className={"done-tick" + (className ? " " + className : "")} style={style}>
      <Icon name="Check" stroke={3} color="#fff" />
    </span>
  );
}

function Chip({ label, on, done, disabled, onClick }) {
  return (
    <button className={"task-chip" + (on ? " on" : "") + (done ? " done" : "")}
            disabled={disabled} onClick={onClick}>
      {done && <DoneTick size={20} />}
      <span>{label}</span>
      {!done && <span className="chip-go"><Icon name="ChevronRight" size={16} /></span>}
    </button>
  );
}

function Tag({ children, tone = "neutral" }) {
  return <span className={"tag tone-" + tone}>{children}</span>;
}

/* ============================================================
   Icon —— 统一图标入口，渲染 Lucide（成熟图标库）官方矢量数据
   用法：<Icon name="Check" /> · name 为 Lucide 的 PascalCase 名（Check / QrCode / ArrowUp …）
   全站所有图标都应走这里，不再手写/手画 SVG。stroke 默认 currentColor，可随文字色变化。
   ============================================================ */
function Icon({ name, size = 18, stroke = 2, color = "currentColor", className, style }) {
  const lib = (typeof window !== "undefined" && window.lucide) || null;
  const node = lib ? (lib[name] || (lib.icons && lib.icons[name])) : null;
  const children = node && Array.isArray(node[2]) ? node[2] : [];
  return (
    <svg className={className} style={style} width={size} height={size} viewBox="0 0 24 24"
         fill="none" stroke={color} strokeWidth={stroke} strokeLinecap="round" strokeLinejoin="round"
         aria-hidden="true" focusable="false">
      {children.map(([tag, attrs], i) => React.createElement(tag, { key: i, ...attrs }))}
    </svg>
  );
}

Object.assign(window, {
  useSize, useTimeline, useScrub, Scrubber, useDebugMode, setDebugMode,
  devUnlocked, setDevUnlocked, useDevUnlocked, LOGO, TILE, GLYPHS, NAMES,
  BrandTile, brandName, Phone, ChatBubble, DeliveryCard, Chip, Tag, Icon, DoneTick,
});
