← back to Crazy News Channel

cartoons/20260926-halftime-show-runaway.html

305 lines

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>The Halftime Show That Kept Adding Acts — P24 Cartoon Desk</title>
<meta name="description" content="An original invented-satire strip: a small-stadium halftime show books just one more act between innings until the game is delayed by its own entertainment." />
<link rel="stylesheet" href="../assets/style.css" />
</head>
<body data-mood="sports" class="cartoon-page cartoon-page-wide">
<header>
  <div>
    <div class="kicker">P24 Cartoon Desk &middot; Invented Satire</div>
    <h1>The Halftime Show That Kept Adding Acts</h1>
    <p class="dek">An invented small-stadium entertainment committee books &ldquo;just one more act&rdquo; between innings. The acts keep arriving. The game does not resume.</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 Booking</span><span id="stamp0"></span></div>
      <svg class="art" id="art0" viewBox="0 0 300 300" role="img" tabindex="0" aria-label="Panel 1, not yet revealed."></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 Queue</span><span id="stamp1"></span></div>
      <svg class="art" id="art1" viewBox="0 0 300 300" role="img" tabindex="0" aria-label="Panel 2, not yet revealed."></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 Delay</span><span id="stamp2"></span></div>
      <svg class="art" id="art2" viewBox="0 0 300 300" role="img" tabindex="0" aria-label="Panel 3, not yet revealed."></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">Announce Act 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>The Glenhaven Municipal Field, its entertainment committee, and every act named here are invented. No actual ballgame was delayed. 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 txt(svg, x, y, s, opts){
    opts = opts || {};
    const t = el('text', {
      x: x, y: y, 'text-anchor': opts.anchor || 'middle',
      'font-size': opts.size || 12,
      'font-weight': opts.weight || '400',
      fill: opts.fill || 'currentColor'
    }, svg);
    t.textContent = s;
    return t;
  }
  function reduced(){ return matchMedia('(prefers-reduced-motion: reduce)').matches; }

  // ---- invented acts booked by the entertainment committee ----
  const ACTS = [
    { name: 'Municipal Kazoo Corps',   short: 'kazoo band',       kind: 'kazoo',    color: '#b5302f' },
    { name: 'Airborne Retrievers',     short: 'dog frisbee team', kind: 'frisbee',  color: '#0f6b52' },
    { name: 'Wheelmen’s Relay',   short: 'unicycle relay',   kind: 'unicycle', color: '#6b4fa0' },
  ];
  let actIdx = 0;
  function act(){ return ACTS[actIdx % ACTS.length]; }
  function nextAct(n){ return ACTS[(actIdx + n) % ACTS.length]; }

  /* ---- flat iconographic performers (no real people, no logos) ---- */
  function performer(svg, cx, cy, s, color, kind){
    const g = el('g', {}, svg);
    if (kind === 'frisbee') {
      // quadruped + a disc in the air
      el('ellipse', { cx: cx, cy: cy, rx: s*0.62, ry: s*0.3, fill: color, 'fill-opacity': '0.85' }, g);
      el('circle', { cx: cx + s*0.66, cy: cy - s*0.24, r: s*0.24, fill: color }, g);         // head
      el('path', { d: `M ${cx-s*0.4} ${cy+s*0.26} v ${s*0.45} M ${cx+s*0.34} ${cy+s*0.26} v ${s*0.45}`,
                   stroke: color, 'stroke-width': Math.max(2, s*0.15), 'stroke-linecap': 'round' }, g); // legs
      el('path', { d: `M ${cx-s*0.6} ${cy-s*0.1} q ${-s*0.3} ${-s*0.3} ${-s*0.1} ${-s*0.5}`,
                   fill: 'none', stroke: color, 'stroke-width': Math.max(2, s*0.13), 'stroke-linecap': 'round' }, g); // tail
      el('ellipse', { cx: cx + s*0.2, cy: cy - s*1.15, rx: s*0.42, ry: s*0.14,
                      fill: 'none', stroke: color, 'stroke-width': Math.max(2, s*0.15) }, g); // disc
      return g;
    }
    if (kind === 'unicycle') {
      el('circle', { cx: cx, cy: cy + s*0.62, r: s*0.46, fill: 'none', stroke: color, 'stroke-width': Math.max(2, s*0.16) }, g);
      el('circle', { cx: cx, cy: cy + s*0.62, r: Math.max(1.5, s*0.07), fill: color }, g);    // hub
      el('path', { d: `M ${cx} ${cy+s*0.16} v ${s*0.3}`, stroke: color, 'stroke-width': Math.max(2, s*0.14) }, g); // post
      el('circle', { cx: cx, cy: cy - s*0.62, r: s*0.26, fill: color }, g);                   // head
      el('path', { d: `M ${cx} ${cy-s*0.32} v ${s*0.48}`, stroke: color, 'stroke-width': Math.max(2, s*0.2), 'stroke-linecap':'round' }, g); // torso
      el('path', { d: `M ${cx-s*0.5} ${cy-s*0.2} h ${s} `, stroke: color, 'stroke-width': Math.max(2, s*0.14), 'stroke-linecap':'round' }, g); // arms
      return g;
    }
    // kazoo: marching figure with a short bar at the mouth + a plume hat
    el('circle', { cx: cx, cy: cy - s*0.5, r: s*0.26, fill: color }, g);                      // head
    el('path', { d: `M ${cx-s*0.28} ${cy-s*0.78} h ${s*0.56}`, stroke: color, 'stroke-width': Math.max(2, s*0.18), 'stroke-linecap':'round' }, g); // hat brim
    el('path', { d: `M ${cx} ${cy-s*0.96} v ${s*0.18}`, stroke: color, 'stroke-width': Math.max(2, s*0.13), 'stroke-linecap':'round' }, g);        // plume
    el('path', { d: `M ${cx+s*0.24} ${cy-s*0.5} h ${s*0.4}`, stroke: color, 'stroke-width': Math.max(2, s*0.15), 'stroke-linecap':'round' }, g);  // kazoo
    el('path', { d: `M ${cx} ${cy-s*0.22} v ${s*0.6}`, stroke: color, 'stroke-width': Math.max(2, s*0.22), 'stroke-linecap':'round' }, g);        // torso
    el('path', { d: `M ${cx-s*0.34} ${cy+s*0.82} l ${s*0.34} ${-s*0.4} l ${s*0.34} ${s*0.4}`,
                 fill: 'none', stroke: color, 'stroke-width': Math.max(2, s*0.16), 'stroke-linecap':'round' }, g); // legs mid-march
    return g;
  }

  function ground(svg, y){
    el('path', { d: `M 10 ${y} H 290`, stroke: 'var(--ink-dim)', 'stroke-width': 2, opacity: 0.7 }, svg);
  }

  function board(svg, x, y, w, h, line1, line2, color){
    const g = el('g', {}, svg);
    el('rect', { x, y, width: w, height: h, rx: 4, fill: 'none', stroke: color, 'stroke-width': 3 }, g);
    el('path', { d: `M ${x + w*0.3} ${y+h} v 14 M ${x + w*0.7} ${y+h} v 14`,
                 stroke: color, 'stroke-width': 3 }, g);
    txt(g, x + w/2, y + h*0.42, line1, { size: 11, weight: '800', fill: color });
    txt(g, x + w/2, y + h*0.78, line2, { size: 10, fill: 'var(--ink-dim)' });
    return g;
  }

  function official(svg, cx, cy, r, color){
    const g = el('g', {}, svg);
    el('circle', { cx, cy: cy - r*0.9, r: r*0.4, fill: color }, g);
    el('path', { d: `M ${cx} ${cy-r*0.45} v ${r}`, stroke: color, 'stroke-width': r*0.34, 'stroke-linecap':'round' }, g);
    // clipboard
    el('rect', { x: cx + r*0.42, y: cy - r*0.3, width: r*0.66, height: r*0.86, rx: 2,
                 fill: 'none', stroke: color, 'stroke-width': 2 }, g);
    return g;
  }

  /* ---- Panel 1: the booking ---- */
  function panel0(svg){
    svg.innerHTML = '';
    const a = act();
    board(svg, 92, 18, 116, 40, 'INNING 4 · 2–1', 'NEXT: ONE MORE ACT', 'var(--seal)');
    ground(svg, 246);
    official(svg, 84, 224, 34, 'var(--ink)');
    performer(svg, 196, 208, 34, a.color, a.kind);
    txt(svg, 150, 96, '"We have room for one more act."', { size: 12, weight: '700' });
    txt(svg, 150, 114, 'The committee books the ' + a.short + '.', { size: 11, fill: 'var(--ink-dim)' });
    txt(svg, 196, 266, a.name, { size: 10, fill: 'var(--ink-dim)' });
    svg.setAttribute('aria-label',
      'Panel 1: a stadium scoreboard reads inning four, two to one, next: one more act. ' +
      'An official with a clipboard books the ' + a.short + '.');
  }

  /* ---- Panel 2: the queue keeps growing ---- */
  function panel1(svg, extra){
    svg.innerHTML = '';
    const n = 3 + extra;
    board(svg, 80, 16, 140, 38, 'INTERMISSION ' + n, 'GAME: STILL INNING 4', 'var(--seal)');
    ground(svg, 232);
    const step = 250 / n;
    for (let i = 0; i < n; i++) {
      const a = nextAct(i);
      const cx = 30 + step * i + step/2;
      performer(svg, cx, 200, Math.max(13, 26 - n), a.color, a.kind);
      el('path', { d: `M ${cx} 226 v 8`, stroke: a.color, 'stroke-width': 2, opacity: 0.6 }, svg);
    }
    txt(svg, 150, 90, n + ' acts are now queued on the baseline.', { size: 12, weight: '700' });
    txt(svg, 150, 108, 'Each was booked to follow the previous one.', { size: 11, fill: 'var(--ink-dim)' });
    txt(svg, 150, 262, 'The grounds crew has stopped raking.', { size: 11, fill: 'var(--ink-dim)' });
    svg.setAttribute('aria-label',
      'Panel 2: ' + n + ' invented acts stand queued along the baseline, each booked to follow the last, ' +
      'while the scoreboard still reads inning four.');
  }

  /* ---- Panel 3: the game is delayed by its own entertainment ---- */
  function panel2(svg, extra){
    svg.innerHTML = '';
    const a = act();
    const mins = 40 + extra * 25;
    board(svg, 74, 14, 152, 44, 'GAME DELAYED', 'CAUSE: ENTERTAINMENT', '#b5302f');
    ground(svg, 240);
    // the act, still performing, center field
    performer(svg, 150, 196, 34, a.color, a.kind);
    // the two teams, seated on the steps, waiting
    for (let i = 0; i < 3; i++) {
      const cx = 36 + i * 22;
      el('circle', { cx, cy: 216, r: 6, fill: 'var(--ink-dim)' }, svg);
      el('path', { d: `M ${cx} 224 v 12`, stroke: 'var(--ink-dim)', 'stroke-width': 4, 'stroke-linecap':'round' }, svg);
    }
    for (let i = 0; i < 3; i++) {
      const cx = 220 + i * 22;
      el('circle', { cx, cy: 216, r: 6, fill: 'var(--ink-dim)' }, svg);
      el('path', { d: `M ${cx} 224 v 12`, stroke: 'var(--ink-dim)', 'stroke-width': 4, 'stroke-linecap':'round' }, svg);
    }
    txt(svg, 150, 90, 'Delay: ' + mins + ' minutes and counting.', { size: 12, weight: '800' });
    txt(svg, 150, 108, 'Both dugouts are seated. The ' + a.short + ' is not finished.', { size: 10, fill: 'var(--ink-dim)' });
    txt(svg, 150, 266, '"We can’t stop mid-act," the committee said.', { size: 11, weight: '700' });
    svg.setAttribute('aria-label',
      'Panel 3: the scoreboard reads game delayed, cause: entertainment, at ' + mins + ' minutes. ' +
      'Both dugouts sit waiting while the ' + a.short + ' performs at center field.');
  }

  const CAPTIONS = [
    'The Glenhaven entertainment committee finds an open slot between innings and books an act to fill it.',
    'Every act is booked to follow the one before it, so the baseline queue only ever grows by one.',
    'The game is now delayed by the entertainment scheduled to fill its delays. Nobody will stop mid-act.',
  ];
  const STAMPS = ['BOOKED', 'QUEUED', 'DELAYED'];

  const state = { revealed: 0, twists: 0 };

  function renderPanel(i){
    const svg = document.getElementById('art' + i);
    if (i === 0) panel0(svg);
    // The queue only ever GROWS — never wrap the escalation back to zero,
    // which would have the strip contradict its own premise. Capped at 5 so
    // the baseline row stays legible at 390px.
    const extra = Math.min(state.twists, 5);
    if (i === 1) panel1(svg, extra);
    if (i === 2) panel2(svg, extra);
    document.getElementById('cap' + i).textContent = CAPTIONS[i];
    document.getElementById('stamp' + i).textContent = i < state.revealed ? STAMPS[i] : '';
  }

  function syncCovers(){
    for (let i = 0; i < 3; i++) {
      const cover = document.getElementById('cover' + i);
      cover.hidden = i < state.revealed;
      cover.disabled = i > state.revealed;
      if (i === state.revealed) {
        cover.textContent = i === 0 ? 'Click, tap, or press N to begin' : 'Click, tap, or press N to announce';
      }
      document.getElementById('art' + i).setAttribute('tabindex', i === state.revealed ? '0' : (i < state.revealed ? '0' : '-1'));
    }
  }

  function syncButtons(){
    const nextBtn = document.getElementById('nextBtn');
    if (state.revealed >= 3) { nextBtn.textContent = 'All 3 acts announced ✓'; nextBtn.disabled = true; }
    else { nextBtn.textContent = `Announce Act ${state.revealed + 1}`; nextBtn.disabled = false; }
  }

  function paintAll(){
    for (let i = 0; i < 3; i++) renderPanel(i);
    syncCovers();
    syncButtons();
  }

  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 acts are on the card. Try Plot Twist!'); return; }
    state.revealed++;
    renderPanel(state.revealed - 1);
    syncCovers();
    syncButtons();
    if (state.revealed >= 3) say('ok', 'All 3 panels announced. Press Plot Twist! to swap the headline act.');
    else say('ok', `Act ${state.revealed} announced. The next one is already booked.`);
  }

  function twist(){
    if (state.revealed < 3) { say('err', 'ERROR HT-409 · Premature twist — announce all 3 acts first.'); return; }
    actIdx = (actIdx + 1) % ACTS.length;
    state.twists++;
    paintAll();
    const a = act();
    say('ok', state.twists <= 5
      ? `Plot twist #${state.twists}: the headline act is now the ${a.short}, and one more act joins the queue.`
      : `Plot twist #${state.twists}: the headline act is now the ${a.short}. The baseline is full; the rest wait in the parking lot.`);
  }

  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', 'Program card open. Press N or click Panel 1 to begin.');
})();
</script>
</body>
</html>