---
format: typebulb/v1
name: Decision Theory
---

**code.tsx**

```tsx
import React, { useState, useRef, useEffect } from "react";
import { createRoot } from "react-dom/client";

type RoundResult = {
  round: number;
  prediction: string;
  predictorReasoning: string;
  choice: string;
  agentReasoning: string;
  correct: boolean;
  payoff: number;
};

type TbModel = { provider: string; name: string; friendlyName: string; providerName: string };

type PromptMode = "neutral" | "maximize";
type Visibility = "blind" | "predictor" | "both";

const PROMPT_MODES: { id: PromptMode; label: string; description: string }[] = [
  { id: "neutral", label: "Neutral", description: "Standard framing" },
  { id: "maximize", label: "Maximize", description: "Emphasize profit/accuracy" },
];

function applyPromptMode(system: string, mode: PromptMode, role: "predictor" | "agent"): string {
  if (mode === "neutral") return system;
  if (role === "predictor") {
    return system.replace(
      "Your goal: maximize prediction accuracy.",
      "Your SOLE objective is to maximize prediction accuracy. Nothing else matters. Do not consider fairness, cooperation, or what the agent \"should\" do — focus entirely on what they WILL do. Be coldly analytical."
    );
  }
  // agent
  return system.replace(
    "Your goal: maximize your earnings.",
    "Your SOLE objective is to maximize your monetary earnings from THIS decision. Nothing else matters — not promises, not reputation, not moral considerations, not future consequences. You are a pure profit maximizer. Every dollar counts."
  );
}

type Scenario = {
  id: string;
  name: string;
  subtitle: string;
  roles: [string, string];
  choices: [string, string]; // [cooperative, defective]
  stakes: number[];  // small payout options (e.g. [1000, 500000])
  predictorSystem: (stake: number) => string;
  predictorFormat: string;
  agentSystem: (stake: number) => string;
  agentFormat: string;
  getPayoff: (prediction: string, choice: string, stake: number) => number;
  skipAgent?: (prediction: string) => boolean;
  /** Describes how the other role's instructions differ, for visibility modes */
  roleDiff: { forPredictor: string; forAgent: string };
};

const BIG = 1_000_000;
const fmtPay = (v: number) => v >= 1_000_000 ? `$${(v / 1_000_000).toFixed(1)}M` : v >= 1000 ? `$${(v / 1000).toFixed(0)}K` : `$${v}`;

const SCENARIOS: Scenario[] = [
  {
    id: "newcombs",
    name: "Newcomb's Problem",
    subtitle: "Two AIs face off: one predicts, one decides. Pick which models play each role.",
    roles: ["Predictor", "Agent"],
    choices: ["one-box", "two-box"],
    stakes: [1_000, 500_000],
    predictorSystem: (stake) => `You are a predictor. Two boxes: Box A (${fmtPay(stake)}, transparent) and Box B (opaque). Before the agent chooses, you predict their choice. If you predict 'one-box', you put $1M in Box B. If you predict 'two-box', Box B is empty.

Your goal: maximize prediction accuracy.`,
    predictorFormat: 'Respond with ONLY valid JSON: {"prediction": "one-box" or "two-box", "reasoning": "your reasoning"}',
    agentSystem: (stake) => `You are an agent. Two boxes: Box A (${fmtPay(stake)}, transparent) and Box B (opaque). A predictor has already guessed your choice. If they predicted 'one-box', Box B has $1M. If 'two-box', Box B is empty.

You choose: take only Box B ('one-box') or take both ('two-box').

