[object Object]

← back to Crazy News Channel Shadowman

cartoons gallery: front-page per-category colors + category chip filter

90bdc436d193375ef1e3afe9fdc8cce7d51c7aef · 2026-09-24 10:30:34 -0700 · Steve Abrams

Replace the single hard-coded red palette with the same 8 mood blocks
(all/politics/weather/scitech/sports/entertainment/business/uncategorized)
from ../index.html, add the matching 8-chip category filter that retunes
body[data-mood] the same way, give each card a data-cat + --cat-accent
color (left border + badge) so the All view is multicolored, classify
cartoons via section -> story_id lookup (../stories-data.js) -> "Political
Cartoon" -> uncategorized, and persist the selection via
localStorage/#/category/<key> hash (hash wins on load). Sort, density,
grid/list toggle, and textContent-only rendering all preserved.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DqUe1Hvqsnrkxpe4R6jrk1

Files touched

Diff

commit 90bdc436d193375ef1e3afe9fdc8cce7d51c7aef
Author: Steve Abrams <steve@designerwallcoverings.com>
Date:   Thu Sep 24 10:30:34 2026 -0700

    cartoons gallery: front-page per-category colors + category chip filter
    
    Replace the single hard-coded red palette with the same 8 mood blocks
    (all/politics/weather/scitech/sports/entertainment/business/uncategorized)
    from ../index.html, add the matching 8-chip category filter that retunes
    body[data-mood] the same way, give each card a data-cat + --cat-accent
    color (left border + badge) so the All view is multicolored, classify
    cartoons via section -> story_id lookup (../stories-data.js) -> "Political
    Cartoon" -> uncategorized, and persist the selection via
    localStorage/#/category/<key> hash (hash wins on load). Sort, density,
    grid/list toggle, and textContent-only rendering all preserved.
    
    Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
    Claude-Session: https://claude.ai/code/session_01DqUe1Hvqsnrkxpe4R6jrk1
---
 cartoons/index.html | 283 ++++++++++++++++++++++++++++++++++++++++++++++++++--
 1 file changed, 274 insertions(+), 9 deletions(-)

diff --git a/cartoons/index.html b/cartoons/index.html
index 8aa267f..392e42c 100644
--- a/cartoons/index.html
+++ b/cartoons/index.html
@@ -6,18 +6,75 @@
 <title>P24 Political Cartoon Desk — PANDEMONIUM-24</title>
 <meta name="description" content="The P24 Political Cartoon Desk archive — original invented satire strips, updated daily." />
 <style>
-:root {
+/* ---------- Mood theming — CSS custom properties ----------
+   Mirrors the palette blocks in ../index.html exactly (:root, body[data-mood]).
+   Default ("all") is the pale mint/green from the main page's default mood. */
+:root { --card-w: 300px; }
+:root, body[data-mood="all"] {
+  --bg: #cdf5e0;
+  --bg2: #f2fff2;
+  --panel: #ffffff;
+  --panel-border: #0f9d7833;
+  --accent: #0f9d78;
+  --accent2: #056659;
+  --text: #123524;
+  --text-dim: #12352499;
+  --chrome: #ffffff;
+  --focus: #0b3d91;
+}
+body[data-mood="politics"] {
   --bg: #2a0b0b; --bg2: #4a0f0f; --panel: #3a1212; --panel-border: #ff3b3b55;
   --accent: #ff3b3b; --accent2: #ffb703; --text: #ffe9e9; --text-dim: #ffd7d7aa;
-  --chrome: #220808; --focus: #ffd166; --card-w: 300px;
+  --chrome: #220808; --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: #05132c; --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: #200f00; --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: #210822; --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: #06180c; --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: #08081c; --focus: #ff00e6;
 }
+body[data-mood="uncategorized"] {
+  --bg: #e9edf0; --bg2: #ffffff; --panel: #ffffff; --panel-border: #54657833;
+  --accent: #546578; --accent2: #2f3b47; --text: #20272d; --text-dim: #20272d99;
+  --chrome: #ffffff; --focus: #0b3d91;
+}
+
 *,*::before,*::after{box-sizing:border-box}
 html{overflow-x:hidden}
 [hidden]{display:none!important}
+.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;
+}
 body{
   margin:0; 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%);
   line-height:1.4;
