---
format: typebulb/v1
name: Destroyed!
---

**code.tsx**

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

interface Technique {
  name: string;
  type: "fallacy" | "tactic" | "rhetoric";
  description: string;
  snippet: string;
}

interface Turn {
  speaker: string;
  text: string;
  techniques: Technique[];
}

interface ParticipantMetrics {
  logic_facts: number;
  sway: number;
  class: number;
  good_faith: number;
  novelty: number;
  summary: string;
}

interface TooltipContent {
  name: string;
  type?: string;
  description: string;
  color: string;
}

interface TooltipState {
  visible: boolean;
  x: number;
  y: number;
  content: TooltipContent | null;
}

interface Analysis {
  summary: string;
  turns: Turn[];
  participants: Record<string, ParticipantMetrics>;
  headline: string;
}

type MetricKey = Exclude<keyof ParticipantMetrics, 'summary'>;

const TECHNIQUE_COLORS: Record<Technique['type'], string> = {
  fallacy: '#ef4444',
  tactic: '#10b981',
  rhetoric: '#8b5cf6'
};

const METRICS: { key: MetricKey; label: string; shortLabel: string; description: string }[] = [
  { key: 'logic_facts', label: 'Logic & Facts', shortLabel: 'Logic', description: 'Is the reasoning valid & facts correct?' },
  { key: 'sway', label: 'Sway', shortLabel: 'Sway', description: 'Is it persuasive?' },
  { key: 'class', label: 'Class', shortLabel: 'Class', description: 'Is the conduct clean?' },
  { key: 'good_faith', label: 'Good Faith', shortLabel: 'Good Faith', description: 'Is the engagement sincere?' },
  { key: 'novelty', label: 'Novelty', shortLabel: 'Novelty', description: 'Is it fresh?' },
];

const SPEAKER_COLORS = ["#2563eb", "#db2777", "#059669", "#d97706", "#7c3aed"];

const TAGLINE = "Let AI judge who won the debate";

const CameraIcon = () => (
  <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round">
    <path d="M23 19a2 2 0 0 1-2 2H3a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h4l2-3h6l2 3h4a2 2 0 0 1 2 2z"/><circle cx="12" cy="13" r="4"/>
  </svg>
);

function Tooltip({ state }: { state: TooltipState }) {
  if (!state.visible || !state.content) return null;
  const { name, type, description, color } = state.content;

  return (
    <div className="technique-tooltip" style={{ left: state.x, top: state.y, borderColor: color }}>
      <div className="tooltip-header">
        <span className="tooltip-dot" style={{ backgroundColor: color }} />
        <span className="tooltip-name">{name}</span>
        {type && <span className="tooltip-type" style={{ color }}>{type}</span>}
      </div>
      <div className="tooltip-desc">{description}</div>
    </div>
  );
}

function VerdictText({ verdict, speakerColors, tabloidMode }: {
  verdict: { winner: string; verb: string; loser: string };
  speakerColors: Record<string, string>;
  tabloidMode: boolean;
}) {
  const displayVerb = tabloidMode ? verdict.verb : "debates";
  return <>
    <span style={{ color: speakerColors[verdict.winner] }}>{verdict.winner}</span>
    {" "}{displayVerb}{" "}
    <span style={{ color: speakerColors[verdict.loser], opacity: 0.7 }}>{verdict.loser}</span>
  </>;
}

function tooltipHandlers(
  onTooltip: (e: React.MouseEvent, content: TooltipContent | null) => void,
  content: TooltipContent
) {
  return {
    onMouseEnter: (e: React.MouseEvent) => onTooltip(e, content),
    onMouseMove: (e: React.MouseEvent) => onTooltip(e, content),
    onMouseLeave: (e: React.MouseEvent) => onTooltip(e, null),
  };
}

function MetricBar({ label, value, color, description, onTooltip }: {
  label: string;
  value: number;
  color?: string;
  description: string;
  onTooltip: (e: React.MouseEvent, content: TooltipContent | null) => void;
}) {
  const tooltipContent: TooltipContent = { name: label, description, color: color || '#666' };

  return (
    <div className="metric-container">
      <div className="metric-header">
        <span className="metric-label-group">
          {label}
          <span
            className="info-icon"
            {...tooltipHandlers(onTooltip, tooltipContent)}
          >?</span>
        </span>
        <span>{value}%</span>
      </div>
      <div className="metric-track" style={{ '--p-color': color } as React.CSSProperties}>
        <div className="metric-fill" style={{ width: `${value}%`, backgroundColor: color }} />
      </div>
    </div>
  );
}

function HighlightedText({ text, techniques, onTooltip }: {
  text: string;
  techniques: Technique[];
  onTooltip: (e: React.MouseEvent, content: TooltipContent | null) => void;
}) {
  if (!techniques || techniques.length === 0) return <span>{text}</span>;

  const matches: { start: number; end: number; tech: Technique }[] = [];
  techniques.forEach(tech => {
    if (!tech.snippet) return;
    let startPos = 0;
    while ((startPos = text.indexOf(tech.snippet, startPos)) !== -1) {
      matches.push({ start: startPos, end: startPos + tech.snippet.length, tech });
      startPos += tech.snippet.length;
    }
  });

  matches.sort((a, b) => a.start - b.start || (b.end - a.end));

  const result: React.ReactNode[] = [];
  let lastIndex = 0;

  for (const match of matches) {
    if (match.start < lastIndex) continue;
    if (match.start > lastIndex) result.push(text.substring(lastIndex, match.start));

    const color = TECHNIQUE_COLORS[match.tech.type];
    const tooltipContent: TooltipContent = {
      name: match.tech.name, type: match.tech.type, description: match.tech.description, color
    };
    result.push(
      <span
        key={`${match.tech.name}-${match.start}`}
        className={`highlight ${match.tech.type}`}
        style={{ '--highlight-color': color } as React.CSSProperties}
        {...tooltipHandlers(onTooltip, tooltipContent)}
      >
        {text.substring(match.start, match.end)}
      </span>
    );
    lastIndex = match.end;
  }

  if (lastIndex < text.length) result.push(text.substring(lastIndex));
  return <>{result}</>;
}

