← back to Crazy News Channel Shadowman
Add P24 cartoon: The Pork Barrel Express (invented infrastructure-bill train satire)
bb599b54eb0d7dd811a09713dce553daf4bd87f3 · 2026-09-24 10:26:35 -0700 · Steve Abrams
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Files touched
A cartoons/20260924-pork-barrel-train.htmlM cartoons/manifest.js
Diff
commit bb599b54eb0d7dd811a09713dce553daf4bd87f3
Author: Steve Abrams <steve@designerwallcoverings.com>
Date: Thu Sep 24 10:26:35 2026 -0700
Add P24 cartoon: The Pork Barrel Express (invented infrastructure-bill train satire)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
---
cartoons/20260924-pork-barrel-train.html | 421 +++++++++++++++++++++++++++++++
cartoons/manifest.js | 10 +
2 files changed, 431 insertions(+)
diff --git a/cartoons/20260924-pork-barrel-train.html b/cartoons/20260924-pork-barrel-train.html
new file mode 100644
index 0000000..5b2331f
--- /dev/null
+++ b/cartoons/20260924-pork-barrel-train.html
@@ -0,0 +1,421 @@
+<!DOCTYPE html>
+<html lang="en">
+<head>
+<meta charset="UTF-8" />
+<meta name="viewport" content="width=device-width, initial-scale=1.0" />
+<title>The Pork Barrel Express — P24 Political Cartoon Desk</title>
+<meta name="description" content="An original invented-satire strip: a fictional infrastructure bill turns into a runaway train as wildly unrelated riders couple on car by car. Click a car for its invented cost." />
+<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}
+.car{cursor:pointer}
+.car:focus-visible{outline:3px solid var(--focus); outline-offset:2px}
+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>The Pork Barrel Express</h1>
+ <p class="dek">An invented infrastructure bill leaves committee as a single train car and picks up wildly unrelated riders at every stop. Click a car to see what it actually costs.</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 · Introduced</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 · Riders Couple On</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 · Fully Loaded</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 · click a car for its invented cost</p>
+</main>
+<footer>H.B. 4417, the Assembly of Larkspur, every official, town, and train car pictured here are invented. No actual legislature, lighthouse, or ferris wheel was ever billed to a taxpayer. 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: 'Comptroller Thistlewood', color: '#b5302f' },
+ { name: 'Assemblymember Grigg', color: '#0f6b52' },
+ { name: 'Track Overseer Pruitt', color: '#c9a227' },
+ { name: 'Whip Delacroix', color: '#6b4fa0' },
+ ];
+ let mascotIdx = 0;
+
+ const BILL_NAME = 'H.B. 4417 — The Interstate Rail Modernization Act';
+ const LEGISLATURE = 'the Assembly of Larkspur';
+
+ // ---- invented cars: the real bill car, riders that couple on by panel, and a rotating pool for twists ----
+ const BASE_CAR = { name: 'The Interstate Rail Modernization Car (the actual bill)', short: 'RAIL CORE', cost: 40000000 };
+ const RIDERS_2 = [
+ { name: 'Desert Lighthouse Car', short: 'LIGHTHOUSE', cost: 12000000 },
+ { name: 'Ferris Wheel Pavilion Car', short: 'FERRIS WHEEL', cost: 8500000 },
+ ];
+ const RIDERS_3 = [
+ { name: 'Commemorative Spoon Museum Car', short: 'SPOON MUSEUM', cost: 3200000 },
+ { name: 'Heated Sidewalk (Zero-Snowfall Town) Car', short: 'HOT SIDEWALK', cost: 6750000 },
+ { name: 'Bridge to the Island (Location TBD) Car', short: 'BRIDGE TBD', cost: 95000000 },
+ ];
+ const TWIST_POOL = [
+ { name: 'Underwater Petting Zoo Car', short: 'PETTING ZOO', cost: 22000000 },
+ { name: 'Solar-Powered Sundial Car', short: 'SUNDIAL', cost: 1400000 },
+ { name: 'Rolling Cheese Wheel Track Car', short: 'CHEESE TRACK', cost: 4800000 },
+ { name: 'Indoor Snowless Ski Slope Car', short: 'SKI SLOPE', cost: 61000000 },
+ { name: 'Museum of Robotic Roosters Car', short: 'ROBO ROOSTERS', cost: 5600000 },
+ { name: 'Scenic Overlook With No View Car', short: 'NO-VIEW LOOKOUT', cost: 900000 },
+ { name: 'Two-Story Garage for One Car Car', short: '1-CAR GARAGE', cost: 3300000 },
+ ];
+
+ const state = { revealed: 0, twists: 0 };
+ const costState = {}; // key `${panelIndex}:${carIdx}` -> boolean, whether the cost callout is open
+ let focusAfterRender = null; // {panel, idx}
+
+ function twistCars(){
+ const arr = [];
+ for (let t = 0; t < state.twists; t++) arr.push(TWIST_POOL[t % TWIST_POOL.length]);
+ return arr;
+ }
+
+ function carsForPanel(i){
+ if (i === 0) return [BASE_CAR];
+ if (i === 1) return [BASE_CAR, ...RIDERS_2];
+ return [BASE_CAR, ...RIDERS_2, ...RIDERS_3, ...twistCars()];
+ }
+
+ function fmtMoney(n){ return '$' + n.toLocaleString('en-US'); }
+ function fmtMoneyShort(n){ return '$' + Math.round(n / 1e6) + 'M'; }
+ function totalCost(cars){ return cars.reduce((s, c) => s + c.cost, 0); }
+
+ function exasperation(n){
+ if (n <= 3) return 'a modest bill so far';
+ if (n <= 5) return 'already sprawling';
+ if (n <= 8) return 'frankly ridiculous';
+ if (n <= 11) return 'a fiscal cry for help';
+ return 'no longer a train, more of a rolling apology';
+ }
+
+ function captionFor(i){
+ const cars = carsForPanel(i);
+ const total = totalCost(cars);
+ if (i === 0) {
+ return `${BILL_NAME} clears committee in ${LEGISLATURE} with exactly one car: the actual train. Running total: ${fmtMoney(total)}.`;
+ }
+ if (i === 1) {
+ const riders = cars.length - 1;
+ return `Before the ink is dry, ${riders} unrelated rider${riders === 1 ? '' : 's'} couple${riders === 1 ? 's' : ''} on. The train now hauls ${cars.length} cars — running total ${fmtMoney(total)}.`;
+ }
+ const twistNote = state.twists > 0 ? ` Plot twist #${state.twists} just tacked on another car.` : '';
+ return `${cars.length} cars now, running total ${fmtMoney(total)} — ${exasperation(cars.length)}.${twistNote}`;
+ }
+
+ // ---- shared flat iconographic drawing helpers ----
+ 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':'10', fill: 'currentColor' }, g);
+ t.textContent = label;
+ }
+ return g;
+ }
+
+ 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+4, 'text-anchor':'middle', 'font-family':'-apple-system,sans-serif', 'font-weight':'800', 'font-size': Math.max(9, r*0.34), fill: color }, g);
+ t.textContent = text;
+ return g;
+ }
+
+ function track(svg){
+ el('line', { x1:10, y1:230, x2:290, y2:230, stroke:'currentColor', 'stroke-width':3, opacity:0.55 }, svg);
+ for (let x = 14; x <= 286; x += 14) {
+ el('line', { x1:x, y1:227, x2:x-4, y2:236, stroke:'currentColor', 'stroke-width':2, opacity:0.35 }, svg);
+ }
+ }
+
+ function drawEngine(svg, x, y, w, h, color){
+ const g = el('g', {}, svg);
+ el('rect', { x, y, width:w, height:h, rx:6, fill:color, 'fill-opacity':0.22, stroke:color, 'stroke-width':3 }, g);
+ el('rect', { x:x+w*0.55, y:y-14, width:w*0.4, height:14, fill:color, 'fill-opacity':0.32, stroke:color, 'stroke-width':2 }, g);
+ el('rect', { x:x+w*0.14, y:y-11, width:6, height:11, fill:color }, g);
+ el('circle', { cx:x+w*0.28, cy:y+h+6, r:6.5, fill:'currentColor', opacity:0.6 }, g);
+ el('circle', { cx:x+w*0.72, cy:y+h+6, r:6.5, fill:'currentColor', opacity:0.6 }, g);
+ }
+
+ function wrapShort(text, group, cx, y, size){
+ const words = text.split(' ');
+ let line1 = '', line2 = '';
+ let i = 0;
+ while (i < words.length && (line1 + words[i]).length <= 9) { line1 += (line1 ? ' ' : '') + words[i]; i++; }
+ line2 = words.slice(i).join(' ');
+ const t = el('text', { x: cx, y, 'text-anchor':'middle', 'font-family':'-apple-system,sans-serif', 'font-weight':'700', 'font-size': size, fill:'currentColor' }, group);
+ const ts1 = el('tspan', { x: cx, dy: 0 }, t);
+ ts1.textContent = line1;
+ if (line2) {
+ const ts2 = el('tspan', { x: cx, dy: size + 1 }, t);
+ ts2.textContent = line2;
+ }
+ }
+
+ function drawCar(svg, x, y, w, h, car, idx, panelIndex, color, positions){
+ const key = panelIndex + ':' + idx;
+ const g = el('g', {
+ class: 'car', tabindex: '0', role: 'button',
+ 'aria-label': `${car.name} — click to reveal its invented cost`,
+ 'aria-pressed': costState[key] ? 'true' : 'false',
+ id: `car-${panelIndex}-${idx}`,
+ }, svg);
+ el('rect', { x, y, width:w, height:h, rx:5, fill:color, 'fill-opacity':0.15, stroke:color, 'stroke-width':2.4 }, g);
+ el('circle', { cx:x+w*0.25, cy:y+h+6, r:6, fill:'currentColor', opacity:0.55 }, g);
+ el('circle', { cx:x+w*0.75, cy:y+h+6, r:6, fill:'currentColor', opacity:0.55 }, g);
+ // $ affordance badge, top-right corner of the car — signals "click me"
+ el('circle', { cx:x+w-7, cy:y+7, r:6.5, fill: color }, g);
+ const dt = el('text', { x:x+w-7, y:y+10, 'text-anchor':'middle', 'font-family':'-apple-system,sans-serif', 'font-weight':'800', 'font-size':'8', fill:'var(--panel)' }, g);
+ dt.textContent = '$';
+ const fontSize = Math.max(5.5, Math.min(8, w*0.17));
+ wrapShort(car.short, g, x + w/2, y + h*0.62, fontSize);
+ g.addEventListener('click', (e) => { e.stopPropagation(); toggleCost(panelIndex, idx); });
+ g.addEventListener('keydown', (e) => {
+ if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); e.stopPropagation(); toggleCost(panelIndex, idx); }
+ });
+ positions.push({ x, y, w, h, car, idx });
+ }
+
+ function drawCallout(svg, pos, panelIndex){
+ const boxW = 118, boxH = 34;
+ const cx = pos.x + pos.w/2;
+ let bx = cx - boxW/2;
+ bx = Math.max(4, Math.min(300 - boxW - 4, bx));
+ let by = pos.y - boxH - 8;
+ if (by < 4) by = pos.y + pos.h + 16;
+ const g = el('g', { class:'cost-callout', style:'pointer-events:none' }, svg);
+ el('rect', { x:bx, y:by, width:boxW, height:boxH, rx:6, fill:'var(--panel)', stroke:'var(--rule)', 'stroke-width':2 }, g);
+ const t1 = el('text', { x:bx+boxW/2, y:by+14, 'text-anchor':'middle', 'font-family':'-apple-system,sans-serif', 'font-size':'8.5', 'font-weight':'700', fill:'currentColor' }, g);
+ t1.textContent = pos.car.short;
+ const t2 = el('text', { x:bx+boxW/2, y:by+27, 'text-anchor':'middle', 'font-family':'-apple-system,sans-serif', 'font-size':'11', 'font-weight':'800', fill:'var(--accent)' }, g);
+ t2.textContent = fmtMoney(pos.car.cost);
+ }
+
+ function toggleCost(panelIndex, idx){
+ const key = panelIndex + ':' + idx;
+ costState[key] = !costState[key];
+ focusAfterRender = { panel: panelIndex, idx };
+ renderPanel(panelIndex);
+ }
+
+ function drawTrain(svg, panelIndex, cars, mascot){
+ svg.innerHTML = '';
+ el('rect', { x:0, y:0, width:300, height:300, fill:'none' }, svg);
+ track(svg);
+
+ const t = el('text', { x:150, y:14, 'text-anchor':'middle', 'font-family':'-apple-system,sans-serif', 'font-size':'9.5', 'font-weight':'700', fill:'currentColor' }, svg);
+ t.textContent = panelIndex === 0 ? BILL_NAME : `${BILL_NAME} + riders`;
+
+ official(svg, 38, 122, 22, mascot.color, mascot.name);
+
+ const total = totalCost(cars);
+ seal(svg, 252, 66, 26, fmtMoneyShort(total), mascot.color);
+
+ const trackLeft = 14, trackRight = 286;
+ const engineW = 42, gap = 4, h = 46, y = 176;
+ const n = cars.length;
+ const avail = (trackRight - trackLeft) - engineW - gap;
+ const carW = Math.max(18, Math.min(44, avail / n - gap));
+
+ let x = trackLeft;
+ drawEngine(svg, x, y, engineW, h, mascot.color);
+ x += engineW + gap;
+
+ const positions = [];
+ cars.forEach((car, idx) => {
+ const color = idx === 0 ? mascot.color : MASCOTS[(mascotIdx + idx) % MASCOTS.length].color;
+ drawCar(svg, x, y, carW, h, car, idx, panelIndex, color, positions);
+ x += carW + gap;
+ });
+
+ positions.forEach((pos) => {
+ if (costState[panelIndex + ':' + pos.idx]) drawCallout(svg, pos, panelIndex);
+ });
+
+ if (focusAfterRender && focusAfterRender.panel === panelIndex) {
+ const target = document.getElementById(`car-${panelIndex}-${focusAfterRender.idx}`);
+ if (target) target.focus();
+ focusAfterRender = null;
+ }
+ }
+
+ const STAMPS = ['FILED', 'COUPLED', 'LOADED'];
+
+ function renderPanel(i){
+ const svg = document.getElementById('art' + i);
+ const mascot = MASCOTS[(mascotIdx + i) % MASCOTS.length];
+ const cars = carsForPanel(i);
+ drawTrain(svg, i, cars, mascot);
+ document.getElementById('cap' + i).textContent = captionFor(i);
+ document.getElementById('stamp' + i).textContent = i < state.revealed ? STAMPS[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 couple on another car.');
+ }
+
+ function twist(){
+ if (state.revealed < 3) { say('err', 'ERROR RIDER-402 · No twist before the train is fully loaded — process all 3 panels first.'); return; }
+ mascotIdx = (mascotIdx + 1) % MASCOTS.length;
+ state.twists++;
+ renderPanel(0); renderPanel(1); renderPanel(2);
+ const cars = carsForPanel(2);
+ say('ok', `Plot twist #${state.twists}: another rider couples onto the back of the train. ${cars.length} cars now, running total ${fmtMoney(totalCost(cars))}.`);
+ }
+
+ 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 6c917a9..c3d7291 100644
--- a/cartoons/manifest.js
+++ b/cartoons/manifest.js
@@ -84,5 +84,15 @@ window.P24_CARTOONS = [
"blurb": "An invented town's budget pie gets re-sliced smaller across three courses while five invented departments hold their forks; drag the slider and watch who ends up with crumbs.",
"thumb": null,
"story_id": null
+ },
+ {
+ "id": "pork-barrel-train",
+ "title": "The Pork Barrel Express",
+ "file": "20260924-pork-barrel-train.html",
+ "created_at": "2026-09-24T22:15:00Z",
+ "category": "Political Cartoon",
+ "blurb": "An invented infrastructure bill leaves committee as a single train car and picks up wildly unrelated riders — a desert lighthouse, a ferris wheel, a spoon museum — at every stop; click a car for its invented cost.",
+ "thumb": null,
+ "story_id": null
}
];
← b0b4970 admin-server: fix 3 verified security/robustness holes
·
back to Crazy News Channel Shadowman
·
P24: add first cartoon for business/entertainment/scitech/sp 3e6ffab →