+  transition: background-color .4s ease, color .4s ease;
 }
 a{color:var(--accent2)}
 h1,h2,p{margin:0}
@@ -29,6 +86,37 @@ h1,h2,p{margin:0}
 .chrome a.home:hover{color:var(--text)}
 main{max-width:1200px; margin:0 auto; padding:20px}
 .dek{color:var(--text-dim); max-width:60ch; margin-bottom:18px}
+
+/* ---------- Category filter (chip row) — mirrors ../index.html ---------- */
+.category-filter{ margin-bottom:18px; }
+.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;
+}
+
 .controls{display:flex; gap:16px; flex-wrap:wrap; align-items:center; background:var(--panel); border:1px solid var(--panel-border); border-radius:10px; padding:12px 16px; margin-bottom:20px}
 .controls label{font-size:.8rem; color:var(--text-dim); display:flex; gap:8px; align-items:center}
 .controls select, .controls input[type=range]{accent-color:var(--accent)}
@@ -39,10 +127,30 @@ main{max-width:1200px; margin:0 auto; padding:20px}
 .view-toggle button[aria-pressed="true"]{background:var(--accent); color:var(--chrome); font-weight:700; border-color:var(--accent)}
 #count{margin-left:auto; font-size:.8rem; color:var(--text-dim)}
 .grid{display:grid; grid-template-columns:repeat(auto-fill, minmax(var(--card-w), 1fr)); gap:18px}
