← back to Crazy News Channel Shadowman
Add P24 cartoon: Cloture o'Clock (invented senate-clock filibuster/cloture satire)
7ddc237131f0b8c62c77ffc86bba3d61f0713398 · 2026-09-24 10:39:12 -0700 · Steve Abrams
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Files touched
A cartoons/20260924-filibuster-vs-cloture.htmlM cartoons/manifest.js
Diff
commit 7ddc237131f0b8c62c77ffc86bba3d61f0713398
Author: Steve Abrams <steve@designerwallcoverings.com>
Date: Thu Sep 24 10:39:12 2026 -0700
Add P24 cartoon: Cloture o'Clock (invented senate-clock filibuster/cloture satire)
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
---
cartoons/20260924-filibuster-vs-cloture.html | 390 +++++++++++++++++++++++++++
cartoons/manifest.js | 10 +
2 files changed, 400 insertions(+)
diff --git a/cartoons/20260924-filibuster-vs-cloture.html b/cartoons/20260924-filibuster-vs-cloture.html
new file mode 100644
index 0000000..6eafd5e
--- /dev/null
+++ b/cartoons/20260924-filibuster-vs-cloture.html
@@ -0,0 +1,390 @@
+<!DOCTYPE html>
+<html lang="en">
+<head>
+<meta charset="UTF-8" />
+<meta name="viewport" content="width=device-width, initial-scale=1.0" />
+<title>Cloture o'Clock — P24 Political Cartoon Desk</title>
+<meta name="description" content="An original invented-satire strip: the Forum of Delegates' ceremonial floor clock is wound tight by one side and wound right back by the other, ticking forward and backward until it just gives up." />
+<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 · Invented Satire</div>
+ <h1>Cloture o'Clock</h1>
+ <p class="dek">The Forum of Delegates' ceremonial floor clock is wound tight by one side and wound right back by the other, ticking forward and backward until it simply gives up.</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>
+ <div class="strip" id="strip">
+ <figure class="panel" data-i="0">
+ <div class="phead"><span>Panel 1 · The Winding</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 · The Unwinding</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 · The Shrug</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/→ next panel · T plot twist · click a panel to advance</p>
+</main>
+<footer>All chambers, floor leaders, and clocks are invented. No actual timepiece 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 chamber + floor leaders — simple flat iconographic mascots, not detailed characters ----
+ const DELEGATES = [
+ { name: 'Floor Leader Bellwether', color: '#b5302f' },
+ { name: 'Floor Leader Cinch', color: '#0f6b52' },
+ ];
+
+ function delegate(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 textline(svg, x, y, str, dim, bold){
+ const t = el('text', { x, y, 'text-anchor':'middle', 'font-family':'-apple-system,sans-serif', 'font-size': bold ? '12' : '11', 'font-weight': bold ? '700' : '400', fill: dim ? 'var(--ink-dim)' : 'currentColor' }, svg);
+ t.textContent = str;
+ return t;
+ }
+
+ // ---- the personified clock's facial expressions — this is what Plot Twist changes ----
+ function faceExpr(parent, cx, cy, r, expr, color){
+ const eyeY = cy - r*0.15;
+ const eyeDX = r*0.32;
+ const eyeR = r*0.09;
+ const mouthY = cy + r*0.4;
+ function normalEyes(){
+ el('circle', { cx: cx-eyeDX, cy: eyeY, r: eyeR, fill: 'currentColor' }, parent);
+ el('circle', { cx: cx+eyeDX, cy: eyeY, r: eyeR, fill: 'currentColor' }, parent);
+ }
+ function spiralEye(ex, ey, rr){
+ let d = `M ${ex} ${ey}`;
+ for (let t = 0.2; t <= 3.2; t += 0.4) {
+ const rad = rr * (t/3.2);
+ const ang = t*4;
+ d += ` L ${(ex+Math.cos(ang)*rad).toFixed(2)} ${(ey+Math.sin(ang)*rad).toFixed(2)}`;
+ }
+ el('path', { d, fill: 'none', stroke: 'currentColor', 'stroke-width': 1.4 }, parent);
+ }
+ function crossEye(ex, ey, rr, inward){
+ el('circle', { cx: ex, cy: ey, r: rr, fill: 'none', stroke: 'currentColor', 'stroke-width': 1.4 }, parent);
+ const px = inward ? ex + rr*0.4 : ex - rr*0.4;
+ el('circle', { cx: px, cy: ey, r: rr*0.4, fill: 'currentColor' }, parent);
+ }
+ function lidHalf(ex, ey, rr){
+ el('path', { d: `M ${ex-rr} ${ey} A ${rr} ${rr} 0 0 1 ${ex+rr} ${ey}`, fill: 'none', stroke: 'currentColor', 'stroke-width': 2, 'stroke-linecap':'round' }, parent);
+ }
+ switch (expr) {
+ case 'eager': // panel 1 default — wide-eyed and racing the clock
+ normalEyes();
+ el('circle', { cx, cy: mouthY, r: r*0.12, fill: 'none', stroke: 'currentColor', 'stroke-width': 2 }, parent);
+ break;
+ case 'queasy': // panel 2 default — being wound the wrong way does not feel great
+ normalEyes();
+ el('path', { d: `M ${cx-r*0.22} ${mouthY} Q ${cx-r*0.1} ${mouthY+6} ${cx} ${mouthY} Q ${cx+r*0.1} ${mouthY-6} ${cx+r*0.22} ${mouthY}`, fill: 'none', stroke: 'currentColor', 'stroke-width': 2, 'stroke-linecap':'round' }, parent);
+ break;
+ case 'flat': // panel 3 default — the shrug face, eyebrows up, mouth level
+ normalEyes();
+ el('line', { x1: cx-r*0.18, y1: mouthY, x2: cx+r*0.18, y2: mouthY, stroke: 'currentColor', 'stroke-width': 2, 'stroke-linecap':'round' }, parent);
+ el('line', { x1: cx-eyeDX-6, y1: eyeY-eyeR-6, x2: cx-eyeDX+6, y2: eyeY-eyeR-10, stroke: 'currentColor', 'stroke-width': 2, 'stroke-linecap':'round' }, parent);
+ el('line', { x1: cx+eyeDX+6, y1: eyeY-eyeR-6, x2: cx+eyeDX-6, y2: eyeY-eyeR-10, stroke: 'currentColor', 'stroke-width': 2, 'stroke-linecap':'round' }, parent);
+ break;
+ case 'dizzy': // Plot Twist option 1
+ spiralEye(cx-eyeDX, eyeY, eyeR*1.7);
+ spiralEye(cx+eyeDX, eyeY, eyeR*1.7);
+ el('path', { d: `M ${cx-r*0.22} ${mouthY} q ${r*0.11} 10 ${r*0.22} 0 q ${r*0.11} 10 ${r*0.22} 0`, fill: 'none', stroke: 'currentColor', 'stroke-width': 2, 'stroke-linecap':'round' }, parent);
+ break;
+ case 'exhausted': // Plot Twist option 2
+ lidHalf(cx-eyeDX, eyeY, eyeR*1.3);
+ lidHalf(cx+eyeDX, eyeY, eyeR*1.3);
+ el('path', { d: `M ${cx-r*0.18} ${mouthY+4} Q ${cx} ${mouthY-4} ${cx+r*0.18} ${mouthY+4}`, fill: 'none', stroke: 'currentColor', 'stroke-width': 2, 'stroke-linecap':'round' }, parent);
+ break;
+ case 'smirking': // Plot Twist option 3
+ normalEyes();
+ el('path', { d: `M ${cx-r*0.2} ${mouthY} Q ${cx} ${mouthY+2} ${cx+r*0.1} ${mouthY-6} Q ${cx+r*0.16} ${mouthY-10} ${cx+r*0.22} ${mouthY-8}`, fill: 'none', stroke: 'currentColor', 'stroke-width': 2, 'stroke-linecap':'round' }, parent);
+ break;
+ case 'crosseyed': // Plot Twist option 4
+ crossEye(cx-eyeDX, eyeY, eyeR*1.3, true);
+ crossEye(cx+eyeDX, eyeY, eyeR*1.3, false);
+ el('circle', { cx, cy: mouthY, r: r*0.1, fill: 'none', stroke: 'currentColor', 'stroke-width': 2 }, parent);
+ break;
+ default:
+ normalEyes();
+ el('line', { x1: cx-r*0.18, y1: mouthY, x2: cx+r*0.18, y2: mouthY, stroke: 'currentColor', 'stroke-width': 2 }, parent);
+ }
+ }
+
+ function clockChar(svg, cx, cy, r, color, hourDeg, minDeg, expr, armsUp){
+ const g = el('g', {}, svg);
+ el('circle', { cx, cy, r, fill: color, 'fill-opacity': '0.14', stroke: color, 'stroke-width': 4 }, g);
+ for (let i=0;i<12;i++){
+ const a = (i/12)*2*Math.PI;
+ const x1 = cx + Math.sin(a)*(r-4), y1 = cy - Math.cos(a)*(r-4);
+ const x2 = cx + Math.sin(a)*(r-11), y2 = cy - Math.cos(a)*(r-11);
+ el('line', { x1: x1.toFixed(2), y1: y1.toFixed(2), x2: x2.toFixed(2), y2: y2.toFixed(2), stroke: color, 'stroke-width': i%3===0 ? 2.4 : 1.4 }, g);
+ }
+ function hand(deg, len, w){
+ const a = deg*Math.PI/180;
+ const x2 = cx + Math.sin(a)*len, y2 = cy - Math.cos(a)*len;
+ el('line', { x1: cx, y1: cy, x2: x2.toFixed(2), y2: y2.toFixed(2), stroke: 'currentColor', 'stroke-width': w, 'stroke-linecap':'round' }, g);
+ }
+ hand(hourDeg, r*0.48, 5);
+ hand(minDeg, r*0.72, 3);
+ el('circle', { cx, cy, r: 4, fill: 'currentColor' }, g);
+ faceExpr(g, cx, cy, r, expr, color);
+ if (armsUp) {
+ [-1, 1].forEach(side => {
+ const sx = cx + side*r;
+ const ex = cx + side*r*1.5;
+ const ey = cy - r*0.9;
+ el('path', { d: `M ${sx} ${cy+r*0.1} Q ${cx+side*r*1.4} ${cy-r*0.1} ${ex} ${ey}`, fill: 'none', stroke: color, 'stroke-width': 7, 'stroke-linecap':'round' }, g);
+ el('circle', { cx: ex, cy: ey, r: 9, fill: color }, g);
+ });
+ }
+ return g;
+ }
+
+ // a small circular winding arrow, clockwise or counter-clockwise, showing which way the key is turning
+ function windArrow(svg, cx, cy, r, color, clockwise){
+ const sweep = clockwise ? 1 : 0;
+ const start = clockwise ? -220 : 40;
+ const end = clockwise ? 40 : -220;
+ function pt(deg){ const a = deg*Math.PI/180; return [cx + Math.cos(a)*r, cy + Math.sin(a)*r]; }
+ const [x1,y1] = pt(start);
+ const [x2,y2] = pt(end);
+ const d = `M ${x1.toFixed(2)} ${y1.toFixed(2)} A ${r} ${r} 0 1 ${sweep} ${x2.toFixed(2)} ${y2.toFixed(2)}`;
+ el('path', { d, fill: 'none', stroke: color, 'stroke-width': 3.5, 'stroke-linecap':'round' }, svg);
+ const tipAngle = (end + (sweep ? 90 : -90)) * Math.PI/180;
+ const size = r*0.45;
+ const b1x = x2 - Math.cos(tipAngle-0.5)*size, b1y = y2 - Math.sin(tipAngle-0.5)*size;
+ const b2x = x2 - Math.cos(tipAngle+0.5)*size, b2y = y2 - Math.sin(tipAngle+0.5)*size;
+ el('polygon', { points: `${x2.toFixed(2)},${y2.toFixed(2)} ${b1x.toFixed(2)},${b1y.toFixed(2)} ${b2x.toFixed(2)},${b2y.toFixed(2)}`, fill: color }, svg);
+ }
+
+ const BASE_EXPR = ['eager', 'queasy', 'flat'];
+ const TWIST_EXPR = ['dizzy', 'exhausted', 'smirking', 'crosseyed'];
+ function currentExpr(i){
+ return state.twists > 0 ? TWIST_EXPR[(state.twists - 1) % TWIST_EXPR.length] : BASE_EXPR[i];
+ }
+
+ function panel0(svg){
+ svg.innerHTML = '';
+ table(svg, 40, 250, 220);
+ delegate(svg, 65, 195, 30, DELEGATES[0].color, DELEGATES[0].name);
+ clockChar(svg, 195, 130, 66, DELEGATES[0].color, 40, 250, currentExpr(0), false);
+ windArrow(svg, 195, 130, 90, DELEGATES[0].color, true);
+ textline(svg, 150, 38, '"Cloture. Now. Before anyone reconsiders."');
+ textline(svg, 150, 56, 'Wound three turns ahead of schedule.', true);
+ }
+
+ function panel1(svg){
+ svg.innerHTML = '';
+ table(svg, 40, 250, 220);
+ delegate(svg, 235, 195, 30, DELEGATES[1].color, DELEGATES[1].name);
+ clockChar(svg, 105, 130, 66, DELEGATES[1].color, 320, 110, currentExpr(1), false);
+ windArrow(svg, 105, 130, 90, DELEGATES[1].color, false);
+ textline(svg, 150, 38, '"Not so fast. We are not nearly finished."');
+ textline(svg, 150, 56, 'Rewound two turns past where it started.', true);
+ }
+
+ function panel2(svg, twists){
+ svg.innerHTML = '';
+ table(svg, 40, 250, 220);
+ delegate(svg, 55, 215, 26, DELEGATES[0].color, DELEGATES[0].name);
+ delegate(svg, 245, 215, 26, DELEGATES[1].color, DELEGATES[1].name);
+ const jitter = (twists % 4) * 22;
+ clockChar(svg, 150, 128, 72, 'var(--seal)', (55+jitter)%360, (285-jitter)%360, currentExpr(2), true);
+ windArrow(svg, 150, 128, 96, DELEGATES[1].color, false);
+ windArrow(svg, 150, 128, 96, DELEGATES[0].color, true);
+ textline(svg, 150, 38, 'Both keys, one clock, zero agreement.');
+ textline(svg, 150, 286, 'Nobody has agreed on what time it is since Tuesday.', true, true);
+ }
+
+ const CAPTIONS = [
+ 'Floor Leader Bellwether cranks the ceremonial chamber clock forward, racing to call the question before anyone reconsiders.',
+ 'Floor Leader Cinch seizes the same key and winds it back, correcting for a debate that has not nearly finished being had.',
+ 'The hands blur past every hour on the dial at once. The clock has stopped keeping anyone’s time but its own.',
+ ];
+
+ const state = { revealed: 0, twists: 0 };
+
+ function renderPanel(i){
+ const svg = document.getElementById('art' + i);
+ if (i === 0) panel0(svg);
+ if (i === 1) panel1(svg);
+ if (i === 2) panel2(svg, state.twists);
+ document.getElementById('cap' + i).textContent = CAPTIONS[i];
+ document.getElementById('stamp' + i).textContent = i <= state.revealed ? ['WOUND','REWOUND','STOPPED'][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);
+ document.getElementById('art'+i).setAttribute('tabindex', i === state.revealed ? '0' : '-1');
+ }
+ 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;
+ document.getElementById('art'+i).setAttribute('tabindex', i === state.revealed ? '0' : '-1');
+ }
+ renderPanel(state.revealed - 1); // the panel that just got processed
+ if (state.revealed < 3) renderPanel(state.revealed); // the newly-active cover's label
+ say('ok', `Panel ${state.revealed} processed.`);
+ syncButtons();
+ if (state.revealed >= 3) say('ok', 'All 3 panels processed. Press Plot Twist! to change the clock’s expression.');
+ }
+
+ function twist(){
+ if (state.revealed < 3) { say('err', 'ERROR CO-409 · Premature twist — process all 3 panels first.'); return; }
+ state.twists++;
+ renderPanel(0); renderPanel(1); renderPanel(2);
+ say('ok', `Plot twist #${state.twists}: the clock’s face goes ${TWIST_EXPR[(state.twists-1)%TWIST_EXPR.length]}.`);
+ }
+
+ 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/manifest.js b/cartoons/manifest.js
index 99b3a9d..e5ab3d1 100644
--- a/cartoons/manifest.js
+++ b/cartoons/manifest.js
@@ -104,5 +104,15 @@ window.P24_CARTOONS = [
"blurb": "An invented legislative chamber plays hide-and-seek to dodge a quorum call, propping paper cutouts in empty chairs while the Sergeant-at-Arms searches a suspiciously form-shaped closet; each plot twist relocates the hiding spot.",
"thumb": null,
"story_id": null
+ },
+ {
+ "id": "cloture-oclock",
+ "title": "Cloture o'Clock",
+ "file": "20260924-filibuster-vs-cloture.html",
+ "created_at": "2026-09-24T23:45:00Z",
+ "category": "Political Cartoon",
+ "blurb": "An invented chamber's ceremonial floor clock is wound tight by one side and wound right back by the other, ticking forward and backward until it just shrugs; each plot twist changes its face.",
+ "thumb": null,
+ "story_id": null
}
];
← 9d290d7 crazy-news-channel: live-verify hardened admin server (vp-en
·
back to Crazy News Channel Shadowman
·
P24: register business/entertainment/scitech/sports/weather dcc569c →