// Counterfactual Dinner · embedded interactive
// Adapted from uploads/counterfactual-dinner_v1.jsx for AGI Intelligence Report v8.
// Changes from source:
//   - ES module import → React globals
//   - fetch(api.anthropic.com) → window.claude.complete (in-page, no API key)
//   - 'Inter' → 'DM Sans' (already loaded by the report)
//   - min-height/margin/padding stripped from .dinner-root so it embeds in a section
//   - Added ReactDOM mount at the bottom

(function () {
const { useState, useEffect, useRef, useCallback } = React;

// ============================================================================
// DINNER DATA
// ============================================================================

const ATTENDEES = {
  rocky: {
    name: "Rocky Yu",
    role: "Host",
    bio: "CEO of AGI House. Convenes the table and steers toward the hardest questions.",
    initials: "RY",
    accent: "#6B6258",
    stance: "Hosts the conversation. Names tensions, asks the questions everyone is dancing around.",
  },
  vp_lab: {
    name: "The Lab Director",
    role: "VP Research at a frontier lab",
    bio: "Co-architected a flagship foundation model. Bets on scale and end-to-end systems.",
    initials: "LD",
    accent: "#8B6F47",
    stance: "End-to-end scaling believer. Sees world models as implicit representations inside large neural networks. Skeptical that explicit structure is required.",
  },
  robotics_prof: {
    name: "The Robotics Professor",
    role: "Stanford faculty, vision & robotics",
    bio: "Spent a decade on physical intelligence. Lives the sim-to-real gap.",
    initials: "RP",
    accent: "#5D7B6F",
    stance: "Robotics-first. Argues explicit world models are necessary in data-scarce domains. Sim-to-real is the bottleneck no one has solved.",
  },
  ex_deepmind_ceo: {
    name: "The Veteran Founder",
    role: "CEO of a stealth agents startup",
    bio: "Twelve years at a top research lab. Now building the system around the model.",
    initials: "VF",
    accent: "#7B5D75",
    stance: "Harness-pilled. The biggest gains today come from better systems around the model, not better weights. Tool use, memory, orchestration.",
  },
  eval_ceo: {
    name: "The Eval Realist",
    role: "CEO of an evaluation infra company",
    bio: "Ex-Hugging Face. Spends her days measuring what models actually do.",
    initials: "ER",
    accent: "#9A6A47",
    stance: "Deployment realist. Real value is in constrained environments with clear feedback loops — insurance, coding, customer ops.",
  },
  architecture_og: {
    name: "The Architecture OG",
    role: "Founder, former Brain researcher",
    bio: "Was in the room for attention. Watches paradigms come and go.",
    initials: "AO",
    accent: "#4F6B83",
    stance: "Architecture-curious. Most labs continue scaling existing paradigms; the next leap may require new architectures, not bigger models.",
  },
};

// Pre-seeded beats for the opening course so playback starts instantly.
// These follow the same shape as API-generated beats: { speaker, text }.
const OPENING_BEATS = [
  {
    speaker: "rocky",
    text: "I want to start where the disagreement lives. Do we actually need explicit world models for AGI, or are end-to-end systems enough on their own?",
  },
  {
    speaker: "vp_lab",
    text: "The honest answer is that scale keeps working. You don't need to bolt structure onto a model that's already learning a world model implicitly inside its weights. The representation is there.",
  },
  {
    speaker: "robotics_prof",
    text: "That holds in domains where data is cheap. In robotics, you don't get to scale your way out of the sim-to-real gap. You need an actual model of physics the agent can simulate against.",
  },
  {
    speaker: "architecture_og",
    text: "We've been here before, by the way. The phrase \"world model\" came out of control theory in the seventies. State plus action equals future state. We rediscovered it, fragmented the meaning, and now half the field uses it for something different than the other half.",
  },
  {
    speaker: "vp_lab",
    text: "Sure, but the implicit version is doing real work. The model can predict, plan, recover from surprise. Whether you call that a world model or not is mostly taxonomic.",
  },
  {
    speaker: "robotics_prof",
    text: "It's taxonomic until you try to deploy. Then the question of whether the model knows the world or just knows the training distribution becomes the whole game.",
  },
];

const TOPICS = [
  {
    id: "what-is-wm",
    title: "What is a World Model — Really?",
    blurb: "The concept predates modern AI by decades. Today the definition has fragmented.",
    cast: ["rocky", "vp_lab", "robotics_prof", "architecture_og"],
    seed: "Rocky opens the question: do we need explicit world models, or are end-to-end systems enough? The Lab Director takes the implicit-representation side. The Robotics Professor pushes back from physical-AI experience. The Architecture OG threads the history of how the term has shifted.",
    openingBeats: OPENING_BEATS,
  },
  {
    id: "gaming",
    title: "Gaming as the First World Model Playground",
    blurb: "World models as a product — playable worlds — versus as a tool for training agents.",
    cast: ["rocky", "vp_lab", "ex_deepmind_ceo", "eval_ceo"],
    seed: "The conversation turns to gaming. The Lab Director frames it as fertile ground for both products and training. The Veteran Founder sees the harness opportunity. The Eval Realist asks what 'works' actually means in this context.",
  },
  {
    id: "robotics",
    title: "Robotics: Where World Models Matter Most",
    blurb: "Real-world data is scarce. Failures are costly. Sim-to-real remains unsolved.",
    cast: ["rocky", "robotics_prof", "vp_lab", "ex_deepmind_ceo"],
    seed: "The Robotics Professor walks through the bottleneck. The Lab Director challenges whether YouTube-scale video will eventually unlock it. The Veteran Founder argues the harness matters even more in physical domains.",
  },
  {
    id: "agents",
    title: "Agents: The System Around the Model",
    blurb: "Better systems, not better weights, deliver the gains today.",
    cast: ["rocky", "ex_deepmind_ceo", "vp_lab", "eval_ceo"],
    seed: "The Veteran Founder makes the case for the harness. The Lab Director concedes some ground but holds the line on model quality. The Eval Realist brings the deployment lens — what actually ships?",
  },
  {
    id: "what-works",
    title: "Where AI Actually Works Today",
    blurb: "Constrained environments with clear feedback loops and structured data.",
    cast: ["rocky", "eval_ceo", "ex_deepmind_ceo", "architecture_og"],
    seed: "The Eval Realist names the domains: insurance, coding, customer service, credit operations. The Veteran Founder agrees the harness shines here. The Architecture OG asks whether constrained wins generalize.",
  },
  {
    id: "timeline",
    title: "The AGI Timeline",
    blurb: "Digital AGI possibly within a year or two. Physical AGI is a different timeline. Compute is the bottleneck.",
    cast: ["rocky", "vp_lab", "robotics_prof", "architecture_og"],
    seed: "Rocky asks the timeline question. The Lab Director is bullish on digital AGI. The Robotics Professor is patient on physical. The Architecture OG flags the compute wall — energy, cooling, memory bandwidth.",
  },
  {
    id: "investment",
    title: "Investment Dynamics",
    blurb: "Back the best researchers in the most important categories.",
    cast: ["rocky", "eval_ceo", "ex_deepmind_ceo", "architecture_og"],
    seed: "The conversation turns to investors. The principle: technical leadership and category dominance, not monetization. But what counts as a category?",
  },
  {
    id: "unsolved",
    title: "The Unsolved Problems",
    blurb: "Cross-modality learning. Architecture vs. scaling. Self-improving systems.",
    cast: ["rocky", "architecture_og", "vp_lab", "robotics_prof"],
    seed: "Closing topic. The Architecture OG names architecture as the underexplored axis. The Lab Director defends scaling. The Robotics Professor returns to perception-cognition. No resolution — that's the point.",
  },
];

// Topics grouped into courses, for the menu display (matches the printed menu above).
const COURSES = [
  { label: "Course One · Appetizer", topicIds: ["what-is-wm", "gaming"] },
  { label: "Course Two · Main",       topicIds: ["robotics", "agents", "what-works"] },
  { label: "Course Three · Dessert",  topicIds: ["timeline", "investment"] },
  { label: "Digestif",                  topicIds: ["unsolved"] },
];

const GLOSSARY = {
  "world model": {
    term: "World Model",
    short: "A function that maps state + action → future state.",
    long: "Predates modern AI by decades. Originally from classical control theory. Today fragmented across three meanings: learned simulations of environments, implicit representations inside large neural networks, and domain-specific tools.",
  },
  "sim-to-real": {
    term: "Sim-to-real",
    short: "The gap between simulated training and real-world deployment.",
    long: "Remains unsolved in robotics. YouTube-scale video data hasn't translated into robotics breakthroughs. High-fidelity physics simulation is still a bottleneck.",
  },
  "harness": {
    term: "Harness",
    short: "The system around the model: tool use, memory, orchestration, human-in-the-loop.",
    long: "The emerging consensus from the dinner: the sweet spot is good enough models plus excellent infrastructure. The harness delivers more immediate value than improving model weights.",
  },
  "end-to-end": {
    term: "End-to-end systems",
    short: "Systems that learn everything implicitly via scale, without explicit structure.",
    long: "One camp at the dinner: end-to-end is enough — just scale. The other: explicit structure is necessary, especially in data-scarce domains like robotics.",
  },
  "scaling laws": {
    term: "Scaling laws",
    short: "Empirical regularities relating model size, data, and capability.",
    long: "New scaling laws for robotics are starting to emerge — but the field is years behind digital domains.",
  },
  "cross-modality": {
    term: "Cross-modality learning",
    short: "Bridging perception and cognition across modalities.",
    long: "Still unsolved. Video data doesn't meaningfully improve reasoning benchmarks. Named at the dinner as one of the hardest remaining problems.",
  },
  "self-improving": {
    term: "Self-improving systems",
    short: "AI accelerating its own progress.",
    long: "Automated research assistants, experiment generation, paper synthesis. AI can assist research but not replace human judgment — taste, problem selection, and intuition remain human advantages.",
  },
  "compute": {
    term: "Compute",
    short: "Energy, cooling, memory bandwidth, chip manufacturing — the real AGI bottleneck.",
    long: "Most compute today is spent on research, not production. Even with new chips, the wall is physical: energy and cooling.",
  },
  "AGI": {
    term: "AGI",
    short: "An agent that can learn from experience at a reasonable rate.",
    long: "The working definition from the dinner. Under this definition, digital AGI could arrive within this decade — possibly within a year or two. Physical AGI is a different timeline.",
  },
};

// Dish emojis cycled around the table — feels like a real spread.
const DISHES = ["🥖", "🥗", "🍷", "🧀", "🥘", "🍲", "🥧", "🍇"];

// ============================================================================
// API
// ============================================================================

async function callClaude(systemPrompt, userMessage, maxTokens = 1024) {
  // Uses the in-page Claude helper. No API key, no CORS — runs under the viewer's quota.
  // window.claude.complete supports a messages array but no separate system role,
  // so we concatenate the system prompt as a leading user instruction.
  const composed = systemPrompt + "\n\n" + userMessage;
  const text = await window.claude.complete({
    messages: [{ role: "user", content: composed }],
  });
  return text;
}
function buildCastBio(castIds) {
  return castIds
    .map((id) => {
      const a = ATTENDEES[id];
      return `${a.name} (${a.role}). Position: ${a.stance}`;
    })
    .join("\n");
}

async function generateTopicConversation(topic) {
  const castBio = buildCastBio(topic.cast);
  const system = `You are scripting a dinner conversation at AGI House between frontier-AI researchers and founders. Stay strictly in character — each guest speaks from the position attributed to them. Do not invent positions they did not hold.

Output a JSON array of 4-6 beats. Each beat is an object with keys "speaker" (one of: ${topic.cast.join(", ")}) and "text" (1-3 sentences, conversational, sharp, no hedging boilerplate, no generic AI-assistant phrasing). Speakers should disagree, build on, or push back on each other — but the tone is collegial, not combative. This is a thoughtful dinner, not a debate.

Use these terms naturally where they fit so they can be auto-highlighted: world model, sim-to-real, harness, end-to-end, scaling laws, cross-modality, self-improving, compute, AGI.

Cast for this topic:
${castBio}`;

  const user = `Topic: ${topic.title}
Seed: ${topic.seed}

Script the beats. JSON array only, no preamble, no markdown fences.`;

  const raw = await callClaude(system, user, 1500);
  const clean = raw.replace(/```json|```/g, "").trim();
  const match = clean.match(/\[[\s\S]*\]/);
  if (!match) throw new Error("No JSON array found");
  return JSON.parse(match[0]);
}

async function generateInterjectionResponse(topic, transcript, userQuestion) {
  const castBio = buildCastBio(topic.cast);
  const recentTranscript = transcript
    .slice(-8)
    .map((b) => `${ATTENDEES[b.speaker]?.name || "Guest"}: ${b.text}`)
    .join("\n");

  const system = `You are scripting a dinner conversation. A guest at the table just spoke up. 2-3 of the attendees respond, in character, in turn. Each response is 1-3 sentences. They engage directly with what the guest said — they may agree, disagree, redirect, or ask a follow-up. Tone is warm and collegial.

Output a JSON array of 2-3 beats. Each: { "speaker": "<id>", "text": "..." }. Speakers must be from: ${topic.cast.join(", ")}. No preamble, no markdown fences.

Cast:
${castBio}`;

  const user = `Topic in progress: ${topic.title}
Recent conversation:
${recentTranscript}

A guest at the table just said: "${userQuestion}"

Have 2-3 attendees respond. JSON only.`;

  const raw = await callClaude(system, user, 1000);
  const clean = raw.replace(/```json|```/g, "").trim();
  const match = clean.match(/\[[\s\S]*\]/);
  if (!match) throw new Error("No JSON array found");
  return JSON.parse(match[0]);
}

async function generateIntelligenceReport(userActions) {
  const system = `You are an editor at AGI House writing a personal "dinner notes" memo for a guest who just attended a conversation on World Models, Agents, and the Path to AGI. The memo is written in second person, addressed to the guest. It is restrained, observational, and intelligent — not flattering. It captures what the guest brought to the table, which topics drew them in, and what their interjections suggest about how they think.

Structure: a short opening (2-3 sentences), then 3-4 short paragraphs. No headers. No bullet points. Editorial magazine voice. 200-280 words.`;

  const summary = `The guest attended these topics: ${userActions.topicsVisited.join(", ")}.
They paused on these: ${userActions.paused.join(", ") || "none"}.
They asked: ${userActions.questions.map((q) => `"${q}"`).join("; ") || "no questions"}.
Glossary terms they opened: ${userActions.glossaryOpened.join(", ") || "none"}.`;

  const user = `Write the dinner notes for this guest.\n\n${summary}`;
  return await callClaude(system, user, 800);
}

// ============================================================================
// TABLE GEOMETRY
// ============================================================================

// Square-table seat positions. "you" sits at the top center. The cast for the
// current course is distributed across left / bottom / right edges. Number of
// guests ranges from 3 to 5 depending on the course.
function squareSeatPositions(total, cx, cy, s, offset) {
  const sides = [
    { name: "left",   start: { x: cx - s - offset, y: cy - s }, end: { x: cx - s - offset, y: cy + s } },
    { name: "bottom", start: { x: cx - s,         y: cy + s + offset }, end: { x: cx + s, y: cy + s + offset } },
    { name: "right",  start: { x: cx + s + offset, y: cy + s }, end: { x: cx + s + offset, y: cy - s } },
  ];

  // Distribute seats across sides as evenly as possible. Bottom gets first dibs
  // on extras since it's the focal point opposite "you".
  const base = Math.floor(total / 3);
  const remainder = total % 3;
  const counts = [base, base, base];
  const remainderOrder = [1, 0, 2]; // bottom, left, right
  for (let i = 0; i < remainder; i++) counts[remainderOrder[i]] += 1;

  const positions = [];
  for (let sideIdx = 0; sideIdx < 3; sideIdx++) {
    const n = counts[sideIdx];
    if (n === 0) continue;
    const side = sides[sideIdx];
    // Push seats outward toward the corners so adjacent seats have more
    // horizontal/vertical room between their caption labels.
    const padding = 0.14; // fraction of edge reserved at each end
    const inner = 1 - 2 * padding;
    for (let i = 0; i < n; i++) {
      const t = n === 1 ? 0.5 : padding + (i / (n - 1)) * inner;
      positions.push({
        x: side.start.x + (side.end.x - side.start.x) * t,
        y: side.start.y + (side.end.y - side.start.y) * t,
        side: side.name,
      });
    }
  }
  return positions;
}

// ============================================================================
// COMPONENT
// ============================================================================

const PLAYBACK_MS_PER_BEAT = 6000;

// SVG person silhouette inside the seat circle.
function PersonGlyph({ cx, cy, r }) {
  // Stylized person: small head + shoulder arc. Sized relative to seat radius.
  const headR = r * 0.32;
  const headCy = cy - r * 0.18;
  const shoulderTop = cy + r * 0.16;
  const shoulderBottomY = cy + r * 0.95;
  const shoulderHalfWidth = r * 0.72;
  return (
    <g style={{ pointerEvents: "none" }} opacity="0.92">
      <circle cx={cx} cy={headCy} r={headR} fill="#faf6ec" />
      <path
        d={`
          M ${cx - shoulderHalfWidth} ${shoulderBottomY}
          C ${cx - shoulderHalfWidth} ${shoulderTop},
            ${cx + shoulderHalfWidth} ${shoulderTop},
            ${cx + shoulderHalfWidth} ${shoulderBottomY}
          Z
        `}
        fill="#faf6ec"
      />
    </g>
  );
}

function CounterfactualDinner() {
  const [topicIndex, setTopicIndex] = useState(0);
  const [beats, setBeats] = useState([]);
  const [visibleCount, setVisibleCount] = useState(0);
  const [isPlaying, setIsPlaying] = useState(false);
  const [isLoadingTopic, setIsLoadingTopic] = useState(false);
  const [loadError, setLoadError] = useState(null);
  const [userQuestion, setUserQuestion] = useState("");
  const [isInterjecting, setIsInterjecting] = useState(false);
  const [glossaryOpen, setGlossaryOpen] = useState(null);
  const [notesOpen, setNotesOpen] = useState(false);
  const [generatedReport, setGeneratedReport] = useState(null);
  const [isGeneratingReport, setIsGeneratingReport] = useState(false);
  const [hoveredSeat, setHoveredSeat] = useState(null);
  const [mode, setMode] = useState("warm"); // 'warm' or 'light'

  // Prefetch cache: topic id -> beats array. Pre-populated for the opening.
  const prefetchRef = useRef({ [TOPICS[0].id]: TOPICS[0].openingBeats });
  const prefetchInflight = useRef({});

  const [userActions, setUserActions] = useState({
    topicsVisited: [],
    paused: [],
    questions: [],
    glossaryOpened: [],
  });

  const timerRef = useRef(null);
  const trailRef = useRef(null);
  const currentTopic = TOPICS[topicIndex];

  // Background prefetch for an arbitrary topic index.
  const prefetchTopic = useCallback(async (idx) => {
    if (idx < 0 || idx >= TOPICS.length) return;
    const topic = TOPICS[idx];
    if (prefetchRef.current[topic.id]) return;
    if (prefetchInflight.current[topic.id]) return;
    prefetchInflight.current[topic.id] = true;
    try {
      const newBeats = await generateTopicConversation(topic);
      prefetchRef.current[topic.id] = newBeats;
    } catch (e) {
      // swallow — we'll retry on demand if the user lands here
    } finally {
      prefetchInflight.current[topic.id] = false;
    }
  }, []);

  // Load a topic. Uses prefetch cache if available (instant); falls back to API.
  const loadTopic = useCallback(
    async (idx) => {
      setLoadError(null);
      setBeats([]);
      setVisibleCount(0);

      const topic = TOPICS[idx];
      const cached = prefetchRef.current[topic.id];

      if (cached) {
        // Instant path
        setBeats(cached);
        setIsPlaying(true);
        setIsLoadingTopic(false);
        // Kick off prefetch for the next course
        prefetchTopic(idx + 1);
        return;
      }

      // Cold path — generate now
      setIsLoadingTopic(true);
      try {
        const newBeats = await generateTopicConversation(topic);
        prefetchRef.current[topic.id] = newBeats;
        setBeats(newBeats);
        setIsPlaying(true);
        prefetchTopic(idx + 1);
      } catch (e) {
        setLoadError(`Couldn't load this course. ${e.message}`);
      } finally {
        setIsLoadingTopic(false);
      }
    },
    [prefetchTopic]
  );

  useEffect(() => {
    loadTopic(topicIndex);
    setUserActions((s) =>
      s.topicsVisited.includes(currentTopic.id)
        ? s
        : { ...s, topicsVisited: [...s.topicsVisited, currentTopic.id] }
    );
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [topicIndex]);

  // Playback timer
  useEffect(() => {
    if (!isPlaying || beats.length === 0) return;
    if (visibleCount >= beats.length) {
      timerRef.current = setTimeout(() => {
        if (topicIndex < TOPICS.length - 1) {
          setTopicIndex((i) => i + 1);
        } else {
          setIsPlaying(false);
        }
      }, PLAYBACK_MS_PER_BEAT);
      return () => clearTimeout(timerRef.current);
    }
    timerRef.current = setTimeout(() => {
      setVisibleCount((c) => c + 1);
    }, PLAYBACK_MS_PER_BEAT);
    return () => clearTimeout(timerRef.current);
  }, [isPlaying, visibleCount, beats, topicIndex]);

  useEffect(() => {
    if (trailRef.current) {
      trailRef.current.scrollTop = trailRef.current.scrollHeight;
    }
  }, [visibleCount, beats.length]);

  const handlePauseToggle = () => {
    if (isPlaying) {
      setUserActions((s) => ({ ...s, paused: [...s.paused, currentTopic.id] }));
    }
    setIsPlaying(!isPlaying);
  };

  const handleReplay = () => {
    setVisibleCount(0);
    setIsPlaying(true);
  };

  const handleJump = (idx) => setTopicIndex(idx);

  const handleInterject = async () => {
    if (!userQuestion.trim()) return;
    const q = userQuestion.trim();
    setUserQuestion("");
    setIsPlaying(false);
    setIsInterjecting(true);

    const userBeat = { speaker: "guest", text: q, isGuest: true };
    setBeats((prev) => [
      ...prev.slice(0, visibleCount),
      userBeat,
      ...prev.slice(visibleCount),
    ]);
    setVisibleCount((c) => c + 1);
    setUserActions((s) => ({ ...s, questions: [...s.questions, q] }));

    try {
      const transcriptForApi = beats.slice(0, visibleCount).concat([userBeat]);
      const responses = await generateInterjectionResponse(currentTopic, transcriptForApi, q);
      setBeats((prev) => {
        const userBeatIdx = prev.findIndex((b) => b.isGuest && b.text === q);
        if (userBeatIdx === -1) return [...prev, ...responses];
        return [
          ...prev.slice(0, userBeatIdx + 1),
          ...responses,
          ...prev.slice(userBeatIdx + 1),
        ];
      });
      setVisibleCount((c) => c + responses.length);
    } catch (e) {
      setLoadError(`The room couldn't respond. ${e.message}`);
    } finally {
      setIsInterjecting(false);
    }
  };

  const openGlossary = (key) => {
    setGlossaryOpen(key);
    setUserActions((s) =>
      s.glossaryOpened.includes(key) ? s : { ...s, glossaryOpened: [...s.glossaryOpened, key] }
    );
  };

  const openNotes = async () => {
    setNotesOpen(true);
    if (!generatedReport && userActions.topicsVisited.length > 0) {
      setIsGeneratingReport(true);
      try {
        const report = await generateIntelligenceReport(userActions);
        setGeneratedReport(report);
      } catch (e) {
        setGeneratedReport("We couldn't generate your notes just now. Try again after a few more topics.");
      } finally {
        setIsGeneratingReport(false);
      }
    }
  };

  const renderTextWithGlossary = (text) => {
    const sortedKeys = Object.keys(GLOSSARY).sort((a, b) => b.length - a.length);
    const parts = [];
    let remaining = text;
    let idCounter = 0;
    while (remaining.length > 0) {
      let matched = false;
      for (const key of sortedKeys) {
        const re = new RegExp(`\\b(${key.replace(/[-/\\^$*+?.()|[\]{}]/g, "\\$&")})\\b`, "i");
        const m = remaining.match(re);
        if (m && m.index === 0) {
          parts.push(
            <button
              key={`g-${idCounter++}`}
              onClick={() => openGlossary(key)}
              data-glossary-key={key}
              data-glossary-term={GLOSSARY[key].term}
              data-glossary-short={GLOSSARY[key].short}
              className="glossary-term"
            >
              {m[1]}
            </button>
          );
          remaining = remaining.slice(m[1].length);
          matched = true;
          break;
        }
        if (m && m.index !== undefined) {
          parts.push(<span key={`t-${idCounter++}`}>{remaining.slice(0, m.index)}</span>);
          parts.push(
            <button
              key={`g-${idCounter++}`}
              onClick={() => openGlossary(key)}
              data-glossary-key={key}
              data-glossary-term={GLOSSARY[key].term}
              data-glossary-short={GLOSSARY[key].short}
              className="glossary-term"
            >
              {m[1]}
            </button>
          );
          remaining = remaining.slice(m.index + m[1].length);
          matched = true;
          break;
        }
      }
      if (!matched) {
        parts.push(<span key={`t-${idCounter++}`}>{remaining}</span>);
        break;
      }
    }
    return parts;
  };

  const visibleBeats = beats.slice(0, visibleCount);
  const currentBeat = visibleBeats[visibleBeats.length - 1];
  const previousBeats = visibleBeats.slice(0, -1).slice(-3);
  const conversationFinished =
    visibleCount >= beats.length && topicIndex === TOPICS.length - 1 && !isPlaying;

  const activeSpeakerId = currentBeat?.isGuest ? "guest" : currentBeat?.speaker;

  // The dinner party is fixed: all six guests sit at the table every course,
  // in stable positions, full opacity. The course's cast just speaks more —
  // others are still present, listening.
  const seatedOrder = Object.keys(ATTENDEES);
  const seatPositions = squareSeatPositions(seatedOrder.length, 50, 52, 18, 7);
  const tableSeats = seatedOrder.map((id, i) => {
    const pos = seatPositions[i];
    return {
      id,
      attendee: ATTENDEES[id],
      x: pos.x,
      y: pos.y,
      side: pos.side,
      isSpeaking: activeSpeakerId === id,
    };
  });

  // Determine if the quote frame should be in compact (status) mode.
  const showingStatus = !currentBeat || isLoadingTopic || loadError;

  return (
    <div className={`dinner-root ${mode === "light" ? "light" : ""}`}>
      <style>{`
        .dinner-root {
          --bg: #f4ede0;
          --ink: #0a1626;
          --ink-soft: #2a3142;
          --ink-faint: #6b7387;
          --rule: #c5bca8;
          --accent: #8a6f3d;
          --accent-soft: #c9a96e;
          --paper: #faf6ec;
          --table: #c9b896;
          --table-edge: #a8956f;
          font-family: 'DM Sans', -apple-system, sans-serif;
          background: var(--bg);
          color: var(--ink);
          transition: background 0.3s ease, color 0.3s ease;
        }
        .dinner-root.light {
          --bg: #ffffff;
          --ink: #0a1626;
          --ink-soft: #2a3142;
          --ink-faint: #8a8e98;
          --rule: #e0dccc;
          --accent: #8a6f3d;
          --accent-soft: #c9a96e;
          --paper: #fbfaf7;
          --table: #ede4cc;
          --table-edge: #c9bc99;
        }
        .dinner-root * { box-sizing: border-box; }

        /* Mode toggle */
        .mode-toggle {
          background: transparent;
          border: 1px solid var(--rule);
          padding: 6px 10px;
          font-family: 'Fraunces', serif;
          font-style: italic;
          font-size: 12px;
          letter-spacing: 0.04em;
          color: var(--ink-soft);
          cursor: pointer;
          transition: all 0.2s ease;
          white-space: nowrap;
        }
        .mode-toggle:hover { color: var(--ink); border-color: var(--ink); }

        .dinner-container {
          max-width: 1280px;
          margin: 0 auto;
          padding: 28px 40px 80px;
        }

        /* Masthead — three columns: title, date/theme, notes button */
        .cd-masthead {
          display: grid;
          grid-template-columns: 1fr auto auto;
          align-items: end;
          gap: 32px;
          border-bottom: 1px solid var(--ink);
          padding-bottom: 14px;
          margin-bottom: 28px;
        }
        .cd-masthead-meta {
          font-size: 11px;
          letter-spacing: 0.14em;
          text-transform: uppercase;
          color: var(--ink-faint);
          margin-bottom: 4px;
        }
        .cd-masthead-title {
          font-family: 'Fraunces', serif;
          font-weight: 500;
          font-size: 22px;
          line-height: 1;
        }
        .cd-masthead-right {
          text-align: right;
          padding-right: 20px;
          border-right: 1px solid var(--rule);
          padding-bottom: 2px;
        }
        .cd-masthead-date {
          font-family: 'Fraunces', serif;
          font-size: 13px;
          letter-spacing: 0.04em;
          color: var(--ink);
          margin-bottom: 4px;
        }
        .cd-masthead-theme {
          font-family: 'Fraunces', serif;
          font-size: 12px;
          font-style: italic;
          color: var(--ink-faint);
          letter-spacing: 0.01em;
          max-width: 280px;
        }
        @media (max-width: 720px) {
          .cd-masthead { grid-template-columns: 1fr auto; }
          .cd-masthead-right { display: none; }
        }
        .cd-masthead-actions {
          display: flex;
          gap: 10px;
          align-items: center;
        }
        .notes-button {
          background: transparent;
          border: 1px solid var(--ink);
          padding: 8px 14px;
          font-family: 'Fraunces', serif;
          font-size: 13px;
          cursor: pointer;
          color: var(--ink);
          font-style: italic;
          transition: all 0.2s ease;
          white-space: nowrap;
        }
        .notes-button:hover { background: var(--ink); color: var(--paper); }
        .notes-button .indicator {
          display: inline-block;
          width: 6px;
          height: 6px;
          background: var(--accent);
          border-radius: 50%;
          margin-right: 8px;
          vertical-align: middle;
        }

        .topic-header { margin-bottom: 20px; }
        .topic-eyebrow {
          font-size: 10px;
          letter-spacing: 0.18em;
          text-transform: uppercase;
          color: var(--ink-faint);
          margin-bottom: 8px;
        }
        .topic-title {
          font-family: 'Fraunces', serif;
          font-weight: 400;
          font-size: 42px;
          line-height: 1.05;
          letter-spacing: -0.025em;
          margin: 0 0 12px 0;
        }
        .topic-blurb {
          font-family: 'Fraunces', serif;
          font-style: italic;
          font-size: 17px;
          line-height: 1.45;
          color: var(--ink-soft);
          max-width: 640px;
          margin: 0;
          font-weight: 300;
        }

        .interject { margin: 24px 0 16px; }
        .interject-label {
          font-size: 10px;
          letter-spacing: 0.18em;
          text-transform: uppercase;
          color: var(--ink-faint);
          margin-bottom: 8px;
        }
        .interject-row { display: flex; align-items: stretch; }
        .interject-input {
          flex: 1;
          background: var(--paper);
          border: 1px solid var(--ink);
          padding: 14px 16px;
          font-family: 'Fraunces', serif;
          font-size: 16px;
          font-style: italic;
          color: var(--ink);
          outline: none;
        }
        .interject-input::placeholder { color: var(--ink-faint); }
        .interject-btn {
          background: var(--ink);
          color: var(--paper);
          border: 1px solid var(--ink);
          border-left: none;
          padding: 0 24px;
          font-family: 'Fraunces', serif;
          font-style: italic;
          font-size: 14px;
          cursor: pointer;
          transition: background 0.15s;
        }
        .interject-btn:hover { background: var(--accent); border-color: var(--accent); }
        .interject-btn:disabled { background: var(--ink-faint); border-color: var(--ink-faint); cursor: wait; }

        .controls {
          display: flex;
          align-items: center;
          gap: 16px;
          margin: 0 0 28px;
          padding: 12px 0;
          border-top: 1px solid var(--rule);
          border-bottom: 1px solid var(--rule);
        }
        .ctrl-btn {
          background: transparent;
          border: none;
          color: var(--ink);
          font-family: 'Fraunces', serif;
          font-size: 13px;
          font-style: italic;
          cursor: pointer;
          padding: 4px 8px;
        }
        .ctrl-btn:hover { color: var(--accent); }
        .ctrl-btn:disabled { color: var(--ink-faint); cursor: default; }
        .ctrl-dot {
          width: 6px; height: 6px;
          border-radius: 50%;
          background: var(--accent);
          display: inline-block;
          margin-right: 6px;
          animation: pulse 1.4s ease-in-out infinite;
        }
        .ctrl-dot.paused { background: var(--ink-faint); animation: none; }
        @keyframes pulse { 0%, 100% { opacity: 0.3; } 50% { opacity: 1; } }
        .ctrl-spacer { flex: 1; }
        .ctrl-progress {
          font-size: 10px;
          letter-spacing: 0.16em;
          text-transform: uppercase;
          color: var(--ink-faint);
        }

        .layout {
          display: grid;
          grid-template-columns: 1fr 280px;
          gap: 48px;
        }
        @media (max-width: 980px) {
          .layout { grid-template-columns: 1fr; }
        }

        /* Quote frame — height transitions between compact (status) and full (beat) */
        .current-quote-frame {
          background: var(--paper);
          border: 1px solid var(--rule);
          padding: 24px 36px;
          margin-bottom: 24px;
          height: 240px;
          display: flex;
          flex-direction: column;
          overflow: hidden;
          transition: height 0.4s ease, padding 0.4s ease;
        }
        .current-quote-frame.compact {
          height: 90px;
          padding: 20px 36px;
        }
        .current-quote-inner {
          flex: 1;
          display: flex;
          flex-direction: column;
          animation: quoteIn 0.5s ease-out;
        }
        @keyframes quoteIn {
          from { opacity: 0; transform: translateY(6px); }
          to { opacity: 1; transform: translateY(0); }
        }
        .current-quote-label {
          font-size: 10px;
          letter-spacing: 0.18em;
          text-transform: uppercase;
          color: var(--ink-faint);
          margin-bottom: 12px;
          flex-shrink: 0;
        }
        .current-quote-text {
          font-family: 'Fraunces', serif;
          font-size: 21px;
          line-height: 1.4;
          color: var(--ink);
          margin: 0;
          font-weight: 400;
          flex: 1;
          overflow: hidden;
          display: -webkit-box;
          -webkit-line-clamp: 5;
          -webkit-box-orient: vertical;
        }
        .current-quote-text.guest-quote { font-style: italic; color: var(--ink-soft); }
        .current-quote-attrib {
          font-family: 'Fraunces', serif;
          font-size: 13px;
          color: var(--ink-faint);
          font-style: italic;
          margin-top: 14px;
          flex-shrink: 0;
        }
        .current-quote-attrib strong {
          font-style: normal;
          color: var(--ink);
          font-weight: 500;
          margin-right: 8px;
        }
        .current-quote-status {
          font-family: 'Fraunces', serif;
          font-style: italic;
          font-size: 17px;
          color: var(--ink-faint);
          margin: 0;
        }
        /* Animated ellipsis — three dots that cycle in */
        .dots::after {
          content: '';
          display: inline-block;
          width: 1.4em;
          text-align: left;
          animation: dots 1.6s steps(4, end) infinite;
        }
        @keyframes dots {
          0%   { content: ''; }
          25%  { content: '.'; }
          50%  { content: '..'; }
          75%  { content: '...'; }
          100% { content: ''; }
        }

        /* Table */
        .table-wrap {
          background: var(--paper);
          border: 1px solid var(--rule);
          padding: 18px 32px 16px;
          margin-bottom: 24px;
          position: relative;
        }
        .table-label {
          font-size: 10px;
          letter-spacing: 0.18em;
          text-transform: uppercase;
          color: var(--ink-faint);
          margin-bottom: 8px;
        }
        .table-svg { width: 100%; height: auto; display: block; max-height: 560px; aspect-ratio: 100 / 82; }

        .trail { margin-top: 24px; }
        .trail-label {
          font-size: 10px;
          letter-spacing: 0.18em;
          text-transform: uppercase;
          color: var(--ink-faint);
          margin-bottom: 12px;
        }
        .trail-scroll {
          max-height: 280px;
          overflow-y: auto;
          padding-right: 6px;
        }
        .trail-scroll::-webkit-scrollbar { width: 4px; }
        .trail-scroll::-webkit-scrollbar-thumb { background: var(--rule); }
        .trail-beat {
          padding: 12px 0;
          border-bottom: 1px solid var(--rule);
          opacity: 0.7;
        }
        .trail-beat:last-child { border-bottom: none; }
        .trail-attrib {
          font-family: 'Fraunces', serif;
          font-size: 12px;
          color: var(--ink-faint);
          font-style: italic;
          margin-bottom: 4px;
        }
        .trail-attrib strong {
          font-style: normal;
          color: var(--ink-soft);
          font-weight: 500;
          margin-right: 6px;
        }
        .trail-text {
          font-family: 'Fraunces', serif;
          font-size: 14px;
          line-height: 1.5;
          color: var(--ink-soft);
        }
        .trail-text.guest { font-style: italic; }

        .sidebar {
          border-left: 1px solid var(--rule);
          padding-left: 32px;
        }
        @media (max-width: 980px) {
          .sidebar { border-left: none; border-top: 1px solid var(--rule); padding-left: 0; padding-top: 32px; }
        }
        .sidebar-section { margin-bottom: 36px; }
        .sidebar-title {
          font-size: 10px;
          letter-spacing: 0.18em;
          text-transform: uppercase;
          color: var(--ink-faint);
          margin-bottom: 14px;
        }
        .topic-list { list-style: none; padding: 0; margin: 0; }
        .topic-item {
          font-family: 'Fraunces', serif;
          font-size: 14px;
          line-height: 1.4;
          padding: 10px 0;
          border-bottom: 1px solid var(--rule);
          cursor: pointer;
          display: flex;
          gap: 10px;
          align-items: baseline;
          color: var(--ink-soft);
          transition: color 0.15s;
        }
        .topic-item:hover { color: var(--accent); }
        .topic-item.active { color: var(--ink); font-weight: 500; }
        .topic-item-num {
          font-size: 11px;
          color: var(--ink-faint);
          font-family: 'DM Sans', sans-serif;
          letter-spacing: 0.1em;
          min-width: 18px;
        }
        .topic-item.visited .topic-item-num::after { content: " ·"; color: var(--accent); }

        .glossary-term {
          background: transparent;
          border: none;
          padding: 0;
          font: inherit;
          color: inherit;
          cursor: pointer;
          border-bottom: 1px dotted var(--accent);
        }
        .glossary-term:hover { background: rgba(139, 58, 31, 0.08); }

        .guest-card {
          position: absolute;
          background: var(--paper);
          border: 1px solid var(--ink);
          padding: 14px 16px;
          width: 240px;
          z-index: 50;
          pointer-events: none;
          box-shadow: 0 8px 24px rgba(26, 22, 18, 0.12);
          animation: cardIn 0.2s ease-out;
        }
        @keyframes cardIn {
          from { opacity: 0; transform: translateY(-4px); }
          to { opacity: 1; transform: translateY(0); }
        }
        .guest-card-name {
          font-family: 'Fraunces', serif;
          font-size: 15px;
          font-weight: 500;
          margin-bottom: 2px;
        }
        .guest-card-role {
          font-family: 'Fraunces', serif;
          font-size: 12px;
          font-style: italic;
          color: var(--ink-faint);
          margin-bottom: 8px;
        }
        .guest-card-bio {
          font-family: 'Fraunces', serif;
          font-size: 13px;
          line-height: 1.45;
          color: var(--ink-soft);
        }

        .modal-overlay {
          position: fixed;
          inset: 0;
          background: rgba(26, 22, 18, 0.55);
          display: flex;
          align-items: center;
          justify-content: center;
          z-index: 100;
          padding: 24px;
        }
        .modal {
          background: var(--paper);
          max-width: 560px;
          width: 100%;
          padding: 40px 44px;
          position: relative;
          max-height: 80vh;
          overflow-y: auto;
        }
        .modal-close {
          position: absolute;
          top: 16px;
          right: 20px;
          background: transparent;
          border: none;
          font-size: 18px;
          cursor: pointer;
          color: var(--ink-faint);
        }
        .modal-eyebrow {
          font-size: 10px;
          letter-spacing: 0.18em;
          text-transform: uppercase;
          color: var(--ink-faint);
          margin-bottom: 10px;
        }
        .modal-title {
          font-family: 'Fraunces', serif;
          font-size: 28px;
          font-weight: 500;
          letter-spacing: -0.015em;
          margin: 0 0 18px 0;
          line-height: 1.1;
        }
        .modal-short {
          font-family: 'Fraunces', serif;
          font-size: 17px;
          font-style: italic;
          color: var(--ink-soft);
          line-height: 1.5;
          margin-bottom: 18px;
        }
        .modal-long {
          font-family: 'Fraunces', serif;
          font-size: 15px;
          color: var(--ink);
          line-height: 1.6;
        }
        .report-body {
          font-family: 'Fraunces', serif;
          font-size: 16px;
          line-height: 1.65;
          color: var(--ink);
          white-space: pre-wrap;
        }
        .report-loading {
          font-family: 'Fraunces', serif;
          font-style: italic;
          color: var(--ink-faint);
          font-size: 15px;
        }
        .report-actions {
          margin-top: 28px;
          padding-top: 20px;
          border-top: 1px solid var(--rule);
          display: flex;
          gap: 12px;
        }
        .report-action-btn {
          background: transparent;
          border: 1px solid var(--ink);
          padding: 8px 14px;
          font-family: 'Fraunces', serif;
          font-size: 13px;
          font-style: italic;
          cursor: pointer;
          color: var(--ink);
        }
        .report-action-btn:hover { background: var(--ink); color: var(--paper); }

        .finished-note {
          margin-top: 32px;
          padding: 24px;
          border: 1px solid var(--ink);
          background: var(--paper);
          font-family: 'Fraunces', serif;
          font-style: italic;
          font-size: 16px;
          color: var(--ink-soft);
          line-height: 1.5;
        }

        @keyframes ripple {
          0% { r: 7; opacity: 0.55; }
          100% { r: 18; opacity: 0; }
        }

        /* ==================================================================
           v2 Layout — vertical, diagram-first (replaces .layout grid)
           ================================================================== */
        .dinner-container {
          /* widened so the diagram has room to breathe */
          max-width: none;
          padding: 20px 32px 56px;
        }
        @media (min-width: 720px) {
          .dinner-container { padding: 28px 56px 72px; }
        }
        .dinner-layout-v2 {
          display: flex;
          flex-direction: column;
          gap: 26px;
          margin-top: 4px;
        }

        /* Big diagram */
        .table-wrap-large {
          padding: 24px 32px 20px;
          /* Component fills the container, with reasonable max for very wide viewports */
          max-width: 1100px;
          margin: 0 auto;
          width: 100%;
        }
        .table-svg-large {
          width: 100%;
          height: auto;
          max-height: none;        /* let the diagram grow */
          aspect-ratio: 100 / 82;  /* same shape as before, just bigger */
        }

        /* Controls bar — add room for relocated mode + notes buttons on the right */
        .ctrl-toggle, .ctrl-notes { color: var(--ink-soft); font-style: italic; }
        .ctrl-toggle::before {
          content: '';
          display: inline-block;
          width: 6px; height: 6px;
          border-radius: 50%;
          background: var(--accent);
          opacity: 0.6;
          margin-right: 6px;
          vertical-align: middle;
        }
        .ctrl-notes { color: var(--ink); }
        .ctrl-notes-dot {
          display: inline-block;
          width: 6px; height: 6px;
          background: var(--accent);
          border-radius: 50%;
          margin-right: 6px;
          vertical-align: middle;
        }
        .ctrl-notes:hover, .ctrl-toggle:hover { color: var(--accent); }

        /* Course Menu — grouped */
        .course-menu {
          margin-top: 4px;
          padding: 22px 26px 26px;
          background: var(--paper);
          border: 1px solid var(--rule);
        }
        .course-menu-label {
          font-size: 10px;
          letter-spacing: 0.18em;
          text-transform: uppercase;
          color: var(--ink-faint);
          margin-bottom: 18px;
        }
        .course-menu-grid {
          display: grid;
          grid-template-columns: repeat(4, 1fr);
          gap: 28px;
        }
        @media (max-width: 980px) { .course-menu-grid { grid-template-columns: repeat(2, 1fr); gap: 24px; } }
        @media (max-width: 560px) { .course-menu-grid { grid-template-columns: 1fr; gap: 20px; } }
        .course-group {
          padding-top: 14px;
          border-top: 1px solid var(--ink);
        }
        .course-group-label {
          font-family: 'Fraunces', serif;
          font-style: italic;
          font-size: 14px;
          color: var(--ink);
          margin-bottom: 10px;
          letter-spacing: 0.005em;
        }
        .course-group-list { list-style: none; padding: 0; margin: 0; }
        .course-topic-item {
          font-family: 'Fraunces', serif;
          font-size: 15px;
          line-height: 1.35;
          padding: 9px 0;
          border-bottom: 1px solid var(--rule);
          cursor: pointer;
          display: grid;
          grid-template-columns: 26px 1fr;
          gap: 8px;
          align-items: baseline;
          color: var(--ink-soft);
          transition: color 0.15s;
        }
        .course-topic-item:last-child { border-bottom: none; }
        .course-topic-item:hover { color: var(--accent); }
        .course-topic-item.active { color: var(--ink); font-weight: 500; }
        .course-topic-item.active .course-topic-num { color: var(--accent); }
        .course-topic-num {
          font-family: 'JetBrains Mono', 'DM Sans', monospace;
          font-size: 10px;
          letter-spacing: 0.12em;
          color: var(--ink-faint);
        }
        .course-topic-item.visited .course-topic-num::after {
          content: ' ·';
          color: var(--accent);
        }
        .course-topic-title { display: block; }

        /* Glossary strip — chips below the menu */
        .glossary-strip {
          padding: 18px 26px 22px;
          border: 1px solid var(--rule);
          border-top: none;
          background: var(--paper);
        }
        .glossary-strip-label {
          font-size: 10px;
          letter-spacing: 0.18em;
          text-transform: uppercase;
          color: var(--ink-faint);
          margin-bottom: 12px;
        }
        .glossary-strip-list {
          display: flex;
          flex-wrap: wrap;
          gap: 6px 10px;
        }
        .glossary-strip-chip {
          background: transparent;
          border: 1px solid var(--rule);
          padding: 6px 12px;
          font-family: 'Fraunces', serif;
          font-style: italic;
          font-size: 13px;
          color: var(--ink-soft);
          cursor: pointer;
          transition: color .15s, border-color .15s, background .15s;
        }
        .glossary-strip-chip:hover { color: var(--ink); border-color: var(--ink); }
        .glossary-strip-chip.opened { color: var(--ink); border-color: var(--ink-faint); background: var(--bg); }

        /* Current quote — full width inside v2 layout */
        .dinner-layout-v2 .current-quote-frame {
          margin-bottom: 0;
        }

        /* ------------------------------------------------------------------
           AT THE TABLE — guest strip above the menu, kept in sync with the
           active speaker (highlights) and the current course's cast (dims
           non-cast members so the user can see who's actually speaking).
           ------------------------------------------------------------------ */
        .cd-attendees {
          margin-top: 4px;
          padding: 22px 26px 20px;
          background: var(--paper);
          border: 1px solid var(--rule);
          border-bottom: none;
        }
        .cd-attendees-label {
          font-size: 10px;
          letter-spacing: 0.18em;
          text-transform: uppercase;
          color: var(--ink-faint);
          margin-bottom: 14px;
        }
        .cd-attendees-list {
          display: grid;
          grid-template-columns: repeat(6, minmax(0, 1fr));
          gap: 16px 18px;
        }
        @media (max-width: 1080px) { .cd-attendees-list { grid-template-columns: repeat(3, minmax(0, 1fr)); } }
        @media (max-width: 620px)  { .cd-attendees-list { grid-template-columns: repeat(2, minmax(0, 1fr)); } }
        .cd-attendee {
          display: grid;
          grid-template-columns: 30px minmax(0, 1fr);
          gap: 10px;
          align-items: center;
          padding: 4px 0;
          cursor: pointer;
          transition: opacity .2s;
        }
        .cd-attendee.muted { opacity: 0.45; }
        .cd-attendee:hover { opacity: 1; }
        .cd-attendee-initials {
          width: 30px; height: 30px;
          display: flex; align-items: center; justify-content: center;
          color: rgba(250, 246, 236, 0.92);
          font-family: 'Fraunces', serif;
          font-size: 12px;
          font-weight: 500;
          letter-spacing: 0.02em;
          transition: box-shadow .2s, transform .2s;
        }
        .cd-attendee.speaking .cd-attendee-initials {
          box-shadow: 0 0 0 2px var(--paper), 0 0 0 3px var(--ink);
        }
        .cd-attendee-text { min-width: 0; }
        .cd-attendee-name {
          font-family: 'Fraunces', serif;
          font-size: 13px;
          font-weight: 500;
          color: var(--ink);
          line-height: 1.18;
          white-space: nowrap;
          overflow: hidden;
          text-overflow: ellipsis;
        }
        .cd-attendee-role {
          font-family: 'Fraunces', serif;
          font-style: italic;
          font-size: 11px;
          color: var(--ink-faint);
          line-height: 1.25;
          white-space: nowrap;
          overflow: hidden;
          text-overflow: ellipsis;
        }
        .cd-attendee.speaking .cd-attendee-name { color: var(--accent); }
        .cd-attendee.speaking::before {
          content: '';
          display: none;
        }

        /* When course menu sits directly UNDER the attendees strip, drop its
           top border so the two blocks read as a single sheet. */
        .course-menu.course-menu-top {
          margin-top: 0;
          border-top: 1px solid var(--rule);
        }
        .cd-attendees + .course-menu.course-menu-top {
          border-top: 1px solid var(--hairline);
        }

      `}</style>

      <div className="dinner-container">
        {/* Topic header */}
        <div className="topic-header">
          <div className="topic-eyebrow">Course {topicIndex + 1} of {TOPICS.length}</div>
          <h1 className="topic-title">{currentTopic.title}</h1>
          <p className="topic-blurb">{currentTopic.blurb}</p>
        </div>

        {/* AT THE TABLE — guests seated tonight (synced with active speaker) */}
        <div className="cd-attendees">
          <div className="cd-attendees-label">At the table · </div>
          <div className="cd-attendees-list">
            {Object.entries(ATTENDEES).map(([id, a]) => {
              const speaking = activeSpeakerId === id;
              const inCast = currentTopic.cast.includes(id);
              return (
                <div
                  key={id}
                  className={`cd-attendee ${speaking ? "speaking" : ""} ${inCast ? "in-cast" : "muted"}`}
                  onMouseEnter={() => setHoveredSeat(id)}
                  onMouseLeave={() => setHoveredSeat(null)}
                  onClick={() => setHoveredSeat(id)}
                  title={a.bio}
                >
                  <div className="cd-attendee-initials" style={{ background: a.accent }}>{a.initials}</div>
                  <div className="cd-attendee-text">
                    <div className="cd-attendee-name">{a.name}</div>
                    <div className="cd-attendee-role">{a.role}</div>
                  </div>
                </div>
              );
            })}
          </div>
        </div>

        {/* COURSE MENU — moved up under the topic title. Stays synced with playback (active highlight). */}
        <div className="course-menu course-menu-top">
          <div className="course-menu-label">The Menu · jump to any course</div>
          <div className="course-menu-grid">
            {COURSES.map((course) => (
              <div className="course-group" key={course.label}>
                <div className="course-group-label">{course.label}</div>
                <ul className="course-group-list">
                  {course.topicIds.map((tid) => {
                    const idx = TOPICS.findIndex((t) => t.id === tid);
                    if (idx < 0) return null;
                    const t = TOPICS[idx];
                    const active = idx === topicIndex;
                    const visited = userActions.topicsVisited.includes(t.id);
                    return (
                      <li
                        key={t.id}
                        className={`course-topic-item ${active ? "active" : ""} ${visited ? "visited" : ""}`}
                        onClick={() => handleJump(idx)}
                      >
                        <span className="course-topic-num">{String(idx + 1).padStart(2, "0")}</span>
                        <span className="course-topic-title">{t.title}</span>
                      </li>
                    );
                  })}
                </ul>
              </div>
            ))}
          </div>
        </div>

        {/* Interject (top) */}
        <div className="interject">
          <div className="interject-label">Pull up a chair</div>
          <div className="interject-row">
            <input
              className="interject-input"
              value={userQuestion}
              onChange={(e) => setUserQuestion(e.target.value)}
              onKeyDown={(e) => e.key === "Enter" && handleInterject()}
              placeholder="What would you ask the table?"
              disabled={isInterjecting || isLoadingTopic}
            />
            <button
              className="interject-btn"
              onClick={handleInterject}
              disabled={isInterjecting || !userQuestion.trim()}
            >
              {isInterjecting ? "asking" : "ask"}
            </button>
          </div>
        </div>

        <div className="controls">
          <button className="ctrl-btn" onClick={handlePauseToggle} disabled={isLoadingTopic || beats.length === 0}>
            <span className={`ctrl-dot ${!isPlaying ? "paused" : ""}`} />
            {isPlaying ? "pause" : "play"}
          </button>
          <button className="ctrl-btn" onClick={handleReplay} disabled={beats.length === 0}>replay</button>
          <button className="ctrl-btn" onClick={() => topicIndex > 0 && handleJump(topicIndex - 1)} disabled={topicIndex === 0}>← previous</button>
          <button className="ctrl-btn" onClick={() => topicIndex < TOPICS.length - 1 && handleJump(topicIndex + 1)} disabled={topicIndex === TOPICS.length - 1}>next →</button>
          <div className="ctrl-spacer" />
          <div className="ctrl-progress">{visibleCount} / {beats.length || "—"}</div>
          <button className="ctrl-btn ctrl-toggle" onClick={() => setMode(mode === "light" ? "warm" : "light")} title="Toggle dining room lighting">
            {mode === "light" ? "candlelight" : "daylight"}
          </button>
          <button className="ctrl-btn ctrl-notes" onClick={openNotes}>
            <span className="ctrl-notes-dot" />
            your notes
          </button>
        </div>

        <div className="dinner-layout-v2">

          {/* Current quote — top, full width */}
          <div className={`current-quote-frame ${showingStatus ? "compact" : ""}`}>
            <div
              className="current-quote-inner"
              key={
                showingStatus
                  ? `status-${isLoadingTopic ? "load" : loadError ? "err" : "idle"}`
                  : `${topicIndex}-${visibleCount}`
              }
            >
              {!showingStatus && currentBeat && (
                <>
                  <div className="current-quote-label">
                    {currentBeat.isGuest ? "You said" : "Now speaking"}
                  </div>
                  <p className={`current-quote-text ${currentBeat.isGuest ? "guest-quote" : ""}`}>
                    {renderTextWithGlossary(currentBeat.text)}
                  </p>
                  <div className="current-quote-attrib">
                    <strong>
                      {currentBeat.isGuest ? "You" : ATTENDEES[currentBeat.speaker]?.name || "Guest"}
                    </strong>
                    {!currentBeat.isGuest && ATTENDEES[currentBeat.speaker]?.role}
                  </div>
                </>
              )}

              {isLoadingTopic && (
                <p className="current-quote-status">
                  <span className="dots">Pouring water, gathering thoughts</span>
                </p>
              )}

              {!isLoadingTopic && !currentBeat && !loadError && (
                <p className="current-quote-status">
                  <span className="dots">The room is settling in</span>
                </p>
              )}

              {loadError && (
                <p className="current-quote-status" style={{ color: "var(--accent)" }}>
                  {loadError}
                </p>
              )}
            </div>
          </div>

          {/* MAIN: Big table diagram */}
          <div className="table-wrap table-wrap-large">
            <div className="table-label">The Table · hover a seat for an introduction</div>
            <svg className="table-svg table-svg-large" viewBox="0 18 100 82" xmlns="http://www.w3.org/2000/svg" preserveAspectRatio="xMidYMid meet">
              <defs>
                <linearGradient id="tableGrad" x1="0%" y1="0%" x2="0%" y2="100%">
                  <stop offset="0%" stopColor="#d8c69e" />
                  <stop offset="100%" stopColor="#b09c72" />
                </linearGradient>
                <filter id="seatGlow">
                  <feGaussianBlur stdDeviation="0.6" result="blur" />
                  <feMerge>
                    <feMergeNode in="blur" />
                    <feMergeNode in="SourceGraphic" />
                  </feMerge>
                </filter>
              </defs>

              {/* Square table surface */}
              <rect x="32" y="34" width="36" height="36" fill="url(#tableGrad)" stroke="var(--table-edge)" strokeWidth="0.4" rx="1" />
              <rect x="35.5" y="37.5" width="29" height="29" fill="none" stroke="rgba(26,22,18,0.08)" strokeWidth="0.2" rx="0.5" />

              {/* Centerpiece — candle */}
              <circle cx="50" cy="52" r="1.4" fill="var(--accent)" opacity="0.7" />
              <circle cx="50" cy="52" r="3" fill="var(--accent)" opacity="0.12" />
              <circle cx="50" cy="52" r="5.5" fill="var(--accent)" opacity="0.05" />
              {/* Centerpiece dish (bread basket) flanking the candle */}
              <text x="44" y="53.4" fontSize="3.6" textAnchor="middle">🥖</text>
              <text x="56" y="53.4" fontSize="3.4" textAnchor="middle">🍷</text>

              {/* Dish in front of "you" — top center, on the inner edge of the table */}
              <text x="50" y="38.4" fontSize="3.2" textAnchor="middle">🍽️</text>

              {/* "You" chair — top center, pulled up to the table */}
              <g style={{ transition: "opacity 0.4s" }}>
                <ellipse cx="50" cy="27.4" rx="4.5" ry="1.2" fill="rgba(26,22,18,0.15)" />
                <circle
                  cx="50"
                  cy="26"
                  r={activeSpeakerId === "guest" ? "4.6" : "4"}
                  fill="var(--paper)"
                  stroke="var(--ink)"
                  strokeWidth="0.4"
                  strokeDasharray={activeSpeakerId === "guest" ? "0" : "0.8 0.6"}
                  style={{ transition: "r 0.4s ease" }}
                />
                <text
                  x="50"
                  y="27"
                  textAnchor="middle"
                  fontFamily="Fraunces, serif"
                  fontSize="2.2"
                  fontStyle="italic"
                  fill="var(--ink)"
                  style={{ pointerEvents: "none" }}
                >
                  you
                </text>
                {activeSpeakerId === "guest" && (
                  <circle
                    cx="50"
                    cy="26"
                    r="6"
                    fill="none"
                    stroke="var(--accent)"
                    strokeWidth="0.4"
                    opacity="0.5"
                    style={{ animation: "ripple 2.6s ease-out infinite" }}
                  />
                )}
              </g>

              {/* Ripples from active seated speaker */}
              {tableSeats.map((seat) => {
                if (!seat.isSpeaking) return null;
                return (
                  <circle
                    key={`ripple-${seat.id}`}
                    cx={seat.x}
                    cy={seat.y}
                    r="7"
                    fill="none"
                    stroke={seat.attendee.accent}
                    strokeWidth="0.5"
                    opacity="0.55"
                    style={{ animation: "ripple 2.6s ease-out infinite" }}
                  />
                );
              })}

              {/* Dishes in front of each guest */}
              {tableSeats.map((seat, i) => {
                let dx, dy;
                if (seat.side === "left") { dx = 35; dy = seat.y; }
                else if (seat.side === "right") { dx = 65; dy = seat.y; }
                else { dx = seat.x; dy = 65.5; }
                const dish = DISHES[(i + 2) % DISHES.length];
                return (
                  <text
                    key={`dish-${seat.id}`}
                    x={dx}
                    y={dy + 1}
                    fontSize="3"
                    textAnchor="middle"
                    style={{ pointerEvents: "none" }}
                  >
                    {dish}
                  </text>
                );
              })}

              {/* Seats with person glyph */}
              {tableSeats.map((seat) => {
                const isActive = seat.isSpeaking;
                let nameLabel, roleLabel, textAnchor;
                if (seat.side === "bottom") {
                  nameLabel = { x: seat.x, y: seat.y + 7.8 };
                  roleLabel = { x: seat.x, y: seat.y + 10.9 };
                  textAnchor = "middle";
                } else if (seat.side === "left") {
                  nameLabel = { x: seat.x - 5.5, y: seat.y - 0.3 };
                  roleLabel = { x: seat.x - 5.5, y: seat.y + 2.2 };
                  textAnchor = "end";
                } else {
                  nameLabel = { x: seat.x + 5.5, y: seat.y - 0.3 };
                  roleLabel = { x: seat.x + 5.5, y: seat.y + 2.2 };
                  textAnchor = "start";
                }
                const seatR = isActive ? 4.4 : 3.8;
                return (
                  <g
                    key={seat.id}
                    style={{ cursor: "pointer", transition: "opacity 0.4s" }}
                    onMouseEnter={() => setHoveredSeat(seat.id)}
                    onMouseLeave={() => setHoveredSeat(null)}
                  >
                    <ellipse cx={seat.x} cy={seat.y + 0.9} rx="4.3" ry="1.1" fill="rgba(26,22,18,0.18)" />
                    <circle
                      cx={seat.x}
                      cy={seat.y}
                      r={seatR}
                      fill={seat.attendee.accent}
                      stroke={isActive ? "var(--ink)" : "transparent"}
                      strokeWidth={isActive ? "0.5" : "0"}
                      filter={isActive ? "url(#seatGlow)" : ""}
                      style={{ transition: "r 0.4s ease, stroke-width 0.3s" }}
                    />
                    <PersonGlyph cx={seat.x} cy={seat.y} r={seatR} />
                    <text
                      x={seat.x}
                      y={seat.y + 3.0}
                      textAnchor="middle"
                      fontFamily="Fraunces, serif"
                      fontSize="1.55"
                      fontWeight="500"
                      fill="rgba(250, 246, 236, 0.85)"
                      style={{ pointerEvents: "none" }}
                    >
                      {seat.attendee.initials}
                    </text>
                    <text
                      x={nameLabel.x}
                      y={nameLabel.y}
                      textAnchor={textAnchor}
                      fontFamily="Fraunces, serif"
                      fontSize="2.3"
                      fill="var(--ink)"
                      style={{ pointerEvents: "none" }}
                    >
                      {seat.attendee.name}
                    </text>
                    <text
                      x={roleLabel.x}
                      y={roleLabel.y}
                      textAnchor={textAnchor}
                      fontFamily="Fraunces, serif"
                      fontSize="1.8"
                      fontStyle="italic"
                      fill="var(--ink-faint)"
                      style={{ pointerEvents: "none" }}
                    >
                      {seat.attendee.role}
                    </text>
                  </g>
                );
              })}
            </svg>

            {hoveredSeat && ATTENDEES[hoveredSeat] && (
              <div className="guest-card" style={{ top: "16px", right: "16px" }}>
                <div className="guest-card-name">{ATTENDEES[hoveredSeat].name}</div>
                <div className="guest-card-role">{ATTENDEES[hoveredSeat].role}</div>
                <div className="guest-card-bio">{ATTENDEES[hoveredSeat].bio}</div>
              </div>
            )}
          </div>

          {/* Trail */}
          {previousBeats.length > 0 && (
            <div className="trail">
              <div className="trail-label">Earlier in the course</div>
              <div className="trail-scroll" ref={trailRef}>
                {previousBeats.map((b, i) => {
                  const isGuest = b.isGuest;
                  const a = isGuest ? null : ATTENDEES[b.speaker];
                  return (
                    <div key={`trail-${topicIndex}-${i}`} className="trail-beat">
                      <div className="trail-attrib">
                        <strong>{isGuest ? "You" : a?.name}</strong>
                        {!isGuest && a?.role}
                      </div>
                      <div className={`trail-text ${isGuest ? "guest" : ""}`}>
                        {renderTextWithGlossary(b.text)}
                      </div>
                    </div>
                  );
                })}
              </div>
            </div>
          )}

          {isInterjecting && (
            <div className="trail-text" style={{ marginTop: 16, fontStyle: "italic", color: "var(--ink-faint)" }}>
              <span className="dots">The table considers your question</span>
            </div>
          )}

          {conversationFinished && (
            <div className="finished-note">
              The dinner has ended. The conversation moved across all eight courses.
              Open <em>your notes</em> for the editor's reading of your evening — or
              jump back into any course from the menu.
            </div>
          )}

          {/* GLOSSARY — strip below the menu */}
          <div className="glossary-strip">
            <div className="glossary-strip-label">Terms in Play</div>
            <div className="glossary-strip-list">
              {Object.keys(GLOSSARY).map((k) => (
                <button
                  key={k}
                  onClick={() => openGlossary(k)}
                  data-glossary-key={k}
                  data-glossary-term={GLOSSARY[k].term}
                  data-glossary-short={GLOSSARY[k].short}
                  className={`glossary-strip-chip ${userActions.glossaryOpened.includes(k) ? "opened" : ""}`}
                >
                  {GLOSSARY[k].term}
                </button>
              ))}
            </div>
          </div>

        </div>
      </div>

      {glossaryOpen && GLOSSARY[glossaryOpen] && (
        <div className="modal-overlay" onClick={() => setGlossaryOpen(null)}>
          <div className="modal" onClick={(e) => e.stopPropagation()}>
            <button className="modal-close" onClick={() => setGlossaryOpen(null)}>✕</button>
            <div className="modal-eyebrow">Term</div>
            <h2 className="modal-title">{GLOSSARY[glossaryOpen].term}</h2>
            <div className="modal-short">{GLOSSARY[glossaryOpen].short}</div>
            <div className="modal-long">{GLOSSARY[glossaryOpen].long}</div>
          </div>
        </div>
      )}

      {notesOpen && (
        <div className="modal-overlay" onClick={() => setNotesOpen(false)}>
          <div className="modal" onClick={(e) => e.stopPropagation()}>
            <button className="modal-close" onClick={() => setNotesOpen(false)}>✕</button>
            <div className="modal-eyebrow">Dinner Notes · for you</div>
            <h2 className="modal-title">What you brought to the table</h2>
            {isGeneratingReport && <div className="report-loading"><span className="dots">The editor is reading your evening</span></div>}
            {!isGeneratingReport && generatedReport && (
              <>
                <div className="report-body">{generatedReport}</div>
                <div className="report-actions">
                  <button className="report-action-btn" onClick={() => navigator.clipboard?.writeText(generatedReport)}>Copy</button>
                  <button className="report-action-btn" onClick={() => { setGeneratedReport(null); openNotes(); }}>Regenerate</button>
                </div>
              </>
            )}
            {!isGeneratingReport && !generatedReport && (
              <div className="report-loading">
                Stay a little longer — visit a topic or ask a question, and your notes will appear here.
              </div>
            )}
          </div>
        </div>
      )}
    </div>
  );
}

// Mount into the report's Dinner Table section.
(function mountCounterfactualDinner() {
  function mount() {
    const el = document.getElementById('counterfactual-dinner-mount');
    if (!el) return false;
    if (el.dataset.mounted === '1') return true;
    el.dataset.mounted = '1';
    ReactDOM.createRoot(el).render(React.createElement(CounterfactualDinner));
    return true;
  }
  if (!mount()) {
    document.addEventListener('DOMContentLoaded', mount);
  }
})();

})();