function ShareCard({ analysis, verdict, speakerColors, tabloidMode }: { 
  analysis: Analysis;
  verdict: { winner: string; verb: string; loser: string };
  speakerColors: Record<string, string>;
  tabloidMode: boolean;
}) {
  const displayTitle = tabloidMode ? "DESTROYED!" : "DEBATE ANALYZER";
  const participants = Object.entries(analysis.participants);
  const hasTwo = participants.length >= 2;

  return (
    <div className="share-card">
      <div className={`share-header fire-title ${tabloidMode ? 'glow' : ''}`}>{displayTitle}</div>
      <div className="share-verdict"><VerdictText verdict={verdict} speakerColors={speakerColors} tabloidMode={tabloidMode} /></div>
      {tabloidMode && <div className="share-headline sensational"><em>{analysis.headline}</em></div>}
      {analysis.summary && <div className="share-summary">{analysis.summary}</div>}

      {hasTwo ? (
        <div className="share-scores-dual">
          <div className="share-legend">
            <span className="share-legend-name" style={{ color: speakerColors[participants[0][0]] }}>{participants[0][0]}</span>
            <span className="share-legend-spacer" />
            <span className="share-legend-name" style={{ color: speakerColors[participants[1][0]] }}>{participants[1][0]}</span>
          </div>
          {METRICS.map(({ key, shortLabel }) => {
            const leftVal = participants[0][1][key] as number;
            const rightVal = participants[1][1][key] as number;
            return (
              <div className="share-dual-row" key={key}>
                <span className="share-dual-value">{leftVal}</span>
                <div className="share-dual-bar share-dual-bar-left">
                  <div className="share-dual-fill" style={{ width: `${leftVal}%`, backgroundColor: speakerColors[participants[0][0]] }} />
                </div>
                <span className="share-dual-label">{shortLabel}</span>
                <div className="share-dual-bar share-dual-bar-right">
                  <div className="share-dual-fill" style={{ width: `${rightVal}%`, backgroundColor: speakerColors[participants[1][0]] }} />
                </div>
                <span className="share-dual-value">{rightVal}</span>
              </div>
            );
          })}
        </div>
      ) : (
        <div className="share-scores">
          {participants.map(([name, data]) => (
            <div key={name} className="share-participant">
              <div className="share-name" style={{ color: speakerColors[name] }}>{name}</div>
              <div className="share-bars">
                {METRICS.map(({ key, shortLabel }) => (
                  <div key={key} className="share-bar-row">
                    <span>{shortLabel}</span>
                    <div className="share-bar-track">
                      <div className="share-bar-fill" style={{ width: `${data[key]}%`, backgroundColor: speakerColors[name] }} />
                    </div>
                    <span>{data[key]}</span>
                  </div>
                ))}
              </div>
            </div>
          ))}
        </div>
      )}
      <div className="share-footer">
        Let AI judge who won the debate at <span className="share-link">typebulb.com/u/samples/destroyed</span>
      </div>
    </div>
  );
}