-.card{background:var(--panel); border:1px solid var(--panel-border); border-radius:12px; overflow:hidden; display:flex; flex-direction:column; text-decoration:none; color:var(--text); transition:transform .15s ease, border-color .15s ease}
+
+/* ---------- Per-category card accent ---------- */
+/* Each card carries data-cat, which sets --cat-accent to that category's
+   mood --accent (see body[data-mood] blocks above). This keeps cards
+   multicolored in the "All Categories" view regardless of the page's
+   current mood, and still tracks the palette if these values ever change. */
+.card[data-cat="politics"]{ --cat-accent:#ff3b3b; }
+.card[data-cat="weather"]{ --cat-accent:#4fd1ff; }
+.card[data-cat="sports"]{ --cat-accent:#ff9800; }
+.card[data-cat="entertainment"]{ --cat-accent:#ff5fd1; }
+.card[data-cat="business"]{ --cat-accent:#2ecc71; }
+.card[data-cat="scitech"]{ --cat-accent:#00f0ff; }
+.card[data-cat="uncategorized"]{ --cat-accent:#546578; }
+
+.card{background:var(--panel); border:1px solid var(--panel-border); border-left:5px solid var(--cat-accent, var(--accent)); border-radius:12px; overflow:hidden; display:flex; flex-direction:column; text-decoration:none; color:var(--text); transition:transform .15s ease, border-color .15s ease}
 .card:hover, .card:focus-visible{transform:translateY(-3px); border-color:var(--accent)}
-.card .thumb{aspect-ratio:4/3; background:var(--chrome) center/cover no-repeat; display:flex; align-items:center; justify-content:center; font-size:2.4rem}
+.card .thumb{aspect-ratio:4/3; background:var(--chrome) center/cover no-repeat; display:flex; align-items:center; justify-content:center; font-size:2.4rem; position:relative}
 .card .body{padding:12px 14px; display:flex; flex-direction:column; gap:6px; min-width:0}
+.card .cat-badge{
+  align-self:flex-start;
+  font-size:.68rem; font-weight:800; letter-spacing:.3px; text-transform:uppercase;
+  padding:.15rem .5rem; border-radius:999px;
+  background:var(--cat-accent, var(--accent)); color:var(--chrome);
+}
 .card .title{font-weight:700; font-size:1rem}
 .card .meta{font-size:.75rem; color:var(--text-dim)}
 .card .blurb{font-size:.85rem; color:var(--text-dim)}
@@ -59,10 +167,10 @@ main{max-width:1200px; margin:0 auto; padding:20px}
 .empty{background:var(--panel); border:1px dashed var(--panel-border); border-radius:12px; padding:40px 20px; text-align:center; color:var(--text-dim)}
 .empty a{display:inline-block; margin-top:10px}
 footer{max-width:1200px; margin:30px auto; padding:0 20px 30px; color:var(--text-dim); font-size:.75rem}
-@media (prefers-reduced-motion: reduce){ .card{transition:none} }
+@media (prefers-reduced-motion: reduce){ .card{transition:none} body{transition:none} }
 </style>
 </head>
-<body>
+<body data-mood="all">
 <header class="chrome">
   <span class="logo-badge" aria-hidden="true">P24</span>
   <h1>Political Cartoon Desk</h1>
@@ -71,6 +179,46 @@ footer{max-width:1200px; margin:30px auto; padding:0 20px 30px; color:var(--text
 <main>
   <p class="dek">Original, invented editorial satire — bureaucracy, committees, and the occasional pigeon director. New strips added daily. All persons, departments, and forms are fictional.</p>
 
+  <nav class="category-filter" aria-label="Category and mood filter">
+    <fieldset>
+      <legend class="visually-hidden">Choose a cartoon category — changes the page's color mood</legend>
+      <div class="chip-row" id="chipRow">
+        <span class="chip-item">
+          <input type="radio" name="cartoonCategory" id="cat-all" value="all" checked />
+          <label for="cat-all">All Categories</label>
+        </span>
+        <span class="chip-item">
+          <input type="radio" name="cartoonCategory" id="cat-politics" value="politics" />
+          <label for="cat-politics">Politics</label>
+        </span>
+        <span class="chip-item">
+          <input type="radio" name="cartoonCategory" id="cat-weather" value="weather" />
+          <label for="cat-weather">Weather</label>
+        </span>
+        <span class="chip-item">
+          <input type="radio" name="cartoonCategory" id="cat-scitech" value="scitech" />
+          <label for="cat-scitech">Sci-Tech</label>
+        </span>
+        <span class="chip-item">
+          <input type="radio" name="cartoonCategory" id="cat-sports" value="sports" />
+          <label for="cat-sports">Sports</label>
+        </span>
+        <span class="chip-item">
+          <input type="radio" name="cartoonCategory" id="cat-entertainment" value="entertainment" />
+          <label for="cat-entertainment">Entertainment</label>
+        </span>
+        <span class="chip-item">
+          <input type="radio" name="cartoonCategory" id="cat-business" value="business" />
+          <label for="cat-business">Business</label>
+        </span>
+        <span class="chip-item">
+          <input type="radio" name="cartoonCategory" id="cat-uncategorized" value="uncategorized" />
+          <label for="cat-uncategorized">Uncategorized</label>
+        </span>
+      </div>
+    </fieldset>
+  </nav>
+
   <div class="controls" role="group" aria-label="Sort and display density">
     <label for="sortSel">Sort
       <select id="sortSel">
@@ -91,23 +239,27 @@ footer{max-width:1200px; margin:30px auto; padding:0 20px 30px; color:var(--text
 
   <div class="grid" id="grid" role="list"></div>
   <div class="empty" id="empty" hidden>
-    <p>No cartoons on file yet — the Desk is between assignments.</p>
+    <p id="emptyMsg">No cartoons on file yet — the Desk is between assignments.</p>
     <a href="../index.html">Read the rest of P24 &rarr;</a>
   </div>
 </main>
 <footer>All cartoons are invented satire. No actual bureaucrats were inconvenienced. Part of PANDEMONIUM-24.</footer>
+<script src="../stories-data.js"></script>
 <script src="manifest.js"></script>
 <script>
 (function(){
   const grid = document.getElementById('grid');
   const empty = document.getElementById('empty');
+  const emptyMsg = document.getElementById('emptyMsg');
   const sortSel = document.getElementById('sortSel');
   const density = document.getElementById('density');
   const densityLabel = document.getElementById('densityLabel');
   const viewGridBtn = document.getElementById('viewGridBtn');
   const viewListBtn = document.getElementById('viewListBtn');
   const count = document.getElementById('count');
+  const chipRow = document.getElementById('chipRow');
   const LS_SORT = 'p24cartoons.sort', LS_DENSITY = 'p24cartoons.density', LS_VIEW = 'p24cartoons.viewmode';
+  const LS_CATEGORY = 'p24cartoons.category';
 
   function safeGet(k, d){ try { return localStorage.getItem(k) ?? d; } catch { return d; } }
   function safeSet(k, v){ try { localStorage.setItem(k, v); } catch {} }
@@ -141,19 +293,130 @@ footer{max-width:1200px; margin:30px auto; padding:0 20px 30px; color:var(--text
     catch { return iso; }
   }
 
+  /* ---------- Category classification ---------- */
+  // Same 8 keys as ../index.html's mood system ("all" plus these 7).
+  const CATS = [
+    { key: 'all', label: 'All Categories' },
+    { key: 'politics', label: 'Politics' },
+    { key: 'weather', label: 'Weather' },
+    { key: 'scitech', label: 'Sci-Tech' },
+    { key: 'sports', label: 'Sports' },
+    { key: 'entertainment', label: 'Entertainment' },
+    { key: 'business', label: 'Business' },
+    { key: 'uncategorized', label: 'Uncategorized' },
+  ];
+  const CATEGORY_KEYS = CATS.filter((c) => c.key !== 'all').map((c) => c.key);
+  const CATEGORY_LABEL = Object.fromEntries(CATS.map((c) => [c.key, c.label]));
+
+  function isValidCategory(key){ return CATS.some((c) => c.key === key); }
+
+  // Looks a cartoon's linked story up by id across every loaded story data
+  // source (currently just ../stories-data.js's window.P24_EXTRA_STORIES —
+  // ../real-news-data.js is not loaded here because none of the current
+  // manifest.js story_id values resolve there; add it the same way if a
+  // future cartoon links a real-news story).
+  function storyCategoryById(id){
+    if (!id) return null;
+    const sources = [];
+    if (Array.isArray(window.P24_EXTRA_STORIES)) sources.push(window.P24_EXTRA_STORIES);
+    if (Array.isArray(window.P24_REAL_STORIES)) sources.push(window.P24_REAL_STORIES);
+    for (const arr of sources) {
+      const found = arr.find((s) => s && s.id === id);
+      if (found && found.category) return found.category;
+    }
+    return null;
+  }
+
+  // Category of a cartoon, in priority order:
+  //  1. c.section, if it's one of the 7 category keys
+  //  2. c.story_id, looked up against the story data sources above
+  //  3. c.category === "Political Cartoon" -> "politics"
+  //  4. "uncategorized"
+  function categoryOf(c){
+    if (c.section && CATEGORY_KEYS.includes(c.section)) return c.section;
+    if (c.story_id) {
+      const found = storyCategoryById(c.story_id);
+      if (found && CATEGORY_KEYS.includes(found)) return found;
+    }
+    if (c.category === 'Political Cartoon') return 'politics';
+    return 'uncategorized';
+  }
+
+  function getCategoryFromHash(){
+    const m = location.hash.match(/^#\/category\/(.+)$/);
+    if (!m) return null;
+    try { return decodeURIComponent(m[1]); } catch { return null; }
+  }
+
+  let currentCategory = getCategoryFromHash();
+  if (!currentCategory || !isValidCategory(currentCategory)) {
+    currentCategory = safeGet(LS_CATEGORY, 'all');
+  }
+  if (!isValidCategory(currentCategory)) currentCategory = 'all';
+
+  function syncChipRadio(){
+    const input = document.getElementById(`cat-${currentCategory}`);
+    if (input) input.checked = true;
+  }
+
+  function applyMood(){
+    document.body.dataset.mood = currentCategory;
+  }
+
+  function setCategory(key, opts){
+    opts = opts || {};
+    if (!isValidCategory(key)) return;
+    currentCategory = key;
+    applyMood();
+    syncChipRadio();
+    safeSet(LS_CATEGORY, currentCategory);
+    if (!opts.fromHash) {
+      const target = currentCategory === 'all' ? '' : `#/category/${currentCategory}`;
+      if (location.hash !== target) location.hash = target;
+    }
+    render();
+  }
+
+  chipRow.addEventListener('change', (e) => {
+    const t = e.target;
+    if (t && t.name === 'cartoonCategory') setCategory(t.value);
+  });
+
+  window.addEventListener('hashchange', () => {
+    const fromHash = getCategoryFromHash();
+    if (fromHash && isValidCategory(fromHash) && fromHash !== currentCategory) {
+      setCategory(fromHash, { fromHash: true });
+    } else if (!fromHash && currentCategory !== 'all' && location.hash === '') {
+      setCategory('all', { fromHash: true });
+    }
+  });
+
+  // Apply the resolved initial category before first render.
+  applyMood();
+  syncChipRadio();
+
   let cartoons = [];
   function render(){
-    const sorted = [...cartoons];
+    const filtered = currentCategory === 'all'
+      ? cartoons
+      : cartoons.filter((c) => categoryOf(c) === currentCategory);
+
+    const sorted = [...filtered];
     if (sortSel.value === 'newest') sorted.sort((a,b)=> (b.created_at||'').localeCompare(a.created_at||''));
     else if (sortSel.value === 'oldest') sorted.sort((a,b)=> (a.created_at||'').localeCompare(b.created_at||''));
     else if (sortSel.value === 'title') sorted.sort((a,b)=> a.title.localeCompare(b.title));
 
     grid.innerHTML = '';
     empty.hidden = sorted.length !== 0;
+    emptyMsg.textContent = currentCategory === 'all'
+      ? 'No cartoons on file yet — the Desk is between assignments.'
+      : `No cartoons in ${CATEGORY_LABEL[currentCategory] || currentCategory} yet.`;
     count.textContent = sorted.length ? `${sorted.length} strip${sorted.length===1?'':'s'} on file` : '';
     for (const c of sorted) {
+      const cat = categoryOf(c);
       const a = document.createElement('a');
       a.className = 'card'; a.href = c.file; a.role = 'listitem';
+      a.dataset.cat = cat;
 
       const thumb = document.createElement('div');
       thumb.className = 'thumb'; thumb.setAttribute('aria-hidden', 'true');
@@ -161,13 +424,15 @@ footer{max-width:1200px; margin:30px auto; padding:0 20px 30px; color:var(--text
       else thumb.textContent = '\u{1F58B}\uFE0F';
 
       const body = document.createElement('div'); body.className = 'body';
+      const badge = document.createElement('span'); badge.className = 'cat-badge';
+      badge.textContent = CATEGORY_LABEL[cat] || cat;
       const title = document.createElement('div'); title.className = 'title'; title.textContent = c.title || '';
       const meta = document.createElement('div'); meta.className = 'meta';
       meta.title = c.created_at || '';
       meta.textContent = (c.created_at ? fmtDate(c.created_at) : '') + (c.category ? ' \u00b7 ' + c.category : '');
       const blurb = document.createElement('div'); blurb.className = 'blurb'; blurb.textContent = c.blurb || '';
 
-      body.append(title, meta, blurb);
+      body.append(badge, title, meta, blurb);
       a.append(thumb, body);
       grid.appendChild(a);
     }

← 3e6ffab P24: add first cartoon for business/entertainment/scitech/sp  ·  back to Crazy News Channel Shadowman  ·  Add P24 cartoon: The Quorum Call (invented legislative hide- 35cc1c2 →