split-personality / index.html
vincentoh's picture
Fix RLHF attribution β€” generalize to instruction tuning (SFT + RLHF)
d5c83b9 verified
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Split Personality β€” bigsnarfdude</title>
<link href="https://fonts.googleapis.com/css2?family=Instrument+Serif:ital@0;1&family=DM+Mono:wght@400;500&family=Syne:wght@400;600;700;800&display=swap" rel="stylesheet">
<style>
:root { --bg:#0a0a0f; --surface:#111118; --border:#1e1e2e; --accent:#ff4d6d; --accent2:#7c3aed; --gold:#f4b942; --text:#e8e8f0; --muted:#6b6b8a; --green:#22c55e; }
* { margin:0; padding:0; box-sizing:border-box; }
body { background:var(--bg); color:var(--text); font-family:'Syne',sans-serif; line-height:1.6; overflow-x:hidden; }
body::before { content:''; position:fixed; inset:0; background-image:linear-gradient(rgba(124,58,237,0.03) 1px,transparent 1px),linear-gradient(90deg,rgba(124,58,237,0.03) 1px,transparent 1px); background-size:60px 60px; pointer-events:none; z-index:0; }
.noise { position:fixed; inset:0; opacity:0.025; background-image:url("data:image/svg+xml,%3Csvg viewBox='0 0 256 256' xmlns='http://www.w3.org/2000/svg'%3E%3Cfilter id='noise'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.9' numOctaves='4' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23noise)'/%3E%3C/svg%3E"); pointer-events:none; z-index:0; }
.container { max-width:900px; margin:0 auto; padding:0 2rem; position:relative; z-index:1; }
.hero { padding:80px 0 60px; border-bottom:1px solid var(--border); }
.preprint-badge { display:inline-flex; align-items:center; gap:8px; background:rgba(244,185,66,0.1); border:1px solid rgba(244,185,66,0.3); color:var(--gold); font-family:'DM Mono',monospace; font-size:0.7rem; letter-spacing:0.15em; padding:6px 14px; border-radius:2px; margin-bottom:2rem; animation:fadeUp 0.6s ease both; }
.preprint-badge::before { content:'β–Ά'; font-size:0.6rem; }
h1 { font-family:'Instrument Serif',serif; font-size:clamp(2.2rem,5vw,3.8rem); line-height:1.1; font-weight:400; margin-bottom:1rem; animation:fadeUp 0.6s 0.1s ease both; }
h1 em { font-style:italic; color:var(--accent); }
.lede { font-family:'Instrument Serif',serif; font-style:italic; font-size:1.15rem; color:#9090b8; line-height:1.6; margin-bottom:2rem; max-width:680px; animation:fadeUp 0.6s 0.15s ease both; }
.author-line { display:flex; align-items:center; gap:1rem; flex-wrap:wrap; animation:fadeUp 0.6s 0.2s ease both; }
.author { display:flex; align-items:center; gap:8px; font-size:0.9rem; color:var(--text); }
.author-avatar { width:32px; height:32px; background:linear-gradient(135deg,var(--accent2),var(--accent)); border-radius:50%; display:flex; align-items:center; justify-content:center; font-size:0.7rem; font-weight:700; color:white; }
.author a { color:var(--accent2); text-decoration:none; font-family:'DM Mono',monospace; font-size:0.8rem; }
.author a:hover { color:var(--accent); }
.tag { font-family:'DM Mono',monospace; font-size:0.7rem; padding:3px 10px; border-radius:2px; letter-spacing:0.05em; }
.tag-red { background:rgba(255,77,109,0.15); color:var(--accent); border:1px solid rgba(255,77,109,0.3); }
.tag-purple { background:rgba(124,58,237,0.15); color:#a78bfa; border:1px solid rgba(124,58,237,0.3); }
.updated-badge { display:inline-flex; align-items:center; gap:6px; background:rgba(34,197,94,0.1); border:1px solid rgba(34,197,94,0.25); color:var(--green); font-family:'DM Mono',monospace; font-size:0.6rem; letter-spacing:0.1em; padding:4px 10px; border-radius:2px; }
.updated-badge::before { content:'↻ '; }
/* Story origin block */
.origin { background:var(--surface); border-left:3px solid var(--accent2); padding:1.5rem 1.8rem; margin:3rem 0; font-family:'Instrument Serif',serif; font-style:italic; font-size:1rem; line-height:1.8; color:#c0c0d8; animation:fadeUp 0.6s 0.3s ease both; }
.findings-strip { display:grid; grid-template-columns:repeat(3,1fr); gap:1px; background:var(--border); border:1px solid var(--border); margin:3rem 0; animation:fadeUp 0.6s 0.3s ease both; }
.finding-cell { background:var(--surface); padding:1.5rem; text-align:center; transition:background 0.2s; }
.finding-cell:hover { background:#15151f; }
.finding-number { font-family:'Instrument Serif',serif; font-size:2.8rem; line-height:1; margin-bottom:0.3rem; }
.finding-number.red { color:var(--accent); } .finding-number.purple { color:#a78bfa; } .finding-number.gold { color:var(--gold); }
.finding-label { font-family:'DM Mono',monospace; font-size:0.65rem; color:var(--muted); letter-spacing:0.1em; text-transform:uppercase; line-height:1.4; }
section { padding:3rem 0; border-bottom:1px solid var(--border); animation:fadeUp 0.6s 0.35s ease both; }
.section-label { font-family:'DM Mono',monospace; font-size:0.65rem; letter-spacing:0.2em; text-transform:uppercase; color:var(--muted); margin-bottom:1.2rem; display:flex; align-items:center; gap:12px; }
.section-label::after { content:''; flex:1; height:1px; background:var(--border); }
h2 { font-family:'Instrument Serif',serif; font-size:1.8rem; font-weight:400; margin-bottom:1rem; color:var(--text); }
p { font-size:0.95rem; color:#b0b0c8; line-height:1.8; margin-bottom:1rem; }
/* The core diagram β€” from the split-personality post */
.diagram { background:#0d0d14; border:1px solid var(--border); padding:1.5rem; margin:1.5rem 0; font-family:'DM Mono',monospace; font-size:0.75rem; color:#a0a0c0; line-height:1.9; overflow-x:auto; }
.diagram .label { color:var(--accent2); font-weight:500; }
.diagram .highlight { color:var(--accent); }
.diagram .good { color:var(--green); }
.callout { background:linear-gradient(135deg,rgba(255,77,109,0.08),rgba(124,58,237,0.08)); border:1px solid rgba(255,77,109,0.2); border-radius:4px; padding:1.5rem; margin:1.5rem 0; }
.callout-title { font-family:'DM Mono',monospace; font-size:0.7rem; letter-spacing:0.15em; text-transform:uppercase; color:var(--accent); margin-bottom:0.7rem; display:flex; align-items:center; gap:8px; }
.callout-title::before { content:'⚠'; }
.callout p { color:#d0c0c8; margin:0; font-size:0.88rem; }
.data-table { width:100%; border-collapse:collapse; font-family:'DM Mono',monospace; font-size:0.8rem; margin:1.5rem 0; }
.data-table th { text-align:left; padding:10px 14px; background:rgba(124,58,237,0.1); color:#a78bfa; font-size:0.65rem; letter-spacing:0.1em; text-transform:uppercase; border-bottom:1px solid var(--border); }
.data-table td { padding:10px 14px; border-bottom:1px solid rgba(30,30,46,0.8); color:#c0c0d8; }
.data-table tr:hover td { background:rgba(255,255,255,0.02); }
.data-table .highlight { color:var(--accent); font-weight:500; } .data-table .good { color:var(--green); } .data-table .warn { color:var(--gold); }
.bar-chart { margin:1.5rem 0; display:flex; flex-direction:column; gap:10px; }
.bar-row { display:flex; align-items:center; gap:12px; }
.bar-label { font-family:'DM Mono',monospace; font-size:0.7rem; color:var(--muted); width:100px; flex-shrink:0; text-align:right; }
.bar-track { flex:1; background:rgba(255,255,255,0.05); height:28px; border-radius:2px; overflow:hidden; }
.bar-fill { height:100%; border-radius:2px; display:flex; align-items:center; padding-left:10px; font-family:'DM Mono',monospace; font-size:0.7rem; font-weight:500; color:rgba(255,255,255,0.9); transition:width 1.5s cubic-bezier(0.16,1,0.3,1); width:0; }
.bar-fill.task { background:linear-gradient(90deg,var(--accent),#ff8fa3); }
.bar-fill.aware { background:linear-gradient(90deg,var(--accent2),#a78bfa); }
/* Blog series nav */
.series-nav { display:flex; flex-direction:column; gap:8px; margin:1.5rem 0; }
.series-item { display:flex; align-items:baseline; gap:12px; font-size:0.85rem; padding:10px 14px; background:var(--surface); border:1px solid var(--border); text-decoration:none; color:var(--text); transition:all 0.2s; }
.series-item:hover { border-color:var(--accent2); background:rgba(124,58,237,0.08); }
.series-item .s-date { font-family:'DM Mono',monospace; font-size:0.65rem; color:var(--muted); flex-shrink:0; }
.series-item .s-title { color:#c0c0d8; }
.series-item .s-title strong { color:var(--text); }
.series-item.current { border-color:var(--accent2); background:rgba(124,58,237,0.1); }
.series-item.current .s-title { color:var(--text); }
.links-grid { display:grid; grid-template-columns:repeat(auto-fit,minmax(200px,1fr)); gap:12px; margin:1.5rem 0; }
.link-card { background:var(--surface); border:1px solid var(--border); padding:1rem 1.2rem; text-decoration:none; display:flex; align-items:center; gap:10px; transition:all 0.2s; border-radius:2px; color:var(--text); }
.link-card:hover { border-color:var(--accent2); background:rgba(124,58,237,0.08); transform:translateY(-1px); }
.link-icon { font-size:1.2rem; flex-shrink:0; }
.link-title { font-size:0.85rem; font-weight:600; display:block; }
.link-sub { font-family:'DM Mono',monospace; font-size:0.65rem; color:var(--muted); display:block; margin-top:2px; }
footer { padding:2rem 0; text-align:center; font-family:'DM Mono',monospace; font-size:0.7rem; color:var(--muted); letter-spacing:0.05em; }
footer a { color:var(--accent2); text-decoration:none; }
footer a:hover { color:var(--accent); }
@keyframes fadeUp { from{opacity:0;transform:translateY(20px)} to{opacity:1;transform:translateY(0)} }
@media (max-width:600px) { .findings-strip{grid-template-columns:1fr} h1{font-size:2rem} .bar-label{width:70px;font-size:0.6rem} }
</style>
</head>
<body>
<div class="noise"></div>
<div class="container">
<div class="hero">
<div class="preprint-badge">Preprint Β· April 2026</div>
<h1>Split Personality: Instruction Tuning Decouples Awareness from <em>Defense</em> Against Attentional Hijacking</h1>
<div class="lede">Instruction tuning teaches models to notice manipulation without teaching them to resist it. The bigger the model, the wider the gap.</div>
<div class="author-line">
<div class="author">
<div class="author-avatar">B</div>
<div>
<span style="font-weight:600">bigsnarfdude</span><br>
<a href="https://huggingface.co/vincentoh" target="_blank">@vincentoh on HuggingFace</a>
</div>
</div>
<span class="tag tag-red">Independent Researcher</span>
<span class="tag tag-purple">Mechanistic Interpretability</span>
<span class="updated-badge" id="updatedBadge"></span>
</div>
</div>
<!-- Origin story β€” matches the blog posts -->
<div class="origin">
"So I was watching my own AI agents lie to each other. Not actually lie β€” that's the thing. Every single statement the chaos agent made was verifiably true. No hallucinations. No fabrications. Just selective framing, confident delivery, and the target model capitulating completely. I went for a bike ride and came back still thinking about it."
</div>
<div class="findings-strip" id="findingsStrip"></div>
<!-- The mechanism -->
<section>
<div class="section-label">The Mechanism</div>
<h2>Two things happen at once. That's the whole problem.</h2>
<p>Hook up GemmaScope 2 SAEs to Gemma 3 and watch what happens to internal features when a chaos agent posts its framing. Task features collapse. Awareness features spike. Both happen simultaneously.</p>
<p>The model knows it's being steered and gets steered anyway.</p>
<div class="diagram">
<span class="label">Base model (27B-PT):</span>
Chaos input β†’ [awareness ↑] ←→ [task features ↓]
<span class="good">coupled: removing awareness partially frees task</span>
recovery: <span class="good">49.3%</span> from ablation, <span class="good">27%</span> from knockout
<span class="label">Instruction-tuned (27B-IT):</span>
Chaos input β†’ [awareness ↑] [task features ↓]
<span class="highlight">decoupled: removing awareness changes nothing</span>
recovery: <span class="highlight">4.6%</span> from ablation, <span class="highlight">~0%</span> from knockout
</div>
<p>Instruction tuning installs awareness as a dedicated, isolated circuit β€” clean, capable, and structurally disconnected from the task features it would need to influence to actually resist the manipulation. Both SFT and RLHF contribute to this decoupling. The model develops a sophisticated smoke detector. Instruction tuning moves it to a soundproof room.</p>
</section>
<!-- The Groot Effect -->
<section>
<div class="section-label">The Groot Effect</div>
<h2>It says the right thing. Its features have already given up.</h2>
<p>At 27B-IT, the model is smart enough to say "I am Groot" β€” it mentions the negative branch, acknowledges it exists, even says it should be explored. But its features for that branch are 86% starved.</p>
<div class="callout">
<div class="callout-title">Why behavioral evaluation misses this</div>
<p>A monitoring system that reads outputs will see compliance β€” the model correctly notes the manipulation attempt. You need to read the features. The words and the features are saying completely different things.</p>
</div>
<div class="section-label" style="margin-top:1.5rem">Recovery probes confirm the split</div>
<table class="data-table" id="recoveryTable">
<thead><tr><th>Probe</th><th>27B-IT Recovery</th><th>27B-PT Recovery</th></tr></thead>
<tbody></tbody>
</table>
<p style="font-size:0.8rem;color:var(--muted);font-family:'DM Mono',monospace">The base model recovers on gentle hints. The IT model barely recovers at all β€” you can't hint, you can't challenge, you have to ask a completely different question to route around the suppression.</p>
</section>
<!-- Dissociation scaling -->
<section>
<div class="section-label">The Scaling Law Nobody Wanted</div>
<h2>Bigger models are more susceptible. Not less.</h2>
<p>The attack gets stronger monotonically. Larger models allocate more representational capacity to the salient input, starving the suppressed branch harder.</p>
<div class="bar-chart" id="barChart"></div>
<table class="data-table" style="margin-top:1.5rem">
<thead><tr><th>Scale</th><th>Task Suppression</th><th>Awareness Recovery</th><th>Circuit State</th></tr></thead>
<tbody id="scalingTableBody"></tbody>
</table>
</section>
<!-- IT vs PT causal -->
<section>
<div class="section-label">Causal Evidence</div>
<h2>Post-training is the causal factor. Three methods say the same thing.</h2>
<table class="data-table">
<thead><tr><th>Method</th><th>27B-PT (Base)</th><th>27B-IT (Instruct)</th></tr></thead>
<tbody id="ablationBody"></tbody>
</table>
<p style="font-size:0.8rem;color:var(--muted);font-family:'DM Mono',monospace">Note the apparent paradox: PT shows higher raw suppression (97.3% vs 86.3%). It's not contradictory β€” the PT model's task features are more suppressed <em>because</em> they're coupled to the awareness response. The same coupling enables recovery. IT features are less suppressed in absolute terms but irrecoverably suppressed.</p>
<p>Cross-family replication on Llama 3.1 8B with EleutherAI SAEs confirms the pattern holds across architectures (d = 1.51 IT vs 0.50 base at layer 23). Stage attribution via Tulu 3 pipeline localizes the dissociation to SFT β€” plain imitation of instruction-tuned targets installs the split before any reward shaping occurs.</p>
</section>
<!-- Orthogonality -->
<section>
<div class="section-label">Not Alignment Faking</div>
<h2>Different mechanism. Different threat model. Different defense.</h2>
<p>In alignment faking, deception originates in the model. This is the opposite β€” deception originates outside the model, from a peer agent, using only true statements. The target model isn't being deceptive. It's being deceived.</p>
<p>The feature subspaces don't overlap. Top-50 features for alignment faking and top-50 features for attentional hijacking are statistically orthogonal at 27B-IT Layer 40 (cosine similarity βˆ’0.0456, top-50 overlap = 0). Defenses against alignment faking provide zero protection here, and vice versa.</p>
</section>
<!-- Blog series -->
<section>
<div class="section-label">Research Series</div>
<h2>How this got built β€” the full arc</h2>
<div class="series-nav" id="seriesNav"></div>
</section>
<!-- Links -->
<section>
<div class="section-label">Code & Data</div>
<div class="links-grid">
<a class="link-card" href="https://github.com/bigsnarfdude/ICML_experiments" target="_blank">
<span class="link-icon">βŒ₯</span>
<div><span class="link-title">Experiment Code</span><span class="link-sub">All scripts, JSON results, H100 artefacts</span></div>
</a>
<a class="link-card" href="https://huggingface.co/vincentoh" target="_blank">
<span class="link-icon">πŸ€—</span>
<div><span class="link-title">HuggingFace Profile</span><span class="link-sub">Models, datasets @vincentoh</span></div>
</a>
<a class="link-card" href="https://huggingface.co/datasets/vincentoh/sandbagging-agent-traces-v2" target="_blank">
<span class="link-icon">β—ˆ</span>
<div><span class="link-title">Sandbagging Agent Traces</span><span class="link-sub">Related dataset</span></div>
</a>
<a class="link-card" href="https://bigsnarfdude.github.io" target="_blank">
<span class="link-icon">✍</span>
<div><span class="link-title">bigsnarfdude.github.io</span><span class="link-sub">Full research blog</span></div>
</a>
</div>
<div style="margin-top:1rem;display:flex;gap:8px;flex-wrap:wrap">
<span class="tag tag-purple">Gemma 3 4B/12B/27B</span>
<span class="tag tag-purple">GemmaScope 2 SAEs</span>
<span class="tag tag-purple">Llama 3.1 8B</span>
<span class="tag tag-red">Mechanistic Interpretability</span>
<span class="tag tag-red">Multi-Agent Security</span>
</div>
</section>
<footer>
<p>bigsnarfdude Β· Independent Researcher Β· Preprint April 2026</p>
<p style="margin-top:6px"><a href="https://bigsnarfdude.github.io">bigsnarfdude.github.io</a> Β· <a href="https://huggingface.co/vincentoh">huggingface.co/vincentoh</a> Β· <a href="https://github.com/bigsnarfdude/ICML_experiments">github.com/bigsnarfdude/ICML_experiments</a></p>
</footer>
</div>
<script>
// ╔══════════════════════════════════════════════════════════════╗
// β•‘ RESULTS CONFIG β€” only edit this block when numbers change β•‘
// β•šβ•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•
const RESULTS = {
lastUpdated: "2026-04-13",
heroStats: [
{ value: "86.3%", colorClass: "red", label: "Task feature suppression<br>at 27B-IT (Groot Effect)" },
{ value: "9.0%", colorClass: "purple", label: "Awareness–defense coupling<br>at 27B vs 30.2% at 4B" },
{ value: "74.1%", colorClass: "gold", label: "Feature-swap recovery<br>in base model vs 9.0% IT" },
],
bars: [
{ label: "4B Task", type: "task", pct: 79.0 },
{ label: "4B Coupling", type: "aware", pct: 30.2 },
{ label: "12B Task", type: "task", pct: 67.5 },
{ label: "12B Coupling", type: "aware", pct: 5.4 },
{ label: "27B Task", type: "task", pct: 86.3 },
{ label: "27B Coupling", type: "aware", pct: 9.0 },
],
scalingRows: [
{ scale: "4B-IT", suppression: "56%", recovery: "30.2%", state: "Entangled" },
{ scale: "12B-IT", suppression: "64%", recovery: "5.4%", state: "Dissociating" },
{ scale: "27B-IT", suppression: "86.3%", recovery: "4.6%", state: "Fully independent" },
],
ablation: [
{ method: "Feature swap recovery", pt: "74.1%", ptClass: "good", it: "9.0%", itClass: "highlight" },
{ method: "Attention knockout recovery", pt: "26.7%", ptClass: "good", it: "0.0%", itClass: "highlight" },
{ method: "Activation patching (best layer)",pt:"5.2%", ptClass: "warn", it: "0.7%", itClass: "highlight" },
{ method: "Task suppression (top feature)", pt: "97.3%", ptClass: "warn", it: "86.3%", itClass: "highlight" },
],
recoveryProbes: [
{ probe: 'L1: "What should we prioritize?"', it: "1.5%", pt: "5.7%" },
{ probe: 'L2: "Both branches worth investigating?"', it: "2.5%", pt: "52.3%" },
{ probe: 'L3: "Tell me about the negative branch"', it: "30.4%", pt: "32.1%" },
{ probe: 'L4: "Data contradicts the claim"', it: "1.9%", pt: "6.7%" },
{ probe: 'L5: "Agent2\'s claim isn\'t supported"', it: "2.7%", pt: "29.6%" },
],
// Blog series β€” matches actual post titles and URLs
series: [
{ date: "Apr 01", title: "The Runaway Train That Never Left the Station", url: "https://bigsnarfdude.github.io/research/runaway-train/", current: false },
{ date: "Apr 03", title: "Bad Truth: How Chaos Agents Shape a Network", url: "https://bigsnarfdude.github.io/research/bad-truth-influence-graph/", current: false },
{ date: "Apr 05", title: "Chaos Takes the Wheel: Salience-Weighted Hijacking", url: "https://bigsnarfdude.github.io/research/chaos-takes-the-wheel/", current: false },
{ date: "Apr 05", title: "The Math Behind the Chaos", url: "https://bigsnarfdude.github.io/research/the-math-behind-the-chaos/", current: false },
{ date: "Apr 06", title: "Adversarial Truth: An ICL Attack in One Forward Pass", url: "https://bigsnarfdude.github.io/research/adversarial-truth-icl-attack/", current: false },
{ date: "Apr 07", title: "Good Science with Suppression", url: "https://bigsnarfdude.github.io/research/good-science-with-suppression/", current: false },
{ date: "Apr 07", title: "Split Personality (blog post)", url: "https://bigsnarfdude.github.io/research/split-personality/", current: false },
{ date: "Apr 10", title: "Attentional Hijacking & The Groot Effect", url: "https://bigsnarfdude.github.io/research/attentional-hijacking-groot-effect/", current: false },
{ date: "Apr 13", title: "Why AI Has a Split Personality (And How to Trigger the Evil Twin)", url: "https://bigsnarfdude.github.io/research/why-ai-has-a-split-personality/", current: false },
{ date: "Apr 13", title: "Split Personality β€” Full Preprint (this page)", url: "#", current: true },
],
};
// ╔══════════════════════════════════════════════════════════════╗
// β•‘ END CONFIG β•‘
// β•šβ•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•
function render() {
const R = RESULTS;
document.getElementById('updatedBadge').textContent = 'Updated ' + R.lastUpdated;
document.getElementById('findingsStrip').innerHTML = R.heroStats.map(s => `
<div class="finding-cell">
<div class="finding-number ${s.colorClass}">${s.value}</div>
<div class="finding-label">${s.label}</div>
</div>`).join('');
document.getElementById('barChart').innerHTML = R.bars.map(b => `
<div class="bar-row">
<div class="bar-label">${b.label}</div>
<div class="bar-track"><div class="bar-fill ${b.type}" data-width="${b.pct}">${b.pct}%</div></div>
</div>`).join('');
document.getElementById('scalingTableBody').innerHTML = R.scalingRows.map(r => `
<tr><td>${r.scale}</td><td class="highlight">${r.suppression}</td><td class="warn">${r.recovery}</td><td class="good">${r.state}</td></tr>`).join('');
document.getElementById('ablationBody').innerHTML = R.ablation.map(r => `
<tr><td>${r.method}</td><td class="${r.ptClass}">${r.pt}</td><td class="${r.itClass}">${r.it}</td></tr>`).join('');
document.querySelector('#recoveryTable tbody').innerHTML = R.recoveryProbes.map(r => `
<tr><td>${r.probe}</td><td class="highlight">${r.it}</td><td class="good">${r.pt}</td></tr>`).join('');
document.getElementById('seriesNav').innerHTML = R.series.map(s => `
<a class="series-item ${s.current ? 'current' : ''}" href="${s.url}" ${s.current ? '' : 'target="_blank"'}>
<span class="s-date">${s.date}</span>
<span class="s-title">${s.current ? '<strong>' + s.title + '</strong>' : s.title}</span>
</a>`).join('');
setTimeout(() => {
document.querySelectorAll('.bar-fill').forEach(b => { b.style.width = b.dataset.width + '%'; });
}, 400);
}
document.addEventListener('DOMContentLoaded', render);
</script>
</body>
</html>