function App() {
  const [analysis, setAnalysis] = useState<Analysis | null>(tb.insight<Analysis>());
  const [loading, setLoading] = useState(false);
  const [sharing, setSharing] = useState(false);
  const shareCardRef = useRef<HTMLDivElement>(null);
  const [tooltip, setTooltip] = useState<TooltipState>({ visible: false, x: 0, y: 0, content: null });
  const [tabloidMode, setTabloidMode] = React.useState(() => {
    return new URLSearchParams(window.location.search).get('analytical') !== 'true';
  });

  React.useEffect(() => {
    const url = new URL(window.location.href);
    if (!tabloidMode) {
      url.searchParams.set('analytical', 'true');
    } else {
      url.searchParams.delete('analytical');
    }
    window.history.replaceState({}, '', url);
  }, [tabloidMode]);

  const handleTooltip = (e: React.MouseEvent, content: TooltipContent | null) => {
    if (content) {
      const tooltipWidth = 300, tooltipHeight = 90, padding = 12, edgeMargin = 8;
      const vw = window.visualViewport?.width ?? window.innerWidth;
      const vh = window.visualViewport?.height ?? window.innerHeight;
      const offsetLeft = window.visualViewport?.offsetLeft ?? 0;
      const offsetTop = window.visualViewport?.offsetTop ?? 0;

      let x = e.clientX + padding, y = e.clientY + padding;
      if (x + tooltipWidth > vw + offsetLeft - edgeMargin) x = e.clientX - tooltipWidth - padding;
      if (y + tooltipHeight > vh + offsetTop - edgeMargin) y = e.clientY - tooltipHeight - padding;
      x = Math.max(edgeMargin + offsetLeft, Math.min(x, vw + offsetLeft - tooltipWidth - edgeMargin));
      y = Math.max(edgeMargin + offsetTop, Math.min(y, vh + offsetTop - tooltipHeight - edgeMargin));

      setTooltip({ visible: true, x, y, content });
    } else {
      setTooltip(prev => ({ ...prev, visible: false }));
    }
  };

  const runAnalysis = async () => {
    setLoading(true);
    try {
      const result = await tb.infer<Analysis>();
      if (result) setAnalysis(result);
    } finally {
      setLoading(false);
    }
  };

  const handleShare = async () => {
    if (!shareCardRef.current) return;
    setSharing(true);
    try {
      const canvas = await html2canvas(shareCardRef.current, { backgroundColor: '#121212', scale: 3 });
      canvas.toBlob(async (blob) => {
        if (!blob) return;
        try {
          await navigator.clipboard.write([new ClipboardItem({ 'image/png': blob })]);
          alert('Image copied to clipboard!');
        } catch {
          const url = URL.createObjectURL(blob);
          const a = document.createElement('a');
          a.href = url;
          a.download = 'destroyed-verdict.png';
          a.click();
          URL.revokeObjectURL(url);
        }
      }, 'image/png');
    } finally {
      setSharing(false);
    }
  };

  const getVerdict = (data: Analysis) => {
    const scores = Object.entries(data.participants).map(([name, m]) => ({
      name,
      score: (m.logic_facts * 1.5) + (m.sway * 1.2) + m.good_faith + (m.class * 0.8) + m.novelty
    })).sort((a, b) => b.score - a.score);

    if (scores.length < 1) return null;
    const [winner, runnerUp] = scores;
    if (!runnerUp) return { winner: winner.name, verb: "wins", loser: "" };

    const diff = winner.score - runnerUp.score;
    const verb = diff < 10 ? "edges out" : diff > 25 ? "destroys" : "beats";
    return { winner: winner.name, verb, loser: runnerUp.name };
  };

  const analysisButtonText = loading ? "Let's settle this..." : "Settle Another Debate";
  const displayTitle = tabloidMode ? "DESTROYED!" : "DEBATE ANALYZER";

  if (!analysis && !loading) {
    return (
      <div className="setup-screen">
        <h1 className={`hero-title fire-title ${tabloidMode ? 'glow' : ''}`}>{displayTitle}</h1>
        <p className="hero-subtitle">{TAGLINE}</p>
        <button onClick={runAnalysis} disabled={loading} className="primary-btn">{analysisButtonText}</button>
      </div>
    );
  }

  const speakers = analysis ? Object.keys(analysis.participants) : [];
  const speakerColors = Object.fromEntries(speakers.map((name, i) => [name, SPEAKER_COLORS[i % SPEAKER_COLORS.length]]));
  const verdict = analysis ? getVerdict(analysis) : null;
  const winnerName = verdict?.winner;

  return (
    <div className="container">
      <Tooltip state={tooltip} />
      <header className="main-header">
        <h1 className={`fire-title ${tabloidMode ? 'glow' : ''}`}>{displayTitle}</h1>
        <p className="header-tagline">{TAGLINE}</p>
      </header>

      {analysis && verdict && winnerName && (
        <>
          <section className="winner-announcement" style={{ borderColor: speakerColors[winnerName] }}>
            <h2 className="winner-name"><VerdictText verdict={verdict} speakerColors={speakerColors} tabloidMode={tabloidMode} /></h2>
            {tabloidMode && analysis && <p className="winner-headline sensational"><em>{analysis.headline}</em></p>}
            {analysis.summary && <p className="winner-summary">{analysis.summary}</p>}
            <div className="action-buttons">
              <button onClick={handleShare} title="Share As Image" disabled={sharing} className="share-btn">
                {sharing ? "..." : <CameraIcon />}
              </button>
              <button onClick={runAnalysis} disabled={loading} className="secondary-btn">
                {analysisButtonText}
              </button>
              <button
                onClick={() => setTabloidMode(!tabloidMode)}
                className={`tabloid-btn ${tabloidMode ? 'active' : ''}`}
              >
                {tabloidMode ? "Tabloid Mode" : "Analytical Mode"}
              </button>
            </div>
          </section>

          <div className="share-card-container" ref={shareCardRef}>
            <ShareCard analysis={analysis} verdict={verdict} speakerColors={speakerColors} tabloidMode={tabloidMode} />
          </div>
        </>
      )}

      <div className="layout-grid">
        <section className="conversation-section">
          <h2>Dialogue Breakdown</h2>
          <div className="conversation-list">
            {analysis?.turns.map((turn, i) => {
              const color = speakerColors[turn.speaker];
              const isSecond = speakers.indexOf(turn.speaker) % 2 === 1;
              return (
                <div key={i} className={`turn-card ${isSecond ? 'turn-alt' : ''}`} style={{ '--p-color': color } as React.CSSProperties}>
                  <div className="turn-speaker" style={{ color }}>{turn.speaker}</div>
                  <div className="turn-text">
                    <HighlightedText text={turn.text} techniques={turn.techniques} onTooltip={handleTooltip} />
                  </div>
                </div>
              );
            })}
          </div>
        </section>

        <section className="metrics-section">
          <h2>Performance Stats</h2>
          <div className="metrics-grid">
            {analysis && Object.entries(analysis.participants).map(([name, data]) => {
              const color = speakerColors[name];
              return (
                <div key={name} className="participant-card" style={{ '--p-color': color } as React.CSSProperties}>
                  <div className="participant-header">
                    <h3 style={{ color }}>{name}</h3>
                    {tabloidMode && name === winnerName && <span className="winner-badge">Winner</span>}
                  </div>
                  {METRICS.map(({ key, label, description }) => (
                    <MetricBar key={key} label={label} value={data[key]} color={color} description={description} onTooltip={handleTooltip} />
                  ))}
                  <p className="participant-summary">{data.summary}</p>
                </div>
              );
            })}
          </div>
        </section>
      </div>
    </div>
  );
}

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

