[object Object]

← back to Model Arena

contrarian FIX FIRST: gate retry to existing competitors (kills metered spend-injection), serialize metered per-model + double-fire guard, stuck-run watchdog + corrupt-record heal, gpt-5.1 id, winner breakout layout + crown tooltip + honest cost label

fbf19ba16e13c96003e55c664ae39b2a2a587ec2 · 2026-07-22 22:38:38 -0700 · Steve

Files touched

Diff

commit fbf19ba16e13c96003e55c664ae39b2a2a587ec2
Author: Steve <steve@designerwallcoverings.com>
Date:   Wed Jul 22 22:38:38 2026 -0700

    contrarian FIX FIRST: gate retry to existing competitors (kills metered spend-injection), serialize metered per-model + double-fire guard, stuck-run watchdog + corrupt-record heal, gpt-5.1 id, winner breakout layout + crown tooltip + honest cost label
---
 5x/sweep-2-contrarian-fixes.md              |  15 +
 data/artifacts/0ac1af2eaed4/gemma3-12b.html | 159 ++++----
 data/artifacts/0ac1af2eaed4/hermes3-8b.html | 122 +++---
 data/artifacts/0ac1af2eaed4/kimi.html       |  57 +++
 data/artifacts/0ac1af2eaed4/qwen3-14b.html  |  13 +
 data/artifacts/74df3b61e7ca/grok.html       | 591 ++++++++++++++++++++++++++++
 data/artifacts/966d54f224fd/gemma3-12b.html | 184 +++++++++
 data/artifacts/966d54f224fd/grok.html       | 256 ++++++++++++
 data/artifacts/966d54f224fd/hermes3-8b.html | 284 +++++++++++++
 data/artifacts/966d54f224fd/kimi.html       | 135 +++++++
 data/challenges.json                        | 204 +++++++++-
 data/costlog.jsonl                          |   4 +
 public/index.html                           |  13 +-
 server.js                                   |  58 ++-
 14 files changed, 1937 insertions(+), 158 deletions(-)

