[object Object]

← back to Crazy News Channel

P24 Political Cartoon Desk: gallery archive, daily prompt generator, first strip (Commissions All the Way Down)

5cd57ad4fa8f91a0e273f671b7f3d84699533789 · 2026-09-24 09:35:42 -0700 · Steve Abrams

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DqUe1Hvqsnrkxpe4R6jrk1

Files touched

Diff

commit 5cd57ad4fa8f91a0e273f671b7f3d84699533789
Author: Steve Abrams <steve@designerwallcoverings.com>
Date:   Thu Sep 24 09:35:42 2026 -0700

    P24 Political Cartoon Desk: gallery archive, daily prompt generator, first strip (Commissions All the Way Down)
    
    Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
    Claude-Session: https://claude.ai/code/session_01DqUe1Hvqsnrkxpe4R6jrk1
---
 cartoons/20260924-commission-turtles.html  | 284 +++++++++++++++++++++++++++++
 cartoons/index.html                        | 139 ++++++++++++++
 cartoons/manifest.json                     |  13 ++
 cartoons/queue/2026-09-24.json             |  49 +++++
 index.html                                 |   2 +
 scripts/build-cartoon-batch.sh             |  87 +++++++++
 scripts/cartoon-prompt-bank.mjs            |  19 ++
 scripts/com.steve.p24-cartoons-daily.plist |  17 ++
 scripts/daily-cartoon-gen.mjs              |  74 ++++++++
 9 files changed, 684 insertions(+)

