← back to Crazy News Channel
cartoons/20260926-bleacher-superstition-tower.html
334 lines
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>The Lucky Seat — P24 Cartoon Desk</title>
<meta name="description" content="An original invented-satire strip: a fan's lucky-seat ritual grows one more superstition object with every win, until the stack on the bleacher bench is taller than the fan." />
<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 · Invented Satire</div>
<h1>The Lucky Seat</h1>
<p class="dek">One invented fan, one bleacher bench, and one rule: every win earns another object that can never be moved.</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 First Win</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 · The Streak</span><span id="stamp1"></span></div>
<svg class="art" id="art1" viewBox="0 0 300 300" role="img" tabindex="0" aria-label="Panel 2, in queue."></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 Tower</span><span id="stamp2"></span></div>
<svg class="art" id="art2" viewBox="0 0 300 300" role="img" tabindex="0" aria-label="Panel 3, in queue."></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">Reveal 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 · once the strip is open, N or a click on Panel 3 stacks the next relic · T plot twist</p>
</main>
<footer>The fan, the bench, the league, and every relic on the pile are invented. No real supporter, team, or seat was consulted. Part of PANDEMONIUM-24.</footer>
<script>
(function(){
'use strict';
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 !!(window.matchMedia && matchMedia('(prefers-reduced-motion: reduce)').matches);
}
// ---- the invented fan (rotated by Plot Twist) ----
const FANS = [
{ name: 'Season-ticket holder Odell Prusk', seat: 'Row F, Seat 12', color: '#b5302f' },
{ name: 'Season-ticket holder Winnie Barlowe', seat: 'Row C, Seat 3', color: '#0f6b52' },
{ name: 'Season-ticket holder Teddy Munce', seat: 'Row K, Seat 20', color: '#6b4fa0' },
{ name: 'Season-ticket holder Junie Halstead', seat: 'Row B, Seat 7', color: '#a35b12' },
];
let fanIdx = 0;
// ---- the relic pile: every entry is an invented superstition object ----
const RELICS = [
{ label: 'rally cap', color: '#b5302f' },
{ label: 'thunder-stick', color: '#c9a227' },
{ label: 'borrowed jersey', color: '#0f6b52' },
{ label: 'unwashed scarf', color: '#6b4fa0' },
{ label: 'third sock', color: '#a35b12' },
{ label: 'folded program', color: '#2f6fb5' },
{ label: 'cold thermos', color: '#b5302f' },
{ label: 'foam finger', color: '#0f6b52' },
{ label: 'dented cowbell', color: '#c9a227' },
{ label: 'seat cushion', color: '#6b4fa0' },
{ label: 'spare whistle', color: '#2f6fb5' },
{ label: 'lucky clipboard', color: '#a35b12' },
{ label: 'second rally cap',color: '#b5302f' },
{ label: 'the old ticket', color: '#0f6b52' },
];
const MAX_RELICS = RELICS.length;
// ---- geometry: one shared stage so all three panels read as one bench ----
const BENCH_Y = 252; // top surface of the bench plank
const BAND_H = 12; // height of one stacked relic
const BAND_GAP= 1;
const STACK_X = 168, STACK_W = 118;
const FAN_X = 84;
const HEAD_TOP = 196; // top of the seated fan's head — the "taller than the fan" line
function bleachers(svg){
// three receding plank rows behind, then the bench being sat on
for (let r = 0; r < 3; r++) {
const y = BENCH_Y - 40 - r * 26;
el('rect', { x: 14 + r * 6, y: y, width: 272 - r * 12, height: 7, rx: 2,
fill: 'currentColor', opacity: String(0.10 + r * 0.02) }, svg);
}
el('rect', { x: 10, y: BENCH_Y, width: 280, height: 12, rx: 3,
fill: 'currentColor', opacity: '0.45' }, svg);
el('rect', { x: 26, y: BENCH_Y + 12, width: 8, height: 30, fill: 'currentColor', opacity: '0.3' }, svg);
el('rect', { x: 266, y: BENCH_Y + 12, width: 8, height: 30, fill: 'currentColor', opacity: '0.3' }, svg);
}
function fan(svg, color, withCap){
const g = el('g', {}, svg);
// seated body: hips on the bench, torso up, knees forward
el('path', { d: `M ${FAN_X - 16} ${BENCH_Y} L ${FAN_X - 16} ${BENCH_Y - 34} Q ${FAN_X} ${BENCH_Y - 46} ${FAN_X + 16} ${BENCH_Y - 34} L ${FAN_X + 16} ${BENCH_Y}`,
fill: color, 'fill-opacity': '0.22', stroke: color, 'stroke-width': 3, 'stroke-linejoin': 'round' }, g);
el('circle', { cx: FAN_X, cy: HEAD_TOP + 14, r: 14, fill: color, 'fill-opacity': '0.2', stroke: color, 'stroke-width': 3 }, g);
if (withCap) { // rally cap: worn backwards, brim pointing away from the field
el('path', { d: `M ${FAN_X - 14} ${HEAD_TOP + 6} Q ${FAN_X} ${HEAD_TOP - 6} ${FAN_X + 14} ${HEAD_TOP + 6} Z`,
fill: color, stroke: color, 'stroke-width': 2 }, g);
el('rect', { x: FAN_X - 27, y: HEAD_TOP + 4, width: 14, height: 4, rx: 2, fill: color }, g);
}
// legs down to the row below
el('path', { d: `M ${FAN_X - 10} ${BENCH_Y} L ${FAN_X - 12} ${BENCH_Y + 30} M ${FAN_X + 10} ${BENCH_Y} L ${FAN_X + 12} ${BENCH_Y + 30}`,
fill: 'none', stroke: color, 'stroke-width': 4, 'stroke-linecap': 'round' }, g);
return g;
}
// one stacked relic band; index 0 sits directly on the bench
function relicBand(svg, i, relic, animate){
const y = BENCH_Y - (i + 1) * (BAND_H + BAND_GAP);
const lean = (i % 2 === 0 ? 1 : -1) * Math.min(6, i * 0.7); // the pile starts to list
const g = el('g', { transform: `translate(${lean} 0)` }, svg);
el('rect', { x: STACK_X, y: y, width: STACK_W, height: BAND_H, rx: 3,
fill: relic.color, 'fill-opacity': '0.28', stroke: relic.color, 'stroke-width': 1.6 }, g);
el('rect', { x: STACK_X + 4, y: y + 3, width: 6, height: BAND_H - 6, rx: 1.5, fill: relic.color }, g);
const t = el('text', { x: STACK_X + 15, y: y + BAND_H - 3.4,
'font-family': '-apple-system, sans-serif', 'font-size': '8',
'font-weight': '600', fill: 'currentColor' }, g);
t.textContent = relic.label;
if (animate && !reduced() && g.animate) {
g.animate([{ transform: `translate(${lean}px, -14px)`, opacity: 0 },
{ transform: `translate(${lean}px, 0px)`, opacity: 1 }],
{ duration: 260, easing: 'ease-out' });
}
return g;
}
function caption(svg, y, text, bold){
const t = el('text', { x: 150, y: y, 'text-anchor': 'middle',
'font-family': '-apple-system, sans-serif',
'font-size': bold ? '12' : '11',
'font-weight': bold ? '700' : '400',
fill: bold ? 'currentColor' : 'var(--ink-dim)' }, svg);
t.textContent = text;
return t;
}
function heightRule(svg, topY){
// The dotted "taller than the fan" datum. It stops short of STACK_X on purpose:
// run it the full width and it strikes through whichever relic label sits at
// head height, which reads as a crossed-out item rather than a measuring line.
el('path', { d: `M 30 ${HEAD_TOP} L ${STACK_X - 8} ${HEAD_TOP}`, fill: 'none', stroke: 'currentColor',
'stroke-width': 1.2, 'stroke-dasharray': '4 4', opacity: '0.55' }, svg);
const t = el('text', { x: 32, y: HEAD_TOP - 5, 'font-family': '-apple-system, sans-serif',
'font-size': '8', 'font-weight': '700', fill: 'var(--ink-dim)' }, svg);
t.textContent = 'top of the fan';
if (topY < HEAD_TOP) {
el('path', { d: `M 292 ${HEAD_TOP} L 292 ${topY}`, fill: 'none', stroke: 'currentColor',
'stroke-width': 1.2, opacity: '0.55' }, svg);
}
}
const state = { revealed: 0, twists: 0, stacked: 8, lastAdded: -1 };
const COUNTS = [1, 4, () => state.stacked];
function countFor(i){ return typeof COUNTS[i] === 'function' ? COUNTS[i]() : COUNTS[i]; }
const CAPTIONS = [
'An invented fan wins one game while wearing a cap backwards, and sets the cap down on the bench rather than risk moving it.',
'Four wins, four objects. Nothing on the bench may be washed, shifted, or taken home, per a rule nobody wrote down.',
'The pile now outranks the fan in both height and seniority. Stacking one more relic is the only permitted response to a win.',
];
const STAMPS = ['ONE WIN', 'ON A STREAK', 'UNMOVABLE'];
function renderPanel(i){
const svg = document.getElementById('art' + i);
svg.innerHTML = '';
const revealedHere = i < state.revealed;
const f = FANS[fanIdx];
bleachers(svg);
fan(svg, f.color, i === 0 ? revealedHere : true);
if (!revealedHere) {
caption(svg, 40, f.seat, false);
caption(svg, 60, i === 0 ? 'Nothing on the bench. Yet.' : 'Reveal the earlier panels first.', false);
svg.setAttribute('aria-label', `Panel ${i + 1}, not yet revealed.`);
} else {
const n = Math.min(countFor(i), MAX_RELICS);
for (let k = 0; k < n; k++) {
relicBand(svg, k, RELICS[k], i === 2 && k === state.lastAdded);
}
const topY = BENCH_Y - n * (BAND_H + BAND_GAP);
if (i === 2) heightRule(svg, topY);
caption(svg, 30, f.seat, false);
caption(svg, i === 2 ? 292 : 48,
i === 0 ? 'One win. One relic.'
: i === 1 ? 'Four wins. Four relics. No washing.'
: `${n} relics stacked. The fan is ${HEAD_TOP > topY ? 'shorter' : 'still taller'} than the pile.`,
i === 2);
svg.setAttribute('aria-label',
`Panel ${i + 1}: ${f.name} on the bleacher bench at ${f.seat}, with ${n} superstition ` +
`object${n === 1 ? '' : 's'} stacked beside them — ${RELICS.slice(0, n).map(r => r.label).join(', ')}. ` +
(topY < HEAD_TOP ? 'The stack is taller than the seated fan.' : 'The stack is not yet taller than the fan.'));
}
document.getElementById('cap' + i).textContent = CAPTIONS[i];
document.getElementById('stamp' + i).textContent = revealedHere ? STAMPS[i] : '';
}
function syncCovers(){
for (let i = 0; i < 3; i++) {
const cover = document.getElementById('cover' + i);
const active = (i === state.revealed);
cover.hidden = i < state.revealed;
cover.disabled = i > state.revealed;
cover.textContent = active ? (i === 0 ? 'Click, tap, or press N to begin' : 'Click, tap, or press N to reveal') : 'In queue';
const art = document.getElementById('art' + i);
// Panel 3 stays the keyboard target once the strip is open — it is where relics stack.
const focusable = active || (state.revealed >= 3 && i === 2);
art.setAttribute('tabindex', focusable ? '0' : '-1');
}
}
function syncButtons(){
const nextBtn = document.getElementById('nextBtn');
if (state.revealed < 3) {
nextBtn.textContent = `Reveal Panel ${state.revealed + 1}`;
nextBtn.disabled = false;
} else if (state.stacked < MAX_RELICS) {
nextBtn.textContent = `Stack relic #${state.stacked + 1}`;
nextBtn.disabled = false;
} else {
nextBtn.textContent = 'Bench at capacity ✓';
nextBtn.disabled = true;
}
}
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;
}
// one action: reveal the next panel, or — once all three are open — stack the next relic
function advance(){
if (state.revealed < 3) {
state.revealed++;
state.lastAdded = -1;
renderPanel(state.revealed - 1);
if (state.revealed < 3) renderPanel(state.revealed);
syncCovers();
syncButtons();
if (state.revealed >= 3) {
say('ok', `All 3 panels open. ${state.stacked} relics on the bench — press N or click Panel 3 to stack the next one.`);
} else {
say('ok', `Panel ${state.revealed} revealed.`);
}
return;
}
if (state.stacked >= MAX_RELICS) {
say('err', 'ERROR LS-451 · Bench at capacity. The league has asked the fan to stop. Try Plot Twist!');
return;
}
state.lastAdded = state.stacked;
state.stacked++;
renderPanel(2);
syncButtons();
say('ok', `Relic #${state.stacked} stacked: ${RELICS[state.stacked - 1].label}. Nothing may be moved.`);
}
function twist(){
if (state.revealed < 3) {
say('err', 'ERROR LS-409 · Premature twist — reveal all 3 panels first.');
return;
}
fanIdx = (fanIdx + 1) % FANS.length;
state.twists++;
state.stacked = 8;
state.lastAdded = -1;
paintAll();
say('ok', `Plot twist #${state.twists}: the seat changed hands. ${FANS[fanIdx].name} inherited the pile, the rule, and ${FANS[fanIdx].seat}.`);
}
document.getElementById('nextBtn').addEventListener('click', advance);
document.getElementById('twistBtn').addEventListener('click', twist);
for (let i = 0; i < 3; i++) {
const hit = () => { if (i === state.revealed || (state.revealed >= 3 && i === 2)) advance(); };
document.getElementById('cover' + i).addEventListener('click', hit);
document.getElementById('art' + i).addEventListener('click', hit);
document.getElementById('art' + i).addEventListener('keydown', (e) => {
if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); hit(); }
});
}
document.addEventListener('keydown', (e) => {
if (e.metaKey || e.ctrlKey || e.altKey) return;
const tag = (e.target.tagName || '').toLowerCase();
if (tag === 'input' || tag === 'textarea' || tag === 'select') return;
// Enter/Space on a focused panel or button is handled by those elements themselves.
if ((e.key === 'Enter' || e.key === ' ') ) return;
const k = e.key.toLowerCase();
if (k === 'n' || e.key === 'ArrowRight') { e.preventDefault(); advance(); }
else if (k === 't') { e.preventDefault(); twist(); }
});
paintAll();
say('info', 'Bench empty. Press N or click Panel 1 to begin the streak.');
})();
</script>
</body>
</html>