diff --git a/5x/sweep-2-contrarian-fixes.md b/5x/sweep-2-contrarian-fixes.md
new file mode 100644
index 0000000..dd2a8d7
--- /dev/null
+++ b/5x/sweep-2-contrarian-fixes.md
@@ -0,0 +1,15 @@
+# post-/contrarian FIX FIRST pass — 2026-07-22
+
+Panel verdict: FIX FIRST (4× FIX FIRST, 1× KILL-as-scoped/Strategist).
+
+## Blockers fixed (this pass)
+1. **Metered spend-injection (Engineer #1, live $-vector):** `/retry/:model` now 400s if the model isn't already a competitor in that battle. Was: anyone with the shared cred could inject paid models onto every challenge and loop. Verified: retry/claude→400, retry/hermes3-8b→200.
+2. **Metered double-spend race (Engineer #2):** metered generations now serialize per-model via onKey('metered:'+id); runModel has a double-fire guard (no-op if already running/queued). Two rapid retries queue instead of both charging.
+3. **Zombie/stuck runs (User/Skeptic/Engineer #3):** startup sentinel now also heals corrupt records (finished_at<started_at); a 30s watchdog fails runs stuck past 660s running / 20min queued so the UI shows an honest error instead of an infinite poll. Verified: all prior zombie runs healed to error.
+4. **gpt-5.3 hallucinated id (Skeptic/Strategist):** the live "neon city" battle proved gpt-5.3 404s; changed to gpt-5.1 (verified against OpenAI /models), env-overridable OPENAI_MODEL.
+5. **Winner not a visual event (Designer):** `.pane.winner` breaks the grid (grid-column:1/-1, order:-1, taller iframe, one-time gold flash) — champion on top, full width.
+6. **Crown unexplained (User):** Crown button now has a title tooltip explaining it records a ledger vote.
+7. **Dishonest cost estimate (Skeptic):** relabeled "Estimated battle cost" → "Rough ceiling (flat per-model guess; actual metered by real tokens)".
+
+## Deferred to Steve (Strategist dissent — a product-scope call, not an autonomous fix)
+The arena's challenges are game/toy demos; the Strategist argues the win-rate ledger only becomes a real "moat" if challenges are drawn from Steve's actual production workloads (product-description gen, vendor-PDF JSON extraction, inquiry classification) with rubric scores, echoing ollama-model-eval. That's a direction change — surfaced for Steve, not applied.
diff --git a/data/artifacts/0ac1af2eaed4/gemma3-12b.html b/data/artifacts/0ac1af2eaed4/gemma3-12b.html
index 395d342..4ef3802 100644
--- a/data/artifacts/0ac1af2eaed4/gemma3-12b.html
+++ b/data/artifacts/0ac1af2eaed4/gemma3-12b.html
@@ -1,117 +1,134 @@
 <!DOCTYPE html>
-<html>
-<head>
+<html><head>
 <title>Fireworks Simulator</title>
 <style>
 body { margin: 0; overflow: hidden; background-color: black }
 canvas { display: block }
 </style>
-</head>
-<body>
+</head><body>
 <canvas id="fireworksCanvas"></canvas>
 <script>
 const canvas = document.getElementById('fireworksCanvas');
 const ctx = canvas.getContext('2d');
+
 canvas.width = window.innerWidth;
 canvas.height = window.innerHeight;
 
 let fireworks = [];
-const numFireworks = 30;
+const numFireworks = 10; // Adjust for density
 
 class Firework {
-  constructor(x, y) {
-    this.x = x;
-    this.y = y;
-    this.radius = Math.random() * 2 + 1;
-    this.color = `hsl(${Math.random() * 360}, 100%, ${Math.random() * 50 + 50}%)`;
-    this.particles = [];
-    this.angle = Math.random() * 2 * Math.PI;
-    this.speed = Math.random() * 5 + 10;
+  constructor() {
+    this.x = Math.random() * canvas.width;
+    this.y = canvas.height;
+    this.angle = Math.random() * Math.PI * 2;
+    this.speed = Math.random() * 5 + 3;
     this.gravity = 0.1;
-    this.damping = 0.95;
+    this.friction = 0.98;
+    this.particles = [];
+    this.lifespan = 100;
   }
 
   launch() {
-      for (let i = 0; i < 30; i++) {
-          const angle = this.angle + Math.random() * 0.4 - 0.2 * Math.PI;
-          this.particles.push({
-              x: this.x + Math.cos(angle) * this.radius,
-              y: this.y + Math.sin(angle) * this.radius,
-              vx: Math.cos(angle) * this.speed,
-              vy: Math.sin(angle) * this.speed,
-              life: 1 - Math.random() * 0.5,
-              color: this.color
-          });
-      }
+    this.velocity = { x: Math.cos(this.angle) * this.speed, y: -Math.sin(this.angle) * this.speed };
   }
 
   update() {
-    if (this.particles.length > 0) {
-      for (let i = this.particles.length - 1; i >= 0; i--) {
-        const particle = this.particles[i];
-        particle.vx *= this.damping;
-        particle.vy -= this.gravity * particle.life;
-        particle.x += particle.vx;
-        particle.y += particle.vy;
-
-        if (particle.life <= 0 || particle.y > canvas.height) {
-          this.particles.splice(i, 1);
-        }
-      }
+    if (!this.launched) return;
+    this.velocity.x *= this.friction;
+    this.velocity.y += this.gravity;
+    this.x += this.velocity.x;
+    this.y += this.velocity.y;
+    this.lifespan--;
+
+    if (this.lifespan <= 0) {
+      this.explode();
+      return true; // Signal to remove the firework
     }
+    return false;
   }
 
-  draw() {
-    ctx.fillStyle = this.color;
-    for (const particle of this.particles) {
-      ctx.beginPath();
-      ctx.arc(particle.x, particle.y, Math.max(1, particle.life * 2), 0, 2 * Math.PI);
-      ctx.fill();
+  explode() {
+    const particleCount = Math.floor(Math.random() * 30 + 30);
+    for (let i = 0; i < particleCount; i++) {
+      const angle = Math.random() * Math.PI * 2;
+      const speed = Math.random() * 2 + 1;
+      this.particles.push({ x: this.x, y: this.y, angle, speed });
     }
   }
-}
 
-function addFirework() {
-  fireworks.push(new Firework(Math.random() * canvas.width, -50));
-}
+  draw() {
+    if (!this.launched) return;
 
+    ctx.beginPath();
+    ctx.arc(this.x, this.y, 5, 0, Math.PI * 2);
+    ctx.fillStyle = 'white';
+    ctx.fill();
+  }
 
-canvas.addEventListener('click', () => {
-  addFirework();
-});
+  drawParticles() {
+      for (let i = 0; i < this.particles.length; i++) {
+          const particle = this.particles[i];
+          particle.angle += Math.random()*0.05 - 0.025; // slight random drift
+          particle.speed *= 0.98;
 
-let autoShow = true; // Auto-show mode
-setInterval(() => {
-    if (autoShow) {
-        addFirework();
-    }
-}, 500); // Adjust interval for frequency
+          ctx.beginPath();
+          ctx.arc(particle.x, particle.y, 3, 0, Math.PI * 2);
+          ctx.fillStyle = 'rgba(255, 255, 255, 0.7)';
+          ctx.fill();
+
+          particle.x += Math.cos(particle.angle) * particle.speed;
+          particle.y += -Math.sin(particle.angle) * particle.speed;
+      }
+  }
+}
 
 
+function init() {
+    for (let i = 0; i < numFireworks; i++) {
+        fireworks.push(new Firework());
+    }
+}
 
-function animate() {
+function draw() {
   ctx.clearRect(0, 0, canvas.width, canvas.height);
 
-  for (let i = fireworks.length - 1; i >= 0; i--) {
-    fireworks[i].launch();
-    fireworks[i].update();
-    fireworks[i].draw();
-
-    if(fireworks[i].particles.length === 0) {
-        fireworks.splice(i, 1);
+  for (let i = 0; i < fireworks.length; i++) {
+    const firework = fireworks[i];
+    if (firework.update()) {
+        fireworks.splice(i, 1); // Remove expired/exploded fireworks
+        i--;
+      continue;
     }
+
+    firework.draw();
+    firework.drawParticles();
   }
 
-  requestAnimationFrame(animate);
+  requestAnimationFrame(draw);
 }
 
-animate();
 
-window.addEventListener('resize', () => {
-    canvas.width = window.innerWidth;
-    canvas.height = window.innerHeight;
+canvas.addEventListener('click', () => {
+  const newFirework = new Firework();
+  newFirework.launch();
+  fireworks.push(newFirework);
 });
 
+
+
+function autoShow() {
+    setInterval(() => {
+        const newFirework = new Firework();
+        newFirework.launch();
+        fireworks.push(newFirework);
+    }, 1500); // Adjust interval as needed
+}
+
+init();
+draw();
+autoShow();
+
+
 </script>
-</body>
-</html>
\ No newline at end of file
+</body></html>
\ No newline at end of file
diff --git a/data/artifacts/0ac1af2eaed4/hermes3-8b.html b/data/artifacts/0ac1af2eaed4/hermes3-8b.html
index 9645581..ad9ab80 100644
--- a/data/artifacts/0ac1af2eaed4/hermes3-8b.html
+++ b/data/artifacts/0ac1af2eaed4/hermes3-8b.html
@@ -3,7 +3,7 @@
 <head>
     <meta charset="UTF-8">
     <meta name="viewport" content="width=device-width, initial-scale=1.0">
-    <title>Interactive Fireworks Simulator</title>
+    <title>Fireworks Simulator</title>
     <style>
         body {
             margin: 0;
@@ -11,97 +11,99 @@
             touch-action: none;
             cursor: crosshair;
         }
-        canvas {
+        #canvas {
             display: block;
         }
     </style>
 </head>
 <body>
-<canvas></canvas>
+<canvas id="canvas" width="400" height="300"></canvas>
 <script>
-    const canvas = document.querySelector('canvas');
+    const canvas = document.getElementById('canvas');
     const ctx = canvas.getContext('2d');
 
-    // Set canvas size
-    canvas.width = window.innerWidth * 0.9;
-    canvas.height = window.innerHeight * 0.8;
-
-    let particles = [];
     let showMode = false;
 
-    class Particle {
-        constructor(x, y) {
-            this.x = x;
-            this.y = y;
-            this.size = Math.random() * 3 + 1;
-            this.speedX = (Math.random() - 0.5) * 4;
-            this.speedY = (Math.random() - 0.5) * 4;
-        }
-        
-        draw() {
-            ctx.beginPath();
-            ctx.arc(this.x, this.y, this.size, 0, Math.PI * 2);
-            ctx.fillStyle = `rgba(255, ${Math.max(0, 255 - this.size)}, 0, 0.75)`;
-            ctx.fill();
-            
-            if (this.size > 1) {
-                ctx.strokeStyle = `rgba(255, ${Math.max(0, 255 - this.size)}, 0, 0.5)`;
-                ctx.lineWidth = Math.floor(this.size / 2);
-                ctx.strokeRect(this.x - (this.size / 2), this.y - (this.size / 2), this.size, this.size);
-            }
-        }
-
-        update() {
-            this.x += this.speedX;
-            this.y += this.speedY;
+    function getRandomValue(min, max) {
+        return Math.random() * (max - min) + min;
+    }
 
-            if (this.x + this.size > canvas.width || this.x - this.size < 0) 
-                this.speedX = -this.speedX;
-                
-            if (this.y + this.size > canvas.height || this.y - this.size < 0)
-                this.speedY = -this.speedY;
-        }
+    function Particle(x, y) {
+        this.x = x;
+        this.y = y;
+        this.size = getRandomValue(10, 15);
+        this.speedX = getRandomValue(-5, 5);
+        this.speedY = -getRandomValue(1, 5);
+        this.color = 'rgba(255, 255, 255, 0.8)';
     }
 
+    Particle.prototype.draw = function() {
+        ctx.beginPath();
+        ctx.arc(this.x, this.y, this.size, 0, Math.PI * 2);
+        ctx.fillStyle = this.color;
+        ctx.fill();
+        ctx.stroke();
+    };
+
     function createExplosion(x, y) {
+        let particles = [];
         for (let i = 0; i < 50; i++) {
             particles.push(new Particle(x, y));
         }
+        return particles;
     }
 
-    function loop() {
+    function drawParticles(particles) {
         ctx.clearRect(0, 0, canvas.width, canvas.height);
+        for (let i = 0; i < particles.length; i++) {
+            let particle = particles[i];
+            particle.draw();
+            particle.y += particle.speedY;
+            particle.x += particle.speedX;
 
-        if (!showMode) {
-            particles.forEach((particle, index, array) => {
-                particle.update();
-                particle.draw();
+            if (
+                particle.x + particle.size > canvas.width ||
+                particle.x - particle.size < 0 ||
+                particle.y + particle.size > canvas.height ||
+                particle.y - particle.size < 0
+            ) {
+                particles.splice(i, 1);
+                i--;
+            }
+        }
+    }
 
-                if (particle.y + particle.size < 0 || particle.y - particle.size > canvas.height ||
-                    particle.x + particle.size < 0 || particle.x - particle.size > canvas.width) {
-                        array.splice(index, 1);
-                    }
-            });
+    function loop() {
+        let particles = [];
+        
+        if (!showMode) {
+            for (let i = 0; i < 5; i++) {
+                let x = getRandomValue(100, canvas.width - 100);
+                let y = canvas.height;
+                particles.push(...createExplosion(x, y));
+            }
         } else {
-            particles.forEach(particle => {
-                particle.size -= Math.random() * 2;
-                if (particle.size <= 0) {
-                    particles.splice(particles.indexOf(particle), 1);
-                }
-                particle.draw();
-            });
+            for (let i = 0; i < 10; i++) {
+                let x = getRandomValue(50, canvas.width - 50);
+                let y = getRandomValue(50, canvas.height - 100);
+                particles.push(...createExplosion(x, y));
+            }
         }
 
-        requestAnimationFrame(loop);
+        drawParticles(particles);
+
+        if (!showMode) {
+            requestAnimationFrame(loop);
+        } else {
+            setTimeout(loop, 1000 / 60); // 60 FPS
+        }
     }
 
     canvas.addEventListener('click', () => {
         showMode = !showMode;
-        createExplosion(canvas.width / 2, canvas.height / 2);
     });
 
     loop();
-
 </script>
 </body>
 </html>
\ No newline at end of file
diff --git a/data/artifacts/0ac1af2eaed4/kimi.html b/data/artifacts/0ac1af2eaed4/kimi.html
new file mode 100644
index 0000000..3e082ba
--- /dev/null
+++ b/data/artifacts/0ac1af2eaed4/kimi.html
@@ -0,0 +1,57 @@
+<!DOCTYPE html>
+<html>
+<head>
+<style>
+body{margin:0;overflow:hidden;background:#000}
+canvas{display:block}
+</style>
+</head>
+<body>
+<canvas id="c"></canvas>
+<script>
+const c=document.getElementById('c'),x=c.getContext('2d');
+let w=c.width=innerWidth,h=c.height=innerHeight;
+onresize=()=>{w=c.width=innerWidth;h=c.height=innerHeight};
+const R=[],P=[];
+class Particle{
+constructor(x,y,c){
+this.x=x;this.y=y;
+const a=Math.random()*6.28,v=Math.random()*6;
+this.vx=Math.cos(a)*v;this.vy=Math.sin(a)*v;
+this.l=1;this.c=c;this.g=0.08;
+}
+update(){
+this.vy+=this.g;this.x+=this.vx;this.y+=this.vy;this.l-=0.015;
+x.globalAlpha=this.l;x.fillStyle=this.c;x.beginPath();x.arc(this.x,this.y,2.5,0,7);x.fill();
+}
+}
+class Rocket{
+constructor(tx,ty){
+this.x=Math.random()*w;this.y=h;this.tx=tx;this.ty=ty;
+const dx=tx-this.x,dy=ty-h,d=Math.sqrt(dx*dx+dy*dy);
+this.vx=dx/d*12;this.vy=dy/d*12;this.dead=0;
+this.c=`hsl(${Math.random()*360},100%,60%)`;
+}
+update(){
+this.x+=this.vx;this.y+=this.vy;
+if(Math.hypot(this.x-this.tx,this.y-this.ty)<10||this.vy>0){
+this.dead=1;
+for(let i=0;i<40;i++)P.push(new Particle(this.x,this.y,this.c));
+}
+x.fillStyle=this.c;x.beginPath();x.arc(this.x,this.y,3,0,7);x.fill();
+}
+}
+function loop(){
+x.globalCompositeOperation='source-over';
+x.fillStyle='rgba(0,0,0,0.15)';x.fillRect(0,0,w,h);
+x.globalCompositeOperation='lighter';
+for(let i=R.length-1;i>=0;i--){R[i].update();if(R[i].dead)R.splice(i,1)}
+for(let i=P.length-1;i>=0;i--){P[i].update();if(P[i].l<=0)P.splice(i,1)}
+requestAnimationFrame(loop);
+}
+c.onclick=e=>R.push(new Rocket(e.clientX,e.clientY));
+setInterval(()=>R.push(new Rocket(Math.random()*w,Math.random()*h*0.6)),600);
+loop();
+</script>
+</body>
+</html>
\ No newline at end of file
diff --git a/data/artifacts/0ac1af2eaed4/qwen3-14b.html b/data/artifacts/0ac1af2eaed4/qwen3-14b.html
new file mode 100644
index 0000000..32e4664
--- /dev/null
+++ b/data/artifacts/0ac1af2eaed4/qwen3-14b.html
@@ -0,0 +1,13 @@
+<!DOCTYPE html>
+<html>
+<head>
+<style>canvas{display:block;background:#000;}</style>
+</head>
+<body>
+<canvas></canvas>
+<script>
+const canvas=document.querySelector("canvas"),ctx=canvas.getContext("2d"),rockets=[],particles=[],gravity=0.5;
+function addRocket(x,y){rockets.push({x,y,vx:0,vy:-5,exploded:false,trail:[]});}
+function explodeRocket(r){for(let i=0;i<50;i++){const a=Math.random()*Math.PI*2,s=Math.random()*5+3;particles.push({x:r.x,y:r.y,vx:Math.cos(a)*s,vy:Math.sin(a)*s-2,life:100,color:`hsl(${Math.random()*360},100%,70%)`,trail:[{x:r.x,y:r.y}]})}r.exploded=true;}
+function update(){for(let i=rockets.length-1;i>=0;i--){let r=rockets[i];if(!r.exploded){r.vy+=gravity;r.x+=r.vx;r.y+=r.vy;r.trail.push({x:r.x,y:r.y});if(r.trail.length>10)r.trail.shift();if(r.y<0)explodeRocket(r)}}for(let i=particles.length-1;i>=0;i--){let p=particles[i];p.vy+=gravity;p.x+=p.vx;p.y+=p.vy;p.life--;p.trail.push({x:p.x,y:p.y});if(p.trail.length>10)p.trail.shift();if(p.life<=0)particles.splice(i,1);}}
+function draw(){ctx.fillStyle="rgba(0,0,0,0.2)";ctx.fillRect(0,0,canvas.width,canvas.height);for(let r of rockets){if(!r.exploded){ctx.beginPath();ctx.arc(r.x,r.y,4,0,Math.PI*2);ctx.fillStyle="white";ctx.fill();ctx.beginPath();for(let j=0;j<r.trail.length;j++){let p=r.trail[j];ctx.moveTo(p.x,p.y);ctx
\ No newline at end of file
diff --git a/data/artifacts/74df3b61e7ca/grok.html b/data/artifacts/74df3b61e7ca/grok.html
new file mode 100644
index 0000000..e5f702b
--- /dev/null
+++ b/data/artifacts/74df3b61e7ca/grok.html
@@ -0,0 +1,591 @@
+<!DOCTYPE html>
+<html lang="en">
+<head>
+<meta charset="UTF-8">
+<meta name="viewport" content="width=device-width, initial-scale=1.0">
+<title>Synthwave Visualizer</title>
+<style>
+  * { margin: 0; padding: 0; box-sizing: border-box; }
+  html, body {
+    width: 100%; height: 100%;
+    overflow: hidden;
+    background: #0a0015;
+    font-family: "Segoe UI", system-ui, sans-serif;
+  }
+  canvas {
+    display: block;
+    width: 100%; height: 100%;
+    position: absolute; inset: 0;
+  }
+  #ui {
+    position: absolute;
+    bottom: 28px; left: 50%;
+    transform: translateX(-50%);
+    z-index: 10;
+    display: flex;
+    gap: 14px;
+    align-items: center;
+  }
+  button {
+    background: linear-gradient(180deg, #ff2a6d 0%, #d4004f 100%);
+    color: #fff;
+    border: 2px solid #ff6b9d;
+    border-radius: 4px;
+    padding: 12px 36px;
+    font-size: 14px;
+    font-weight: 700;
+    letter-spacing: 0.2em;
+    text-transform: uppercase;
+    cursor: pointer;
+    box-shadow:
+      0 0 20px rgba(255, 42, 109, 0.5),
+      inset 0 1px 0 rgba(255,255,255,0.25);
+    transition: transform 0.1s, box-shadow 0.15s;
+  }
+  button:hover {
+    box-shadow:
+      0 0 30px rgba(255, 42, 109, 0.8),
+      inset 0 1px 0 rgba(255,255,255,0.3);
+  }
+  button:active { transform: scale(0.96); }
+  button.playing {
+    background: linear-gradient(180deg, #05d9e8 0%, #018a94 100%);
+    border-color: #5ef0ff;
+    box-shadow:
+      0 0 20px rgba(5, 217, 232, 0.5),
+      inset 0 1px 0 rgba(255,255,255,0.25);
+  }
+  #title {
+    position: absolute;
+    top: 22px; left: 50%;
+    transform: translateX(-50%);
+    z-index: 10;
+    color: #ff2a6d;
+    font-size: clamp(14px, 3vw, 22px);
+    font-weight: 800;
+    letter-spacing: 0.45em;
+    text-transform: uppercase;
+    text-shadow:
+      0 0 10px rgba(255,42,109,0.8),
+      0 0 40px rgba(255,42,109,0.4),
+      2px 2px 0 #05d9e8;
+    pointer-events: none;
+    white-space: nowrap;
+  }
+</style>
+</head>
+<body>
+<canvas id="c"></canvas>
+<div id="title">SYNTHWAVE</div>
+<div id="ui">
+  <button id="btn" type="button">PLAY</button>
+</div>
+<script>
+(function () {
+  const canvas = document.getElementById("c");
+  const ctx = canvas.getContext("2d");
+  const btn = document.getElementById("btn");
+
+  let W, H, dpr;
+  function resize() {
+    dpr = Math.min(window.devicePixelRatio || 1, 2);
+    W = window.innerWidth;
+    H = window.innerHeight;
+    canvas.width = W * dpr;
+    canvas.height = H * dpr;
+    ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
+  }
+  window.addEventListener("resize", resize);
+  resize();
+
+  // ─── Audio ───────────────────────────────────────────────
+  let audioCtx = null;
+  let masterGain, analyser;
+  let playing = false;
+  let schedTimer = null;
+  let nextNoteTime = 0;
+  let step = 0;
+  const lookahead = 0.08;
+  const scheduleAhead = 0.25;
+  const bpm = 108;
+  const stepDur = (60 / bpm) / 4; // 16th notes
+
+  // Bass pattern (A minor synthwave)
+  const bassNotes = [
+    55.00, 0, 55.00, 0, 55.00, 0, 65.41, 0,
+    73.42, 0, 73.42, 0, 65.41, 0, 55.00, 0,
+    49.00, 0, 49.00, 0, 49.00, 0, 55.00, 0,
+    58.27, 0, 58.27, 0, 55.00, 0, 49.00, 0
+  ];
+
+  // Lead arpeggio
+  const leadNotes = [
+    220.00, 261.63, 329.63, 392.00,
+    329.63, 261.63, 220.00, 261.63,
+    246.94, 293.66, 369.99, 440.00,
+    369.99, 293.66, 246.94, 293.66,
+    196.00, 246.94, 293.66, 369.99,
+    293.66, 246.94, 196.00, 246.94,
+    220.00, 277.18, 329.63, 415.30,
+    329.63, 277.18, 220.00, 277.18
+  ];
+
+  // Pad chords (root frequencies for layered saws)
+  const padChords = [
+    [110, 164.81, 220],      // Am
+    [98, 146.83, 196],       // G
+    [87.31, 130.81, 174.61], // F
+    [98, 146.83, 196]        // G
+  ];
+
+  function initAudio() {
+    audioCtx = new (window.AudioContext || window.webkitAudioContext)();
+    masterGain = audioCtx.createGain();
+    masterGain.gain.value = 0.35;
+    analyser = audioCtx.createAnalyser();
+    analyser.fftSize = 256;
+    analyser.smoothingTimeConstant = 0.78;
+    masterGain.connect(analyser);
+    analyser.connect(audioCtx.destination);
+  }
+
+  function playOsc(type, freq, t0, dur, gainVal, detune) {
+    const osc = audioCtx.createOscillator();
+    const g = audioCtx.createGain();
+    const f = audioCtx.createBiquadFilter();
+    osc.type = type;
+    osc.frequency.setValueAtTime(freq, t0);
+    if (detune) osc.detune.setValueAtTime(detune, t0);
+    f.type = "lowpass";
+    f.frequency.setValueAtTime(1800, t0);
+    f.Q.value = 4;
+    g.gain.setValueAtTime(0.0001, t0);
+    g.gain.exponentialRampToValueAtTime(gainVal, t0 + 0.02);
+    g.gain.exponentialRampToValueAtTime(0.0001, t0 + dur);
+    osc.connect(f);
+    f.connect(g);
+    g.connect(masterGain);
+    osc.start(t0);
+    osc.stop(t0 + dur + 0.05);
+  }
+
+  function playNoiseHit(t0, dur, gainVal) {
+    const len = Math.max(1, Math.floor(audioCtx.sampleRate * dur));
+    const buf = audioCtx.createBuffer(1, len, audioCtx.sampleRate);
+    const data = buf.getChannelData(0);
+    for (let i = 0; i < len; i++) data[i] = Math.random() * 2 - 1;
+    const src = audioCtx.createBufferSource();
+    src.buffer = buf;
+    const g = audioCtx.createGain();
+    const f = audioCtx.createBiquadFilter();
+    f.type = "highpass";
+    f.frequency.value = 4000;
+    g.gain.setValueAtTime(gainVal, t0);
+    g.gain.exponentialRampToValueAtTime(0.0001, t0 + dur);
+    src.connect(f);
+    f.connect(g);
+    g.connect(masterGain);
+    src.start(t0);
+    src.stop(t0 + dur + 0.02);
+  }
+
+  function playKick(t0) {
+    const osc = audioCtx.createOscillator();
+    const g = audioCtx.createGain();
+    osc.type = "sine";
+    osc.frequency.setValueAtTime(140, t0);
+    osc.frequency.exponentialRampToValueAtTime(40, t0 + 0.12);
+    g.gain.setValueAtTime(0.9, t0);
+    g.gain.exponentialRampToValueAtTime(0.0001, t0 + 0.28);
+    osc.connect(g);
+    g.connect(masterGain);
+    osc.start(t0);
+    osc.stop(t0 + 0.3);
+  }
+
+  function scheduleNote(s, t) {
+    const i = s % 32;
+    const beat = s % 16;
+
+    // Kick on 1 and 3
+    if (beat === 0 || beat === 8) playKick(t);
+    // Snare-ish noise on 2 and 4
+    if (beat === 4 || beat === 12) playNoiseHit(t, 0.1, 0.18);
+    // Hi-hat every other 16th
+    if (s % 2 === 0) playNoiseHit(t, 0.035, 0.05);
+
+    // Bass
+    const bn = bassNotes[i];
+    if (bn > 0) {
+      playOsc("sawtooth", bn, t, stepDur * 1.6, 0.22, 0);
+      playOsc("square", bn * 0.5, t, stepDur * 1.6, 0.1, -8);
+    }
+
+    // Lead every step with slight rests
+    const ln = leadNotes[i];
+    if (i % 4 !== 3) {
+      playOsc("square", ln, t, stepDur * 0.9, 0.07, 0);
+      playOsc("square", ln, t, stepDur * 0.9, 0.04, 7);
+    }
+
+    // Pad on bar starts
+    if (i % 8 === 0) {
+      const chord = padChords[Math.floor(i / 8) % 4];
+      const padDur = stepDur * 8.5;
+      chord.forEach((f, idx) => {
+        playOsc("sawtooth", f, t, padDur, 0.04, idx * 3);
+        playOsc("sawtooth", f * 2, t, padDur, 0.02, -idx * 2);
+      });
+    }
+  }
+
+  function scheduler() {
+    if (!playing) return;
+    const now = audioCtx.currentTime;
+    while (nextNoteTime < now + scheduleAhead) {
+      scheduleNote(step, nextNoteTime);
+      nextNoteTime += stepDur;
+      step++;
+    }
+    schedTimer = setTimeout(scheduler, lookahead * 1000);
+  }
+
+  function start() {
+    if (!audioCtx) initAudio();
+    if (audioCtx.state === "suspended") audioCtx.resume();
+    playing = true;
+    step = 0;
+    nextNoteTime = audioCtx.currentTime + 0.08;
+    scheduler();
+    btn.textContent = "STOP";
+    btn.classList.add("playing");
+  }
+
+  function stop() {
+    playing = false;
+    if (schedTimer) { clearTimeout(schedTimer); schedTimer = null; }
+    btn.textContent = "PLAY";
+    btn.classList.remove("playing");
+  }
+
+  btn.addEventListener("click", () => {
+    if (playing) stop(); else start();
+  });
+
+  // ─── Visuals ─────────────────────────────────────────────
+  const freqData = new Uint8Array(128);
+  let time = 0;
+  let smoothBass = 0;
+  let smoothMid = 0;
+  let smoothHi = 0;
+  let pulse = 0;
+
+  function getEnergy() {
+    if (!analyser || !playing) {
+      smoothBass *= 0.96;
+      smoothMid *= 0.96;
+      smoothHi *= 0.96;
+      pulse *= 0.95;
+      return;
+    }
+    analyser.getByteFrequencyData(freqData);
+    const n = freqData.length;
+    let bass = 0, mid = 0, hi = 0;
+    const bEnd = Math.floor(n * 0.12);
+    const mEnd = Math.floor(n * 0.45);
+    for (let i = 0; i < bEnd; i++) bass += freqData[i];
+    for (let i = bEnd; i < mEnd; i++) mid += freqData[i];
+    for (let i = mEnd; i < n; i++) hi += freqData[i];
+    bass = (bass / (bEnd * 255)) || 0;
+    mid = (mid / ((mEnd - bEnd) * 255)) || 0;
+    hi = (hi / ((n - mEnd) * 255)) || 0;
+    smoothBass += (bass - smoothBass) * 0.35;
+    smoothMid += (mid - smoothMid) * 0.25;
+    smoothHi += (hi - smoothHi) * 0.2;
+    pulse += (bass * 1.4 - pulse) * 0.4;
+  }
+
+  function drawSky() {
+    const g = ctx.createLinearGradient(0, 0, 0, H * 0.62);
+    g.addColorStop(0, "#0b001a");
+    g.addColorStop(0.35, "#1a0533");
+    g.addColorStop(0.55, "#3b0a54");
+    g.addColorStop(0.75, "#6b1d5c");
+    g.addColorStop(0.9, "#c44b2b");
+    g.addColorStop(1, "#ff6b35");
+    ctx.fillStyle = g;
+    ctx.fillRect(0, 0, W, H);
+  }
+
+  function drawStars() {
+    const seed = 42;
+    function rand(i) {
+      const x = Math.sin(i * 127.1 + seed * 311.7) * 43758.5453;
+      return x - Math.floor(x);
+    }
+    const count = 80;
+    for (let i = 0; i < count; i++) {
+      const x = rand(i) * W;
+      const y = rand(i + 100) * H * 0.5;
+      const tw = 0.4 + 0.6 * Math.sin(time * 2 + i);
+      const a = (0.3 + rand(i + 200) * 0.7) * tw * (0.5 + smoothHi);
+      ctx.fillStyle = `rgba(255,255,255,${a})`;
+      const sz = 1 + rand(i + 300) * 1.5;
+      ctx.fillRect(x, y, sz, sz);
+    }
+  }
+
+  function drawSun() {
+    const cx = W * 0.5;
+    const horizon = H * 0.55;
+    const baseR = Math.min(W, H) * 0.16;
+    const r = baseR * (1 + pulse * 0.08);
+
+    // Glow
+    const glow = ctx.createRadialGradient(cx, horizon - r * 0.15, r * 0.2, cx, horizon - r * 0.15, r * 2.2);
+    glow.addColorStop(0, `rgba(255,100,50,${0.35 + pulse * 0.25})`);
+    glow.addColorStop(0.4, `rgba(255,50,100,${0.12 + pulse * 0.1})`);
+    glow.addColorStop(1, "rgba(80,0,80,0)");
+    ctx.fillStyle = glow;
+    ctx.beginPath();
+    ctx.arc(cx, horizon - r * 0.15, r * 2.2, 0, Math.PI * 2);
+    ctx.fill();
+
+    // Sun body
+    const sg = ctx.createLinearGradient(cx, horizon - r * 1.3, cx, horizon + r * 0.4);
+    sg.addColorStop(0, "#fff5a0");
+    sg.addColorStop(0.25, "#ffcc33");
+    sg.addColorStop(0.5, "#ff6b35");
+    sg.addColorStop(0.75, "#ff2a6d");
+    sg.addColorStop(1, "#c41e6a");
+    ctx.save();
+    ctx.beginPath();
+    ctx.arc(cx, horizon - r * 0.1, r, 0, Math.PI * 2);
+    ctx.clip();
+    ctx.fillStyle = sg;
+    ctx.fillRect(cx - r - 2, horizon - r * 1.4, r * 2 + 4, r * 2.5);
+
+    // Horizontal scan lines on sun
+    const lines = 14;
+    for (let i = 0; i < lines; i++) {
+      const t = i / lines;
+      const yOff = (t - 0.15) * r * 2;
+      const y = horizon - r * 0.1 + yOff;
+      const thickness = 1.5 + t * 5 + pulse * 2;
+      const gap = 3 + t * 6;
+      if (t > 0.28) {
+        ctx.fillStyle = `rgba(10,0,25,${0.55 + t * 0.35})`;
+        ctx.fillRect(cx - r - 2, y, r * 2 + 4, thickness);
+        // skip gap already by spacing
+        i; // spacing controlled by loop density
+      }
+    }
+    // Better scan line approach
+    ctx.restore();
+
+    // Redraw sun with proper lines
+    ctx.save();
+    ctx.beginPath();
+    ctx.arc(cx, horizon - r * 0.1, r, 0, Math.PI * 2);
+    ctx.clip();
+    ctx.fillStyle = sg;
+    ctx.fillRect(cx - r - 2, horizon - r * 1.4, r * 2 + 4, r * 2.5);
+
+    let y = horizon - r * 0.1 - r * 0.15;
+    let spacing = 4;
+    while (y < horizon - r * 0.1 + r) {
+      const rel = (y - (horizon - r * 0.1)) / r; // -1 to 1
+      if (rel > -0.2) {
+        const th = 2 + Math.max(0, rel) * 7;
+        ctx.fillStyle = `rgba(8,0,20,${0.5 + Math.max(0, rel) * 0.4})`;
+        ctx.fillRect(cx - r, y, r * 2, th);
+        spacing = 5 + Math.max(0, rel) * 10;
+      }
+      y += spacing;
+    }
+    ctx.restore();
+
+    // Rim light
+    ctx.strokeStyle = `rgba(255,200,100,${0.4 + pulse * 0.3})`;
+    ctx.lineWidth = 2;
+    ctx.beginPath();
+    ctx.arc(cx, horizon - r * 0.1, r, 0, Math.PI * 2);
+    ctx.stroke();
+  }
+
+  function drawMountains() {
+    const horizon = H * 0.55;
+    // Back range
+    ctx.beginPath();
+    ctx.moveTo(0, horizon);
+    const peaks = [
+      [0.0, 0], [0.1, 0.08], [0.18, 0.03], [0.28, 0.14],
+      [0.38, 0.05], [0.48, 0.12], [0.55, 0.04], [0.65, 0.16],
+      [0.75, 0.06], [0.85, 0.11], [0.95, 0.03], [1.0, 0.07]
+    ];
+    peaks.forEach(([px, py]) => {
+      ctx.lineTo(W * px, horizon - H * py * (0.9 + pulse * 0.1));
+    });
+    ctx.lineTo(W, horizon);
+    ctx.closePath();
+    const mg = ctx.createLinearGradient(0, horizon - H * 0.18, 0, horizon);
+    mg.addColorStop(0, "#2a0845");
+    mg.addColorStop(1, "#120020");
+    ctx.fillStyle = mg;
+    ctx.fill();
+
+    // Front silhouette
+    ctx.beginPath();
+    ctx.moveTo(0, horizon);
+    const peaks2 = [
+      [0, 0], [0.08, 0.05], [0.15, 0.02], [0.25, 0.09],
+      [0.4, 0.03], [0.5, 0.07], [0.6, 0.02], [0.72, 0.1],
+      [0.82, 0.04], [0.92, 0.06], [1, 0.02]
+    ];
+    peaks2.forEach(([px, py]) => {
+      ctx.lineTo(W * px, horizon - H * py * 0.7);
+    });
+    ctx.lineTo(W, horizon);
+    ctx.closePath();
+    ctx.fillStyle = "#0d0018";
+    ctx.fill();
+  }
+
+  function drawGrid() {
+    const horizon = H * 0.55;
+    const vanishX = W * 0.5;
+    const bassBoost = 1 + pulse * 0.35;
+    const scroll = (time * 0.35) % 1;
+
+    // Ground fill
+    const gg = ctx.createLinearGradient(0, horizon, 0, H);
+    gg.addColorStop(0, "#120028");
+    gg.addColorStop(1, "#050010");
+    ctx.fillStyle = gg;
+    ctx.fillRect(0, horizon, W, H - horizon);
+
+    // Horizontal lines (perspective)
+    ctx.lineWidth = 1.5;
+    const rows = 24;
+    for (let i = 0; i <= rows; i++) {
+      const t = (i + scroll) / rows;
+      if (t <= 0 || t > 1) continue;
+      // Ease for perspective
+      const p = t * t;
+      const y = horizon + p * (H - horizon) * bassBoost * 0.98;
+      if (y > H) continue;
+      const alpha = 0.15 + t * 0.65 + pulse * 0.2;
+      const r = Math.floor(255 * (0.2 + smoothMid * 0.3));
+      const g = Math.floor(50 + 150 * smoothHi);
+      const b = Math.floor(200 + 55 * pulse);
+      ctx.strokeStyle = `rgba(${Math.min(255, r + 100)}, ${Math.min(255, g)}, ${b}, ${alpha})`;
+      ctx.beginPath();
+      ctx.moveTo(0, y);
+      ctx.lineTo(W, y);
+      ctx.stroke();
+    }
+
+    // Vertical lines converging to vanishing point
+    const cols = 18;
+    for (let i = -cols; i <= cols; i++) {
+      if (i === 0) continue;
+      const xBottom = vanishX + i * (W / cols) * 1.4;
+      const alpha = 0.25 + pulse * 0.2;
+      ctx.strokeStyle = `rgba(255, 40, 120, ${alpha})`;
+      ctx.lineWidth = 1.2;
+      ctx.beginPath();
+      ctx.moveTo(vanishX, horizon);
+      ctx.lineTo(xBottom, H + 10);
+      ctx.stroke();
+    }
+
+    // Center glow line
+    ctx.strokeStyle = `rgba(5, 217, 232, ${0.35 + pulse * 0.4})`;
+    ctx.lineWidth = 2;
+    ctx.beginPath();
+    ctx.moveTo(vanishX, horizon);
+    ctx.lineTo(vanishX, H);
+    ctx.stroke();
+
+    // Horizon neon line
+    const hg = ctx.createLinearGradient(0, horizon, W, horizon);
+    hg.addColorStop(0, "rgba(255,42,109,0)");
+    hg.addColorStop(0.5, `rgba(255,42,109,${0.85 + pulse * 0.15})`);
+    hg.addColorStop(1, "rgba(255,42,109,0)");
+    ctx.strokeStyle = hg;
+    ctx.lineWidth = 2 + pulse * 3;
+    ctx.beginPath();
+    ctx.moveTo(0, horizon);
+    ctx.lineTo(W, horizon);
+    ctx.stroke();
+
+    // Cyan accent on horizon
+    ctx.strokeStyle = `rgba(5,217,232,${0.4 + pulse * 0.3})`;
+    ctx.lineWidth = 1;
+    ctx.beginPath();
+    ctx.moveTo(W * 0.2, horizon);
+    ctx.lineTo(W * 0.8, horizon);
+    ctx.stroke();
+  }
+
+  function drawSpectrum() {
+    if (!analyser) return;
+    const horizon = H * 0.55;
+    const bars = 48;
+    const dataStep = Math.floor(freqData.length / bars);
+    const barW = W / bars;
+    for (let i = 0; i < bars; i++) {
+      let sum = 0;
+      for (let j = 0; j < dataStep; j++) sum += freqData[i * dataStep + j] || 0;
+      const v = (sum / (dataStep * 255)) || 0;
+      const h = v * H * 0.12 * (1 + pulse * 0.3);
+      if (h < 1) continue;
+      const x = i * barW;
+      const grad = ctx.createLinearGradient(0, horizon - h, 0, horizon);
+      grad.addColorStop(0, "rgba(5,217,232,0.7)");
+      grad.addColorStop(0.5, "rgba(255,42,109,0.5)");
+      grad.addColorStop(1, "rgba(255,42,109,0)");
+      ctx.fillStyle = grad;
+      ctx.fillRect(x + 1, horizon - h, barW - 2, h);
+    }
+  }
+
+  function drawVignette() {
+    const vg = ctx.createRadialGradient(W / 2, H / 2, H * 0.25, W / 2, H / 2, H * 0.85);
+    vg.addColorStop(0, "rgba(0,0,0,0)");
+    vg.addColorStop(1, "rgba(0,0,0,0.55)");
+    ctx.fillStyle = vg;
+    ctx.fillRect(0, 0, W, H);
+  }
+
+  function drawScanlines() {
+    ctx.fillStyle = "rgba(0,0,0,0.07)";
+    for (let y = 0; y < H; y += 3) {
+      ctx.fillRect(0, y, W, 1);
+    }
+  }
+
+  let last = performance.now();
+  function frame(now) {
+    const dt = Math.min(0.05, (now - last) / 1000);
+    last = now;
+    time += dt;
+
+    getEnergy();
+
+    drawSky();
+    drawStars();
+    drawSun();
+    drawMountains();
+    drawSpectrum();
+    drawGrid();
+    drawVignette();
+    drawScanlines();
+
+    requestAnimationFrame(frame);
+  }
+  requestAnimationFrame(frame);
+})();
+</script>
+</body>
+</html>
\ No newline at end of file
diff --git a/data/artifacts/966d54f224fd/gemma3-12b.html b/data/artifacts/966d54f224fd/gemma3-12b.html
new file mode 100644
index 0000000..6419fbc
--- /dev/null
+++ b/data/artifacts/966d54f224fd/gemma3-12b.html
@@ -0,0 +1,184 @@
+<!DOCTYPE html>
+<html>
+<head>
+<title>Tron Game</title>
+<style>
+body { margin: 0; overflow: hidden; background-color: black; }
+canvas { display: block; }
+</style>
+</head>
+<body>
+
+<canvas id="tronCanvas"></canvas>
+
+<script>
+const canvas = document.getElementById('tronCanvas');
+const ctx = canvas.getContext('2d');
+
+canvas.width = window.innerWidth;
+canvas.height = window.innerHeight;
+
+let playerX = 50;
+let playerY = 50;
+let speedX = 2;
+let speedY = 0;
+let trail = [];
+let enemyX = canvas.width - 50;
+let enemyY = 50;
+let enemySpeedX = -2;
+let enemySpeedY = 0;
+let enemyTrail = [];
+let score = 0;
+
+function drawPlayer() {
+    ctx.fillStyle = 'red';
+    ctx.fillRect(playerX, playerY, 10, 10);
+}
+
+function drawEnemy() {
+    ctx.fillStyle = 'blue';
+    ctx.fillRect(enemyX, enemyY, 10, 10);
+}
+
+function updatePlayer() {
+    playerX += speedX;
+    playerY += speedY;
+
+    if (playerX < 0) playerX = canvas.width;
+    if (playerX > canvas.width) playerX = 0;
+    if (playerY < 0) playerY = canvas.height;
+    if (playerY > canvas.height) playerY = 0;
+}
+
+function updateEnemy() {
+    enemyX += enemySpeedX;
+    enemyY += enemySpeedY;
+
+    if(enemyX < 0) enemyX = canvas.width;
+    if(enemyX > canvas.width) enemyX = 0;
+    if (enemyY < 0) enemyY = canvas.height;
+    if(enemyY > canvas.height) enemyY = 0;
+}
+
+function drawTrail() {
+    ctx.fillStyle = 'rgba(255, 0, 0, 0.3)';
+    for (let i = 0; i < trail.length; i++) {
+        ctx.fillRect(trail[i].x, trail[i].y, 10, 10);
+    }
+}
+
+function drawEnemyTrail() {
+    ctx.fillStyle = 'rgba(0, 0, 255, 0.3)';
+    for (let i = 0; i < enemyTrail.length; i++) {
+        ctx.fillRect(enemyTrail[i].x, enemyTrail[i].y, 10, 10);
+    }
+}
+
+function collision() {
+    // Player - Enemy Collision
+    if (playerX === enemyX && playerY === enemyY) {
+      alert("Game Over! Score: " + score);
+      reset();
+    }
+
+    //Player - Trail Collision
+    for(let i = 0; i < trail.length; i++){
+        if (playerX === trail[i].x && playerY === trail[i].y){
+          alert("Game Over! Score: " + score);
+          reset();
+        }
+    }
+
+    //Enemy - Trail Collision
+    for(let i = 0; i < enemyTrail.length; i++){
+      if (enemyX === enemyTrail[i].x && enemyY === enemyTrail[i].y){
+        alert("Game Over! Score: " + score);
+        reset();
+      }
+    }
+}
+
+function reset() {
+  playerX = 50;
+  playerY = 50;
+  speedX = 2;
+  speedY = 0;
+  trail = [];
+  enemyX = canvas.width - 50;
+  enemyY = 50;
+  enemySpeedX = -2;
+  enemySpeedY = 0;
+  enemyTrail = [];
+  score = 0;
+}
+
+function gameLoop() {
+    ctx.clearRect(0, 0, canvas.width, canvas.height);
+
+    drawPlayer();
+    drawEnemy();
+    drawTrail();
+    drawEnemyTrail();
+
+    collision();
+
+    updatePlayer();
+    updateEnemy();
+
+    trail.push({ x: playerX, y: playerY });
+    if (trail.length > 20) {
+        trail.shift();
+    }
+
+    enemyTrail.push({x: enemyX, y: enemyY});
+    if(enemyTrail.length > 20){
+      enemyTrail.shift();
+    }
+
+    requestAnimationFrame(gameLoop);
+}
+
+
+document.addEventListener('keydown', function(event) {
+    switch (event.key) {
+        case 'ArrowUp':
+            speedY = -2;
+            break;
+        case 'ArrowDown':
+            speedY = 2;
+            break;
+        case 'ArrowLeft':
+            speedX = -2;
+            break;
+        case 'ArrowRight':
+            speedX = 2;
+            break;
+    }
+});
+
+document.addEventListener('keyup', function(event) {
+    if (event.key === 'ArrowUp') speedY = 0;
+    else if (event.key === 'ArrowDown') speedY = 0;
+    else if (event.key === 'ArrowLeft') speedX = 0;
+    else if (event.key === 'ArrowRight') speedX = 0;
+});
+
+//AI enemy movement - simple chase
+function updateEnemyAI(){
+  enemySpeedX = playerX - enemyX;
+  enemySpeedY = playerY - enemyY;
+
+  let distance = Math.sqrt(Math.pow(playerX - enemyX, 2) + Math.pow(playerY - enemyY, 2));
+  if (distance > 50){
+    enemySpeedX /= 2;
+    enemySpeedY /= 2;
+  }
+
+  updateEnemy();
+}
+
+gameLoop();
+</script>
+
+</body>
+</html>
\ No newline at end of file
diff --git a/data/artifacts/966d54f224fd/grok.html b/data/artifacts/966d54f224fd/grok.html
new file mode 100644
index 0000000..ff9c772
--- /dev/null
+++ b/data/artifacts/966d54f224fd/grok.html
@@ -0,0 +1,256 @@
+<!DOCTYPE html>
+<html lang="en">
+<head>
+<meta charset="UTF-8">
+<meta name="viewport" content="width=device-width, initial-scale=1.0">
+<title>Tron Light Cycles</title>
+<style>
+*{margin:0;padding:0;box-sizing:border-box}
+body{background:#000;display:flex;justify-content:center;align-items:center;min-height:100vh;font-family:'Segoe UI',system-ui,sans-serif;overflow:hidden;color:#0ff}
+#wrap{position:relative;display:flex;flex-direction:column;align-items:center;gap:12px}
+canvas{border:2px solid #0ff;box-shadow:0 0 20px #0ff4,inset 0 0 40px #0ff1;background:#050510;image-rendering:pixelated;cursor:none}
+#hud{display:flex;gap:32px;font-size:14px;letter-spacing:2px;text-transform:uppercase;text-shadow:0 0 8px #0ff}
+#hud span{color:#fff}
+#overlay{position:absolute;inset:0;display:flex;flex-direction:column;justify-content:center;align-items:center;background:rgba(0,0,0,.85);z-index:10;gap:16px}
+#overlay h1{font-size:48px;letter-spacing:8px;text-shadow:0 0 20px #0ff,0 0 40px #0ff;color:#0ff}
+#overlay p{font-size:16px;color:#8ff;letter-spacing:2px;max-width:400px;text-align:center;line-height:1.6}
+#overlay .btn{margin-top:12px;padding:12px 36px;border:2px solid #0ff;background:transparent;color:#0ff;font-size:18px;letter-spacing:4px;cursor:pointer;text-transform:uppercase;transition:all .2s;font-family:inherit}
+#overlay .btn:hover{background:#0ff;color:#000;box-shadow:0 0 30px #0ff}
+#scores{display:flex;gap:40px;font-size:13px;letter-spacing:1px}
+.p1{color:#0ff;text-shadow:0 0 6px #0ff}
+.p2{color:#f0f;text-shadow:0 0 6px #f0f}
+.hidden{display:none!important}
+</style>
+</head>
+<body>
+<div id="wrap">
+<div id="hud">
+<div class="p1">YOU <span id="s1">0</span></div>
+<div class="p2">CPU <span id="s2">0</span></div>
+</div>
+<canvas id="c" width="640" height="480"></canvas>
+<div id="overlay">
+<h1>TRON</h1>
+<p>Light Cycles — survive longer than the CPU.<br>Arrow keys or WASD to steer.<br>Don't hit walls or trails.</p>
+<button class="btn" id="startBtn">Start</button>
+</div>
+</div>
+<script>
+const canvas=document.getElementById('c');
+const ctx=canvas.getContext('2d');
+const overlay=document.getElementById('overlay');
+const startBtn=document.getElementById('startBtn');
+const s1El=document.getElementById('s1');
+const s2El=document.getElementById('s2');
+
+const W=640,H=480,CELL=8;
+const COLS=W/CELL,ROWS=H/CELL;
+const DIRS={UP:[0,-1],DOWN:[0,1],LEFT:[-1,0],RIGHT:[1,0]};
+const OPP={UP:'DOWN',DOWN:'UP',LEFT:'RIGHT',RIGHT:'LEFT'};
+
+let grid,players,running,frame,scores=[0,0],keys={},animId;
+
+function resetGrid(){
+  grid=new Array(COLS*ROWS).fill(0);
+}
+
+function idx(x,y){return y*COLS+x;}
+
+function makePlayer(x,y,dir,color,glow,isAI){
+  return{x,y,dir,nextdir:dir,color,glow,alive:true,trail:[],isAI,path:[]};
+}
+
+function initGame(){
+  resetGrid();
+  players=[
+    makePlayer(Math.floor(COLS*0.25),Math.floor(ROWS/2),'RIGHT','#00ffff','#00ffff88',false),
+    makePlayer(Math.floor(COLS*0.75),Math.floor(ROWS/2),'LEFT','#ff00ff','#ff00ff88',true)
+  ];
+  running=true;
+  frame=0;
+  keys={};
+}
+
+function drawGridBg(){
+  ctx.fillStyle='#050510';
+  ctx.fillRect(0,0,W,H);
+  ctx.strokeStyle='#0a0a20';
+  ctx.lineWidth=1;
+  for(let x=0;x<=W;x+=CELL){ctx.beginPath();ctx.moveTo(x,0);ctx.lineTo(x,H);ctx.stroke();}
+  for(let y=0;y<=H;y+=CELL){ctx.beginPath();ctx.moveTo(0,y);ctx.lineTo(W,y);ctx.stroke();}
+}
+
+function drawTrail(p){
+  if(p.trail.length<1)return;
+  ctx.shadowBlur=12;
+  ctx.shadowColor=p.color;
+  ctx.strokeStyle=p.color;
+  ctx.lineWidth=CELL-2;
+  ctx.lineCap='square';
+  ctx.lineJoin='miter';
+  ctx.beginPath();
+  const t=p.trail;
+  ctx.moveTo(t[0][0]*CELL+CELL/2,t[0][1]*CELL+CELL/2);
+  for(let i=1;i<t.length;i++){
+    ctx.lineTo(t[i][0]*CELL+CELL/2,t[i][1]*CELL+CELL/2);
+  }
+  ctx.lineTo(p.x*CELL+CELL/2,p.y*CELL+CELL/2);
+  ctx.stroke();
+  ctx.shadowBlur=0;
+  // head glow
+  ctx.fillStyle=p.color;
+  ctx.shadowBlur=18;
+  ctx.shadowColor=p.color;
+  ctx.fillRect(p.x*CELL+1,p.y*CELL+1,CELL-2,CELL-2);
+  ctx.shadowBlur=0;
+}
+
+function valid(x,y){
+  return x>=0&&x<COLS&&y>=0&&y<ROWS&&grid[idx(x,y)]===0;
+}
+
+function aiThink(p){
+  const dirs=['UP','DOWN','LEFT','RIGHT'];
+  const cur=p.dir;
+  const candidates=[];
+  for(const d of dirs){
+    if(d===OPP[cur])continue;
+    const [dx,dy]=DIRS[d];
+    const nx=p.x+dx,ny=p.y+dy;
+    if(valid(nx,ny)){
+      // score by open space ahead
+      let open=0,cx=nx,cy=ny;
+      for(let i=0;i<12;i++){
+        cx+=dx;cy+=dy;
+        if(!valid(cx,cy))break;
+        open++;
+      }
+      // look sideways a bit
+      const side=d==='UP'||d==='DOWN'?['LEFT','RIGHT']:['UP','DOWN'];
+      for(const s of side){
+        const [sx,sy]=DIRS[s];
+        if(valid(nx+sx,ny+sy))open+=0.5;
+      }
+      // prefer not turning if straight is good
+      if(d===cur)open+=1.5;
+      candidates.push({d,open});
+    }
+  }
+  if(candidates.length===0)return;
+  candidates.sort((a,b)=>b.open-a.open);
+  // slight randomness among top
+  const top=candidates.filter(c=>c.open>=candidates[0].open-1);
+  p.nextdir=top[Math.floor(Math.random()*top.length)].d;
+}
+
+function step(){
+  if(!running)return;
+  frame++;
+  // AI
+  for(const p of players){
+    if(p.alive&&p.isAI&&frame%2===0)aiThink(p);
+  }
+  // apply next dir
+  for(const p of players){
+    if(!p.alive)continue;
+    if(p.nextdir!==OPP[p.dir])p.dir=p.nextdir;
+  }
+  // move
+  for(const p of players){
+    if(!p.alive)continue;
+    p.trail.push([p.x,p.y]);
+    grid[idx(p.x,p.y)]=1;
+    const [dx,dy]=DIRS[p.dir];
+    p.x+=dx;p.y+=dy;
+  }
+  // collisions (after all moved — simultaneous)
+  const dead=[];
+  for(let i=0;i<players.length;i++){
+    const p=players[i];
+    if(!p.alive)continue;
+    if(p.x<0||p.x>=COLS||p.y<0||p.y>=ROWS){dead.push(i);continue;}
+    // hit trail or head-on
+    if(grid[idx(p.x,p.y)]!==0){dead.push(i);continue;}
+    for(let j=0;j<players.length;j++){
+      if(i===j)continue;
+      const o=players[j];
+      if(o.alive&&o.x===p.x&&o.y===p.y){dead.push(i);break;}
+    }
+  }
+  for(const i of dead)players[i].alive=false;
+  // mark new heads
+  for(const p of players){
+    if(p.alive)grid[idx(p.x,p.y)]=1;
+  }
+  // check end
+  const alive=players.filter(p=>p.alive);
+  if(alive.length<=1){
+    running=false;
+    if(alive.length===1){
+      if(!alive[0].isAI){scores[0]++;s1El.textContent=scores[0];showEnd('YOU WIN');}
+      else{scores[1]++;s2El.textContent=scores[1];showEnd('CPU WINS');}
+    }else{
+      showEnd('DRAW');
+    }
+  }
+}
+
+function showEnd(msg){
+  overlay.classList.remove('hidden');
+  overlay.querySelector('h1').textContent=msg;
+  overlay.querySelector('p').textContent='Press Start or Space for another round';
+  startBtn.textContent='Again';
+}
+
+function render(){
+  drawGridBg();
+  for(const p of players)drawTrail(p);
+}
+
+function loop(){
+  step();
+  render();
+  if(running)animId=requestAnimationFrame(()=>{
+    // speed control ~12 moves/sec
+    setTimeout(loop,1000/14);
+  });
+}
+
+function start(){
+  cancelAnimationFrame(animId);
+  overlay.classList.add('hidden');
+  initGame();
+  render();
+  loop();
+}
+
+startBtn.addEventListener('click',start);
+
+document.addEventListener('keydown',e=>{
+  const k=e.key.toLowerCase();
+  if(k===' '||k==='enter'){
+    if(!running)start();
+    e.preventDefault();
+    return;
+  }
+  if(!running||!players[0].alive)return;
+  const p=players[0];
+  let nd=null;
+  if(k==='arrowup'||k==='w')nd='UP';
+  else if(k==='arrowdown'||k==='s')nd='DOWN';
+  else if(k==='arrowleft'||k==='a')nd='LEFT';
+  else if(k==='arrowright'||k==='d')nd='RIGHT';
+  if(nd&&nd!==OPP[p.dir]){p.nextdir=nd;e.preventDefault();}
+});
+
+// initial splash render
+resetGrid();
+players=[];
+drawGridBg();
+ctx.fillStyle='#0ff';
+ctx.font='bold 20px monospace';
+ctx.textAlign='center';
+ctx.shadowBlur=10;ctx.shadowColor='#0ff';
+</script>
+</body>
+</html>
\ No newline at end of file
diff --git a/data/artifacts/966d54f224fd/hermes3-8b.html b/data/artifacts/966d54f224fd/hermes3-8b.html
new file mode 100644
index 0000000..8c7aeb4
--- /dev/null
+++ b/data/artifacts/966d54f224fd/hermes3-8b.html
@@ -0,0 +1,284 @@
+<!DOCTYPE html>
+<html lang="en">
+<head>
+    <meta charset="UTF-8">
+    <meta name="viewport" content="width=device-width, initial-scale=1.0">
+    <title>Tron Game</title>
+    <style>
+        * {
+            margin: 0;
+            padding: 0;
+            font-family: Arial, sans-serif;
+        }
+        
+        canvas {
+            display: block;
+            background-color: #000;
+        }
+
+        #score {
+            color: white;
+            position: absolute;
+            top: 10px;
+            right: 20px;
+            z-index: 1;
+        }
+
+        .circle {
+            position: absolute;
+            width: 5px;
+            height: 5px;
+            border-radius: 50%;
+            background-color: #fff;
+            z-index: 2;
+        }
+    </style>
+</head>
+<body>
+    <div id="score">Player 1: 0 - Player 2: 0</div>
+    <canvas id="gameCanvas" width="800" height="400"></canvas>
+
+    <script>
+        const canvas = document.getElementById('gameCanvas');
+        const ctx = canvas.getContext('2d');
+
+        let player1Position = { x: 5, y: 200 };
+        let player2Position = { x: 795, y: 200 };
+        let playersPositionsHistory = [{}, {}];
+        let score = [0, 0];
+
+        function drawPlayers() {
+            ctx.fillStyle = 'yellow';
+            ctx.beginPath();
+            ctx.arc(player1Position.x, player1Position.y, 2.5, 0, 2 * Math.PI);
+            ctx.fill();
+
+            ctx.fillStyle = 'blue';
+            ctx.beginPath();
+            ctx.arc(player2Position.x, player2Position.y, 2.5, 0, 2 * Math.PI);
+            ctx.fill();
+        }
+
+        function drawLines() {
+            ctx.lineWidth = 1;
+            ctx.strokeStyle = 'white';
+
+            for (let i = -10; i <= canvas.height + 10; i++) {
+                ctx.beginPath();
+                ctx.moveTo(0, i);
+                ctx.lineTo(canvas.width, i);
+                ctx.stroke();
+            }
+
+            for (let i = -10; i <= canvas.width + 10; i += 20) {
+                ctx.beginPath();
+                ctx.moveTo(i, 0);
+                ctx.lineTo(i, canvas.height);
+                ctx.stroke();
+            }
+        }
+
+        function updatePlayerPosition(playerIndex, dx, dy) {
+            playersPositionsHistory[playerIndex][`x${dx > 0 ? '++' : '--'}y${dy > 0 ? '++' : '--'}`] = true;
+            playerPositionsHistory[playerIndex].x += dx;
+            playerPositionsHistory[playerIndex].y += dy;
+
+            if (playersPositionsHistory[playerIndex].x < 0 || playersPositionsHistory[playerIndex].x > canvas.width ||
+                playersPositionsHistory[playerIndex].y < 0 || playersPositionsHistory[playerIndex].y > canvas.height) {
+                return false;
+            }
+
+            return true;
+        }
+
+        function checkCollision(playerIndex, dx, dy) {
+            let collided = false;
+
+            for (let i = 0; i < playerIndex !== 0 ? 1 : 2; i++) {
+                if (i === playerIndex) continue;
+
+                const otherPlayerHistory = playersPositionsHistory[i];
+                const currentOtherPlayerPosition = { x: otherPlayerHistory.x, y: otherPlayerHistory.y };
+                
+                for (const [key, value] of Object.entries(otherPlayerHistory)) {
+                    if (!value) continue;
+                    
+                    switch (key) {
+                        case 'x++y++':
+                            if (updatePlayerPosition(playerIndex, dx > 0 ? 1 : -1, dy > 0 ? 1 : -1) && 
+                                updatePlayerPosition(i, value.x - currentOtherPlayerPosition.x, value.y - currentOtherPlayerPosition.y)) {
+                                collided = true;
+                                break;
+                            }
+                            break;
+                        case 'x--y++':
+                            if (updatePlayerPosition(playerIndex, dx < 0 ? 1 : -1, dy > 0 ? 1 : -1) && 
+                                updatePlayerPosition(i, value.x + currentOtherPlayerPosition.x, value.y - currentOtherPlayerPosition.y)) {
+                                collided = true;
+                                break;
+                            }
+                            break;
+                        case 'x++y--':
+                            if (updatePlayerPosition(playerIndex, dx > 0 ? 1 : -1, dy < 0 ? 1 : -1) && 
+                                updatePlayerPosition(i, value.x - currentOtherPlayerPosition.x, value.y + currentOtherPlayerPosition.y)) {
+                                collided = true;
+                                break;
+                            }
+                            break;
+                        case 'x--y--':
+                            if (updatePlayerPosition(playerIndex, dx < 0 ? 1 : -1, dy < 0 ? 1 : -1) && 
+                                updatePlayerPosition(i, value.x + currentOtherPlayerPosition.x, value.y + currentOtherPlayerPosition.y)) {
+                                collided = true;
+                                break;
+                            }
+                            break;
+                    }
+                }
+
+                if (collided) return true;
+            }
+
+            return false;
+        }
+
+        function movePlayers(event) {
+            const dx = event.keyCode === 37 || event.keyCode === 65 ? -1 : event.keyCode === 39 || event.keyCode === 68 ? 1 : 0;
+            const dy = event.keyCode === 87 || event.keyCode === 83 ? 1 : event.keyCode === 81 || event.keyCode === 69 ? -1 : 0;
+
+            if (dx !== 0 && !checkCollision(0, dx, dy)) {
+                player1Position.x += 10 * dx;
+                player1Position.y += 10 * dy;
+            }
+
+            if (dy !== 0 && !checkCollision(1, dx, dy)) {
+                player2Position.x += 10 * dx;
+                player2Position.y += 10 * dy;
+            }
+        }
+
+        document.addEventListener('keydown', movePlayers);
+
+        function gameLoop() {
+            ctx.clearRect(0, 0, canvas.width, canvas.height);
+            drawPlayers();
+            drawLines();
+
+            for (let i = 0; i < playersPositionsHistory.length; i++) {
+                const playerHistory = playersPositionsHistory[i];
+                let playerIndex = i === 0 ? 1 : 0;
+
+                for (const [key, value] of Object.entries(playerHistory)) {
+                    if (!value) continue;
+                    
+                    switch (key) {
+                        case 'x++y++':
+                            player1Position.x = parseInt(key.split('x++y')[0]) + 10 * (playerIndex === 0 ? 1 : -1);
+                            player1Position.y = parseInt(key.split('y++')[1]) + 10 * (playerIndex === 0 ? 1 : -1);
+                            break;
+                        case 'x--y++':
+                            player2Position.x = parseInt(key.split('x--y')[0]) + 10 * (playerIndex === 0 ? -1 : 1);
+                            player2Position.y = parseInt(key.split('y++')[1]) + 10 * (playerIndex === 0 ? 1 : -1);
+                            break;
+                        case 'x++y--':
+                            player1Position.x = parseInt(key.split('x++y')[0]) + 10 * (playerIndex === 0 ? 1 : -1);
+                            player1Position.y = parseInt(key.split('y--')[1]) + 10 * (playerIndex === 0 ? -1 : 1);
+                            break;
+                        case 'x--y--':
+                            player2Position.x = parseInt(key.split('x--y')[0]) + 10 * (playerIndex === 0 ? -1 : 1);
+                            player2Position.y = parseInt(key.split('y--')[1]) + 10 * (playerIndex === 0 ? -1 : 1);
+                    }
+                }
+            }
+
+            setTimeout(gameLoop, 100);
+        }
+
+        gameLoop();
+
+        setInterval(() => {
+            if (
+                canvas.width <= Math.min(player1Position.x, player2Position.x) ||
+                player1Position.x >= canvas.width + 10 ||
+                player1Position.y >= canvas.height + 10 ||
+                player2Position.x <= -10 ||
+                player2Position.y >= canvas.height + 10
+            ) {
+                score = [score[1], score[0]];
+                document.getElementById('score').innerText = `Player 1: ${score[0]} - Player 2: ${score[1]}`;
+                return;
+            }
+
+            const player1LastPositionsHistory = playersPositionsHistory[0];
+            const player2LastPositionsHistory = playersPositionsHistory[1];
+
+            for (const [key, value] of Object.entries(player1LastPositionsHistory)) {
+                if (!value) continue;
+
+                switch (key) {
+                    case 'x++y++':
+                        if (player1Position.x <= canvas.width && player1Position.y <= canvas.height &&
+                            !player2PositionsHistory[1].hasOwnProperty(key)) {
+                            score[0]++;
+                            document.getElementById('score').innerText = `Player 1: ${score[0]} - Player 2: ${score[1]}`;
+                        }
+                        break;
+                    case 'x--y++':
+                        if (player1Position.x >= -10 && player1Position.y <= canvas.height &&
+                            !player2PositionsHistory[1].hasOwnProperty(key)) {
+                            score[0]++;
+                            document.getElementById('score').innerText = `Player 1: ${score[0]} - Player 2: ${score[1]}`;
+                        }
+                        break;
+                    case 'x++y--':
+                        if (player1Position.x <= canvas.width && player1Position.y >= -10 &&
+                            !player2PositionsHistory[1].hasOwnProperty(key)) {
+                            score[0]++;
+                            document.getElementById('score').innerText = `Player 1: ${score[0]} - Player 2: ${score[1]}`;
+                        }
+                        break;
+                    case 'x--y--':
+                        if (player1Position.x >= -10 && player1Position.y >= -10 &&
+                            !player2PositionsHistory[1].hasOwnProperty(key)) {
+                            score[0]++;
+                            document.getElementById('score').innerText = `Player 1: ${score[0]} - Player 2: ${score[1]}`;
+                        }
+                }
+            }
+
+            for (const [key, value] of Object.entries(player2LastPositionsHistory)) {
+                if (!value) continue;
+
+                switch (key) {
+                    case 'x++y++':
+                        if (player2Position.x >= -10 && player2Position.y <= canvas.height &&
+                            !player1PositionsHistory[0].hasOwnProperty(key)) {
+                            score[1]++;
+                            document.getElementById('score').innerText = `Player 1: ${score[0]} - Player 2: ${score[1]}`;
+                        }
+                        break;
+                    case 'x--y++':
+                        if (player2Position.x <= canvas.width && player2Position.y <= canvas.height &&
+                            !player1PositionsHistory[0].hasOwnProperty(key)) {
+                            score[1]++;
+                            document.getElementById('score').innerText = `Player 1: ${score[0]} - Player 2: ${score[1]}`;
+                        }
+                        break;
+                    case 'x++y--':
+                        if (player2Position.x >= -10 && player2Position.y >= -10 &&
+                            !player1PositionsHistory[0].hasOwnProperty(key)) {
+                            score[1]++;
+                            document.getElementById('score').innerText = `Player 1: ${score[0]} - Player 2: ${score[1]}`;
+                        }
+                        break;
+                    case 'x--y--':
+                        if (player2Position.x <= canvas.width && player2Position.y >= -10 &&
+                            !player1PositionsHistory[0].hasOwnProperty(key)) {
+                            score[1]++;
+                            document.getElementById('score').innerText = `Player 1: ${score[0]} - Player 2: ${score[1]}`;
+                        }
+                }
+            }
+        }, 500);
+    </script>
+</body>
+</html>
\ No newline at end of file
diff --git a/data/artifacts/966d54f224fd/kimi.html b/data/artifacts/966d54f224fd/kimi.html
new file mode 100644
index 0000000..a5fb067
--- /dev/null
+++ b/data/artifacts/966d54f224fd/kimi.html
@@ -0,0 +1,135 @@
+<!DOCTYPE html>
+<html lang="en">
+<head>
+<meta charset="UTF-8">
+<title>Tron Light Cycles</title>
+<style>
+body { margin: 0; overflow: hidden; background: #050505; display: flex; justify-content: center; align-items: center; height: 100vh; font-family: 'Courier New', monospace; }
+canvas { box-shadow: 0 0 30px #00f3ff, inset 0 0 30px rgba(0,243,255,0.1); border: 2px solid #00f3ff; background: #000; }
+#ui { position: absolute; top: 20px; left: 50%; transform: translateX(-50%); color: #00f3ff; text-align: center; text-shadow: 0 0 10px #00f3ff; pointer-events: none; font-size: 14px; letter-spacing: 2px; }
+#ui span { display: block; margin-top: 5px; font-size: 12px; opacity: 0.8; }
+</style>
+</head>
+<body>
+<div id="ui">TRON LIGHT CYCLES<span>ARROWS to STEER | R to RESET</span></div>
+<canvas id="c"></canvas>
+<script>
+const canvas = document.getElementById('c');
+const ctx = canvas.getContext('2d');
+const W = 100, H = 60, CELL = 10;
+canvas.width = W * CELL;
+canvas.height = H * CELL;
+
+let grid, player, ai, gameOver, frame;
+
+class Bike {
+  constructor(x, y, dir, color, isAI) {
+    this.x = x; this.y = y; this.dir = dir; this.color = color; this.isAI = isAI; this.alive = true;
+  }
+  
+  aiMove() {
+    const opp = {0:2, 2:0, 1:3, 3:1};
+    const dx = [0, 1, 0, -1], dy = [-1, 0, 1, 0];
+    let best = this.dir, bestScore = -1;
+    
+    for (let d = 0; d < 4; d++) {
+      if (d === opp[this.dir]) continue;
+      let nx = this.x + dx[d], ny = this.y + dy[d];
+      if (nx < 0 || nx >= W || ny < 0 || ny >= H || grid[nx][ny]) continue;
+      
+      let score = 0, cx = nx, cy = ny;
+      for (let i = 0; i < 6; i++) {
+        if (cx < 0 || cx >= W || cy < 0 || cy >= H || grid[cx][cy]) break;
+        score++; cx += dx[d]; cy += dy[d];
+      }
+      if (score > bestScore) { bestScore = score; best = d; }
+    }
+    this.dir = best;
+  }
+}
+
+function reset() {
+  grid = Array(W).fill(0).map(() => Array(H).fill(false));
+  player = new Bike(20, 30, 1, '#00f3ff', false);
+  ai = new Bike(80, 30, 3, '#ff0055', true);
+  gameOver = false; frame = 0;
+}
+
+function update() {
+  if (gameOver) return;
+  
+  if (frame % 3 === 0) {
+    const dx = [0, 1, 0, -1], dy = [-1, 0, 1, 0];
+    
+    if (ai.alive) ai.aiMove();
+    
+    let pNext = {x: player.x + dx[player.dir], y: player.y + dy[player.dir]};
+    let aNext = {x: ai.x + dx[ai.dir], y: ai.y + dy[ai.dir]};
+    
+    let pDead = pNext.x < 0 || pNext.x >= W || pNext.y < 0 || pNext.y >= H || grid[pNext.x][pNext.y];
+    let aDead = aNext.x < 0 || aNext.x >= W || aNext.y < 0 || aNext.y >= H || grid[aNext.x][aNext.y];
+    
+    if (pNext.x === aNext.x && pNext.y === aNext.y) pDead = aDead = true;
+    if (pNext.x === ai.x && pNext.y === ai.y) pDead = true;
+    if (aNext.x === player.x && aNext.y === player.y) aDead = true;
+    
+    if (!pDead) {
+      grid[player.x][player.y] = true;
+      player.x = pNext.x; player.y = pNext.y;
+    } else player.alive = false;
+    
+    if (!aDead) {
+      grid[ai.x][ai.y] = true;
+      ai.x = aNext.x; ai.y = aNext.y;
+    } else ai.alive = false;
+    
+    if (!player.alive || !ai.alive) gameOver = true;
+  }
+  frame++;
+}
+
+function draw() {
+  ctx.fillStyle = 'rgba(0,0,0,0.3)';
+  ctx.fillRect(0, 0, canvas.width, canvas.height);
+  
+  ctx.fillStyle = '#111';
+  for (let x = 0; x < W; x++) for (let y = 0; y < H; y++) if (grid[x][y]) ctx.fillRect(x*CELL, y*CELL, CELL-1, CELL-1);
+  
+  ctx.shadowBlur = 15;
+  if (player.alive) {
+    ctx.fillStyle = player.color; ctx.shadowColor = player.color;
+    ctx.fillRect(player.x*CELL, player.y*CELL, CELL-1, CELL-1);
+  }
+  if (ai.alive) {
+    ctx.fillStyle = ai.color; ctx.shadowColor = ai.color;
+    ctx.fillRect(ai.x*CELL, ai.y*CELL, CELL-1, CELL-1);
+  }
+  ctx.shadowBlur = 0;
+  
+  if (gameOver) {
+    ctx.fillStyle = 'white';
+    ctx.font = 'bold 40px monospace';
+    ctx.textAlign = 'center';
+    let msg = !player.alive && !ai.alive ? 'DRAW' : (player.alive ? 'YOU WIN' : 'AI WINS');
+    ctx.fillText(msg, canvas.width/2, canvas.height/2);
+    ctx.font = '20px monospace';
+    ctx.fillStyle = '#888';
+    ctx.fillText('Press R', canvas.width/2, canvas.height/2 + 40);
+  }
+}
+
+function loop() { update(); draw(); requestAnimationFrame(loop); }
+
+document.addEventListener('keydown', e => {
+  if (gameOver && (e.key === 'r' || e.key === 'R')) { reset(); return; }
+  const opp = {0:2, 2:0, 1:3, 3:1};
+  const map = {'ArrowUp':0, 'ArrowRight':1, 'ArrowDown':2, 'ArrowLeft':3};
+  if (map.hasOwnProperty(e.key) && player.dir !== opp[map[e.key]]) player.dir = map[e.key];
+  e.preventDefault();
+});
+
+reset();
+loop();
+</script>
+</body>
+</html>
\ No newline at end of file
diff --git a/data/challenges.json b/data/challenges.json
index 48b7803..c1f04ff 100644
--- a/data/challenges.json
+++ b/data/challenges.json
@@ -8,34 +8,212 @@
     "runs": [
       {
         "model": "hermes3-8b",
-        "status": "error",
-        "error": "server restarted mid-run",
-        "seconds": 12,
+        "status": "done",
+        "error": null,
+        "seconds": 18,
         "cost": 0,
-        "started_at": "2026-07-23T05:25:58.638Z",
-        "bytes": 3086,
-        "finished_at": "2026-07-23T05:25:51.776Z"
+        "started_at": "2026-07-23T05:38:07.592Z",
+        "bytes": 2904,
+        "finished_at": "2026-07-23T05:38:25.720Z"
       },
       {
         "model": "qwen25-7b",
         "status": "error",
-        "error": "server restarted mid-run",
+        "error": "timeout after 600s",
         "seconds": 600,
         "cost": null,
-        "started_at": "2026-07-23T05:25:10.008Z",
-        "finished_at": "2026-07-23T05:12:46.664Z"
+        "started_at": "2026-07-23T05:26:27.484Z",
+        "finished_at": "2026-07-23T05:36:27.494Z"
       },
       {
         "model": "gemma3-12b",
         "status": "done",
         "error": null,
-        "seconds": 25,
+        "seconds": 28,
+        "cost": 0,
+        "started_at": "2026-07-23T05:36:16.579Z",
+        "bytes": 3117,
+        "finished_at": "2026-07-23T05:36:44.762Z"
+      },
+      {
+        "model": "qwen3-14b",
+        "status": "done",
+        "error": null,
+        "seconds": 138,
         "cost": 0,
-        "started_at": "2026-07-23T05:25:14.115Z",
-        "bytes": 2760,
-        "finished_at": "2026-07-23T05:25:39.475Z"
+        "started_at": "2026-07-23T05:32:56.078Z",
+        "bytes": 1295,
+        "finished_at": "2026-07-23T05:35:14.449Z"
+      },
+      {
+        "model": "kimi",
+        "status": "done",
+        "error": null,
+        "seconds": 65,
+        "cost": 0.0067,
+        "started_at": "2026-07-23T05:35:30.432Z",
+        "bytes": 1682,
+        "finished_at": "2026-07-23T05:36:35.854Z"
       }
     ],
     "voted_at": "2026-07-23T05:15:22.408Z"
+  },
+  {
+    "id": "966d54f224fd",
+    "title": "neon city",
+    "prompt": "playable html game like tron",
+    "created_at": "2026-07-23T05:29:23.102Z",
+    "winner": null,
+    "runs": [
+      {
+        "model": "qwen3-14b",
+        "status": "error",
+        "error": "no HTML document in model output (0 chars)",
+        "seconds": 142,
+        "cost": null,
+        "started_at": "2026-07-23T05:29:23.133Z",
+        "finished_at": "2026-07-23T05:31:44.783Z"
+      },
+      {
+        "model": "gemma3-12b",
+        "status": "done",
+        "error": null,
+        "seconds": 35,
+        "cost": 0,
+        "started_at": "2026-07-23T05:31:44.784Z",
+        "bytes": 3946,
+        "finished_at": "2026-07-23T05:32:19.763Z"
+      },
+      {
+        "model": "hermes3-8b",
+        "status": "done",
+        "error": null,
+        "seconds": 36,
+        "cost": 0,
+        "started_at": "2026-07-23T05:32:19.764Z",
+        "bytes": 12191,
+        "finished_at": "2026-07-23T05:32:56.077Z"
+      },
+      {
+        "model": "qwen25-7b",
+        "status": "error",
+        "error": "server restarted mid-run",
+        "seconds": 0,
+        "cost": null,
+        "started_at": "2026-07-23T05:36:27.495Z"
+      },
+      {
+        "model": "claude",
+        "status": "error",
+        "error": "anthropic error: {\"type\":\"invalid_request_error\",\"message\":\"Your credit balance is too low to access the Anthropic API. Please go to Plans & Billing to upgrade or purchase credits.\"}",
+        "seconds": 0,
+        "cost": null,
+        "started_at": "2026-07-23T05:29:23.105Z",
+        "finished_at": "2026-07-23T05:29:23.403Z"
+      },
+      {
+        "model": "kimi",
+        "status": "done",
+        "error": null,
+        "seconds": 185,
+        "cost": 0.0162,
+        "started_at": "2026-07-23T05:29:23.128Z",
+        "bytes": 4559,
+        "finished_at": "2026-07-23T05:32:28.001Z"
+      },
+      {
+        "model": "gpt",
+        "status": "error",
+        "error": "openai error: {\"message\":\"The model `gpt-5.3` does not exist or you do not have access to it.\",\"type\":\"invalid_request_error\",\"param\":null,\"code\":\"model_not_found\"}",
+        "seconds": 1,
+        "cost": null,
+        "started_at": "2026-07-23T05:29:23.130Z",
+        "finished_at": "2026-07-23T05:29:24.073Z"
+      },
+      {
+        "model": "grok",
+        "status": "done",
+        "error": null,
+        "seconds": 36,
+        "cost": 0.0443,
+        "started_at": "2026-07-23T05:29:23.131Z",
+        "bytes": 7691,
+        "finished_at": "2026-07-23T05:29:58.821Z"
+      }
+    ]
+  },
+  {
+    "id": "74df3b61e7ca",
+    "title": "Synthwave Visualizer",
+    "prompt": "Build a synthwave music visualizer using WebAudio oscillators (no external audio files): a retro sun over an animated perspective grid that pulses to generated music, with play/stop controls.",
+    "created_at": "2026-07-23T05:36:38.158Z",
+    "winner": null,
+    "runs": [
+      {
+        "model": "qwen3-14b",
+        "status": "error",
+        "error": "server restarted mid-run",
+        "seconds": 0,
+        "cost": null,
+        "started_at": "2026-07-23T05:36:44.763Z"
+      },
+      {
+        "model": "gemma3-12b",
+        "status": "error",
+        "error": "server restarted mid-run",
+        "seconds": 0,
+        "cost": null
+      },
+      {
+        "model": "hermes3-8b",
+        "status": "error",
+        "error": "server restarted mid-run",
+        "seconds": 0,
+        "cost": null
+      },
+      {
+        "model": "qwen25-7b",
+        "status": "error",
+        "error": "server restarted mid-run",
+        "seconds": 0,
+        "cost": null
+      },
+      {
+        "model": "claude",
+        "status": "error",
+        "error": "anthropic error: {\"type\":\"invalid_request_error\",\"message\":\"Your credit balance is too low to access the Anthropic API. Please go to Plans & Billing to upgrade or purchase credits.\"}",
+        "seconds": 0,
+        "cost": null,
+        "started_at": "2026-07-23T05:36:38.160Z",
+        "finished_at": "2026-07-23T05:36:38.403Z"
+      },
+      {
+        "model": "kimi",
+        "status": "error",
+        "error": "server restarted mid-run",
+        "seconds": 0,
+        "cost": null,
+        "started_at": "2026-07-23T05:36:38.162Z"
+      },
+      {
+        "model": "gpt",
+        "status": "error",
+        "error": "openai error: {\"message\":\"The model `gpt-5.3` does not exist or you do not have access to it.\",\"type\":\"invalid_request_error\",\"param\":null,\"code\":\"model_not_found\"}",
+        "seconds": 0,
+        "cost": null,
+        "started_at": "2026-07-23T05:36:38.162Z",
+        "finished_at": "2026-07-23T05:36:38.452Z"
+      },
+      {
+        "model": "grok",
+        "status": "done",
+        "error": null,
+        "seconds": 58,
+        "cost": 0.0997,
+        "started_at": "2026-07-23T05:36:38.163Z",
+        "bytes": 17543,
+        "finished_at": "2026-07-23T05:37:35.718Z"
+      }
+    ]
   }
 ]
\ No newline at end of file
diff --git a/data/costlog.jsonl b/data/costlog.jsonl
new file mode 100644
index 0000000..819ee84
--- /dev/null
+++ b/data/costlog.jsonl
@@ -0,0 +1,4 @@
+{"ts":"2026-07-23T05:29:58.819Z","provider":"xai","model":"grok-4.5","task":"model-arena","input_tokens":292,"output_tokens":2893,"cost_usd":0.044271}
+{"ts":"2026-07-23T05:32:27.999Z","provider":"moonshot","model":"kimi-k2.5","task":"model-arena","input_tokens":90,"output_tokens":6449,"cost_usd":0.016176}
+{"ts":"2026-07-23T05:36:35.853Z","provider":"moonshot","model":"kimi-k2.5","task":"model-arena","input_tokens":127,"output_tokens":2649,"cost_usd":0.006699}
+{"ts":"2026-07-23T05:37:35.716Z","provider":"xai","model":"grok-4.5","task":"model-arena","input_tokens":322,"output_tokens":6581,"cost_usd":0.099681}
diff --git a/public/index.html b/public/index.html
index c656550..b49b794 100644
--- a/public/index.html
+++ b/public/index.html
@@ -68,7 +68,12 @@ input[type=range]{accent-color:var(--neon);width:130px}
 #detail .meta{color:var(--dim);font-size:12px;margin-bottom:14px;white-space:pre-wrap}
 .arena{display:grid;grid-template-columns:repeat(auto-fit,minmax(420px,1fr));gap:16px}
 .pane{background:var(--panel);border:1px solid var(--line)}
-.pane.winner{border-color:var(--gold);box-shadow:0 0 18px rgba(255,199,46,.25)}
+/* the crowned pane breaks out of the equal grid and takes the top, full width —
+   the fight has a champion, not a slightly-yellower column */
+.pane.winner{grid-column:1/-1;order:-1;border-color:var(--gold);box-shadow:0 0 26px rgba(255,199,46,.35);animation:crowned .9s ease-out 1}
+@keyframes crowned{0%{box-shadow:0 0 60px rgba(255,199,46,.9)}100%{box-shadow:0 0 26px rgba(255,199,46,.35)}}
+.pane.winner iframe{height:560px}
+.pane.winner .bar{background:linear-gradient(90deg,rgba(255,199,46,.14),transparent)}
 .pane .bar{display:flex;align-items:center;gap:10px;padding:8px 12px;border-bottom:1px solid var(--line);font-size:12px}
 .pane .bar .name{color:#fff;font-weight:700}
 .pane .bar .stat{color:var(--dim);font-size:11px}
@@ -224,7 +229,9 @@ async function loadModels(){
 function picked(){ return [...document.querySelectorAll('#model-picks input:checked')].map(i=>i.value); }
 function updateEst(){
   const est = picked().reduce((s,id)=>{ const m=models.find(x=>x.id===id); return s+(m?m.estCost:0); },0);
-  $('#est').textContent = 'Estimated battle cost: ' + (est ? '~$'+est.toFixed(2)+' (metered models selected)' : '$0 (all local)');
+  $('#est').textContent = est
+    ? 'Rough ceiling: ~$' + est.toFixed(2) + ' (flat per-model guess; actual is metered by real tokens and logged after each run)'
+    : 'Cost: $0 (all local)';
 }
 $('#f-go').onclick = async ()=>{
   const title=$('#f-title').value.trim(), prompt=$('#f-prompt').value.trim(), ids=picked();
@@ -277,7 +284,7 @@ async function renderDetail(id, statusOnly){
         <span class="name">${mLabel(run.model)}</span>
         <span class="stat">${run.status==='done'?stats.join(' · '):run.status}</span>
         <span class="right">
-          ${run.status==='done'&&c.winner!==run.model?`<button class="btn" data-win="${run.model}">👑 Crown</button>`:''}
+          ${run.status==='done'&&c.winner!==run.model?`<button class="btn" data-win="${run.model}" title="Crown this model the winner of this battle — records a vote into the real-world win-rate ledger. One winner per battle; you can re-crown.">👑 Crown</button>`:''}
           ${c.winner===run.model?'<span class="chip winner">WINNER</span>':''}
           ${(run.status==='error'||run.status==='done')?`<button class="btn pink" data-retry="${run.model}">↻</button>`:''}
         </span>
diff --git a/server.js b/server.js
index 8ceb105..1b6e3e0 100644
--- a/server.js
+++ b/server.js
@@ -35,7 +35,7 @@ const MODELS = [
   { id: 'qwen25-7b',  label: 'Qwen2.5 7B (Mac1)', kind: 'local', host: 'http://192.168.1.133:11434', model: 'qwen2.5:7b', estCost: 0 },
   { id: 'claude',     label: 'Claude Fable 5', kind: 'metered', provider: 'anthropic', model: 'claude-fable-5',   envKey: 'ANTHROPIC_API_KEY', estCost: 0.15 },
   { id: 'kimi',       label: 'Kimi K2.5',      kind: 'metered', provider: 'moonshot',  model: 'kimi-k2.5',        envKey: 'MOONSHOT_API_KEY',  estCost: 0.03 },
-  { id: 'gpt',        label: 'GPT-5.3',        kind: 'metered', provider: 'openai',    model: 'gpt-5.3',          envKey: 'OPENAI_API_KEY',    estCost: 0.10 },
+  { id: 'gpt',        label: 'GPT-5.1',        kind: 'metered', provider: 'openai',    model: process.env.OPENAI_MODEL || 'gpt-5.1', envKey: 'OPENAI_API_KEY', estCost: 0.10 },
   { id: 'grok',       label: 'Grok 4.5',       kind: 'metered', provider: 'xai',       model: process.env.GROK_MODEL || 'grok-4.5', envKey: 'XAI_API_KEY', estCost: 0.10 },
 ];
 
@@ -47,8 +47,15 @@ function saveChallenges(list) {
   fs.renameSync(tmp, CH_FILE);
 }
 let challenges = loadChallenges();
-// anything left "running" from a previous process died with it
-for (const c of challenges) for (const r of c.runs) if (r.status === 'running' || r.status === 'queued') { r.status = 'error'; r.error = 'server restarted mid-run'; }
+// anything left "running"/"queued" from a previous process died with it; also
+// heal corrupt records (finished before started) that a mid-write restart left behind
+for (const c of challenges) for (const r of c.runs) {
+  const corrupt = r.started_at && r.finished_at && r.finished_at < r.started_at;
+  if (r.status === 'running' || r.status === 'queued' || (r.status !== 'done' && corrupt)) {
+    r.status = 'error'; r.error = 'server restarted mid-run';
+    if (r.seconds == null) r.seconds = 0;
+  }
+}
 saveChallenges(challenges);
 
 function appendJsonl(file, obj) { try { fs.appendFileSync(file, JSON.stringify(obj) + '\n'); } catch {} }
@@ -92,12 +99,13 @@ function httpJson(url, opts, body, timeoutMs) {
   });
 }
 
-// serialize local generations per host so ollama doesn't thrash model loads
-const hostQueues = {};
-function onHost(host, fn) {
-  const prev = hostQueues[host] || Promise.resolve();
+// serialize local generations per host (ollama model-load thrash) AND metered
+// generations per model (so two rapid retries queue instead of double-spending)
+const queues = {};
+function onKey(key, fn) {
+  const prev = queues[key] || Promise.resolve();
   const next = prev.then(fn, fn);
-  hostQueues[host] = next.catch(() => {});
+  queues[key] = next.catch(() => {});
   return next;
 }
 
@@ -152,7 +160,10 @@ function runModel(challenge, modelId) {
   const m = MODELS.find(x => x.id === modelId);
   const run = challenge.runs.find(r => r.model === modelId);
   if (!m || !run) return;
-  run.status = 'queued'; run.error = null; saveChallenges(challenges);
+  // double-fire guard: never start a model that's already in flight (blocks the
+  // metered double-spend race from two rapid retries on the same run)
+  if (run.status === 'running' || run.status === 'queued') return;
+  run.status = 'queued'; run.error = null; run.started_at = null; run.finished_at = null; saveChallenges(challenges);
   const prompt = ARENA_PROMPT(challenge.prompt);
   const exec = async () => {
     run.status = 'running'; run.started_at = new Date().toISOString(); saveChallenges(challenges);
@@ -173,9 +184,30 @@ function runModel(challenge, modelId) {
     run.finished_at = new Date().toISOString();
     saveChallenges(challenges);
   };
-  if (m.kind === 'local') onHost(m.host, exec); else exec();
+  if (m.kind === 'local') onKey(m.host, exec); else onKey('metered:' + m.id, exec);
 }
 
+// stuck-run watchdog — a wedged Ollama host (e.g. Mac1) can leave a run
+// 'running'/'queued' far past its budget; the UI would poll it forever. Every
+// 30s, fail any run stuck well past its expected ceiling so it surfaces as an
+// honest error instead of an infinite spinner.
+const STUCK_RUNNING_MS = 660000; // local timeout is 600s; grace 60s
+const STUCK_QUEUED_MS = 1200000; // 20 min waiting to even start = host is dead
+setInterval(() => {
+  const now = Date.now();
+  let dirty = false;
+  for (const c of challenges) for (const r of c.runs) {
+    if (r.status === 'running' && r.started_at && now - Date.parse(r.started_at) > STUCK_RUNNING_MS) {
+      r.status = 'error'; r.error = 'timed out (watchdog) — model host unresponsive'; r.finished_at = new Date().toISOString(); dirty = true;
+    } else if (r.status === 'queued' && r.started_at == null) {
+      // stamp a queue-entry time so we can age it out; reuse challenge age as the floor
+      const floor = Date.parse(c.created_at || new Date().toISOString());
+      if (now - floor > STUCK_QUEUED_MS) { r.status = 'error'; r.error = 'never started — model host busy/down'; r.seconds = 0; r.finished_at = new Date().toISOString(); dirty = true; }
+    }
+  }
+  if (dirty) saveChallenges(challenges);
+}, 30000).unref();
+
 // ---------- ledger ----------
 function buildLedger() {
   const stats = {};
@@ -269,7 +301,11 @@ const server = http.createServer(async (req, res) => {
   if ((m = p.match(/^\/api\/challenges\/([a-f0-9]+)\/retry\/([\w-]+)$/)) && req.method === 'POST') {
     const c = challenges.find(x => x.id === m[1]);
     if (!c) return send(res, 404, { error: 'not found' });
-    if (!c.runs.some(r => r.model === m[2])) c.runs.push({ model: m[2], status: 'queued', error: null, seconds: null, cost: null });
+    // retry ONLY re-runs a model already in this battle. Adding an arbitrary
+    // (metered) model via retry was a spend-injection vector — anyone with the
+    // shared cred could loop paid models onto every challenge. New competitors
+    // are added at battle-creation time, not here.
+    if (!c.runs.some(r => r.model === m[2])) return send(res, 400, { error: 'model not in this battle — retry only re-runs an existing competitor' });
     runModel(c, m[2]);
     return send(res, 200, c);
   }

← c838657 Kimi K2.5 is now a pre-checked default arena participant (+  ·  back to Model Arena  ·  auto-save: 2026-07-22T22:47:10 (5 files) — data/challenges.j 45826ed →