diff --git a/cartoons/20260924-commission-turtles.html b/cartoons/20260924-commission-turtles.html
new file mode 100644
index 0000000..3ede9e8
--- /dev/null
+++ b/cartoons/20260924-commission-turtles.html
@@ -0,0 +1,284 @@
+<!DOCTYPE html>
+<html lang="en">
+<head>
+<meta charset="UTF-8" />
+<meta name="viewport" content="width=device-width, initial-scale=1.0" />
+<title>Commissions All the Way Down — P24 Political Cartoon Desk</title>
+<meta name="description" content="An original invented-satire strip: a blue-ribbon commission recommends forming a commission to study its own recommendation, nesting smaller and smaller." />
+<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:900px;margin:0 auto;padding:18px 20px}
+.strip{display:grid;grid-template-columns:repeat(3,1fr);gap:14px}
+@media (max-width:720px){ .strip{grid-template-columns:1fr} }
+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:1/1;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:.8rem;color:var(--ink-dim);padding:8px 10px}
+.cover{width:100%;height:100%;background:repeating-linear-gradient(45deg,var(--panel),var(--panel) 10px,color-mix(in srgb, var(--panel) 85%, var(--ink)) 10px,color-mix(in srgb, var(--panel) 85%, var(--ink)) 20px);border:none;display:flex;align-items:center;justify-content:center;font-family:-apple-system,sans-serif;font-size:.85rem;color:var(--ink-dim);cursor:pointer;aspect-ratio:1/1}
+.cover:hover{color:var(--ink)}
+.dock{display:flex;gap:10px;flex-wrap:wrap;align-items:center;margin:16px 0;font-family:-apple-system,sans-serif}
+button.primary,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(--rule)}
+button.primary{background:var(--ink);color:var(--panel)}
+button.twist{background:var(--seal);color:#241c14;border-color:var(--seal)}
+button: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:900px;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; } }
+.seal-anim{transition:transform .3s ease}
+@media (prefers-reduced-motion: reduce){ .seal-anim{transition:none} }
+</style>
+</head>
+<body>
+<header>
+  <div>
+    <div class="kicker">P24 Political Cartoon Desk &middot; Invented Satire</div>
+    <h1>Commissions All the Way Down</h1>
+    <p class="dek">An invented blue-ribbon commission recommends forming a commission to study its own recommendation. It keeps going.</p>
+  </div>
+  <div style="display:flex;flex-direction:column;gap:8px;align-items:flex-end">
+    <a class="back" href="index.html">&larr; Cartoon Desk</a>
+    <button class="theme" id="themeBtn" type="button" aria-pressed="false">Dark mode: off</button>
+  </div>
+</header>
+<main>
+  <div class="strip" id="strip">
+    <figure class="panel" data-i="0">
+      <div class="phead"><span>Panel 1 &middot; The Recommendation</span><span id="stamp0"></span></div>
+      <svg class="art" id="art0" viewBox="0 0 300 300" role="img" tabindex="0"></svg>
+      <button class="cover" id="cover0" type="button">Click, tap, or press N to begin</button>
+      <figcaption id="cap0"></figcaption>
+    </figure>
+    <figure class="panel" data-i="1">
+      <div class="phead"><span>Panel 2 &middot; The Study</span><span id="stamp1"></span></div>
+      <svg class="art" id="art1" viewBox="0 0 300 300" role="img" tabindex="0"></svg>
+      <button class="cover" id="cover1" type="button" disabled>In queue</button>
+      <figcaption id="cap1"></figcaption>
+    </figure>
+    <figure class="panel" data-i="2">
+      <div class="phead"><span>Panel 3 &middot; The Nesting</span><span id="stamp2"></span></div>
+      <svg class="art" id="art2" viewBox="0 0 300 300" role="img" tabindex="0"></svg>
+      <button class="cover" id="cover2" type="button" disabled>In queue</button>
+      <figcaption id="cap2"></figcaption>
+    </figure>
+  </div>
+
+  <div class="dock">
+    <button class="primary" id="nextBtn" type="button">Process Panel 1</button>
+    <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/&rarr; next panel &middot; T plot twist &middot; click a panel to advance</p>
+</main>
+<footer>All commissions, officials, and turtles are invented. No actual blue-ribbon panel was inconvenienced. 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 reduced(){ return matchMedia('(prefers-reduced-motion: reduce)').matches; }
+
+  // ---- invented "official" mascots — simple flat iconographic seals, not detailed characters ----
+  const MASCOTS = [
+    { name: 'Sub-Commissioner Wren', color: '#b5302f' },
+    { name: 'Assistant Undersecretary Pell', color: '#0f6b52' },
+    { name: 'Deputy Envoy Fitch', color: '#c9a227' },
+    { name: 'Acting Liaison Okafor', color: '#6b4fa0' },
+  ];
+  let mascotIdx = 0;
+
+  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); // head
+    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); // shoulders
+    if (label) {
+      const t = el('text', { x: cx, y: cy + r + 16, 'text-anchor':'middle', 'font-family':'-apple-system,sans-serif', 'font-size':'11', fill: 'currentColor' }, g);
+      t.textContent = label;
+    }
+    return g;
+  }
+
+  function table(svg, x, y, w){
+    el('rect', { x, y, width: w, height: 10, fill: 'currentColor', opacity: 0.5, rx: 2 }, svg);
+  }
+
+  function seal(svg, cx, cy, r, text, color){
+    const g = el('g', { transform: `rotate(-12 ${cx} ${cy})` }, svg);
+    el('circle', { cx, cy, r, fill: 'none', stroke: color, 'stroke-width': 4, 'stroke-dasharray':'4 3' }, g);
+    const t = el('text', { x: cx, y: cy+5, 'text-anchor':'middle', 'font-family':'-apple-system,sans-serif', 'font-weight':'800', 'font-size': Math.max(9, r*0.32), fill: color }, g);
+    t.textContent = text;
+    return g;
+  }
+
+  function panel0(svg, mascot){
+    svg.innerHTML = '';
+    el('rect', { x:0, y:0, width:300, height:300, fill:'none' }, svg);
+    table(svg, 60, 218, 180);
+    official(svg, 100, 150, 34, mascot.color, mascot.name);
+    official(svg, 200, 150, 34, MASCOTS[(mascotIdx+1)%MASCOTS.length].color, 'Chairperson');
+    const b = el('text', { x:150, y:60, 'text-anchor':'middle', 'font-family':'-apple-system,sans-serif', 'font-size':'13', 'font-weight':'700', fill:'currentColor' }, svg);
+    b.textContent = '"We recommend forming a commission."';
+    const b2 = el('text', { x:150, y:80, 'text-anchor':'middle', 'font-family':'-apple-system,sans-serif', 'font-size':'11', fill:'var(--ink-dim)' }, svg);
+    b2.textContent = 'to study this recommendation.';
+  }
+
+  function panel1(svg, mascot){
+    svg.innerHTML = '';
+    table(svg, 60, 214, 180);
+    official(svg, 100, 150, 30, mascot.color, mascot.name);
+    seal(svg, 210, 130, 42, 'STUDY\nPENDING', mascot.color);
+    // fix multi-line text (SVG text doesn't wrap \n) -> use tspans
+    const seals = svg.querySelectorAll('text');
+    const last = seals[seals.length-1];
+    if (last && last.textContent.includes('\n')) {
+      const parts = last.textContent.split('\n');
+      last.textContent = '';
+      parts.forEach((p,i) => {
+        const ts = el('tspan', { x: last.getAttribute('x'), dy: i===0?0:12 }, last);
+        ts.textContent = p;
+      });
+    }
+    const b = el('text', { x:150, y:250, 'text-anchor':'middle', 'font-family':'-apple-system,sans-serif', 'font-size':'12', fill:'currentColor' }, svg);
+    b.textContent = 'A second commission is formed to study the first.';
+  }
+
+  function panel2(svg, mascot, depth){
+    svg.innerHTML = '';
+    depth = depth || 3;
+    function nest(cx, cy, r, d, color){
+      if (d <= 0 || r < 14) return;
+      el('circle', { cx, cy, r, fill:'none', stroke: color, 'stroke-width': 2.4 }, svg);
+      if (d === depth) {
+        official(svg, cx, cy - r*0.55, Math.max(10, r*0.22), color, null);
+      }
+      nest(cx, cy, r*0.62, d-1, d%2===0 ? mascot.color : MASCOTS[(mascotIdx+2)%MASCOTS.length].color);
+    }
+    nest(150, 150, 110, depth, mascot.color);
+    const b = el('text', { x:150, y:280, 'text-anchor':'middle', 'font-family':'-apple-system,sans-serif', 'font-size':'12', 'font-weight':'700', fill:'currentColor' }, svg);
+    b.textContent = 'Nobody has left the room since March.';
+  }
+
+  const CAPTIONS = [
+    'A blue-ribbon commission convenes to consider an invented civic question, and promptly recommends further study.',
+    'The study commission is seated. Its first act is to form a commission to review its charter.',
+    'Depth of the nesting is now uncountable by anyone still in the building.',
+  ];
+
+  const state = { revealed: 0, twists: 0 };
+
+  function renderPanel(i){
+    const svg = document.getElementById('art' + i);
+    const mascot = MASCOTS[(mascotIdx + i) % MASCOTS.length];
+    if (i === 0) panel0(svg, mascot);
+    if (i === 1) panel1(svg, mascot);
+    if (i === 2) panel2(svg, mascot, 3 + (state.twists % 3));
+    document.getElementById('cap' + i).textContent = CAPTIONS[i];
+    document.getElementById('stamp' + i).textContent = i <= state.revealed ? ['RECEIVED','REFERRED','NESTED'][i] : '';
+    const cover = document.getElementById('cover' + i);
+    cover.hidden = i <= state.revealed - 1 || (i === state.revealed);
+    if (i === state.revealed) { cover.hidden = false; cover.disabled = false; cover.textContent = i === 0 ? 'Click, tap, or press N to begin' : 'Click, tap, or press N to process'; }
+  }
+
+  function paintAll(){ for (let i=0;i<3;i++) renderPanel(i); syncButtons(); }
+
+  function syncButtons(){
+    const nextBtn = document.getElementById('nextBtn');
+    if (state.revealed >= 3) { nextBtn.textContent = 'All 3 panels processed ✓'; nextBtn.disabled = true; }
+    else { nextBtn.textContent = `Process Panel ${state.revealed+1}`; nextBtn.disabled = false; }
+  }
+
+  function say(kind, msg){
+    const s = document.getElementById('status');
+    s.className = kind; s.textContent = msg;
+  }
+
+  function next(){
+    if (state.revealed >= 3) { say('info', 'All 3 panels are already on file. Try Plot Twist!'); return; }
+    state.revealed++;
+    for (let i=0;i<3;i++) {
+      const cover = document.getElementById('cover'+i);
+      cover.hidden = i < state.revealed;
+      cover.disabled = i > state.revealed;
+    }
+    renderPanel(state.revealed - 1);
+    say('ok', `Panel ${state.revealed} processed.`);
+    syncButtons();
+    if (state.revealed >= 3) say('ok', 'All 3 panels processed. Press Plot Twist! to nest it deeper.');
+  }
+
+  function twist(){
+    if (state.revealed < 3) { say('err', 'ERROR PT-409 · Premature twist — process all 3 panels first.'); return; }
+    mascotIdx = (mascotIdx + 1) % MASCOTS.length;
+    state.twists++;
+    renderPanel(0); renderPanel(1); renderPanel(2);
+    say('ok', `Plot twist #${state.twists}: a new official is on the case, and the nesting goes one level deeper.`);
+  }
+
+  document.getElementById('nextBtn').addEventListener('click', next);
+  document.getElementById('twistBtn').addEventListener('click', twist);
+  for (let i=0;i<3;i++) {
+    document.getElementById('cover'+i).addEventListener('click', () => { if (i === state.revealed) next(); });
+    document.getElementById('art'+i).addEventListener('click', () => { if (i === state.revealed) next(); });
+    document.getElementById('art'+i).addEventListener('keydown', (e) => { if ((e.key==='Enter'||e.key===' ') && i===state.revealed) { e.preventDefault(); next(); } });
+  }
+
+  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(); });
+
+  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(); next(); }
+    else if (k === 't') { e.preventDefault(); twist(); }
+  });
+
+  paintAll();
+  say('info', 'Case file open. Press N or click Panel 1 to begin.');
+})();
+</script>
+</body>
+</html>
diff --git a/cartoons/index.html b/cartoons/index.html
new file mode 100644
index 0000000..0a9e68a
--- /dev/null
+++ b/cartoons/index.html
@@ -0,0 +1,139 @@
+<!DOCTYPE html>
+<html lang="en">
+<head>
+<meta charset="UTF-8" />
+<meta name="viewport" content="width=device-width, initial-scale=1.0" />
+<title>P24 Political Cartoon Desk — PANDEMONIUM-24</title>
+<meta name="description" content="The P24 Political Cartoon Desk archive — original invented satire strips, updated daily." />
+<style>
+:root {
+  --bg: #2a0b0b; --bg2: #4a0f0f; --panel: #3a1212; --panel-border: #ff3b3b55;
+  --accent: #ff3b3b; --accent2: #ffb703; --text: #ffe9e9; --text-dim: #ffd7d7aa;
+  --chrome: #220808; --focus: #ffd166; --card-w: 300px;
+}
+*,*::before,*::after{box-sizing:border-box}
+html{overflow-x:hidden}
+[hidden]{display:none!important}
+body{
+  margin:0; min-height:100vh; font-family:-apple-system,"Segoe UI",Helvetica,Arial,sans-serif;
+  color:var(--text); background:radial-gradient(circle at 20% -10%, var(--bg2), var(--bg) 60%);
+  line-height:1.4;
+}
+a{color:var(--accent2)}
+h1,h2,p{margin:0}
+:focus-visible{outline:3px solid var(--focus); outline-offset:2px}
+.chrome{background:var(--chrome); border-bottom:3px solid var(--accent); padding:14px 20px; display:flex; align-items:center; gap:14px; flex-wrap:wrap}
+.logo-badge{background:var(--accent); color:var(--chrome); font-weight:900; padding:4px 10px; border-radius:4px; letter-spacing:.05em}
+.chrome h1{font-size:1.15rem; letter-spacing:.02em}
+.chrome a.home{color:var(--text-dim); text-decoration:none; font-size:.85rem; border:1px solid var(--panel-border); padding:6px 10px; border-radius:6px}
+.chrome a.home:hover{color:var(--text)}
+main{max-width:1200px; margin:0 auto; padding:20px}
+.dek{color:var(--text-dim); max-width:60ch; margin-bottom:18px}
+.controls{display:flex; gap:16px; flex-wrap:wrap; align-items:center; background:var(--panel); border:1px solid var(--panel-border); border-radius:10px; padding:12px 16px; margin-bottom:20px}
+.controls label{font-size:.8rem; color:var(--text-dim); display:flex; gap:8px; align-items:center}
+.controls select, .controls input[type=range]{accent-color:var(--accent)}
+.controls select{background:var(--chrome); color:var(--text); border:1px solid var(--panel-border); border-radius:6px; padding:4px 8px}
+#count{margin-left:auto; font-size:.8rem; color:var(--text-dim)}
+.grid{display:grid; grid-template-columns:repeat(auto-fill, minmax(var(--card-w), 1fr)); gap:18px}
+.card{background:var(--panel); border:1px solid var(--panel-border); border-radius:12px; overflow:hidden; display:flex; flex-direction:column; text-decoration:none; color:var(--text); transition:transform .15s ease, border-color .15s ease}
+.card:hover, .card:focus-visible{transform:translateY(-3px); border-color:var(--accent)}
+.card .thumb{aspect-ratio:4/3; background:var(--chrome) center/cover no-repeat; display:flex; align-items:center; justify-content:center; font-size:2.4rem}
+.card .body{padding:12px 14px; display:flex; flex-direction:column; gap:6px}
+.card .title{font-weight:700; font-size:1rem}
+.card .meta{font-size:.75rem; color:var(--text-dim)}
+.card .blurb{font-size:.85rem; color:var(--text-dim)}
+.empty{background:var(--panel); border:1px dashed var(--panel-border); border-radius:12px; padding:40px 20px; text-align:center; color:var(--text-dim)}
+.empty a{display:inline-block; margin-top:10px}
+footer{max-width:1200px; margin:30px auto; padding:0 20px 30px; color:var(--text-dim); font-size:.75rem}
+@media (prefers-reduced-motion: reduce){ .card{transition:none} }
+</style>
+</head>
+<body>
+<header class="chrome">
+  <span class="logo-badge" aria-hidden="true">P24</span>
+  <h1>Political Cartoon Desk</h1>
+  <a class="home" href="../index.html">&larr; Back to PANDEMONIUM-24</a>
+</header>
+<main>
+  <p class="dek">Original, invented editorial satire — bureaucracy, committees, and the occasional pigeon director. New strips added daily. All persons, departments, and forms are fictional.</p>
+
+  <div class="controls" role="group" aria-label="Sort and display density">
+    <label for="sortSel">Sort
+      <select id="sortSel">
+        <option value="newest">Newest</option>
+        <option value="oldest">Oldest</option>
+        <option value="title">Title A&rarr;Z</option>
+      </select>
+    </label>
+    <label for="density">Density
+      <input type="range" id="density" min="220" max="420" step="20" value="300" aria-valuetext="card width">
+    </label>
+    <span id="count"></span>
+  </div>
+
+  <div class="grid" id="grid" role="list"></div>
+  <div class="empty" id="empty" hidden>
+    <p>No cartoons on file yet — the Desk is between assignments.</p>
+    <a href="../index.html">Read the rest of P24 &rarr;</a>
+  </div>
+</main>
+<footer>All cartoons are invented satire. No actual bureaucrats were inconvenienced. Part of PANDEMONIUM-24.</footer>
+<script>
+(function(){
+  const grid = document.getElementById('grid');
+  const empty = document.getElementById('empty');
+  const sortSel = document.getElementById('sortSel');
+  const density = document.getElementById('density');
+  const count = document.getElementById('count');
+  const LS_SORT = 'p24cartoons.sort', LS_DENSITY = 'p24cartoons.density';
+
+  function safeGet(k, d){ try { return localStorage.getItem(k) ?? d; } catch { return d; } }
+  function safeSet(k, v){ try { localStorage.setItem(k, v); } catch {} }
+
+  sortSel.value = safeGet(LS_SORT, 'newest');
+  density.value = safeGet(LS_DENSITY, '300');
+  document.documentElement.style.setProperty('--card-w', density.value + 'px');
+
+  function fmtDate(iso){
+    try { return new Date(iso).toLocaleDateString(undefined, { year:'numeric', month:'short', day:'numeric' }); }
+    catch { return iso; }
+  }
+
+  let cartoons = [];
+  function render(){
+    const sorted = [...cartoons];
+    if (sortSel.value === 'newest') sorted.sort((a,b)=> (b.created_at||'').localeCompare(a.created_at||''));
+    else if (sortSel.value === 'oldest') sorted.sort((a,b)=> (a.created_at||'').localeCompare(b.created_at||''));
+    else if (sortSel.value === 'title') sorted.sort((a,b)=> a.title.localeCompare(b.title));
+
+    grid.innerHTML = '';
+    empty.hidden = sorted.length !== 0;
+    count.textContent = sorted.length ? `${sorted.length} strip${sorted.length===1?'':'s'} on file` : '';
+    for (const c of sorted) {
+      const a = document.createElement('a');
+      a.className = 'card'; a.href = c.file; a.role = 'listitem';
+      a.innerHTML = `
+        <div class="thumb" style="${c.thumb ? `background-image:url('${c.thumb}')` : ''}" aria-hidden="true">${c.thumb ? '' : '🖋️'}</div>
+        <div class="body">
+          <div class="title">${c.title}</div>
+          <div class="meta" title="${c.created_at||''}">${c.created_at ? fmtDate(c.created_at) : ''}${c.category ? ' · ' + c.category : ''}</div>
+          <div class="blurb">${c.blurb || ''}</div>
+        </div>`;
+      grid.appendChild(a);
+    }
+  }
+
+  sortSel.addEventListener('change', () => { safeSet(LS_SORT, sortSel.value); render(); });
+  density.addEventListener('input', () => {
+    document.documentElement.style.setProperty('--card-w', density.value + 'px');
+    safeSet(LS_DENSITY, density.value);
+  });
+
+  fetch('manifest.json').then(r => r.json()).then(data => {
+    cartoons = data.cartoons || [];
+    render();
+  }).catch(() => { empty.hidden = false; });
+})();
+</script>
+</body>
+</html>
diff --git a/cartoons/manifest.json b/cartoons/manifest.json
new file mode 100644
index 0000000..bb0624f
--- /dev/null
+++ b/cartoons/manifest.json
@@ -0,0 +1,13 @@
+{
+  "cartoons": [
+    {
+      "id": "commission-turtles",
+      "title": "Commissions All the Way Down",
+      "file": "20260924-commission-turtles.html",
+      "created_at": "2026-09-24T17:05:00Z",
+      "category": "Political Cartoon",
+      "blurb": "A blue-ribbon commission recommends forming a commission to study its own recommendation. It keeps going.",
+      "thumb": null
+    }
+  ]
+}
\ No newline at end of file
diff --git a/cartoons/queue/2026-09-24.json b/cartoons/queue/2026-09-24.json
new file mode 100644
index 0000000..0e93094
--- /dev/null
+++ b/cartoons/queue/2026-09-24.json
@@ -0,0 +1,49 @@
+{
+  "date": "2026-09-24",
+  "generated_at": "2026-09-24T16:30:39.105Z",
+  "count": 6,
+  "briefs": [
+    {
+      "id": "commission-turtles",
+      "title": "Commissions All the Way Down",
+      "brief": "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.",
+      "slug": "20260924-commission-turtles",
+      "status": "built"
+    },
+    {
+      "id": "filibuster-marathon",
+      "title": "The Filibuster Marathon",
+      "brief": "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).",
+      "slug": "20260924-filibuster-marathon",
+      "status": "queued"
+    },
+    {
+      "id": "earmark-tug-of-war",
+      "title": "The Earmark Tug-of-War",
+      "brief": "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.",
+      "slug": "20260924-earmark-tug-of-war",
+      "status": "queued"
+    },
+    {
+      "id": "quorum-call-hide-seek",
+      "title": "The Quorum Call",
+      "brief": "A three-panel SVG strip: an INVENTED legislative chamber plays hide-and-seek to dodge a quorum call \u2014 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.",
+      "slug": "20260924-quorum-call-hide-seek",
+      "status": "queued"
+    },
+    {
+      "id": "sunset-clause-sundial",
+      "title": "The Sunset Clause That Never Sets",
+      "brief": "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'.",
+      "slug": "20260924-sunset-clause-sundial",
+      "status": "queued"
+    },
+    {
+      "id": "pork-barrel-train",
+      "title": "The Pork Barrel Express",
+      "brief": "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.",
+      "slug": "20260924-pork-barrel-train",
+      "status": "queued"
+    }
+  ]
+}
\ No newline at end of file
diff --git a/index.html b/index.html
index c526b9a..064a9e9 100644
--- a/index.html
+++ b/index.html
@@ -739,6 +739,8 @@ body.chaos-tier-3 main#channelContainer {
     LIVE&nbsp;<time id="clock">00:00:00</time>
   </div>
 
