[object Object]

← back to Crazy News Channel Shadowman

TK-12241: Shadow Man gallery page on the Cartoon Desk (approved pieces only)

c3b7dd0d5d7321bec888fb4ff3a5e8ad874a5f6d · 2026-09-25 11:07:45 -0700 · Steve

cartoons/shadow-man.html + shadow-man.js, linked from cartoons/index.html. Data comes from
cartoons/shadow-man-data.js, written by the Inkwell export script, which exports ONLY pieces
approved in the Inkwell curation UI (currently 0 -> page shows an empty state). Theme/era filters,
sort, density slider (persisted), date+time chips, <dialog> viewer. Styling added to
assets/style.css as PART 4 scoped under body.shadow-man-page; no inline styles. Not added to
manifest.js, the daily queue, or the Shorts pipeline.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

Files touched

Diff

commit c3b7dd0d5d7321bec888fb4ff3a5e8ad874a5f6d
Author: Steve <steve@designerwallcoverings.com>
Date:   Fri Sep 25 11:07:45 2026 -0700

    TK-12241: Shadow Man gallery page on the Cartoon Desk (approved pieces only)
    
    cartoons/shadow-man.html + shadow-man.js, linked from cartoons/index.html. Data comes from
    cartoons/shadow-man-data.js, written by the Inkwell export script, which exports ONLY pieces
    approved in the Inkwell curation UI (currently 0 -> page shows an empty state). Theme/era filters,
    sort, density slider (persisted), date+time chips, <dialog> viewer. Styling added to
    assets/style.css as PART 4 scoped under body.shadow-man-page; no inline styles. Not added to
    manifest.js, the daily queue, or the Shorts pipeline.
    
    Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
---
 assets/style.css             | 19 +++++++++++
 cartoons/index.html          |  1 +
 cartoons/shadow-man-data.js  |  6 ++++
 cartoons/shadow-man.html     | 57 +++++++++++++++++++++++++++++++++
 cartoons/shadow-man.js       | 76 ++++++++++++++++++++++++++++++++++++++++++++
 cartoons/shadow-man/.gitkeep |  0
 6 files changed, 159 insertions(+)

