← back to Crazy News Channel Shadowman

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 &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 class="header-actions">
    <a class="back" href="index.html">&larr; Cartoon Desk</a>
  </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);
      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>