// Mosaic site — STORY page (direction S1, de-boxed, framings pushed up) // Exports: StoryPage const Measure = ({ children, w = 720, style }) => (
{children}
); const RecordGroup = ({ story, group, idx, mode, open, onToggle }) => { const f = story.framings[group.f]; const c = framingColor(f, group.f, mode); return (
{open && group.items.map((it, i) => (
{it.d} {FAVS[it.s].n} {it.h}
))}
); }; const StoryPage = ({ id, mode, onBack, onOpenStory }) => { const story = SITE_STORIES.find((s) => s.id === id) || SITE_STORIES[0]; const idx = SITE_STORIES.indexOf(story); const next = SITE_STORIES[(idx + 1) % SITE_STORIES.length]; const allSrcs = [...new Set(story.framings.flatMap((f) => f.srcs))]; const totalSources = story.framings.reduce((s, f) => s + f.n, 0); const rec = recordOf(story); const [openGroups, setOpenGroups] = React.useState({ 0: true }); return (
Story {idx + 1} of {SITE_STORIES.length}
{/* HERO — collage cover with overlaid kicker / title / moodline */}
{story.tags} · updated {story.upd} · {totalSources} sources · {story.framings.length} framings

{story.title}

{story.dek &&

{story.dek}

}
{tickLabel(story, mode)}
Illustration: Mosaic — conceptual, not a depiction of real events.
{/* HOW IT'S FRAMED — wide feature, grouped directly under the hero (the two wide sections together) */}
How it's framed
Columns are narratives — a source sits under the framing its coverage advances on this story, not its label.
{/* reading column — lede + everything else, all at the narrow measure (width steps once, here) */}

{story.standfirst}

{totalSources} sources across {story.framings.length} framings
{/* Flexible markdown body (dossier prose: what happened · why it matters · fault · blind · next · record). Renders any sections without UI changes. Falls back to the structured blocks below if absent. */} {story.body_md ? () : ( {/* fault line — inline, no tinted box */}
The fault line
{story.fault}
{/* facts — plain checklist */}
The facts — what everyone agrees on
{story.facts.map((f, i) => (
{f}
))}
{/* history */} {story.history && (
The story so far
{story.history.map((h, i) => ( {i > 0 && } {h.d} {mode === "neutral" ? NEUTRAL_PALETTE.map((c, j) => ) : ["#FF601C", "#FFC200", "#00AA47"].map((c, j) => )} {h.l} ))}
)} {/* blind spot — plain */}
⊘ The blind spot. {story.blind}
{/* full record */}
The full record · {rec.reduce((s, g) => s + g.items.length, 0)} items, grouped by framing
{rec.map((g, i) => ( setOpenGroups({ ...openGroups, [i]: !openGroups[i] })} /> ))}
)} {/* footer */}
); }; Object.assign(window, { StoryPage });