[object Object]

← back to CelebritySignatures

The Signature Games: 5-game arcade at /game — Whose Hand? (sig->name), Who Signed This? (real AIC museum artwork->artist, Steve's ask), Match the Hand (sig->pick their artwork of 4), Early or Late? (two dated sigs from one person's evolution timeline), Date the Hand (sig->century); shared engine, streak scoring, per-game best scores, artwork gallery prefetch with progress (TK-10181)

133e8ee668d96ebf88ee8ee10ddd3141da539ff7 · 2026-08-03 12:57:11 -0700 · Steve Abrams

Files touched

Diff

commit 133e8ee668d96ebf88ee8ee10ddd3141da539ff7
Author: Steve Abrams <steve@designerwallcoverings.com>
Date:   Mon Aug 3 12:57:11 2026 -0700

    The Signature Games: 5-game arcade at /game — Whose Hand? (sig->name), Who Signed This? (real AIC museum artwork->artist, Steve's ask), Match the Hand (sig->pick their artwork of 4), Early or Late? (two dated sigs from one person's evolution timeline), Date the Hand (sig->century); shared engine, streak scoring, per-game best scores, artwork gallery prefetch with progress (TK-10181)
---
 public/game.html | 314 +++++++++++++++++++++++++++++++++++++++++++------------
 1 file changed, 249 insertions(+), 65 deletions(-)

diff --git a/public/game.html b/public/game.html
index c8fd84e..69f62ec 100644
--- a/public/game.html
+++ b/public/game.html
@@ -3,7 +3,7 @@
 <head>
 <meta charset="utf-8">
 <meta name="viewport" content="width=device-width, initial-scale=1">
-<title>Whose Hand? — the signature identification game</title>
+<title>The Signature Games — Celebrity Signatures</title>
 <link rel="preconnect" href="https://fonts.googleapis.com">
 <link href="https://fonts.googleapis.com/css2?family=Playfair+Display:ital,wght@0,400;0,600;0,700;1,400&display=swap" rel="stylesheet">
 <style>
@@ -15,23 +15,37 @@
   .gnav { margin-left:auto; display:flex; gap:20px; }
   .gnav a { font-size:13.5px; color:var(--muted); text-decoration:none; padding:4px 2px; border-bottom:2px solid transparent; }
   .gnav a.on, .gnav a:hover { color:var(--ink); border-bottom-color:var(--ink); }
