[object Object]

← back to Crazy News Channel

Category view: V4 Newspaper Column dense listing + cartoon-thumb slot for politics (TK-12138)

0839cdd1fd80291bb20bc68c3cb98d6bd5c010db · 2026-09-24 10:08:36 -0700 · Steve Abrams

Steve picked V4 (of 6 Figma mockups: figma.com/design/oqcZhYvN4kWvw0VRUa1FYG) — dense
multi-column newspaper-style headline listing for the category route (#/category/<x>),
distinct from the 'All Categories' home view which keeps its original photo-card stack.

- .story-grid.newspaper-mode: true CSS column-count flow (1/2/3/4 cols by breakpoint),
  restyles the SAME storyCardHTML() markup used everywhere else — no markup fork, so
  patchStoryGrid()'s keyed morph reconcile never bridges a shape change from this toggle
  alone (a category route change already swaps the id set on its own).
- Inline category label (.np-cat-label) replaces the badge-pill kicker in dense mode;
  detail paragraph + read-more link hidden (headline does the work); escalation bar
  shrinks to a left rail-style accent.
- Toggled in renderStories(): els.storyGrid.classList.toggle('newspaper-mode',
  state.category !== 'all').
- findCartoonForStory(): looks up cartoons/manifest.js (window.P24_CARTOONS, now loaded
  via a new <script> tag) by story_id. A POLITICS story with a linked cartoon shows a
  cartoon-thumb link INSTEAD OF its photo, in the same image slot, styled for both card
  and newspaper mode. Gracefully falls back to the plain photo (or nothing) when no
  cartoon is registered — safe to ship ahead of / independent from the cartoon content
  landing. Real-news political stories are NEVER cartoon-linked (only invented pw-*
  stories get one — the Cartoon Desk's own policy: invented bureaucracy only, never real
  people/parties).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XKaojLnVW8hyxsVriY5f9a

Files touched

Diff

commit 0839cdd1fd80291bb20bc68c3cb98d6bd5c010db
Author: Steve Abrams <steve@designerwallcoverings.com>
Date:   Thu Sep 24 10:08:36 2026 -0700

    Category view: V4 Newspaper Column dense listing + cartoon-thumb slot for politics (TK-12138)
    
    Steve picked V4 (of 6 Figma mockups: figma.com/design/oqcZhYvN4kWvw0VRUa1FYG) — dense
    multi-column newspaper-style headline listing for the category route (#/category/<x>),
    distinct from the 'All Categories' home view which keeps its original photo-card stack.
    
    - .story-grid.newspaper-mode: true CSS column-count flow (1/2/3/4 cols by breakpoint),
      restyles the SAME storyCardHTML() markup used everywhere else — no markup fork, so
      patchStoryGrid()'s keyed morph reconcile never bridges a shape change from this toggle
      alone (a category route change already swaps the id set on its own).
    - Inline category label (.np-cat-label) replaces the badge-pill kicker in dense mode;
      detail paragraph + read-more link hidden (headline does the work); escalation bar
      shrinks to a left rail-style accent.
    - Toggled in renderStories(): els.storyGrid.classList.toggle('newspaper-mode',
      state.category !== 'all').
    - findCartoonForStory(): looks up cartoons/manifest.js (window.P24_CARTOONS, now loaded
      via a new <script> tag) by story_id. A POLITICS story with a linked cartoon shows a
      cartoon-thumb link INSTEAD OF its photo, in the same image slot, styled for both card
      and newspaper mode. Gracefully falls back to the plain photo (or nothing) when no
      cartoon is registered — safe to ship ahead of / independent from the cartoon content
      landing. Real-news political stories are NEVER cartoon-linked (only invented pw-*
      stories get one — the Cartoon Desk's own policy: invented bureaucracy only, never real
      people/parties).
    
    Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
    Claude-Session: https://claude.ai/code/session_01XKaojLnVW8hyxsVriY5f9a
---
 index.html | 118 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-
 1 file changed, 117 insertions(+), 1 deletion(-)

diff --git a/index.html b/index.html
index 11a59de..0c1cce3 100644
--- a/index.html
+++ b/index.html
@@ -309,6 +309,22 @@ main#channelContainer::after {
 }
 .story-card.concluded .story-photo { filter: grayscale(.6); }
 .article-photo img { width: 100%; height: auto; border-radius: .5rem; display: block; }
+/* Cartoon-thumb base style (card-view / "All Categories" default — the
+   compact inline-link treatment for newspaper-mode overrides this above). A
+   politics story with a linked cartoon shows this INSTEAD OF a photo, same
+   banner slot, so it never looks like a missing image. */
+.story-cartoon-thumb {
+  display: flex; align-items: center; gap: .6rem;
+  width: calc(100% + 2rem); margin: -1rem -1rem .2rem; padding: 1.1rem 1rem;
+  border-radius: .6rem .6rem 0 0;
+  background: color-mix(in srgb, var(--accent) 12%, var(--panel));
+  border-bottom: 1px solid var(--panel-border);
+  text-decoration: none; color: var(--accent2);
+  font-weight: 700; font-size: .95rem;
+}
+.story-cartoon-thumb:hover, .story-cartoon-thumb:focus-visible { background: color-mix(in srgb, var(--accent) 20%, var(--panel)); }
+.story-cartoon-thumb .ct-icon { font-size: 1.3rem; }
+.story-card.concluded .story-cartoon-thumb { filter: grayscale(.5); }
 .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 {
@@ -339,6 +355,74 @@ main#channelContainer::after {
   background: var(--text); color: var(--bg);
 }
 
+/* ---------- 5b. Category-view "Newspaper Column" (V4) ----------
+   Applied only when state.category !== "all" (a specific category route,
+   e.g. #/category/scitech) via .story-grid.newspaper-mode, toggled in
+   renderStories(). Restyles the SAME storyCardHTML() markup used by the "All
+   Categories" card stack — no markup fork, so patchStoryGrid()'s keyed morph
+   reconcile never has to bridge a shape change from this toggle alone. True
+   CSS multi-column flow (column-count), inline category label instead of the
+   badge pill, headline-only density (detail paragraph hidden), a left rail
+   whose height reads escalation, and a compact photo/cartoon thumbnail. */
+.np-cat-label { display: none; }
+.story-grid.newspaper-mode {
+  display: block;
+  column-gap: 1.5rem;
+  column-count: 1;
+}
+@media (min-width: 640px) { .story-grid.newspaper-mode { column-count: 2; } }
+@media (min-width: 1024px) { .story-grid.newspaper-mode { column-count: 3; } }
+@media (min-width: 1440px) { .story-grid.newspaper-mode { column-count: 4; } }
+.story-grid.newspaper-mode .story-card {
+  break-inside: avoid;
+  display: block;
+  margin: 0 0 .9rem;
+  padding: 0 0 .1rem .7rem;
+  border: 0;
+  border-left: 3px solid var(--panel-border);
+  border-radius: 0;
+  box-shadow: none;
+}
+.story-grid.newspaper-mode .story-card.lead { border-left-color: var(--accent); border-left-width: 4px; }
+.story-grid.newspaper-mode .story-card.concluded { border-left-color: var(--text-dim); }
+.story-grid.newspaper-mode .story-card .kicker { display: none; }
+.story-grid.newspaper-mode .story-card .np-cat-label {
+  display: block;
+  font: 800 .62rem/1 -apple-system, sans-serif;
+  text-transform: uppercase; letter-spacing: .05em;
+  color: var(--accent); margin-bottom: .2rem;
+}
+.story-grid.newspaper-mode .story-card.concluded .np-cat-label { color: var(--text-dim); }
+.story-grid.newspaper-mode .story-card h3 {
+  font-family: Georgia, "Times New Roman", serif;
+  font-size: 1rem; line-height: 1.22; margin-bottom: .25rem;
+}
+.story-grid.newspaper-mode .story-card.lead h3 { font-size: 1.3rem; font-style: italic; }
+.story-grid.newspaper-mode .story-card .story-detail,
+.story-grid.newspaper-mode .story-card .story-read-more { display: none; }
+.story-grid.newspaper-mode .story-card .escalation { margin: .3rem 0 0; gap: .35rem; }
+.story-grid.newspaper-mode .story-card .escalation .level-label { display: none; }
+.story-grid.newspaper-mode .story-card .escalation .seg { width: .55rem; height: .18rem; }
+.story-grid.newspaper-mode .story-card .story-meta {
+  font: 500 .66rem/1.3 ui-monospace, Menlo, monospace;
+  color: var(--text-dim); gap: .45rem; margin-top: .25rem;
+}
+.story-grid.newspaper-mode .story-card .story-meta span:last-child { display: none; } /* drop the "Updated HH:MM" clause — density */
+.story-grid.newspaper-mode .story-card .story-photo {
+  float: left; width: 56px; height: 56px; margin: 0 .55rem .3rem 0;
+  border-radius: .3rem; aspect-ratio: 1/1; object-fit: cover;
+}
+.story-grid.newspaper-mode .story-card .story-cartoon-thumb {
+  display: flex; align-items: center; gap: .4rem;
+  font: 700 .7rem/1.2 -apple-system, sans-serif;
+  color: var(--accent2); text-decoration: none;
+  border: 1px dashed var(--panel-border); border-radius: .35rem;
+  padding: .3rem .5rem; margin-bottom: .3rem;
+}
+.story-grid.newspaper-mode .story-card .story-cartoon-thumb:hover,
+.story-grid.newspaper-mode .story-card .story-cartoon-thumb:focus-visible { border-color: var(--accent); }
+.story-grid.newspaper-mode .story-card .ct-icon { font-size: .95rem; }
+
 /* ---------- 6. Ticker ---------- */
 .ticker-wrap {
   position: fixed; left: 0; right: 0; bottom: 0; z-index: 20;
@@ -978,6 +1062,7 @@ body.chaos-tier-3 main#channelContainer {
 
 <script src="stories-data.js"></script>
 <script src="real-news-data.js"></script>
+<script src="cartoons/manifest.js"></script>
 <script>
 /* ============================================================
    PANDEMONIUM-24 — behaviour
@@ -1460,6 +1545,19 @@ function computeLead(list) {
   return live.reduce((lead, s) => (s.stageIndex > lead.stageIndex ? s : lead), live[0]);
 }
 
+// Looks up a Political Cartoon Desk entry whose story_id matches this story
+// (cartoons/manifest.js, loaded as window.P24_CARTOONS — see <script> tag near
+// stories-data.js). Returns null if the manifest hasn't loaded or no cartoon
+// is linked to this story. Only invented politics stories (pw-*) are ever
+// linked — real-news political stories are never given a satirical cartoon
+// (the Cartoon Desk's own policy: invented bureaucracy only, never real
+// people/parties — see scripts/cartoon-prompt-bank.mjs header comment).
+function findCartoonForStory(storyId) {
+  const list = window.P24_CARTOONS;
+  if (!Array.isArray(list)) return null;
+  return list.find((c) => c.story_id === storyId) || null;
+}
+
 function storyCardHTML(story, isLead) {
   const stage = story.stages[story.stageIndex];
   const concluded = story.stageIndex === story.stages.length - 1;
@@ -1470,16 +1568,27 @@ function storyCardHTML(story, isLead) {
   const ariaLevel = `Escalation level ${story.stageIndex + 1} of ${story.stages.length}: ${levelLabel}`;
 
   const justAdded = story.id === state.justCreatedId;
+  const cartoon = findCartoonForStory(story.id);
+  // Image slot: a linked cartoon (politics stories only) takes priority over
+  // the plain photo. Both are present-or-absent by the same rule in every
+  // render mode — the CATEGORY-VIEW "newspaper" CSS below just restyles
+  // whichever one is here, it never changes which one gets picked.
+  const imageSlotHTML = cartoon
+    ? `<a class="story-cartoon-thumb" href="cartoons/${escapeHTML(cartoon.file)}"><span class="ct-icon" aria-hidden="true">🖋️</span><span class="ct-label">Cartoon: ${escapeHTML(cartoon.title)}</span></a>`
+    : (story.image && !FAILED_IMAGES.has(story.image)
+        ? `<img class="story-photo" src="${escapeHTML(story.image)}" alt="" width="800" height="457" ${isLead ? 'fetchpriority="high"' : 'loading="lazy"'} onerror="imageFailed(this)">`
+        : "");
   return `
     <article class="story-card${isLead ? " lead" : ""}${concluded ? " concluded" : ""}${justAdded ? " just-added" : ""}"
               data-id="${escapeHTML(story.id)}"
               aria-label="${isLead ? "Lead story" : "Story"}: ${escapeHTML(story.categoryLabel)}">
-      ${story.image && !FAILED_IMAGES.has(story.image) ? `<img class="story-photo" src="${escapeHTML(story.image)}" alt="" width="800" height="457" ${isLead ? 'fetchpriority="high"' : 'loading="lazy"'} onerror="imageFailed(this)">` : ""}
+      ${imageSlotHTML}
       <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>
+      <span class="np-cat-label" aria-hidden="true">${escapeHTML(story.categoryLabel)} · </span>
       <h3><a href="#/article/${escapeHTML(story.id)}">${escapeHTML(stage.headline)}</a></h3>
       <p class="story-detail">${escapeHTML(stage.detail)}</p>
       ${concluded ? `<p class="story-detail"><em>${escapeHTML(stage.conclusion)}</em></p>` : ""}
@@ -1619,6 +1728,13 @@ function renderStories() {
   // genuinely over a card, the next real pointerover re-sets this within a
   // tick, so this costs nothing on the normal hover path.
   state.hoveredCardId = null;
+  // Category view ("#/category/<x>") gets the dense "Newspaper Column"
+  // treatment (V4); the "All Categories" home view keeps the original
+  // photo-card stack. storyCardHTML()'s markup is IDENTICAL in both modes —
+  // only this CSS-mode class differs — so the keyed morph reconcile in
+  // patchStoryGrid() never has to reconcile across a markup-shape change
+  // from this toggle alone (a route change already swaps the id set).
+  els.storyGrid.classList.toggle("newspaper-mode", state.category !== "all");
   // Top-level completion state: every story has been deleted (a state that
   // didn't exist before the admin panel could delete the seeded 3) — this
   // is distinct from the per-category empty state below.

← c832bd4 Add admin button to refresh real news via a tiny local admin  ·  back to Crazy News Channel  ·  cartoons: 6 new political cartoons for invented politics sto daf7ea3 →