// Coframe Interview Playback · embedded interactive
// Adapted from uploads/coframe_interview_playback_experience_v2.jsx for AGI Intelligence Report v8.
// Changes from source:
//   - ES module imports → React globals + inline shims (no framer-motion / lucide-react CDNs)
//   - <motion.X animate={...}> → plain <X> via a tiny Proxy shim that strips motion props
//   - <AnimatePresence> → fragment (no exit animations)
//   - lucide-react icons → inline SVG icon components matching the lucide API (size, className)
//   - StyleGuide component removed (it injected body{} and clobbered the report's :root vars)
//   - AUDIO_SRC blanked so the audio element fails to load — the existing timer fallback then
//     drives playback simulation course-by-course (no external m4a dependency)
//   - Tailwind utility classes preserved; loaded via the play CDN with preflight disabled.
//     Editorial overrides live in the parent stylesheet under .coframe-mount.

(function () {

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

// ---- framer-motion shim ---------------------------------------------------
// motion.div / motion.svg / motion.circle ... → plain element. animate / initial /
// exit / transition / whileHover / whileTap are filtered out; everything else passes
// through unchanged.
const MOTION_PROPS = new Set(['animate','initial','exit','transition','whileHover','whileTap','whileInView','layout','layoutId','variants','custom','drag','dragConstraints']);
function stripMotionProps(props) {
  const out = {};
  for (const k in props) {
    if (!MOTION_PROPS.has(k)) out[k] = props[k];
  }
  return out;
}
const motion = new Proxy({}, {
  get(_, tag) {
    return React.forwardRef(function MotionShim(props, ref) {
      return React.createElement(tag, Object.assign({ ref }, stripMotionProps(props)), props.children);
    });
  }
});
const AnimatePresence = ({ children }) => React.createElement(React.Fragment, null, children);

// ---- lucide-react icon stubs ---------------------------------------------
// Match the lucide API: <Icon size={N} className="..." />. SVGs are drawn fresh,
// not lifted from lucide; the stroke geometry just needs to read at small sizes.
function makeIcon(viewBox, paths) {
  return function Icon({ size = 18, className = '', ...rest }) {
    return React.createElement('svg', Object.assign({
      width: size, height: size, viewBox: viewBox,
      fill: 'none', stroke: 'currentColor', strokeWidth: 1.8,
      strokeLinecap: 'round', strokeLinejoin: 'round', className
    }, rest), paths);
  };
}
const Bot = makeIcon('0 0 24 24', [
  React.createElement('rect', { key: 'b1', x: 4, y: 8,  width: 16, height: 12, rx: 2 }),
  React.createElement('circle',{ key: 'b2', cx: 9,  cy: 14, r: 1 }),
  React.createElement('circle',{ key: 'b3', cx: 15, cy: 14, r: 1 }),
  React.createElement('path',  { key: 'b4', d: 'M12 4v4' }),
  React.createElement('circle',{ key: 'b5', cx: 12, cy: 3,  r: 1 }),
  React.createElement('path',  { key: 'b6', d: 'M4 14h-2 M20 14h2' })
]);
const Pause = makeIcon('0 0 24 24', [
  React.createElement('rect', { key:'p1', x: 6,  y: 5, width: 4, height: 14, rx: 0.5 }),
  React.createElement('rect', { key:'p2', x: 14, y: 5, width: 4, height: 14, rx: 0.5 })
]);
const Play = makeIcon('0 0 24 24', [
  React.createElement('path', { key:'pl', d: 'M7 5l12 7-12 7z', fill: 'currentColor' })
]);
const Quote = makeIcon('0 0 24 24', [
  React.createElement('path', { key:'q', d: 'M7 7h4v6c0 2-2 4-4 4 M14 7h4v6c0 2-2 4-4 4' })
]);
const Radio = makeIcon('0 0 24 24', [
  React.createElement('circle',{ key:'r1', cx:12, cy:12, r:2 }),
  React.createElement('path', { key:'r2', d: 'M4.93 19.07a10 10 0 010-14.14 M19.07 4.93a10 10 0 010 14.14 M7.76 16.24a6 6 0 010-8.48 M16.24 7.76a6 6 0 010 8.48' })
]);
const Search = makeIcon('0 0 24 24', [
  React.createElement('circle',{ key:'s1', cx:11, cy:11, r:7 }),
  React.createElement('path', { key:'s2', d: 'M21 21l-4-4' })
]);
const Send = makeIcon('0 0 24 24', [
  React.createElement('path', { key:'snd', d: 'M22 2L11 13 M22 2l-7 20-4-9-9-4z' })
]);
const SkipBack = makeIcon('0 0 24 24', [
  React.createElement('path', { key:'sb1', d: 'M19 20L9 12l10-8z', fill: 'currentColor' }),
  React.createElement('rect',{ key:'sb2', x:5, y:5, width: 2, height: 14, fill:'currentColor', stroke:'none' })
]);
const SkipForward = makeIcon('0 0 24 24', [
  React.createElement('path', { key:'sf1', d: 'M5 4l10 8-10 8z', fill: 'currentColor' }),
  React.createElement('rect',{ key:'sf2', x:17, y:5, width: 2, height: 14, fill:'currentColor', stroke:'none' })
]);
const ScrollText = makeIcon('0 0 24 24', [
  React.createElement('path', { key:'st1', d: 'M8 21h12a2 2 0 002-2v-1a2 2 0 00-2-2H6 M4 3h12 M4 3v14a4 4 0 004 4 M9 7h7 M9 11h7' })
]);

const AUDIO_SRC = "";

const transcriptLog = [
  { id: 0, speaker: "Kat", time: "00:00", topicId: "origin", text: "Hey Josh, we're honored to have you here today. You're a serial entrepreneur with amazing stories. Can you tell us a little bit about your inspiration for Coframe and how it got started?" },
  { id: 1, speaker: "Josh", time: "00:21", topicId: "origin", text: "Coframe started out as this more abstract idea that the internet in the future, instead of being static and unadaptive, is going to have its own sense of life and intelligence. We call this idea living interfaces." },
  { id: 2, speaker: "Josh", time: "01:20", topicId: "origin", text: "The origin story actually starts here at the AGI House. We were hosting a dinner, and at these dinners you have incredible people who are luminaries in the space. The discussions always create and spark new ideas." },
  { id: 3, speaker: "Josh", time: "02:15", topicId: "origin", text: "The thing that struck me most was that the internet would look nothing like it does today. It looks a lot more like interfacing with a living being — almost like Jarvis in Iron Man." },
  { id: 4, speaker: "Kat", time: "04:10", topicId: "acceleration", text: "What were you seeing in 2023 that made you feel like the Internet of Agents was not only a possibility, but a likely potentiality?" },
  { id: 5, speaker: "Josh", time: "04:35", topicId: "acceleration", text: "Something I've had to learn is adapting to the pace of acceleration we're experiencing right now. Not just acceleration, but the pace at which that accelerates — the second derivative. All of the predictions people make tend to undershoot." },
  { id: 6, speaker: "Kat", time: "08:35", topicId: "hci", text: "Before Coframe, you were at the GSB. You founded the Stanford COVID Response Lab and then Access built an autograph. How did those experiences come together for your insights?" },
  { id: 7, speaker: "Josh", time: "09:12", topicId: "hci", text: "The common thread for me is how people interact with the digital world. Before I started doing business building, my research was primarily at the intersection of AI and human-computer interaction." },
  { id: 8, speaker: "Kat", time: "14:20", topicId: "agents", text: "One framework around agent experience is Matt Billman's four pillars of access, context, tools, and orchestration. Where does Coframe shine and where has the technology been most challenging?" },
  { id: 9, speaker: "Josh", time: "15:06", topicId: "agents", text: "The two pillars we focus a lot on are context and tools. On the context side, we're pulling in the business's context. Tools are also very important because agents have goals, and that's why they exist." },
  { id: 10, speaker: "Kat", time: "20:45", topicId: "evals", text: "How does your team think about designing agent evaluation frameworks so that the human stays in the loop at the right time?" },
  { id: 11, speaker: "Josh", time: "21:22", topicId: "evals", text: "This part of the process is actually a lot more like trading — quant trading. Alpha is a signal that lets you outperform beta. In this case, beta could be thought of as the original user experience." },
  { id: 12, speaker: "Josh", time: "26:50", topicId: "approval", text: "After it's gone through the idea and the variation, then it gets to the human. That part is still pretty manual. But we're able to take that feedback and feed it back into our system." },
  { id: 13, speaker: "Kat", time: "33:40", topicId: "multimodal", text: "There's been a lot of buzz around multimodal optimizations. How has Coframe thought about these challenges and kept ahead of the curve?" },
  { id: 14, speaker: "Josh", time: "34:15", topicId: "multimodal", text: "Coframe is a multimodal native company. The lesson was that you don't want to be in the warpath of these big model companies. You want to build stuff that benefits from that advancement." },
  { id: 15, speaker: "Kat", time: "41:15", topicId: "superhuman", text: "What surprised you about how the model approached these problems? Can you tell us one of those war stories?" },
  { id: 16, speaker: "Josh", time: "42:10", topicId: "superhuman", text: "The reason we're able to get such large wins is because there are brand new types of strategies that weren't feasible before. One strategy is being able to throw massive swings at the problem — dozens of completely brand new pages instead of smaller changes." },
  { id: 17, speaker: "Josh", time: "47:40", topicId: "superhuman", text: "There's augmentation, and then there's completely different category type of strategies. What we're seeing, the 10x returns are on the second bucket, which are superhuman capabilities." },
  { id: 18, speaker: "Kat", time: "49:00", topicId: "haystacks", text: "You had the recent Haystacks AI acquisition. What capabilities are you most looking forward to with this new combined team?" },
  { id: 19, speaker: "Josh", time: "49:35", topicId: "haystacks", text: "Every function in the company has the opportunity to be automated and level up at the same time. The Haystacks acquisition is primarily focused on the go-to-market function." },
  { id: 20, speaker: "Kat", time: "50:40", topicId: "haystacks", text: "So the acquisition is not just about adding a sales tool, but expanding the pattern of self-improvement into another company function?" },
  { id: 21, speaker: "Josh", time: "51:05", topicId: "haystacks", text: "Exactly. The same underlying idea applies: take a function that has signals, workflows, and feedback loops, and build a system that can continuously improve how that function performs." },
  { id: 22, speaker: "Kat", time: "52:15", topicId: "superhuman", text: "That seems connected to your earlier point about AI not only speeding up human work, but making new categories of strategy possible." },
  { id: 23, speaker: "Josh", time: "52:44", topicId: "superhuman", text: "That's the part I think is most important. The bigger opportunity is when the system can explore a search space that a human team would not have the time or bandwidth to explore manually." },
  { id: 24, speaker: "Kat", time: "54:10", topicId: "approval", text: "Where do you think the human role becomes most important as these systems become more autonomous?" },
  { id: 25, speaker: "Josh", time: "54:38", topicId: "approval", text: "Humans are still critical for judgment, brand, taste, and deciding which risks are acceptable. The machine can generate and test, but the organization still needs to know what it stands for." },
  { id: 26, speaker: "Kat", time: "56:00", topicId: "multimodal", text: "As models get better at visual reasoning and interface generation, does the product become more about orchestration than generation itself?" },
  { id: 27, speaker: "Josh", time: "56:31", topicId: "multimodal", text: "That is how we think about it. The model layer keeps improving, so the durable work is building the system around it: context, tooling, evaluation, workflow, and trust." },
  { id: 28, speaker: "Kat", time: "58:05", topicId: "agents", text: "When you talk about context, are you thinking mostly about customer data, business goals, brand constraints, or something broader than that?" },
  { id: 29, speaker: "Josh", time: "58:36", topicId: "agents", text: "It is broader. Context is the business, the customer, the product surface, the conversion goal, the brand boundaries, and the historical feedback from what has worked before." },
  { id: 30, speaker: "Kat", time: "59:50", topicId: "evals", text: "So the evaluation layer becomes the memory of the organization, not just a report on whether one experiment won." },
  { id: 31, speaker: "Josh", time: "60:14", topicId: "evals", text: "Exactly. The point is not only to know what won. The point is to compound learning so the next generation of ideas starts from a better place." },
  { id: 32, speaker: "Kat", time: "61:30", topicId: "hci", text: "That makes the interface feel less like a screen and more like a relationship between the company, the customer, and the system interpreting both." },
  { id: 33, speaker: "Josh", time: "61:58", topicId: "hci", text: "That is a good way to put it. The interface becomes the meeting point where business intent and user behavior can actually adapt to each other." },
  { id: 34, speaker: "Kat", time: "63:05", topicId: "approval", text: "What do you think teams misunderstand about keeping humans in the loop?" },
  { id: 35, speaker: "Josh", time: "63:30", topicId: "approval", text: "They often think human review means slowing the system down. But the better framing is that human review gives the system a stronger taste function and safer boundaries." },
  { id: 36, speaker: "Kat", time: "64:42", topicId: "superhuman", text: "It sounds like the real strategic advantage is knowing when to trust the system to explore and when to ask people to judge." },
  { id: 37, speaker: "Josh", time: "65:10", topicId: "superhuman", text: "Yes. Exploration is where AI can be superhuman. Judgment is where the organization has to decide what it actually wants to become." },
  { id: 38, speaker: "Kat", time: "66:20", topicId: "haystacks", text: "What should people watch for next from Coframe after the Haystacks acquisition?" },
  { id: 39, speaker: "Josh", time: "66:48", topicId: "haystacks", text: "I would watch for more company functions becoming adaptive. Growth is the starting point, but the broader pattern is that every workflow with feedback can become more intelligent over time." },
];

const moodStyles = {
  visionary: { accent: "#4267D9", glow: "rgba(66,103,217,.30)", glow2: "rgba(117,83,201,.16)", wash: "rgba(66,103,217,.08)", node2: "rgba(117,83,201,.85)" },
  urgent: { accent: "#3D77A8", glow: "rgba(61,119,168,.28)", glow2: "rgba(176,110,36,.13)", wash: "rgba(61,119,168,.08)", node2: "rgba(176,110,36,.72)" },
  reflective: { accent: "#7255C9", glow: "rgba(114,85,201,.24)", glow2: "rgba(236,239,243,.10)", wash: "rgba(114,85,201,.07)", node2: "rgba(236,239,243,.78)" },
  systemic: { accent: "#15958B", glow: "rgba(21,149,139,.28)", glow2: "rgba(66,103,217,.12)", wash: "rgba(21,149,139,.08)", node2: "rgba(66,103,217,.75)" },
  analytical: { accent: "#B06E24", glow: "rgba(176,110,36,.28)", glow2: "rgba(66,103,217,.10)", wash: "rgba(176,110,36,.08)", node2: "rgba(66,103,217,.64)" },
  cautious: { accent: "#B94C61", glow: "rgba(185,76,97,.25)", glow2: "rgba(236,239,243,.09)", wash: "rgba(185,76,97,.07)", node2: "rgba(236,239,243,.74)" },
  curious: { accent: "#3D77A8", glow: "rgba(61,119,168,.24)", glow2: "rgba(66,103,217,.13)", wash: "rgba(61,119,168,.07)", node2: "rgba(66,103,217,.78)" },
  electric: { accent: "#7553C9", glow: "rgba(117,83,201,.34)", glow2: "rgba(66,103,217,.18)", wash: "rgba(117,83,201,.10)", node2: "rgba(66,103,217,.86)" },
  forward: { accent: "#475160", glow: "rgba(71,81,96,.25)", glow2: "rgba(21,149,139,.14)", wash: "rgba(71,81,96,.08)", node2: "rgba(21,149,139,.75)" },
};

const topics = [
  { id: "origin", title: "Living Interfaces", mood: "visionary", summary: "Josh explains Coframe as a bet on adaptive, self-improving interfaces rather than static websites.", quoteLineId: 1, visual: "network" },
  { id: "acceleration", title: "The Second Derivative", mood: "urgent", summary: "The conversation turns to AI acceleration and why forecasts consistently underestimate the pace of change.", quoteLineId: 5, visual: "curve" },
  { id: "hci", title: "AI × Human-Computer Interaction", mood: "reflective", summary: "Josh connects his founder journey to a core interest in how humans interact with the digital world.", quoteLineId: 7, visual: "network" },
  { id: "agents", title: "Agents Need Context + Tools", mood: "systemic", summary: "Coframe’s product logic is framed around giving agents the context and tools needed to act effectively.", quoteLineId: 9, visual: "flow" },
  { id: "evals", title: "Growth as Quant Trading", mood: "analytical", summary: "Evaluation is described like quant trading: experiments are signals that compete against the baseline experience.", quoteLineId: 11, visual: "curve" },
  { id: "approval", title: "Human-in-the-Loop Approval", mood: "cautious", summary: "Automation generates and tests variations, but human review remains essential before decisions are finalized.", quoteLineId: 12, visual: "approval" },
  { id: "multimodal", title: "Multimodal Native", mood: "curious", summary: "Josh positions Coframe as building on top of model progress rather than competing directly with foundation model labs.", quoteLineId: 14, visual: "flow" },
  { id: "superhuman", title: "Superhuman Strategy", mood: "electric", summary: "The strongest claim is that AI unlocks strategies humans would never attempt manually, creating the biggest upside.", quoteLineId: 17, visual: "burst" },
  { id: "haystacks", title: "Go-to-Market Agents", mood: "forward", summary: "The Haystacks acquisition broadens the automation story into go-to-market and internal workflow acceleration.", quoteLineId: 19, visual: "pipeline" },
];

function getMood(topic) { return moodStyles[topic?.mood] || moodStyles.visionary; }
function findTopic(topicId) { return topics.find((t) => t.id === topicId) || topics[0]; }
function timeToSeconds(time) {
  const parts = time.split(":").map((value) => Number(value));
  if (parts.length !== 2 || parts.some((value) => Number.isNaN(value))) return 0;
  return parts[0] * 60 + parts[1];
}
function formatSeconds(value) {
  if (!Number.isFinite(value)) return "00:00";
  const minutes = Math.floor(value / 60);
  const seconds = Math.floor(value % 60);
  return `${String(minutes).padStart(2, "0")}:${String(seconds).padStart(2, "0")}`;
}
function clampIndex(index) {
  if (!Number.isFinite(index)) return 0;
  return Math.max(0, Math.min(transcriptLog.length - 1, index));
}

function activeIndexForTime(seconds) {
  let index = 0;
  for (let i = 0; i < transcriptLog.length; i += 1) {
    if (timeToSeconds(transcriptLog[i].time) <= seconds) index = i;
  }
  return clampIndex(index);
}

function sourceOnlyAnswer(question, turn = 0) {
  const q = question.trim().toLowerCase();
  if (!q) return "Raise your hand to ask a question";

  const stopWords = new Set(["what", "when", "where", "which", "why", "how", "does", "about", "that", "this", "with", "from", "into", "your", "their", "there", "would", "could", "should", "think", "tell", "more", "question"]);
  const clean = q.replaceAll("?", " ").replaceAll(",", " ").replaceAll(".", " ").replaceAll("—", " ").replaceAll(":", " ").replaceAll(";", " ").replaceAll("!", " ");
  const words = clean.split(" ").map((w) => w.trim()).filter((w) => w.length > 2 && !stopWords.has(w));

  const topicScores = topics.map((topic) => {
    const relatedLines = transcriptLog.filter((line) => line.topicId === topic.id);
    const haystack = [topic.title, topic.summary, ...relatedLines.map((line) => line.text)].join(" ").toLowerCase();
    const keywordScore = words.reduce((acc, word) => acc + (haystack.includes(word) ? 1 : 0), 0);
    const directBoost = q.includes(topic.id) || topic.title.toLowerCase().split(" ").some((part) => part.length > 4 && q.includes(part)) ? 2 : 0;
    return { topic, score: keywordScore + directBoost };
  }).sort((a, b) => b.score - a.score);

  const selectedTopic = topicScores[0]?.score > 0 ? topicScores[0].topic : topics[turn % topics.length];
  const related = transcriptLog.filter((line) => line.topicId === selectedTopic.id);
  const quoteCandidates = related.length ? related : transcriptLog;
  const quoteLine = quoteCandidates[turn % quoteCandidates.length];
  const secondLine = quoteCandidates[(turn + 2) % quoteCandidates.length];

  if (q.includes("technical") || q.includes("eval") || q.includes("layer") || q.includes("design")) {
    return `I’d explain it simply this way: the eval layer is the part of the system that helps the company learn what actually works. It should remember the goal, compare new ideas against the old experience, collect human feedback, and feed that learning back into the next round. Josh gets at this when he says, “${quoteLine.text}” I’d keep the technical version practical: inputs, candidate ideas, tests, human review, and a learning memory.`;
  }

  if (q.includes("thesis") || q.includes("main idea") || q.includes("big idea")) {
    return `The big idea is that websites are starting to become more alive. Instead of being fixed pages, they can learn from signals, try new versions, and improve over time. Josh calls this “living interfaces,” and the clearest line is: “${quoteLine.text}”`;
  }

  if (q.includes("matter") || q.includes("important")) {
    return `It matters because this changes the role of a website or interface. It’s no longer just a place where people click around. It becomes a system that can understand context, test ideas, and get better with feedback. That’s why the human judgment part still matters too — someone has to decide what the company actually wants to stand for.`;
  }

  if (q.includes("agent")) {
    return `Josh is saying agents need more than a chat box. They need context so they understand the situation, and tools so they can actually do something useful. The friendly version is: an agent is only as helpful as what it knows and what it’s allowed to act on. One supporting moment is: “${quoteLine.text}”`;
  }

  if (q.includes("human") || q.includes("approval") || q.includes("trust") || q.includes("review")) {
    return `The human role is still really important. AI can explore, generate, and test, but people still bring taste, judgment, brand sense, and boundaries. I hear Josh’s point as: let the machine search widely, but let humans decide what is actually right for the company.`;
  }

  if (q.includes("superhuman") || q.includes("10x") || q.includes("strategy")) {
    return `The superhuman part is not just doing human work faster. It’s using AI to explore options a human team would never have time to try manually. Josh describes this through larger experimental swings — not just tiny tweaks, but whole new directions.`;
  }

  return `I’d connect your question to ${selectedTopic.title}. The simple read is: ${selectedTopic.summary} Josh says, “${quoteLine.text}” Another useful moment is, “${secondLine.text}”`;
}

function StyleGuide() { return null; }

function LogoMark() {
  return <div className="grid h-7 w-7 place-items-center border border-[var(--ink)] text-[var(--ink)]"><svg viewBox="0 0 32 32" className="h-5 w-5" fill="none"><rect x="6" y="6" width="20" height="20" stroke="currentColor" strokeWidth="1.5" /><rect x="11" y="11" width="10" height="10" stroke="currentColor" strokeWidth="1.5" /><circle cx="16" cy="16" r="1.5" fill="currentColor" /></svg></div>;
}

function Waveform({ playing, activeIndex, topic, currentTime = 0, duration = 0 }) {
  const mood = getMood(topic);
  const segmentEnergy = {
    origin: 0.62,
    acceleration: 0.78,
    hci: 0.5,
    agents: 0.72,
    evals: 0.86,
    approval: 0.58,
    multimodal: 0.68,
    superhuman: 0.94,
    haystacks: 0.74,
  }[topic.id] || 0.65;
  const speakerPulse = transcriptLog[activeIndex]?.speaker === "Josh" ? 1.08 : 0.92;
  const playFrac = duration > 0 ? Math.min(1, currentTime / duration) : 0;
  // Bars are positional — each carries a natural height (--bh), per-bar
  // duration (--bd) and delay (--bdelay); CSS keyframes drive the live pulse.
  const bars = useMemo(() => Array.from({ length: 168 }, (_, i) => {
    const broadWave = Math.abs(Math.sin((i + activeIndex * 5) * 0.12));
    const midWave = Math.abs(Math.sin((i * 0.43 + activeIndex * 0.7) * 0.43));
    const fineWave = Math.abs(Math.sin(i * 1.73 + activeIndex * 0.27));
    const phraseSpike = i % 17 === (activeIndex * 3) % 17 ? 1.32 : 1;
    const h = Math.max(7, (10 + broadWave * 22 + midWave * 18 + fineWave * 8) * segmentEnergy * speakerPulse * phraseSpike);
    return {
      h: Math.round(h * 10) / 10,
      d: 0.4 + (i % 7) * 0.06,
      delay: ((i % 11) * 0.07) % 1.0,
    };
  }), [activeIndex, topic.id]);
  return (
    <div className="relative flex h-14 w-full items-end gap-[2px] overflow-hidden bg-[#070A0E] px-2 py-2">
      <div
        className="pointer-events-none absolute inset-0 opacity-50"
        style={{ background: `linear-gradient(90deg, transparent 0%, ${mood.wash} 35%, ${mood.glow} 58%, transparent 100%)` }}
      />
      <div className="pointer-events-none absolute inset-x-2 top-1/2 h-px bg-white/10" />
      {bars.map((b, i) => {
        const bgColor =
          i / bars.length < playFrac
            ? (i % 8 === 0 ? mood.accent : i % 13 === 0 ? mood.node2 : "rgba(236,239,243,0.92)")
            : (i % 8 === 0 ? mood.accent + "99" : i % 13 === 0 ? mood.node2 + "99" : "rgba(236,239,243,0.38)");
        return (
          <div
            key={i}
            className={`cf-wave-bar relative z-10 min-w-[1px] flex-1 rounded-full ${playing ? "is-playing" : ""}`}
            style={{
              background: bgColor,
              "--bh": b.h + "px",
              "--bd": b.d + "s",
              "--bdelay": b.delay + "s",
            }}
          />
        );
      })}
    </div>
  );
}

// Editorial illustrated avatar — SVG portrait with CSS-driven animations.
// The motion-shim in this build strips framer-motion animate props, so all
// motion here is keyframe-driven (breathing, blinking, lip-sync, glow).
function JoshPortrait() {
  return (
    <svg viewBox="0 0 160 200" className="cf-avatar-svg" preserveAspectRatio="xMidYMid meet">
      <defs>
        <linearGradient id="cf-bg-josh" x1="0" y1="0" x2="0" y2="1">
          <stop offset="0" stopColor="#3D77A8" stopOpacity="0.92" />
          <stop offset="0.55" stopColor="#2A5680" stopOpacity="0.95" />
          <stop offset="1" stopColor="#1A3A5C" />
        </linearGradient>
        <radialGradient id="cf-face-josh" cx="0.5" cy="0.4" r="0.7">
          <stop offset="0" stopColor="#F0C9A9" />
          <stop offset="1" stopColor="#D9A982" />
        </radialGradient>
        <radialGradient id="cf-spot-josh" cx="0.5" cy="0.2" r="0.6">
          <stop offset="0" stopColor="rgba(255,255,255,0.25)" />
          <stop offset="1" stopColor="rgba(255,255,255,0)" />
        </radialGradient>
      </defs>
      <rect width="160" height="200" fill="url(#cf-bg-josh)" />
      <rect width="160" height="200" fill="url(#cf-spot-josh)" />
      {/* jacket */}
      <path d="M 0 200 L 0 178 Q 36 152 80 148 Q 124 152 160 178 L 160 200 Z" fill="#0F2740" />
      {/* shirt collar */}
      <path d="M 64 174 L 80 168 L 96 174 L 92 200 L 68 200 Z" fill="#E7ECF3" />
      <path d="M 80 168 L 80 192" stroke="#B8C3D2" strokeWidth="0.8" />
      {/* lapel notch */}
      <path d="M 56 200 L 80 180 L 104 200" fill="none" stroke="#06192C" strokeWidth="1.4" />
      {/* neck */}
      <path d="M 68 138 Q 68 158 70 168 L 90 168 Q 92 158 92 138 Z" fill="url(#cf-face-josh)" />
      <path d="M 68 156 Q 80 162 92 156 L 92 168 L 68 168 Z" fill="rgba(0,0,0,0.16)" />
      {/* ears */}
      <ellipse cx="48" cy="106" rx="6" ry="10" fill="#C99572" />
      <ellipse cx="112" cy="106" rx="6" ry="10" fill="#C99572" />
      {/* face */}
      <path d="M 50 96 Q 50 64 80 60 Q 110 64 110 96 Q 110 124 96 138 Q 80 148 64 138 Q 50 124 50 96 Z" fill="url(#cf-face-josh)" />
      {/* hair — short, side-swept */}
      <path d="M 49 92 Q 49 56 80 54 Q 112 56 112 92 Q 112 78 108 74 Q 96 64 80 66 Q 64 64 56 72 Q 50 80 49 92 Z" fill="#1B1410" />
      <path d="M 56 76 Q 70 64 90 66 Q 100 66 106 72 Q 96 70 82 72 Q 68 74 60 80 Z" fill="#2A1E18" opacity="0.7" />
      {/* brows */}
      <path d="M 60 92 Q 66 89 74 92" stroke="#2A1A12" strokeWidth="2.4" fill="none" strokeLinecap="round" />
      <path d="M 86 92 Q 94 89 100 92" stroke="#2A1A12" strokeWidth="2.4" fill="none" strokeLinecap="round" />
      {/* eyes */}
      <g className="cf-eyes">
        <ellipse cx="68" cy="102" rx="3.4" ry="3.8" fill="#FAFAF8" />
        <ellipse cx="92" cy="102" rx="3.4" ry="3.8" fill="#FAFAF8" />
        <ellipse cx="68.5" cy="102.5" rx="2.2" ry="2.6" fill="#3A2A1E" />
        <ellipse cx="92.5" cy="102.5" rx="2.2" ry="2.6" fill="#3A2A1E" />
        <circle cx="69" cy="101.5" r="0.8" fill="#FFF" />
        <circle cx="93" cy="101.5" r="0.8" fill="#FFF" />
      </g>
      {/* nose */}
      <path d="M 80 108 Q 78 118 76 124 Q 80 127 84 124 Q 82 118 80 108" fill="rgba(0,0,0,0.08)" />
      {/* mouth */}
      <g className="cf-mouth-group">
        <ellipse className="cf-mouth" cx="80" cy="132" rx="7" ry="2" fill="#8E4438" />
      </g>
      {/* subtle stubble shadow */}
      <ellipse cx="80" cy="136" rx="22" ry="6" fill="rgba(60,40,30,0.10)" />
    </svg>
  );
}

function KatPortrait() {
  return (
    <svg viewBox="0 0 160 200" className="cf-avatar-svg" preserveAspectRatio="xMidYMid meet">
      <defs>
        <linearGradient id="cf-bg-kat" x1="0" y1="0" x2="0" y2="1">
          <stop offset="0" stopColor="#9F86CA" stopOpacity="0.92" />
          <stop offset="0.55" stopColor="#7553C9" stopOpacity="0.92" />
          <stop offset="1" stopColor="#4A348A" />
        </linearGradient>
        <radialGradient id="cf-face-kat" cx="0.5" cy="0.4" r="0.7">
          <stop offset="0" stopColor="#F5D4BA" />
          <stop offset="1" stopColor="#E0B493" />
        </radialGradient>
        <radialGradient id="cf-spot-kat" cx="0.5" cy="0.2" r="0.6">
          <stop offset="0" stopColor="rgba(255,255,255,0.3)" />
          <stop offset="1" stopColor="rgba(255,255,255,0)" />
        </radialGradient>
      </defs>
      <rect width="160" height="200" fill="url(#cf-bg-kat)" />
      <rect width="160" height="200" fill="url(#cf-spot-kat)" />
      {/* blouse */}
      <path d="M 0 200 L 0 178 Q 36 152 80 150 Q 124 152 160 178 L 160 200 Z" fill="#F4EEDE" />
      {/* neckline V */}
      <path d="M 64 168 L 80 188 L 96 168" fill="none" stroke="#C9BCA1" strokeWidth="1.2" />
      <path d="M 64 168 Q 80 174 96 168 L 96 200 L 64 200 Z" fill="#E9DFC9" />
      {/* neck */}
      <path d="M 68 138 Q 68 158 70 168 L 90 168 Q 92 158 92 138 Z" fill="url(#cf-face-kat)" />
      <path d="M 68 156 Q 80 162 92 156 L 92 168 L 68 168 Z" fill="rgba(0,0,0,0.14)" />
      {/* hair back — shoulder length */}
      <path d="M 36 158 Q 30 100 52 70 Q 80 50 108 70 Q 130 100 124 158 Q 116 130 110 116 L 110 96 Q 110 70 80 66 Q 50 70 50 96 L 50 116 Q 44 130 36 158 Z" fill="#1B1410" />
      {/* ears */}
      <ellipse cx="48" cy="106" rx="5" ry="9" fill="#D49574" />
      <ellipse cx="112" cy="106" rx="5" ry="9" fill="#D49574" />
      {/* face */}
      <path d="M 52 96 Q 52 66 80 62 Q 108 66 108 96 Q 108 126 94 138 Q 80 148 66 138 Q 52 126 52 96 Z" fill="url(#cf-face-kat)" />
      {/* bangs / fringe */}
      <path d="M 50 88 Q 52 64 80 60 Q 108 64 110 88 Q 106 82 100 80 Q 90 76 80 78 Q 68 80 60 84 Q 54 86 50 88 Z" fill="#1B1410" />
      <path d="M 50 88 Q 64 96 86 92 Q 100 88 110 88 Q 106 78 96 76 Q 80 72 64 78 Q 54 82 50 88 Z" fill="#1B1410" />
      {/* cheek blush */}
      <ellipse cx="62" cy="118" rx="6" ry="4" fill="#E89A8A" opacity="0.42" />
      <ellipse cx="98" cy="118" rx="6" ry="4" fill="#E89A8A" opacity="0.42" />
      {/* brows */}
      <path d="M 60 96 Q 66 93 74 96" stroke="#2A1A12" strokeWidth="2.2" fill="none" strokeLinecap="round" />
      <path d="M 86 96 Q 94 93 100 96" stroke="#2A1A12" strokeWidth="2.2" fill="none" strokeLinecap="round" />
      {/* eyes */}
      <g className="cf-eyes">
        <ellipse cx="68" cy="106" rx="3.6" ry="4" fill="#FAFAF8" />
        <ellipse cx="92" cy="106" rx="3.6" ry="4" fill="#FAFAF8" />
        <ellipse cx="68.5" cy="106.5" rx="2.4" ry="2.8" fill="#3A2418" />
        <ellipse cx="92.5" cy="106.5" rx="2.4" ry="2.8" fill="#3A2418" />
        <circle cx="69" cy="105.5" r="0.9" fill="#FFF" />
        <circle cx="93" cy="105.5" r="0.9" fill="#FFF" />
        {/* lashes */}
        <path d="M 64 103 Q 68 102 72 103" stroke="#1B1410" strokeWidth="1.2" fill="none" strokeLinecap="round" />
        <path d="M 88 103 Q 92 102 96 103" stroke="#1B1410" strokeWidth="1.2" fill="none" strokeLinecap="round" />
      </g>
      {/* nose */}
      <path d="M 80 112 Q 78 122 76 128 Q 80 131 84 128 Q 82 122 80 112" fill="rgba(0,0,0,0.07)" />
      {/* mouth — lipstick */}
      <g className="cf-mouth-group">
        <ellipse className="cf-mouth" cx="80" cy="136" rx="6.5" ry="2.4" fill="#B94C61" />
      </g>
      <path d="M 74 136 Q 80 134 86 136" stroke="#8E2A40" strokeWidth="0.6" fill="none" opacity="0.5" />
    </svg>
  );
}

// ============================================================================
// ALTERNATE AVATAR STYLES
// Each preserves the .cf-eyes and .cf-mouth animation hooks so blink + lip-sync
// continue to work. Tabs on top let the reader switch styles live.
// ============================================================================

// — Style 2: Editorial line-art. Single-weight ink strokes on warm paper.
function JoshPortraitLine() {
  return (
    <svg viewBox="0 0 160 200" className="cf-avatar-svg" preserveAspectRatio="xMidYMid meet">
      <defs>
        <pattern id="cf-line-hatch-josh" patternUnits="userSpaceOnUse" width="3" height="3" patternTransform="rotate(35)">
          <line x1="0" y1="0" x2="0" y2="3" stroke="#1c1b17" strokeWidth="1.2" />
        </pattern>
      </defs>
      <rect width="160" height="200" fill="#EFE6D2" />
      {/* margin rule, like a magazine plate */}
      <rect x="6" y="6" width="148" height="188" fill="none" stroke="#1c1b17" strokeWidth="0.6" opacity="0.5" />
      {/* jacket */}
      <path d="M 18 200 L 22 168 Q 50 152 80 150 Q 110 152 138 168 L 142 200 Z" fill="none" stroke="#1c1b17" strokeWidth="1.6" />
      <path d="M 68 170 L 80 188 L 92 170" fill="none" stroke="#1c1b17" strokeWidth="1.2" />
      <path d="M 60 200 L 80 178 L 100 200" fill="none" stroke="#1c1b17" strokeWidth="1.2" />
      {/* neck */}
      <path d="M 68 140 Q 68 160 70 168 L 90 168 Q 92 160 92 140" fill="none" stroke="#1c1b17" strokeWidth="1.4" />
      {/* face */}
      <path d="M 52 96 Q 52 64 80 60 Q 108 64 108 96 Q 108 124 96 138 Q 80 150 64 138 Q 52 124 52 96 Z" fill="#EFE6D2" stroke="#1c1b17" strokeWidth="1.6" />
      {/* hair — hatched mass */}
      <path d="M 50 92 Q 49 56 80 54 Q 111 56 110 92 Q 108 78 100 72 Q 88 64 80 66 Q 64 64 56 74 Q 51 82 50 92 Z" fill="url(#cf-line-hatch-josh)" stroke="#1c1b17" strokeWidth="1.5" />
      {/* ears */}
      <path d="M 48 100 Q 44 106 48 116" fill="none" stroke="#1c1b17" strokeWidth="1.4" />
      <path d="M 112 100 Q 116 106 112 116" fill="none" stroke="#1c1b17" strokeWidth="1.4" />
      {/* brows */}
      <path d="M 60 93 Q 66 90 74 93" stroke="#1c1b17" strokeWidth="1.8" fill="none" strokeLinecap="round" />
      <path d="M 86 93 Q 94 90 100 93" stroke="#1c1b17" strokeWidth="1.8" fill="none" strokeLinecap="round" />
      {/* eyes */}
      <g className="cf-eyes">
        <circle cx="68" cy="103" r="2.6" fill="#1c1b17" />
        <circle cx="92" cy="103" r="2.6" fill="#1c1b17" />
      </g>
      {/* nose — single line */}
      <path d="M 80 110 L 78 124 Q 80 126 82 124" stroke="#1c1b17" strokeWidth="1.2" fill="none" strokeLinecap="round" />
      {/* mouth */}
      <g className="cf-mouth-group">
        <path className="cf-mouth" d="M 73 134 Q 80 136 87 134" stroke="#1c1b17" strokeWidth="1.6" fill="none" strokeLinecap="round" />
      </g>
      {/* stubble dots */}
      <g fill="#1c1b17" opacity="0.45">
        {Array.from({ length: 24 }, (_, i) => {
          const x = 64 + (i % 6) * 5;
          const y = 138 + Math.floor(i / 6) * 3;
          return <circle key={i} cx={x} cy={y} r="0.5" />;
        })}
      </g>
    </svg>
  );
}
function KatPortraitLine() {
  return (
    <svg viewBox="0 0 160 200" className="cf-avatar-svg" preserveAspectRatio="xMidYMid meet">
      <defs>
        <pattern id="cf-line-hatch-kat" patternUnits="userSpaceOnUse" width="3" height="3" patternTransform="rotate(-35)">
          <line x1="0" y1="0" x2="0" y2="3" stroke="#1c1b17" strokeWidth="1.2" />
        </pattern>
      </defs>
      <rect width="160" height="200" fill="#EFE6D2" />
      <rect x="6" y="6" width="148" height="188" fill="none" stroke="#1c1b17" strokeWidth="0.6" opacity="0.5" />
      {/* blouse */}
      <path d="M 16 200 L 22 172 Q 50 154 80 152 Q 110 154 138 172 L 144 200 Z" fill="none" stroke="#1c1b17" strokeWidth="1.6" />
      <path d="M 66 168 L 80 188 L 94 168" fill="none" stroke="#1c1b17" strokeWidth="1.2" />
      {/* neck */}
      <path d="M 68 140 Q 68 160 70 168 L 90 168 Q 92 160 92 140" fill="none" stroke="#1c1b17" strokeWidth="1.4" />
      {/* hair back — hatched */}
      <path d="M 38 168 Q 30 100 52 70 Q 80 50 108 70 Q 130 100 122 168 Q 116 132 110 116 L 110 96 Q 110 70 80 66 Q 50 70 50 96 L 50 116 Q 44 132 38 168 Z" fill="url(#cf-line-hatch-kat)" stroke="#1c1b17" strokeWidth="1.5" />
      {/* face */}
      <path d="M 54 96 Q 54 66 80 62 Q 106 66 106 96 Q 106 126 94 138 Q 80 150 66 138 Q 54 126 54 96 Z" fill="#EFE6D2" stroke="#1c1b17" strokeWidth="1.6" />
      {/* bangs */}
      <path d="M 52 88 Q 54 64 80 60 Q 106 64 108 88 Q 96 80 80 80 Q 64 80 52 88 Z" fill="url(#cf-line-hatch-kat)" stroke="#1c1b17" strokeWidth="1.5" />
      {/* ears */}
      <path d="M 48 102 Q 46 110 50 118" fill="none" stroke="#1c1b17" strokeWidth="1.4" />
      <path d="M 112 102 Q 114 110 110 118" fill="none" stroke="#1c1b17" strokeWidth="1.4" />
      {/* brows */}
      <path d="M 60 98 Q 66 95 74 98" stroke="#1c1b17" strokeWidth="1.6" fill="none" strokeLinecap="round" />
      <path d="M 86 98 Q 94 95 100 98" stroke="#1c1b17" strokeWidth="1.6" fill="none" strokeLinecap="round" />
      {/* eyes */}
      <g className="cf-eyes">
        <circle cx="68" cy="107" r="2.6" fill="#1c1b17" />
        <circle cx="92" cy="107" r="2.6" fill="#1c1b17" />
        <path d="M 63 103 Q 68 101 73 103" stroke="#1c1b17" strokeWidth="1.2" fill="none" />
        <path d="M 87 103 Q 92 101 97 103" stroke="#1c1b17" strokeWidth="1.2" fill="none" />
      </g>
      {/* nose */}
      <path d="M 80 114 L 78 128 Q 80 130 82 128" stroke="#1c1b17" strokeWidth="1.2" fill="none" strokeLinecap="round" />
      {/* mouth */}
      <g className="cf-mouth-group">
        <path className="cf-mouth" d="M 73 138 Q 80 141 87 138" stroke="#1c1b17" strokeWidth="2" fill="none" strokeLinecap="round" />
      </g>
      {/* earring */}
      <circle cx="48" cy="120" r="1.6" fill="#1c1b17" />
      <circle cx="112" cy="120" r="1.6" fill="#1c1b17" />
    </svg>
  );
}

// — Style 3: Risograph. Two-tone halftone overprint, slightly off-register.
function RisoPortrait({ skin, accent, ink, hair, blouse, isKat }) {
  const dotId = `cf-riso-dot-${isKat ? 'kat' : 'josh'}`;
  return (
    <svg viewBox="0 0 160 200" className="cf-avatar-svg" preserveAspectRatio="xMidYMid meet">
      <defs>
        <pattern id={dotId} patternUnits="userSpaceOnUse" width="4" height="4">
          <circle cx="2" cy="2" r="0.9" fill={ink} opacity="0.55" />
        </pattern>
      </defs>
      {/* paper */}
      <rect width="160" height="200" fill="#F2EAD3" />
      {/* off-register shadow plate — accent shifted up-left */}
      <g transform="translate(-2.5,-2)" opacity="0.85">
        <path d="M 0 200 L 0 174 Q 36 150 80 148 Q 124 150 160 174 L 160 200 Z" fill={accent} />
        <circle cx="80" cy="100" r="42" fill={accent} />
        {!isKat
          ? <path d="M 50 90 Q 49 56 80 54 Q 111 56 110 92 Q 96 70 80 68 Q 64 70 50 92 Z" fill={hair} />
          : <path d="M 36 158 Q 30 100 52 70 Q 80 50 108 70 Q 130 100 124 158 Q 116 130 110 116 L 110 96 Q 110 70 80 66 Q 50 70 50 96 L 50 116 Q 44 130 36 158 Z" fill={hair} />
        }
      </g>
      {/* ink plate */}
      {/* shoulders */}
      <path d="M 0 200 L 0 174 Q 36 150 80 148 Q 124 150 160 174 L 160 200 Z" fill={blouse} />
      <path d="M 0 200 L 0 174 Q 36 150 80 148 Q 124 150 160 174 L 160 200 Z" fill={`url(#${dotId})`} />
      {/* face */}
      <circle cx="80" cy="100" r="42" fill={skin} />
      <circle cx="80" cy="100" r="42" fill={`url(#${dotId})`} opacity="0.55" />
      {/* hair */}
      {!isKat
        ? <path d="M 50 90 Q 49 56 80 54 Q 111 56 110 92 Q 96 70 80 68 Q 64 70 50 92 Z" fill={hair} />
        : <g>
            <path d="M 36 158 Q 30 100 52 70 Q 80 50 108 70 Q 130 100 124 158 Q 116 130 110 116 L 110 96 Q 110 70 80 66 Q 50 70 50 96 L 50 116 Q 44 130 36 158 Z" fill={hair} />
            <path d="M 50 88 Q 64 96 86 92 Q 100 88 110 88 Q 106 78 96 76 Q 80 72 64 78 Q 54 82 50 88 Z" fill={hair} />
          </g>
      }
      {/* hair halftone */}
      {!isKat
        ? <path d="M 50 90 Q 49 56 80 54 Q 111 56 110 92 Q 96 70 80 68 Q 64 70 50 92 Z" fill={`url(#${dotId})`} opacity="0.4" />
        : <path d="M 36 158 Q 30 100 52 70 Q 80 50 108 70 Q 130 100 124 158 L 110 96 Q 110 70 80 66 Q 50 70 50 96 Z" fill={`url(#${dotId})`} opacity="0.4" />
      }
      {/* brows */}
      <path d={isKat ? "M 60 96 Q 66 93 74 96" : "M 60 92 Q 66 89 74 92"} stroke={ink} strokeWidth="2.4" fill="none" strokeLinecap="round" />
      <path d={isKat ? "M 86 96 Q 94 93 100 96" : "M 86 92 Q 94 89 100 92"} stroke={ink} strokeWidth="2.4" fill="none" strokeLinecap="round" />
      {/* eyes */}
      <g className="cf-eyes">
        <ellipse cx="68" cy={isKat ? 106 : 102} rx="3.4" ry="3.8" fill={ink} />
        <ellipse cx="92" cy={isKat ? 106 : 102} rx="3.4" ry="3.8" fill={ink} />
      </g>
      {/* nose triangle */}
      <path d={isKat ? "M 80 114 L 76 128 L 84 128 Z" : "M 80 110 L 76 124 L 84 124 Z"} fill={ink} opacity="0.35" />
      {/* mouth */}
      <g className="cf-mouth-group">
        <ellipse className="cf-mouth" cx="80" cy={isKat ? 136 : 132} rx={isKat ? 7 : 7.5} ry="2.4" fill={ink} />
      </g>
      {/* small accent geom */}
      <rect x="18" y="18" width="14" height="2" fill={accent} />
      <rect x="128" y="180" width="14" height="2" fill={accent} />
    </svg>
  );
}
function JoshPortraitRiso() {
  return <RisoPortrait skin="#F2C7A0" accent="#E36A35" ink="#1F2C4A" hair="#1F2C4A" blouse="#1F2C4A" isKat={false} />;
}
function KatPortraitRiso() {
  return <RisoPortrait skin="#F6D3B8" accent="#E66A8C" ink="#3F2570" hair="#3F2570" blouse="#F2EAD3" isKat={true} />;
}

// — Style 4: Geometric / Bauhaus. Abstract face from primitive shapes.
function GeoPortrait({ palette, isKat }) {
  const { bg, face, hair, accent, ink, shirt } = palette;
  return (
    <svg viewBox="0 0 160 200" className="cf-avatar-svg" preserveAspectRatio="xMidYMid meet">
      <rect width="160" height="200" fill={bg} />
      {/* big shapes — Bauhaus composition */}
      <circle cx="38" cy="38" r="22" fill={accent} opacity="0.85" />
      <rect x="118" y="14" width="28" height="28" fill={ink} />
      <path d="M 0 200 L 30 170 L 0 170 Z" fill={accent} opacity="0.6" />
      {/* shoulders block */}
      <rect x="20" y="160" width="120" height="40" fill={shirt} />
      <path d="M 60 160 L 80 184 L 100 160 L 100 200 L 60 200 Z" fill={ink} />
      {/* head as circle */}
      <circle cx="80" cy="100" r="44" fill={face} />
      {/* hair — half circle */}
      {!isKat
        ? <path d="M 36 100 A 44 44 0 0 1 124 100 L 124 84 Q 80 64 36 84 Z" fill={hair} />
        : <g>
            <circle cx="80" cy="100" r="48" fill={hair} />
            <circle cx="80" cy="108" r="40" fill={face} />
          </g>
      }
      {/* cheek dot for warmth */}
      <circle cx="60" cy="118" r="4" fill={accent} opacity="0.55" />
      <circle cx="100" cy="118" r="4" fill={accent} opacity="0.55" />
      {/* brow lines */}
      <line x1="60" y1="92" x2="74" y2="92" stroke={ink} strokeWidth="3" strokeLinecap="round" />
      <line x1="86" y1="92" x2="100" y2="92" stroke={ink} strokeWidth="3" strokeLinecap="round" />
      {/* eyes — small circles */}
      <g className="cf-eyes">
        <circle cx="68" cy="104" r="3" fill={ink} />
        <circle cx="92" cy="104" r="3" fill={ink} />
      </g>
      {/* nose — triangle */}
      <path d="M 80 110 L 76 124 L 84 124 Z" fill={ink} opacity="0.85" />
      {/* mouth — rectangle */}
      <g className="cf-mouth-group">
        <rect className="cf-mouth" x="73" y="131" width="14" height="4" fill={accent} />
      </g>
      {/* signature rule */}
      <line x1="14" y1="190" x2="40" y2="190" stroke={ink} strokeWidth="2" />
    </svg>
  );
}
function JoshPortraitGeo() {
  return <GeoPortrait isKat={false} palette={{ bg: "#1F3B5E", face: "#F0C49C", hair: "#10131A", accent: "#E8B83D", ink: "#0B0E14", shirt: "#0B0E14" }} />;
}
function KatPortraitGeo() {
  return <GeoPortrait isKat={true} palette={{ bg: "#7E5BB8", face: "#F5D2BB", hair: "#0B0E14", accent: "#E36A8B", ink: "#0B0E14", shirt: "#F2EAD3" }} />;
}

const AVATAR_STYLES = [
  { id: "realistic", label: "Realistic" },
  { id: "line",      label: "Line-art"  },
  { id: "riso",      label: "Risograph" },
  { id: "geo",       label: "Geometric" },
];

function PortraitFor({ person, style }) {
  const isJosh = person === "Josh";
  if (style === "line") return isJosh ? <JoshPortraitLine /> : <KatPortraitLine />;
  if (style === "riso") return isJosh ? <JoshPortraitRiso /> : <KatPortraitRiso />;
  if (style === "geo")  return isJosh ? <JoshPortraitGeo /> : <KatPortraitGeo />;
  return isJosh ? <JoshPortrait /> : <KatPortrait />;
}

function Avatar({ person, activeSpeaker, playing, avatarStyle }) {
  const isActive = person === activeSpeaker;
  const isSpeaking = isActive && Boolean(playing);
  const isJosh = person === "Josh";
  return (
    <div
      className={`cf-avatar ${isActive ? "is-active" : ""} ${isSpeaking ? "is-speaking" : ""} ${isJosh ? "is-josh" : "is-kat"}`}
    >
      <div className="cf-avatar-label">{person}</div>
      <div className="cf-avatar-frame">
        <PortraitFor person={person} style={avatarStyle} />
        <div className="cf-avatar-aura" aria-hidden="true" />
        {isActive ? <div className="cf-avatar-live" aria-hidden="true" /> : null}
      </div>
      <div className="cf-avatar-ground" aria-hidden="true" />
    </div>
  );
}

function AvatarStyleSwitcher({ value, onChange }) {
  return (
    <div className="cf-avatar-styler" role="tablist" aria-label="Portrait style">
      <span className="cf-avatar-styler-label">PORTRAIT STYLE</span>
      <div className="cf-avatar-styler-tabs">
        {AVATAR_STYLES.map((s) => (
          <button
            key={s.id}
            type="button"
            role="tab"
            aria-selected={value === s.id}
            className={`cf-avatar-styler-tab ${value === s.id ? "is-on" : ""}`}
            onClick={() => onChange(s.id)}
          >
            {s.label}
          </button>
        ))}
      </div>
    </div>
  );
}

function BroadcastVisual({ topic, playing }) {
  const mood = getMood(topic);
  const dots = useMemo(() => Array.from({ length: 26 }, (_, i) => ({ x: 7 + ((i * 37) % 86), y: 10 + ((i * 29) % 76), s: 2.5 + ((i * 7) % 7) })), [topic.id]);
  if (topic.visual === "curve") return <svg viewBox="0 0 600 230" className="h-full w-full"><defs><pattern id={`grid-${topic.id}`} width="28" height="28" patternUnits="userSpaceOnUse"><path d="M 28 0 L 0 0 0 28" fill="none" stroke="rgba(236,239,243,.075)" strokeWidth="1" /></pattern></defs><rect width="600" height="230" fill={`url(#grid-${topic.id})`} /><motion.path d="M24 188 C 105 185, 120 152, 174 145 S 254 112, 314 95 S 390 58, 462 45 S 536 34, 576 22" fill="none" stroke="rgba(236,239,243,.82)" strokeWidth="4" strokeLinecap="square" animate={{ pathLength: playing ? [0.35, 1, 0.75] : 0.85 }} transition={{ duration: 4, repeat: playing ? Infinity : 0 }} />{[65, 148, 238, 330, 438, 540].map((x, i) => <motion.rect key={x} x={x - 5} y={188 - i * 29 - 5} width="10" height="10" fill={i % 2 ? mood.node2 : mood.accent} animate={{ scale: playing ? [1, 1.6, 1] : 1, opacity: playing ? [0.45, 1, 0.65] : 0.75 }} transition={{ duration: 1.7, delay: i * 0.2, repeat: playing ? Infinity : 0 }} />)}</svg>;
  if (topic.visual === "approval") return <div className="grid h-full grid-cols-3 items-center gap-3">{[0, 1, 2].map((i) => <motion.div key={i} className="h-24 border bg-white/[.055]" style={{ borderColor: i === 1 ? mood.accent : "rgba(236,239,243,.18)" }} animate={{ y: playing ? [0, -8, 0] : 0, boxShadow: playing && i === 1 ? [`0 0 0 rgba(0,0,0,0)`, `0 0 28px ${mood.glow}`, `0 0 0 rgba(0,0,0,0)`] : "none" }} transition={{ duration: 2.2, delay: i * 0.35, repeat: playing ? Infinity : 0 }}><div className="m-3 h-2" style={{ background: i === 1 ? mood.accent : "rgba(236,239,243,.28)" }} /><div className="mx-3 h-12 border border-white/15" /></motion.div>)}</div>;
  if (topic.visual === "flow" || topic.visual === "pipeline") return <svg viewBox="0 0 600 230" className="h-full w-full"><defs><pattern id={`flow-grid-${topic.id}`} width="28" height="28" patternUnits="userSpaceOnUse"><path d="M 28 0 L 0 0 0 28" fill="none" stroke="rgba(236,239,243,.075)" strokeWidth="1" /></pattern></defs><rect width="600" height="230" fill={`url(#flow-grid-${topic.id})`} />{[70, 195, 320, 445].map((x, i) => <React.Fragment key={x}><motion.rect x={x} y={80} width="82" height="54" fill="rgba(255,255,255,.06)" stroke={i % 2 === 0 ? mood.accent : "rgba(236,239,243,.22)"} strokeWidth="1.2" animate={{ y: playing ? [80, 76, 80] : 80 }} transition={{ duration: 2.2, delay: i * 0.2, repeat: playing ? Infinity : 0 }} />{i < 3 && <motion.line x1={x + 82} y1={107} x2={x + 125} y2={107} stroke={mood.accent} strokeWidth="2" strokeDasharray="5 5" animate={{ pathLength: playing ? [0.2, 1, 0.4] : 0.8 }} transition={{ duration: 2.6, delay: i * 0.15, repeat: playing ? Infinity : 0 }} />}</React.Fragment>)}</svg>;
  if (topic.visual === "burst") return <svg viewBox="0 0 600 230" className="h-full w-full">{Array.from({ length: 18 }).map((_, i) => { const angle = (i / 18) * Math.PI * 2; const x2 = 300 + Math.cos(angle) * 95; const y2 = 115 + Math.sin(angle) * 70; return <motion.line key={i} x1="300" y1="115" x2={x2} y2={y2} stroke={i % 3 === 0 ? mood.accent : "rgba(236,239,243,.24)"} strokeWidth="2" animate={{ opacity: playing ? [0.25, 1, 0.35] : 0.45 }} transition={{ duration: 1.6 + i * 0.05, repeat: playing ? Infinity : 0 }} />; })}<motion.circle cx="300" cy="115" r="16" fill={mood.accent} animate={{ scale: playing ? [1, 1.25, 1] : 1 }} transition={{ duration: 1.5, repeat: playing ? Infinity : 0 }} /></svg>;
  return <svg viewBox="0 0 600 230" className="h-full w-full"><defs><pattern id={`network-grid-${topic.id}`} width="28" height="28" patternUnits="userSpaceOnUse"><path d="M 28 0 L 0 0 0 28" fill="none" stroke="rgba(236,239,243,.075)" strokeWidth="1" /></pattern><radialGradient id={`node-glow-${topic.id}`} cx="50%" cy="50%" r="50%"><stop offset="0%" stopColor={mood.accent} stopOpacity="1" /><stop offset="100%" stopColor={mood.accent} stopOpacity="0" /></radialGradient></defs><rect width="600" height="230" fill={`url(#network-grid-${topic.id})`} />{dots.slice(0, 13).map((d, i) => dots.slice(13).map((e, j) => <motion.line key={`${i}-${j}`} x1={d.x * 6} y1={d.y * 2.3} x2={e.x * 6} y2={e.y * 2.3} stroke={i % 4 === 0 ? mood.glow : "rgba(236,239,243,.135)"} strokeWidth="1" initial={{ pathLength: 0 }} animate={{ pathLength: playing ? [0.18, 1, 0.42] : 0.55 }} transition={{ duration: 4 + j, repeat: playing ? Infinity : 0 }} />))}{dots.map((d, i) => <motion.rect key={i} x={d.x * 6 - d.s / 2} y={d.y * 2.3 - d.s / 2} width={d.s} height={d.s} fill={i % 5 === 0 ? mood.accent : i % 3 === 0 ? mood.node2 : "rgba(236,239,243,.68)"} animate={{ opacity: playing ? [0.35, 1, 0.45] : 0.65, scale: playing && i % 5 === 0 ? [1, 1.55, 1] : 1 }} transition={{ duration: 2 + (i % 5), repeat: playing ? Infinity : 0 }} />)}<motion.circle cx="300" cy="118" r="34" fill={`url(#node-glow-${topic.id})`} animate={{ opacity: playing ? [0.08, 0.22, 0.08] : 0.12 }} transition={{ duration: 3.2, repeat: playing ? Infinity : 0 }} /></svg>;
}

function VisualStage({ topic, line, playing, audienceAnswer, onAudienceDismiss, avatarStyle }) {
  const mood = getMood(topic);
  const stageSpeaker = audienceAnswer ? "Kat" : line.speaker;

  return (
    <div className="relative min-h-[460px] overflow-hidden rounded-[1.35rem] border border-[var(--ink)] bg-[var(--ink)] p-3 shadow-[0_28px_70px_rgba(11,15,20,.24)]">
      <div
        className="absolute inset-0 opacity-25"
        style={{
          backgroundImage:
            "linear-gradient(rgba(236,239,243,.075) 1px, transparent 1px), linear-gradient(90deg, rgba(236,239,243,.075) 1px, transparent 1px)",
          backgroundSize: "32px 32px",
        }}
      />

      <motion.div
        className="absolute right-[-10%] top-[-35%] h-80 w-80 rounded-full blur-3xl"
        style={{ background: mood.glow }}
        animate={{ opacity: playing ? [0.16, 0.32, 0.16] : 0.2 }}
        transition={{ duration: 5, repeat: playing ? Infinity : 0 }}
      />
      <motion.div
        className="absolute bottom-[-34%] left-[-12%] h-72 w-72 rounded-full blur-3xl"
        style={{ background: mood.glow2 }}
        animate={{ opacity: playing ? [0.08, 0.2, 0.08] : 0.12 }}
        transition={{ duration: 6, repeat: playing ? Infinity : 0 }}
      />
      <div
        className="absolute inset-0 opacity-70"
        style={{
          background: `radial-gradient(circle at 52% 35%, ${mood.wash} 0%, transparent 48%), linear-gradient(135deg, transparent 0%, ${mood.glow2} 100%)`,
        }}
      />

      <div className="relative z-10 grid min-h-[300px] gap-3 rounded-[1.05rem] border border-white/[.10] bg-black/[.24] p-3 backdrop-blur md:grid-cols-[.9fr_1.1fr]">
        <div className="flex flex-col justify-between rounded-xl border border-white/[.08] bg-white/[.055] p-4 text-white shadow-inner">
          <div>
            <div className="mb-4 inline-flex items-center gap-2 rounded-md bg-white/[.10] px-2.5 py-1.5 font-mono text-[10px] uppercase tracking-[0.16em] text-white/[.60]">
              <Radio size={11} />
              <span>{audienceAnswer ? "Audience question" : line.time}</span>
            </div>

            <AnimatePresence mode="wait">
              <motion.div
                key={audienceAnswer ? "audience" : topic.id}
                initial={{ opacity: 0, y: 10 }}
                animate={{ opacity: 1, y: 0 }}
                exit={{ opacity: 0, y: -8 }}
              >
                <h2 className="max-w-md text-3xl font-semibold leading-[.98] tracking-[-0.04em] text-white drop-shadow-[0_2px_8px_rgba(0,0,0,0.45)] md:text-4xl">
                  {audienceAnswer ? "Kat answers live" : topic.title}
                </h2>
                <p className="mt-3 max-w-md text-sm leading-6 text-white/[.78]">
                  {audienceAnswer
                    ? "The interview pauses while Kat responds from the exchange."
                    : topic.summary}
                </p>
              </motion.div>
            </AnimatePresence>
          </div>

          </div>

        <div className="relative min-h-[210px] overflow-hidden rounded-xl border border-white/[.08] bg-black/[.28] p-3">
          <BroadcastVisual topic={topic} playing={playing && !audienceAnswer} />
          <div className="pointer-events-none absolute inset-0 rounded-xl ring-1 ring-white/[.08]" />
        </div>
      </div>

      <div className="relative z-20 -mt-12 flex min-h-[130px] items-end justify-between px-2 md:px-10">
        <Avatar person="Kat" activeSpeaker={stageSpeaker} playing={playing || Boolean(audienceAnswer)} avatarStyle={avatarStyle} />

        <AnimatePresence mode="wait">
          {audienceAnswer ? (
            <motion.div
              key="audience-answer"
              initial={{ opacity: 0, y: 14, scale: 0.97 }}
              animate={{ opacity: 1, y: 0, scale: 1 }}
              exit={{ opacity: 0, y: -10, scale: 0.98 }}
              transition={{ duration: 0.3 }}
              className="relative mb-3 max-w-md rounded-lg bg-[#E9FBF8] px-4 py-3 text-left text-[var(--ink)] shadow-[0_14px_34px_rgba(0,0,0,.24)]"
            >
              <div className="font-serif text-sm leading-6">{audienceAnswer}</div>
              <button
                onClick={onAudienceDismiss}
                className="mt-3 rounded-full bg-[#15958B] px-4 py-1.5 text-[11px] font-semibold text-white shadow-sm transition hover:bg-[#117E75]"
              >
                OK
              </button>
              <div className="absolute -left-2 bottom-8 h-4 w-4 rotate-45 bg-[#E9FBF8]" />
            </motion.div>
          ) : (
            <motion.div
              key={line.id}
              initial={{ opacity: 0, y: 8 }}
              animate={{ opacity: 1, y: 0 }}
              exit={{ opacity: 0, y: -8 }}
              className="mb-3 hidden max-w-sm rounded-lg bg-white/[.10] px-3 py-2.5 text-center text-white shadow-lg backdrop-blur md:block"
            >
              <div className="mb-1 font-mono text-[9px] uppercase tracking-[0.14em] text-white/[.45]">
                {line.speaker} · {line.time}
              </div>
              <div className="font-serif text-xs leading-5 text-white/[.90]">“{line.text}”</div>
            </motion.div>
          )}
        </AnimatePresence>

        <Avatar person="Josh" activeSpeaker={audienceAnswer ? "" : stageSpeaker} playing={playing && !audienceAnswer} avatarStyle={avatarStyle} />
      </div>
    </div>
  );
}

function Player({ topic, active, playing, audioReady, currentTime, duration, onSeek, prev, next, setPlaying }) {
  const mood = getMood(topic);
  const safeDuration = duration || timeToSeconds(transcriptLog[transcriptLog.length - 1].time);
  const progress = safeDuration ? Math.min(100, (currentTime / safeDuration) * 100) : Math.round(((active + 1) / transcriptLog.length) * 100);
  const waveRef = useRef(null);
  const [isDragging, setIsDragging] = useState(false);

  const seekFromEvent = (e) => {
    const el = waveRef.current; if (!el || !safeDuration) return;
    const r = el.getBoundingClientRect();
    const cx = (e.touches && e.touches[0] ? e.touches[0].clientX : e.clientX);
    const pct = Math.max(0, Math.min(1, (cx - r.left) / r.width));
    onSeek(pct * safeDuration);
  };
  const onDown = (e) => { setIsDragging(true); seekFromEvent(e); };
  const onMove = (e) => { if (isDragging) seekFromEvent(e); };
  const onUp   = () => setIsDragging(false);

  useEffect(() => {
    if (!isDragging) return;
    const m = (e) => seekFromEvent(e);
    const u = () => setIsDragging(false);
    window.addEventListener("mousemove", m);
    window.addEventListener("mouseup", u);
    window.addEventListener("touchmove", m, { passive: false });
    window.addEventListener("touchend", u);
    return () => {
      window.removeEventListener("mousemove", m);
      window.removeEventListener("mouseup", u);
      window.removeEventListener("touchmove", m);
      window.removeEventListener("touchend", u);
    };
  }, [isDragging, safeDuration]);

  // Compute transcript moment markers along the waveform
  const moments = transcriptLog.map((line, i) => ({
    i,
    time: timeToSeconds(line.time),
    pct: safeDuration ? (timeToSeconds(line.time) / safeDuration) * 100 : 0,
    speaker: line.speaker,
  }));

  return (
    <div className="rounded-[1.75rem] bg-[var(--ink)] p-4 text-[var(--paper)] shadow-xl">
      <div className="mb-3 flex items-center justify-between gap-3">
        <div className="flex items-center gap-2">
          <button onClick={prev} className="rounded-full bg-white/[.08] p-3 hover:bg-white/[.15]"><SkipBack size={16} /></button>
          <button onClick={() => setPlaying(!playing)} className="rounded-full bg-[var(--paper)] px-5 py-4 text-[var(--ink)] shadow-lg hover:scale-105">{playing ? <Pause size={20} /> : <Play size={20} />}</button>
          <button onClick={next} className="rounded-full bg-white/[.08] p-3 hover:bg-white/[.15]"><SkipForward size={16} /></button>
        </div>
        <div className="font-mono text-[10px] uppercase tracking-[0.16em] text-white/[.45]">
          <span>{formatSeconds(currentTime)}</span>
          <span className="mx-2">/</span>
          <span>{formatSeconds(safeDuration)}</span>
        </div>
      </div>

      <div
        ref={waveRef}
        className="cf-wave-scrub relative cursor-pointer select-none"
        onMouseDown={onDown}
        onMouseMove={onMove}
        onMouseUp={onUp}
        onTouchStart={onDown}
        role="slider"
        aria-label="Interview waveform · click or drag to seek"
        aria-valuemin={0}
        aria-valuemax={Math.round(safeDuration)}
        aria-valuenow={Math.round(currentTime)}
      >
        <Waveform playing={playing} activeIndex={active} topic={topic} currentTime={currentTime} duration={safeDuration} />

        {/* Played-portion overlay */}
        <div
          className="cf-wave-played pointer-events-none absolute inset-y-0 left-0"
          style={{ width: progress + "%", background: "linear-gradient(90deg, rgba(255,255,255,0.0), rgba(255,255,255,0.08))" }}
        />

        {/* Moment markers, synced to transcript */}
        <div className="cf-wave-markers pointer-events-none absolute inset-0">
          {moments.map((m) => (
            <div
              key={m.i}
              className={`cf-wave-marker absolute top-0 bottom-0 ${m.i === active ? "is-active" : ""}`}
              style={{
                left: m.pct + "%",
                width: m.i === active ? 2 : 1,
                background: m.i === active ? mood.accent : "rgba(236,239,243,0.18)",
              }}
            />
          ))}
        </div>

        {/* Playhead */}
        <div
          className="cf-wave-playhead pointer-events-none absolute top-0 bottom-0"
          style={{ left: progress + "%", width: 2, background: "var(--paper)", boxShadow: "0 0 12px rgba(255,255,255,0.6)" }}
        />
      </div>

      <div className="mt-2 flex items-center justify-between font-mono text-[9px] uppercase tracking-[0.14em] text-white/[.42]">
        <span>{transcriptLog[active].speaker} · {transcriptLog[active].time}</span>
        <span>Click or drag the waveform to seek</span>
      </div>
    </div>
  );
}

function JoinDialogue({ onAudienceAnswer }) {
  const [question, setQuestion] = useState("");
  const [turn, setTurn] = useState(0);
  const hasQuestion = question.trim().length > 0;

  const ask = () => {
    const nextTurn = turn + 1;
    const prompt = question.trim() || "What is the most important idea in this exchange?";
    const generated = sourceOnlyAnswer(prompt, nextTurn);
    setTurn(nextTurn);
    setQuestion("");
    if (onAudienceAnswer) onAudienceAnswer(generated);
  };

  return <div className="rounded-[2rem] bg-[#E9F2FF] p-5 shadow-[0_22px_55px_rgba(66,103,217,.16)]"><div className="mb-4 flex items-center gap-3"><div className="grid h-11 w-11 shrink-0 place-items-center rounded-full bg-[var(--signal)] text-white shadow-[0_8px_24px_rgba(66,103,217,.35)]"><Bot size={18} /></div><h3 className="text-2xl font-semibold tracking-[-0.03em]">Join the dialogue</h3></div><div className="rounded-[1.5rem] bg-white p-2 shadow-inner"><div className="flex gap-2"><input value={question} onChange={(e) => setQuestion(e.target.value)} onKeyDown={(e) => e.key === "Enter" && ask()} placeholder="Raise your hand to ask a question" className="min-h-[58px] w-full rounded-[1.2rem] bg-[#F8FBFF] px-4 py-3 text-base shadow-sm outline-none placeholder:text-[var(--muted)] focus:bg-white" /><button onClick={ask} className={`rounded-[1.2rem] px-5 py-3 text-white shadow-lg transition hover:scale-105 hover:opacity-90 active:scale-95 ${hasQuestion ? "bg-[#15958B]" : "bg-[var(--ink)]"}`}><Send size={18} /></button></div><div className="mt-2 flex flex-wrap gap-2 px-1 pb-1">{["What’s the big idea here?", "Why does this matter?", "How would you design the eval layer technically?"].map((sample) => <button key={sample} onClick={() => setQuestion(sample)} className="rounded-full bg-[#F3F7FF] px-3 py-1.5 text-xs text-[var(--slate)] transition hover:bg-[#DDEBFF] hover:text-[var(--signal)]">{sample}</button>)}</div></div></div>;
}

function TranscriptLog({ lines, activeIndex, setActive }) {
  const scrollRef = useRef(null);
  const scrollPosition = useRef(0);
  const [showFull, setShowFull] = useState(false);
  const jumpToLine = (i) => {
    if (scrollRef.current) scrollPosition.current = scrollRef.current.scrollTop;
    setActive(i);
    requestAnimationFrame(() => {
      if (scrollRef.current) scrollRef.current.scrollTop = scrollPosition.current;
    });
  };

  return (
    <div className="border border-[var(--rule)] bg-[var(--paper-warm)] p-4 shadow-sm">
      <div className="mb-3 flex items-center gap-2">
        <ScrollText size={16} />
        <h3 className="text-base font-semibold">Transcript log</h3>
        <button onClick={() => setShowFull(true)} className="ml-auto text-sm font-semibold text-[var(--signal)] underline-offset-4 hover:underline">
          Full Transcript
        </button>
      </div>

      <div ref={scrollRef} onScroll={(e) => { scrollPosition.current = e.currentTarget.scrollTop; }} className="max-h-[420px] overflow-y-auto pr-1 overscroll-contain">
        <div className="space-y-2">
          {lines.map((line, i) => {
            const isActive = i === activeIndex;
            return (
              <button key={line.id} onClick={() => jumpToLine(i)} className={`block w-full border px-3 py-2 text-left transition ${isActive ? "border-[var(--ink)] bg-white" : "border-transparent hover:border-[var(--rule)] hover:bg-white/70"}`}>
                <div className="text-sm leading-6 text-[var(--muted)]">
                  <span className="mr-2 font-mono text-[10px] uppercase tracking-[0.12em] text-[var(--muted)]">{line.time}</span>
                  <span className={`mr-2 ${isActive ? "font-bold text-[var(--ink)]" : "font-semibold text-[var(--graphite)]"}`}>{line.speaker}:</span>
                  <span className={isActive ? "font-bold text-[var(--ink)]" : ""}>{line.text}</span>
                </div>
              </button>
            );
          })}
        </div>
      </div>

      <AnimatePresence>
        {showFull && (
          <motion.div className="fixed inset-0 z-50 grid place-items-center bg-black/55 p-4 backdrop-blur-sm" initial={{ opacity: 0 }} animate={{ opacity: 1 }} exit={{ opacity: 0 }}>
            <motion.div initial={{ y: 28, opacity: 0, scale: 0.98 }} animate={{ y: 0, opacity: 1, scale: 1 }} exit={{ y: 18, opacity: 0, scale: 0.985 }} className="max-h-[86vh] w-full max-w-4xl overflow-hidden rounded-[2rem] bg-[#F4F6F9] shadow-[0_38px_100px_rgba(0,0,0,.34)]">
              <div className="relative overflow-hidden bg-[var(--ink)] px-7 py-7 text-center text-white">
                <div className="absolute inset-0 opacity-20" style={{ backgroundImage: "linear-gradient(rgba(236,239,243,.08) 1px, transparent 1px), linear-gradient(90deg, rgba(236,239,243,.08) 1px, transparent 1px)", backgroundSize: "26px 26px" }} />
                <div className="relative z-10 mx-auto max-w-2xl">
                  <div className="mb-2 font-mono text-[10px] uppercase tracking-[0.18em] text-white/[.45]">Coframe interview</div>
                  <h3 className="font-serif text-4xl leading-none tracking-[-0.04em] text-white md:text-5xl">Full Q&A Transcript</h3>
                  <p className="mx-auto mt-3 max-w-xl text-sm leading-6 text-white/[.62]">A clean reading view of Kat and Josh’s exchange, styled like an intelligence report dossier.</p>
                </div>
                <button onClick={() => setShowFull(false)} className="absolute right-5 top-5 rounded-full bg-white px-4 py-2 text-sm font-semibold text-[var(--ink)] shadow-sm transition hover:bg-[#E9FBF8]">
                  Close
                </button>
              </div>

              <div className="grid border-b border-[var(--hairline)] bg-white px-7 py-4 md:grid-cols-3">
                <div className="font-mono text-[10px] uppercase tracking-[0.14em] text-[var(--muted)]">40 moments</div>
                <div className="font-mono text-[10px] uppercase tracking-[0.14em] text-[var(--muted)]">Kat / Josh</div>
                <div className="font-mono text-[10px] uppercase tracking-[0.14em] text-[var(--muted)]">Living interfaces</div>
              </div>

              <div className="max-h-[58vh] overflow-y-auto px-5 py-5 md:px-8 md:py-7">
                <div className="mx-auto max-w-3xl space-y-4">
                  {lines.map((line) => {
                    const isKat = line.speaker === "Kat";
                    return (
                      <div key={`full-${line.id}`} className={`grid gap-3 rounded-2xl bg-white p-4 shadow-sm md:grid-cols-[92px_1fr] ${isKat ? "border-l-4 border-[#7553C9]" : "border-l-4 border-[#3D77A8]"}`}>
                        <div>
                          <div className="font-sans text-sm font-bold text-[var(--ink)]">{line.speaker}</div>
                          <div className="mt-1 font-mono text-[10px] uppercase tracking-[0.12em] text-[var(--muted)]">{line.time}</div>
                        </div>
                        <p className="font-serif text-lg leading-8 text-[var(--graphite)]">{line.text}</p>
                      </div>
                    );
                  })}
                </div>
              </div>
            </motion.div>
          </motion.div>
        )}
      </AnimatePresence>
    </div>
  );
}

function ConversationalContextMap({ activeTopicId, setActiveFromTopic }) {
  const graphNodes = [
    { id: "origin", label: "Living interfaces", x: 120, y: 115, r: 34 },
    { id: "hci", label: "Human-computer interaction", x: 245, y: 78, r: 28 },
    { id: "acceleration", label: "AI acceleration", x: 360, y: 108, r: 30 },
    { id: "agents", label: "Context + tools", x: 480, y: 84, r: 34 },
    { id: "evals", label: "Evaluation memory", x: 570, y: 170, r: 36 },
    { id: "approval", label: "Human judgment", x: 438, y: 238, r: 32 },
    { id: "multimodal", label: "Multimodal orchestration", x: 300, y: 236, r: 30 },
    { id: "superhuman", label: "Superhuman search", x: 165, y: 245, r: 38 },
    { id: "haystacks", label: "GTM workflows", x: 70, y: 185, r: 28 },
  ];

  const edges = [
    ["origin", "hci"],
    ["origin", "acceleration"],
    ["acceleration", "agents"],
    ["agents", "evals"],
    ["evals", "approval"],
    ["evals", "multimodal"],
    ["approval", "superhuman"],
    ["superhuman", "haystacks"],
    ["multimodal", "agents"],
    ["hci", "approval"],
    ["superhuman", "origin"],
  ];

  const nodeById = Object.fromEntries(graphNodes.map((node) => [node.id, node]));

  return (
    <div className="border border-[var(--rule)] bg-[var(--paper-warm)] p-4 shadow-sm">
      <div className="mb-4 border-b border-[var(--hairline)] pb-3">
        <h3 className="text-base font-semibold">Abstract knowledge graph</h3>
        <p className="mt-1 text-xs leading-5 text-[var(--muted)]">
          A semantic map of the interview’s ideas: concepts cluster by relationship, not chronology. Select a node to jump to its anchor moment.
        </p>
      </div>

      <div className="relative overflow-hidden bg-[var(--ink)] p-3 text-white">
        <div className="absolute inset-0 opacity-25" style={{ backgroundImage: "linear-gradient(rgba(236,239,243,.075) 1px, transparent 1px), linear-gradient(90deg, rgba(236,239,243,.075) 1px, transparent 1px)", backgroundSize: "28px 28px" }} />
        <svg viewBox="0 0 650 340" className="relative z-10 h-[360px] w-full">
          <defs>
            <filter id="softGlow" x="-50%" y="-50%" width="200%" height="200%">
              <feGaussianBlur stdDeviation="6" result="blur" />
              <feMerge>
                <feMergeNode in="blur" />
                <feMergeNode in="SourceGraphic" />
              </feMerge>
            </filter>
          </defs>

          {edges.map(([a, b], i) => {
            const from = nodeById[a];
            const to = nodeById[b];
            const active = activeTopicId === a || activeTopicId === b;
            return (
              <motion.line
                key={`${a}-${b}`}
                x1={from.x}
                y1={from.y}
                x2={to.x}
                y2={to.y}
                stroke={active ? "rgba(158,231,221,.72)" : "rgba(236,239,243,.16)"}
                strokeWidth={active ? 2.2 : 1}
                strokeDasharray={i % 3 === 0 ? "4 6" : ""}
                animate={{ opacity: active ? [0.55, 1, 0.55] : 0.45 }}
                transition={{ duration: 2.4, repeat: Infinity }}
              />
            );
          })}

          {graphNodes.map((node) => {
            const topic = findTopic(node.id);
            const mood = getMood(topic);
            const selected = activeTopicId === node.id;
            return (
              <g key={node.id} onClick={() => setActiveFromTopic(topic.quoteLineId)} className="cursor-pointer">
                <motion.circle
                  cx={node.x}
                  cy={node.y}
                  r={node.r + 13}
                  fill={mood.glow}
                  opacity={selected ? 0.55 : 0.22}
                  filter="url(#softGlow)"
                  animate={{ scale: selected ? [1, 1.08, 1] : 1 }}
                  transition={{ duration: 2, repeat: selected ? Infinity : 0 }}
                />
                <motion.circle
                  cx={node.x}
                  cy={node.y}
                  r={node.r}
                  fill={selected ? mood.accent : "rgba(236,239,243,.11)"}
                  stroke={selected ? "#9EE7DD" : mood.accent}
                  strokeWidth={selected ? 2.6 : 1.4}
                  animate={{ opacity: selected ? [0.88, 1, 0.88] : 0.8 }}
                  transition={{ duration: 1.8, repeat: Infinity }}
                />
                <circle cx={node.x - node.r * 0.18} cy={node.y - node.r * 0.22} r={Math.max(5, node.r * 0.18)} fill="rgba(255,255,255,.62)" />
                <text x={node.x} y={node.y + node.r + 18} textAnchor="middle" fontSize="11" fontWeight="700" fill="rgba(236,239,243,.92)">{node.label}</text>
                <text x={node.x} y={node.y + node.r + 33} textAnchor="middle" fontSize="9" fill="rgba(236,239,243,.48)">{transcriptLog.find((line) => line.id === topic.quoteLineId)?.time}</text>
              </g>
            );
          })}
        </svg>
      </div>
    </div>
  );
}

function CoframeInterviewPlayback() {
  const [active, setActive] = useState(0);
  const [playing, setPlaying] = useState(false);
  const [avatarStyle, setAvatarStyle] = useState("line");
  const [search, setSearch] = useState("");
  const [audienceAnswer, setAudienceAnswer] = useState("");
  const [audioSrc] = useState(AUDIO_SRC);
  const [audioReady, setAudioReady] = useState(false);
  const [currentTime, setCurrentTime] = useState(0);
  const [duration, setDuration] = useState(0);
  const audioRef = useRef(null);
  const resumeRef = useRef(false);
  const audienceTimer = useRef(null);
  const timer = useRef(null);
  const safeActive = clampIndex(active);
  const currentLine = transcriptLog[safeActive] || transcriptLog[0];
  const currentTopic = findTopic(currentLine.topicId);

  useEffect(() => {
    if (!playing || audienceAnswer || audioReady) return undefined;
    // No-audio fallback: drive a virtual clock so the player marker moves
    // smoothly along the waveform. The active transcript line is derived from
    // currentTime, so both advance together.
    const maxTime = timeToSeconds(transcriptLog[transcriptLog.length - 1].time) + 8;
    const rate = 16; // virtual seconds elapsed per real second
    const tickMs = 120;
    let last = (typeof performance !== "undefined" ? performance.now() : Date.now());
    timer.current = window.setInterval(() => {
      const now = (typeof performance !== "undefined" ? performance.now() : Date.now());
      const dt = (now - last) / 1000;
      last = now;
      setCurrentTime((t) => {
        let next = t + dt * rate;
        if (next > maxTime) next = 0;
        setActive(activeIndexForTime(next));
        return next;
      });
    }, tickMs);
    return () => {
      if (timer.current) window.clearInterval(timer.current);
    };
  }, [playing, audienceAnswer, audioReady]);

  useEffect(() => {
    const audio = audioRef.current;
    if (!audio || !audioReady) return undefined;
    if (playing && !audienceAnswer) {
      audio.play().catch(() => setPlaying(false));
    } else {
      audio.pause();
    }
    return undefined;
  }, [playing, audienceAnswer, audioReady]);

  useEffect(() => {
    return () => {
      if (audienceTimer.current) window.clearTimeout(audienceTimer.current);
      if (timer.current) window.clearInterval(timer.current);
      if (typeof window !== "undefined" && window.speechSynthesis) window.speechSynthesis.cancel();
    };
  }, []);

  const handleAudienceAnswer = (answer) => {
    resumeRef.current = playing;
    setPlaying(false);
    setAudienceAnswer(answer);
    if (typeof window !== "undefined" && window.speechSynthesis) window.speechSynthesis.cancel();
    if (audienceTimer.current) window.clearTimeout(audienceTimer.current);
  };

  const handleAudienceDismiss = () => {
    setAudienceAnswer("");
    if (resumeRef.current) setPlaying(true);
  };

  const syncToTime = (time) => {
    setCurrentTime(time);
    setActive(activeIndexForTime(time));
  };

  const seekToLine = (index) => {
    const safeIndex = clampIndex(index);
    const seconds = timeToSeconds(transcriptLog[safeIndex].time);
    setActive(safeIndex);
    setCurrentTime(seconds);
    if (audioRef.current && audioReady) audioRef.current.currentTime = seconds;
  };

  const handleSeek = (seconds) => {
    setCurrentTime(seconds);
    setActive(activeIndexForTime(seconds));
    if (audioRef.current && audioReady) audioRef.current.currentTime = seconds;
  };

  const next = () => seekToLine((active + 1) % transcriptLog.length);
  const prev = () => seekToLine((active - 1 + transcriptLog.length) % transcriptLog.length);

  return <div className="min-h-screen bg-[var(--paper)] p-4 text-[var(--ink)] md:p-6"><StyleGuide /><audio ref={audioRef} src={audioSrc || undefined} preload={audioSrc ? "auto" : "none"} onLoadedMetadata={(e) => { setAudioReady(true); setDuration(e.currentTarget.duration || 0); }} onTimeUpdate={(e) => syncToTime(e.currentTarget.currentTime)} onEnded={() => setPlaying(false)} onError={() => setAudioReady(false)} /><div className="mx-auto max-w-7xl"><header className="mb-4"><div className="mb-4 flex items-center gap-3"><LogoMark /><div className="font-mono text-[10px] uppercase tracking-[0.18em] text-[var(--muted)]">AI-native interview broadcast</div></div><div className="grid gap-5 border-y border-[var(--hairline)] py-5 lg:grid-cols-[1.1fr_.9fr]"><h1 className="max-w-5xl text-4xl font-bold leading-[.9] tracking-[-0.055em] md:text-7xl">Coframe Interview: <span className="font-serif italic font-normal">Living Interfaces</span></h1><p className="self-end text-base leading-7 text-[var(--slate)] md:text-lg">Kat and Josh trace how static websites become adaptive systems: interfaces that listen, test, learn, and invite humans back in at the moments where judgment matters.</p></div></header><main className="space-y-4"><AvatarStyleSwitcher value={avatarStyle} onChange={setAvatarStyle} /><VisualStage topic={currentTopic} line={currentLine} playing={playing} audienceAnswer={audienceAnswer} onAudienceDismiss={handleAudienceDismiss} avatarStyle={avatarStyle} /><Player topic={currentTopic} active={safeActive} playing={playing} audioReady={audioReady} currentTime={currentTime} duration={duration} onSeek={handleSeek} prev={prev} next={next} setPlaying={setPlaying} /><JoinDialogue onAudienceAnswer={handleAudienceAnswer} /><section className="grid gap-4 md:grid-cols-2"><TranscriptLog lines={transcriptLog} activeIndex={safeActive} setActive={seekToLine} /><ConversationalContextMap activeTopicId={currentTopic.id} setActiveFromTopic={seekToLine} /></section></main></div></div>;
}

// Mount into the report's Field Notes section.
(function mountCoframe() {
  function mount() {
    const el = document.getElementById('coframe-mount');
    if (!el) return false;
    if (el.dataset.mounted === '1') return true;
    el.dataset.mounted = '1';
    ReactDOM.createRoot(el).render(React.createElement(CoframeInterviewPlayback));
    return true;
  }
  if (!mount()) {
    document.addEventListener('DOMContentLoaded', mount);
  }
})();

})();