diff --git a/assets/style.css b/assets/style.css
index 25e1934..9f8aa1e 100644
--- a/assets/style.css
+++ b/assets/style.css
@@ -1360,3 +1360,22 @@ body.cartoon-page-reference footer { text-align: center; color: var(--text-dim);
 .featured-cartoon-caption {
   margin: 0; font-style: italic; color: var(--text-dim); font-size: .95rem;
 }
+
+/* ============================================================
+   PART 4 — Shadow Man gallery (cartoons/shadow-man.html, TK-12241)
+   Scoped under body.shadow-man-page. Reuses Part 2's .grid/.card/
+   .controls; only the bits that differ live here.
+   ============================================================ */
+body.shadow-man-page .sm-card { font: inherit; text-align: left; cursor: pointer; padding: 0; width: 100%; }
+body.shadow-man-page .sm-card .thumb { aspect-ratio: 7 / 6; background: #fff; }
+body.shadow-man-page .sm-card .thumb img { width: 100%; height: 100%; object-fit: contain; display: block; }
+body.shadow-man-page .sm-card-caption { font-style: italic; }
+body.shadow-man-page .sm-tags { display: flex; flex-wrap: wrap; gap: 6px; align-items: center; }
+body.shadow-man-page .sm-era { font-size: .68rem; font-weight: 800; letter-spacing: .3px; padding: .15rem .5rem; border-radius: 999px; border: 1px solid var(--panel-border); color: var(--text-dim); }
+body.shadow-man-page .sm-dialog { max-width: min(960px, 94vw); width: 100%; background: var(--panel); color: var(--text); border: 1px solid var(--panel-border); border-radius: 12px; padding: 18px 20px 20px; }
+body.shadow-man-page .sm-dialog::backdrop { background: rgba(0, 0, 0, .7); }
+body.shadow-man-page .sm-dialog h2 { margin: 0 32px 4px 0; font-size: 1.3rem; }
+body.shadow-man-page .sm-dialog img { width: 100%; height: auto; display: block; background: #fff; border-radius: 6px; margin: 10px 0; }
+body.shadow-man-page .sm-meta { font-size: .8rem; color: var(--text-dim); margin: 0; }
+body.shadow-man-page .sm-caption { font-style: italic; margin: 0; }
+body.shadow-man-page .sm-close { position: absolute; top: 10px; right: 12px; background: none; border: 0; color: var(--text); font-size: 1.6rem; cursor: pointer; line-height: 1; }
diff --git a/cartoons/index.html b/cartoons/index.html
index 211815b..4d7c4a9 100644
--- a/cartoons/index.html
+++ b/cartoons/index.html
@@ -11,6 +11,7 @@
 <header class="chrome">
   <span class="logo-badge" aria-hidden="true">P24</span>
   <h1>Political Cartoon Desk</h1>
+  <a class="home" href="shadow-man.html">Shadow Man Gallery &rarr;</a>
   <a class="home" href="reference/index.html">Style Reference: Classic Editorial Ink &rarr;</a>
   <a class="home" href="../index.html">&larr; Back to PANDEMONIUM-24</a>
 </header>
diff --git a/cartoons/shadow-man-data.js b/cartoons/shadow-man-data.js
new file mode 100644
index 0000000..b7244e1
--- /dev/null
+++ b/cartoons/shadow-man-data.js
@@ -0,0 +1,6 @@
+// Generated by the Inkwell export script (TK-12241) — do not edit by hand.
+// Only pieces approved in the Inkwell curation UI are exported.
+window.P24_SHADOW_MAN = {
+  "exported_at": "2026-09-25T18:06:40.257Z",
+  "items": []
+};
diff --git a/cartoons/shadow-man.html b/cartoons/shadow-man.html
new file mode 100644
index 0000000..f38a3fd
--- /dev/null
+++ b/cartoons/shadow-man.html
@@ -0,0 +1,57 @@
+<!DOCTYPE html>
+<html lang="en">
+<head>
+<meta charset="UTF-8" />
+<meta name="viewport" content="width=device-width, initial-scale=1.0" />
+<title>Shadow Man — P24 Political Cartoon Desk</title>
+<meta name="description" content="Shadow Man: original single-panel editorial cartoons from the P24 Cartoon Desk, written fresh from classic editorial themes and eras." />
+<link rel="stylesheet" href="../assets/style.css" />
+</head>
+<body data-mood="all" class="shadow-man-page">
+<header class="chrome">
+  <span class="logo-badge" aria-hidden="true">P24</span>
+  <h1>Shadow Man</h1>
+  <a class="home" href="index.html">&larr; Cartoon Desk</a>
+  <a class="home" href="../index.html">PANDEMONIUM-24</a>
+</header>
+<main>
+  <p class="dek">Original single-panel editorial cartoons signed &ldquo;Shadow Man&rdquo; &mdash; new scenes and captions written from classic editorial themes and eras. Every figure is an unnamed archetype; no piece redraws any published cartoon.</p>
+
+  <div class="controls" role="group" aria-label="Filter, sort and display density">
+    <label for="smTheme">Theme
+      <select id="smTheme"><option value="all">All themes</option></select>
+    </label>
+    <label for="smEra">Era
+      <select id="smEra"><option value="all">All eras</option></select>
+    </label>
+    <label for="smSort">Sort
+      <select id="smSort">
+        <option value="newest">Newest</option>
+        <option value="oldest">Oldest</option>
+        <option value="title">Title A&rarr;Z</option>
+        <option value="theme">Theme</option>
+        <option value="era">Era</option>
+      </select>
+    </label>
+    <label for="smDensity">Density
+      <input type="range" id="smDensity" min="220" max="520" step="20" value="320" aria-valuetext="card width">
+    </label>
+    <span id="smCount"></span>
+  </div>
+
+  <div class="grid sm-grid" id="smGrid" role="list"></div>
+  <div class="empty" id="smEmpty" hidden><p id="smEmptyMsg">No Shadow Man cartoons match this filter.</p></div>
+
+  <dialog class="sm-dialog" id="smDialog" aria-labelledby="smDialogTitle">
+    <button type="button" class="sm-close" id="smClose" aria-label="Close">&times;</button>
+    <h2 id="smDialogTitle"></h2>
+    <p class="sm-meta" id="smDialogMeta"></p>
+    <img id="smDialogImg" alt="" />
+    <p class="sm-caption" id="smDialogCaption"></p>
+  </dialog>
+</main>
+<footer>Shadow Man cartoons are original, AI-assisted invented satire rendered locally. All persons are fictional archetypes. Part of PANDEMONIUM-24.</footer>
+<script src="shadow-man-data.js"></script>
+<script src="shadow-man.js"></script>
+</body>
+</html>
diff --git a/cartoons/shadow-man.js b/cartoons/shadow-man.js
new file mode 100644
index 0000000..27773c7
--- /dev/null
+++ b/cartoons/shadow-man.js
@@ -0,0 +1,76 @@
+// P24 — Shadow Man gallery (TK-12241). Data: shadow-man-data.js (window.P24_SHADOW_MAN).
+// Theme + era filters, sort, density slider — all persisted to localStorage. Styling lives in
+// ../assets/style.css (body.shadow-man-page); this file sets no inline styles except the
+// --card-w custom property the shared gallery grid already reads.
+(function () {
+  const ITEMS = (window.P24_SHADOW_MAN && window.P24_SHADOW_MAN.items) || [];
+  const $ = (id) => document.getElementById(id);
+  const LS = { theme: 'p24shadowman.theme', era: 'p24shadowman.era', sort: 'p24shadowman.sort', density: 'p24shadowman.density' };
+  const get = (k, d) => { try { return localStorage.getItem(k) ?? d; } catch { return d; } };
+  const put = (k, v) => { try { localStorage.setItem(k, v); } catch {} };
+  const esc = (s) => String(s == null ? '' : s).replace(/[&<>"']/g, c => ({ '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;' }[c]));
+  const fmt = (iso) => { const d = new Date(iso); return isNaN(d) ? '' : d.toLocaleString(undefined, { year: 'numeric', month: 'short', day: 'numeric', hour: 'numeric', minute: '2-digit' }); };
+
+  const state = { theme: get(LS.theme, 'all'), era: get(LS.era, 'all'), sort: get(LS.sort, 'newest'), density: get(LS.density, '320') };
+
+  function fill(sel, values, cur, label) {
+    sel.innerHTML = `<option value="all">${esc(label)}</option>` + values.map(v => `<option value="${esc(v)}">${esc(v)}</option>`).join('');
+    sel.value = values.includes(cur) ? cur : 'all';
+    return sel.value;
+  }
+
+  function sorted(list) {
+    const t = (x) => new Date(x.created_at).getTime() || 0;
+    const a = [...list];
+    switch (state.sort) {
+      case 'oldest': return a.sort((x, y) => t(x) - t(y) || x.id.localeCompare(y.id));
+      case 'title': return a.sort((x, y) => x.title.localeCompare(y.title));
+      case 'theme': return a.sort((x, y) => x.theme.localeCompare(y.theme) || x.title.localeCompare(y.title));
+      case 'era': return a.sort((x, y) => x.era.localeCompare(y.era) || x.title.localeCompare(y.title));
+      default: return a.sort((x, y) => t(y) - t(x) || y.id.localeCompare(x.id));
+    }
+  }
+
+  function render() {
+    const list = sorted(ITEMS.filter(i => (state.theme === 'all' || i.theme === state.theme) && (state.era === 'all' || i.era === state.era)));
+    $('smCount').textContent = `${list.length} of ${ITEMS.length}`;
+    $('smEmpty').hidden = list.length > 0;
+    $('smEmptyMsg').textContent = ITEMS.length ? 'No Shadow Man cartoons match this filter.' : 'No Shadow Man cartoons have been approved for publication yet.';
+    $('smGrid').innerHTML = list.map(it => `
+      <button type="button" class="card sm-card" role="listitem" data-id="${esc(it.id)}">
+        <span class="thumb"><img src="${esc(it.src)}" alt="${esc(it.title)} — ${esc(it.caption)}" loading="lazy"></span>
+        <span class="body">
+          <span class="title">${esc(it.title)}</span>
+          <span class="blurb sm-card-caption">${esc(it.caption)}</span>
+          <span class="sm-tags"><span class="cat-badge">${esc(it.theme)}</span><span class="sm-era">${esc(it.era)}</span></span>
+          <span class="meta" title="${esc(it.created_at)}">🕓 ${esc(fmt(it.created_at))}</span>
+        </span>
+      </button>`).join('');
+  }
+
+  function open(id) {
+    const it = ITEMS.find(x => x.id === id);
+    if (!it) return;
+    $('smDialogTitle').textContent = it.title;
+    $('smDialogMeta').textContent = `${it.theme} · ${it.era} · signed ${it.signature}`;
+    $('smDialogImg').src = it.src;
+    $('smDialogImg').alt = `${it.title} — ${it.caption}`;
+    $('smDialogCaption').textContent = it.caption;
+    $('smDialog').showModal();
+  }
+
+  state.theme = fill($('smTheme'), [...new Set(ITEMS.map(i => i.theme))].sort(), state.theme, 'All themes');
+  state.era = fill($('smEra'), [...new Set(ITEMS.map(i => i.era))].sort(), state.era, 'All eras');
+  $('smSort').value = state.sort;
+  $('smDensity').value = state.density;
+  document.documentElement.style.setProperty('--card-w', state.density + 'px');
+
+  $('smTheme').addEventListener('change', e => { state.theme = e.target.value; put(LS.theme, state.theme); render(); });
+  $('smEra').addEventListener('change', e => { state.era = e.target.value; put(LS.era, state.era); render(); });
+  $('smSort').addEventListener('change', e => { state.sort = e.target.value; put(LS.sort, state.sort); render(); });
+  $('smDensity').addEventListener('input', e => { state.density = e.target.value; put(LS.density, state.density); document.documentElement.style.setProperty('--card-w', state.density + 'px'); });
+  $('smGrid').addEventListener('click', e => { const c = e.target.closest('.sm-card'); if (c) open(c.dataset.id); });
+  $('smClose').addEventListener('click', () => $('smDialog').close());
+  $('smDialog').addEventListener('click', e => { if (e.target === $('smDialog')) $('smDialog').close(); });
+  render();
+})();
diff --git a/cartoons/shadow-man/.gitkeep b/cartoons/shadow-man/.gitkeep
new file mode 100644
index 0000000..e69de29

← 9c5d41b auto-data-snapshot: 2026-09-25T10:22:25 (2 data files) — .cl  ·  back to Crazy News Channel Shadowman  ·  (newest)