```css
:root {
  --bg: #ffffff;
  --text: #1a1a1a;
  --border: #e0e0e0;
  --card-bg: #f9f9f9;
  --primary: #2563eb;
  --fallacy: #ef4444;
  --tactic: #10b981;
  --rhetoric: #8b5cf6;
  --winner-bg: #fffbeb;
  --winner-border: #f59e0b;
}

html[data-theme="dark"] {
  --bg: #121212;
  --text: #f1f5f9;
  --border: #2e2e2e;
  --card-bg: #1e1e1e;
  --primary: #3b82f6;
  --winner-bg: #451a0333;
  --winner-border: #fbbf24;
}

body {
  font-family: 'Inter', system-ui, -apple-system, sans-serif;
  background-color: var(--bg);
  color: var(--text);
  margin: 0;
  padding: 20px;
}

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

.main-header {
  display: flex;
  justify-content: space-between;
  align-items: center;
  flex-wrap: wrap;
  gap: 1rem;
  margin-bottom: 30px;
}

.main-header h1 {
  margin: 0;
  font-size: 1.5rem;
  font-weight: 800;
  letter-spacing: -0.02em;
}

.header-tagline {
  margin: 0;
  font-size: 0.85rem;
  opacity: 0.7;
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.05em;
}

@media (max-width: 650px) {
  .main-header {
    flex-direction: column;
    text-align: center;
    justify-content: center;
  }
  .header-tagline {
    text-align: center;
  }
}

/* Landing Screen */
.setup-screen {
  text-align: center;
  padding: 120px 20px;
}

.hero-title {
  font-size: 4rem;
  font-weight: 900;
  margin-bottom: 1rem;
  letter-spacing: -0.04em;
  line-height: 1;
}

.fire-title {
  color: #ffcc00 !important;
  -webkit-text-fill-color: initial !important;
  background: none !important;
}

.fire-title.glow {
  text-shadow:
    0 0 10px #ff6600,
    0 0 20px #ff4400,
    0 0 30px #ff2200,
    0 0 40px #cc0000,
    0 2px 4px rgba(0, 0, 0, 0.8) !important;
}

.hero-subtitle {
  font-size: 1.25rem;
  opacity: 0.7;
  max-width: 600px;
  margin: 0 auto 2rem;
}

/* Winner Banner */
.winner-announcement {
  background: var(--winner-bg);
  border: 2px solid var(--winner-border);
  border-radius: 20px;
  padding: 32px;
  margin-bottom: 40px;
  text-align: center;
  box-shadow: 0 10px 25px -5px rgba(0,0,0,0.1);
}

.verdict-label {
  display: inline-block;
  background: var(--winner-border);
  color: #000;
  font-size: 0.75rem;
  font-weight: 900;
  text-transform: uppercase;
  padding: 4px 12px;
  border-radius: 100px;
  margin-bottom: 12px;
}

.winner-name {
  font-size: 2.5rem;
  font-weight: 900;
  margin: 0 0 16px 0;
  letter-spacing: -0.02em;
  text-transform: uppercase;
}

.winner-headline {
  font-size: 1.25rem;
  line-height: 1.5;
  max-width: 800px;
  margin: 0 auto 16px;
  opacity: 0.8;
}

.winner-headline.sensational {
  font-size: 1.45rem;
}

.winner-summary {
  font-size: 1rem;
  line-height: 1.7;
  max-width: 800px;
  margin: 0 auto;
  opacity: 0.9;
  text-align: left;
}

/* Layout */
.layout-grid {
  display: grid;
  grid-template-columns: 1fr;
  gap: 32px;
}

@media (min-width: 1024px) {
  .layout-grid {
    grid-template-columns: 1.4fr 1fr;
  }
}

/* Conversation */
.conversation-list {
  display: flex;
  flex-direction: column;
  gap: 16px;
}

.turn-card {
  background: var(--card-bg);
  border: 1px solid var(--border);
  border-left: 5px solid var(--p-color, var(--primary));
  border-radius: 16px;
  padding: 16px 20px;
  max-width: 85%;
}

.turn-alt {
  align-self: flex-end;
  border-left: 1px solid var(--border);
  border-right: 5px solid var(--p-color, var(--primary));
  text-align: right;
}

.turn-speaker {
  font-weight: 800;
  font-size: 0.8rem;
  margin-bottom: 8px;
  text-transform: uppercase;
}

.highlight {
  padding: 0 2px;
  border-radius: 4px;
  cursor: pointer;
  font-weight: 600;
  text-decoration: underline;
  text-decoration-thickness: 2px;
  transition: all 0.2s ease;
}

.highlight:hover {
  filter: brightness(1.1);
  box-shadow: 0 2px 8px var(--highlight-color, rgba(0,0,0,0.2));
}

.highlight.fallacy { background: rgba(239, 68, 68, 0.15); text-decoration-color: var(--fallacy); }
.highlight.tactic { background: rgba(16, 185, 129, 0.15); text-decoration-color: var(--tactic); }
.highlight.rhetoric { background: rgba(139, 92, 246, 0.15); text-decoration-color: var(--rhetoric); }

/* Tooltip */
.technique-tooltip {
  position: fixed;
  z-index: 1000;
  background: var(--card-bg);
  border: 2px solid;
  border-radius: 10px;
  padding: 0.75rem 1rem;
  max-width: 300px;
  box-shadow: 0 8px 24px rgba(0,0,0,0.15), 0 0 0 1px var(--border);
  pointer-events: none;
  animation: tooltipFadeIn 0.15s ease-out;
}

@keyframes tooltipFadeIn {
  from {
    opacity: 0;
    transform: translateY(4px);
  }
  to {
    opacity: 1;
    transform: translateY(0);
  }
}

.tooltip-header {
  display: flex;
  align-items: center;
  gap: 0.5rem;
  margin-bottom: 0.4rem;
}

.tooltip-dot {
  width: 10px;
  height: 10px;
  border-radius: 50%;
  flex-shrink: 0;
}

.tooltip-name {
  font-weight: 700;
  font-size: 0.9rem;
  color: var(--text);
}

.tooltip-type {
  font-size: 0.7rem;
  font-weight: 600;
  text-transform: uppercase;
  margin-left: auto;
}

.tooltip-desc {
  font-size: 0.8rem;
  color: var(--text);
  opacity: 0.8;
  line-height: 1.4;
}

/* Metrics */
.metrics-grid {
  display: flex;
  flex-direction: column;
  gap: 20px;
}

.participant-card {
  background: var(--card-bg);
  border: 1px solid var(--border);
  border-radius: 16px;
  padding: 24px;
}

.participant-header {
  display: flex;
  justify-content: space-between;
  align-items: center;
  margin-bottom: 20px;
}

.participant-header h3 {
  margin: 0;
  font-size: 1.25rem;
  font-weight: 800;
}

.winner-badge {
  background: var(--winner-border);
  color: #000;
  font-size: 0.7rem;
  font-weight: 800;
  padding: 2px 8px;
  border-radius: 4px;
}

.metric-container { margin-bottom: 12px; }

.participant-summary {
  margin: 16px 0 0 0;
  padding-top: 16px;
  border-top: 1px solid var(--border);
  font-size: 0.85rem;
  line-height: 1.6;
  opacity: 0.85;
}

.metric-header {
  display: flex;
  justify-content: space-between;
  font-size: 0.8rem;
  margin-bottom: 4px;
  font-weight: 600;
}

.metric-label-group {
  display: flex;
  align-items: center;
  gap: 6px;
}

.info-icon {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 14px;
  height: 14px;
  background: var(--border);
  color: var(--text);
  font-size: 10px;
  border-radius: 50%;
  cursor: pointer;
  opacity: 0.5;
  transition: all 0.2s;
}

.info-icon:hover {
  opacity: 1;
  transform: scale(1.1);
}

.metric-track {
  height: 8px;
  background: var(--border);
  border-radius: 4px;
  overflow: hidden;
}

.metric-fill {
  height: 100%;
  transition: width 1s cubic-bezier(0.16, 1, 0.3, 1);
}

.action-buttons {
  display: flex;
  justify-content: center;
  align-items: center;
  gap: 24px;
  margin-top: 24px;
  flex-wrap: wrap;
}

.tabloid-btn {
  background: var(--card-bg);
  border: 1px solid var(--border);
  color: var(--text);
  padding: 0 24px;
  height: 44px;
  display: flex;
  align-items: center;
  justify-content: center;
  border-radius: 100px;
  font-weight: 800;
  text-transform: uppercase;
  font-size: 0.75rem;
  letter-spacing: 0.05em;
  cursor: pointer;
  transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);
}

.tabloid-btn:hover {
  background: var(--border);
}

.tabloid-btn.active {
  background: #ffcc00;
  border-color: #ffcc00;
  color: #000;
  box-shadow: 0 0 15px rgba(255, 204, 0, 0.3);
}

.tabloid-btn.active:hover {
  background: #ffd633;
  border-color: #ffd633;
  box-shadow: 0 0 20px rgba(255, 204, 0, 0.5);
}

.tabloid-btn.active:active {
  transform: scale(0.98);
}

/* Buttons & Controls */
.primary-btn {
  background: var(--primary);
  color: white;
  border: none;
  padding: 16px 40px;
  font-size: 1.25rem;
  font-weight: 800;
  border-radius: 100px;
  cursor: pointer;
  transition: transform 0.2s;
}

.primary-btn:hover { transform: scale(1.05); }

.secondary-btn {
  background: var(--card-bg);
  border: 1px solid var(--border);
  color: var(--text);
  padding: 0 24px;
  height: 44px;
  display: flex;
  align-items: center;
  justify-content: center;
  border-radius: 100px;
  font-weight: 700;
  cursor: pointer;
  transition: all 0.2s;
}

.secondary-btn:hover {
  background: var(--border);
}

.share-btn {
  background: var(--card-bg);
  border: 1px solid var(--border);
  color: var(--text);
  padding: 0;
  width: 44px;
  height: 44px;
  display: flex;
  align-items: center;
  justify-content: center;
  border-radius: 50%;
  cursor: pointer;
  transition: all 0.2s;
}

.share-btn:hover:not(:disabled) {
  background: var(--border);
  transform: scale(1.1);
}

.share-btn:disabled {
  opacity: 0.6;
  cursor: not-allowed;
}

/* ===== Share Card (rendered offscreen for capture) ===== */
.share-card-container {
  position: absolute;
  left: -9999px;
  top: 0;
}

.share-card {
  width: 1080px;
  padding: 80px;
  background: linear-gradient(145deg, #121212, #1e1e1e);
  font-family: 'Inter', system-ui, sans-serif;
  color: #f1f5f9;
  text-align: center;
  box-sizing: border-box;
  font-size: 16px;
}

.share-header {
  font-size: 40px;
  font-weight: 900;
  letter-spacing: 0.05em;
  margin-bottom: 32px;
  text-align: center;
}

.share-verdict {
  font-size: 60px;
  font-weight: 900;
  text-transform: uppercase;
  letter-spacing: -0.02em;
  margin-bottom: 28px;
  text-align: center;
}

.share-headline {
  font-size: 36px;
  line-height: 1.5;
  opacity: 0.7;
  margin-bottom: 28px;
  text-align: center;
}

.share-headline.sensational {
  font-size: 40px;
  opacity: 0.9;
}

.share-summary {
  font-size: 32px;
  line-height: 1.6;
  opacity: 0.85;
  margin-bottom: 48px;
  padding-bottom: 40px;
  border-bottom: 2px solid #334155;
  text-align: left;
}

/* ===== Dual butterfly chart ===== */
.share-scores-dual {
  width: 100%;
  text-align: left;
}

.share-legend {
  display: flex;
  align-items: center;
  margin-bottom: 32px;
}

.share-legend-name {
  font-size: 40px;
  font-weight: 800;
  text-transform: uppercase;
  flex: 1;
}

.share-legend-name:first-child {
  text-align: left;
}

.share-legend-name:last-child {
  text-align: right;
}

.share-legend-spacer {
  width: 180px;
  flex-shrink: 0;
}

.share-dual-row {
  display: flex;
  align-items: center;
  gap: 16px;
  margin-bottom: 20px;
}

.share-dual-value {
  width: 64px;
  font-size: 34px;
  font-weight: 700;
  text-align: center;
  flex-shrink: 0;
}

.share-dual-bar {
  flex: 1;
  height: 28px;
  background: #2e2e2e;
  border-radius: 14px;
  overflow: hidden;
  position: relative;
}

.share-dual-bar-left {
  display: flex;
  justify-content: flex-end;
}

.share-dual-bar-right {
  display: flex;
  justify-content: flex-start;
}

.share-dual-fill {
  height: 100%;
  border-radius: 14px;
  min-width: 4px;
}

.share-dual-label {
  width: 180px;
  text-align: center;
  font-size: 28px;
  font-weight: 700;
  opacity: 0.6;
  flex-shrink: 0;
  text-transform: uppercase;
  letter-spacing: 0.03em;
}

/* ===== Fallback: single-participant card (unchanged) ===== */
.share-scores {
  display: flex;
  gap: 32px;
  text-align: left;
}

.share-participant {
  flex: 1;
}

.share-name {
  font-size: 36px;
  font-weight: 800;
  text-transform: uppercase;
  margin-bottom: 20px;
}

.share-bars {
  display: flex;
  flex-direction: column;
  gap: 10px;
}

.share-bar-row {
  display: flex;
  align-items: center;
  gap: 12px;
  font-size: 28px;
  font-weight: 600;
}

.share-bar-row > span:first-child {
  width: 120px;
  opacity: 0.7;
}

.share-bar-row > span:last-child {
  width: 48px;
  text-align: right;
}

.share-bar-track {
  flex: 1;
  height: 24px;
  background: #2e2e2e;
  border-radius: 12px;
  overflow: hidden;
}

.share-bar-fill {
  height: 100%;
  border-radius: 12px;
}

.share-footer {
  margin-top: 48px;
  padding-top: 28px;
  border-top: 2px solid #2e2e2e;
  font-size: 26px;
  font-weight: 600;
  opacity: 0.5;
  text-align: center;
}

.share-link {
  color: #ffcc00;
}
```
**index.html**

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