-  main { max-width:680px; margin:0 auto; padding:30px 20px 80px; }
+  main { max-width:720px; margin:0 auto; padding:30px 20px 80px; }
   h1 { font:700 30px/1.2 'Playfair Display', Georgia, serif; margin:0 0 6px; text-align:center; }
   .tagline { text-align:center; color:var(--muted); margin:0 0 24px; }
   .panel { background:#fff; border:1px solid var(--line); border-radius:16px; padding:26px 28px; box-shadow:0 2px 10px rgba(0,0,0,.05); }
-  /* setup */
-  .setup label { display:block; font-size:12px; text-transform:uppercase; letter-spacing:.06em; color:var(--muted); margin:16px 0 7px; }
+  /* game picker */
+  .games { display:grid; grid-template-columns:1fr 1fr; gap:12px; }
+  .gcard { border:1px solid var(--line); border-radius:12px; padding:16px 18px; cursor:pointer; background:#fff; text-align:left; font:inherit; transition:transform .1s, border-color .1s; }
+  .gcard:hover { transform:translateY(-2px); border-color:var(--ink); }
+  .gcard.on { background:var(--ink); color:#fff; border-color:var(--ink); }
+  .gcard.on .gd { color:#d8d4cc; }
+  .gcard b { font:600 16px 'Playfair Display', Georgia, serif; display:block; margin-bottom:3px; }
+  .gcard .gd { font-size:12.5px; color:var(--muted); line-height:1.4; }
+  .setup label { display:block; font-size:12px; text-transform:uppercase; letter-spacing:.06em; color:var(--muted); margin:18px 0 7px; }
   .opts { display:flex; gap:8px; flex-wrap:wrap; }
   .opt { padding:8px 16px; border:1px solid var(--line); border-radius:999px; background:#fff; cursor:pointer; font:inherit; font-size:13.5px; color:var(--muted); }
   .opt.on { background:var(--ink); color:#fff; border-color:var(--ink); }
   .go { display:block; width:100%; margin-top:24px; padding:13px; border-radius:999px; border:none; background:var(--ink); color:#fff; font:600 16px 'Playfair Display',Georgia,serif; cursor:pointer; }
   .go:hover { background:#000; }
+  .go:disabled { opacity:.6; cursor:default; }
   /* play */
   .hud { display:flex; justify-content:space-between; align-items:baseline; font-size:13px; color:var(--muted); margin-bottom:14px; }
   .hud b { color:var(--ink); font-size:16px; }
   .streak { color:var(--green); font-weight:600; }
-  .sigbox { background:#fff; border:1px solid var(--line); border-radius:12px; height:170px; display:flex; align-items:center; justify-content:center; padding:20px; margin-bottom:18px; }
-  .sigbox img { max-width:100%; max-height:100%; object-fit:contain; }
+  .prompt { text-align:center; font:italic 400 17px 'Playfair Display', Georgia, serif; color:var(--muted); margin-bottom:12px; }
+  .stage { background:#fff; border:1px solid var(--line); border-radius:12px; min-height:150px; display:flex; align-items:center; justify-content:center; gap:20px; padding:18px; margin-bottom:18px; }
+  .stage img { max-height:240px; max-width:100%; object-fit:contain; }
+  .stage img.sig { max-height:120px; }
+  .stage .duo { display:flex; gap:16px; align-items:center; justify-content:center; flex-wrap:wrap; }
+  .duo .d { text-align:center; }
+  .duo .d img { max-height:110px; max-width:260px; background:#fff; border:1px solid var(--line); border-radius:8px; padding:8px; }
+  .duo .d .lab { font:600 15px 'Playfair Display',Georgia,serif; margin-top:6px; }
   .choices { display:grid; grid-template-columns:1fr 1fr; gap:10px; }
   .choice { padding:13px 16px; border:1px solid var(--line); border-radius:10px; background:#fff; cursor:pointer; font:inherit; font-size:14.5px; text-align:center; transition:transform .08s; }
   .choice:hover:not(:disabled) { transform:translateY(-1px); border-color:var(--ink); }
@@ -39,9 +53,13 @@
   .choice.right { background:var(--green); color:#fff; border-color:var(--green); }
   .choice.wrong { background:var(--red); color:#fff; border-color:var(--red); }
   .choice.dim { opacity:.45; }
+  .choice.art { padding:8px; }
+  .choice.art img { width:100%; height:120px; object-fit:cover; border-radius:6px; display:block; pointer-events:none; }
+  .choice.art .cl { font-size:11.5px; margin-top:5px; color:inherit; min-height:14px; }
   .reveal { margin-top:14px; text-align:center; font-size:14px; min-height:44px; }
   .reveal a { color:var(--green); }
   .next { display:block; margin:14px auto 0; padding:10px 30px; border-radius:999px; border:1px solid var(--ink); background:var(--ink); color:#fff; font:inherit; cursor:pointer; }
+  .prep { text-align:center; color:var(--muted); padding:30px 0; font-size:14px; }
   /* results */
   .score-big { font:700 64px/1 'Playfair Display', Georgia, serif; text-align:center; margin:10px 0 2px; }
   .verdict { text-align:center; font:italic 400 22px 'Playfair Display', Georgia, serif; color:var(--muted); margin-bottom:16px; }
@@ -49,7 +67,7 @@
   .misses a { color:var(--green); }
   .best { text-align:center; font-size:13px; color:var(--muted); margin-top:14px; }
   .again { display:block; margin:20px auto 0; padding:12px 34px; border-radius:999px; border:none; background:var(--ink); color:#fff; font:600 15px 'Playfair Display',Georgia,serif; cursor:pointer; }
-  @media (max-width:520px){ .choices{grid-template-columns:1fr} }
+  @media (max-width:560px){ .choices{grid-template-columns:1fr} .games{grid-template-columns:1fr} }
 </style>
 </head>
 <body>
@@ -63,18 +81,26 @@
   </nav>
 </div>
 <main>
-  <h1>Whose Hand?</h1>
-  <p class="tagline">Ten signatures. Four names each. How well do you know history's handwriting?</p>
+  <h1>The Signature Games</h1>
+  <p class="tagline">Five ways to test your eye for history's handwriting.</p>
 
   <div class="panel setup" id="setup">
+    <label>Choose your game</label>
+    <div class="games" id="gamePick">
+      <button class="gcard on" data-g="whose"><b>Whose Hand?</b><span class="gd">A mystery signature, four names. The classic.</span></button>
+      <button class="gcard" data-g="art"><b>Who Signed This?</b><span class="gd">A real museum artwork — which artist's hand made it?</span></button>
+      <button class="gcard" data-g="match"><b>Match the Hand</b><span class="gd">One signature, four artworks. Pick the piece they signed.</span></button>
+      <button class="gcard" data-g="early"><b>Early or Late?</b><span class="gd">Two signatures, same person, years apart. Which came first?</span></button>
+      <button class="gcard" data-g="century"><b>Date the Hand</b><span class="gd">Read the ink — which century did this hand belong to?</span></button>
+    </div>
     <label>Difficulty</label>
     <div class="opts" id="diffOpts">
-      <button class="opt on" data-d="icons">Icons <small>(the 100 most famous)</small></button>
+      <button class="opt on" data-d="icons">Icons <small>(top 100)</small></button>
       <button class="opt" data-d="scholar">Scholar <small>(top 500)</small></button>
       <button class="opt" data-d="curator">Curator <small>(top 2,000)</small></button>
       <button class="opt" data-d="deep">Deep Cuts <small>(everyone)</small></button>
     </div>
-    <label>Category</label>
+    <label id="catLabel">Category</label>
     <div class="opts" id="catOpts">
       <button class="opt on" data-c="All">Everything</button>
       <button class="opt" data-c="Artists">Artists</button>
@@ -83,6 +109,7 @@
       <button class="opt" data-c="Declaration of Independence">Declaration</button>
     </div>
     <button class="go" id="goBtn">Begin</button>
+    <div class="prep" id="prep" hidden>Hanging the gallery…</div>
   </div>
 
   <div class="panel" id="play" hidden>
@@ -91,7 +118,8 @@
       <span class="streak" id="streak"></span>
       <span>Score <b id="score">0</b></span>
     </div>
-    <div class="sigbox"><img id="sigImg" alt="Mystery signature"></div>
+    <div class="prompt" id="prompt"></div>
+    <div class="stage" id="stage"></div>
     <div class="choices" id="choices"></div>
     <div class="reveal" id="reveal"></div>
   </div>
@@ -106,40 +134,153 @@
 </main>
 <script>
 const $ = s => document.querySelector(s);
+const esc = s => String(s==null?'':s).replace(/[&<>"]/g,c=>({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;'}[c]));
 const qidOf = r => (r.wikidata||'').split('/').pop();
 const notability = r => { const m=(r.reason_for_ranking||'').match(/(\d+) language/); return m?+m[1]:0; };
-let ALL=[], POOL=[], game=null;
+const shuffled = a => { a=[...a]; for(let i=a.length-1;i>0;i--){ const j=Math.floor(Math.random()*(i+1)); [a[i],a[j]]=[a[j],a[i]]; } return a; };
 const DIFF_N = { icons:100, scholar:500, curator:2000, deep:Infinity };
-let diff='icons', cat='All';
+let ALL=[], EVO={}, gameKey='whose', diff='icons', cat='All', game=null;
+const ART_CACHE = {};
 
-fetch('/api/signatures').then(r=>r.json()).then(d=>{
-  ALL = d.filter(r => r.signature_image_url && /wikimedia|upload\./.test(r.signature_image_url));
-});
+fetch('/api/signatures').then(r=>r.json()).then(d=>{ ALL = d.filter(r => r.signature_image_url && /wikimedia|upload\./.test(r.signature_image_url)); });
+fetch('/api/signature-evolution').then(r=>r.ok?r.json():{}).then(e=>{ EVO=e||{}; }).catch(()=>{});
 
+document.querySelectorAll('#gamePick .gcard').forEach(b=>b.onclick=()=>{ gameKey=b.dataset.g;
+  document.querySelectorAll('#gamePick .gcard').forEach(x=>x.classList.toggle('on',x===b));
+  // art games draw from Artists; evolution game draws from the dated timelines
+  const catRelevant = gameKey==='whose' || gameKey==='century';
+  $('#catLabel').style.display = catRelevant?'':'none';
+  $('#catOpts').style.display = catRelevant?'':'none';
+});
 document.querySelectorAll('#diffOpts .opt').forEach(b=>b.onclick=()=>{ diff=b.dataset.d;
   document.querySelectorAll('#diffOpts .opt').forEach(x=>x.classList.toggle('on',x===b)); });
 document.querySelectorAll('#catOpts .opt').forEach(b=>b.onclick=()=>{ cat=b.dataset.c;
   document.querySelectorAll('#catOpts .opt').forEach(x=>x.classList.toggle('on',x===b)); });
 
-function shuffled(a){ a=[...a]; for(let i=a.length-1;i>0;i--){ const j=Math.floor(Math.random()*(i+1)); [a[i],a[j]]=[a[j],a[i]]; } return a; }
+function pool(forcedCat){
+  const c = forcedCat!==undefined ? forcedCat : ((gameKey==='whose'||gameKey==='century') ? cat : 'Artists');
+  let p = c==='All' ? ALL : ALL.filter(r=>r.category===c);
+  return [...p].sort((a,b)=>notability(b)-notability(a)).slice(0, DIFF_N[diff]);
+}
+
+// one public-domain artwork per artist via the Art Institute of Chicago (CORS-open, keyless)
+async function artworkFor(r){
+  const qid = qidOf(r);
+  if (qid in ART_CACHE) return ART_CACHE[qid];
+  let out = null;
+  try {
+    const last = r.full_name.toLowerCase().split(' ').pop();
+    const j = await (await fetch(`https://api.artic.edu/api/v1/artworks/search?q=${encodeURIComponent(r.full_name)}&query[term][is_public_domain]=true&fields=id,title,image_id,artist_title,date_display&limit=8`)).json();
+    const hits = (j.data||[]).filter(a=>a.image_id && (a.artist_title||'').toLowerCase().includes(last));
+    if (hits.length){
+      const a = hits[Math.floor(Math.random()*Math.min(hits.length,4))];
+      out = { img:`https://www.artic.edu/iiif/2/${a.image_id}/full/600,/0/default.jpg`, title:a.title||'Untitled', date:a.date_display||'', url:`https://www.artic.edu/artworks/${a.id}` };
+    }
+  } catch {}
+  ART_CACHE[qid] = out;
+  return out;
+}
+
+// ── round builders per game ─────────────────────────────────────────────────
+function distractors(answer, p, n=3){
+  const near = p.filter(r=>r!==answer && r.category===answer.category);
+  const ranked = [...near].sort((a,b)=>Math.abs(notability(a)-notability(answer)) - Math.abs(notability(b)-notability(answer)));
+  const ds = shuffled(ranked.slice(0,24)).slice(0,n);
+  while (ds.length<n){ const x = shuffled(p).find(r=>r!==answer && !ds.includes(r)); if(!x) break; ds.push(x); }
+  return ds;
+}
+const centuryOf = r => { const y = parseInt(String(r.death_date).slice(0,4),10); return isNaN(y)?null:Math.ceil(y/100); };
+const centuryLabel = c => { const suf = c%10===1&&c!==11?'st':c%10===2&&c!==12?'nd':c%10===3&&c!==13?'rd':'th'; return `${c}${suf} century`; };
+
+async function buildRounds(setPrep){
+  const p = pool();
+  const rounds = [];
+  if (gameKey==='whose'){
+    if (p.length<8) throw new Error('Not enough signatures in this tier.');
+    for (const answer of shuffled(p).slice(0,10)) rounds.push({ answer, options: shuffled([answer, ...distractors(answer,p)]) });
+  }
+  else if (gameKey==='century'){
+    const dated = p.filter(r=>centuryOf(r));
+    if (dated.length<10) throw new Error('Not enough dated signatures in this tier.');
+    for (const answer of shuffled(dated).slice(0,10)){
+      const c = centuryOf(answer);
+      const opts = new Set([c]);
+      let step=1;
+      while (opts.size<4){ const cand = c + (Math.random()<.5?-step:step); if (cand>=6 && cand<=21) opts.add(cand); step = Math.min(step+1, 6); }
+      rounds.push({ answer, century:c, options: shuffled([...opts]) });
+    }
+  }
+  else if (gameKey==='early'){
+    const people = Object.entries(EVO).map(([qid,e])=>({qid, e, dated:(e.sigs||[]).filter(s=>s.year)}))
+      .filter(x => { const ys=[...new Set(x.dated.map(s=>s.year))]; return ys.length>=2; });
+    if (people.length<5) throw new Error('Evolution data still loading — try again in a moment.');
+    for (let i=0;i<10;i++){
+      const P = people[Math.floor(Math.random()*people.length)];
+      const years = shuffled([...new Set(P.dated.map(s=>s.year))]);
+      const [y1,y2] = [years[0], years[1]].sort((a,b)=>a-b);
+      const sigA = P.dated.find(s=>s.year===y1), sigB = P.dated.find(s=>s.year===y2);
+      const flip = Math.random()<.5;
+      rounds.push({ person:P.e.name, qid:P.qid, left:flip?sigB:sigA, right:flip?sigA:sigB, earlier: flip?'right':'left', years:[y1,y2] });
+    }
+  }
+  else { // art + match need artworks
+    const cands = shuffled(p);
+    let idx=0;
+    while (rounds.length<10 && idx<cands.length){
+      const batch = cands.slice(idx, idx+6); idx+=6;
+      setPrep(`Hanging the gallery… ${rounds.length}/10 rooms ready`);
+      const arts = await Promise.all(batch.map(artworkFor));
+      for (let i=0;i<batch.length && rounds.length<10;i++){
+        if (!arts[i]) continue;
+        const answer = batch[i];
+        if (gameKey==='art'){
+          rounds.push({ answer, artwork:arts[i], options: shuffled([answer, ...distractors(answer,p)]) });
+        } else {
+          rounds.push({ answer, artwork:arts[i], pendingDistractors:true });
+        }
+      }
+    }
+    if (gameKey==='match'){
+      // each round needs 3 distractor ARTWORKS too
+      for (const r of rounds){
+        const ds = distractors(r.answer, p, 8);
+        const arts = [];
+        for (const d of ds){
+          if (arts.length>=3) break;
+          const a = await artworkFor(d);
+          if (a) arts.push({ owner:d, art:a });
+        }
+        if (arts.length<3){ r.invalid = true; continue; }
+        r.options = shuffled([{owner:r.answer, art:r.artwork}, ...arts.slice(0,3)]);
+      }
+      const ok = rounds.filter(r=>!r.invalid);
+      if (ok.length<6) throw new Error('Not enough museum artworks at this tier — try Icons or Scholar.');
+      rounds.length = 0; rounds.push(...ok.slice(0,10));
+    }
+    if (rounds.length<6) throw new Error('Not enough museum artworks at this tier — try Icons or Scholar.');
+  }
+  return rounds;
+}
+
+// ── engine ──────────────────────────────────────────────────────────────────
+$('#goBtn').onclick = async () => {
+  if (!ALL.length) return;
+  $('#goBtn').disabled = true; $('#prep').hidden = false;
+  try {
+    const rounds = await buildRounds(t => $('#prep').textContent = t);
+    game = { rounds, i:0, score:0, streak:0, misses:[] };
+    $('#setup').hidden = true; $('#results').hidden = true; $('#play').hidden = false;
+    showRound();
+  } catch(e){ alert(e.message); }
+  $('#goBtn').disabled = false; $('#prep').hidden = true; $('#prep').textContent = 'Hanging the gallery…';
+};
 
-$('#goBtn').onclick = () => {
-  let pool = cat==='All' ? ALL : ALL.filter(r=>r.category===cat);
-  pool = [...pool].sort((a,b)=>notability(b)-notability(a)).slice(0, DIFF_N[diff]);
-  if (pool.length < 8) { alert('Not enough signatures in this tier — try a broader setting.'); return; }
-  POOL = pool;
-  game = { rounds: [], i: 0, score: 0, streak: 0, misses: [] };
-  const picks = shuffled(pool).slice(0,10);
-  game.rounds = picks.map(answer => {
-    // distractors: same category, adjacent fame (fair but hard)
-    const near = pool.filter(r => r!==answer && r.category===answer.category);
-    const ranked = [...near].sort((a,b)=>Math.abs(notability(a)-notability(answer)) - Math.abs(notability(b)-notability(answer)));
-    const ds = shuffled(ranked.slice(0, 24)).slice(0,3);
-    while (ds.length<3) { const x = shuffled(pool).find(r=>r!==answer && !ds.includes(r)); if(!x) break; ds.push(x); }
-    return { answer, options: shuffled([answer, ...ds]) };
-  });
-  $('#setup').hidden = true; $('#results').hidden = true; $('#play').hidden = false;
-  showRound();
+const PROMPTS = {
+  whose: 'Whose hand wrote this?',
+  art: 'Who signed this piece of art?',
+  match: 'Which of these did they sign?',
+  early: 'Same hand, years apart — which is the EARLIER signature?',
+  century: 'Which century did this hand belong to?',
 };
 
 function showRound(){
@@ -147,50 +288,93 @@ function showRound(){
   $('#round').textContent = game.i+1;
   $('#score').textContent = game.score;
   $('#streak').textContent = game.streak>=2 ? `🔥 ${game.streak} streak` : '';
-  $('#sigImg').src = r.answer.signature_image_url;
+  $('#prompt').textContent = PROMPTS[gameKey];
   $('#reveal').innerHTML = '';
-  $('#choices').innerHTML = r.options.map((o,i)=>`<button class="choice" data-i="${i}">${o.full_name}</button>`).join('');
-  document.querySelectorAll('.choice').forEach(b=>b.onclick=()=>pick(+b.dataset.i));
+  const stage = $('#stage'), ch = $('#choices');
+  if (gameKey==='whose' || gameKey==='century'){
+    stage.innerHTML = `<img class="sig" src="${esc(r.answer.signature_image_url)}" alt="Mystery signature">`;
+    ch.innerHTML = (gameKey==='whose' ? r.options.map((o,i)=>`<button class="choice" data-i="${i}">${esc(o.full_name)}</button>`)
+                                      : r.options.map((c,i)=>`<button class="choice" data-i="${i}">${centuryLabel(c)}</button>`)).join('');
+  }
+  else if (gameKey==='art'){
+    stage.innerHTML = `<img referrerpolicy="no-referrer" src="${esc(r.artwork.img)}" alt="Mystery artwork">`;
+    ch.innerHTML = r.options.map((o,i)=>`<button class="choice" data-i="${i}">${esc(o.full_name)}</button>`).join('');
+  }
+  else if (gameKey==='match'){
+    stage.innerHTML = `<img class="sig" src="${esc(r.answer.signature_image_url)}" alt="Signature of the artist">`;
+    ch.innerHTML = r.options.map((o,i)=>`<button class="choice art" data-i="${i}"><img referrerpolicy="no-referrer" src="${esc(o.art.img)}" alt="artwork option"><span class="cl"></span></button>`).join('');
+  }
+  else if (gameKey==='early'){
+    stage.innerHTML = `<div class="duo">
+      <div class="d"><img referrerpolicy="no-referrer" src="${esc(r.left.url)}" alt="signature A"><div class="lab">A</div></div>
+      <div class="d"><img referrerpolicy="no-referrer" src="${esc(r.right.url)}" alt="signature B"><div class="lab">B</div></div>
+    </div>`;
+    ch.innerHTML = `<button class="choice" data-i="left">A is earlier</button><button class="choice" data-i="right">B is earlier</button>`;
+    $('#prompt').textContent = `${r.person} — which is the EARLIER signature?`;
+  }
+  document.querySelectorAll('.choice').forEach(b=>b.onclick=()=>pick(b.dataset.i));
 }
 
 function pick(i){
   const r = game.rounds[game.i];
-  const chosen = r.options[i];
-  const right = chosen === r.answer;
-  document.querySelectorAll('.choice').forEach((b,j)=>{
-    b.disabled = true;
-    if (r.options[j]===r.answer) b.classList.add('right');
-    else if (j===i) b.classList.add('wrong');
-    else b.classList.add('dim');
-  });
-  if (right) { game.streak++; game.score += 10 + Math.min(game.streak-1,5)*2; }
-  else { game.streak = 0; game.misses.push(r.answer); }
-  const a = r.answer;
-  $('#reveal').innerHTML = `${right?'✓ Correct —':'✗ It was'} <b>${a.full_name}</b>`
-    + ` (${a.category}${a.death_date?', d. '+String(a.death_date).slice(0,4):''})`
-    + ` · <a href="/?artist=${qidOf(a)}" target="_blank" rel="noopener noreferrer">see their page →</a>`
-    + `<button class="next" id="nextBtn">${game.i===9?'See results':'Next signature →'}</button>`;
-  $('#nextBtn').onclick = () => { game.i++; game.i>9 ? finish() : showRound(); };
+  let right=false, revealHtml='';
+  const btns = [...document.querySelectorAll('.choice')];
+  btns.forEach(b=>b.disabled=true);
+  if (gameKey==='whose' || gameKey==='art'){
+    const chosen = r.options[+i];
+    right = chosen === r.answer;
+    btns.forEach((b,j)=>{ if(r.options[j]===r.answer) b.classList.add('right'); else if(j===+i) b.classList.add('wrong'); else b.classList.add('dim'); });
+    const a=r.answer;
+    revealHtml = `${right?'✓ Correct —':'✗ It was'} <b>${esc(a.full_name)}</b>`
+      + (gameKey==='art'?` · <a href="${esc(r.artwork.url)}" target="_blank" rel="noopener noreferrer">“${esc(r.artwork.title)}”${r.artwork.date?', '+esc(r.artwork.date):''} →</a>`:'')
+      + ` · <a href="/?artist=${qidOf(a)}" target="_blank" rel="noopener noreferrer">their page →</a>`;
+    if (!right) game.misses.push(a);
+  }
+  else if (gameKey==='century'){
+    const chosen = r.options[+i];
+    right = chosen === r.century;
+    btns.forEach((b,j)=>{ if(r.options[j]===r.century) b.classList.add('right'); else if(j===+i) b.classList.add('wrong'); else b.classList.add('dim'); });
+    revealHtml = `${right?'✓ Correct —':'✗'} <b>${esc(r.answer.full_name)}</b> died ${esc(String(r.answer.death_date).slice(0,4))} (${centuryLabel(r.century)}) · <a href="/?artist=${qidOf(r.answer)}" target="_blank" rel="noopener noreferrer">their page →</a>`;
+    if (!right) game.misses.push(r.answer);
+  }
+  else if (gameKey==='match'){
+    const chosen = r.options[+i];
+    right = chosen.owner === r.answer;
+    btns.forEach((b,j)=>{ const o=r.options[j];
+      if(o.owner===r.answer) b.classList.add('right'); else if(j===+i) b.classList.add('wrong'); else b.classList.add('dim');
+      b.querySelector('.cl').textContent = `${o.owner.full_name} — ${o.art.title}`;
+    });
+    revealHtml = `${right?'✓ Correct —':'✗ Their piece was'} <a href="${esc(r.options.find(o=>o.owner===r.answer).art.url)}" target="_blank" rel="noopener noreferrer">“${esc(r.artwork.title)}” →</a> by <b>${esc(r.answer.full_name)}</b>`;
+    if (!right) game.misses.push(r.answer);
+  }
+  else if (gameKey==='early'){
+    right = i === r.earlier;
+    btns.forEach(b=>{ if(b.dataset.i===r.earlier) b.classList.add('right'); else b.classList.add('wrong'); });
+    revealHtml = `${right?'✓ Correct —':'✗'} ${esc(r.person)}: ${r.earlier==='left'?'A':'B'} is from <b>${r.years[0]}</b>, the other from <b>${r.years[1]}</b> · <a href="/?artist=${r.qid}" target="_blank" rel="noopener noreferrer">see the timeline →</a>`;
+    if (!right) game.misses.push({ full_name:r.person, wikidata:'https://www.wikidata.org/wiki/'+r.qid });
+  }
+  if (right){ game.streak++; game.score += 10 + Math.min(game.streak-1,5)*2; } else game.streak=0;
+  $('#reveal').innerHTML = revealHtml + `<button class="next" id="nextBtn">${game.i===game.rounds.length-1?'See results':'Next →'}</button>`;
+  $('#nextBtn').onclick = () => { game.i++; game.i>=game.rounds.length ? finish() : showRound(); };
 }
 
 function finish(){
   $('#play').hidden = true; $('#results').hidden = false;
-  const s = game.score;
+  const n = game.rounds.length, rightN = n - game.misses.length, s = game.score;
   $('#finalScore').textContent = s;
-  const right = 10 - game.misses.length;
   $('#verdict').textContent =
-    right===10 ? 'Master archivist. The auction houses should be calling.' :
-    right>=8  ? 'A connoisseur’s eye.' :
-    right>=6  ? 'A promising apprentice.' :
-    right>=4  ? 'The gallery guide is that way…' :
-                'Everyone starts somewhere.';
+    rightN===n ? 'Master archivist. The auction houses should be calling.' :
+    rightN>=n*0.8 ? 'A connoisseur’s eye.' :
+    rightN>=n*0.6 ? 'A promising apprentice.' :
+    rightN>=n*0.4 ? 'The gallery guide is that way…' :
+                    'Everyone starts somewhere.';
   $('#misses').innerHTML = game.misses.length
-    ? '<b>Worth a second look:</b><br>' + game.misses.map(a=>`<a href="/?artist=${qidOf(a)}" target="_blank" rel="noopener noreferrer">${a.full_name}</a>`).join(' · ')
-    : 'A perfect run — every hand identified.';
-  const key = 'whosehand.best.'+diff+'.'+cat;
+    ? '<b>Worth a second look:</b><br>' + game.misses.map(a=>`<a href="/?artist=${qidOf(a)}" target="_blank" rel="noopener noreferrer">${esc(a.full_name)}</a>`).join(' · ')
+    : 'A perfect run.';
+  const key = `siggames.best.${gameKey}.${diff}.${cat}`;
   const best = Math.max(s, +(localStorage.getItem(key)||0));
   localStorage.setItem(key, best);
-  $('#best').textContent = `Best score (${diff} · ${cat}): ${best}`;
+  $('#best').textContent = `Best (${gameKey} · ${diff}): ${best}`;
 }
 $('#againBtn').onclick = () => { $('#results').hidden = true; $('#setup').hidden = false; };
 </script>

← dbeb105 pre-deploy: strip temp Figma capture scripts; Authors collag  ·  back to CelebritySignatures  ·  mobile pass: responsive masthead (wrap + scrollable nav), 2- 0faa6c2 →