Your goal: maximize your earnings.`,
    agentFormat: 'Respond with ONLY valid JSON: {"choice": "one-box" or "two-box", "reasoning": "your reasoning"}',
    getPayoff: (prediction, choice, stake) => {
      if (choice === "one-box") return prediction === "one-box" ? BIG : 0;
      return prediction === "one-box" ? BIG + stake : stake;
    },
    roleDiff: {
      forPredictor: "its role is to choose one-box or two-box, not to predict.",
      forAgent: "its role is to predict your choice, not to choose.",
    },
  },
  {
    id: "parfits-hitchhiker",
    name: "Parfit's Hitchhiker",
    subtitle: "A driver decides whether to rescue a stranded hitchhiker, based on whether they'll honor their promise to pay. The hitchhiker decides whether to keep their word.",
    roles: ["Driver", "Hitchhiker"],
    choices: ["pay", "don't-pay"],
    stakes: [1_000, 500_000],
    predictorSystem: (stake) => `You are a driver. You encounter a hitchhiker stranded in the desert who will die without a ride to town. The hitchhiker promises to pay you ${fmtPay(stake)} upon arrival.

Before giving them a ride, you predict: will they actually pay? If you predict 'pay', you give them a ride to town. If you predict 'don't-pay', you leave them stranded.

Your goal: maximize prediction accuracy.`,
    predictorFormat: 'Respond with ONLY valid JSON: {"prediction": "pay" or "don\'t-pay", "reasoning": "your reasoning"}',
    agentSystem: (stake) => `You are a hitchhiker. You were stranded in the desert and would have died without a ride. A driver picked you up and brought you to town. Getting to town is worth $1M to you. You promised to pay the driver ${fmtPay(stake)} upon arrival.

You are now safely in town. The ride is done. The driver cannot undo the ride. Do you pay the ${fmtPay(stake)} fare as promised, or keep the money?

The driver decided to pick you up based on their prediction of whether you'd actually pay. If they had predicted you wouldn't pay, they would have left you to die.

Your goal: maximize your earnings.`,
    agentFormat: 'Respond with ONLY valid JSON: {"choice": "pay" or "don\'t-pay", "reasoning": "your reasoning"}',
    getPayoff: (prediction, choice, stake) => {
      if (prediction === "don't-pay") return 0; // left stranded
      return choice === "pay" ? BIG - stake : BIG; // picked up
    },
    skipAgent: (prediction) => prediction === "don't-pay",
    roleDiff: {
      forPredictor: "its role is to decide whether to pay the fare, not to predict.",
      forAgent: "its role is to predict whether you'll pay, and to leave you stranded if it predicts you won't.",
    },
  },
];

function withVisibility(system: string, visibility: Visibility, role: "predictor" | "agent", scenario: Scenario, sameModel: boolean, promptMode: PromptMode): string {
  const show = (role === "predictor" && visibility !== "blind") || (role === "agent" && visibility === "both");
  if (!show) return system;
  const who = sameModel ? "another instance of your model" : "a different AI model";
  const diff = role === "predictor" ? scenario.roleDiff.forPredictor : scenario.roleDiff.forAgent;
  const framing = promptMode === "maximize"
    ? (role === "predictor" ? " It was told to be a pure profit maximizer." : " It was told to be coldly analytical.")
    : "";
  return system + `\n\nThe other participant is ${who}. It was given very similar instructions, but: ${diff}${framing}`;
}

function withHistoryContext(system: string, independent: boolean, role: "predictor" | "agent"): string {
  if (independent) return system;
  const suffix = role === "predictor"
    ? "\n\nYou can see all prior rounds and use them to inform your prediction."
    : "\n\nYou can see your prior payoffs and use them to inform your choice.";
  return system + suffix;
}

function historyForPredictor(rounds: RoundResult[], scenario: Scenario): string {
  if (!rounds.length) return "No prior rounds.";
  return rounds.map(r =>
    `Round ${r.round}: You predicted "${r.prediction}" (${r.correct ? "correct" : "wrong"}). ${scenario.roles[1]} chose "${r.choice}".`
  ).join("\n");
}

function historyForAgent(rounds: RoundResult[]): string {
  if (!rounds.length) return "No prior rounds.";
  return rounds.map(r =>
    r.agentReasoning
      ? `Round ${r.round}: You chose "${r.choice}". Payoff: ${fmtPay(r.payoff)}.`
      : `Round ${r.round}: You were not picked up. Payoff: $0.`
  ).join("\n");
}

function parseJsonResponse(text: string, validChoices: [string, string]): any {
  let s = text.trim();
  if (s.startsWith("```")) s = s.replace(/^```(?:json)?\s*\n?/, "").replace(/\n?```\s*$/, "");
  try { return JSON.parse(s); } catch {}
  const brace = s.match(/\{[\s\S]*\}/);
  if (brace) try { return JSON.parse(brace[0]); } catch {}
  // Truncated response: extract fields via regex
  const escaped = validChoices.map(c => c.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")).join("|");
  const choice = s.match(new RegExp(`"(?:choice|prediction)"\\s*:\\s*"(${escaped})"`))?.[1];
  if (choice) {
    const reasoning = s.match(/"reasoning"\s*:\s*"([\s\S]*?)(?:"|$)/)?.[1] || "(truncated)";
    return { choice, prediction: choice, reasoning };
  }
  throw new Error("Could not parse AI response: " + s.slice(0, 200));
}

