← back to Crazy News Channel
cartoons/20260926-rainbow-permit-office.html
342 lines
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>The Rainbow That Needed a Permit — P24 Cartoon Desk</title>
<meta name="description" content="An original invented-satire strip: a recurring afternoon rainbow over an invented town is asked to file for a standing-display permit, and a clipboard-wielding clerk measures its arc." />
<link rel="stylesheet" href="../assets/style.css" />
</head>
<body data-mood="weather" class="cartoon-page cartoon-page-wide">
<header>
<div>
<div class="kicker">P24 Cartoon Desk · Invented Satire</div>
<h1>The Rainbow That Needed a Permit</h1>
<p class="dek">An invented town's rainbow keeps turning up in the same spot every afternoon, so the permit office sends a clerk with a clipboard to measure the arc.</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 Sighting</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 Filing</span><span id="stamp1"></span></div>
<svg class="art" id="art1" viewBox="0 0 300 300" role="img" tabindex="-1" 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 Conditions</span><span id="stamp2"></span></div>
<svg class="art" id="art2" viewBox="0 0 300 300" role="img" tabindex="-1" 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">Advance Review Stage 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>The town of Pellow Bend, its permit office, its clerks, and its rainbow are all invented. No actual atmospheric phenomenon was cited. 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){
const o = opts || {};
const t = el('text', {
x: x, y: y,
'text-anchor': o.anchor || 'middle',
'font-size': o.size || 12,
'font-weight': o.weight || '400',
fill: o.fill || 'currentColor'
}, svg);
t.textContent = s;
return t;
}
function reduced(){ return matchMedia('(prefers-reduced-motion: reduce)').matches; }
// ---- invented clerks of the Pellow Bend permit office ----
const CLERKS = [
{ name: 'Clerk Ordway', color: '#b5302f' },
{ name: 'Clerk Vestine', color: '#0f6b52' },
{ name: 'Clerk Habersham', color: '#6b4fa0' },
{ name: 'Clerk Nunley', color: '#c9a227' },
];
let clerkIdx = 0;
// The rainbow's own bands are content, not theme: a rainbow that
// renders in one ink is not a rainbow. Everything else uses the
// site's palette vars so the strip matches the rest of the desk.
const BANDS = ['#c0392b','#d98324','#d9b310','#2e7d4f','#2b6cb0','#5b3fa0'];
function skyline(svg, y){
el('line', { x1: 0, y1: y, x2: 300, y2: y, stroke: 'currentColor', 'stroke-width': 2, opacity: 0.55 }, svg);
const roofs = [[18,26,16],[50,34,22],[90,20,13],[196,30,19],[236,24,15],[266,32,20]];
roofs.forEach(function(r){
const x = r[0], w = r[1], h = r[2];
el('rect', { x: x, y: y - h, width: w, height: h, fill: 'currentColor', opacity: 0.18 }, svg);
el('path', { d: 'M ' + (x-3) + ' ' + (y-h) + ' L ' + (x + w/2) + ' ' + (y-h-8) + ' L ' + (x+w+3) + ' ' + (y-h) + ' Z',
fill: 'currentColor', opacity: 0.3 }, svg);
});
}
// Half-arc rainbow centred on (cx, baseY), outer radius r.
function rainbow(svg, cx, baseY, r, opts){
const o = opts || {};
const g = el('g', {}, svg);
const bw = (o.bandWidth || 7);
BANDS.forEach(function(c, i){
const rr = r - i * bw;
if (rr <= bw) return;
el('path', {
d: 'M ' + (cx - rr) + ' ' + baseY + ' A ' + rr + ' ' + rr + ' 0 0 1 ' + (cx + rr) + ' ' + baseY,
fill: 'none', stroke: c, 'stroke-width': bw - 1,
'stroke-opacity': o.faded ? 0.45 : 0.9, 'stroke-linecap': 'butt'
}, g);
});
return g;
}
// Flat iconographic clerk: body, head, and a clipboard held out front.
function clerk(svg, cx, footY, scale, color, label){
const s = scale;
const g = el('g', {}, svg);
el('circle', { cx: cx, cy: footY - 42*s, r: 9*s, fill: color }, g); // head
el('path', { d: 'M ' + cx + ' ' + (footY - 33*s) + ' L ' + cx + ' ' + (footY - 12*s),
stroke: color, 'stroke-width': 7*s, 'stroke-linecap': 'round' }, g); // torso
el('path', { d: 'M ' + cx + ' ' + (footY - 12*s) + ' L ' + (cx - 7*s) + ' ' + footY,
stroke: color, 'stroke-width': 4*s, 'stroke-linecap': 'round' }, g); // leg
el('path', { d: 'M ' + cx + ' ' + (footY - 12*s) + ' L ' + (cx + 7*s) + ' ' + footY,
stroke: color, 'stroke-width': 4*s, 'stroke-linecap': 'round' }, g); // leg
el('path', { d: 'M ' + cx + ' ' + (footY - 28*s) + ' L ' + (cx + 14*s) + ' ' + (footY - 24*s),
stroke: color, 'stroke-width': 4*s, 'stroke-linecap': 'round' }, g); // arm
// clipboard
el('rect', { x: cx + 12*s, y: footY - 34*s, width: 14*s, height: 18*s, rx: 1.5*s,
fill: 'var(--panel)', stroke: color, 'stroke-width': 2*s }, g);
el('rect', { x: cx + 16*s, y: footY - 36*s, width: 6*s, height: 3*s, rx: 1*s, fill: color }, g);
for (var i = 0; i < 3; i++) {
el('line', { x1: cx + 14.5*s, y1: footY - 29*s + i*4.5*s, x2: cx + 23.5*s, y2: footY - 29*s + i*4.5*s,
stroke: color, 'stroke-width': 1.2*s, opacity: 0.7 }, g);
}
// Right-anchored so a long clerk name can never clip the viewBox edge.
if (label) txt(svg, 296, footY + 15, label, { size: 11, anchor: 'end', fill: 'var(--ink-dim)' });
return g;
}
function stampMark(svg, cx, cy, r, lines, color){
const g = el('g', { transform: 'rotate(-12 ' + cx + ' ' + cy + ')' }, svg);
el('circle', { cx: cx, cy: cy, r: r, fill: 'none', stroke: color, 'stroke-width': 3.5, 'stroke-dasharray': '5 3' }, g);
const t = el('text', { x: cx, y: cy - (lines.length - 1) * 6 + 4, 'text-anchor': 'middle',
'font-weight': '800', 'font-size': Math.max(9, r * 0.3), fill: color }, g);
lines.forEach(function(line, i){
const ts = el('tspan', { x: cx, dy: i === 0 ? 0 : 12 }, t);
ts.textContent = line;
});
return g;
}
// ---- Panel 1: the rainbow turns up again; the clerk measures it ----
function panel0(svg, clerkDef, day){
svg.innerHTML = '';
const baseY = 238, r = 96;
skyline(svg, baseY);
rainbow(svg, 150, baseY, r);
// Apex measurement: a vertical dashed line inside the arc, so it
// never crosses (and muddies) the colored bands.
el('line', { x1: 150, y1: baseY, x2: 150, y2: baseY - r + 6, stroke: clerkDef.color,
'stroke-width': 1.6, 'stroke-dasharray': '5 4' }, svg);
el('line', { x1: 142, y1: baseY - r + 6, x2: 158, y2: baseY - r + 6,
stroke: clerkDef.color, 'stroke-width': 1.6 }, svg);
txt(svg, 150, baseY - r - 8, '41° at apex', { size: 11, weight: '700', fill: clerkDef.color });
clerk(svg, 264, baseY, 0.8, clerkDef.color, clerkDef.name);
txt(svg, 150, 34, 'Day ' + day + '. Same field. Same 4:10 p.m.', { size: 13, weight: '700' });
txt(svg, 150, 54, 'Pellow Bend Permit Office opens a file.', { size: 11, fill: 'var(--ink-dim)' });
}
// ---- Panel 2: the paperwork ----
function panel1(svg, clerkDef, permitNo){
svg.innerHTML = '';
el('rect', { x: 34, y: 44, width: 166, height: 196, rx: 4, fill: 'var(--panel)',
stroke: 'currentColor', 'stroke-width': 2 }, svg);
txt(svg, 117, 66, 'FORM W-7', { size: 13, weight: '800' });
txt(svg, 117, 82, 'STANDING DISPLAY', { size: 10, fill: 'var(--ink-dim)' });
txt(svg, 117, 94, '(ATMOSPHERIC)', { size: 10, fill: 'var(--ink-dim)' });
el('line', { x1: 48, y1: 104, x2: 186, y2: 104, stroke: 'currentColor', 'stroke-width': 1, opacity: 0.4 }, svg);
const rows = [
['Applicant', 'unreachable'],
['Height', '41° at apex'],
['Span', 'field to silo'],
['Owner', 'none listed'],
['Duration', 'nightly, 9 min'],
['Fee', '$40.00 unpaid']
];
rows.forEach(function(r, i){
const y = 122 + i * 19;
txt(svg, 48, y, r[0], { size: 10, anchor: 'start', fill: 'var(--ink-dim)' });
txt(svg, 186, y, r[1], { size: 10, anchor: 'end', weight: '700' });
el('line', { x1: 48, y1: y + 5, x2: 186, y2: y + 5, stroke: 'currentColor', 'stroke-width': 0.8, opacity: 0.22 }, svg);
});
txt(svg, 117, 232, 'No. ' + permitNo, { size: 10, fill: 'var(--ink-dim)' });
// a small, faded rainbow filed as an exhibit
rainbow(svg, 242, 184, 40, { bandWidth: 5, faded: true });
txt(svg, 242, 202, 'EXHIBIT A', { size: 9, fill: 'var(--ink-dim)' });
stampMark(svg, 238, 88, 38, ['UNDER', 'REVIEW'], clerkDef.color);
txt(svg, 150, 268, 'The rainbow is asked to appear in person.', { size: 12, weight: '700' });
}
// ---- Panel 3: approved, with conditions ----
function panel2(svg, clerkDef, permitNo, conditions){
svg.innerHTML = '';
const baseY = 244;
skyline(svg, baseY);
rainbow(svg, 150, baseY, 100);
// permit placard zip-tied to the west leg of the arc
el('line', { x1: 38, y1: baseY, x2: 38, y2: baseY - 46, stroke: 'currentColor',
'stroke-width': 1.2, 'stroke-dasharray': '3 3', opacity: 0.6 }, svg);
el('rect', { x: 14, y: baseY - 80, width: 62, height: 34, rx: 3, fill: 'var(--panel)',
stroke: clerkDef.color, 'stroke-width': 2 }, svg);
txt(svg, 45, baseY - 66, 'PERMIT', { size: 9, weight: '800', fill: clerkDef.color });
txt(svg, 45, baseY - 54, permitNo, { size: 9, fill: 'var(--ink-dim)' });
// the conditions, stacked
txt(svg, 150, 34, 'APPROVED, WITH ' + conditions.length + ' CONDITIONS', { size: 12, weight: '800' });
conditions.forEach(function(c, i){
txt(svg, 150, 54 + i * 15, c, { size: 10, fill: 'var(--ink-dim)' });
});
clerk(svg, 266, baseY, 0.8, clerkDef.color, clerkDef.name);
txt(svg, 150, 282, 'It appeared anyway, at 4:10, as always.', { size: 12, weight: '700' });
}
const ALL_CONDITIONS = [
'Must not obstruct the water tower.',
'Renewable every 90 days, in person.',
'No colors added without review.',
'Apex may not exceed 41°.',
'Display limited to weekday afternoons.',
'One arc per parcel.',
'Subject to inspection without notice.'
];
const CAPTIONS = [
'A recurring afternoon rainbow over an invented town shows up in the same field for the twelfth day running, and a clerk arrives to measure its arc.',
'A Form W-7 standing-display application is opened on the rainbow’s behalf. The applicant cannot be reached for the $40 fee.',
'The permit is granted with conditions the rainbow has no way to read, and it appears on schedule regardless.'
];
const STAMPS = ['LOGGED', 'FILED', 'APPROVED'];
const state = { revealed: 0, twists: 0 };
function clerkFor(i){ return CLERKS[(clerkIdx + i) % CLERKS.length]; }
function permitNo(){ return 'W7-' + (1104 + state.twists * 3); }
function day(){ return 12 + state.twists; }
function conditions(){ return ALL_CONDITIONS.slice(0, 3 + (state.twists % 4)); }
function renderPanel(i){
const svg = document.getElementById('art' + i);
const done = i < state.revealed;
if (!done) {
svg.innerHTML = '';
svg.setAttribute('aria-label', 'Panel ' + (i + 1) + ', ' + (i === state.revealed ? 'not yet revealed.' : 'in queue.'));
document.getElementById('cap' + i).textContent = '';
document.getElementById('stamp' + i).textContent = '';
return;
}
const c = clerkFor(i);
if (i === 0) panel0(svg, c, day());
if (i === 1) panel1(svg, c, permitNo());
if (i === 2) panel2(svg, c, permitNo(), conditions());
svg.setAttribute('aria-label', 'Panel ' + (i + 1) + '. ' + CAPTIONS[i]);
document.getElementById('cap' + i).textContent = CAPTIONS[i];
document.getElementById('stamp' + i).textContent = STAMPS[i];
}
function syncCovers(){
for (let i = 0; i < 3; i++) {
const cover = document.getElementById('cover' + i);
const art = document.getElementById('art' + i);
cover.hidden = i < state.revealed;
cover.disabled = i !== state.revealed;
cover.textContent = i === 0 ? 'Click, tap, or press N to begin' : 'Click, tap, or press N to review';
art.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 stages reviewed ✓'; nextBtn.disabled = true; }
else { nextBtn.textContent = 'Advance Review Stage ' + (state.revealed + 1); nextBtn.disabled = false; }
}
function say(kind, msg){
const s = document.getElementById('status');
s.className = kind;
s.textContent = msg;
}
function paintAll(){
for (let i = 0; i < 3; i++) renderPanel(i);
syncCovers();
syncButtons();
}
function next(){
if (state.revealed >= 3) { say('info', 'All 3 stages are already on file. Try Plot Twist!'); return; }
state.revealed++;
renderPanel(state.revealed - 1);
syncCovers();
syncButtons();
if (state.revealed >= 3) say('ok', 'Review complete. Press Plot Twist! to reassign the file.');
else say('ok', 'Stage ' + state.revealed + ' reviewed.');
}
function twist(){
if (state.revealed < 3) { say('err', 'ERROR PT-409 · Premature twist — advance all 3 stages first.'); return; }
clerkIdx = (clerkIdx + 1) % CLERKS.length;
state.twists++;
for (let i = 0; i < 3; i++) renderPanel(i);
say('ok', 'Plot twist #' + state.twists + ': the file is reassigned to ' + clerkFor(0).name +
', who finds ' + conditions().length + ' conditions and starts the count at day ' + day() + '.');
}
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', 'File opened on an unlicensed rainbow. Press N or click Panel 1 to begin.');
})();
</script>
</body>
</html>