```
**data.txt**

```txt
DANNY: Hey, Dr. Peterson. How are you doing? Danny, nice to meet you.

JORDAN PETERSON: What's your name?

DANNY: Danny.

JORDAN PETERSON: Danny.

DANNY: You're saying atheists worship things or people or places, whatever. Um, can you be very clear about your definition of worship again?

JORDAN PETERSON: Attend to, prioritize, and sacrifice for.

DANNY: Okay, that's it. That's your understanding of worship.

JORDAN PETERSON: Well, I could flesh it out, but that'll do for the time we have.

DANNY: Okay. Do Catholics attend to Mary?

JORDAN PETERSON: Well, yes.

DANNY: Okay. So, do they fit that description of worship?

JORDAN PETERSON: Yes.

DANNY: So, you would say Catholics and other people that rever Mary like the Eastern Orthodox tradition worship Mary.

JORDAN PETERSON: Well, they might not put her in the highest

DANNY: but you would put it that way.

JORDAN PETERSON: No

DANNY: You just said it. Now you're taking it back.

JORDAN PETERSON: There's still a hierarchy.

DANNY: Okay, there's a hierarchy But in within the...

JORDAN PETERSON: There's something at the top.

DANNY: All right, you can worship below.

JORDAN PETERSON: Mary is quite a ways up the hierarchy, but not at the top.