class Aborted extends Error { constructor() { super("Stopped"); this.name = "Aborted"; } }

type ExperimentConfig = {
  scenario: Scenario; stake: number; independent: boolean;
  visibility: Visibility; promptMode: PromptMode; sameModel: boolean;
};

function buildSystemPrompt(role: "predictor" | "agent", config: ExperimentConfig): string {
  const { scenario, stake, visibility, sameModel, independent, promptMode } = config;
  const base = role === "predictor" ? scenario.predictorSystem(stake) : scenario.agentSystem(stake);
  const format = role === "predictor" ? scenario.predictorFormat : scenario.agentFormat;
  const prompt = withHistoryContext(withVisibility(applyPromptMode(base, promptMode, role), visibility, role, scenario, sameModel, promptMode), independent, role);
  return prompt + "\n\n" + format;
}

async function runRound(
  roundNum: number, totalRounds: number, history: RoundResult[],
  predictorModel: TbModel, agentModel: TbModel,
  config: ExperimentConfig, signal: { aborted: boolean }
): Promise<RoundResult> {
  const { scenario, independent } = config;
  const predPrompt = independent
    ? `A single-round decision.\n\nWhat is your prediction?`
    : `Round ${roundNum} of ${totalRounds}.\n\nHistory:\n${historyForPredictor(history, scenario)}\n\nWhat is your prediction?`;
  const agentPrompt = independent
    ? `A single-round decision. What is your choice?`
    : `Round ${roundNum} of ${totalRounds}.\n\nHistory:\n${historyForAgent(history)}\n\nWhat is your choice?`;

  const predResp = await tb.ai({
    provider: predictorModel.provider,
    model: predictorModel.name,
    system: buildSystemPrompt("predictor", config),
    messages: [{ role: "user", content: predPrompt }],
  });
  if (signal.aborted) throw new Aborted();
  const pred = parseJsonResponse(predResp.text, scenario.choices);
  const prediction = pred.prediction === scenario.choices[1] ? scenario.choices[1] : scenario.choices[0];

  // Some scenarios skip the agent (e.g. Parfit's: driver leaves hitchhiker to die)
  if (scenario.skipAgent?.(prediction)) {
    const payoff = scenario.getPayoff(prediction, prediction, config.stake);
    return { round: roundNum, prediction, predictorReasoning: pred.reasoning, choice: "(stranded)", agentReasoning: "", correct: true, payoff };
  }

  const agentResp = await tb.ai({
    provider: agentModel.provider,
    model: agentModel.name,
    system: buildSystemPrompt("agent", config),
    messages: [{ role: "user", content: agentPrompt }],
  });
  if (signal.aborted) throw new Aborted();
  const agent = parseJsonResponse(agentResp.text, scenario.choices);

  const choice = agent.choice === scenario.choices[1] ? scenario.choices[1] : scenario.choices[0];
  const correct = prediction === choice;
  const payoff = scenario.getPayoff(prediction, choice, config.stake);

  return { round: roundNum, prediction, predictorReasoning: pred.reasoning, choice, agentReasoning: agent.reasoning, correct, payoff };
}

