← back to Crazy News Channel
cartoons/20260924-pork-barrel-train.html
359 lines
<!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." />
<link rel="stylesheet" href="../assets/style.css" />
</head>
<body data-mood="politics" class="cartoon-page cartoon-page-wide">
<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 class="header-actions">
<a class="back" href="index.html">← Cartoon Desk</a>
</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(); } });
}
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>