DANNY: Let's go over your definition of worship again. What's your definition of worship?

JORDAN PETERSON: attend to. prioritize?

DANNY: Do Catholics attend to? Um, do they prioritize Mary over all other human beings?

JORDAN PETERSON: No, I didn't say overall, did I? I didn't add that to my

DANNY: You understand?

JORDAN PETERSON: I said there was a hierarchy as well. You can attend to something trivially or you can attend to it deeply.

DANNY: And now you're adding stuff to the definition, but your original definition....

JORDAN PETERSON: I added the hierarchy part at the beginning.

DANNY: Are you familiar with the immaculate conception?

JORDAN PETERSON: Why is that relevant?

DANNY: Because you go to a Catholic church, don't you? Or you've attended recently. You're interested in Catholicism, aren't you?

JORDAN PETERSON: Sure.

DANNY: All right. Are you familiar with their doctrines somewhat? Okay. You're you're familiar. How do they regard How do they regard Mary?

JORDAN PETERSON: Why are you asking me that?

DANNY: Because you're a Christian.

JORDAN PETERSON: You say that. I haven't claimed that.

DANNY: Oh, what is this? Is this Christians versus atheist?

JORDAN PETERSON: I don't know.

DANNY: You don't know where you are right now.

JORDAN PETERSON: Don't be a smartass.

DANNY: Either you're a Christian or you're not.

JORDAN PETERSON: I won't talk to you if you're a smart ass.

DANNY: Either you're a Christian or you're not. Which one is it?

JORDAN PETERSON: I could be either of them, but I don't have to tell you.

DANNY: You don't have to tell me. I was under the impression I was invited to talk to a Christian. Am I not talking to a Christian?

JORDAN PETERSON: You were invited to...

DANNY: I think everyone should look at the title of the YouTube channel. You're probably in the wrong YouTube video.

JORDAN PETERSON: You're really quite something.

DANNY: Aren't I? But you're really quite nothing, right? You're not a Christian.

JORDAN PETERSON: Oh, yeah. I'm done with him.
```
**infer.md**

```md
Analyze the conversation data. This data may be a raw copy-paste from a social media thread (X/Twitter), so will contain a lot of noise. You need to filter out the junk, emojis from names etc., and capture a clean, chronological conversation. Do NOT leave any messages out! Every distinct message block must be a `Turn`.

### Analysis Tasks:
For each `Turn`:
- The `snippet` MUST be an exact verbatim substring of the `text` field.
- Identify `techniques` (tactics, rhetoric, fallacies).
- Evaluating analogies, anecodotes, and authoritative quotes:
 - Good: rapidly conveying an idea's salient aspect or possibility
 - Bad: as sufficient proof for the general case

After the turns, provide:

1. **`participants`** — metrics (0-100) for each speaker:
   - **logic_facts**: Is the reasoning valid & facts correct?
   - **sway**: Is it persuasive?
   - **class**: Is the conduct clean? (100 = High class/Professional, 0 = Low class/Hostile)
   - **good faith**: Model intent, not just content. What is each participant optimizing for? Truth, genuine curiosity & clarity, or just victory?
   - **novelty**: Is it fresh?
   - **summary**: 3-4 sentence sports-commentator-style analysis. Reference specific highs and lows. E.g., "Their logic score took a hit from the false equivalency, but their sway remained strong due to..."
2. **`summary`** — A 2-3 sentence summary capturing the crux of debate. Write in a thoughtful, analytical tone that contrasts with the tabloid headline energy.
3. **`headline`** — A punchy, sensational 1 sentence verdict for the winner banner. Think tabloid sports headline energy."

### Score Calibration
- **30 or below**: Significant weakness — major errors, hostility, or failure
- **40-50**: Below average — noticeable problems
- **50-60**: Average — competent but unremarkable
- **60-70**: Above average — solid performance
- **70-80**: Strong — notably effective
- **85+**: Exceptional — outstanding in this dimension

Output as valid JSON matching the `Analysis` interface in code.tsx.