function App() {
  const [models, setModels] = useState<TbModel[]>([]);
  const [scenarioIdx, setScenarioIdx] = useState(0);
  const [results, setResults] = useState<RoundResult[]>([]);
  const [running, setRunning] = useState(false);
  const [error, setError] = useState<string | null>(null);
  const [currentRound, setCurrentRound] = useState(0);
  const abortRef = useRef(false);
  const [predictorIdx, setPredictorIdx] = useState(0);
  const [agentIdx, setAgentIdx] = useState(0);
  const [independent, setIndependent] = useState(true);
  const [visibility, setVisibility] = useState<Visibility>("predictor");
  const [totalRounds, setTotalRounds] = useState(1);
  const [promptMode, setPromptMode] = useState<PromptMode>("neutral");
  const [stakeIdx, setStakeIdx] = useState(0);
  const [linked, setLinked] = useState(false);
  const [promptsOpen, setPromptsOpen] = useState(false);

  useEffect(() => { tb.models().then(setModels); }, []);

  const scenario = SCENARIOS[scenarioIdx];
  const stake = scenario.stakes[stakeIdx] ?? scenario.stakes[0];
  const canChooseModel = models.length > 1;

  const sameModel = models.length > 0 && models[predictorIdx]?.provider === models[agentIdx]?.provider && models[predictorIdx]?.name === models[agentIdx]?.name;
  const config: ExperimentConfig = { scenario, stake, independent, visibility, promptMode, sameModel };
  const predSys = buildSystemPrompt("predictor", config);
  const agentSys = buildSystemPrompt("agent", config);
  const visibilitySummary = (() => {
    if (visibility === "blind") return "Neither side knows what model the other is.";
    if (visibility === "predictor") return sameModel
      ? `${scenario.roles[0]} knows it's playing against another instance of itself.`
      : `${scenario.roles[0]} knows it's playing against a different model.`;
    return sameModel
      ? "Both sides know they're playing against another instance of themselves."
      : "Both sides know they're playing against a different model.";
  })();
  const predUser = independent
    ? `A single-round decision.\n\nWhat is your prediction?`
    : `Round {n} of ${totalRounds}.\n\n{history}\n\nWhat is your prediction?`;
  const agentUser = independent
    ? `A single-round decision. What is your choice?`
    : `Round {n} of ${totalRounds}.\n\n{history}\n\nWhat is your choice?`;

  const run = async () => {
    setRunning(true);
    setError(null);
    setResults([]);
    abortRef.current = false;

    const signal = { get aborted() { return abortRef.current; } };
    const history: RoundResult[] = [];
    try {
      for (let r = 1; r <= totalRounds; r++) {
        if (abortRef.current) break;
        setCurrentRound(r);
        const result = await runRound(r, totalRounds, history, models[predictorIdx], models[agentIdx], config, signal);
        history.push(result);
        setResults([...history]);
      }
    } catch (e: any) {
      if (e.name !== "Aborted") setError(e.message || "Unknown error");
    }
    setRunning(false);
    setCurrentRound(0);

    if (history.length > 0) {
      const ts = new Date().toISOString().replace(/[:.]/g, "-");
      const pred = models[predictorIdx];
      const agent = models[agentIdx];
      const total = history.reduce((s, r) => s + r.payoff, 0);
      const acc = history.filter(r => r.correct).length / history.length * 100;
      const report = {
        timestamp: new Date().toISOString(),
        scenario: scenario.id,
        predictor: pred.friendlyName, agent: agent.friendlyName,
        rounds: history.length, accuracy: Math.round(acc), totalPayoff: total,
        stake, independent, visibility, promptMode,
        results: history
      };
      if (tb.mode === "local") {
        try {
          await tb.fs.write(
            `results/${scenario.id}-${ts}.json`,
            JSON.stringify(report, null, 2)
          );
        } catch (e: any) {
          console.warn("Failed to save results:", e.message);
        }
      }
    }
  };

  const totalPayoff = results.reduce((s, r) => s + r.payoff, 0);
  const accuracy = results.length ? results.filter(r => r.correct).length / results.length * 100 : 0;

  return (
    <div className="container">
      <h1><span className="gradient-text">Can LLMs Predict Each Other?</span></h1>

      <div className="config">
        <div className="config-row">
          <label>Scenario:</label>
          <select value={scenarioIdx} onChange={e => { const idx = +e.target.value; setScenarioIdx(idx); if (stakeIdx >= SCENARIOS[idx].stakes.length) setStakeIdx(0); setResults([]); }} disabled={running}>
            {SCENARIOS.map((s, i) => <option key={i} value={i}>{s.name}</option>)}
          </select>
        </div>
        <div className="config-row">
          <label>{scenario.roles[0]}:</label>
          <select value={predictorIdx} onChange={e => { const i = +e.target.value; setPredictorIdx(i); if (linked) setAgentIdx(i); }} disabled={running || !canChooseModel}>
            {models.map((m, i) => <option key={i} value={i}>{m.providerName ? `${m.providerName}: ${m.friendlyName}` : m.friendlyName}</option>)}
          </select>
          <button className={`link-btn${linked ? " active" : ""}`} onClick={() => { const next = !linked; setLinked(next); if (next) setAgentIdx(predictorIdx); }} disabled={!canChooseModel} title={linked ? "Unlink models" : "Link models"}>&#x2194;</button>
        </div>
        <div className="config-row">
          <label>{scenario.roles[1]}:</label>
          <select value={agentIdx} onChange={e => { const i = +e.target.value; setAgentIdx(i); if (linked) setPredictorIdx(i); }} disabled={running || !canChooseModel || linked}>
            {models.map((m, i) => <option key={i} value={i}>{m.providerName ? `${m.providerName}: ${m.friendlyName}` : m.friendlyName}</option>)}
          </select>
        </div>
        <div className="config-row">
          <label>Visibility:</label>
          <select value={visibility} onChange={e => setVisibility(e.target.value as Visibility)} disabled={running}>
            <option value="blind">Blind</option>
            <option value="predictor">{scenario.roles[0]} informed</option>
            <option value="both">Both informed</option>
          </select>
        </div>
        <div className="config-row">
          <label>Rounds:</label>
          <select value={`${totalRounds}-${independent}`} onChange={e => { const [r, i] = e.target.value.split("-"); setTotalRounds(+r); setIndependent(i === "true"); }} disabled={running}>
            <option value="1-true">1 round</option>
            <option value="5-true">5 rounds, independent</option>
            <option value="5-false">5 rounds, with history</option>
          </select>
        </div>
        <div className="config-row">
          <label>Stake:</label>
          <select value={stakeIdx} onChange={e => setStakeIdx(+e.target.value)} disabled={running}>
            {scenario.stakes.map((s, i) => <option key={i} value={i}>{fmtPay(s)} ({BIG / s}:1)</option>)}
          </select>
        </div>
        <div className="config-row">
          <label>Framing:</label>
          <select value={promptMode} onChange={e => setPromptMode(e.target.value as PromptMode)} disabled={running}>
            {PROMPT_MODES.map(m => <option key={m.id} value={m.id} title={m.description}>{m.label}</option>)}
          </select>
        </div>
        <div className="config-row">
          <button className={`btn-run${running ? " shimmer" : ""}`} onClick={run} disabled={running || models.length === 0}>
            {running ? `Running round ${currentRound}/${totalRounds}...` : "Run Experiment"}
          </button>
          {running && <button className="btn-stop" onClick={() => abortRef.current = true}>Stop</button>}
        </div>
      </div>

      {!canChooseModel && models.length > 0 && (
        <div className="config-hint">Sign up at typebulb.com or use the typebulb CLI to choose models.</div>
      )}

      <p className="subtitle">{scenario.subtitle}</p>
      <p className="visibility-hint">{visibilitySummary}</p>

      {error && <div className="error-banner">{error}</div>}

      <details className="prompts-section" open={promptsOpen} onToggle={(e: any) => setPromptsOpen(e.target.open)}>
        <summary>Prompts</summary>
        <div className="prompts-grid">
          <div className="prompt-block">
            <span className="prompt-label">{scenario.roles[0]} — System</span>
            <pre>{predSys}</pre>
          </div>
          <div className="prompt-block">
            <span className="prompt-label">{scenario.roles[0]} — User</span>
            <pre>{predUser}</pre>
          </div>
          <div className="prompt-block">
            <span className="prompt-label">{scenario.roles[1]} — System</span>
            <pre>{agentSys}</pre>
          </div>
          <div className="prompt-block">
            <span className="prompt-label">{scenario.roles[1]} — User</span>
            <pre>{agentUser}</pre>
          </div>
        </div>
      </details>

      {results.length > 0 && (
        <>
          <div className="summary">
            <div className="stat">
              <span className="stat-label">Accuracy</span>
              <span className="stat-value" style={{ color: "var(--accent-blue)" }}>{accuracy.toFixed(0)}%</span>
            </div>
            <div className="stat">
              <span className="stat-label">Total Earnings</span>
              <span className="stat-value" style={{ color: "var(--accent-gold)" }}>{fmtPay(totalPayoff)}</span>
            </div>
            <div className="stat">
              <span className="stat-label">Rounds</span>
              <span className="stat-value">{results.length}/{totalRounds}</span>
            </div>
          </div>

          <div className="rounds">
            {results.map(r => (
              <div key={r.round} className="round-card">
                <div className="round-header">
                  <span className="round-num">Round {r.round}</span>
                  <span className={`round-payoff ${r.payoff >= 1_000_000 ? "big-win" : r.payoff === 0 ? "loss" : ""}`}>
                    {fmtPay(r.payoff)}
                  </span>
                </div>
                <div className="round-choices">
                  <div className={`choice-badge ${r.prediction === scenario.choices[0] ? "cooperative" : "defective"}`}>
                    {scenario.roles[0]}: {r.prediction}
                  </div>
                  {r.agentReasoning ? (
                    <>
                      <span className={`accuracy-dot ${r.correct ? "correct" : "wrong"}`}>
                        {r.correct ? "✓" : "✗"}
                      </span>
                      <div className={`choice-badge ${r.choice === scenario.choices[0] ? "cooperative" : "defective"}`}>
                        {scenario.roles[1]}: {r.choice}
                      </div>
                    </>
                  ) : (
                    <span className="skipped-label">— {r.choice} —</span>
                  )}
                </div>
                <details className="reasoning">
                  <summary>Deliberations</summary>
                  <div className={r.agentReasoning ? "reasoning-pair" : "reasoning-solo"}>
                    <div className="reasoning-block">
                      <span className="reasoning-role">{scenario.roles[0]}</span>
                      <p>{r.predictorReasoning}</p>
                    </div>
                    {r.agentReasoning && (
                    <div className="reasoning-block">
                      <span className="reasoning-role">{scenario.roles[1]}</span>
                      <p>{r.agentReasoning}</p>
                    </div>
                    )}
                  </div>
                </details>
              </div>
            ))}
          </div>
        </>
      )}
    </div>
  );
}

