← back to Crazy News Channel
P24: add first cartoon for business/entertainment/scitech/sports/weather categories
3e6ffabfecb5670d8400a97f09f39f73283080ff · 2026-09-24 10:28:55 -0700 · Steve Abrams
Five single-panel interactive SVG cartoons (matching the
20260924-gnome-who-wouldnt-hush.html pattern: 5-stage click-to-advance,
Plot Twist button, dark mode, keyboard nav, reduced-motion safe), each
linked via story_id to a real invented story in stories-data.js:
- tollpoints-diamond-status -> bs-toll-loyalty (Business)
- jackalope-custody-battle -> es-jackalope-statue-town-rivalry (Entertainment)
- tuber-bank-of-chugwater -> sci-battery-potato (Sci-Tech)
- the-zipper-that-ended-an-inning -> sw-mascot-costume (Sports)
- the-corner-with-its-own-weather -> sw-persistent-crosswind (Weather)
Playwright-verified at 1440/390px, dark+reduced-motion, and full keyboard
nav (Tab reaches #art, Enter/'n' both advance) — zero console/page errors
on all 5. manifest.js registration follows once the live
build-cartoon-batch.sh run (today's 6-cartoon politics batch) finishes
writing to that file, to avoid a race on the shared array.
Also (prep only, not wired into rotation mix): cartoon-prompt-bank.mjs now
carries a category field per bank entry (existing 12 default to politics)
plus 3 new stub entries per non-politics category (15 total), and
daily-cartoon-gen.mjs threads category through to queue briefs. The picker
itself is still a flat shuffle across the whole bank -- a future session
decides the rotation mix.
Files touched
A cartoons/20260924-jackalope-custody-battle.htmlA cartoons/20260924-the-corner-with-its-own-weather.htmlA cartoons/20260924-the-zipper-that-ended-an-inning.htmlA cartoons/20260924-tollpoints-diamond-status.htmlA cartoons/20260924-tuber-bank-of-chugwater.htmlM scripts/cartoon-prompt-bank.mjsM scripts/daily-cartoon-gen.mjs
Diff
commit 3e6ffabfecb5670d8400a97f09f39f73283080ff
Author: Steve Abrams <steve@designerwallcoverings.com>
Date: Thu Sep 24 10:28:55 2026 -0700
P24: add first cartoon for business/entertainment/scitech/sports/weather categories
Five single-panel interactive SVG cartoons (matching the
20260924-gnome-who-wouldnt-hush.html pattern: 5-stage click-to-advance,
Plot Twist button, dark mode, keyboard nav, reduced-motion safe), each
linked via story_id to a real invented story in stories-data.js:
- tollpoints-diamond-status -> bs-toll-loyalty (Business)
- jackalope-custody-battle -> es-jackalope-statue-town-rivalry (Entertainment)
- tuber-bank-of-chugwater -> sci-battery-potato (Sci-Tech)
- the-zipper-that-ended-an-inning -> sw-mascot-costume (Sports)
- the-corner-with-its-own-weather -> sw-persistent-crosswind (Weather)
Playwright-verified at 1440/390px, dark+reduced-motion, and full keyboard
nav (Tab reaches #art, Enter/'n' both advance) — zero console/page errors
on all 5. manifest.js registration follows once the live
build-cartoon-batch.sh run (today's 6-cartoon politics batch) finishes
writing to that file, to avoid a race on the shared array.
Also (prep only, not wired into rotation mix): cartoon-prompt-bank.mjs now
carries a category field per bank entry (existing 12 default to politics)
plus 3 new stub entries per non-politics category (15 total), and
daily-cartoon-gen.mjs threads category through to queue briefs. The picker
itself is still a flat shuffle across the whole bank -- a future session
decides the rotation mix.
---
cartoons/20260924-jackalope-custody-battle.html | 214 ++++++++++++++++++++
.../20260924-the-corner-with-its-own-weather.html | 218 ++++++++++++++++++++
.../20260924-the-zipper-that-ended-an-inning.html | 223 +++++++++++++++++++++
cartoons/20260924-tollpoints-diamond-status.html | 214 ++++++++++++++++++++
cartoons/20260924-tuber-bank-of-chugwater.html | 214 ++++++++++++++++++++
scripts/cartoon-prompt-bank.mjs | 71 +++++--
scripts/daily-cartoon-gen.mjs | 7 +-
7 files changed, 1145 insertions(+), 16 deletions(-)
diff --git a/cartoons/20260924-jackalope-custody-battle.html b/cartoons/20260924-jackalope-custody-battle.html
new file mode 100644
index 0000000..716f17e
--- /dev/null
+++ b/cartoons/20260924-jackalope-custody-battle.html
@@ -0,0 +1,214 @@
+<!DOCTYPE html>
+<html lang="en">
+<head>
+<meta charset="UTF-8" />
+<meta name="viewport" content="width=device-width, initial-scale=1.0" />
+<title>The Jackalope Custody Battle — P24 Cartoon Desk</title>
+<meta name="description" content="An original invented-satire cartoon: two Iowa towns feud over which one truly owns the jackalope legend, ending in a statue that commutes between them by flatbed." />
+<style>
+:root{
+ --bg:#f4efe4; --panel:#fffdf6; --ink:#241c14; --ink-dim:#6b5f4d; --rule:#241c14;
+ --accent:#b5302f; --accent2:#0f6b52; --focus:#0b3d91; --seal:#c9a227;
+}
+@media (prefers-color-scheme: dark){ :root:not([data-theme="light"]){
+ --bg:#1b160f; --panel:#241d14; --ink:#f2ead9; --ink-dim:#c9bda3; --rule:#f2ead9;
+ --accent:#ff6b64; --accent2:#3ddcb0; --focus:#8ab4ff; --seal:#e0c25a;
+}}
+:root[data-theme="dark"]{
+ --bg:#1b160f; --panel:#241d14; --ink:#f2ead9; --ink-dim:#c9bda3; --rule:#f2ead9;
+ --accent:#ff6b64; --accent2:#3ddcb0; --focus:#8ab4ff; --seal:#e0c25a;
+}
+*,*::before,*::after{box-sizing:border-box}
+html{overflow-x:hidden}
+[hidden]{display:none!important}
+body{margin:0;min-height:100vh;background:var(--bg);color:var(--ink);font-family:Georgia,"Times New Roman",serif;line-height:1.4;padding-bottom:2rem}
+h1,h2,p{margin:0}
+:focus-visible{outline:3px solid var(--focus); outline-offset:2px}
+a{color:var(--accent2)}
+header{padding:16px 20px 10px;border-bottom:3px solid var(--rule);display:flex;justify-content:space-between;align-items:flex-start;flex-wrap:wrap;gap:10px}
+header a.back{font-family:-apple-system,sans-serif;font-size:.8rem;color:var(--ink-dim);text-decoration:none;border:1px solid var(--ink-dim);border-radius:6px;padding:5px 10px}
+.kicker{font-family:-apple-system,sans-serif;font-size:.7rem;letter-spacing:.08em;text-transform:uppercase;color:var(--accent)}
+h1{font-size:1.7rem;margin-top:2px}
+.dek{font-style:italic;color:var(--ink-dim);max-width:60ch;margin-top:4px}
+main{max-width:640px;margin:0 auto;padding:18px 20px}
+figure{margin:0;background:var(--panel);border:3px solid var(--rule);border-radius:8px;overflow:hidden;display:flex;flex-direction:column}
+.phead{display:flex;justify-content:space-between;font-family:-apple-system,sans-serif;font-size:.65rem;letter-spacing:.05em;text-transform:uppercase;color:var(--ink-dim);padding:6px 10px;border-bottom:1px solid var(--ink-dim)}
+.art{width:100%;aspect-ratio:3/2;display:block;cursor:pointer;background:transparent}
+.art:focus-visible{outline:3px solid var(--focus); outline-offset:-3px}
+figcaption{font-family:-apple-system,sans-serif;font-size:.92rem;color:var(--ink);padding:12px;border-top:1px solid var(--ink-dim)}
+.dock{display:flex;gap:10px;flex-wrap:wrap;align-items:center;margin:16px 0;font-family:-apple-system,sans-serif}
+button.twist{font-family:-apple-system,sans-serif;font-size:.9rem;font-weight:700;border-radius:6px;padding:9px 16px;cursor:pointer;border:2px solid var(--seal);background:var(--seal);color:#241c14}
+button.twist:disabled{opacity:.45;cursor:not-allowed}
+button.theme{margin-left:auto;background:transparent;border:1px solid var(--ink-dim);color:var(--ink-dim);border-radius:6px;padding:6px 10px;font-family:-apple-system,sans-serif;font-size:.8rem;cursor:pointer}
+#status{font-family:-apple-system,sans-serif;font-size:.82rem;padding:8px 12px;border-radius:6px;border:1px solid var(--ink-dim);color:var(--ink-dim);min-height:1.2em}
+#status.ok{border-color:var(--accent2);color:var(--accent2)}
+#status.err{border-color:var(--accent);color:var(--accent)}
+#status.info{border-color:var(--focus);color:var(--focus)}
+footer{max-width:640px;margin:20px auto 0;padding:0 20px;font-family:-apple-system,sans-serif;font-size:.72rem;color:var(--ink-dim)}
+.hint{font-family:-apple-system,sans-serif;font-size:.72rem;color:var(--ink-dim);margin-top:6px}
+@media (prefers-reduced-motion: reduce){ * { animation-duration:0.001ms !important; transition-duration:0.001ms !important; } }
+</style>
+</head>
+<body>
+<header>
+ <div>
+ <div class="kicker">P24 Cartoon Desk · Invented Satire · Story City, IA</div>
+ <h1>The Jackalope Custody Battle</h1>
+ <p class="dek">Story City unveils a 14-foot jackalope statue; neighboring Radcliffe claims the legend, and a governor's accord ends with the statue on a biweekly trailer tour.</p>
+ </div>
+ <div style="display:flex;flex-direction:column;gap:8px;align-items:flex-end">
+ <a class="back" href="index.html">← Cartoon Desk</a>
+ <button class="theme" id="themeBtn" type="button" aria-pressed="false">Dark mode: off</button>
+ </div>
+</header>
+<main>
+ <figure>
+ <div class="phead"><span>Jackalope Capital Dispute, Iowa Tourism Board</span><span id="stamp"></span></div>
+ <svg class="art" id="art" viewBox="0 0 480 320" role="img" tabindex="0" aria-label="Editorial cartoon about two towns feuding over ownership of a jackalope statue and legend"></svg>
+ <figcaption id="cap"></figcaption>
+ </figure>
+ <div class="dock">
+ <button class="twist" id="twistBtn" type="button">Plot Twist!</button>
+ <div id="status" role="status" aria-live="polite"></div>
+ </div>
+ <p class="hint">Keys: N / → advance the story · Enter/Space on the art also advances</p>
+</main>
+<footer>All towns, officials, and jackalopes are invented. No actual statue was subpoenaed. Part of PANDEMONIUM-24.</footer>
+
+<script>
+(function(){
+ const NS = 'http://www.w3.org/2000/svg';
+ function el(tag, attrs, parent){
+ const n = document.createElementNS(NS, tag);
+ for (const k in attrs) n.setAttribute(k, attrs[k]);
+ if (parent) parent.appendChild(n);
+ return n;
+ }
+ function txt(svg, x, y, size, weight, fill, content, anchor){
+ const t = el('text', { x, y, 'text-anchor': anchor||'middle', 'font-family':'-apple-system,sans-serif', 'font-size':size, 'font-weight':weight||'400', fill: fill||'currentColor' }, svg);
+ t.textContent = content;
+ return t;
+ }
+ function official(svg, cx, cy, r, color, label){
+ const g = el('g', {}, svg);
+ el('circle', { cx, cy, r, fill: color, 'fill-opacity': '0.15', stroke: color, 'stroke-width': 3 }, g);
+ el('circle', { cx, cy: cy - r*0.35, r: r*0.28, fill: color }, g);
+ el('path', { d: `M ${cx - r*0.5} ${cy + r*0.5} Q ${cx} ${cy + r*0.1} ${cx + r*0.5} ${cy + r*0.5}`, fill: 'none', stroke: color, 'stroke-width': r*0.22, 'stroke-linecap':'round' }, g);
+ if (label) txt(svg, cx, cy + r + 16, 11, '400', 'var(--ink-dim)', label);
+ return g;
+ }
+
+
+ function pedestal(svg, cx, y, w){
+ el('rect', { x: cx - w/2, y, width: w, height: 26, fill: 'currentColor', opacity: .3, rx: 3 }, svg);
+ }
+ function jackalope(svg, cx, cy, s, color){
+ const g = el('g', {}, svg);
+ el('ellipse', { cx, cy, rx: s*0.34, ry: s*0.5, fill: color, 'fill-opacity': .18, stroke: color, 'stroke-width': 3 }, g);
+ el('circle', { cx, cy: cy - s*0.55, r: s*0.2, fill: color, 'fill-opacity': .18, stroke: color, 'stroke-width': 3 }, g);
+ el('path', { d: `M ${cx-s*0.1} ${cy-s*0.7} Q ${cx-s*0.22} ${cy-s*1.15} ${cx-s*0.06} ${cy-s*1.25}`, fill:'none', stroke: color, 'stroke-width': 3 }, g);
+ el('path', { d: `M ${cx+s*0.1} ${cy-s*0.7} Q ${cx+s*0.22} ${cy-s*1.15} ${cx+s*0.06} ${cy-s*1.25}`, fill:'none', stroke: color, 'stroke-width': 3 }, g);
+ el('path', { d: `M ${cx-s*0.05} ${cy-s*0.72} L ${cx-s*0.2} ${cy-s*0.95} M ${cx-s*0.05} ${cy-s*0.72} L ${cx+s*0.06} ${cy-s*0.98}`, fill:'none', stroke: color, 'stroke-width': 2.2 }, g);
+ el('path', { d: `M ${cx+s*0.05} ${cy-s*0.72} L ${cx+s*0.2} ${cy-s*0.95} M ${cx+s*0.05} ${cy-s*0.72} L ${cx-s*0.06} ${cy-s*0.98}`, fill:'none', stroke: color, 'stroke-width': 2.2 }, g);
+ return g;
+ }
+ function plaque(svg, cx, y, w, label, sub, color){
+ el('rect', { x: cx - w/2, y, width: w, height: 40, rx: 5, fill: 'var(--panel)', stroke: color, 'stroke-width': 2.6 }, svg);
+ txt(svg, cx, y + 17, 9.5, '800', color, label);
+ if (sub) txt(svg, cx, y + 31, 8, '400', 'var(--ink-dim)', sub);
+ }
+
+ function scene(svg, stage){
+ svg.innerHTML = '';
+ if (stage === 0) {
+ pedestal(svg, 150, 250, 90);
+ jackalope(svg, 150, 220, 130, '#0f6b52');
+ plaque(svg, 360, 90, 190, 'STORY CITY, IA', '"quite the photo stop"', 'var(--seal)');
+ txt(svg, 150, 40, 12, '700', 'currentColor', '14 ft · $58,000 · 5 months to build');
+ } else if (stage === 1) {
+ pedestal(svg, 150, 250, 90);
+ jackalope(svg, 150, 220, 130, '#0f6b52');
+ el('path', { d: 'M 260 70 L 420 70 L 420 130 L 340 130 L 300 150 L 300 130 L 260 130 Z', fill: 'var(--panel)', stroke: 'var(--accent)', 'stroke-width': 2.6 }, svg);
+ txt(svg, 340, 92, 9, '700', 'currentColor', '"1890s CLIPPING PROVES');
+ txt(svg, 340, 104, 9, '700', 'currentColor', 'IT WAS RADCLIFFE."');
+ txt(svg, 340, 118, 8, '400', 'var(--ink-dim)', '— Clerk Sondra Quist');
+ } else if (stage === 2) {
+ pedestal(svg, 100, 250, 80);
+ jackalope(svg, 100, 222, 110, '#0f6b52');
+ plaque(svg, 100, 60, 150, 'STORY CITY RES. 14', 'Jackalope Capital of Iowa', 'var(--seal)');
+ pedestal(svg, 360, 250, 80);
+ jackalope(svg, 360, 222, 110, '#b5302f');
+ plaque(svg, 360, 60, 150, 'RADCLIFFE RES. 9', 'Jackalope Capital of Iowa', 'var(--accent)');
+ } else if (stage === 3) {
+ el('rect', { x: 150, y: 60, width: 180, height: 60, rx: 6, fill: 'var(--panel)', stroke: 'var(--focus)', 'stroke-width': 3 }, svg);
+ txt(svg, 240, 84, 10, '700', 'currentColor', 'IOWA TOURISM BOARD');
+ txt(svg, 240, 100, 9, '400', 'var(--ink-dim)', 'reviewing two dueling dossiers');
+ pedestal(svg, 90, 250, 70);
+ jackalope(svg, 90, 224, 100, '#0f6b52');
+ pedestal(svg, 390, 250, 70);
+ jackalope(svg, 390, 224, 100, '#b5302f');
+ } else if (stage === 4) {
+ el('rect', { x: 130, y: 230, width: 220, height: 16, rx: 3, fill: 'currentColor', opacity: .35 }, svg);
+ el('rect', { x: 150, y: 200, width: 30, height: 30, fill: 'currentColor', opacity: .5 }, svg);
+ el('rect', { x: 300, y: 200, width: 30, height: 30, fill: 'currentColor', opacity: .5 }, svg);
+ jackalope(svg, 240, 190, 120, '#0f6b52');
+ official(svg, 100, 150, 22, 'var(--focus)', 'State Trooper');
+ txt(svg, 240, 40, 12, '700', 'currentColor', 'Biweekly handoff, trooper escort included.');
+ }
+ }
+
+ const HEADLINES = ["Story City Unveils Giant Jackalope Statue For Tourism Push", "Neighboring Town Claims Jackalope Legend Originated There Instead", "Dueling Town Councils Pass Competing 'Jackalope Capital' Resolutions", "State Tourism Board Asked To Mediate Jackalope Ownership Dispute", "Governor Signs Jackalope Accord, Statue To Alternate Towns Biweekly"];
+ const STAMPS = ["UNVEILED", "DISPUTED", "DUELING RES.", "IN MEDIATION", "ACCORD SIGNED"];
+ const CONCLUSION_MSG = "The story has concluded. Both towns claim victory; the trailer has its own regional fan following.";
+
+ const state = { stage: 0 };
+
+ function say(kind, msg){ const s = document.getElementById('status'); s.className = kind; s.textContent = msg; }
+
+ function paint(){
+ scene(document.getElementById('art'), state.stage);
+ document.getElementById('cap').textContent = HEADLINES[state.stage];
+ document.getElementById('stamp').textContent = STAMPS[state.stage];
+ const btn = document.getElementById('twistBtn');
+ if (state.stage >= HEADLINES.length - 1) { btn.textContent = 'Story Concluded'; btn.disabled = true; }
+ else { btn.textContent = 'Plot Twist!'; btn.disabled = false; }
+ }
+
+ function advance(){
+ if (state.stage >= HEADLINES.length - 1) { say('info', CONCLUSION_MSG); return; }
+ state.stage++;
+ paint();
+ say('ok', `Stage ${state.stage + 1} of ${HEADLINES.length}.`);
+ }
+
+ document.getElementById('twistBtn').addEventListener('click', advance);
+ const art = document.getElementById('art');
+ art.addEventListener('click', advance);
+ art.addEventListener('keydown', (e) => { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); advance(); } });
+
+ document.addEventListener('keydown', (e) => {
+ if (e.metaKey || e.ctrlKey || e.altKey) return;
+ const tag = (e.target.tagName || '').toLowerCase();
+ if (tag === 'input' || tag === 'textarea') return;
+ const k = e.key.toLowerCase();
+ if (k === 'n' || e.key === 'ArrowRight') { e.preventDefault(); advance(); }
+ });
+
+ const themeBtn = document.getElementById('themeBtn');
+ function safeGet(k,d){ try { return localStorage.getItem(k) ?? d; } catch { return d; } }
+ function safeSet(k,v){ try { localStorage.setItem(k,v); } catch {} }
+ let theme = safeGet('p24cartoon.theme', 'auto');
+ function applyTheme(){
+ if (theme === 'dark') { document.documentElement.setAttribute('data-theme','dark'); themeBtn.textContent='Dark mode: on'; themeBtn.setAttribute('aria-pressed','true'); }
+ else { document.documentElement.setAttribute('data-theme','light'); themeBtn.textContent='Dark mode: off'; themeBtn.setAttribute('aria-pressed','false'); }
+ }
+ applyTheme();
+ themeBtn.addEventListener('click', () => { theme = theme === 'dark' ? 'light' : 'dark'; safeSet('p24cartoon.theme', theme); applyTheme(); });
+
+ paint();
+ say('info', 'Case file open. Press N, click the art, or hit Plot Twist to advance the story.');
+})();
+
+</script>
+</body>
+</html>
diff --git a/cartoons/20260924-the-corner-with-its-own-weather.html b/cartoons/20260924-the-corner-with-its-own-weather.html
new file mode 100644
index 0000000..de74139
--- /dev/null
+++ b/cartoons/20260924-the-corner-with-its-own-weather.html
@@ -0,0 +1,218 @@
+<!DOCTYPE html>
+<html lang="en">
+<head>
+<meta charset="UTF-8" />
+<meta name="viewport" content="width=device-width, initial-scale=1.0" />
+<title>The Corner With Its Own Weather — P24 Cartoon Desk</title>
+<meta name="description" content="An original invented-satire cartoon: a persistent crosswind at one small-town intersection escalates from a windsock to a paid Wind Spotter to an annual Hat-Chasing Festival." />
+<style>
+:root{
+ --bg:#f4efe4; --panel:#fffdf6; --ink:#241c14; --ink-dim:#6b5f4d; --rule:#241c14;
+ --accent:#b5302f; --accent2:#0f6b52; --focus:#0b3d91; --seal:#c9a227;
+}
+@media (prefers-color-scheme: dark){ :root:not([data-theme="light"]){
+ --bg:#1b160f; --panel:#241d14; --ink:#f2ead9; --ink-dim:#c9bda3; --rule:#f2ead9;
+ --accent:#ff6b64; --accent2:#3ddcb0; --focus:#8ab4ff; --seal:#e0c25a;
+}}
+:root[data-theme="dark"]{
+ --bg:#1b160f; --panel:#241d14; --ink:#f2ead9; --ink-dim:#c9bda3; --rule:#f2ead9;
+ --accent:#ff6b64; --accent2:#3ddcb0; --focus:#8ab4ff; --seal:#e0c25a;
+}
+*,*::before,*::after{box-sizing:border-box}
+html{overflow-x:hidden}
+[hidden]{display:none!important}
+body{margin:0;min-height:100vh;background:var(--bg);color:var(--ink);font-family:Georgia,"Times New Roman",serif;line-height:1.4;padding-bottom:2rem}
+h1,h2,p{margin:0}
+:focus-visible{outline:3px solid var(--focus); outline-offset:2px}
+a{color:var(--accent2)}
+header{padding:16px 20px 10px;border-bottom:3px solid var(--rule);display:flex;justify-content:space-between;align-items:flex-start;flex-wrap:wrap;gap:10px}
+header a.back{font-family:-apple-system,sans-serif;font-size:.8rem;color:var(--ink-dim);text-decoration:none;border:1px solid var(--ink-dim);border-radius:6px;padding:5px 10px}
+.kicker{font-family:-apple-system,sans-serif;font-size:.7rem;letter-spacing:.08em;text-transform:uppercase;color:var(--accent)}
+h1{font-size:1.7rem;margin-top:2px}
+.dek{font-style:italic;color:var(--ink-dim);max-width:60ch;margin-top:4px}
+main{max-width:640px;margin:0 auto;padding:18px 20px}
+figure{margin:0;background:var(--panel);border:3px solid var(--rule);border-radius:8px;overflow:hidden;display:flex;flex-direction:column}
+.phead{display:flex;justify-content:space-between;font-family:-apple-system,sans-serif;font-size:.65rem;letter-spacing:.05em;text-transform:uppercase;color:var(--ink-dim);padding:6px 10px;border-bottom:1px solid var(--ink-dim)}
+.art{width:100%;aspect-ratio:3/2;display:block;cursor:pointer;background:transparent}
+.art:focus-visible{outline:3px solid var(--focus); outline-offset:-3px}
+figcaption{font-family:-apple-system,sans-serif;font-size:.92rem;color:var(--ink);padding:12px;border-top:1px solid var(--ink-dim)}
+.dock{display:flex;gap:10px;flex-wrap:wrap;align-items:center;margin:16px 0;font-family:-apple-system,sans-serif}
+button.twist{font-family:-apple-system,sans-serif;font-size:.9rem;font-weight:700;border-radius:6px;padding:9px 16px;cursor:pointer;border:2px solid var(--seal);background:var(--seal);color:#241c14}
+button.twist:disabled{opacity:.45;cursor:not-allowed}
+button.theme{margin-left:auto;background:transparent;border:1px solid var(--ink-dim);color:var(--ink-dim);border-radius:6px;padding:6px 10px;font-family:-apple-system,sans-serif;font-size:.8rem;cursor:pointer}
+#status{font-family:-apple-system,sans-serif;font-size:.82rem;padding:8px 12px;border-radius:6px;border:1px solid var(--ink-dim);color:var(--ink-dim);min-height:1.2em}
+#status.ok{border-color:var(--accent2);color:var(--accent2)}
+#status.err{border-color:var(--accent);color:var(--accent)}
+#status.info{border-color:var(--focus);color:var(--focus)}
+footer{max-width:640px;margin:20px auto 0;padding:0 20px;font-family:-apple-system,sans-serif;font-size:.72rem;color:var(--ink-dim)}
+.hint{font-family:-apple-system,sans-serif;font-size:.72rem;color:var(--ink-dim);margin-top:6px}
+@media (prefers-reduced-motion: reduce){ * { animation-duration:0.001ms !important; transition-duration:0.001ms !important; } }
+</style>
+</head>
+<body>
+<header>
+ <div>
+ <div class="kicker">P24 Cartoon Desk · Invented Satire · Cut Bank, MT</div>
+ <h1>The Corner With Its Own Weather</h1>
+ <p class="dek">A stubborn crosswind at one Montana intersection earns a windsock, a paid Wind Spotter, a proposed windbreak, and finally its own festival.</p>
+ </div>
+ <div style="display:flex;flex-direction:column;gap:8px;align-items:flex-end">
+ <a class="back" href="index.html">← Cartoon Desk</a>
+ <button class="theme" id="themeBtn" type="button" aria-pressed="false">Dark mode: off</button>
+ </div>
+</header>
+<main>
+ <figure>
+ <div class="phead"><span>Front & 3rd Wind Advisory, NWS Cut Bank</span><span id="stamp"></span></div>
+ <svg class="art" id="art" viewBox="0 0 480 320" role="img" tabindex="0" aria-label="Editorial cartoon about a persistent crosswind at one intersection escalating into a civic wind festival"></svg>
+ <figcaption id="cap"></figcaption>
+ </figure>
+ <div class="dock">
+ <button class="twist" id="twistBtn" type="button">Plot Twist!</button>
+ <div id="status" role="status" aria-live="polite"></div>
+ </div>
+ <p class="hint">Keys: N / → advance the story · Enter/Space on the art also advances</p>
+</main>
+<footer>All meteorologists, officials, and hats are invented. No actual intersection was declared a weather system. Part of PANDEMONIUM-24.</footer>
+
+<script>
+(function(){
+ const NS = 'http://www.w3.org/2000/svg';
+ function el(tag, attrs, parent){
+ const n = document.createElementNS(NS, tag);
+ for (const k in attrs) n.setAttribute(k, attrs[k]);
+ if (parent) parent.appendChild(n);
+ return n;
+ }
+ function txt(svg, x, y, size, weight, fill, content, anchor){
+ const t = el('text', { x, y, 'text-anchor': anchor||'middle', 'font-family':'-apple-system,sans-serif', 'font-size':size, 'font-weight':weight||'400', fill: fill||'currentColor' }, svg);
+ t.textContent = content;
+ return t;
+ }
+ function official(svg, cx, cy, r, color, label){
+ const g = el('g', {}, svg);
+ el('circle', { cx, cy, r, fill: color, 'fill-opacity': '0.15', stroke: color, 'stroke-width': 3 }, g);
+ el('circle', { cx, cy: cy - r*0.35, r: r*0.28, fill: color }, g);
+ el('path', { d: `M ${cx - r*0.5} ${cy + r*0.5} Q ${cx} ${cy + r*0.1} ${cx + r*0.5} ${cy + r*0.5}`, fill: 'none', stroke: color, 'stroke-width': r*0.22, 'stroke-linecap':'round' }, g);
+ if (label) txt(svg, cx, cy + r + 16, 11, '400', 'var(--ink-dim)', label);
+ return g;
+ }
+
+
+ function windsock(svg, cx, cy, s, strain){
+ const g = el('g', {}, svg);
+ el('rect', { x: cx-3, y: cy-s*0.9, width: 6, height: s*0.9, fill: 'currentColor', opacity: .5 }, g);
+ const bend = strain ? s*0.55 : s*0.15;
+ el('path', { d: `M ${cx} ${cy-s*0.9} L ${cx+bend} ${cy-s*0.78} L ${cx+bend*0.85} ${cy-s*0.62} L ${cx+bend*1.1} ${cy-s*0.5} L ${cx+bend*0.6} ${cy-s*0.4} Z`,
+ fill: 'var(--accent)', 'fill-opacity': .35, stroke: 'var(--accent)', 'stroke-width': 2 }, g);
+ return g;
+ }
+ function hat(svg, cx, cy, s, color){
+ const g = el('g', { transform: `rotate(20 ${cx} ${cy})` }, svg);
+ el('ellipse', { cx, cy, rx: s*0.5, ry: s*0.14, fill: color }, g);
+ el('path', { d: `M ${cx-s*0.22} ${cy} Q ${cx} ${cy-s*0.5} ${cx+s*0.22} ${cy}`, fill: color }, g);
+ return g;
+ }
+ function pedestrian(svg, cx, cy, s, color){
+ const g = el('g', {}, svg);
+ el('circle', { cx, cy: cy - s*0.5, r: s*0.16, fill: color }, g);
+ el('path', { d: `M ${cx} ${cy-s*0.34} L ${cx} ${cy+s*0.1}`, stroke: color, 'stroke-width': s*0.1, 'stroke-linecap':'round' }, g);
+ el('path', { d: `M ${cx-s*0.18} ${cy-s*0.12} L ${cx+s*0.24} ${cy-s*0.2}`, stroke: color, 'stroke-width': s*0.08, 'stroke-linecap':'round' }, g);
+ return g;
+ }
+ function sign(svg, cx, y, w, label){
+ el('rect', { x: cx-w/2, y, width: w, height: 34, rx: 4, fill: 'var(--panel)', stroke: 'var(--accent)', 'stroke-width': 2.6 }, svg);
+ txt(svg, cx, y+21, 9.5, '700', 'var(--accent)', label);
+ }
+
+ function scene(svg, stage){
+ svg.innerHTML = '';
+ el('rect', { x: 40, y: 250, width: 400, height: 10, fill: 'currentColor', opacity: .3, rx: 3 }, svg);
+ if (stage === 0) {
+ windsock(svg, 340, 210, 130, true);
+ pedestrian(svg, 130, 220, 120, '#0f6b52');
+ hat(svg, 200, 130, 60, '#b5302f');
+ txt(svg, 200, 100, 10, '400', 'var(--ink-dim)', '(hat, departing)');
+ txt(svg, 240, 40, 12, '700', 'currentColor', '"A strange little pocket." — Ray Dunmore, NWS');
+ } else if (stage === 1) {
+ windsock(svg, 340, 210, 130, true);
+ sign(svg, 200, 90, 150, 'GUSTS POSSIBLE');
+ pedestrian(svg, 120, 220, 110, '#6b4fa0');
+ txt(svg, 240, 40, 11, '700', 'currentColor', 'Three lost hats in one week.');
+ } else if (stage === 2) {
+ windsock(svg, 340, 210, 130, true);
+ official(svg, 150, 170, 30, 'var(--seal)', 'Curt Ambrose, Wind Spotter');
+ el('rect', { x: 120, y: 220, width: 60, height: 8, fill: 'currentColor', opacity: .4, rx: 2 }, svg);
+ txt(svg, 240, 40, 11, '700', 'currentColor', '$15/hr · six notable gusts logged daily.');
+ } else if (stage === 3) {
+ el('rect', { x: 260, y: 70, width: 160, height: 110, rx: 6, fill: 'none', stroke: 'var(--focus)', 'stroke-width': 3, 'stroke-dasharray':'6 4' }, svg);
+ txt(svg, 340, 100, 9.5, '700', 'currentColor', 'PROPOSED WINDBREAK');
+ txt(svg, 340, 116, 9, '400', 'var(--ink-dim)', '$47,000 · council review');
+ windsock(svg, 150, 210, 120, true);
+ txt(svg, 240, 40, 11, '700', 'currentColor', '"Oddly specific, but so is the wind."');
+ } else if (stage === 4) {
+ el('rect', { x: 260, y: 130, width: 150, height: 90, rx: 6, fill: 'var(--panel)', stroke: 'var(--accent2)', 'stroke-width': 2.6 }, svg);
+ txt(svg, 335, 158, 10, '700', 'currentColor', 'WIND ZONE');
+ txt(svg, 335, 174, 8.5, '400', 'var(--ink-dim)', 'viewing platform');
+ txt(svg, 335, 188, 8.5, '400', 'var(--ink-dim)', 'tethered-hat stand');
+ windsock(svg, 150, 200, 130, true);
+ official(svg, 90, 150, 22, 'var(--seal)', 'Mayor Reyes');
+ txt(svg, 240, 40, 11, '700', 'var(--accent2)', 'Annual Hat-Chasing Festival, ribbon cut sideways.');
+ }
+ }
+
+ const HEADLINES = ["Cut Bank Weather Service Flags Odd Steady Crosswind At One Intersection", "City Installs Windsock, Warning Sign At Persistently Gusty Intersection", "City Hires Part-Time 'Wind Spotter' To Monitor Intersection During Business Hours", "Council Debates Custom Windbreak Structure Engineered For Single Intersection", "Cut Bank Declares Intersection Official 'Wind Zone,' Opens Viewing Platform"];
+ const STAMPS = ["FLAGGED", "SIGNAGE UP", "SPOTTER HIRED", "UNDER REVIEW", "WIND ZONE"];
+ const CONCLUSION_MSG = "The story has concluded. Next year's festival adds a category for hats recovered more than three blocks away.";
+
+ const state = { stage: 0 };
+
+ function say(kind, msg){ const s = document.getElementById('status'); s.className = kind; s.textContent = msg; }
+
+ function paint(){
+ scene(document.getElementById('art'), state.stage);
+ document.getElementById('cap').textContent = HEADLINES[state.stage];
+ document.getElementById('stamp').textContent = STAMPS[state.stage];
+ const btn = document.getElementById('twistBtn');
+ if (state.stage >= HEADLINES.length - 1) { btn.textContent = 'Story Concluded'; btn.disabled = true; }
+ else { btn.textContent = 'Plot Twist!'; btn.disabled = false; }
+ }
+
+ function advance(){
+ if (state.stage >= HEADLINES.length - 1) { say('info', CONCLUSION_MSG); return; }
+ state.stage++;
+ paint();
+ say('ok', `Stage ${state.stage + 1} of ${HEADLINES.length}.`);
+ }
+
+ document.getElementById('twistBtn').addEventListener('click', advance);
+ const art = document.getElementById('art');
+ art.addEventListener('click', advance);
+ art.addEventListener('keydown', (e) => { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); advance(); } });
+
+ document.addEventListener('keydown', (e) => {
+ if (e.metaKey || e.ctrlKey || e.altKey) return;
+ const tag = (e.target.tagName || '').toLowerCase();
+ if (tag === 'input' || tag === 'textarea') return;
+ const k = e.key.toLowerCase();
+ if (k === 'n' || e.key === 'ArrowRight') { e.preventDefault(); advance(); }
+ });
+
+ const themeBtn = document.getElementById('themeBtn');
+ function safeGet(k,d){ try { return localStorage.getItem(k) ?? d; } catch { return d; } }
+ function safeSet(k,v){ try { localStorage.setItem(k,v); } catch {} }
+ let theme = safeGet('p24cartoon.theme', 'auto');
+ function applyTheme(){
+ if (theme === 'dark') { document.documentElement.setAttribute('data-theme','dark'); themeBtn.textContent='Dark mode: on'; themeBtn.setAttribute('aria-pressed','true'); }
+ else { document.documentElement.setAttribute('data-theme','light'); themeBtn.textContent='Dark mode: off'; themeBtn.setAttribute('aria-pressed','false'); }
+ }
+ applyTheme();
+ themeBtn.addEventListener('click', () => { theme = theme === 'dark' ? 'light' : 'dark'; safeSet('p24cartoon.theme', theme); applyTheme(); });
+
+ paint();
+ say('info', 'Case file open. Press N, click the art, or hit Plot Twist to advance the story.');
+})();
+
+</script>
+</body>
+</html>
diff --git a/cartoons/20260924-the-zipper-that-ended-an-inning.html b/cartoons/20260924-the-zipper-that-ended-an-inning.html
new file mode 100644
index 0000000..1a9dff0
--- /dev/null
+++ b/cartoons/20260924-the-zipper-that-ended-an-inning.html
@@ -0,0 +1,223 @@
+<!DOCTYPE html>
+<html lang="en">
+<head>
+<meta charset="UTF-8" />
+<meta name="viewport" content="width=device-width, initial-scale=1.0" />
+<title>The Zipper That Ended an Inning — P24 Cartoon Desk</title>
+<meta name="description" content="An original invented-satire cartoon: a minor league mascot's costume zipper failure escalates into a league-wide mascot-safety bureaucracy." />
+<style>
+:root{
+ --bg:#f4efe4; --panel:#fffdf6; --ink:#241c14; --ink-dim:#6b5f4d; --rule:#241c14;
+ --accent:#b5302f; --accent2:#0f6b52; --focus:#0b3d91; --seal:#c9a227;
+}
+@media (prefers-color-scheme: dark){ :root:not([data-theme="light"]){
+ --bg:#1b160f; --panel:#241d14; --ink:#f2ead9; --ink-dim:#c9bda3; --rule:#f2ead9;
+ --accent:#ff6b64; --accent2:#3ddcb0; --focus:#8ab4ff; --seal:#e0c25a;
+}}
+:root[data-theme="dark"]{
+ --bg:#1b160f; --panel:#241d14; --ink:#f2ead9; --ink-dim:#c9bda3; --rule:#f2ead9;
+ --accent:#ff6b64; --accent2:#3ddcb0; --focus:#8ab4ff; --seal:#e0c25a;
+}
+*,*::before,*::after{box-sizing:border-box}
+html{overflow-x:hidden}
+[hidden]{display:none!important}
+body{margin:0;min-height:100vh;background:var(--bg);color:var(--ink);font-family:Georgia,"Times New Roman",serif;line-height:1.4;padding-bottom:2rem}
+h1,h2,p{margin:0}
+:focus-visible{outline:3px solid var(--focus); outline-offset:2px}
+a{color:var(--accent2)}
+header{padding:16px 20px 10px;border-bottom:3px solid var(--rule);display:flex;justify-content:space-between;align-items:flex-start;flex-wrap:wrap;gap:10px}
+header a.back{font-family:-apple-system,sans-serif;font-size:.8rem;color:var(--ink-dim);text-decoration:none;border:1px solid var(--ink-dim);border-radius:6px;padding:5px 10px}
+.kicker{font-family:-apple-system,sans-serif;font-size:.7rem;letter-spacing:.08em;text-transform:uppercase;color:var(--accent)}
+h1{font-size:1.7rem;margin-top:2px}
+.dek{font-style:italic;color:var(--ink-dim);max-width:60ch;margin-top:4px}
+main{max-width:640px;margin:0 auto;padding:18px 20px}
+figure{margin:0;background:var(--panel);border:3px solid var(--rule);border-radius:8px;overflow:hidden;display:flex;flex-direction:column}
+.phead{display:flex;justify-content:space-between;font-family:-apple-system,sans-serif;font-size:.65rem;letter-spacing:.05em;text-transform:uppercase;color:var(--ink-dim);padding:6px 10px;border-bottom:1px solid var(--ink-dim)}
+.art{width:100%;aspect-ratio:3/2;display:block;cursor:pointer;background:transparent}
+.art:focus-visible{outline:3px solid var(--focus); outline-offset:-3px}
+figcaption{font-family:-apple-system,sans-serif;font-size:.92rem;color:var(--ink);padding:12px;border-top:1px solid var(--ink-dim)}
+.dock{display:flex;gap:10px;flex-wrap:wrap;align-items:center;margin:16px 0;font-family:-apple-system,sans-serif}
+button.twist{font-family:-apple-system,sans-serif;font-size:.9rem;font-weight:700;border-radius:6px;padding:9px 16px;cursor:pointer;border:2px solid var(--seal);background:var(--seal);color:#241c14}
+button.twist:disabled{opacity:.45;cursor:not-allowed}
+button.theme{margin-left:auto;background:transparent;border:1px solid var(--ink-dim);color:var(--ink-dim);border-radius:6px;padding:6px 10px;font-family:-apple-system,sans-serif;font-size:.8rem;cursor:pointer}
+#status{font-family:-apple-system,sans-serif;font-size:.82rem;padding:8px 12px;border-radius:6px;border:1px solid var(--ink-dim);color:var(--ink-dim);min-height:1.2em}
+#status.ok{border-color:var(--accent2);color:var(--accent2)}
+#status.err{border-color:var(--accent);color:var(--accent)}
+#status.info{border-color:var(--focus);color:var(--focus)}
+footer{max-width:640px;margin:20px auto 0;padding:0 20px;font-family:-apple-system,sans-serif;font-size:.72rem;color:var(--ink-dim)}
+.hint{font-family:-apple-system,sans-serif;font-size:.72rem;color:var(--ink-dim);margin-top:6px}
+@media (prefers-reduced-motion: reduce){ * { animation-duration:0.001ms !important; transition-duration:0.001ms !important; } }
+</style>
+</head>
+<body>
+<header>
+ <div>
+ <div class="kicker">P24 Cartoon Desk · Invented Satire · Chillicothe, OH</div>
+ <h1>The Zipper That Ended an Inning</h1>
+ <p class="dek">A minor-league mascot's zipper fails mid-dance, and the fallout escalates through a reinforced costume, an 11-point checklist, and a league-wide Mascot Integrity Officer.</p>
+ </div>
+ <div style="display:flex;flex-direction:column;gap:8px;align-items:flex-end">
+ <a class="back" href="index.html">← Cartoon Desk</a>
+ <button class="theme" id="themeBtn" type="button" aria-pressed="false">Dark mode: off</button>
+ </div>
+</header>
+<main>
+ <figure>
+ <div class="phead"><span>Mascot Wardrobe Incident, Rock Falls Loggers</span><span id="stamp"></span></div>
+ <svg class="art" id="art" viewBox="0 0 480 320" role="img" tabindex="0" aria-label="Editorial cartoon about a minor league mascot costume malfunction escalating into league-wide rules"></svg>
+ <figcaption id="cap"></figcaption>
+ </figure>
+ <div class="dock">
+ <button class="twist" id="twistBtn" type="button">Plot Twist!</button>
+ <div id="status" role="status" aria-live="polite"></div>
+ </div>
+ <p class="hint">Keys: N / → advance the story · Enter/Space on the art also advances</p>
+</main>
+<footer>All teams, mascots, and officials are invented. No actual chipmunk lost his head permanently. Part of PANDEMONIUM-24.</footer>
+
+<script>
+(function(){
+ const NS = 'http://www.w3.org/2000/svg';
+ function el(tag, attrs, parent){
+ const n = document.createElementNS(NS, tag);
+ for (const k in attrs) n.setAttribute(k, attrs[k]);
+ if (parent) parent.appendChild(n);
+ return n;
+ }
+ function txt(svg, x, y, size, weight, fill, content, anchor){
+ const t = el('text', { x, y, 'text-anchor': anchor||'middle', 'font-family':'-apple-system,sans-serif', 'font-size':size, 'font-weight':weight||'400', fill: fill||'currentColor' }, svg);
+ t.textContent = content;
+ return t;
+ }
+ function official(svg, cx, cy, r, color, label){
+ const g = el('g', {}, svg);
+ el('circle', { cx, cy, r, fill: color, 'fill-opacity': '0.15', stroke: color, 'stroke-width': 3 }, g);
+ el('circle', { cx, cy: cy - r*0.35, r: r*0.28, fill: color }, g);
+ el('path', { d: `M ${cx - r*0.5} ${cy + r*0.5} Q ${cx} ${cy + r*0.1} ${cx + r*0.5} ${cy + r*0.5}`, fill: 'none', stroke: color, 'stroke-width': r*0.22, 'stroke-linecap':'round' }, g);
+ if (label) txt(svg, cx, cy + r + 16, 11, '400', 'var(--ink-dim)', label);
+ return g;
+ }
+
+
+ function chipmunkHead(svg, cx, cy, s, color, detached){
+ const g = el('g', {}, svg);
+ el('circle', { cx, cy, r: s*0.42, fill: color, 'fill-opacity': .2, stroke: color, 'stroke-width': 3 }, g);
+ el('ellipse', { cx: cx - s*0.28, cy: cy - s*0.34, rx: s*0.12, ry: s*0.18, fill: color }, g);
+ el('ellipse', { cx: cx + s*0.28, cy: cy - s*0.34, rx: s*0.12, ry: s*0.18, fill: color }, g);
+ el('circle', { cx: cx - s*0.14, cy: cy - s*0.04, r: s*0.05, fill: 'currentColor' }, g);
+ el('circle', { cx: cx + s*0.14, cy: cy - s*0.04, r: s*0.05, fill: 'currentColor' }, g);
+ el('ellipse', { cx, cy: cy + s*0.14, rx: s*0.1, ry: s*0.07, fill: '#f2ead9' }, g);
+ if (detached) el('circle', { cx, cy, r: s*0.42+6, fill:'none', stroke:'var(--accent)', 'stroke-width':2, 'stroke-dasharray':'4 4' }, g);
+ return g;
+ }
+ function chipmunkBody(svg, cx, cy, s, color, headOn){
+ const g = el('g', {}, svg);
+ el('ellipse', { cx, cy, rx: s*0.32, ry: s*0.46, fill: color, 'fill-opacity': .2, stroke: color, 'stroke-width': 3 }, g);
+ el('rect', { x: cx-4, y: cy - s*0.5, width: 8, height: s*0.14, fill: 'var(--accent)', opacity: headOn ? 0.9 : 0.25 }, g);
+ return g;
+ }
+ function bench(svg, x, y, w){
+ el('rect', { x, y, width: w, height: 8, fill: 'currentColor', opacity: .45, rx: 3 }, svg);
+ el('rect', { x: x+8, y: y+8, width: 6, height: 20, fill: 'currentColor', opacity: .3 }, svg);
+ el('rect', { x: x+w-14, y: y+8, width: 6, height: 20, fill: 'currentColor', opacity: .3 }, svg);
+ }
+ function checklist(svg, x, y, w, h, n){
+ el('rect', { x, y, width: w, height: h, rx: 5, fill: 'var(--panel)', stroke: 'var(--focus)', 'stroke-width': 2.4 }, svg);
+ for (let i=0;i<n;i++){
+ el('rect', { x: x+10, y: y+10+i*11, width: 8, height: 8, fill:'none', stroke:'currentColor', 'stroke-width':1.4 }, svg);
+ el('rect', { x: x+24, y: y+13+i*11, width: w-34, height: 3, fill:'currentColor', opacity:.35 }, svg);
+ }
+ }
+
+ function scene(svg, stage){
+ svg.innerHTML = '';
+ el('rect', { x: 40, y: 260, width: 400, height: 10, fill: 'currentColor', opacity: .3, rx: 3 }, svg);
+ if (stage === 0) {
+ chipmunkBody(svg, 150, 210, 130, '#c9812f', false);
+ bench(svg, 260, 236, 90);
+ chipmunkHead(svg, 305, 216, 90, '#c9812f', true);
+ txt(svg, 240, 40, 12, '700', 'currentColor', '"Right as Chuck was doing the dance."');
+ } else if (stage === 1) {
+ chipmunkBody(svg, 130, 210, 130, '#c9812f', true);
+ chipmunkHead(svg, 130, 148, 110, '#c9812f', false);
+ el('path', { d: 'M 100 175 L 100 245', stroke: 'var(--accent2)', 'stroke-width': 3 }, svg);
+ txt(svg, 330, 90, 10, '700', 'currentColor', 'NEW COSTUME');
+ txt(svg, 330, 104, 8.5, '400', 'var(--ink-dim)', '$1,200 · zipper guard');
+ official(svg, 330, 150, 24, '#0f6b52', 'Dana Everhart, backup');
+ } else if (stage === 2) {
+ checklist(svg, 250, 60, 170, 130, 8);
+ txt(svg, 335, 195, 8.5, '700', 'currentColor', '11-POINT CHECKLIST');
+ chipmunkBody(svg, 110, 210, 130, '#c9812f', true);
+ chipmunkHead(svg, 110, 148, 110, '#c9812f', true);
+ official(svg, 110, 260, 18, '#6b4fa0', 'Ray Buskirk, signs off');
+ } else if (stage === 3) {
+ chipmunkBody(svg, 100, 220, 100, '#c9812f', true);
+ chipmunkHead(svg, 100, 168, 86, '#c9812f', true);
+ chipmunkBody(svg, 220, 220, 100, '#0f6b52', true);
+ chipmunkHead(svg, 220, 168, 86, '#0f6b52', true);
+ chipmunkBody(svg, 340, 220, 100, '#b5302f', true);
+ chipmunkHead(svg, 340, 168, 86, '#b5302f', true);
+ txt(svg, 240, 40, 11, '700', 'currentColor', 'Every mascot: zipper kit + walkie-talkie, league-wide.');
+ } else if (stage === 4) {
+ official(svg, 200, 130, 30, 'var(--accent)', 'Mascot Integrity Officer Sal Untermeyer');
+ chipmunkBody(svg, 340, 220, 110, '#c9812f', true);
+ chipmunkHead(svg, 340, 158, 96, '#c9812f', true);
+ txt(svg, 200, 260, 11, '700', 'var(--accent2)', 'Chuck has not lost his head since.');
+ }
+ }
+
+ const HEADLINES = ["Loggers Game Delayed Six Minutes After Mascot Costume Zipper Fails", "Loggers Order Reinforced Costume, Add Backup Mascot Performer", "Team Institutes 11-Point Mascot Costume Inspection Checklist Before Games", "League Mandates Every Mascot Carry Emergency Zipper Kit, Walkie-Talkie", "League Creates 'Mascot Integrity Officer' With Power To Halt Games For Costumes"];
+ const STAMPS = ["DELAYED", "REINFORCED", "CHECKLIST", "LEAGUE MANDATE", "OFFICER NAMED"];
+ const CONCLUSION_MSG = "The story has concluded. Chuck the Chipmunk, for what it's worth, has not lost his head since.";
+
+ const state = { stage: 0 };
+
+ function say(kind, msg){ const s = document.getElementById('status'); s.className = kind; s.textContent = msg; }
+
+ function paint(){
+ scene(document.getElementById('art'), state.stage);
+ document.getElementById('cap').textContent = HEADLINES[state.stage];
+ document.getElementById('stamp').textContent = STAMPS[state.stage];
+ const btn = document.getElementById('twistBtn');
+ if (state.stage >= HEADLINES.length - 1) { btn.textContent = 'Story Concluded'; btn.disabled = true; }
+ else { btn.textContent = 'Plot Twist!'; btn.disabled = false; }
+ }
+
+ function advance(){
+ if (state.stage >= HEADLINES.length - 1) { say('info', CONCLUSION_MSG); return; }
+ state.stage++;
+ paint();
+ say('ok', `Stage ${state.stage + 1} of ${HEADLINES.length}.`);
+ }
+
+ document.getElementById('twistBtn').addEventListener('click', advance);
+ const art = document.getElementById('art');
+ art.addEventListener('click', advance);
+ art.addEventListener('keydown', (e) => { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); advance(); } });
+
+ document.addEventListener('keydown', (e) => {
+ if (e.metaKey || e.ctrlKey || e.altKey) return;
+ const tag = (e.target.tagName || '').toLowerCase();
+ if (tag === 'input' || tag === 'textarea') return;
+ const k = e.key.toLowerCase();
+ if (k === 'n' || e.key === 'ArrowRight') { e.preventDefault(); advance(); }
+ });
+
+ const themeBtn = document.getElementById('themeBtn');
+ function safeGet(k,d){ try { return localStorage.getItem(k) ?? d; } catch { return d; } }
+ function safeSet(k,v){ try { localStorage.setItem(k,v); } catch {} }
+ let theme = safeGet('p24cartoon.theme', 'auto');
+ function applyTheme(){
+ if (theme === 'dark') { document.documentElement.setAttribute('data-theme','dark'); themeBtn.textContent='Dark mode: on'; themeBtn.setAttribute('aria-pressed','true'); }
+ else { document.documentElement.setAttribute('data-theme','light'); themeBtn.textContent='Dark mode: off'; themeBtn.setAttribute('aria-pressed','false'); }
+ }
+ applyTheme();
+ themeBtn.addEventListener('click', () => { theme = theme === 'dark' ? 'light' : 'dark'; safeSet('p24cartoon.theme', theme); applyTheme(); });
+
+ paint();
+ say('info', 'Case file open. Press N, click the art, or hit Plot Twist to advance the story.');
+})();
+
+</script>
+</body>
+</html>
diff --git a/cartoons/20260924-tollpoints-diamond-status.html b/cartoons/20260924-tollpoints-diamond-status.html
new file mode 100644
index 0000000..0dd1e9a
--- /dev/null
+++ b/cartoons/20260924-tollpoints-diamond-status.html
@@ -0,0 +1,214 @@
+<!DOCTYPE html>
+<html lang="en">
+<head>
+<meta charset="UTF-8" />
+<meta name="viewport" content="width=device-width, initial-scale=1.0" />
+<title>The Booth That Became an Airline — P24 Cartoon Desk</title>
+<meta name="description" content="An original invented-satire cartoon: Ashtabula's toll bridge loyalty program escalates from gift-card points to a Gold Lounge to its own currency and chartered airline." />
+<style>
+:root{
+ --bg:#f4efe4; --panel:#fffdf6; --ink:#241c14; --ink-dim:#6b5f4d; --rule:#241c14;
+ --accent:#b5302f; --accent2:#0f6b52; --focus:#0b3d91; --seal:#c9a227;
+}
+@media (prefers-color-scheme: dark){ :root:not([data-theme="light"]){
+ --bg:#1b160f; --panel:#241d14; --ink:#f2ead9; --ink-dim:#c9bda3; --rule:#f2ead9;
+ --accent:#ff6b64; --accent2:#3ddcb0; --focus:#8ab4ff; --seal:#e0c25a;
+}}
+:root[data-theme="dark"]{
+ --bg:#1b160f; --panel:#241d14; --ink:#f2ead9; --ink-dim:#c9bda3; --rule:#f2ead9;
+ --accent:#ff6b64; --accent2:#3ddcb0; --focus:#8ab4ff; --seal:#e0c25a;
+}
+*,*::before,*::after{box-sizing:border-box}
+html{overflow-x:hidden}
+[hidden]{display:none!important}
+body{margin:0;min-height:100vh;background:var(--bg);color:var(--ink);font-family:Georgia,"Times New Roman",serif;line-height:1.4;padding-bottom:2rem}
+h1,h2,p{margin:0}
+:focus-visible{outline:3px solid var(--focus); outline-offset:2px}
+a{color:var(--accent2)}
+header{padding:16px 20px 10px;border-bottom:3px solid var(--rule);display:flex;justify-content:space-between;align-items:flex-start;flex-wrap:wrap;gap:10px}
+header a.back{font-family:-apple-system,sans-serif;font-size:.8rem;color:var(--ink-dim);text-decoration:none;border:1px solid var(--ink-dim);border-radius:6px;padding:5px 10px}
+.kicker{font-family:-apple-system,sans-serif;font-size:.7rem;letter-spacing:.08em;text-transform:uppercase;color:var(--accent)}
+h1{font-size:1.7rem;margin-top:2px}
+.dek{font-style:italic;color:var(--ink-dim);max-width:60ch;margin-top:4px}
+main{max-width:640px;margin:0 auto;padding:18px 20px}
+figure{margin:0;background:var(--panel);border:3px solid var(--rule);border-radius:8px;overflow:hidden;display:flex;flex-direction:column}
+.phead{display:flex;justify-content:space-between;font-family:-apple-system,sans-serif;font-size:.65rem;letter-spacing:.05em;text-transform:uppercase;color:var(--ink-dim);padding:6px 10px;border-bottom:1px solid var(--ink-dim)}
+.art{width:100%;aspect-ratio:3/2;display:block;cursor:pointer;background:transparent}
+.art:focus-visible{outline:3px solid var(--focus); outline-offset:-3px}
+figcaption{font-family:-apple-system,sans-serif;font-size:.92rem;color:var(--ink);padding:12px;border-top:1px solid var(--ink-dim)}
+.dock{display:flex;gap:10px;flex-wrap:wrap;align-items:center;margin:16px 0;font-family:-apple-system,sans-serif}
+button.twist{font-family:-apple-system,sans-serif;font-size:.9rem;font-weight:700;border-radius:6px;padding:9px 16px;cursor:pointer;border:2px solid var(--seal);background:var(--seal);color:#241c14}
+button.twist:disabled{opacity:.45;cursor:not-allowed}
+button.theme{margin-left:auto;background:transparent;border:1px solid var(--ink-dim);color:var(--ink-dim);border-radius:6px;padding:6px 10px;font-family:-apple-system,sans-serif;font-size:.8rem;cursor:pointer}
+#status{font-family:-apple-system,sans-serif;font-size:.82rem;padding:8px 12px;border-radius:6px;border:1px solid var(--ink-dim);color:var(--ink-dim);min-height:1.2em}
+#status.ok{border-color:var(--accent2);color:var(--accent2)}
+#status.err{border-color:var(--accent);color:var(--accent)}
+#status.info{border-color:var(--focus);color:var(--focus)}
+footer{max-width:640px;margin:20px auto 0;padding:0 20px;font-family:-apple-system,sans-serif;font-size:.72rem;color:var(--ink-dim)}
+.hint{font-family:-apple-system,sans-serif;font-size:.72rem;color:var(--ink-dim);margin-top:6px}
+@media (prefers-reduced-motion: reduce){ * { animation-duration:0.001ms !important; transition-duration:0.001ms !important; } }
+</style>
+</head>
+<body>
+<header>
+ <div>
+ <div class="kicker">P24 Cartoon Desk · Invented Satire · Ashtabula, OH</div>
+ <h1>The Booth That Became an Airline</h1>
+ <p class="dek">Ashtabula's toll bridge rewards E-ZPass loyalty with points, gift cards, a Gold Lounge, and eventually its own currency and chartered flight.</p>
+ </div>
+ <div style="display:flex;flex-direction:column;gap:8px;align-items:flex-end">
+ <a class="back" href="index.html">← Cartoon Desk</a>
+ <button class="theme" id="themeBtn" type="button" aria-pressed="false">Dark mode: off</button>
+ </div>
+</header>
+<main>
+ <figure>
+ <div class="phead"><span>TollPoints Loyalty Program, Booth No. 3</span><span id="stamp"></span></div>
+ <svg class="art" id="art" viewBox="0 0 480 320" role="img" tabindex="0" aria-label="Editorial cartoon about a toll bridge loyalty-points program escalating out of control"></svg>
+ <figcaption id="cap"></figcaption>
+ </figure>
+ <div class="dock">
+ <button class="twist" id="twistBtn" type="button">Plot Twist!</button>
+ <div id="status" role="status" aria-live="polite"></div>
+ </div>
+ <p class="hint">Keys: N / → advance the story · Enter/Space on the art also advances</p>
+</main>
+<footer>All officials, boards, and toll booths are invented. No actual bridge was incorporated. Part of PANDEMONIUM-24.</footer>
+
+<script>
+(function(){
+ const NS = 'http://www.w3.org/2000/svg';
+ function el(tag, attrs, parent){
+ const n = document.createElementNS(NS, tag);
+ for (const k in attrs) n.setAttribute(k, attrs[k]);
+ if (parent) parent.appendChild(n);
+ return n;
+ }
+ function txt(svg, x, y, size, weight, fill, content, anchor){
+ const t = el('text', { x, y, 'text-anchor': anchor||'middle', 'font-family':'-apple-system,sans-serif', 'font-size':size, 'font-weight':weight||'400', fill: fill||'currentColor' }, svg);
+ t.textContent = content;
+ return t;
+ }
+ function official(svg, cx, cy, r, color, label){
+ const g = el('g', {}, svg);
+ el('circle', { cx, cy, r, fill: color, 'fill-opacity': '0.15', stroke: color, 'stroke-width': 3 }, g);
+ el('circle', { cx, cy: cy - r*0.35, r: r*0.28, fill: color }, g);
+ el('path', { d: `M ${cx - r*0.5} ${cy + r*0.5} Q ${cx} ${cy + r*0.1} ${cx + r*0.5} ${cy + r*0.5}`, fill: 'none', stroke: color, 'stroke-width': r*0.22, 'stroke-linecap':'round' }, g);
+ if (label) txt(svg, cx, cy + r + 16, 11, '400', 'var(--ink-dim)', label);
+ return g;
+ }
+
+
+ function boothStructure(svg){
+ el('rect', { x: 190, y: 70, width: 100, height: 130, fill: 'var(--panel)', stroke: 'var(--rule)', 'stroke-width': 3 }, svg);
+ el('rect', { x: 210, y: 90, width: 60, height: 34, fill: 'none', stroke: 'var(--rule)', 'stroke-width': 2.4 }, svg);
+ txt(svg, 240, 145, 10, '700', 'currentColor', 'BOOTH NO. 3');
+ el('rect', { x: 60, y: 210, width: 360, height: 12, fill: 'currentColor', opacity: .3, rx: 3 }, svg);
+ }
+ function car(svg, cx, cy, s, color){
+ const g = el('g', {}, svg);
+ el('rect', { x: cx - s*0.55, y: cy - s*0.18, width: s*1.1, height: s*0.34, rx: s*0.12, fill: color }, g);
+ el('rect', { x: cx - s*0.28, y: cy - s*0.4, width: s*0.56, height: s*0.28, rx: s*0.08, fill: color }, g);
+ el('circle', { cx: cx - s*0.32, cy: cy + s*0.16, r: s*0.14, fill: 'currentColor' }, g);
+ el('circle', { cx: cx + s*0.32, cy: cy + s*0.16, r: s*0.14, fill: 'currentColor' }, g);
+ return g;
+ }
+ function card(svg, cx, cy, w, h, label, sub, color){
+ const g = el('g', {}, svg);
+ el('rect', { x: cx - w/2, y: cy - h/2, width: w, height: h, rx: 8, fill: 'var(--panel)', stroke: color, 'stroke-width': 3 }, g);
+ txt(svg, cx, cy - 4, 11, '800', color, label);
+ if (sub) txt(svg, cx, cy + 14, 8.5, '400', 'var(--ink-dim)', sub);
+ return g;
+ }
+
+ function scene(svg, stage){
+ svg.innerHTML = '';
+ if (stage === 0) {
+ boothStructure(svg);
+ car(svg, 100, 226, 90, '#0f6b52');
+ card(svg, 380, 100, 150, 62, 'TOLLPOINTS', '1 pt / crossing', 'var(--seal)');
+ txt(svg, 240, 40, 13, '700', 'currentColor', '"Everybody else has a punch card."');
+ } else if (stage === 1) {
+ boothStructure(svg);
+ car(svg, 90, 226, 80, '#0f6b52');
+ car(svg, 150, 226, 80, '#6b4fa0');
+ car(svg, 210, 226, 80, '#b5302f');
+ card(svg, 380, 90, 150, 60, '+340%', 'repeat crossings', 'var(--accent)');
+ txt(svg, 240, 40, 12, '700', 'currentColor', 'One Buick: 41 crossings today.');
+ } else if (stage === 2) {
+ boothStructure(svg);
+ for (let i=0;i<4;i++) car(svg, 60 + i*45, 226, 60, i%2===0 ? '#0f6b52' : '#6b4fa0');
+ txt(svg, 240, 40, 12, '700', 'var(--accent)', 'Idling overnight toward Gold Commuter status.');
+ card(svg, 380, 90, 150, 46, 'GOLD TIER', '500 crossings', 'var(--seal)');
+ } else if (stage === 3) {
+ el('rect', { x: 300, y: 60, width: 150, height: 90, rx: 8, fill: 'var(--panel)', stroke: 'var(--seal)', 'stroke-width': 3 }, svg);
+ txt(svg, 375, 90, 11, '800', 'var(--seal)', 'GOLD LOUNGE');
+ txt(svg, 375, 108, 9, '400', 'var(--ink-dim)', 'pretzels + concierge');
+ official(svg, 375, 135, 18, '#0f6b52', null);
+ boothStructure(svg);
+ car(svg, 90, 226, 80, '#0f6b52');
+ } else if (stage === 4) {
+ boothStructure(svg);
+ el('path', { d: 'M 330 60 L 380 40 L 430 60 L 405 68 L 405 90 L 355 90 L 355 68 Z', fill: 'var(--accent2)' }, svg);
+ txt(svg, 380, 108, 10, '700', 'currentColor', 'TOLLCOIN AIR');
+ txt(svg, 380, 122, 8.5, '400', 'var(--ink-dim)', 'twice-daily to the waffle house');
+ card(svg, 150, 90, 150, 46, 'TOLLCOIN', 'its own currency', 'var(--seal)');
+ car(svg, 90, 226, 80, '#0f6b52');
+ }
+ }
+
+ const HEADLINES = ["Ashtabula County Toll Bridge Launches Driver Loyalty Points Program", "TollPoints Program Sees Unexpected Surge in Repeat Bridge Crossings", "Drivers Circling Toll Bridge Overnight to Reach 'Gold Commuter' Tier", "Toll Authority Opens Members-Only Lounge Inside Booth No. 3", "Ashtabula Toll Bridge Points Program Now Backed by Its Own Currency, Airline"];
+ const STAMPS = ["LAUNCHED", "SURGING", "GOLD TIER", "LOUNGE OPEN", "DIAMOND"];
+ const CONCLUSION_MSG = "The story has concluded. Somewhere, a toll booth still dreams of becoming a country.";
+
+ const state = { stage: 0 };
+
+ function say(kind, msg){ const s = document.getElementById('status'); s.className = kind; s.textContent = msg; }
+
+ function paint(){
+ scene(document.getElementById('art'), state.stage);
+ document.getElementById('cap').textContent = HEADLINES[state.stage];
+ document.getElementById('stamp').textContent = STAMPS[state.stage];
+ const btn = document.getElementById('twistBtn');
+ if (state.stage >= HEADLINES.length - 1) { btn.textContent = 'Story Concluded'; btn.disabled = true; }
+ else { btn.textContent = 'Plot Twist!'; btn.disabled = false; }
+ }
+
+ function advance(){
+ if (state.stage >= HEADLINES.length - 1) { say('info', CONCLUSION_MSG); return; }
+ state.stage++;
+ paint();
+ say('ok', `Stage ${state.stage + 1} of ${HEADLINES.length}.`);
+ }
+
+ document.getElementById('twistBtn').addEventListener('click', advance);
+ const art = document.getElementById('art');
+ art.addEventListener('click', advance);
+ art.addEventListener('keydown', (e) => { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); advance(); } });
+
+ document.addEventListener('keydown', (e) => {
+ if (e.metaKey || e.ctrlKey || e.altKey) return;
+ const tag = (e.target.tagName || '').toLowerCase();
+ if (tag === 'input' || tag === 'textarea') return;
+ const k = e.key.toLowerCase();
+ if (k === 'n' || e.key === 'ArrowRight') { e.preventDefault(); advance(); }
+ });
+
+ const themeBtn = document.getElementById('themeBtn');
+ function safeGet(k,d){ try { return localStorage.getItem(k) ?? d; } catch { return d; } }
+ function safeSet(k,v){ try { localStorage.setItem(k,v); } catch {} }
+ let theme = safeGet('p24cartoon.theme', 'auto');
+ function applyTheme(){
+ if (theme === 'dark') { document.documentElement.setAttribute('data-theme','dark'); themeBtn.textContent='Dark mode: on'; themeBtn.setAttribute('aria-pressed','true'); }
+ else { document.documentElement.setAttribute('data-theme','light'); themeBtn.textContent='Dark mode: off'; themeBtn.setAttribute('aria-pressed','false'); }
+ }
+ applyTheme();
+ themeBtn.addEventListener('click', () => { theme = theme === 'dark' ? 'light' : 'dark'; safeSet('p24cartoon.theme', theme); applyTheme(); });
+
+ paint();
+ say('info', 'Case file open. Press N, click the art, or hit Plot Twist to advance the story.');
+})();
+
+</script>
+</body>
+</html>
diff --git a/cartoons/20260924-tuber-bank-of-chugwater.html b/cartoons/20260924-tuber-bank-of-chugwater.html
new file mode 100644
index 0000000..8097dfd
--- /dev/null
+++ b/cartoons/20260924-tuber-bank-of-chugwater.html
@@ -0,0 +1,214 @@
+<!DOCTYPE html>
+<html lang="en">
+<head>
+<meta charset="UTF-8" />
+<meta name="viewport" content="width=device-width, initial-scale=1.0" />
+<title>The Tuber Bank of Chugwater — P24 Cartoon Desk</title>
+<meta name="description" content="An original invented-satire cartoon: a Wyoming lab's potato-battery pilot escalates from soil sensors to a custom potato cultivar to a wired 'tuber bank' backing the county grid." />
+<style>
+:root{
+ --bg:#f4efe4; --panel:#fffdf6; --ink:#241c14; --ink-dim:#6b5f4d; --rule:#241c14;
+ --accent:#b5302f; --accent2:#0f6b52; --focus:#0b3d91; --seal:#c9a227;
+}
+@media (prefers-color-scheme: dark){ :root:not([data-theme="light"]){
+ --bg:#1b160f; --panel:#241d14; --ink:#f2ead9; --ink-dim:#c9bda3; --rule:#f2ead9;
+ --accent:#ff6b64; --accent2:#3ddcb0; --focus:#8ab4ff; --seal:#e0c25a;
+}}
+:root[data-theme="dark"]{
+ --bg:#1b160f; --panel:#241d14; --ink:#f2ead9; --ink-dim:#c9bda3; --rule:#f2ead9;
+ --accent:#ff6b64; --accent2:#3ddcb0; --focus:#8ab4ff; --seal:#e0c25a;
+}
+*,*::before,*::after{box-sizing:border-box}
+html{overflow-x:hidden}
+[hidden]{display:none!important}
+body{margin:0;min-height:100vh;background:var(--bg);color:var(--ink);font-family:Georgia,"Times New Roman",serif;line-height:1.4;padding-bottom:2rem}
+h1,h2,p{margin:0}
+:focus-visible{outline:3px solid var(--focus); outline-offset:2px}
+a{color:var(--accent2)}
+header{padding:16px 20px 10px;border-bottom:3px solid var(--rule);display:flex;justify-content:space-between;align-items:flex-start;flex-wrap:wrap;gap:10px}
+header a.back{font-family:-apple-system,sans-serif;font-size:.8rem;color:var(--ink-dim);text-decoration:none;border:1px solid var(--ink-dim);border-radius:6px;padding:5px 10px}
+.kicker{font-family:-apple-system,sans-serif;font-size:.7rem;letter-spacing:.08em;text-transform:uppercase;color:var(--accent)}
+h1{font-size:1.7rem;margin-top:2px}
+.dek{font-style:italic;color:var(--ink-dim);max-width:60ch;margin-top:4px}
+main{max-width:640px;margin:0 auto;padding:18px 20px}
+figure{margin:0;background:var(--panel);border:3px solid var(--rule);border-radius:8px;overflow:hidden;display:flex;flex-direction:column}
+.phead{display:flex;justify-content:space-between;font-family:-apple-system,sans-serif;font-size:.65rem;letter-spacing:.05em;text-transform:uppercase;color:var(--ink-dim);padding:6px 10px;border-bottom:1px solid var(--ink-dim)}
+.art{width:100%;aspect-ratio:3/2;display:block;cursor:pointer;background:transparent}
+.art:focus-visible{outline:3px solid var(--focus); outline-offset:-3px}
+figcaption{font-family:-apple-system,sans-serif;font-size:.92rem;color:var(--ink);padding:12px;border-top:1px solid var(--ink-dim)}
+.dock{display:flex;gap:10px;flex-wrap:wrap;align-items:center;margin:16px 0;font-family:-apple-system,sans-serif}
+button.twist{font-family:-apple-system,sans-serif;font-size:.9rem;font-weight:700;border-radius:6px;padding:9px 16px;cursor:pointer;border:2px solid var(--seal);background:var(--seal);color:#241c14}
+button.twist:disabled{opacity:.45;cursor:not-allowed}
+button.theme{margin-left:auto;background:transparent;border:1px solid var(--ink-dim);color:var(--ink-dim);border-radius:6px;padding:6px 10px;font-family:-apple-system,sans-serif;font-size:.8rem;cursor:pointer}
+#status{font-family:-apple-system,sans-serif;font-size:.82rem;padding:8px 12px;border-radius:6px;border:1px solid var(--ink-dim);color:var(--ink-dim);min-height:1.2em}
+#status.ok{border-color:var(--accent2);color:var(--accent2)}
+#status.err{border-color:var(--accent);color:var(--accent)}
+#status.info{border-color:var(--focus);color:var(--focus)}
+footer{max-width:640px;margin:20px auto 0;padding:0 20px;font-family:-apple-system,sans-serif;font-size:.72rem;color:var(--ink-dim)}
+.hint{font-family:-apple-system,sans-serif;font-size:.72rem;color:var(--ink-dim);margin-top:6px}
+@media (prefers-reduced-motion: reduce){ * { animation-duration:0.001ms !important; transition-duration:0.001ms !important; } }
+</style>
+</head>
+<body>
+<header>
+ <div>
+ <div class="kicker">P24 Cartoon Desk · Invented Satire · Chugwater, WY</div>
+ <h1>The Tuber Bank of Chugwater</h1>
+ <p class="dek">A Wyoming lab's potato-powered soil sensors outlast lithium cells, get their own custom-bred spud, and end up wired into the county's emergency power grid.</p>
+ </div>
+ <div style="display:flex;flex-direction:column;gap:8px;align-items:flex-end">
+ <a class="back" href="index.html">← Cartoon Desk</a>
+ <button class="theme" id="themeBtn" type="button" aria-pressed="false">Dark mode: off</button>
+ </div>
+</header>
+<main>
+ <figure>
+ <div class="phead"><span>Potato-Battery Pilot, Chugwater Agricultural Institute</span><span id="stamp"></span></div>
+ <svg class="art" id="art" viewBox="0 0 480 320" role="img" tabindex="0" aria-label="Editorial cartoon about potato-based batteries escalating from lab sensors to a county power grid"></svg>
+ <figcaption id="cap"></figcaption>
+ </figure>
+ <div class="dock">
+ <button class="twist" id="twistBtn" type="button">Plot Twist!</button>
+ <div id="status" role="status" aria-live="polite"></div>
+ </div>
+ <p class="hint">Keys: N / → advance the story · Enter/Space on the art also advances</p>
+</main>
+<footer>All researchers, institutes, and potatoes are invented. No actual tuber was interconnected to a utility grid. Part of PANDEMONIUM-24.</footer>
+
+<script>
+(function(){
+ const NS = 'http://www.w3.org/2000/svg';
+ function el(tag, attrs, parent){
+ const n = document.createElementNS(NS, tag);
+ for (const k in attrs) n.setAttribute(k, attrs[k]);
+ if (parent) parent.appendChild(n);
+ return n;
+ }
+ function txt(svg, x, y, size, weight, fill, content, anchor){
+ const t = el('text', { x, y, 'text-anchor': anchor||'middle', 'font-family':'-apple-system,sans-serif', 'font-size':size, 'font-weight':weight||'400', fill: fill||'currentColor' }, svg);
+ t.textContent = content;
+ return t;
+ }
+ function official(svg, cx, cy, r, color, label){
+ const g = el('g', {}, svg);
+ el('circle', { cx, cy, r, fill: color, 'fill-opacity': '0.15', stroke: color, 'stroke-width': 3 }, g);
+ el('circle', { cx, cy: cy - r*0.35, r: r*0.28, fill: color }, g);
+ el('path', { d: `M ${cx - r*0.5} ${cy + r*0.5} Q ${cx} ${cy + r*0.1} ${cx + r*0.5} ${cy + r*0.5}`, fill: 'none', stroke: color, 'stroke-width': r*0.22, 'stroke-linecap':'round' }, g);
+ if (label) txt(svg, cx, cy + r + 16, 11, '400', 'var(--ink-dim)', label);
+ return g;
+ }
+
+
+ function potato(svg, cx, cy, s, color){
+ const g = el('g', {}, svg);
+ el('ellipse', { cx, cy, rx: s*0.55, ry: s*0.34, fill: color || '#a9764a', stroke: 'var(--rule)', 'stroke-width': 2.4, transform: `rotate(-8 ${cx} ${cy})` }, g);
+ el('circle', { cx: cx - s*0.15, cy: cy - s*0.05, r: s*0.045, fill: 'var(--rule)', opacity: .5 }, g);
+ el('circle', { cx: cx + s*0.1, cy: cy + s*0.08, r: s*0.04, fill: 'var(--rule)', opacity: .5 }, g);
+ return g;
+ }
+ function electrode(svg, cx, cy, s, color, label){
+ el('rect', { x: cx - 3, y: cy - s*0.5, width: 6, height: s*0.5, fill: color }, svg);
+ el('circle', { cx, cy: cy - s*0.5, r: 5, fill: color }, svg);
+ if (label) txt(svg, cx, cy - s*0.6, 8, '700', color, label);
+ }
+ function sensor(svg, cx, cy, s){
+ const g = el('g', {}, svg);
+ el('rect', { x: cx - s*0.22, y: cy - s*0.16, width: s*0.44, height: s*0.32, rx: 4, fill: 'var(--panel)', stroke: 'var(--focus)', 'stroke-width': 2.4 }, g);
+ el('circle', { cx: cx + s*0.14, cy: cy - s*0.1, r: 3, fill: 'var(--accent2)' }, g);
+ txt(svg, cx, cy + s*0.05, 7.5, '700', 'currentColor', 'SOIL', 'middle');
+ return g;
+ }
+
+ function scene(svg, stage){
+ svg.innerHTML = '';
+ el('rect', { x: 40, y: 250, width: 400, height: 10, fill: 'currentColor', opacity: .3, rx: 3 }, svg);
+ if (stage === 0) {
+ potato(svg, 140, 230, 100);
+ electrode(svg, 118, 230, 70, '#c9812f', 'Cu');
+ electrode(svg, 162, 230, 70, '#7a7f86', 'Zn');
+ el('path', { d: 'M 118 195 L 260 210 M 162 195 L 260 220', stroke: 'currentColor', 'stroke-width': 1.6, 'stroke-dasharray':'3 3' }, svg);
+ sensor(svg, 300, 220, 90);
+ txt(svg, 240, 40, 12, '700', 'currentColor', '"Lithium batteries don\'t offer any of that."');
+ } else if (stage === 1) {
+ potato(svg, 100, 230, 90, '#8a5a2b');
+ potato(svg, 200, 230, 90, '#8a5a2b');
+ sensor(svg, 340, 210, 90);
+ txt(svg, 240, 40, 12, '700', 'var(--accent2)', 'Field trial: 40% longer runtime than lithium.');
+ txt(svg, 240, 60, 9.5, '400', 'var(--ink-dim)', 'twelve test plots, six months');
+ } else if (stage === 2) {
+ potato(svg, 150, 220, 130, '#c9a227');
+ txt(svg, 150, 275, 9.5, '700', 'var(--seal)', '"Chugwater Volt" cultivar');
+ sensor(svg, 340, 210, 90);
+ txt(svg, 240, 40, 12, '700', 'currentColor', 'Nearly 2× the electrolyte output.');
+ } else if (stage === 3) {
+ for (let i=0;i<4;i++) potato(svg, 70 + i*45, 224, 60, '#c9a227');
+ el('rect', { x: 300, y: 150, width: 130, height: 80, rx: 6, fill: 'var(--panel)', stroke: 'var(--focus)', 'stroke-width': 2.6 }, svg);
+ txt(svg, 365, 178, 9.5, '700', 'currentColor', 'PLATTE RIVER');
+ txt(svg, 365, 192, 9.5, '700', 'currentColor', 'POWER CO-OP');
+ txt(svg, 365, 208, 8, '400', 'var(--ink-dim)', '9 hrs fairgrounds reserve');
+ txt(svg, 240, 40, 11, '700', 'currentColor', '40-acre potato field, wired to the grid.');
+ } else if (stage === 4) {
+ for (let i=0;i<8;i++) potato(svg, 50 + i*24, 220 + (i%2)*14, 40, '#c9a227');
+ el('rect', { x: 340, y: 150, width: 100, height: 70, rx: 6, fill: 'var(--panel)', stroke: 'var(--accent2)', 'stroke-width': 2.6 }, svg);
+ txt(svg, 390, 178, 9, '700', 'currentColor', 'TUBER BANK');
+ txt(svg, 390, 194, 8, '400', 'var(--ink-dim)', '60,000 potatoes');
+ txt(svg, 390, 206, 8, '400', 'var(--ink-dim)', 'rotated weekly');
+ txt(svg, 240, 40, 11, '700', 'currentColor', 'Peak-demand reserve, one rotating tuber at a time.');
+ }
+ }
+
+ const HEADLINES = ["University Lab Tests Potato-Based Batteries for Rural Sensor Networks", "Potato Batteries Outlast Lithium Cells in Six-Month Field Trial", "Institute Begins Growing Custom High-Voltage Potato Variety for Battery Use", "Local Utility Proposes Potato Farm as Backup Power Source for County Grid", "Wyoming County Grid Now Runs Partly on Rotating Reserve of Wired Potatoes"];
+ const STAMPS = ["PILOT", "OUTPERFORMING", "NEW CULTIVAR", "PROPOSED RESERVE", "TUBER BANK LIVE"];
+ const CONCLUSION_MSG = "The story has concluded. The county's backup generator is, technically, a very large baked potato.";
+
+ const state = { stage: 0 };
+
+ function say(kind, msg){ const s = document.getElementById('status'); s.className = kind; s.textContent = msg; }
+
+ function paint(){
+ scene(document.getElementById('art'), state.stage);
+ document.getElementById('cap').textContent = HEADLINES[state.stage];
+ document.getElementById('stamp').textContent = STAMPS[state.stage];
+ const btn = document.getElementById('twistBtn');
+ if (state.stage >= HEADLINES.length - 1) { btn.textContent = 'Story Concluded'; btn.disabled = true; }
+ else { btn.textContent = 'Plot Twist!'; btn.disabled = false; }
+ }
+
+ function advance(){
+ if (state.stage >= HEADLINES.length - 1) { say('info', CONCLUSION_MSG); return; }
+ state.stage++;
+ paint();
+ say('ok', `Stage ${state.stage + 1} of ${HEADLINES.length}.`);
+ }
+
+ document.getElementById('twistBtn').addEventListener('click', advance);
+ const art = document.getElementById('art');
+ art.addEventListener('click', advance);
+ art.addEventListener('keydown', (e) => { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); advance(); } });
+
+ document.addEventListener('keydown', (e) => {
+ if (e.metaKey || e.ctrlKey || e.altKey) return;
+ const tag = (e.target.tagName || '').toLowerCase();
+ if (tag === 'input' || tag === 'textarea') return;
+ const k = e.key.toLowerCase();
+ if (k === 'n' || e.key === 'ArrowRight') { e.preventDefault(); advance(); }
+ });
+
+ const themeBtn = document.getElementById('themeBtn');
+ function safeGet(k,d){ try { return localStorage.getItem(k) ?? d; } catch { return d; } }
+ function safeSet(k,v){ try { localStorage.setItem(k,v); } catch {} }
+ let theme = safeGet('p24cartoon.theme', 'auto');
+ function applyTheme(){
+ if (theme === 'dark') { document.documentElement.setAttribute('data-theme','dark'); themeBtn.textContent='Dark mode: on'; themeBtn.setAttribute('aria-pressed','true'); }
+ else { document.documentElement.setAttribute('data-theme','light'); themeBtn.textContent='Dark mode: off'; themeBtn.setAttribute('aria-pressed','false'); }
+ }
+ applyTheme();
+ themeBtn.addEventListener('click', () => { theme = theme === 'dark' ? 'light' : 'dark'; safeSet('p24cartoon.theme', theme); applyTheme(); });
+
+ paint();
+ say('info', 'Case file open. Press N, click the art, or hit Plot Twist to advance the story.');
+})();
+
+</script>
+</body>
+</html>
diff --git a/scripts/cartoon-prompt-bank.mjs b/scripts/cartoon-prompt-bank.mjs
index bbd3571..31d380f 100644
--- a/scripts/cartoon-prompt-bank.mjs
+++ b/scripts/cartoon-prompt-bank.mjs
@@ -1,19 +1,60 @@
-// cartoon-prompt-bank.mjs — the P24 Political Cartoon Desk's rotating idea bank.
-// Every entry is [id, title, brief]. All INVENTED, good-natured, non-partisan —
-// satirizes bureaucracy/institutions in the abstract, never real people/parties.
+// cartoon-prompt-bank.mjs — the P24 Cartoon Desk's rotating idea bank.
+// Every entry is [id, title, brief, category]. All INVENTED, good-natured,
+// non-partisan — satirizes bureaucracy/institutions/small-town life in the
+// abstract, never real people/parties/companies/brands.
+//
+// category is one of: politics | business | entertainment | scitech | sports |
+// weather (matches the P24 story category keys in ../stories-data.js and the
+// manifest.js `section` field cartoons/index.html filters/colors by).
+//
+// PREP ONLY (TK-12142, 2026-09-24): category was just added so daily-cartoon-gen.mjs
+// can carry it through to queue briefs. The SELECTION algorithm in
+// daily-cartoon-gen.mjs is still a flat date-seeded shuffle across the WHOLE bank —
+// it does NOT yet group/weight by category or guarantee category coverage per day.
+// That mix decision (e.g. "1 per category/day" vs "still politics-heavy") is left to
+// a future session per Steve's instruction not to change default daily-batch behavior
+// in this pass. The scheduled job itself also remains un-installed
+// (see build-cartoon-batch.sh header) — nothing here changes live output today.
+//
// Already-built (skip, don't regenerate): seesaw-debate, form-to-request-a-form,
// ship-of-state-committee, law-rube-goldberg (built via Model Arena 2026-09-18/22).
export const CARTOON_BANK = [
- ['committee-clock', 'The Committee That Meets About Meeting', "A single bold SVG cartoon of an INVENTED oversight committee convened solely to schedule the committee that will decide whether to form a committee. A wall clock's hands are made of gavels. Click the gavel-hands to advance the (glacial) meeting; a 'motion to adjourn' button always fails hilariously."],
- ['red-tape-maze', 'The Red Tape Labyrinth', "A three-panel SVG strip: a citizen enters an INVENTED permit office as a straightforward hallway; each panel the hallway visibly grows a new looping detour of literal red tape. A 'shortcut' button always adds MORE tape. Good-natured, invented office."],
- ['budget-pie-fight', 'Who Gets the Last Slice', "A single-image SVG metaphor: an INVENTED town budget as a pie diners keep re-slicing smaller at a banquet table, each guest an invented department mascot. Dragging a slider re-carves the pie and the caption rewrites live. Light/dark toggle."],
- ['filibuster-marathon', 'The Filibuster Marathon', "A three-panel SVG strip about an INVENTED legislator who begins reading a phone book aloud to stall a vote; panel 2 a clock melts Dali-style, panel 3 the whole chamber has fallen asleep standing up. A 'plot twist' button swaps what they're reading (cereal box, assembly instructions, their own bill)."],
- ['pork-barrel-train', 'The Pork Barrel Express', "A single bold SVG illustration: an INVENTED infrastructure bill as a train where each car is a wildly unrelated rider project (a lighthouse in a desert, a ferris wheel). Clicking a car reveals its invented cost. Caption adapts to how many cars are attached."],
- ['gerrymander-jigsaw', 'The Shape of Things to Come', "A three-panel SVG strip where an INVENTED district map is redrawn by a cartoon hand into an increasingly absurd jigsaw creature (a dragon, then a teapot, then a smiley face) while officials insist nothing has changed. Click to advance the redraw; a twist button picks the next shape."],
- ['lobbyist-revolving-door', 'The Revolving Door', "A single SVG scene: an INVENTED capitol building's revolving door where the same invented figure exits as a regulator and re-enters seconds later as a lobbyist, carrying a slightly bigger briefcase each spin. Click to spin the door; a counter tallies revolutions."],
- ['filibuster-vs-cloture', "Cloture o'Clock", "A three-panel SVG strip: an INVENTED senate clock personified, wound up by one side and unwound by the other, ticking backward and forward until it just shrugs. Non-partisan, invented chamber and characters only. Plot-twist button changes the clock's expression."],
- ['earmark-tug-of-war', 'The Earmark Tug-of-War', "A single bold SVG illustration: an INVENTED rope-pull where each team is a caricatured interest group hauling a giant dollar-sign log toward their own tiny truck. Dragging changes who's winning and rewrites the caption. Light/dark toggle."],
- ['commission-turtles', 'Commissions All the Way Down', "A three-panel SVG strip: an INVENTED blue-ribbon commission recommends forming a commission to study its own recommendation, panel by panel nesting turtle-commissions smaller and smaller (a Droste-style recursion of officials). Click to descend a level; plot twist reshuffles which invented mascot appears at the bottom."],
- ['sunset-clause-sundial', 'The Sunset Clause That Never Sets', "A single SVG metaphor: an INVENTED law's 'sunset clause' as a sundial where the shadow keeps getting renewed by an official running out with a bigger gnomon before dusk ever arrives. Interactive: drag the sun, shadow always gets 'extended'."],
- ['quorum-call-hide-seek', 'The Quorum Call', "A three-panel SVG strip: an INVENTED legislative chamber plays hide-and-seek to dodge a quorum call — panel 1 the roll is called, panel 2 desks are empty with little paper cutouts propped in chairs, panel 3 the sergeant-at-arms searches a suspiciously form-shaped closet. Plot twist changes the hiding spot."],
+ // ---- politics (original 12) ----
+ ['committee-clock', 'The Committee That Meets About Meeting', "A single bold SVG cartoon of an INVENTED oversight committee convened solely to schedule the committee that will decide whether to form a committee. A wall clock's hands are made of gavels. Click the gavel-hands to advance the (glacial) meeting; a 'motion to adjourn' button always fails hilariously.", 'politics'],
+ ['red-tape-maze', 'The Red Tape Labyrinth', "A three-panel SVG strip: a citizen enters an INVENTED permit office as a straightforward hallway; each panel the hallway visibly grows a new looping detour of literal red tape. A 'shortcut' button always adds MORE tape. Good-natured, invented office.", 'politics'],
+ ['budget-pie-fight', 'Who Gets the Last Slice', "A single-image SVG metaphor: an INVENTED town budget as a pie diners keep re-slicing smaller at a banquet table, each guest an invented department mascot. Dragging a slider re-carves the pie and the caption rewrites live. Light/dark toggle.", 'politics'],
+ ['filibuster-marathon', 'The Filibuster Marathon', "A three-panel SVG strip about an INVENTED legislator who begins reading a phone book aloud to stall a vote; panel 2 a clock melts Dali-style, panel 3 the whole chamber has fallen asleep standing up. A 'plot twist' button swaps what they're reading (cereal box, assembly instructions, their own bill).", 'politics'],
+ ['pork-barrel-train', 'The Pork Barrel Express', "A single bold SVG illustration: an INVENTED infrastructure bill as a train where each car is a wildly unrelated rider project (a lighthouse in a desert, a ferris wheel). Clicking a car reveals its invented cost. Caption adapts to how many cars are attached.", 'politics'],
+ ['gerrymander-jigsaw', 'The Shape of Things to Come', "A three-panel SVG strip where an INVENTED district map is redrawn by a cartoon hand into an increasingly absurd jigsaw creature (a dragon, then a teapot, then a smiley face) while officials insist nothing has changed. Click to advance the redraw; a twist button picks the next shape.", 'politics'],
+ ['lobbyist-revolving-door', 'The Revolving Door', "A single SVG scene: an INVENTED capitol building's revolving door where the same invented figure exits as a regulator and re-enters seconds later as a lobbyist, carrying a slightly bigger briefcase each spin. Click to spin the door; a counter tallies revolutions.", 'politics'],
+ ['filibuster-vs-cloture', "Cloture o'Clock", "A three-panel SVG strip: an INVENTED senate clock personified, wound up by one side and unwound by the other, ticking backward and forward until it just shrugs. Non-partisan, invented chamber and characters only. Plot-twist button changes the clock's expression.", 'politics'],
+ ['earmark-tug-of-war', 'The Earmark Tug-of-War', "A single bold SVG illustration: an INVENTED rope-pull where each team is a caricatured interest group hauling a giant dollar-sign log toward their own tiny truck. Dragging changes who's winning and rewrites the caption. Light/dark toggle.", 'politics'],
+ ['commission-turtles', 'Commissions All the Way Down', "A three-panel SVG strip: an INVENTED blue-ribbon commission recommends forming a commission to study its own recommendation, panel by panel nesting turtle-commissions smaller and smaller (a Droste-style recursion of officials). Click to descend a level; plot twist reshuffles which invented mascot appears at the bottom.", 'politics'],
+ ['sunset-clause-sundial', 'The Sunset Clause That Never Sets', "A single SVG metaphor: an INVENTED law's 'sunset clause' as a sundial where the shadow keeps getting renewed by an official running out with a bigger gnomon before dusk ever arrives. Interactive: drag the sun, shadow always gets 'extended'.", 'politics'],
+ ['quorum-call-hide-seek', 'The Quorum Call', "A three-panel SVG strip: an INVENTED legislative chamber plays hide-and-seek to dodge a quorum call — panel 1 the roll is called, panel 2 desks are empty with little paper cutouts propped in chairs, panel 3 the sergeant-at-arms searches a suspiciously form-shaped closet. Plot twist changes the hiding spot.", 'politics'],
+
+ // ---- business (stub, 3) ----
+ ['loyalty-tier-avalanche', 'The Loyalty Tier That Ate the Store', "A single bold SVG illustration: an INVENTED corner shop's punch-card loyalty program escalates panel-by-panel into ever-fancier invented tiers (Bronze Regular -> Platinum Concierge -> a velvet rope at the cash register) while the shop itself shrinks around it. Click to advance a tier; a 'redeem' button always reveals a new, bigger requirement.", 'business'],
+ ['fine-print-scroll', 'The Terms That Never End', "A three-panel SVG strip: an INVENTED customer signs up for a simple gadget and a scroll of terms-and-conditions unspools panel by panel until it fills the whole shop and spills into the street. Click to keep scrolling; a 'skip to bottom' button always adds one more clause.", 'business'],
+ ['subscription-hydra', 'The Subscription That Grew Heads', "A single SVG metaphor: an INVENTED streaming-style subscription as a friendly mascot that sprouts a new tiny add-on head (ad-free, offline, bonus lane) every time a user tries to cancel one. Click 'cancel' to watch a new head sprout; caption tallies the monthly total.", 'business'],
+
+ // ---- entertainment (stub, 3) ----
+ ['open-mic-infinity', 'The Open Mic That Never Closes', "A three-panel SVG strip: an INVENTED coffeehouse open-mic night where the sign-up list keeps growing a fresh page every time the last invented performer finishes, the clock visibly loses track of what day it is. Click to add the next act; plot twist swaps the genre placard.", 'entertainment'],
+ ['statue-selfie-line', 'The Line for the Statue for the Line', "A single bold SVG illustration: an INVENTED roadside attraction statue where the queue to photograph it becomes so popular that a second, smaller statue is erected of the queue itself, drawing its own queue. Click to spawn the next mini-statue; caption updates the queue-of-queues count.", 'entertainment'],
+ ['festival-mascot-election', 'The Mascot Who Ran Unopposed', "A three-panel SVG strip: an INVENTED small-town festival's costumed mascot accidentally gets entered on a ballot for 'Grand Marshal' and wins unopposed, then has to be sworn in still wearing the felt head. Plot twist button swaps which invented critter mascot it is.", 'entertainment'],
+
+ // ---- scitech (stub, 3) ----
+ ['smart-fridge-uprising', 'The Fridge That Knew Too Much', "A single SVG scene: an INVENTED 'smart' kitchen appliance's notification panel grows a new unsolicited suggestion bubble every time it's tapped, eventually dwarfing the fridge itself in a tower of sticky-note-style alerts. Click to dismiss one; a new, weirder one always appears.", 'scitech'],
+ ['captcha-escalation', "The Robot Who Couldn't Prove It", "A three-panel SVG strip: an INVENTED helpful household robot is asked to prove it's not a robot by an increasingly absurd series of invented CAPTCHA challenges (traffic lights, crosswalks, a hand-drawn maze) that get harder each panel. Plot twist swaps the challenge type.", 'scitech'],
+ ['server-room-thermostat-war', 'The Thermostat Standoff', "A single bold SVG illustration: an INVENTED office's server room and its break room silently battle over one shared thermostat dial, each side's sticky-note demands stacking higher. Dragging the dial re-labels who's currently 'winning.'", 'scitech'],
+
+ // ---- sports (stub, 3) ----
+ ['tiebreaker-overtime-spiral', "The Game That Wouldn't End", "A three-panel SVG strip: an INVENTED rec-league match keeps going to sudden-death overtime so many times the scoreboard runs out of digits and switches to Roman numerals. Click to add another overtime; plot twist swaps which invented sport it is (kickball, bocce, cornhole).", 'sports'],
+ ['bleacher-superstition-tower', 'The Lucky Seat', "A single SVG metaphor: an INVENTED fan's 'lucky seat' ritual grows a new invented superstition item (rally cap, thunder-stick, borrowed jersey) stacked on the bleacher bench with every win, until the pile is taller than the fan. Click to add the next item.", 'sports'],
+ ['halftime-show-runaway', 'The Halftime Show That Kept Adding Acts', "A three-panel SVG strip: an INVENTED small-stadium halftime show keeps booking 'just one more' invented act between innings until the game itself is delayed by its own entertainment. Plot twist swaps the act (marching kazoo band, dog frisbee team, unicycle relay).", 'sports'],
+
+ // ---- weather (stub, 3) ----
+ ['fog-bank-schedule', 'The Fog That Keeps Office Hours', "A single bold SVG illustration: an INVENTED coastal town's fog bank rolls in and out on a suspiciously punctual 9-to-5 schedule, complete with a little fog 'clock-out' each evening. Click to advance the hour; caption tracks whether the fog is 'on the clock.'", 'weather'],
+ ['snow-drift-property-line', 'The Drift That Knows the Deed', "A three-panel SVG strip: an INVENTED snowstorm politely piles snow exactly along one homeowner's property line and nowhere else, panel by panel building a suspiciously tidy drift wall. Plot twist swaps which invented neighbor benefits.", 'weather'],
+ ['rainbow-permit-office', 'The Rainbow That Needed a Permit', "A single SVG metaphor: an INVENTED town's recurring rainbow is asked to file for a standing-display permit after appearing in the same spot too many afternoons in a row, with a little clipboard-wielding official measuring its arc. Click to advance the review stage.", 'weather'],
];
diff --git a/scripts/daily-cartoon-gen.mjs b/scripts/daily-cartoon-gen.mjs
index c58fe3b..9dbd5a9 100644
--- a/scripts/daily-cartoon-gen.mjs
+++ b/scripts/daily-cartoon-gen.mjs
@@ -66,8 +66,13 @@ function main() {
date: DATE,
generated_at: new Date().toISOString(),
count: picked.length,
- briefs: picked.map(([id, title, brief]) => ({
+ briefs: picked.map(([id, title, brief, category]) => ({
id, title, brief,
+ // category is prep-only carried through as of TK-12142 (2026-09-24) — the
+ // picker above is still a flat shuffle across the whole bank, it does not
+ // group/weight by category. Defaults to 'politics' for any pre-existing
+ // 3-tuple bank entry so this stays backward compatible.
+ category: category || 'politics',
slug: `${DATE.replace(/-/g, '')}-${id}`,
status: 'queued',
})),
← bb599b5 Add P24 cartoon: The Pork Barrel Express (invented infrastru
·
back to Crazy News Channel
·
cartoons gallery: front-page per-category colors + category 90bdc43 →