← back to Crazy News Channel

cartoons/20260924-quorum-call-hide-seek.html

270 lines

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>The Quorum Call — P24 Political Cartoon Desk</title>
<meta name="description" content="An original invented-satire strip: an invented legislative chamber plays hide-and-seek to dodge a quorum call, with a new hiding spot each plot twist." />
<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>The Quorum Call</h1>
    <p class="dek">An invented legislative chamber plays hide-and-seek to dodge a quorum call. Nobody has technically left the building.</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 Roll</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 Vacancy</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 Search</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 delegates, clerks, sergeants-at-arms, and closets are invented. No actual legislative body 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 delegates — simple flat iconographic figures, not detailed characters ----
  const MASCOTS = [
    { name: 'Delegate Rourke', color: '#b5302f' },
    { name: 'Delegate Pham', color: '#0f6b52' },
    { name: 'Delegate Struble', color: '#c9a227' },
    { name: 'Delegate Okonkwo', color: '#6b4fa0' },
  ];
  let mascotIdx = 0;

  // ---- invented hiding spots — cycled by the Plot Twist button ----
  const HIDING_SPOTS = [
    { name: 'the Supply Closet', narrow: false },
    { name: 'the Cloak Room', narrow: false },
    { name: 'the Recording Booth', narrow: true },
    { name: 'a Very Narrow Broom Closet', narrow: true },
  ];
  let hideIdx = 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 desk(svg, x, y, w){
    el('rect', { x, y, width: w, height: 10, fill: 'currentColor', opacity: 0.5, rx: 2 }, svg);
    el('rect', { x: x+6, y: y+10, width: 6, height: 22, fill:'currentColor', opacity:0.35 }, svg);
    el('rect', { x: x+w-12, y: y+10, width: 6, height: 22, fill:'currentColor', opacity:0.35 }, svg);
  }

  function podium(svg, cx, cy){
    el('rect', { x: cx-16, y: cy-6, width: 32, height: 40, fill:'none', stroke:'currentColor', 'stroke-width':2.5, rx:3 }, svg);
    el('rect', { x: cx-20, y: cy-14, width: 40, height: 10, fill:'currentColor', opacity:0.5, rx:2 }, svg);
  }

  // a paper cutout propped in a chair — the classic "gone but not really" decoy
  function cutout(svg, cx, cy, color){
    const g = el('g', {}, svg);
    el('rect', { x: cx-16, y: cy-2, width: 32, height: 38, fill: 'none', stroke: color, 'stroke-width': 2, 'stroke-dasharray':'3 3', rx: 4 }, g);
    el('circle', { cx, cy: cy-14, r: 10, fill:'none', stroke: color, 'stroke-width':2, 'stroke-dasharray':'3 3' }, g);
    // two dot eyes drawn on, like a kid's paper mask
    el('circle', { cx: cx-3, cy: cy-15, r: 1.2, fill: color }, g);
    el('circle', { cx: cx+3, cy: cy-15, r: 1.2, fill: color }, g);
    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+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);
    podium(svg, 150, 70);
    desk(svg, 50, 170, 60); desk(svg, 130, 170, 60); desk(svg, 210, 170, 60);
    desk(svg, 50, 220, 60); desk(svg, 130, 220, 60); desk(svg, 210, 220, 60);
    official(svg, 80, 155, 20, mascot.color, null);
    official(svg, 240, 155, 20, MASCOTS[(mascotIdx+1)%MASCOTS.length].color, null);
    official(svg, 160, 205, 20, MASCOTS[(mascotIdx+2)%MASCOTS.length].color, null);
    const b = el('text', { x:150, y:36, 'text-anchor':'middle', 'font-family':'-apple-system,sans-serif', 'font-size':'13', 'font-weight':'700', fill:'currentColor' }, svg);
    b.textContent = '"Calling the roll —"';
    const b2 = el('text', { x:150, y:284, 'text-anchor':'middle', 'font-family':'-apple-system,sans-serif', 'font-size':'11', fill:'var(--ink-dim)' }, svg);
    b2.textContent = 'The Clerk of ' + CHAMBER + ' begins the count.';
  }

  function panel1(svg, mascot){
    svg.innerHTML = '';
    podium(svg, 150, 60);
    desk(svg, 50, 170, 60); desk(svg, 130, 170, 60); desk(svg, 210, 170, 60);
    desk(svg, 50, 220, 60); desk(svg, 130, 220, 60); desk(svg, 210, 220, 60);
    // every seat is now a cutout — the tell of panel 2
    cutout(svg, 80, 150, mascot.color);
    cutout(svg, 160, 150, MASCOTS[(mascotIdx+1)%MASCOTS.length].color);
    cutout(svg, 240, 150, MASCOTS[(mascotIdx+2)%MASCOTS.length].color);
    cutout(svg, 160, 200, MASCOTS[(mascotIdx+3)%MASCOTS.length].color);
    const b = el('text', { x:150, y:36, 'text-anchor':'middle', 'font-family':'-apple-system,sans-serif', 'font-size':'12', 'font-weight':'700', fill:'currentColor' }, svg);
    b.textContent = '"...present?"';
    const b2 = el('text', { x:150, y:270, 'text-anchor':'middle', 'font-family':'-apple-system,sans-serif', 'font-size':'11', fill:'var(--ink-dim)' }, svg);
    b2.textContent = 'Every chair answers. Nobody sitting in one does.';
  }

  function panel2(svg, mascot, spot){
    svg.innerHTML = '';
    // a sergeant-at-arms, mid-search
    official(svg, 90, 160, 26, mascot.color, 'Sergeant-at-Arms');
    // the closet — a door drawn with a suspicious person-shaped panel line, narrower for the "very narrow" spots
    const cx = 205, top = 60, bottom = 250;
    const halfTop = spot.narrow ? 32 : 42;
    const halfMid = spot.narrow ? 16 : 30;
    const g = el('g', {}, svg);
    el('rect', { x: cx-halfTop, y: top, width: halfTop*2, height: bottom-top, fill:'none', stroke:'currentColor', 'stroke-width':3, rx:4 }, g);
    el('circle', { cx: cx+halfTop-8, cy: (top+bottom)/2, r: 3, fill:'currentColor' }, g); // door handle
    // faint person-shaped bulge outline behind the door — "suspiciously form-shaped"
    el('path', {
      d: `M ${cx} ${top+18} q ${halfMid*0.9} 2 ${halfMid*0.9} ${halfMid*1.1} l 0 ${bottom-top-halfMid*2.6} q 0 ${halfMid*0.9} -${halfMid*0.9} ${halfMid*0.9} q -${halfMid*0.9} 0 -${halfMid*0.9} -${halfMid*0.9} l 0 -${bottom-top-halfMid*2.6} q 0 -${halfMid*1.05} ${halfMid*0.9} -${halfMid*1.05} z`,
      fill: mascot.color, 'fill-opacity':'0.16', stroke: mascot.color, 'stroke-width':1.4, 'stroke-dasharray':'2 3'
    }, g);
    const label = el('text', { x: cx, y: bottom+16, 'text-anchor':'middle', 'font-family':'-apple-system,sans-serif', 'font-size':'10', 'font-weight':'700', fill:'var(--ink-dim)' }, svg);
    label.textContent = spot.name.toUpperCase();
    seal(svg, 205, 40, 24, '?', mascot.color);
    const b = el('text', { x:150, y:284, 'text-anchor':'middle', 'font-family':'-apple-system,sans-serif', 'font-size':'11', 'font-weight':'700', fill:'currentColor' }, svg);
    b.textContent = 'That door is not usually shaped like that.';
  }

  const CHAMBER = 'the Assembly of the Provisional Compact';

  const CAPTIONS = [
    'The Clerk of ' + CHAMBER + ' opens the roll call for an invented quorum vote.',
    'Every desk answers present. Every desk is lying — the members left paper stand-ins propped in the chairs.',
    'The Sergeant-at-Arms narrows it down to one door that is doing an unconvincing job of looking empty.',
  ];

  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, HIDING_SPOTS[hideIdx]);
    document.getElementById('cap' + i).textContent = i === 2
      ? CAPTIONS[2] + ' Current suspect: ' + HIDING_SPOTS[hideIdx].name + '.'
      : CAPTIONS[i];
    document.getElementById('stamp' + i).textContent = i <= state.revealed ? ['CALLED','VACANT','SEARCHING'][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 relocate the hiding spot.');
  }

  function twist(){
    if (state.revealed < 3) { say('err', 'ERROR QC-409 · Premature twist — process all 3 panels first.'); return; }
    mascotIdx = (mascotIdx + 1) % MASCOTS.length;
    hideIdx = (hideIdx + 1) % HIDING_SPOTS.length;
    state.twists++;
    renderPanel(0); renderPanel(1); renderPanel(2);
    say('ok', `Plot twist #${state.twists}: the hiding spot has relocated to ${HIDING_SPOTS[hideIdx].name}.`);
  }

  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>