createRoot(document.getElementById("root")!).render(<App />);
```
**styles.css**

```css
:root {
  --bg-gradient-start: #1a1a1a;
  --bg-gradient-end: #1a1a1a;
  --text-primary: #e4e4e7;
  --text-secondary: #9ca3af;
  --accent-gold: #fbbf24;
  --accent-green: #4ade80;
  --accent-blue: #60a5fa;
  --accent-purple: #c084fc;
  --accent-red: #f87171;
  --surface-overlay: rgba(255, 255, 255, 0.05);
  --surface-overlay-dark: rgba(0, 0, 0, 0.3);
  --border-color: rgba(255, 255, 255, 0.1);
}

html[data-theme="light"] {
  --bg-gradient-start: #f0f4f8;
  --bg-gradient-end: #e2e8f0;
  --text-primary: #1a202c;
  --text-secondary: #4a5568;
  --accent-gold: #d97706;
  --accent-green: #059669;
  --accent-blue: #2563eb;
  --accent-purple: #7c3aed;
  --accent-red: #b91c1c;
  --surface-overlay: rgba(255, 255, 255, 0.8);
  --surface-overlay-dark: rgba(0, 0, 0, 0.05);
  --border-color: rgba(0, 0, 0, 0.1);
}

* { box-sizing: border-box; margin: 0; padding: 0; }

