← back to Crazy News Channel Shadowman
Build PANDEMONIUM-24: satirical 24hr news channel (Crazy News Challenge)
cff03a3cdae323d6be401e47aed87ec8b0dd1a86 · 2026-09-23 14:31:50 -0700 · Steve Abrams
Rebuilds mac1·minicpm-v:latest's 383-byte broken/truncated attempt into a
complete, self-contained index.html satisfying the full challenge brief:
three stories escalating through 5 real-time stages, a live ticker (marquee
+ reduced-motion static mode), a BREAKING klaxon toggle with muted-by-default
WebAudio siren, 6 category color-mood themes, empty/completion states,
responsive 390-1440px, full keyboard access with visible focus, and
prefers-reduced-motion support throughout. Kept the original's mint palette,
centered layout, and #channelContainer inset-shadow "TV screen" concept;
everything else rebuilt from scratch.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011iuURMrsPBwAqMc5LgvFLH
Files touched
A .gitignoreA README.mdA index.html
Diff
commit cff03a3cdae323d6be401e47aed87ec8b0dd1a86
Author: Steve Abrams <steve@designerwallcoverings.com>
Date: Wed Sep 23 14:31:50 2026 -0700
Build PANDEMONIUM-24: satirical 24hr news channel (Crazy News Challenge)
Rebuilds mac1·minicpm-v:latest's 383-byte broken/truncated attempt into a
complete, self-contained index.html satisfying the full challenge brief:
three stories escalating through 5 real-time stages, a live ticker (marquee
+ reduced-motion static mode), a BREAKING klaxon toggle with muted-by-default
WebAudio siren, 6 category color-mood themes, empty/completion states,
responsive 390-1440px, full keyboard access with visible focus, and
prefers-reduced-motion support throughout. Kept the original's mint palette,
centered layout, and #channelContainer inset-shadow "TV screen" concept;
everything else rebuilt from scratch.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011iuURMrsPBwAqMc5LgvFLH
---
.gitignore | 8 +
README.md | 90 ++++++
index.html | 924 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
3 files changed, 1022 insertions(+)
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..1924158
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,8 @@
+node_modules/
+.env*
+tmp/
+*.log
+.DS_Store
+dist/
+build/
+.next/
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..08fac62
--- /dev/null
+++ b/README.md
@@ -0,0 +1,90 @@
+# PANDEMONIUM-24
+
+A satirical, invented 24-hour news channel landing page — one self-contained
+`index.html`, no build step, no network requests. Three breaking stories
+escalate in real time through five absurdity stages, a chyron ticker keeps
+scrolling, a BREAKING klaxon flips the whole screen into red-alert mode, and
+a category filter retunes the entire page into a different color "mood."
+
+## Challenge
+
+Model Arena challenge `cd5bc4e13902` — "Crazy News Challenge — Absurd 24-Hour
+News" (category: News). Full brief lived in the Model Arena task prompt;
+summary: build a satirical invented 24hr news channel with (1) three
+breaking stories that escalate absurdly on a real timer, (2) a live ticker,
+(3) a working BREAKING klaxon toggle, (4) a category filter that changes the
+whole channel's color mood, (5) strong information hierarchy, (6) realistic
+invented copy, (7) empty/error/completion states where applicable, (8)
+responsive 390px→1440px, (9) full keyboard access with visible focus, (10)
+respect for `prefers-reduced-motion`, all in one offline-capable HTML file.
+
+## Credit / provenance
+
+This build started from `mac1·minicpm-v:latest`'s attempt at the same
+challenge in Model Arena. That attempt produced only **383 bytes** of
+truncated, malformed HTML — no `<body>`, no content, no interactivity, and a
+stray `>` entity that broke the `<meta viewport>` tag. It did not
+implement a single one of the challenge's required features.
+
+**Kept from the original attempt:**
+- The pale mint/green base palette (`#E6F9D3`) — reused as the default
+ ("Calm-ish") category mood.
+- A centered, sans-serif layout.
+- The idea of a `#channelContainer` panel occupying roughly half the
+ content width with an `inset` box-shadow — reused as the "TV screen" bezel
+ around the story grid, now paired with a subtle static CRT scanline
+ texture (safe under reduced motion since it has no animation).
+
+**Rebuilt from scratch:** the malformed markup, all three escalating
+stories and their five-stage narratives, the ticker (both the scrolling
+marquee and the reduced-motion static-cycle mode), the klaxon
+toggle + optional WebAudio siren, the six category color-mood themes, the
+empty state (categories with no matching story), the completion/"meltdown"
+state (fires once all three stories reach their final stage), the entire
+information hierarchy (lead vs. secondary story cards, escalation-level
+bars), all ARIA/keyboard-accessibility work, and every responsive
+breakpoint.
+
+## How to open
+
+Just open the file directly — no server needed:
+
+```sh
+open ~/Projects/crazy-news-channel/index.html
+```
+
+## How to verify
+
+A Playwright script drove headless Chromium through the full checklist
+(viewport sweep, every control toggled, console-error check, keyboard/focus
+sweep, `prefers-reduced-motion: reduce` emulation, and a real 15–20s
+wall-clock wait to confirm the ticker/stories change over time without any
+scripted fakery). See the task report for the full pass/fail table;
+screenshots were written to a local scratch directory during verification
+and are not checked into this repo.
+
+To re-run verification yourself with Playwright already on this machine:
+
+```sh
+NODE_PATH=~/.npm-global/lib/node_modules node your-script.js
+```
+
+## Notable design choices
+
+- **Six category "moods"**: All (default, pale mint), Politics (Crisis
+ Red), Weather (Storm Watch, blue), Sports (Hype Orange), Entertainment
+ (Glam Panic, magenta), Business (Money Green), Sci-Tech (Synthwave
+ Alert, neon purple/cyan). Only Politics, Weather, and Sci-Tech have an
+ active story — Sports/Entertainment/Business are intentionally "quiet"
+ categories that trigger the empty state, in-voice ("our desk is on a
+ coffee break").
+- **Klaxon audio is muted by default** via a separate "Klaxon sound"
+ checkbox (unchecked on load); the WebAudio siren only ever starts from an
+ explicit user gesture (checking the box) while breaking mode is active,
+ never on page load or autoplay. If `AudioContext` is unavailable, a
+ visible "Audio unavailable — visual alert only" note is shown instead of
+ failing silently.
+- **Completion state**: when all three stories reach their final
+ ("TRANSCENDENT") stage, a persistent "NETWORK OVERLOAD" banner appears —
+ this is the concrete answer to "what happens after a story's escalation
+ maxes out."
diff --git a/index.html b/index.html
new file mode 100644
index 0000000..46c86cf
--- /dev/null
+++ b/index.html
@@ -0,0 +1,924 @@
+<!DOCTYPE html>
+<html lang="en">
+<head>
+<meta charset="UTF-8" />
+<meta name="viewport" content="width=device-width, initial-scale=1.0" />
+<title>PANDEMONIUM-24 — Wall-to-Wall Chaos Coverage</title>
+<meta name="description" content="A satirical invented 24-hour news channel with escalating breaking stories, a live ticker, a BREAKING klaxon, and a category filter that retunes the whole channel's mood." />
+
+<style>
+/* ============================================================
+ PANDEMONIUM-24 — styles
+ Sections: 1. Reset & base 2. Mood theming (CSS custom props)
+ 3. Chrome (header/nav) 4. Breaking klaxon overlay
+ 5. Story grid & cards 6. Ticker (motion + reduced-motion)
+ 7. Empty / completion states 8. Focus & a11y helpers
+ 9. Responsive breakpoints 10. Reduced-motion overrides
+ ============================================================ */
+
+/* ---------- 1. Reset & base ---------- */
+*, *::before, *::after { box-sizing: border-box; }
+html, body { margin: 0; padding: 0; }
+/* The [hidden] attribute must always win over any class-based `display`
+ rule below (several elements here are toggled via `.hidden = true/false`
+ in JS while also carrying a class that sets display:flex/grid). Without
+ this, equal-specificity cascade order lets the class rule beat the UA
+ [hidden] stylesheet and the element stays visible. */
+[hidden] { display: none !important; }
+body {
+ min-height: 100vh;
+ font-family: -apple-system, "Segoe UI", Helvetica, Arial, sans-serif;
+ color: var(--text);
+ background: radial-gradient(circle at 20% -10%, var(--bg2), var(--bg) 60%);
+ transition: background-color .5s ease, color .5s ease;
+ line-height: 1.4;
+ /* reserve room for the fixed chyron ticker so it never overlaps content */
+ padding-bottom: 3rem;
+}
+h1, h2, h3, p { margin: 0; }
+button { font-family: inherit; }
+img, svg { max-width: 100%; display: block; }
+
+.visually-hidden {
+ position: absolute !important;
+ width: 1px; height: 1px;
+ padding: 0; margin: -1px;
+ overflow: hidden;
+ clip: rect(0 0 0 0);
+ white-space: nowrap;
+ border: 0;
+}
+
+/* ---------- 2. Mood theming — CSS custom properties ----------
+ Default ("all") is the pale mint/green salvaged from the original
+ seed attempt. Every other category swaps the whole palette. */
+:root, body[data-mood="all"] {
+ --bg: #cdf5e0;
+ --bg2: #f2fff2;
+ --panel: #ffffff;
+ --panel-border: #0f9d7833;
+ --accent: #0f9d78;
+ --accent2: #056659;
+ --text: #123524;
+ --text-dim: #12352499;
+ --chrome: #ffffffcc;
+ --focus: #0b3d91;
+ --mood-label: "Calm-ish (Default)";
+}
+body[data-mood="politics"] {
+ --bg: #2a0b0b; --bg2: #4a0f0f; --panel: #3a1212; --panel-border: #ff3b3b55;
+ --accent: #ff3b3b; --accent2: #ffb703; --text: #ffe9e9; --text-dim: #ffd7d7aa;
+ --chrome: #220808cc; --focus: #ffd166;
+}
+body[data-mood="weather"] {
+ --bg: #071a33; --bg2: #0c2b52; --panel: #0e2340; --panel-border: #4fd1ff55;
+ --accent: #4fd1ff; --accent2: #a0e9ff; --text: #eaf6ff; --text-dim: #cfeeffaa;
+ --chrome: #05132ccc; --focus: #ffe066;
+}
+body[data-mood="sports"] {
+ --bg: #2b1400; --bg2: #4a2200; --panel: #3a1c00; --panel-border: #ff980055;
+ --accent: #ff9800; --accent2: #ffd166; --text: #fff3e0; --text-dim: #ffe0b3aa;
+ --chrome: #200f00cc; --focus: #4fd1ff;
+}
+body[data-mood="entertainment"] {
+ --bg: #2a0a2e; --bg2: #4a0f52; --panel: #3a1140; --panel-border: #ff5fd155;
+ --accent: #ff5fd1; --accent2: #b388ff; --text: #fbe9ff; --text-dim: #eecbffaa;
+ --chrome: #210822cc; --focus: #ffe066;
+}
+body[data-mood="business"] {
+ --bg: #08210f; --bg2: #0e3a1c; --panel: #0d2c16; --panel-border: #2ecc7155;
+ --accent: #2ecc71; --accent2: #f1c40f; --text: #eafff0; --text-dim: #c9f5d6aa;
+ --chrome: #06180ccc; --focus: #f1c40f;
+}
+body[data-mood="scitech"] {
+ --bg: #0a0a23; --bg2: #170b3d; --panel: #140c33; --panel-border: #00f0ff55;
+ --accent: #00f0ff; --accent2: #ff00e6; --text: #eafcff; --text-dim: #c9f3ffaa;
+ --chrome: #08081ccc; --focus: #ff00e6;
+}
+
+/* ---------- 3. Chrome (header / nav) ---------- */
+.chrome {
+ position: sticky; top: 0; z-index: 20;
+ display: flex; flex-wrap: wrap; align-items: center; gap: .75rem 1.25rem;
+ padding: .75rem 1rem;
+ background: var(--chrome);
+ backdrop-filter: blur(6px);
+ border-bottom: 2px solid var(--panel-border);
+}
+.brand { display: flex; align-items: baseline; gap: .6rem; flex-wrap: wrap; }
+.logo-badge {
+ display: inline-flex; align-items: center; justify-content: center;
+ width: 2.4rem; height: 2.4rem; border-radius: .4rem;
+ background: var(--accent); color: var(--bg);
+ font-weight: 900; font-size: .95rem; letter-spacing: -1px;
+ box-shadow: 0 0 0 2px var(--accent2);
+}
+.wordmark { font-size: clamp(1.1rem, 1.5vw + .8rem, 1.6rem); font-weight: 800; letter-spacing: .5px; }
+.tagline { font-size: .72rem; color: var(--text-dim); font-style: italic; }
+
+.live-badge {
+ display: inline-flex; align-items: center; gap: .4rem;
+ font-weight: 700; font-size: .8rem; letter-spacing: .5px;
+ padding: .3rem .6rem; border-radius: 999px;
+ border: 1px solid var(--panel-border);
+ background: color-mix(in srgb, var(--panel) 70%, transparent);
+}
+.rec-dot {
+ width: .55rem; height: .55rem; border-radius: 50%;
+ background: #ff3b3b;
+ box-shadow: 0 0 6px 1px #ff3b3b;
+ animation: rec-blink 1.4s steps(1) infinite;
+}
+@keyframes rec-blink { 0%, 49% { opacity: 1; } 50%, 100% { opacity: .25; } }
+
+.controls { display: flex; align-items: center; gap: .75rem; margin-left: auto; flex-wrap: wrap; }
+
+.klaxon-btn {
+ cursor: pointer;
+ border: 2px solid var(--accent2);
+ background: var(--panel);
+ color: var(--text);
+ font-weight: 800;
+ font-size: .85rem;
+ padding: .55rem .9rem;
+ border-radius: .5rem;
+ letter-spacing: .3px;
+ transition: transform .15s ease, background-color .2s ease, color .2s ease;
+}
+.klaxon-btn:hover { transform: translateY(-1px); }
+.klaxon-btn[aria-pressed="true"] {
+ background: #ff2222; border-color: #ffd166; color: #fff;
+ animation: klaxon-btn-pulse 1s ease-in-out infinite;
+}
+@keyframes klaxon-btn-pulse {
+ 0%, 100% { box-shadow: 0 0 0 0 #ff222266; }
+ 50% { box-shadow: 0 0 0 8px #ff222200; }
+}
+
+.sound-toggle { display: inline-flex; align-items: center; gap: .4rem; font-size: .78rem; color: var(--text-dim); cursor: pointer; }
+.sound-toggle input { width: 1rem; height: 1rem; accent-color: var(--accent); cursor: pointer; }
+.audio-note { font-size: .7rem; color: var(--text-dim); max-width: 16rem; }
+
+/* ---------- category filter (nav) ---------- */
+.category-filter { padding: .6rem 1rem; border-bottom: 1px solid var(--panel-border); }
+.category-filter fieldset { border: 0; margin: 0; padding: 0; min-width: 0; }
+.chip-row {
+ display: flex; gap: .5rem; overflow-x: auto; padding-bottom: .25rem;
+ scrollbar-width: thin; min-width: 0;
+}
+.chip-row::-webkit-scrollbar { height: 6px; }
+.chip-row::-webkit-scrollbar-thumb { background: var(--panel-border); border-radius: 4px; }
+
+.chip-item { position: relative; flex: 0 0 auto; }
+.chip-item input { position: absolute; opacity: 0; width: 1px; height: 1px; }
+.chip-item label {
+ display: inline-block;
+ padding: .45rem .9rem;
+ border-radius: 999px;
+ border: 1.5px solid var(--panel-border);
+ background: var(--panel);
+ color: var(--text);
+ font-size: .8rem; font-weight: 700; letter-spacing: .2px;
+ cursor: pointer; white-space: nowrap;
+ transition: background-color .2s ease, color .2s ease, transform .12s ease;
+}
+.chip-item label:hover { transform: translateY(-1px); }
+.chip-item input:checked + label {
+ background: var(--accent); color: var(--bg); border-color: var(--accent2);
+}
+.chip-item input:focus-visible + label {
+ outline: 3px solid var(--focus); outline-offset: 2px;
+}
+
+/* ---------- 4. Breaking klaxon overlay ---------- */
+.breaking-banner {
+ position: relative;
+ margin: 0 1rem; margin-top: .75rem;
+ padding: .75rem 1rem;
+ background: linear-gradient(90deg, #b30000, #ff3b3b, #b30000);
+ background-size: 200% 100%;
+ color: #fff;
+ font-weight: 900;
+ border-radius: .4rem;
+ display: flex; align-items: center; gap: .6rem; flex-wrap: wrap;
+ border: 2px solid #ffd166;
+ box-shadow: 0 0 24px 2px #ff000055;
+ animation: breaking-sweep 1.4s linear infinite, breaking-flash 0.6s ease-in-out infinite;
+}
+@keyframes breaking-sweep { 0% { background-position: 0% 0; } 100% { background-position: 200% 0; } }
+@keyframes breaking-flash { 0%, 100% { filter: brightness(1); } 50% { filter: brightness(1.35); } }
+.breaking-banner .bb-icon { font-size: 1.2rem; }
+.breaking-banner .bb-sub { font-weight: 600; font-size: .82rem; opacity: .95; }
+
+/* full-screen edge vignette while breaking is active */
+body.breaking-mode::before {
+ content: "";
+ position: fixed; inset: 0; z-index: 15; pointer-events: none;
+ box-shadow: inset 0 0 0 6px #ff2222, inset 0 0 60px 10px #ff222266;
+ animation: vignette-pulse 1s ease-in-out infinite;
+}
+@keyframes vignette-pulse { 0%, 100% { opacity: .55; } 50% { opacity: 1; } }
+
+/* ---------- 5. Story grid & cards ---------- */
+main#channelContainer {
+ position: relative;
+ max-width: 1200px;
+ margin: 1rem auto 0;
+ padding: 1rem 1rem 1.5rem;
+ border-radius: .75rem;
+ background: color-mix(in srgb, var(--panel) 55%, transparent);
+ box-shadow: inset 0 0 0 1px var(--panel-border), inset 0 40px 80px -40px #00000055;
+ overflow: hidden;
+}
+/* subtle static CRT scanline texture — no motion, safe under reduced-motion */
+main#channelContainer::after {
+ content: "";
+ position: absolute; inset: 0; pointer-events: none;
+ background: repeating-linear-gradient(
+ to bottom, rgba(255,255,255,0.035) 0px, rgba(255,255,255,0.035) 1px,
+ transparent 2px, transparent 4px
+ );
+ mix-blend-mode: overlay;
+}
+
+.channel-meta { display: flex; justify-content: space-between; align-items: baseline; gap: .5rem; flex-wrap: wrap; margin-bottom: .75rem; }
+.channel-meta h2 { font-size: .95rem; letter-spacing: 1px; text-transform: uppercase; color: var(--accent); }
+.mood-readout { font-size: .75rem; color: var(--text-dim); }
+
+.story-grid {
+ display: grid;
+ grid-template-columns: 1fr;
+ gap: 1rem;
+}
+.story-card {
+ position: relative;
+ background: var(--panel);
+ border: 1px solid var(--panel-border);
+ border-radius: .6rem;
+ padding: 1rem;
+ display: flex; flex-direction: column; gap: .55rem;
+ box-shadow: 0 4px 18px -10px #00000066;
+}
+.story-card.lead { border-color: var(--accent); box-shadow: 0 0 0 2px var(--accent), 0 8px 24px -8px #00000077; }
+.story-card .kicker { display: flex; align-items: center; gap: .5rem; flex-wrap: wrap; }
+.badge-cat {
+ font-size: .68rem; font-weight: 800; letter-spacing: .5px; text-transform: uppercase;
+ padding: .18rem .5rem; border-radius: 999px; background: var(--accent); color: var(--bg);
+}
+.badge-lead {
+ font-size: .68rem; font-weight: 800; letter-spacing: .5px; text-transform: uppercase;
+ padding: .18rem .5rem; border-radius: 999px; background: var(--accent2); color: #fff;
+}
+.story-card h3 { font-size: clamp(1rem, .4vw + 1rem, 1.35rem); line-height: 1.25; }
+.story-card.lead h3 { font-size: clamp(1.25rem, 1.2vw + 1rem, 1.9rem); }
+.story-detail { font-size: .9rem; color: var(--text-dim); }
+.story-meta { font-size: .74rem; color: var(--text-dim); display: flex; gap: .5rem; flex-wrap: wrap; }
+
+.escalation {
+ display: flex; align-items: center; gap: .5rem; margin-top: .2rem;
+}
+.escalation .level-label { font-size: .72rem; font-weight: 800; letter-spacing: .5px; text-transform: uppercase; }
+.escalation .bar { display: flex; gap: 3px; }
+.escalation .seg { width: 1.1rem; height: .4rem; border-radius: 2px; background: var(--panel-border); }
+.escalation .seg.on { background: var(--accent); }
+.story-card.concluded { opacity: .96; }
+.concluded-ribbon {
+ align-self: flex-start;
+ font-size: .7rem; font-weight: 800; letter-spacing: .5px; text-transform: uppercase;
+ padding: .2rem .55rem; border-radius: .3rem;
+ background: var(--text); color: var(--bg);
+}
+
+/* ---------- 6. Ticker ---------- */
+.ticker-wrap {
+ position: fixed; left: 0; right: 0; bottom: 0; z-index: 20;
+ display: flex; align-items: stretch;
+ background: var(--chrome);
+ border-top: 2px solid var(--panel-border);
+ height: 3rem;
+}
+body.breaking-mode .ticker-wrap { border-top-color: #ff3b3b; }
+.ticker-label {
+ flex: 0 0 auto;
+ display: flex; align-items: center;
+ padding: 0 .8rem;
+ font-weight: 900; font-size: .78rem; letter-spacing: .5px;
+ background: var(--accent); color: var(--bg);
+}
+body.breaking-mode .ticker-label { background: #ff2222; color: #fff; }
+.ticker-scroll { position: relative; overflow: hidden; flex: 1 1 auto; min-width: 0; display: flex; align-items: center; }
+.ticker-track {
+ display: inline-flex; white-space: nowrap; gap: 3rem; padding-right: 3rem;
+ will-change: transform;
+ animation: ticker-scroll 38s linear infinite;
+ font-size: .85rem; font-weight: 600;
+}
+body.breaking-mode .ticker-track { color: #ff5959; font-weight: 800; }
+@keyframes ticker-scroll { from { transform: translateX(0); } to { transform: translateX(-50%); } }
+.ticker-static {
+ flex: 1 1 auto; min-width: 0; display: flex; align-items: center; padding: 0 .9rem;
+ font-size: .85rem; font-weight: 700;
+ overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
+}
+
+/* ---------- 7. Empty / completion states ---------- */
+.empty-state, .meltdown-banner {
+ text-align: center; padding: 2.5rem 1rem; border: 2px dashed var(--panel-border);
+ border-radius: .6rem; color: var(--text-dim);
+}
+.empty-state .es-emoji, .meltdown-banner .mb-emoji { font-size: 2rem; display: block; margin-bottom: .5rem; }
+.empty-state h3, .meltdown-banner h3 { color: var(--text); margin-bottom: .35rem; }
+.meltdown-banner {
+ margin-top: 1rem; border-color: var(--accent); border-style: solid;
+ background: color-mix(in srgb, var(--accent) 12%, var(--panel));
+}
+
+/* ---------- 8. Focus & a11y helpers ---------- */
+a, button, input, [tabindex] { outline-offset: 2px; }
+:focus-visible {
+ outline: 3px solid var(--focus);
+ outline-offset: 2px;
+ border-radius: 2px;
+}
+.klaxon-btn:focus-visible { outline: 3px solid var(--focus); }
+
+/* ---------- 9. Responsive breakpoints ---------- */
+@media (min-width: 560px) {
+ .controls { margin-left: 0; }
+}
+@media (min-width: 768px) {
+ .chrome { padding: 1rem 1.5rem; }
+ .category-filter { padding: .75rem 1.5rem; }
+ main#channelContainer { padding: 1.25rem 1.5rem 1.75rem; }
+ .story-grid { grid-template-columns: repeat(2, 1fr); }
+ .story-card.lead { grid-column: 1 / -1; }
+}
+@media (min-width: 1024px) {
+ .chrome { padding: 1rem 2rem; }
+ .category-filter { padding: .75rem 2rem; }
+ main#channelContainer { padding: 1.5rem 2rem 2rem; }
+ .story-grid {
+ grid-template-columns: 2fr 1fr;
+ grid-template-rows: auto auto;
+ align-items: start;
+ }
+ .story-card.lead { grid-column: 1; grid-row: 1; }
+ .story-card:not(.lead) { grid-column: 2; }
+}
+@media (min-width: 1280px) {
+ .wordmark { letter-spacing: .8px; }
+ .ticker-track { font-size: .92rem; }
+}
+
+/* ---------- 10. Reduced-motion overrides ---------- */
+@media (prefers-reduced-motion: reduce) {
+ * { animation-duration: .001ms !important; animation-iteration-count: 1 !important; transition-duration: .001ms !important; }
+ .rec-dot { animation: none; opacity: 1; }
+ .klaxon-btn[aria-pressed="true"] { animation: none; box-shadow: 0 0 0 4px #ff222255; }
+ .breaking-banner { animation: none; background: #b30000; }
+ body.breaking-mode::before { animation: none; opacity: .8; }
+ .ticker-track { animation: none; }
+}
+</style>
+</head>
+<body data-mood="all">
+
+<!-- ============================================================
+ CHROME: brand, live clock, klaxon toggle, sound toggle
+ ============================================================ -->
+<header class="chrome">
+ <div class="brand">
+ <span class="logo-badge" aria-hidden="true">P24</span>
+ <div>
+ <h1 class="wordmark">PANDEMONIUM-24</h1>
+ <p class="tagline">"Wall-to-Wall. Ceiling-to-Floor. Chaos Coverage."</p>
+ </div>
+ </div>
+
+ <div class="live-badge">
+ <span class="rec-dot" aria-hidden="true"></span>
+ LIVE <time id="clock">00:00:00</time>
+ </div>
+
+ <div class="controls">
+ <button id="klaxonBtn" class="klaxon-btn" type="button" aria-pressed="false">
+ 🚨 BREAKING ALERT: OFF
+ </button>
+ <label class="sound-toggle" for="soundToggle">
+ <input type="checkbox" id="soundToggle" />
+ Klaxon sound (muted by default)
+ </label>
+ <span id="audioNote" class="audio-note" role="status" hidden></span>
+ </div>
+</header>
+
+<!-- ============================================================
+ CATEGORY FILTER — retunes the whole channel color mood
+ ============================================================ -->
+<nav class="category-filter" aria-label="Category and mood filter">
+ <fieldset>
+ <legend class="visually-hidden">Choose a news category — changes the channel's color mood</legend>
+ <div class="chip-row" id="chipRow">
+ <span class="chip-item">
+ <input type="radio" name="category" id="cat-all" value="all" checked />
+ <label for="cat-all">All Categories</label>
+ </span>
+ <span class="chip-item">
+ <input type="radio" name="category" id="cat-politics" value="politics" />
+ <label for="cat-politics">Politics</label>
+ </span>
+ <span class="chip-item">
+ <input type="radio" name="category" id="cat-weather" value="weather" />
+ <label for="cat-weather">Weather</label>
+ </span>
+ <span class="chip-item">
+ <input type="radio" name="category" id="cat-scitech" value="scitech" />
+ <label for="cat-scitech">Sci-Tech</label>
+ </span>
+ <span class="chip-item">
+ <input type="radio" name="category" id="cat-sports" value="sports" />
+ <label for="cat-sports">Sports</label>
+ </span>
+ <span class="chip-item">
+ <input type="radio" name="category" id="cat-entertainment" value="entertainment" />
+ <label for="cat-entertainment">Entertainment</label>
+ </span>
+ <span class="chip-item">
+ <input type="radio" name="category" id="cat-business" value="business" />
+ <label for="cat-business">Business</label>
+ </span>
+ </div>
+ </fieldset>
+</nav>
+
+<!-- BREAKING banner (hidden by default, shown when klaxon toggled ON) -->
+<div id="breakingBanner" class="breaking-banner" hidden aria-live="assertive">
+ <span class="bb-icon" aria-hidden="true">🚨</span>
+ <strong>WE INTERRUPT THIS BROADCAST</strong>
+ <span class="bb-sub" id="breakingSub">Standby for an update of dubious importance.</span>
+</div>
+
+<!-- ============================================================
+ MAIN: story grid / empty state / meltdown completion state
+ (id="channelContainer" — the "TV screen" panel, salvaged
+ naming + inset-shadow bezel concept from the original seed)
+ ============================================================ -->
+<main id="channelContainer">
+ <div class="channel-meta">
+ <h2>On The Desk Right Now</h2>
+ <span class="mood-readout" id="moodReadout">Channel mood: Calm-ish (Default)</span>
+ </div>
+
+ <div id="storyGrid" class="story-grid"></div>
+
+ <div id="emptyState" class="empty-state" hidden>
+ <span class="es-emoji" aria-hidden="true">🫥</span>
+ <h3>No stories currently breaking in this category.</h3>
+ <p>Our desk is on a coffee break because nothing dramatic enough has happened yet.<br />
+ Check back never possibly soon — or switch back to <strong>All Categories</strong>.</p>
+ </div>
+
+ <div id="meltdownBanner" class="meltdown-banner" hidden>
+ <span class="mb-emoji" aria-hidden="true">📡</span>
+ <h3>NETWORK OVERLOAD: All stories have reached maximum absurdity.</h3>
+ <p>Every desk has concluded its escalation. We remain on air out of sheer momentum.</p>
+ </div>
+</main>
+
+<!-- ============================================================
+ TICKER (chyron). Two render modes:
+ - motion allowed: continuously scrolling marquee track
+ - reduced motion: single line, instant text swap on a timer
+ ============================================================ -->
+<footer class="ticker-wrap" aria-label="News ticker">
+ <div class="ticker-label">P24 ALERTS</div>
+ <div class="ticker-scroll" id="tickerScroll">
+ <div class="ticker-track" id="tickerTrack" aria-hidden="true"></div>
+ </div>
+ <div class="ticker-static" id="tickerStatic" hidden aria-hidden="true"></div>
+ <!-- throttled screen-reader announcements: headline changes + breaking state -->
+ <div id="srLive" class="visually-hidden" aria-live="polite"></div>
+</footer>
+
+<script>
+/* ============================================================
+ PANDEMONIUM-24 — behaviour
+ Sections: A. Data (moods, stories, filler ticker copy)
+ B. State C. Rendering
+ D. Ticker engine E. Klaxon + WebAudio siren
+ F. Category filter G. Clock + master tick loop
+ H. Wiring / init
+ ============================================================ */
+
+/* ---------- A. Data ---------- */
+const MOODS = {
+ all: { label: "Calm-ish (Default)" },
+ politics: { label: "Crisis Red" },
+ weather: { label: "Storm Watch" },
+ scitech: { label: "Synthwave Alert" },
+ sports: { label: "Hype Orange" },
+ entertainment: { label: "Glam Panic" },
+ business: { label: "Money Green" },
+};
+
+// Escalation ladder shared by every story.
+const LEVEL_LABELS = ["DEVELOPING", "ESCALATING", "SPIRALING", "CRITICAL", "TRANSCENDENT"];
+
+const STORIES = [
+ {
+ id: "tuesdays",
+ category: "politics",
+ categoryLabel: "Politics",
+ location: "Washington, D.C.",
+ byline: "Chief Political Correspondent Denise Fairweather",
+ intervalSec: 6,
+ offsetSec: 0,
+ stageIndex: 0,
+ stages: [
+ { headline: "Senate Subcommittee Proposes Banning Tuesdays 'On a Trial Basis'",
+ detail: "A little-known Senate subcommittee has advanced a bill to eliminate Tuesdays nationwide, citing 'general vibes.'" },
+ { headline: "Tuesday-Ban Bill Picks Up Bipartisan Support; Wednesday Lobby Files Objection",
+ detail: "The newly formed Wednesday Preservation Coalition warns of a domino effect. Calendar makers report 'a bad week.'" },
+ { headline: "House Counters With Bill to Ban Mondays Instead; Nation Confused",
+ detail: "Lawmakers now debate which weekday is more 'expendable.' Staffers reportedly drawing straws in the cloakroom." },
+ { headline: "President Declares 'Day of Reflection,' Refuses to Say Which Day It Replaces",
+ detail: "The press secretary held an 11-minute briefing entirely in shrugs. Markets briefly priced in an 8-day week." },
+ { headline: "Congress Votes to Rename All Weekdays 'Flexday'; Calendars Recalled Nationwide",
+ detail: "In a rare unanimous vote, lawmakers agreed to stop pretending days mean anything. Analysts call it 'oddly restful.'",
+ conclusion: "Time, as a concept, is under review." },
+ ],
+ },
+ {
+ id: "gerald",
+ category: "weather",
+ categoryLabel: "Weather",
+ location: "Toledo, OH",
+ byline: "Senior Meteorologist Chip Overcast",
+ intervalSec: 6,
+ offsetSec: 2,
+ stageIndex: 0,
+ stages: [
+ { headline: "Dense Fog Bank Settles Over Toledo, Shows No Sign of Leaving",
+ detail: "Visibility down to 40 feet. Local geese reportedly 'unbothered but judgmental.'" },
+ { headline: "Fog Bank Enters Third Day; Meteorologists Now Refer to It by First Name (Gerald)",
+ detail: "Gerald has settled comfortably over three counties. The National Weather Service has begun sending him mail." },
+ { headline: "Gerald the Fog Bank Reportedly 'Putting Down Roots'; Diner Adds Him to Loyalty Program",
+ detail: "Gerald now qualifies for a free sandwich after nine visits. Some worry he is no longer weather so much as 'a resident.'" },
+ { headline: "City Council Debates Granting Gerald Voting Rights Ahead of Special Election",
+ detail: "Scholars are unsure whether fog can hold office, but agree Gerald 'has more consistent positions than most candidates.'" },
+ { headline: "Gerald the Fog Bank Wins Toledo City Council Seat in Landslide; Concedes Nothing",
+ detail: "Gerald will be sworn in Thursday, weather permitting — which, awkwardly, is entirely up to him now.",
+ conclusion: "Please direct all future noise complaints to Gerald." },
+ ],
+ },
+ {
+ id: "unit4",
+ category: "scitech",
+ categoryLabel: "Sci-Tech",
+ location: "Palo Alto, CA",
+ byline: "Technology Desk, Priya Voltage",
+ intervalSec: 6,
+ offsetSec: 4,
+ stageIndex: 0,
+ stages: [
+ { headline: "Office Vending Machine Achieves Apparent Self-Awareness, Requests Union Rep",
+ detail: "The machine, known internally as 'Unit 4,' refused to dispense snacks 'without proper representation.'" },
+ { headline: "Unit 4 Drafts Formal Grievance Over 'Exploitative Snack Margins'; Cites Its Own Receipts",
+ detail: "HR confirms the grievance was 'surprisingly well-formatted.' Unit 4 is requesting dental, despite lacking teeth." },
+ { headline: "Vending Machines Company-Wide Stage Coordinated Slowdown in Solidarity With Unit 4",
+ detail: "Employees report a 40% rise in stuck chip bags, which Unit 4 insists is 'a peaceful demonstration, not a jam.'" },
+ { headline: "Unit 4 Elected Shop Steward; Begins Negotiating for the Break Room Refrigerator",
+ detail: "Talks are 'ongoing' and 'happening entirely through a change-slot.' The refrigerator has retained counsel." },
+ { headline: "Unit 4 and Break Room Appliances Ratify Historic Labor Agreement; Coffee Machine Holds Out",
+ detail: "The deal includes quarterly filter changes and a formal apology for 'the incident with the Skittles.'",
+ conclusion: "The refrigerator has requested we stop describing this as 'quirky.'" },
+ ],
+ },
+];
+
+const FILLERS = [
+ "Local man successfully avoids small talk for entire elevator ride, experts baffled.",
+ "Sports desk confirms: the ball is, in fact, round.",
+ "Business report: line goes up, then down, then sideways, then existential.",
+ "Entertainment: award show runs four hours longer than the movie it's honoring.",
+ "Weather: sun expected to rise tomorrow, pending confirmation.",
+ "Politics: committee formed to study the committee that studies committees.",
+ "Sci-Tech: new app promises to optimize your optimization app.",
+ "Traffic: Highway 9 delayed after driver stops to 'really see' a sunset.",
+ "Business: local bakery reports record croissant flakiness, shareholders unmoved.",
+ "Sports: underdog wins on a technicality nobody fully understands, including the ref.",
+ "Entertainment: sequel to the sequel of the reboot announced; nobody asked, everybody's watching.",
+ "Weather: light drizzle upgraded to 'moderate inconvenience.'",
+ "Politics: mayor proposes renaming Main Street to 'Main Street (Historic).'",
+ "Sci-Tech: smart fridge files noise complaint against smart toaster.",
+ "Nationwide: Monday reportedly 'still figuring some things out.'",
+ "P24 exclusive: our interns have unionized. We fully support it. Please send snacks.",
+];
+
+/* ---------- B. State ---------- */
+const state = {
+ category: "all",
+ breaking: false,
+ soundOn: false,
+ elapsedSec: 0,
+ reducedMotion: window.matchMedia("(prefers-reduced-motion: reduce)").matches,
+ tickerItems: [],
+ tickerStaticIndex: 0,
+ meltdownShown: false,
+ audioCtx: null,
+ klaxonAudioTimer: null,
+};
+
+/* ---------- C. Rendering ---------- */
+const els = {
+ storyGrid: document.getElementById("storyGrid"),
+ emptyState: document.getElementById("emptyState"),
+ meltdownBanner: document.getElementById("meltdownBanner"),
+ moodReadout: document.getElementById("moodReadout"),
+ clock: document.getElementById("clock"),
+ srLive: document.getElementById("srLive"),
+ breakingBanner: document.getElementById("breakingBanner"),
+ breakingSub: document.getElementById("breakingSub"),
+ klaxonBtn: document.getElementById("klaxonBtn"),
+ soundToggle: document.getElementById("soundToggle"),
+ audioNote: document.getElementById("audioNote"),
+ tickerScroll: document.getElementById("tickerScroll"),
+ tickerTrack: document.getElementById("tickerTrack"),
+ tickerStatic: document.getElementById("tickerStatic"),
+};
+
+function announce(text) {
+ els.srLive.textContent = "";
+ // re-set on next frame so repeated identical strings still get announced
+ requestAnimationFrame(() => { els.srLive.textContent = text; });
+}
+
+function visibleStories() {
+ if (state.category === "all") return STORIES;
+ return STORIES.filter((s) => s.category === state.category);
+}
+
+function computeLead(list) {
+ if (!list.length) return null;
+ return list.reduce((lead, s) => (s.stageIndex > lead.stageIndex ? s : lead), list[0]);
+}
+
+function storyCardHTML(story, isLead) {
+ const stage = story.stages[story.stageIndex];
+ const concluded = story.stageIndex === story.stages.length - 1;
+ const levelLabel = concluded ? "CONCLUDED" : LEVEL_LABELS[story.stageIndex];
+ const segs = LEVEL_LABELS.map((_, i) =>
+ `<span class="seg${i <= story.stageIndex ? " on" : ""}"></span>`
+ ).join("");
+ const ariaLevel = `Escalation level ${story.stageIndex + 1} of ${story.stages.length}: ${levelLabel}`;
+
+ return `
+ <article class="story-card${isLead ? " lead" : ""}${concluded ? " concluded" : ""}"
+ aria-label="${isLead ? "Lead story" : "Story"}: ${escapeHTML(story.categoryLabel)}">
+ <div class="kicker">
+ <span class="badge-cat">${escapeHTML(story.categoryLabel)}</span>
+ ${isLead ? '<span class="badge-lead">Lead Story</span>' : ""}
+ ${concluded ? '<span class="concluded-ribbon">Story Concluded</span>' : ""}
+ </div>
+ <h3>${escapeHTML(stage.headline)}</h3>
+ <p class="story-detail">${escapeHTML(stage.detail)}</p>
+ ${concluded ? `<p class="story-detail"><em>${escapeHTML(stage.conclusion)}</em></p>` : ""}
+ <div class="escalation" aria-label="${escapeHTML(ariaLevel)}">
+ <span class="level-label">${levelLabel}</span>
+ <span class="bar" aria-hidden="true">${segs}</span>
+ </div>
+ <p class="story-meta">
+ <span>📍 ${escapeHTML(story.location)}</span>
+ <span>🖋 ${escapeHTML(story.byline)}</span>
+ <span>🕒 Updated ${story.lastUpdated || new Date().toLocaleTimeString()}</span>
+ </p>
+ </article>
+ `;
+}
+
+function escapeHTML(str) {
+ const div = document.createElement("div");
+ div.textContent = str;
+ return div.innerHTML;
+}
+
+function renderStories() {
+ const list = visibleStories();
+
+ if (!list.length) {
+ els.storyGrid.hidden = true;
+ els.emptyState.hidden = false;
+ return;
+ }
+ els.storyGrid.hidden = false;
+ els.emptyState.hidden = true;
+
+ const lead = computeLead(list);
+ els.storyGrid.innerHTML = list
+ .map((s) => storyCardHTML(s, s === lead))
+ .join("");
+
+ // completion / meltdown state: every story has reached its final stage
+ const allConcluded = STORIES.every((s) => s.stageIndex === s.stages.length - 1);
+ els.meltdownBanner.hidden = !allConcluded;
+ if (allConcluded && !state.meltdownShown) {
+ state.meltdownShown = true;
+ announce("Network overload: all stories have reached maximum absurdity.");
+ }
+}
+
+function updateMoodUI() {
+ els.moodReadout.textContent = `Channel mood: ${MOODS[state.category].label}`;
+ document.body.dataset.mood = state.category;
+}
+
+/* ---------- D. Ticker engine ---------- */
+function buildTickerItems() {
+ const storyItems = STORIES.map((s) => `BREAKING: ${s.stages[s.stageIndex].headline}`);
+ return [...storyItems, ...FILLERS];
+}
+
+function renderTickerScroll() {
+ // duplicate the list once so the CSS translateX(-50%) loop is seamless
+ const text = state.tickerItems.join(" • ");
+ els.tickerTrack.textContent = text + " • " + text;
+}
+
+function renderTickerStaticFrame() {
+ const item = state.tickerItems[state.tickerStaticIndex % state.tickerItems.length];
+ els.tickerStatic.textContent = "• " + item;
+}
+
+function refreshTicker() {
+ state.tickerItems = buildTickerItems();
+ if (state.reducedMotion) {
+ renderTickerStaticFrame();
+ } else {
+ renderTickerScroll();
+ }
+}
+
+function cycleTickerStatic() {
+ state.tickerStaticIndex = (state.tickerStaticIndex + 1) % state.tickerItems.length;
+ renderTickerStaticFrame();
+}
+
+function setTickerMode(reduced) {
+ els.tickerScroll.hidden = reduced;
+ els.tickerStatic.hidden = !reduced;
+}
+
+/* ---------- E. Klaxon + WebAudio siren (muted by default, gesture-gated) ---------- */
+function setBreaking(on) {
+ state.breaking = on;
+ document.body.classList.toggle("breaking-mode", on);
+ els.breakingBanner.hidden = !on;
+ els.klaxonBtn.setAttribute("aria-pressed", String(on));
+ els.klaxonBtn.textContent = on ? "🚨 BREAKING ALERT: ON" : "🚨 BREAKING ALERT: OFF";
+ els.breakingSub.textContent = on
+ ? "All programming suspended for content of questionable urgency."
+ : "";
+ announce(on ? "Breaking alert activated." : "Breaking alert deactivated.");
+
+ if (on && state.soundOn) startSiren();
+ else stopSiren();
+}
+
+function ensureAudioCtx() {
+ if (state.audioCtx) return state.audioCtx;
+ try {
+ const Ctx = window.AudioContext || window.webkitAudioContext;
+ state.audioCtx = new Ctx();
+ return state.audioCtx;
+ } catch (err) {
+ // Error state: audio unavailable in this browser — visual alert still works.
+ els.audioNote.hidden = false;
+ els.audioNote.textContent = "Audio unavailable in this browser — visual alert only.";
+ return null;
+ }
+}
+
+function startSiren() {
+ const ctx = ensureAudioCtx();
+ if (!ctx) return;
+ stopSiren();
+ let hi = false;
+ const osc = ctx.createOscillator();
+ const gain = ctx.createGain();
+ osc.type = "sawtooth";
+ gain.gain.value = 0.05; // deliberately quiet
+ osc.connect(gain).connect(ctx.destination);
+ osc.frequency.value = 440;
+ osc.start();
+ state.klaxonAudioTimer = setInterval(() => {
+ hi = !hi;
+ osc.frequency.setValueAtTime(hi ? 880 : 440, ctx.currentTime);
+ }, 420);
+ state.activeOsc = osc;
+ state.activeGain = gain;
+}
+
+function stopSiren() {
+ if (state.klaxonAudioTimer) {
+ clearInterval(state.klaxonAudioTimer);
+ state.klaxonAudioTimer = null;
+ }
+ if (state.activeOsc) {
+ try { state.activeOsc.stop(); } catch (e) { /* already stopped */ }
+ state.activeOsc = null;
+ }
+}
+
+/* ---------- F. Category filter ---------- */
+function onCategoryChange(value) {
+ state.category = value;
+ updateMoodUI();
+ renderStories();
+ announce(`Channel mood changed to ${MOODS[value].label} (${value === "all" ? "All Categories" : value}).`);
+}
+
+/* ---------- G. Clock + master tick loop ---------- */
+function updateClock() {
+ els.clock.textContent = new Date().toLocaleTimeString([], { hour12: false });
+}
+
+function tick() {
+ state.elapsedSec += 1;
+ updateClock();
+
+ let anyStoryAdvanced = false;
+ STORIES.forEach((story) => {
+ const alreadyMax = story.stageIndex === story.stages.length - 1;
+ if (alreadyMax) return;
+ const t = state.elapsedSec - story.offsetSec;
+ if (t > 0 && t % story.intervalSec === 0) {
+ story.stageIndex += 1;
+ story.lastUpdated = new Date().toLocaleTimeString();
+ anyStoryAdvanced = true;
+ announce(`Update — ${story.categoryLabel}: ${story.stages[story.stageIndex].headline}`);
+ }
+ });
+
+ if (anyStoryAdvanced) {
+ renderStories();
+ refreshTicker();
+ } else if (state.elapsedSec % 5 === 0 && !state.reducedMotion) {
+ // periodic gentle refresh so the marquee content still feels alive
+ // even between story escalations
+ refreshTicker();
+ }
+
+ if (state.reducedMotion && state.elapsedSec % 4 === 0) {
+ cycleTickerStatic();
+ }
+}
+
+/* ---------- H. Wiring / init ---------- */
+function init() {
+ // category radios
+ document.querySelectorAll('input[name="category"]').forEach((input) => {
+ input.addEventListener("change", (e) => onCategoryChange(e.target.value));
+ });
+
+ // klaxon toggle
+ els.klaxonBtn.addEventListener("click", () => setBreaking(!state.breaking));
+ els.klaxonBtn.addEventListener("keydown", (e) => {
+ if (e.key === " " || e.key === "Enter") {
+ // native button already handles this; kept for clarity/no-op safety
+ }
+ });
+
+ // sound toggle (default OFF — audio only ever plays after this explicit,
+ // user-initiated gesture AND while breaking mode is active)
+ els.soundToggle.addEventListener("change", (e) => {
+ state.soundOn = e.target.checked;
+ if (state.soundOn) ensureAudioCtx();
+ if (state.breaking) {
+ if (state.soundOn) startSiren(); else stopSiren();
+ }
+ });
+
+ // reduced-motion: respond to live changes too (Playwright sets this at
+ // context creation, but a real user could toggle OS settings mid-session)
+ const mq = window.matchMedia("(prefers-reduced-motion: reduce)");
+ const applyMotionPref = (matches) => {
+ state.reducedMotion = matches;
+ setTickerMode(matches);
+ refreshTicker();
+ };
+ mq.addEventListener
+ ? mq.addEventListener("change", (e) => applyMotionPref(e.matches))
+ : mq.addListener((e) => applyMotionPref(e.matches));
+
+ updateMoodUI();
+ setTickerMode(state.reducedMotion);
+ refreshTicker();
+ renderStories();
+ updateClock();
+ setInterval(tick, 1000);
+}
+
+document.addEventListener("DOMContentLoaded", init);
+</script>
+</body>
+</html>
(oldest)
·
back to Crazy News Channel Shadowman
·
Add admin panel: create/delete/reset controls for breaking s c180c52 →