+  <a href="cartoons/index.html" class="cartoon-desk-link" style="display:flex;align-items:center;gap:6px;color:var(--text-dim);text-decoration:none;font-size:.8rem;border:1px solid var(--panel-border);padding:6px 10px;border-radius:6px;white-space:nowrap">🖋️ Political Cartoon Desk</a>
+
   <div class="controls">
     <button id="klaxonBtn" class="klaxon-btn" type="button" aria-pressed="false">
       🚨 BREAKING ALERT: OFF
diff --git a/scripts/build-cartoon-batch.sh b/scripts/build-cartoon-batch.sh
new file mode 100644
index 0000000..0915324
--- /dev/null
+++ b/scripts/build-cartoon-batch.sh
@@ -0,0 +1,87 @@
+#!/bin/bash
+# build-cartoon-batch.sh — the P24 Political Cartoon Desk's daily batch builder.
+#
+# NOT YET INSTALLED AS A SCHEDULED JOB. This script + its launchd plist are drafted
+# to ~/.claude/yolo-queue/pending-approval/ for Steve's explicit sign-off, per
+# CLAUDE.md's hard gate on "money/spend" + "autonomous claude -p execution" — a
+# recurring unattended job that spawns paid Claude sessions every day is exactly
+# the class that must stay Steve's yes, even after a live "go" on the concept.
+#
+# What it does when run:
+#  1. node scripts/daily-cartoon-gen.mjs   -> picks today's N briefs (default 6),
+#     writing cartoons/queue/<date>.json. $0, local, deterministic.
+#  2. For each brief still 'queued', spawns ONE autonomous `claude -p` session
+#     scoped to that single brief: build a self-contained P24 cartoon strip
+#     (3 panels, click-to-advance, Plot Twist button, non-partisan invented
+#     satire, 390-1440px responsive, keyboard accessible, reduced-motion safe,
+#     zero console errors — same acceptance bar as 20260924-commission-turtles.html),
+#     e2e-test it with Playwright, add it to cartons/manifest.json, and commit.
+#  3. Logs the whole run to yolo/cartoon-batch-log.jsonl.
+#
+# COST: each `claude -p` build is a real paid Anthropic API session. Today's single
+# scoped cartoon (commission-turtles) took roughly a dozen tool calls end-to-end
+# including one fix-and-retest cycle; a batch of 6/day run unattended, without a
+# live contrarian pass on every single one (that would roughly double the cost),
+# is the realistic ongoing shape. Steve should treat the first week as a
+# cost-calibration period and check yolo/cartoon-batch-log.jsonl + his Anthropic
+# usage dashboard before trusting a long-run estimate.
+#
+# Usage once approved: bash scripts/build-cartoon-batch.sh [--count=6] [--dry-run]
+set -uo pipefail
+DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
+cd "$DIR"
+COUNT=6
+DRY=0
+for a in "$@"; do
+  case "$a" in
+    --count=*) COUNT="${a#--count=}" ;;
+    --dry-run) DRY=1 ;;
+  esac
+done
+
+TS="$(/bin/date -u +%Y-%m-%dT%H:%M:%SZ)"
+DATE="$(/bin/date -u +%Y-%m-%d)"
+LOG="$DIR/yolo/cartoon-batch-log.jsonl"
+mkdir -p "$DIR/yolo"
+
+node scripts/daily-cartoon-gen.mjs --count="$COUNT" --date="$DATE" || { echo "$TS generator FAILED" >>"$LOG"; exit 1; }
+
+QFILE="$DIR/cartoons/queue/$DATE.json"
+[ -f "$QFILE" ] || { echo "$TS no queue file for $DATE" >>"$LOG"; exit 1; }
+
+python3 - "$QFILE" "$DRY" "$LOG" "$TS" "$DIR" <<'PYEOF'
+import json, sys, subprocess, os
+qfile, dry, log, ts, root = sys.argv[1:6]
+q = json.load(open(qfile))
+for b in q['briefs']:
+    if b.get('status') != 'queued':
+        continue
+    prompt = (
+        f"Build one P24 (PANDEMONIUM-24) Political Cartoon Desk strip, ticket-tracked "
+        f"(export TK_AGENT=p24-cartoon-batch; tk new for this brief under project crazy-news-channel). "
+        f"TITLE: {b['title']}\nBRIEF: {b['brief']}\n\n"
+        f"Follow the exact style, acceptance bar, and file structure of "
+        f"~/Projects/crazy-news-channel/cartoons/20260924-commission-turtles.html "
+        f"(read it first as the reference implementation): 3-panel click-to-advance SVG strip, "
+        f"a Plot Twist button, non-partisan invented satire only (no real people/parties), "
+        f"390-1440px responsive, keyboard accessible with visible focus, prefers-reduced-motion "
+        f"respected, dark-mode toggle, zero console errors. Save to "
+        f"~/Projects/crazy-news-channel/cartoons/{b['slug']}.html. Playwright-test at 1440/1024/390 "
+        f"+ reduced-motion + dark (require playwright via ~/Projects/animals/node_modules/playwright, "
+        f"launch chromium with channel:'chrome'). Fix any real defects found, then add an entry to "
+        f"~/Projects/crazy-news-channel/cartoons/manifest.json (id, title, file, created_at ISO now, "
+        f"category 'Political Cartoon', blurb, thumb:null) and commit with author "
+        f"steve@designerwallcoverings.com. Do not deploy, push, or touch anything outside this repo."
+    )
+    if dry == "1":
+        print(f"[DRY RUN] would build: {b['id']} — {b['title']}")
+        continue
+    print(f"Building {b['id']} — {b['title']} ...")
+    r = subprocess.run(["claude", "-p", prompt], cwd=root, capture_output=True, text=True, timeout=1800)
+    ok = r.returncode == 0
+    with open(log, "a") as f:
+        f.write(json.dumps({"ts": ts, "id": b["id"], "title": b["title"], "exit": r.returncode, "ok": ok}) + "\n")
+    b['status'] = 'built' if ok else 'failed'
+json.dump(q, open(qfile, 'w'), indent=2)
+PYEOF
+echo "$TS batch complete (count=$COUNT dry=$DRY)" >>"$LOG"
diff --git a/scripts/cartoon-prompt-bank.mjs b/scripts/cartoon-prompt-bank.mjs
new file mode 100644
index 0000000..bbd3571
--- /dev/null
+++ b/scripts/cartoon-prompt-bank.mjs
@@ -0,0 +1,19 @@
+// 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.
+// 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."],
+];
diff --git a/scripts/com.steve.p24-cartoons-daily.plist b/scripts/com.steve.p24-cartoons-daily.plist
new file mode 100644
index 0000000..d09067f
--- /dev/null
+++ b/scripts/com.steve.p24-cartoons-daily.plist
@@ -0,0 +1,17 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
+<plist version="1.0">
+<dict>
+  <key>Label</key><string>com.steve.p24-cartoons-daily</string>
+  <key>ProgramArguments</key>
+  <array>
+    <string>/bin/bash</string>
+    <string>/Users/macstudio3/Projects/crazy-news-channel/scripts/build-cartoon-batch.sh</string>
+    <string>--count=6</string>
+  </array>
+  <key>StartCalendarInterval</key><dict><key>Hour</key><integer>9</integer><key>Minute</key><integer>15</integer></dict>
+  <key>StandardOutPath</key><string>/Users/macstudio3/Projects/crazy-news-channel/yolo/daily-wrapper.log</string>
+  <key>StandardErrorPath</key><string>/Users/macstudio3/Projects/crazy-news-channel/yolo/daily-wrapper.log</string>
+  <key>RunAtLoad</key><false/>
+</dict>
+</plist>
diff --git a/scripts/daily-cartoon-gen.mjs b/scripts/daily-cartoon-gen.mjs
new file mode 100644
index 0000000..caa3e03
--- /dev/null
+++ b/scripts/daily-cartoon-gen.mjs
@@ -0,0 +1,74 @@
+#!/usr/bin/env node
+// daily-cartoon-gen.mjs — picks TODAY's 6 P24 Political Cartoon briefs from the
+// rotating bank (cartoon-prompt-bank.mjs), skipping anything already built
+// (cartoons/manifest.json), and writes the day's queue to cartoons/queue/<date>.json.
+//
+// $0 — pure local selection, no model calls. This is STEP 1 of the pipeline; it
+// only decides WHAT to build today. Building each cartoon (STEP 2) is a separate,
+// heavier operation (currently: an interactive Claude session per brief, same
+// process as TK-12111's form-to-request-a-form) — see README-cartoons.md.
+//
+// Usage: node scripts/daily-cartoon-gen.mjs [--count=6] [--date=YYYY-MM-DD]
+import fs from 'node:fs';
+import path from 'node:path';
+import { fileURLToPath } from 'node:url';
+import { CARTOON_BANK } from './cartoon-prompt-bank.mjs';
+
+const __dirname = path.dirname(fileURLToPath(import.meta.url));
+const ROOT = path.join(__dirname, '..');
+const MANIFEST = path.join(ROOT, 'cartoons', 'manifest.json');
+const QUEUE_DIR = path.join(ROOT, 'cartoons', 'queue');
+
+const args = Object.fromEntries(process.argv.slice(2).map(a => {
+  const m = a.match(/^--([^=]+)=(.*)$/);
+  return m ? [m[1], m[2]] : [a.replace(/^--/, ''), true];
+}));
+const COUNT = parseInt(args.count || '6', 10);
+const DATE = args.date || new Date().toISOString().slice(0, 10);
+
+function loadManifest() {
+  try { return JSON.parse(fs.readFileSync(MANIFEST, 'utf8')); } catch { return { cartoons: [] }; }
+}
+
+function seededShuffle(arr, seed) {
+  // deterministic per-day shuffle so re-running the same date is idempotent
+  let s = seed;
+  const rand = () => { s = (s * 1103515245 + 12345) & 0x7fffffff; return s / 0x7fffffff; };
+  const a = [...arr];
+  for (let i = a.length - 1; i > 0; i--) {
+    const j = Math.floor(rand() * (i + 1));
+    [a[i], a[j]] = [a[j], a[i]];
+  }
+  return a;
+}
+
+function dateSeed(d) {
+  return d.split('-').reduce((acc, n) => acc * 31 + parseInt(n, 10), 7);
+}
+
+function main() {
+  const manifest = loadManifest();
+  const built = new Set(manifest.cartoons.map(c => c.id));
+  const available = CARTOON_BANK.filter(([id]) => !built.has(id));
+  const pool = available.length >= COUNT ? available : CARTOON_BANK; // wrap around once the bank is exhausted
+  const picked = seededShuffle(pool, dateSeed(DATE)).slice(0, COUNT);
+
+  fs.mkdirSync(QUEUE_DIR, { recursive: true });
+  const queueFile = path.join(QUEUE_DIR, `${DATE}.json`);
+  const queue = {
+    date: DATE,
+    generated_at: new Date().toISOString(),
+    count: picked.length,
+    briefs: picked.map(([id, title, brief]) => ({
+      id, title, brief,
+      slug: `${DATE.replace(/-/g, '')}-${id}`,
+      status: 'queued',
+    })),
+  };
+  fs.writeFileSync(queueFile, JSON.stringify(queue, null, 2));
+  console.log(`Queued ${picked.length} cartoon(s) for ${DATE} -> ${path.relative(ROOT, queueFile)}`);
+  for (const b of queue.briefs) console.log(`  - [${b.id}] ${b.title}`);
+  return queue;
+}
+
+main();

← 3a5ce94 Stop tracking Playwright screen-recording video output; igno  ·  back to Crazy News Channel  ·  Fix Cody's 4 verified findings: XSS in cartoon gallery, file 7518fc5 →