← back to Crazy News Channel
cartoons/20260924-commission-turtles.html
232 lines
<!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." />
<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>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 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 · 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 · 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 · 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/→ next panel · T plot twist · 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);
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 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(); } });
}
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>