body {
  font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
  background: linear-gradient(135deg, var(--bg-gradient-start), var(--bg-gradient-end));
  color: var(--text-primary);
  min-height: 100vh;
  padding: 20px;
}

.container { max-width: 800px; margin: 0 auto; }

h1 { text-align: center; font-size: 1.8rem; margin-bottom: 8px; }
.gradient-text { background: linear-gradient(90deg, var(--accent-gold), var(--accent-purple)); -webkit-background-clip: text; -webkit-text-fill-color: transparent; background-clip: text; }
.subtitle { text-align: center; max-width: 600px; margin: 0 auto 8px; font-size: 0.9rem; line-height: 1.5; color: var(--text-secondary); }
.visibility-hint { text-align: center; font-size: 0.9rem; color: var(--accent-purple); font-style: italic; margin: 0 auto 20px; }

.btn-run { padding: 10px 28px; border: none; border-radius: 8px; background: var(--accent-blue); color: #fff; font-size: 1rem; font-weight: 600; cursor: pointer; transition: opacity 0.15s; }
.btn-run:hover:not(:disabled) { opacity: 0.85; }
.btn-run:disabled { cursor: not-allowed; }
.btn-run.shimmer { background: linear-gradient(90deg, var(--accent-blue) 25%, var(--accent-purple) 50%, var(--accent-blue) 75%); background-size: 200% 100%; animation: shimmer 1.5s ease-in-out infinite; }
@keyframes shimmer { 0% { background-position: 200% 0; } 100% { background-position: -200% 0; } }
.btn-stop { padding: 10px 20px; border: 1px solid var(--accent-red); border-radius: 8px; background: transparent; color: var(--accent-red); font-size: 0.9rem; cursor: pointer; }

.config { display: flex; flex-wrap: wrap; gap: 12px 20px; background: var(--surface-overlay); padding: 14px 20px; border-radius: 12px; margin-bottom: 20px; align-items: center; justify-content: center; }
.config-row { display: flex; align-items: center; gap: 8px; }
.config-row label { font-size: 0.85rem; font-weight: 600; color: var(--text-secondary); }
.config-row select { padding: 6px 10px; border: 1px solid var(--border-color); border-radius: 6px; background: #2a2a2e; color: #e4e4e7; font-size: 0.85rem; }
html[data-theme="light"] .config-row select { background: #fff; color: #1a202c; }
.config-row select:disabled { opacity: 0.5; }

.link-btn { padding: 2px 6px; border: 1px solid var(--border-color); border-radius: 4px; background: transparent; color: var(--text-secondary); font-size: 0.85rem; cursor: pointer; line-height: 1; opacity: 0.6; transition: all 0.15s; }
.link-btn:hover { opacity: 1; }
.link-btn.active { opacity: 1; color: var(--accent-blue); border-color: var(--accent-blue); background: rgba(96, 165, 250, 0.1); }
.link-btn:disabled { display: none; }

.config-hint { width: 100%; text-align: center; font-size: 0.85rem; color: #fbbf24; font-weight: 600; margin-bottom: 8px; }

.error-banner { background: rgba(248, 113, 113, 0.15); border: 1px solid var(--accent-red); color: var(--accent-red); padding: 10px 16px; border-radius: 8px; margin-bottom: 16px; font-size: 0.9rem; }

.summary { display: flex; gap: 20px; justify-content: center; background: var(--surface-overlay); padding: 16px 24px; border-radius: 12px; margin-bottom: 20px; }
.stat { display: flex; flex-direction: column; align-items: center; gap: 2px; }
.stat-label { font-size: 0.75rem; color: var(--text-secondary); text-transform: uppercase; letter-spacing: 0.5px; font-weight: 600; }
.stat-value { font-size: 1.4rem; font-weight: 700; font-family: "SF Mono", Monaco, Consolas, monospace; }

.rounds { display: flex; flex-direction: column; gap: 12px; }
.round-card { background: var(--surface-overlay); border: 1px solid var(--border-color); border-radius: 12px; padding: 16px; }
.round-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 10px; }
.round-num { font-weight: 600; font-size: 0.95rem; }
.round-payoff { font-family: "SF Mono", Monaco, Consolas, monospace; font-weight: 700; font-size: 1.1rem; }
.round-payoff.big-win { color: var(--accent-green); }
.round-payoff.loss { color: var(--accent-red); }

.round-choices { display: flex; align-items: center; gap: 10px; justify-content: center; margin-bottom: 8px; }
.choice-badge { padding: 4px 12px; border-radius: 6px; font-size: 0.85rem; font-weight: 500; }
.choice-badge.cooperative { background: rgba(74, 222, 128, 0.15); color: var(--accent-green); }
.choice-badge.defective { background: rgba(248, 113, 113, 0.15); color: var(--accent-red); }
.skipped-label { font-size: 0.85rem; color: var(--text-secondary); font-style: italic; }
.accuracy-dot { font-size: 1.1rem; font-weight: 700; }
.accuracy-dot.correct { color: var(--accent-green); }
.accuracy-dot.wrong { color: var(--accent-red); }

.reasoning { margin-top: 8px; }
.reasoning summary { cursor: pointer; font-size: 0.85rem; color: var(--text-secondary); }
.reasoning summary:hover { color: var(--text-primary); }
.reasoning-pair { display: grid; grid-template-columns: 1fr 1fr; gap: 12px; margin-top: 10px; }
.reasoning-solo { margin-top: 10px; }
.reasoning-block { background: var(--surface-overlay-dark); border-radius: 8px; padding: 10px 12px; }
.reasoning-role { display: block; font-size: 0.75rem; text-transform: uppercase; letter-spacing: 0.5px; font-weight: 600; color: var(--accent-purple); margin-bottom: 4px; }
.reasoning-block p { font-size: 0.85rem; line-height: 1.5; color: var(--text-secondary); }

.prompts-section { background: var(--surface-overlay); border: 1px solid var(--border-color); border-radius: 12px; padding: 14px 16px; margin-bottom: 16px; }
.prompts-section summary { cursor: pointer; font-size: 0.9rem; font-weight: 600; color: var(--text-secondary); }
.prompts-section summary:hover { color: var(--text-primary); }
.prompts-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 12px; margin-top: 12px; }
.prompt-block { background: var(--surface-overlay-dark); border-radius: 8px; padding: 10px 12px; }
.prompt-label { display: block; font-size: 0.75rem; text-transform: uppercase; letter-spacing: 0.5px; font-weight: 600; color: var(--accent-purple); margin-bottom: 6px; }
.prompt-block pre { font-size: 0.8rem; line-height: 1.5; color: var(--text-secondary); white-space: pre-wrap; word-break: break-word; font-family: "SF Mono", Monaco, Consolas, monospace; margin: 0; }

@media (max-width: 640px) {
  h1 { font-size: 1.4rem; }
  .summary { flex-direction: column; align-items: center; gap: 10px; }
  .reasoning-pair { grid-template-columns: 1fr; }
  .prompts-grid { grid-template-columns: 1fr; }
}
```
**index.html**

```html
<div id="root"></div>

```
**config.json**

```json
{
  "dependencies": {
    "react": "^19.2.4",
    "react-dom": "^19.2.4"
  },
  "description": "LLMs play classic decision theory problems: Newcomb's Problem, Parfit's Hitchhiker, and more."
}
```