scripts/emergence-page.js
import { squarify } from '../assets/emergence-layout.js';
import { projectEqualEarth } from '../assets/emergence-projection.js';
const esc = s => String(s ?? '').replace(/[&<>"']/g, c => ({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }[c]));
const addedAt = e => e.distillation?.distilled_at || null;
const addedTag = e => {
const at = addedAt(e);
if (!at) return '';
const t = new Date(at);
return `<time class="em-added" datetime="${esc(at)}" data-added="${esc(at)}">added ${String(t.getUTCHours()).padStart(2, '0')}:${String(t.getUTCMinutes()).padStart(2, '0')} UTC</time>`;
};
const plural = n => `${n} report${n === 1 ? '' : 's'}`;
const route = (...parts) => '#t/' + parts.map(encodeURIComponent).join('/');
const sourceNames = { aljazeera: 'Al Jazeera', 'bbc-world': 'BBC World', 'guardian-world': 'The Guardian World', reuters: 'Reuters' };
const snapshotEssence = (snap, id) => (snap.essences || []).find(e => e.id === id && !e.failure) || null;
// All record appearances share this identity and source treatment, including samples.
function record(e, { boundary = false, anchor = false } = {}) {
const a = e.article || e;
const uncertainty = `<div class="em-uncertainty"><p class="em-label">Ambiguous</p><p>${esc(e.uncertainty || 'No uncertainty note supplied.')}</p></div>`;
const claim = `<div class="em-claim"><p class="em-label">Report</p><p>${esc(e.essence)}</p></div>`;
return `<article class="em-record${boundary ? ' em-boundary-record' : ''}" data-record-id="${esc(e.id)}"${anchor ? ` id="record-${esc(e.id)}"` : ''}>
<div class="em-record-identity"><span class="em-status">not fact-checked · summarized from news reports</span><span class="em-record-type">${esc(e.event_type || 'report')}</span>${addedTag(e)}</div>
<h3>${esc(a.title || 'Source report')}</h3>
<a class="em-source" href="${esc(a.url)}" target="_blank" rel="noopener noreferrer">${esc(a.source_name)} ↗</a>
${boundary ? uncertainty + claim : claim + uncertainty}
<details class="em-record-context"><summary>Context</summary><dl>
<dt>Principles · AI interpretation</dt><dd>${(e.principles || []).map(esc).join(' · ') || 'None supplied'}</dd>
<dt>Actors</dt><dd>${(e.actors || []).map(esc).join(' · ') || 'None supplied'}</dd>
<dt>Places · country-level</dt><dd>${(e.locations || []).map(l => esc(l.name)).join(' · ') || 'None supplied'}</dd>
</dl></details>
</article>`;
}
const eventsCount = t => t.events ?? t.count;
const tileValue = t => t.score ?? t.count;
const tileSummary = t => t.events != null ? `${t.events} event${t.events === 1 ? '' : 's'} · ${plural(t.count)}` : plural(t.count);
function topicTiles(tree) {
return squarify(tree.filter(t => t.count > 0).map((t, i) => ({ value: tileValue(t), t, i }))).map(tile =>
`<a class="em-tile em-tone-${tile.i % 4}" href="#topic-${esc(tile.t.topic)}" aria-label="${esc(tile.t.name)}, ${esc(tileSummary(tile.t))}. Explore topic." title="${esc(tile.t.name)} · ${esc(tileSummary(tile.t))}" data-label="${esc(tile.t.name)}" data-count="${eventsCount(tile.t)}"${tile.value < 0.04 ? ' data-compact' : ''} style="left:${tile.x * 100}%;top:${tile.y * 100}%;width:${tile.w * 100}%;height:${tile.h * 100}%" data-go="${esc(route(tile.t.topic))}"><span class="em-tile-copy"><b>${esc(tile.t.name)}</b><span>${esc(tileSummary(tile.t))} <span aria-hidden="true">↗</span></span></span><span class="em-tile-marker" aria-hidden="true">${eventsCount(tile.t)}</span></a>`
).join('');
}
const TOPIC_SHORT = { 'conflict-security': 'conflict', 'politics-governance': 'politics', 'economy-business': 'economy', 'society-culture': 'society', 'health-medicine': 'health', 'environment-climate': 'environment', 'science-discovery': 'science', 'technology-ai': 'tech', 'arts-sports': 'arts', 'other': 'other' };
const topicClass = t => t && TOPIC_SHORT[t] ? 'em-t-' + t : 'em-t-other';
const topicShort = t => TOPIC_SHORT[t || 'other'] || 'other';
function worldMap(countries, world, snap_places = []) {
const p = world.projection;
const sized = countries.map(c => c.events ?? c.count);
const max = Math.max(1, ...sized);
const fills = countries.map((c, i) => world.countries[c.country_code] ? `<a href="#country-${esc(c.country_code)}" tabindex="-1" aria-hidden="true"><path class="em-hot" fill-opacity="${(0.14 + 0.34 * sized[i] / max).toFixed(2)}" d="${world.countries[c.country_code]}"/></a>` : '').join('');
const places = (snap_places || []);
const maxPlaceWeight = Math.max(1, ...places.map(p => p.weight));
const placeMarks = places.map(pl => {
const [px, py] = projectEqualEarth(pl.lon, pl.lat, p);
const summary = `${pl.events} event${pl.events === 1 ? '' : 's'} · ${pl.reports} report${pl.reports === 1 ? '' : 's'} · ${topicShort(pl.dominant_topic)}`;
const sample = pl.essences[0] ? `${pl.essences[0].essence.replace(/\s+/g, ' ').slice(0, 160)}${pl.essences[0].essence.length > 160 ? '…' : ''}` : '';
return `<a class="${topicClass(pl.dominant_topic)}" href="#country-${esc(pl.country_code)}" aria-label="${esc(pl.name)}: ${esc(summary)}" data-topic="${esc(pl.dominant_topic || '')}" data-map-name="${esc(pl.name)}" data-map-summary="${esc(summary)}" data-map-sample="${esc(sample)}" data-map-source="${esc(pl.essences[0]?.source_name || '')}" data-map-url="${esc(pl.essences[0]?.url || '')}" data-map-lon="${pl.lon}" data-map-lat="${pl.lat}" data-last-update="${pl.last_update_at || ''}" data-map-first="${pl.first_seen_at || ''}" data-map-place><title>${esc(pl.name)}: ${esc(summary)}</title><circle class="em-place" cx="${px.toFixed(2)}" cy="${py.toFixed(2)}" r="${(1.1 + 1.8 * Math.sqrt(pl.weight / maxPlaceWeight)).toFixed(2)}"/></a>`;
}).join('');
const marks = countries.map((c, i) => {
const summary = c.events != null ? `${c.events} event${c.events === 1 ? '' : 's'} · ${c.count} report${c.count === 1 ? '' : 's'} · ${topicShort(c.dominant_topic)}` : `${c.count} report${c.count === 1 ? '' : 's'}`;
const sample = c.essences[0] ? `${c.essences[0].essence.replace(/\s+/g, ' ').slice(0, 160)}${c.essences[0].essence.length > 160 ? '…' : ''}` : '';
const [x, y] = projectEqualEarth(c.lon, c.lat, p);
return `<a class="${topicClass(c.dominant_topic)}" href="#country-${esc(c.country_code)}" aria-label="${esc(c.name)}: ${esc(summary)}" data-topic="${esc(c.dominant_topic || '')}" data-map-name="${esc(c.name)}" data-map-summary="${esc(summary)}" data-map-lon="${c.lon}" data-map-lat="${c.lat}" data-last-update="${c.last_update_at || ''}" data-map-first="${c.first_seen_at || ''}" data-map-sample="${esc(sample)}" data-map-source="${esc(c.essences[0]?.source_name || '')}" data-map-url="${esc(c.essences[0]?.url || '')}"><title>${esc(c.name)}: ${esc(summary)}</title><circle class="em-dot" cx="${x.toFixed(2)}" cy="${y.toFixed(2)}" r="${(3.5 + 8.5 * Math.sqrt(sized[i] / max)).toFixed(1)}"/></a>`;
}).join('');
const regions = world.regions.map(r => `<a href="#map-region-${esc(r.id)}" class="em-map-region" data-region="${esc(r.id)}" aria-label="${esc(r.name)} · ${esc(r.category)} in reference data. Read boundary notes."><title>${esc(r.name)} · ${esc(r.category)}</title><path class="em-contested-ground" d="${r.d}"/><path class="em-contested-hatch" d="${r.d}"/></a>`).join('');
const labels = {ordinary:'Reference boundary',disputed:'Disputed boundary',control:'Control or ceasefire line',uncertain:'Boundary uncertain',claim:'Claim outline',zone:'Buffer or reference zone'};
const lines = Object.entries(world.boundaries).map(([kind, d]) => kind === 'ordinary' ? `<path class="em-border em-border-ordinary" d="${d}" aria-hidden="true"/>` : `<a href="#map-boundary-notes" aria-label="${labels[kind]} · reference map. Read the key."><path class="em-border em-border-${kind}" d="${d}"/></a>`).join('');
return `<svg class="em-map" viewBox="0 0 ${p.W} ${p.H}" role="group" aria-label="Equal Earth attention map. Country counts describe reporting, not sovereignty. Hatched areas have boundary or status notes.">
<defs><pattern id="em-dispute-hatch" width="5" height="5" patternUnits="userSpaceOnUse"><path d="M-1,1L1,-1M0,5L5,0M4,6L6,4" class="em-hatch-line"/></pattern><mask id="em-attention-mask" maskUnits="userSpaceOnUse" x="0" y="0" width="${p.W}" height="${p.H}"><rect width="${p.W}" height="${p.H}" fill="white"/>${world.regions.map(r => `<path d="${r.d}" fill="black"/>`).join('')}</mask></defs>
<path class="em-land" d="${world.land}"/><g mask="url(#em-attention-mask)">${fills}</g>${regions}${lines}${marks}${placeMarks}</svg>`;
}
function mapBoundaryNotes(world) {
const external = (label, url) => `<a href="${esc(url)}" target="_blank" rel="noopener noreferrer">${esc(label)} ↗</a>`;
const key = (kind, label) => `<li><svg class="em-key-line" viewBox="0 0 36 8" aria-hidden="true"><path class="em-border em-border-${kind}" d="M1,4L35,4"/></svg>${label}</li>`;
return `<details class="em-map-notes" id="map-boundary-notes"><summary>Boundaries & sources</summary>
<p>Equal Earth preserves relative areas. Dashed lines and hatching mark disputed boundaries, claims or status in the reference data; they do not assign ownership.</p>
<ul class="em-map-key">${key('disputed','Disputed boundary')}${key('claim','Claim outline')}${key('control','Control / ceasefire line')}${key('uncertain','Boundary uncertain')}${key('zone','Buffer / reference zone')}</ul>
<p class="em-note">Reference geometry: Natural Earth 5.1.2 · corrections checked 2 Oct 2026. Some features remain reference-only. No live front lines or complete maritime claims. Legal recognition, administration and claims are distinct.</p>
<p>${external('Source boundary policy',world.review.referenceUrl)} · <a href="../assets/emergence-world.md">Provenance & license</a> · <a href="../assets/emergence-world.json" download>Download map data</a></p>
<div class="em-map-region-list">${world.regions.map(r => `<details class="em-region-note" id="map-region-${esc(r.id)}"><summary><span>${esc(r.name)}</span><span class="em-region-status">${r.reviewedAt ? 'Source note' : 'Reference only'}</span></summary><p class="em-note">${esc(r.category)}</p><p>${esc(r.note)}</p>${r.reviewedAt ? `<p class="em-note">Source note checked <time datetime="${r.reviewedAt}">2 Oct 2026</time>; geometry retains its reference date.</p>` : ''}<p class="em-region-sources">${[...r.sources.map(s => external(s.label,s.url)),external('Natural Earth geometry','https://github.com/nvkelso/natural-earth-vector/tree/'+world.sources.commit+'/geojson')].join(' · ')}</p><button type="button" class="em-map-locate" data-map-bounds="${r.bounds.join(',')}" data-map-region="${esc(r.id)}" hidden>Show on map</button></details>`).join('')}</div>
<details class="em-map-corrections"><summary>Corrections & omissions</summary><p>Leases and historical reference lines are not labelled as active disputes.</p><ul>${world.review.corrections.map(c => `<li>${esc(c.reason)} ${external('Source',c.source)}</li>`).join('')}</ul></details>
</details>`;
}
export function emergencePreview(snap) {
const records = snap.essences.filter(e => !e.failure), topics = snap.topics.tree.filter(t => t.count > 0);
const events = snap.events || [];
const tiles = squarify(topics.map((t, i) => ({ value: tileValue(t), t, i }))).map(tile => `<a class="fact-day-tile fact-day-tone-${tile.i % 4}" href="emergence/${route(tile.t.topic)}" aria-label="${esc(tile.t.name)}: ${esc(tileSummary(tile.t))}. Open dashboard." title="${esc(tile.t.name)} · ${esc(tileSummary(tile.t))}" style="left:${tile.x * 100}%;top:${tile.y * 100}%;width:${tile.w * 100}%;height:${tile.h * 100}%"><span>${tile.value > .07 ? `<b>${esc(tile.t.name)}</b>` : ''}<span>${tile.t.events ?? tile.t.count}</span></span></a>`).join('');
const date = new Date(snap.date + 'T00:00Z').toLocaleDateString('en-GB', { day: 'numeric', month: 'short', year: 'numeric', timeZone: 'UTC' });
return `<section class="fact-day-dashboard" aria-labelledby="fact-day-title"><div class="fact-day-heading"><h2 id="fact-day-title">Emergence</h2><time datetime="${esc(snap.date)}">${esc(date)} · UTC</time></div><p class="fact-day-description">Global sensor net of newsroom reporting.</p><p class="fact-day-status">not fact-checked · summarized from news reports</p><dl class="fact-day-stats"><div><dt>Events</dt><dd>${events.length}</dd></div><div><dt>Reports</dt><dd>${records.length}</dd></div><div><dt>Countries</dt><dd>${(snap.gis.countries || []).length}</dd></div><div><dt>Verified · inactive</dt><dd>0</dd></div></dl><div class="fact-day-portrait" aria-label="Latest day's coverage by primary topic">${tiles}</div><p class="fact-day-caption">Area = event weight (coverage, corroboration, nuance), never importance or truth.</p><a class="fact-day-open" href="emergence/">Open dashboard →</a></section>`;
}
export function emergenceBody(snap, world, v = "") {
const records = snap.essences.filter(e => !e.failure), tree = snap.topics.tree.filter(t => t.count > 0);
const countries = snap.gis.countries || [], byId = new Map(records.map(e => [e.id, e]));
const dateLabel = new Date(snap.date + 'T00:00:00Z').toLocaleDateString('en-GB', { day: 'numeric', month: 'long', year: 'numeric', timeZone: 'UTC' });
const typeCounts = {};
for (const e of records) { const t = e.event_type || 'report'; typeCounts[t] = (typeCounts[t] || 0) + 1; }
const types = Object.keys(typeCounts).sort((a, b) => typeCounts[b] - typeCounts[a]);
const initialType = types[0];
const initialRecords = records.filter(e => (e.event_type || 'report') === initialType);
// Match the pipeline's primary-topic rollup and list each full record once.
const groups = [...tree.map(t => ({ ...t, records: records.filter(e => e.topics?.[0]?.topic === t.topic) })),
{ topic: 'unmapped', name: 'Other / unmapped', records: records.filter(e => !tree.some(t => t.topic === e.topics?.[0]?.topic)) }].filter(t => t.records.length);
const groupHtml = groups.map(t => `<details class="em-group" id="topic-${esc(t.topic)}"><summary><span>${esc(t.name)}</span><span>${t.records.length} records <span aria-hidden="true">+</span></span></summary><div class="em-record-grid">${t.records.map(e => record(e, { anchor: true })).join('')}</div></details>`).join('');
const countryHtml = countries.map((c, i) => `<details class="em-country" id="country-${esc(c.country_code)}"${i === 0 ? ' open' : ''}><summary><span>${esc(c.name)}</span><span>${c.events != null ? `${c.events} event${c.events === 1 ? '' : 's'} · ${c.count}` : c.count}</span></summary><p class="em-note">${c.essences.length} of ${c.count} previews${c.events != null ? ` · ${c.events} deduplicated event${c.events === 1 ? '' : 's'}` : ''}</p>${c.essences.map(sample => {
const e = byId.get(sample.id) || sample, a = e.article || e;
return `<article class="em-record-reference" data-record-id="${esc(e.id)}"><p class="em-status">not fact-checked · summarized from news reports</p><p>${esc(e.essence)}</p><a class="em-source" href="${esc(a.url)}" target="_blank" rel="noopener noreferrer">${esc(a.source_name)} ↗</a>${byId.has(e.id) ? `<a class="em-read-record" href="#record-${esc(e.id)}">Inspect →</a>` : ''}</article>`;
}).join('')}</details>`).join('');
const sourceRows = Object.entries(snap.day.sources).map(([id, s]) => `<li><span>${esc(sourceNames[id] || id)}</span><span>${esc(s.status)}${s.status === 'ok' ? ` · ${s.fetched} / ${s.new}` : ''}</span></li>`).join('');
const dateShort = new Date(snap.date + 'T00:00:00Z').toLocaleDateString('en-GB', { day: 'numeric', month: 'short', year: 'numeric', timeZone: 'UTC' });
const feedsOk = Object.values(snap.day.sources).filter(s => s.status === 'ok').length;
const railStats = [
['Events', (snap.events || []).length],
['Reports', records.length],
['Countries', countries.length],
['Feeds', `${feedsOk} ok`],
['Spend', `$${(snap.day.spend.usd || 0).toFixed(2)} / $1`],
].map(([k, v]) => `<div><dt>${esc(k)}</dt><dd>${esc(String(v))}</dd></div>`).join('');
return `<div class="em-shell" data-shell data-shell-default="map">
<aside class="em-rail" aria-label="Net status">
<div class="em-rail-id"><h1>Emergence</h1><p class="em-status">not fact-checked · summarized from news reports</p></div>
<p class="em-rail-date"><time datetime="${esc(snap.date)}">${esc(dateShort)}</time> · UTC</p>
<dl class="em-rail-stats">${railStats}</dl>
<p class="em-rail-verified">Fact-checking council: <b>inactive</b> · 0 verified records</p>
<p class="em-rail-note">Summaries by glm-5.3-flash · sources attributed per record</p>
<p class="em-rail-cycle" id="em-cycle" hidden></p>
</aside>
<section class="em-head" aria-labelledby="em-title">
<div class="em-edition"><p class="em-label">Global sensor net</p><p><time datetime="${esc(snap.date)}">${esc(dateLabel)}</time> · UTC</p></div>
<div class="em-introduction"><h1 id="em-title">Emergence</h1><p class="em-note">not fact-checked · summarized from news reports</p></div>
<p class="em-app-intro">News reports gathered into a daily dashboard. Explore coverage, claims and sources.</p>
<dl class="em-stats"><div><dt>Events</dt><dd>${(snap.events || []).length}</dd></div><div><dt>Reports</dt><dd>${records.length}</dd></div><div><dt>Countries</dt><dd>${countries.length}</dd></div><div><dt>Feeds</dt><dd>${Object.values(snap.day.sources).filter(s => s.status === 'ok').length}</dd></div><div><dt>Verified</dt><dd>0 <a href="#verified">Inactive ↗</a></dd></div></dl>
</section>
<nav class="em-lens-nav" aria-label="Explore this snapshot" data-shell-nav><a href="#map">Map</a><a href="#day">Topics</a><a href="#boundaries">Ambiguous</a><a href="#topics">Records</a><a href="#compare">Over time</a><a href="#method">Method</a><a href="#about">About</a></nav>
<section class="em-section" id="map" aria-labelledby="map-title" data-pane>
<div class="em-section-heading"><h2 id="map-title">Map</h2><p class="em-section-aside">Size = attention, never importance or truth.</p></div>
<figure class="em-world">${worldMap(countries, world, snap.gis.places || [])}<div class="em-map-key" role="group" aria-label="Key: colour shows the dominant topic of reporting; shade shows the age of the newest report, solid new to faded older."><div class="em-k-row">${(() => { const present = [...new Set([...countries.map(c => c.dominant_topic), ...(snap.gis.places || []).map(pl => pl.dominant_topic)].filter(Boolean))].sort(); return present.map(t => `<span><i class="em-t-${t}"></i>${topicShort(t)}</span>`).join(""); })()}</div><div class="em-k-row em-k-age"><span>new<i class="em-k-shade" style="opacity:1"></i><em class="em-k-bar"></em><i class="em-k-shade" style="opacity:.18"></i>older</span></div></div><figcaption><span><i aria-hidden="true"></i> Country-level mentions</span><span class="em-place-legend">Pins: named places</span><a class="em-map-legend-link" href="#map-boundary-notes">Dashed / hatched: boundary notes</a></figcaption></figure>
${mapBoundaryNotes(world)}
<details class="em-country-list"><summary>${countries.length} countries</summary><div class="em-countries">${countryHtml}</div></details>
</section>
<section class="em-section" id="day" aria-labelledby="day-title" data-pane data-pane-prefixes="t">
<div class="em-section-heading"><h2 id="day-title">Topics</h2><p class="em-section-aside">Area = event weight: coverage, corroboration, nuance. Never importance or truth.</p></div>
<div class="em-portrait-layout"><div class="em-portrait"><nav class="em-crumb" id="em-crumb" aria-label="Topic zoom path"><span aria-current="page">All topics</span></nav><div class="em-treemap" id="em-treemap">${topicTiles(tree)}<div class="em-tile-preview" id="em-tile-preview" role="tooltip" aria-hidden="true" hidden></div></div><div class="em-detail" id="em-detail" hidden></div><p class="em-note em-portrait-caption" id="em-drill-status" role="status" hidden></p></div>
<aside class="em-topic-index"><p class="em-label">Primary topics</p><ol id="em-topic-index">${tree.map((t, i) => `<li><a href="#topic-${esc(t.topic)}" data-go="${esc(route(t.topic))}"><span class="em-index-dot em-tone-${i % 4}" aria-hidden="true"></span><span>${esc(t.name)}</span><span>${t.events ?? t.count}</span></a></li>`).join('')}</ol></aside></div>
</section>
<section class="em-section" id="boundaries" aria-labelledby="boundary-title" data-pane>
<div class="em-section-heading"><h2 id="boundary-title">Ambiguous</h2><div class="em-control" id="em-boundary-control" hidden><label class="em-visually-hidden" for="em-event-type">Event type</label><select id="em-event-type">${types.map(type => `<option value="${esc(type)}"${type === initialType ? ' selected' : ''}>${esc(type)}</option>`).join('')}</select></div></div>
<p class="em-note" id="em-boundary-status" role="status">${Math.min(2, initialRecords.length)} of ${initialRecords.length} · snapshot order</p>
<div class="em-record-grid em-boundary-grid" id="em-boundary-records">${initialRecords.slice(0, 2).map(e => record(e, { boundary: true })).join('')}</div>
</section>
<section class="em-section" id="topics" aria-labelledby="records-title" data-pane>
<div class="em-section-heading"><h2 id="records-title">Records</h2><p class="em-section-aside">${records.length} · by primary topic</p></div>
<div class="em-search" id="em-search-controls" hidden><label for="em-search">Search</label><input id="em-search" type="search" placeholder="Claims, actors, principles…"><p class="em-note" id="em-search-status" role="status">${records.length} records</p></div>
<div id="em-all-records">${groupHtml}</div><p id="em-empty" class="em-empty" hidden>No matching records. Try another name or phrase.</p>
</section>
${(() => {
const threads = (snap.events || []).filter(e => e.thread_days > 1).sort((a, b) => b.thread_days - a.thread_days || b.weight - a.weight);
const threadHtml = threads.length ? `<p class="em-note">${threads.length} ongoing events · historical comparison unavailable.</p><ol class="em-thread-list">${threads.map(e => {
const seed = snapshotEssence(snap, e.seed), article = seed?.article || seed;
return `<li class="em-thread-item"><p class="em-status">not fact-checked · summarized from news reports</p><a class="em-thread-open" href="#record-${esc(e.seed)}">Day ${e.thread_days} · ${(e.sources || []).length} source${(e.sources || []).length === 1 ? '' : 's'} · ${e.reports} report${e.reports === 1 ? '' : 's'} →</a>${seed ? `<p class="em-thread-essence">${esc(seed.essence.slice(0, 160))}${seed.essence.length > 160 ? '…' : ''}</p><a class="em-source" href="${esc(article.url)}" target="_blank" rel="noopener noreferrer">${esc(article.source_name)} ↗</a>` : ''}</li>`;
}).join('')}</ol>` : `<p class="em-note">Historical comparison unavailable. No continuing events in this snapshot.</p>`;
return `<section class="em-section em-compare" id="compare" aria-labelledby="compare-title" data-pane><div><h2 id="compare-title">Over time</h2>${threadHtml}</div><div class="em-timeline"><span class="em-timeline-dot" aria-hidden="true"></span><time datetime="${esc(snap.date)}">${esc(dateLabel)}</time></div></section>`;
})()}
<section class="em-section" id="method" aria-labelledby="method-title" data-pane>
<div class="em-section-heading"><h2 id="method-title">Method & status</h2><a class="em-inline-link" href="data.json">Snapshot JSON ↗</a></div>
<div class="em-method-grid"><div><p>Summaries by <strong>glm-5.3-flash</strong> · Cloudflare Workers AI.</p><details class="em-method-details"><summary>Method & limitations</summary><p>Newsroom teasers; one AI call per article, fixed neutral-framing prompt. Summaries and tags can contain errors.</p><p>Primary-topic rollups. Named places where the gazetteer resolves them; otherwise country centroids, not event locations. Unlit places are not evidence of inactivity.</p><p>Basemap: vendored Natural Earth, public domain.</p></details><dl class="em-method-stats"><div><dt>Articles / essences / failures</dt><dd>${snap.day.counts.articles} / ${snap.day.counts.essences} / ${snap.day.counts.failures}</dd></div><div><dt>Spend · USD</dt><dd>$${(snap.day.spend.usd || 0).toFixed(3)} <span class="em-note">/ $1 daily cap</span></dd></div><div><dt>Tokens in / out</dt><dd>${snap.day.spend.tokens_in.toLocaleString('en')} / ${snap.day.spend.tokens_out.toLocaleString('en')}</dd></div></dl></div>
<div><p class="em-label">Feeds · latest cycle · fetched / new</p><ul class="em-source-status">${sourceRows}</ul><p class="em-note">Reuters deferred: no legitimate feed route.</p></div></div>
<aside class="em-verified" id="verified" aria-labelledby="verified-title"><div class="em-verified-count">0<span>verified records</span></div><div><p class="em-label">Built · inactive</p><h3 id="verified-title">Verified lane</h3><p>AI council inactive by design.</p></div></aside>
</section><section class="em-section" id="about" aria-labelledby="about-title" data-pane data-pane-hosts-footer>
<div class="em-section-heading"><h2 id="about-title">About</h2><p class="em-section-aside">How the pipe works, the sources, and the basis for using them.</p></div>
<div class="em-method-grid">
<div>
<p class="em-label">How the pipe works</p>
<ol class="em-pipe-steps">
<li><b>Take in.</b> Reputable newsrooms' public feeds, at the depth they offer: titles and summaries. No full-text scraping, no paywall or bot-gate bypass.</li>
<li><b>Summarize.</b> One AI call per article (glm-5.3-flash) turns each report into a neutral summary. Nothing here is fact-checked.</li>
<li><b>Group.</b> Reports about the same happening become one event; coverage repeated on following days becomes a thread.</li>
<li><b>Publish.</b> This dashboard. Every record links to its original article. Cost this day: $${(snap.day.spend.usd || 0).toFixed(2)}, hard-capped at $1.</li>
</ol>
</div>
<div>
<p class="em-label">Sources we cover</p>
<ul class="em-source-list">
${Object.entries(snap.day.sources).map(([id, s]) => `<li class="em-source-row">
<div class="em-source-head"><a href="${esc(s.site || "#")}" target="_blank" rel="noopener noreferrer">${esc(s.name || id)} ↗</a><span class="em-source-status-chip">${esc(s.status)}</span></div>
${s.takes ? `<p class="em-source-takes">${esc(s.takes)}</p>` : ""}
${s.basis ? `<p class="em-source-basis">${esc(s.basis)}</p>` : ""}
</li>`).join("")}
</ul>
<p class="em-note">Roadmap: country-level lanes that add reputable local outlets, for finer-grained local aggregation.</p>
<p class="em-note">Your read marks stay in this browser only (local storage). fact.ngo keeps no record of what you read.</p>
</div>
</div>
<p class="em-note">Funding, licensing, and attribution fineprint below.</p>
</section>
</div><script>window.EMERGENCE_ASSET_V = ${JSON.stringify(v || "")}; window.EMERGENCE_LIVE_URL = "https://emergence-net.zauh.workers.dev/latest.json";</script><script type="module" src="../assets/emergence.js?v=${esc(v || "")}"></script>`;
}