← back to Crazy News Channel
cartoons/20260926-festival-mascot-election.html
299 lines
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>The Mascot Who Ran Unopposed — P24 Cartoon Desk</title>
<meta name="description" content="An original invented-satire strip: a small-town festival's costumed mascot is accidentally entered on the Grand Marshal ballot, wins unopposed, and is sworn in still wearing the felt head." />
<link rel="stylesheet" href="../assets/style.css" />
</head>
<body data-mood="entertainment" class="cartoon-page cartoon-page-wide">
<header>
<div>
<div class="kicker">P24 Cartoon Desk · Invented Satire</div>
<h1>The Mascot Who Ran Unopposed</h1>
<p class="dek">A festival mascot is typed onto the Grand Marshal ballot by clerical accident, wins with every vote cast, and takes the oath without removing the head.</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 Filing</span><span id="stamp0"></span></div>
<svg class="art" id="art0" viewBox="0 0 300 200" 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 · The Count</span><span id="stamp1"></span></div>
<svg class="art" id="art1" viewBox="0 0 300 200" 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 · The Oath</span><span id="stamp2"></span></div>
<svg class="art" id="art2" viewBox="0 0 300 200" 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">Certify 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 · T plot twist · click a panel to advance</p>
</main>
<footer>Every critter, festival, clerk, and oath here is invented. No real mascot, town, party, or officeholder is depicted. 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(parent, x, y, s, opts){
opts = opts || {};
const t = el('text', {
x: x, y: y, 'text-anchor': opts.anchor || 'middle',
'font-size': opts.size || 11,
'font-weight': opts.weight || '400',
fill: opts.fill || 'currentColor'
}, parent);
t.textContent = s;
return t;
}
function reduced(){ return matchMedia('(prefers-reduced-motion: reduce)').matches; }
/* ---- the invented critters. The Plot Twist button swaps WHICH one
got elected, so the same clerical accident replays with a
different felt head. Flat iconographic shapes only — the joke is
the paperwork, not the costume detail. ---- */
const CRITTERS = [
{ name: 'Chomper the Rivermuskrat', fest: 'Bracken Hollow Silt Festival', ears: 'round', color: 'var(--rule)' },
{ name: 'Pip the Orchard Moth', fest: 'Quillan Falls Blossom Days', ears: 'antenna', color: 'var(--seal)' },
{ name: 'Dozer the Quarry Badger', fest: 'Marlow Gap Gravel Jubilee', ears: 'flat', color: 'var(--rule)' },
{ name: 'Sprocket the Ditch Heron', fest: 'Tumbleside Drainage Carnival', ears: 'crest', color: 'var(--seal)' },
];
let critterIdx = 0;
// A costumed mascot: oversized felt head + small body. cy is the head center.
function mascot(svg, cx, cy, r, critter, opts){
opts = opts || {};
const g = el('g', {}, svg);
const c = critter.color;
// body
el('path', {
d: `M ${cx - r*0.62} ${cy + r*2.1} L ${cx - r*0.44} ${cy + r*0.78} L ${cx + r*0.44} ${cy + r*0.78} L ${cx + r*0.62} ${cy + r*2.1}`,
fill: 'none', stroke: 'currentColor', 'stroke-width': 2.2, 'stroke-linejoin': 'round'
}, g);
// ears / antennae, drawn behind the head
if (critter.ears === 'round') {
el('circle', { cx: cx - r*0.78, cy: cy - r*0.62, r: r*0.34, fill: c, 'fill-opacity': '0.25', stroke: c, 'stroke-width': 2 }, g);
el('circle', { cx: cx + r*0.78, cy: cy - r*0.62, r: r*0.34, fill: c, 'fill-opacity': '0.25', stroke: c, 'stroke-width': 2 }, g);
} else if (critter.ears === 'antenna') {
el('path', { d: `M ${cx - r*0.3} ${cy - r*0.9} Q ${cx - r*0.9} ${cy - r*1.7} ${cx - r*1.1} ${cy - r*1.1}`, fill: 'none', stroke: c, 'stroke-width': 2.2, 'stroke-linecap': 'round' }, g);
el('path', { d: `M ${cx + r*0.3} ${cy - r*0.9} Q ${cx + r*0.9} ${cy - r*1.7} ${cx + r*1.1} ${cy - r*1.1}`, fill: 'none', stroke: c, 'stroke-width': 2.2, 'stroke-linecap': 'round' }, g);
} else if (critter.ears === 'flat') {
el('rect', { x: cx - r*1.12, y: cy - r*0.72, width: r*0.5, height: r*0.34, rx: 3, fill: c, 'fill-opacity': '0.25', stroke: c, 'stroke-width': 2 }, g);
el('rect', { x: cx + r*0.62, y: cy - r*0.72, width: r*0.5, height: r*0.34, rx: 3, fill: c, 'fill-opacity': '0.25', stroke: c, 'stroke-width': 2 }, g);
} else { // crest
el('path', { d: `M ${cx - r*0.4} ${cy - r*0.92} L ${cx} ${cy - r*1.6} L ${cx + r*0.4} ${cy - r*0.92}`, fill: c, 'fill-opacity': '0.25', stroke: c, 'stroke-width': 2, 'stroke-linejoin': 'round' }, g);
}
// the felt head — never removed, in any panel
el('circle', { cx, cy, r, fill: c, 'fill-opacity': '0.18', stroke: c, 'stroke-width': 3 }, g);
el('circle', { cx: cx - r*0.34, cy: cy - r*0.08, r: Math.max(1.6, r*0.11), fill: c }, g);
el('circle', { cx: cx + r*0.34, cy: cy - r*0.08, r: Math.max(1.6, r*0.11), fill: c }, g);
// fixed permanent grin — the costume cannot stop smiling through any of this
el('path', { d: `M ${cx - r*0.4} ${cy + r*0.34} Q ${cx} ${cy + r*0.76} ${cx + r*0.4} ${cy + r*0.34}`, fill: 'none', stroke: c, 'stroke-width': 2.2, 'stroke-linecap': 'round' }, g);
if (opts.hand) { // a raised paw, for oath-taking
el('path', { d: `M ${cx + r*0.44} ${cy + r*1.0} L ${cx + r*1.05} ${cy + r*0.1}`, fill: 'none', stroke: 'currentColor', 'stroke-width': 2.2, 'stroke-linecap': 'round' }, g);
el('circle', { cx: cx + r*1.14, cy: cy - r*0.02, r: r*0.2, fill: c, 'fill-opacity': '0.3', stroke: c, 'stroke-width': 1.8 }, g);
}
if (opts.sash) { // GRAND MARSHAL sash
el('path', { d: `M ${cx - r*0.44} ${cy + r*0.9} L ${cx + r*0.5} ${cy + r*1.7}`, fill: 'none', stroke: c, 'stroke-width': r*0.36, 'stroke-linecap': 'round', 'stroke-opacity': '0.35' }, g);
}
return g;
}
// A clerk: plain official, no costume, so the contrast reads instantly.
function clerk(svg, cx, cy, r, label){
const g = el('g', {}, svg);
el('circle', { cx, cy, r: r*0.42, fill: 'none', stroke: 'currentColor', 'stroke-width': 2.2 }, g);
el('path', {
d: `M ${cx - r*0.62} ${cy + r*1.9} Q ${cx} ${cy + r*0.5} ${cx + r*0.62} ${cy + r*1.9}`,
fill: 'none', stroke: 'currentColor', 'stroke-width': 2.2
}, g);
if (label) txt(g, cx, cy + r*2.4, label, { size: 9, fill: 'var(--ink-dim)' });
return g;
}
function form(svg, x, y, w, h, lines, color){
const g = el('g', {}, svg);
el('rect', { x, y, width: w, height: h, rx: 3, fill: 'none', stroke: 'currentColor', 'stroke-width': 1.6 }, g);
for (let i = 0; i < lines; i++) {
const ly = y + h*0.26 + i * (h*0.52/Math.max(1, lines-1 || 1));
el('line', { x1: x + w*0.14, y1: ly, x2: x + w*0.86, y2: ly, stroke: color, 'stroke-width': 1.4, 'stroke-opacity': '0.65' }, g);
}
return g;
}
function seal(svg, cx, cy, r, lines, color){
const g = el('g', { transform: `rotate(-11 ${cx} ${cy})` }, svg);
el('circle', { cx, cy, r, fill: 'none', stroke: color, 'stroke-width': 3, 'stroke-dasharray': '4 3' }, g);
const size = Math.max(7, r*0.3);
const start = cy + size*0.36 - (lines.length - 1) * size*0.58;
lines.forEach((ln, i) => txt(g, cx, start + i*size*1.16, ln, { size, weight: '800', fill: color }));
return g;
}
/* --- Panel 1: the clerical accident. The mascot's costume name is
typed into the Grand Marshal field of the wrong form. --- */
function panel0(svg, critter){
svg.innerHTML = '';
clerk(svg, 62, 74, 22, 'Festival Clerk');
form(svg, 104, 52, 84, 76, 4, critter.color);
txt(svg, 146, 46, 'FORM GM-1 · GRAND MARSHAL', { size: 8, weight: '700', fill: 'var(--ink-dim)' });
mascot(svg, 244, 72, 20, critter);
txt(svg, 150, 152, 'The costume name goes in the candidate field.', { size: 11, weight: '700' });
txt(svg, 150, 170, 'Nobody notices until the ballots are printed.', { size: 10, fill: 'var(--ink-dim)' });
txt(svg, 150, 188, critter.fest, { size: 9, fill: 'var(--ink-dim)' });
}
/* --- Panel 2: the count. One name on the ballot, every vote for it,
and a turnout number that makes it worse. --- */
function panel1(svg, critter, votes){
svg.innerHTML = '';
form(svg, 26, 44, 92, 96, 1, critter.color);
txt(svg, 72, 38, 'OFFICIAL BALLOT', { size: 8, weight: '700', fill: 'var(--ink-dim)' });
txt(svg, 72, 78, critter.name.split(' ')[0], { size: 10, weight: '700' });
txt(svg, 72, 94, '(no other candidate filed)', { size: 7.5, fill: 'var(--ink-dim)' });
el('rect', { x: 34, y: 66, width: 9, height: 9, fill: critter.color }, svg);
seal(svg, 200, 84, 44, ['ELECTED', 'UNOPPOSED'], critter.color);
txt(svg, 150, 160, votes + ' votes cast. ' + votes + ' votes for the mascot.', { size: 11, weight: '700' });
txt(svg, 150, 178, 'The costume was not eligible to vote for itself.', { size: 10, fill: 'var(--ink-dim)' });
}
/* --- Panel 3: the oath. The head stays on, because the oath is
administered to the name on the certificate. --- */
function panel2(svg, critter, depth){
svg.innerHTML = '';
clerk(svg, 66, 70, 22, 'Town Attorney');
mascot(svg, 196, 68, 26, critter, { hand: true, sash: true });
txt(svg, 196, 130, 'GRAND MARSHAL', { size: 8, weight: '800', fill: critter.color });
el('line', { x1: 96, y1: 72, x2: 160, y2: 72, stroke: 'currentColor', 'stroke-width': 1.4, 'stroke-dasharray': '3 3' }, svg);
txt(svg, 150, 152, '"Raise your right paw."', { size: 11, weight: '700' });
txt(svg, 150, 170, 'The head stays on. The oath is to the name on the form.', { size: 9.5, fill: 'var(--ink-dim)' });
if (depth > 0) {
txt(svg, 150, 190, 'Sworn-in term extended ' + depth + ' more year' + (depth === 1 ? '' : 's') + ' for continuity.', { size: 9, fill: 'var(--ink-dim)' });
}
}
function captions(critter, votes, depth){
return [
'A clerk at the ' + critter.fest + ' types ' + critter.name + ' into the Grand Marshal candidate field of Form GM-1, where the costume performer’s legal name was supposed to go.',
'Ballots print with one name on them. All ' + votes + ' votes cast go to the mascot, which is the only option and is not itself eligible to vote.',
'The oath is administered to the name on the certificate, so the felt head stays on for the swearing-in' + (depth > 0 ? ', and the term is extended ' + depth + ' year' + (depth === 1 ? '' : 's') + ' to avoid a second printing.' : '.'),
];
}
const STAMPS = ['FILED', 'CERTIFIED', 'SWORN IN'];
const state = { revealed: 0, twists: 0 };
function votesFor(){ return 214 + state.twists * 37; }
function renderPanel(i){
const svg = document.getElementById('art' + i);
const critter = CRITTERS[critterIdx];
const votes = votesFor();
const depth = state.twists;
if (i === 0) panel0(svg, critter);
if (i === 1) panel1(svg, critter, votes);
if (i === 2) panel2(svg, critter, depth);
svg.setAttribute('aria-label', 'Panel ' + (i+1) + ' of 3. ' + captions(critter, votes, depth)[i]);
document.getElementById('cap' + i).textContent = captions(critter, votes, depth)[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 certify';
} else if (i > state.revealed) {
cover.textContent = 'In queue';
}
document.getElementById('art' + i).setAttribute('tabindex', i === state.revealed || (state.revealed >= 3 && i === 2) ? '0' : '-1');
}
}
function syncButtons(){
const nextBtn = document.getElementById('nextBtn');
if (state.revealed >= 3) { nextBtn.textContent = 'All 3 panels certified ✓'; nextBtn.disabled = true; }
else { nextBtn.textContent = 'Certify Panel ' + (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 panels are already certified. Try Plot Twist!'); return; }
state.revealed++;
renderPanel(state.revealed - 1);
syncCovers();
syncButtons();
if (state.revealed >= 3) say('ok', 'All 3 panels certified. Press Plot Twist! to swap which critter won.');
else say('ok', 'Panel ' + state.revealed + ' certified.');
}
function twist(){
if (state.revealed < 3) { say('err', 'ERROR GM-409 · Premature twist — certify all 3 panels first.'); return; }
critterIdx = (critterIdx + 1) % CRITTERS.length;
state.twists++;
for (let i = 0; i < 3; i++) renderPanel(i);
const c = CRITTERS[critterIdx];
say('ok', 'Plot twist #' + state.twists + ': it was ' + c.name + ' all along, at the ' + c.fest + ' — same form, same result.');
}
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', function(){ if (i === state.revealed) next(); });
document.getElementById('art' + i).addEventListener('click', function(){ if (i === state.revealed) next(); });
document.getElementById('art' + i).addEventListener('keydown', function(e){
if ((e.key === 'Enter' || e.key === ' ') && i === state.revealed) { e.preventDefault(); next(); }
});
}
document.addEventListener('keydown', function(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', 'Election file open. Press N or click Panel 1 to begin.' + (reduced() ? '' : ''));
})();
</script>
</body>
</html>