← back to Crazy News Channel

cartoons/20260924-filibuster-vs-cloture.html

330 lines

<!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." />
<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>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 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 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 &middot; 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 &middot; 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/&rarr; next panel &middot; T plot twist &middot; 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(); } });
  }


  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>