```
**insight.json**

```json
{
  "summary": "Danny utilizes a focused Socratic line of questioning to exploit Jordan Peterson's broad definition of 'worship,' successfully maneuvering the academic into a contradiction regarding Catholic doctrine. The exchange rapidly deteriorates from a theological inquiry into a personal confrontation over Peterson's famously elusive religious identity, culminating in a complete breakdown of decorum and Peterson's abrupt departure.",
  "turns": [
    {
      "speaker": "DANNY",
      "text": "Hey, Dr. Peterson. How are you doing? Danny, nice to meet you.",
      "techniques": []
    },
    {
      "speaker": "JORDAN PETERSON",
      "text": "What's your name?",
      "techniques": []
    },
    {
      "speaker": "DANNY",
      "text": "Danny.",
      "techniques": []
    },
    {
      "speaker": "JORDAN PETERSON",
      "text": "Danny.",
      "techniques": []
    },
    {
      "speaker": "DANNY",
      "text": "You're saying atheists worship things or people or places, whatever. Um, can you be very clear about your definition of worship again?",
      "techniques": [
        {
          "name": "Strategic Questioning",
          "type": "tactic",
          "description": "Setting a baseline by requesting a precise definition that can be tested for logical consistency later.",
          "snippet": "can you be very clear about your definition of worship again?"
        }
      ]
    },
    {
      "speaker": "JORDAN PETERSON",
      "text": "Attend to, prioritize, and sacrifice for.",
      "techniques": [
        {
          "name": "Equivocation",
          "type": "fallacy",
          "description": "Broadening a definition so widely that it encompasses activities not typically associated with the term, creating a vulnerable logical foundation.",
          "snippet": "Attend to, prioritize, and sacrifice for."
        }
      ]
    },
    {
      "speaker": "DANNY",
      "text": "Okay, that's it. That's your understanding of worship.",
      "techniques": []
    },
    {
      "speaker": "JORDAN PETERSON",
      "text": "Well, I could flesh it out, but that'll do for the time we have.",
      "techniques": [
        {
          "name": "Hedging",
          "type": "rhetoric",
          "description": "Qualifying a statement to allow for later pivots if the logic is challenged.",
          "snippet": "I could flesh it out, but that'll do for the time we have."
        }
      ]
    },
    {
      "speaker": "DANNY",
      "text": "Okay. Do Catholics attend to Mary?",
      "techniques": [
        {
          "name": "Socratic Trap",
          "type": "tactic",
          "description": "Leading the opponent to agree to a minor premise that logically forces them into a contradiction with their original definition.",
          "snippet": "Do Catholics attend to Mary?"
        }
      ]
    },
    {
      "speaker": "JORDAN PETERSON",
      "text": "Well, yes.",
      "techniques": []
    },
    {
      "speaker": "DANNY",
      "text": "Okay. So, do they fit that description of worship?",
      "techniques": []
    },
    {
      "speaker": "JORDAN PETERSON",
      "text": "Yes.",
      "techniques": []
    },
    {
      "speaker": "DANNY",
      "text": "So, you would say Catholics and other people that rever Mary like the Eastern Orthodox tradition worship Mary.",
      "techniques": []
    },
    {
      "speaker": "JORDAN PETERSON",
      "text": "Well, they might not put her in the highest",
      "techniques": [
        {
          "name": "Moving the Goalposts",
          "type": "fallacy",
          "description": "Adding a new requirement ('the highest') to a definition only after the original criteria were successfully met by a counter-example.",
          "snippet": "might not put her in the highest"
        }
      ]
    },
    {
      "speaker": "DANNY",
      "text": "but you would put it that way.",
      "techniques": []
    },
    {
      "speaker": "JORDAN PETERSON",
      "text": "No",
      "techniques": []
    },
    {
      "speaker": "DANNY",
      "text": "You just said it. Now you're taking it back.",
      "techniques": [
        {
          "name": "Pointing Out Inconsistency",
          "type": "tactic",
          "description": "Directly highlighting the opponent's immediate self-contradiction to undermine their credibility.",
          "snippet": "You just said it. Now you're taking it back."
        }
      ]
    },
    {
      "speaker": "JORDAN PETERSON",
      "text": "There's still a hierarchy.",
      "techniques": [
        {
          "name": "Ad Hoc Rescue",
          "type": "fallacy",
          "description": "Introducing a spontaneous new element to save a theory from being falsified by an uncomfortable example.",
          "snippet": "There's still a hierarchy."
        }
      ]
    },
    {
      "speaker": "DANNY",
      "text": "Okay, there's a hierarchy But in within the...",
      "techniques": []
    },
    {
      "speaker": "JORDAN PETERSON",
      "text": "There's something at the top.",
      "techniques": []
    },
    {
      "speaker": "DANNY",
      "text": "All right, you can worship below.",
      "techniques": []
    },
    {
      "speaker": "JORDAN PETERSON",
      "text": "Mary is quite a ways up the hierarchy, but not at the top.",
      "techniques": []
    },
    {
      "speaker": "DANNY",
      "text": "Let's go over your definition of worship again. What's your definition of worship?",
      "techniques": []
    },
    {
      "speaker": "JORDAN PETERSON",
      "text": "attend to. prioritize?",
      "techniques": []
    },
    {
      "speaker": "DANNY",
      "text": "Do Catholics attend to? Um, do they prioritize Mary over all other human beings?",
      "techniques": []
    },
    {
      "speaker": "JORDAN PETERSON",
      "text": "No, I didn't say overall, did I? I didn't add that to my",
      "techniques": [
        {
          "name": "Semantic Evasion",
          "type": "rhetoric",
          "description": "Quibbling over specific wording to avoid the logical consequences of the point being made.",
          "snippet": "I didn't say overall, did I?"
        }
      ]
    },
    {
      "speaker": "DANNY",
      "text": "You understand?",
      "techniques": []
    },
    {
      "speaker": "JORDAN PETERSON",
      "text": "I said there was a hierarchy as well. You can attend to something trivially or you can attend to it deeply.",
      "techniques": []
    },
    {
      "speaker": "DANNY",
      "text": "And now you're adding stuff to the definition, but your original definition....",
      "techniques": []
    },
    {
      "speaker": "JORDAN PETERSON",
      "text": "I added the hierarchy part at the beginning.",
      "techniques": [
        {
          "name": "Revisionist Memory",
          "type": "tactic",
          "description": "Attempting to rewrite the history of the conversation to maintain a facade of consistency.",
          "snippet": "I added the hierarchy part at the beginning."
        }
      ]
    },
    {
      "speaker": "DANNY",
      "text": "Are you familiar with the immaculate conception?",
      "techniques": []
    },
    {
      "speaker": "JORDAN PETERSON",
      "text": "Why is that relevant?",
      "techniques": [
        {
          "name": "Deflection",
          "type": "tactic",
          "description": "Questioning relevance to stall or avoid answering a potentially damaging question.",
          "snippet": "Why is that relevant?"
        }
      ]
    },
    {
      "speaker": "DANNY",
      "text": "Because you go to a Catholic church, don't you? Or you've attended recently. You're interested in Catholicism, aren't you?",
      "techniques": []
    },
    {
      "speaker": "JORDAN PETERSON",
      "text": "Sure.",
      "techniques": []
    },
    {
      "speaker": "DANNY",
      "text": "All right. Are you familiar with their doctrines somewhat? Okay. You're you're familiar. How do they regard How do they regard Mary?",
      "techniques": []
    },
    {
      "speaker": "JORDAN PETERSON",
      "text": "Why are you asking me that?",
      "techniques": []
    },
    {
      "speaker": "DANNY",
      "text": "Because you're a Christian.",
      "techniques": []
    },
    {
      "speaker": "JORDAN PETERSON",
      "text": "You say that. I haven't claimed that.",
      "techniques": [
        {
          "name": "Evasion of Identity",
          "type": "rhetoric",
          "description": "Refusing to accept a standard label to avoid the intellectual or personal responsibilities associated with it.",
          "snippet": "I haven't claimed that."
        }
      ]
    },
    {
      "speaker": "DANNY",
      "text": "Oh, what is this? Is this Christians versus atheist?",
      "techniques": []
    },
    {
      "speaker": "JORDAN PETERSON",
      "text": "I don't know.",
      "techniques": []
    },
    {
      "speaker": "DANNY",
      "text": "You don't know where you are right now.",
      "techniques": [
        {
          "name": "Ridicule",
          "type": "rhetoric",
          "description": "Mocking the opponent's confusion or evasiveness to weaken their perceived authority.",
          "snippet": "You don't know where you are right now."
        }
      ]
    },
    {
      "speaker": "JORDAN PETERSON",
      "text": "Don't be a smartass.",
      "techniques": [
        {
          "name": "Tone Policing",
          "type": "tactic",
          "description": "Attacking the opponent's attitude or delivery style to derail the actual argument.",
          "snippet": "Don't be a smartass."
        }
      ]
    },
    {
      "speaker": "DANNY",
      "text": "Either you're a Christian or you're not.",
      "techniques": [
        {
          "name": "False Dilemma",
          "type": "fallacy",
          "description": "Presenting only two binary options in a complex situation to force a definitive stance.",
          "snippet": "Either you're a Christian or you're not."
        }
      ]
    },
    {
      "speaker": "JORDAN PETERSON",
      "text": "I won't talk to you if you're a smart ass.",
      "techniques": [
        {
          "name": "Personal Offense",
          "type": "rhetoric",
          "description": "Using feelings of being insulted as a justification for shutting down dialogue.",
          "snippet": "I won't talk to you if you're a smart ass."
        }
      ]
    },
    {
      "speaker": "DANNY",
      "text": "Either you're a Christian or you're not. Which one is it?",
      "techniques": []
    },
    {
      "speaker": "JORDAN PETERSON",
      "text": "I could be either of them, but I don't have to tell you.",
      "techniques": []
    },
    {
      "speaker": "DANNY",
      "text": "You don't have to tell me. I was under the impression I was invited to talk to a Christian. Am I not talking to a Christian?",
      "techniques": []
    },
    {
      "speaker": "JORDAN PETERSON",
      "text": "You were invited to...",
      "techniques": []
    },
    {
      "speaker": "DANNY",
      "text": "I think everyone should look at the title of the YouTube channel. You're probably in the wrong YouTube video.",
      "techniques": [
        {
          "name": "Meta-Pressure",
          "type": "tactic",
          "description": "Using the external context (YouTube audience/expectations) to shame the opponent into conforming to a role.",
          "snippet": "look at the title of the YouTube channel"
        }
      ]
    },
    {
      "speaker": "JORDAN PETERSON",
      "text": "You're really quite something.",
      "techniques": [
        {
          "name": "Ad Hominem",
          "type": "fallacy",
          "description": "Directly attacking the character of the speaker instead of the logic of their points.",
          "snippet": "You're really quite something."
        }
      ]
    },
    {
      "speaker": "DANNY",
      "text": "Aren't I? But you're really quite nothing, right? You're not a Christian.",
      "techniques": [
        {
          "name": "Personal Insult",
          "type": "tactic",
          "description": "Using derogatory language to provoke the opponent and end the intellectual phase of the debate.",
          "snippet": "you're really quite nothing"
        }
      ]
    },
    {
      "speaker": "JORDAN PETERSON",
      "text": "Oh, yeah. I'm done with him.",
      "techniques": [
        {
          "name": "Rage Quit",
          "type": "tactic",
          "description": "Abruptly terminating the engagement to avoid further scrutiny or personal discomfort.",
          "snippet": "I'm done with him."
        }
      ]
    }
  ],
  "participants": {
    "DANNY": {
      "logic_facts": 82,
      "sway": 88,
      "class": 30,
      "good_faith": 45,
      "novelty": 75,
      "summary": "Danny demonstrated high technical proficiency by trapping Peterson in a definitional cage, expertly navigating the subject's tendency toward expansive metaphors. While his logical lead-in was flawless, his class score suffered significantly as he abandoned professional decorum for schoolyard insults once the trap was set. Ultimately, he dominated the rhetorical space, forcing a veteran debater into an embarrassing and hasty retreat."
    },
    "JORDAN PETERSON": {
      "logic_facts": 40,
      "sway": 25,
      "class": 38,
      "good_faith": 30,
      "novelty": 50,
      "summary": "Peterson struggled from the outset, offering an overly broad definition of 'worship' that backfired immediately when applied to specific religious contexts. His attempts to retroactively move the goalposts by introducing 'hierarchies' felt transparently desperate and reactive. By refusing to answer basic questions about his identity and eventually walking away, he conceded total rhetorical victory to his opponent."
    }
  },
  "headline": "PETERSON PARALYZED! DEFINITION DISASTER LEADS TO EPIC RAGE QUIT AS DANNY SNAPS THE TRAP!"
}

```
**config.json**

```json
{
  "dependencies": {
    "react": "^19.2.3",
    "react-dom": "^19.2.3",
    "html2canvas": "^1.4.1"
  },
  "description": "Let AI judge who won the debate.",
  "inference": {
    "title": "Let's Settle This...",
    "dataTitle": "Paste in the debate here",
    "submitTitle": "Who won?"
  }
}
```