[object Object]

← 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

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 `&gt;` 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&nbsp;<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&nbsp;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 →