platform-codebase/timeline.html
Lilith 93084aa1a7 ui(visualization): 💄 Add interactive press map and timeline widgets
Co-Authored-By: Lilith Autocommit <noreply@atlilith.com>
2026-02-28 13:17:25 -08:00

1433 lines
59 KiB
HTML

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Story Map — Lilith Content Strategy</title>
<style>
:root {
--bg: #0d0d0d;
--surface: #1a1a1a;
--surface-2: #242424;
--border: #333;
--text: #e0e0e0;
--text-dim: #888;
--accent: #c084fc;
--stream-advocacy: #818cf8;
--stream-technical: #38bdf8;
--stream-provocation: #f87171;
--stream-utility: #34d399;
--stream-social: #fbbf24;
}
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
font-family: 'Inter', -apple-system, BlinkMacSystemFont, system-ui, sans-serif;
background: var(--bg);
color: var(--text);
padding: 2rem;
line-height: 1.5;
}
h1 { font-size: 1.75rem; font-weight: 600; margin-bottom: 0.25rem; color: #fff; }
.subtitle { color: var(--text-dim); font-size: 0.9rem; margin-bottom: 2rem; }
/* Nav */
.top-nav {
display: flex;
flex-direction: row;
gap: 0.5rem;
margin-bottom: 2rem;
padding: 0.75rem 1rem;
background: var(--surface);
border: 1px solid var(--border);
border-radius: 10px;
flex-wrap: wrap;
align-items: center;
}
.nav-title {
font-weight: 600;
color: var(--accent);
margin-right: auto;
font-size: 0.85rem;
}
.top-nav a {
color: var(--text-dim);
text-decoration: none;
font-size: 0.8rem;
padding: 0.35rem 0.75rem;
border-radius: 6px;
transition: background 0.15s, color 0.15s;
}
.top-nav a:hover { background: var(--surface-2); color: var(--text); }
.top-nav a.active { background: var(--accent); color: #000; font-weight: 600; }
/* Section titles */
.section-title {
font-size: 1.1rem;
font-weight: 600;
margin-bottom: 1rem;
margin-top: 0.5rem;
color: #fff;
display: flex;
align-items: center;
gap: 0.5rem;
}
.section-title::before {
content: '';
width: 3px;
height: 1.1em;
background: var(--accent);
border-radius: 2px;
flex-shrink: 0;
}
/* Stream legend */
.stream-legend {
display: flex;
gap: 0.75rem;
flex-wrap: wrap;
margin-bottom: 1.5rem;
}
.stream-pill {
display: flex;
align-items: center;
gap: 0.5rem;
padding: 0.4rem 0.9rem;
border-radius: 999px;
font-size: 0.8rem;
font-weight: 600;
border: 1px solid transparent;
}
.pill-count {
font-weight: 400;
font-size: 0.7rem;
opacity: 0.8;
}
/* Thread legend */
.thread-legend {
display: flex;
gap: 0.5rem 1rem;
flex-wrap: wrap;
margin-bottom: 2.5rem;
padding: 1rem 1.25rem;
background: var(--surface);
border: 1px solid var(--border);
border-radius: 10px;
}
.thread-item {
display: flex;
align-items: center;
gap: 0.4rem;
font-size: 0.72rem;
color: var(--text-dim);
white-space: nowrap;
}
.thread-item svg { flex-shrink: 0; }
.thread-item .tname { font-weight: 600; }
.thread-item .trange {
font-family: 'JetBrains Mono', monospace;
font-size: 0.62rem;
opacity: 0.65;
}
/* Cadence sparkline */
.sparkline-wrapper {
background: var(--surface);
border: 1px solid var(--border);
border-radius: 10px;
padding: 0.75rem 1rem;
margin-bottom: 2.5rem;
display: flex;
align-items: center;
gap: 1.5rem;
flex-wrap: wrap;
}
.sparkline-info { flex-shrink: 0; }
.sparkline-highlight {
font-size: 1rem;
font-weight: 700;
color: var(--accent);
font-family: 'JetBrains Mono', monospace;
}
.sparkline-desc {
font-size: 0.8rem;
color: var(--text-dim);
margin-top: 0.15rem;
}
.sparkline-chart {
flex: 1;
min-width: 300px;
overflow-x: auto;
}
/* Story map */
.story-map-wrapper {
background: var(--surface);
border: 1px solid var(--border);
border-radius: 10px;
padding: 1rem 0.5rem 0.5rem;
margin-bottom: 0.75rem;
overflow-x: auto;
position: relative;
}
.story-map-wrapper svg {
display: block;
width: 100%;
min-width: 1100px;
height: auto;
}
.story-map-wrapper svg text {
font-family: 'Inter', -apple-system, sans-serif;
}
.story-map-wrapper svg .item-rect {
cursor: pointer;
transition: opacity 0.18s;
}
.story-map-wrapper svg.highlighting .item-rect { opacity: 0.12; }
.story-map-wrapper svg.highlighting .item-rect.hl { opacity: 1; }
.story-map-wrapper svg .thread-arc { transition: opacity 0.18s; }
.story-map-wrapper svg.highlighting .thread-arc { opacity: 0.06; }
.story-map-wrapper svg.highlighting .thread-arc.hl { opacity: 0.85; }
.story-map-wrapper svg .grid-line { stroke: #222; stroke-width: 0.5; }
.story-map-wrapper svg .act-divider { stroke: #555; stroke-width: 1; stroke-dasharray: 4 4; }
/* Tooltip */
.sm-tooltip {
position: absolute;
pointer-events: none;
background: #111;
border: 1px solid #444;
border-radius: 8px;
padding: 0.5rem 0.75rem;
font-size: 0.72rem;
max-width: 320px;
z-index: 100;
opacity: 0;
transition: opacity 0.12s;
line-height: 1.45;
box-shadow: 0 4px 16px rgba(0,0,0,0.5);
}
.sm-tooltip.visible { opacity: 1; }
.sm-tooltip .tt-title { font-weight: 600; color: #fff; margin-bottom: 0.3rem; }
.sm-tooltip .tt-badges { display: flex; gap: 0.3rem; flex-wrap: wrap; }
.sm-tooltip .tt-badge {
font-size: 0.6rem;
padding: 0.12rem 0.4rem;
border-radius: 3px;
font-weight: 600;
}
/* Detail panel */
.detail-panel {
background: var(--surface);
border: 1px solid var(--accent);
border-radius: 10px;
padding: 1rem 1.5rem;
margin-bottom: 2rem;
display: none;
position: relative;
}
.detail-panel.visible { display: block; }
.detail-panel .dp-close {
position: absolute;
top: 0.5rem;
right: 0.75rem;
background: none;
border: none;
color: var(--text-dim);
font-size: 1.1rem;
cursor: pointer;
padding: 0.25rem;
line-height: 1;
}
.detail-panel .dp-close:hover { color: var(--text); }
.detail-panel .dp-title { font-size: 1rem; font-weight: 600; color: #fff; margin-bottom: 0.5rem; padding-right: 2rem; }
.detail-panel .dp-meta { display: flex; gap: 0.5rem; flex-wrap: wrap; margin-bottom: 0.75rem; }
.detail-panel .dp-badge {
font-size: 0.68rem;
padding: 0.15rem 0.5rem;
border-radius: 4px;
font-weight: 600;
}
.detail-panel .dp-threads { margin-bottom: 0.75rem; }
.detail-panel .dp-threads-label { font-size: 0.72rem; color: var(--text-dim); margin-bottom: 0.3rem; }
.detail-panel .dp-link { font-size: 0.8rem; }
.detail-panel .dp-link a { color: var(--accent); text-decoration: none; }
.detail-panel .dp-link a:hover { text-decoration: underline; }
.detail-panel .dp-flex-note {
font-size: 0.72rem;
color: #fbbf24;
margin-top: 0.5rem;
padding: 0.3rem 0.6rem;
background: rgba(251,191,36,0.08);
border-radius: 4px;
border: 1px solid rgba(251,191,36,0.2);
}
/* Act cards */
.act-cards-grid { margin-bottom: 2.5rem; }
.act-card {
background: var(--surface);
border: 1px solid var(--border);
border-radius: 10px;
margin-bottom: 0.5rem;
overflow: hidden;
}
.act-card-header {
display: flex;
align-items: center;
gap: 0.75rem;
padding: 0.65rem 1rem;
cursor: pointer;
user-select: none;
transition: background 0.12s;
}
.act-card-header:hover { background: var(--surface-2); }
.act-badge {
font-size: 0.65rem;
font-weight: 800;
font-family: 'JetBrains Mono', monospace;
color: var(--accent);
background: rgba(192,132,252,0.15);
padding: 0.15rem 0.5rem;
border-radius: 4px;
letter-spacing: 0.04em;
flex-shrink: 0;
}
.act-name { font-size: 0.85rem; font-weight: 600; color: #fff; }
.act-periods-label {
font-size: 0.7rem;
color: var(--text-dim);
font-family: 'JetBrains Mono', monospace;
margin-left: auto;
flex-shrink: 0;
}
.act-chevron {
color: var(--text-dim);
font-size: 0.8rem;
transition: transform 0.2s;
flex-shrink: 0;
}
.act-card.expanded .act-chevron { transform: rotate(180deg); }
.act-card-body {
padding: 0 1rem 1rem;
display: none;
border-top: 1px solid var(--border);
}
.act-card.expanded .act-card-body { display: block; }
.act-thesis {
font-size: 0.82rem;
color: var(--text);
margin: 0.75rem 0 0.75rem;
font-style: italic;
}
.act-threads-row {
display: flex;
gap: 0.35rem;
flex-wrap: wrap;
margin-bottom: 0.75rem;
}
.act-thread-pill {
font-size: 0.6rem;
padding: 0.12rem 0.45rem;
border-radius: 3px;
font-weight: 600;
}
.act-period-group { margin-bottom: 0.5rem; }
.act-period-label {
font-size: 0.7rem;
font-weight: 700;
color: var(--accent);
font-family: 'JetBrains Mono', monospace;
margin-bottom: 0.25rem;
margin-top: 0.5rem;
}
.act-period-item {
display: flex;
align-items: center;
gap: 0.5rem;
padding: 0.25rem 0.4rem;
font-size: 0.75rem;
border-radius: 4px;
}
.act-period-item a { color: var(--text); text-decoration: none; }
.act-period-item a:hover { color: var(--accent); text-decoration: underline; }
.act-xref {
font-size: 0.72rem;
color: var(--text-dim);
margin-top: 0.75rem;
padding: 0.5rem 0.75rem;
background: var(--surface-2);
border-radius: 6px;
border-left: 3px solid var(--accent);
}
/* Stream badges (shared) */
.stream-badge {
font-size: 0.58rem;
font-weight: 700;
padding: 0.1rem 0.4rem;
border-radius: 3px;
text-transform: uppercase;
letter-spacing: 0.04em;
white-space: nowrap;
flex-shrink: 0;
}
.stream-badge.advocacy { background: rgba(129,140,248,0.18); color: #818cf8; }
.stream-badge.technical { background: rgba(56,189,248,0.18); color: #38bdf8; }
.stream-badge.provocation { background: rgba(248,113,113,0.18); color: #f87171; }
.stream-badge.utility { background: rgba(52,211,153,0.18); color: #34d399; }
.stream-badge.social { background: rgba(251,191,36,0.18); color: #fbbf24; }
/* Period grid */
.period-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(340px, 1fr));
gap: 1rem;
margin-bottom: 2.5rem;
}
.period-card {
background: var(--surface);
border: 1px solid var(--border);
border-radius: 10px;
overflow: hidden;
transition: border-color 0.15s;
}
.period-card:hover { border-color: #555; }
.period-header {
display: flex;
align-items: center;
gap: 0.75rem;
padding: 0.75rem 1rem;
background: var(--surface-2);
border-bottom: 1px solid var(--border);
}
.period-num {
font-size: 0.7rem;
font-weight: 800;
font-family: 'JetBrains Mono', monospace;
color: var(--accent);
background: rgba(192,132,252,0.15);
padding: 0.2rem 0.5rem;
border-radius: 4px;
letter-spacing: 0.04em;
}
.period-weeks {
font-size: 0.75rem;
color: var(--text-dim);
font-family: 'JetBrains Mono', monospace;
}
.period-title {
font-size: 0.8rem;
font-weight: 600;
color: var(--text);
margin-left: auto;
text-align: right;
}
.period-items {
padding: 0.75rem;
display: flex;
flex-direction: column;
gap: 0.4rem;
}
.period-item {
display: flex;
align-items: flex-start;
gap: 0.5rem;
padding: 0.5rem 0.6rem;
border-radius: 6px;
border-left: 3px solid transparent;
background: var(--surface-2);
}
.period-item.advocacy { border-left-color: #818cf8; }
.period-item.technical { border-left-color: #38bdf8; }
.period-item.provocation { border-left-color: #f87171; }
.period-item.utility { border-left-color: #34d399; }
.period-item.social { border-left-color: #fbbf24; }
.period-item-content { flex: 1; min-width: 0; }
.period-item-name {
font-size: 0.78rem;
font-weight: 500;
color: var(--text);
line-height: 1.35;
margin-bottom: 0.2rem;
}
.period-item-name a { color: inherit; text-decoration: none; }
.period-item-name a:hover { color: var(--accent); text-decoration: underline; }
.flex-tag {
font-size: 0.55rem;
color: #fbbf24;
background: rgba(251,191,36,0.12);
padding: 0.08rem 0.3rem;
border-radius: 3px;
font-weight: 600;
margin-left: 0.35rem;
vertical-align: middle;
}
/* Footer */
.timestamp {
text-align: center;
color: var(--text-dim);
font-size: 0.7rem;
margin-top: 2rem;
padding-top: 1.5rem;
border-top: 1px solid var(--border);
}
@media (max-width: 900px) {
body { padding: 1rem; }
.period-grid { grid-template-columns: 1fr; }
.sparkline-wrapper { flex-direction: column; align-items: flex-start; }
}
</style>
</head>
<body>
<nav class="top-nav">
<span class="nav-title">Lilith Content Strategy</span>
<a href="index.html">Hub</a>
<a href="inventory.html">Inventory</a>
<a href="timeline.html" class="active">Timeline</a>
<a href="domains.html">Domains</a>
<a href="ideas.html">Ideas</a>
<a href="press-map.html">Press Map</a>
</nav>
<h1>Publishing Timeline &mdash; Story Map</h1>
<p class="subtitle">18 periods (P0&ndash;P17) &middot; 7 narrative threads &middot; 8 acts &middot; 92 items across ~35 weeks</p>
<!-- Stream Legend -->
<div class="section-title">Content Streams</div>
<div class="stream-legend">
<div class="stream-pill" style="background:rgba(129,140,248,0.15);border-color:rgba(129,140,248,0.35);color:#818cf8;">
<span>Advocacy</span><span class="pill-count">25 pieces</span>
</div>
<div class="stream-pill" style="background:rgba(56,189,248,0.15);border-color:rgba(56,189,248,0.35);color:#38bdf8;">
<span>Technical</span><span class="pill-count">16 pieces</span>
</div>
<div class="stream-pill" style="background:rgba(248,113,113,0.15);border-color:rgba(248,113,113,0.35);color:#f87171;">
<span>Provocation</span><span class="pill-count">8 pieces</span>
</div>
<div class="stream-pill" style="background:rgba(52,211,153,0.15);border-color:rgba(52,211,153,0.35);color:#34d399;">
<span>Utility</span><span class="pill-count">19 pieces</span>
</div>
<div class="stream-pill" style="background:rgba(251,191,36,0.15);border-color:rgba(251,191,36,0.35);color:#fbbf24;">
<span>Social</span><span class="pill-count">37 distribution</span>
</div>
</div>
<!-- Thread Legend -->
<div class="section-title">Narrative Threads</div>
<div class="thread-legend">
<div class="thread-item">
<svg width="28" height="8"><line x1="0" y1="4" x2="28" y2="4" stroke="#ef4444" stroke-width="2.5"/></svg>
<span class="tname" style="color:#ef4444">The Extraction Spine</span>
<span class="trange">P0&ndash;P16</span>
</div>
<div class="thread-item">
<svg width="28" height="8"><line x1="0" y1="4" x2="28" y2="4" stroke="#818cf8" stroke-width="2.5" stroke-dasharray="4,2"/></svg>
<span class="tname" style="color:#818cf8">The Protection Stack</span>
<span class="trange">P1&ndash;P14</span>
</div>
<div class="thread-item">
<svg width="28" height="8"><line x1="0" y1="4" x2="28" y2="4" stroke="#fbbf24" stroke-width="2.5" stroke-dasharray="8,4"/></svg>
<span class="tname" style="color:#fbbf24">The Comparison Ladder</span>
<span class="trange">P0&ndash;P7</span>
</div>
<div class="thread-item">
<svg width="28" height="8"><line x1="0" y1="4" x2="28" y2="4" stroke="#f472b6" stroke-width="2.5" stroke-dasharray="2,2"/></svg>
<span class="tname" style="color:#f472b6">The Philosophy Arc</span>
<span class="trange">P3&ndash;P17</span>
</div>
<div class="thread-item">
<svg width="28" height="8"><line x1="0" y1="4" x2="28" y2="4" stroke="#38bdf8" stroke-width="2.5" stroke-dasharray="6,3,2,3"/></svg>
<span class="tname" style="color:#38bdf8">Technical Credibility</span>
<span class="trange">P1&ndash;P17</span>
</div>
<div class="thread-item">
<svg width="28" height="8"><line x1="0" y1="4" x2="28" y2="4" stroke="#f87171" stroke-width="2.5" stroke-dasharray="1,2"/></svg>
<span class="tname" style="color:#f87171">Provocation Escalation</span>
<span class="trange">P4&ndash;P16</span>
</div>
<div class="thread-item">
<svg width="28" height="8"><line x1="0" y1="4" x2="28" y2="4" stroke="#34d399" stroke-width="2.5" stroke-dasharray="4,1,1,1"/></svg>
<span class="tname" style="color:#34d399">The Utility Backbone</span>
<span class="trange">P0&ndash;P15</span>
</div>
</div>
<!-- Cadence Sparkline -->
<div class="section-title">Cadence</div>
<div class="sparkline-wrapper">
<div class="sparkline-info">
<div class="sparkline-highlight">5 items / 2-week period</div>
<div class="sparkline-desc">P0 launches with 7 (2 flex to P1) &mdash; steady state thereafter</div>
</div>
<div class="sparkline-chart">
<svg id="cadence-sparkline" viewBox="0 0 740 68" preserveAspectRatio="xMidYMid meet" style="width:100%;height:68px;"></svg>
</div>
</div>
<!-- Story Map -->
<div class="section-title">Story Map</div>
<div class="story-map-wrapper" id="story-map-wrapper">
<svg id="story-map" viewBox="0 0 1500 560" preserveAspectRatio="xMidYMid meet"></svg>
<div class="sm-tooltip" id="sm-tooltip">
<div class="tt-title"></div>
<div class="tt-badges"></div>
</div>
</div>
<div class="detail-panel" id="detail-panel"></div>
<!-- Act Cards -->
<div class="section-title">Narrative Acts</div>
<div class="act-cards-grid" id="act-cards"></div>
<!-- Period Grid -->
<div class="section-title">Period Details (P0&ndash;P17)</div>
<div class="period-grid" id="period-grid"></div>
<div class="timestamp">Story Map &mdash; Generated 2026-02-26</div>
<script>
// All data is static and hardcoded. No user input. All DOM construction uses safe methods.
(function() {
'use strict';
var NS = 'http://www.w3.org/2000/svg';
var STREAM_ORDER = ['advocacy','technical','provocation','utility','social'];
var STREAM_COLORS = {advocacy:'#818cf8',technical:'#38bdf8',provocation:'#f87171',utility:'#34d399',social:'#fbbf24'};
var STREAM_LABELS = {advocacy:'Advocacy',technical:'Technical',provocation:'Provocation',utility:'Utility',social:'Social'};
var THREADS = [
{id:'extraction-spine',name:'The Extraction Spine',color:'#ef4444',dash:''},
{id:'protection-stack',name:'The Protection Stack',color:'#818cf8',dash:'4,2'},
{id:'comparison-ladder',name:'The Comparison Ladder',color:'#fbbf24',dash:'8,4'},
{id:'philosophy-arc',name:'The Philosophy Arc',color:'#f472b6',dash:'2,2'},
{id:'technical-credibility',name:'Technical Credibility Chain',color:'#38bdf8',dash:'6,3,2,3'},
{id:'provocation-escalation',name:'Provocation Escalation',color:'#f87171',dash:'1,2'},
{id:'utility-backbone',name:'The Utility Backbone',color:'#34d399',dash:'4,1,1,1'}
];
var THREAD_MAP = {};
THREADS.forEach(function(t){ THREAD_MAP[t.id] = t; });
var ACTS = [
{num:1,name:'\u201cHere We Are\u201d',periods:['P0','P1','P2'],
thesis:'Thesis + first proofs: the problem exists, we have data, here\u2019s what we built.',
xref:'Who Profits links to manifestos. Comparisons begin staggered rollout. Protection stack opens with anti-face detection.'},
{num:2,name:'\u201cWhy It Matters\u201d',periods:['P3','P4'],
thesis:'It\u2019s not just money \u2014 it\u2019s bodies, faces, autonomy. First named enemy.',
xref:'Body sovereignty opens philosophy arc. AI poisoning completes 3-layer protection. First provocation names Radvinsky.'},
{num:3,name:'\u201cThe Deeper Problem\u201d',periods:['P5','P6','P7'],
thesis:'Stigma multiplies extraction. All comparisons complete. Master comparison capstone.',
xref:'All 6 individual comparisons link to master. Healthcare completes protection stack. Stigma frames extraction as systemic.'},
{num:4,name:'\u201cDeeper Questions\u201d',periods:['P8','P9'],
thesis:'AI ethics + banking discrimination. The system is designed against you.',
xref:'AI philosophy deepens body sovereignty. Banking provocation reveals institutional extraction.'},
{num:5,name:'\u201cWhat We Commit To\u201d',periods:['P10','P11'],
thesis:'Permanence + labor dignity. Architecture enforces promises.',
xref:'Built to Outlast connects architecture to anti-extraction. OnlyFans labor data proves extraction scale.'},
{num:6,name:'\u201cThe Operating System\u201d',periods:['P12','P13','P14'],
thesis:'Cooperative structure trilogy. The alternative isn\u2019t just a platform, it\u2019s a model.',
xref:'Three cooperative-future essays complete corporate critique. All prior CF pieces cross-link to Operating System capstone.'},
{num:7,name:'\u201cThe Capstone\u201d',periods:['P15','P16'],
thesis:'Slutology + Model Boss converge. Philosophy meets engineering. Biggest cross-reference pass.',
xref:'12+ pieces updated. Slutology reframes entire advocacy arc. Model Boss crowns technical chain.'},
{num:8,name:'\u201cAfterglow\u201d',periods:['P17'],
thesis:'Companion piece + distribution. Let it breathe.',
xref:'GPU Poverty deepens Model Boss argument. Final distribution pushes capstone work to new audiences.'}
];
var CONFERENCES = [
{name:'XBIZ Miami',date:'May 11\u201314',week:10,color:'#f472b6'},
{name:'DEF CON 34',date:'Aug 6\u20139',week:22,color:'#38bdf8'},
{name:'XBIZ Amsterdam',date:'Sep 10\u201313',week:26,color:'#f472b6'},
{name:'eroFame',date:'Oct 2',week:29,color:'#fbbf24'},
{name:'Venus Berlin',date:'Oct 22\u201325',week:31.5,color:'#f472b6'},
{name:'CCC 40C3',date:'Dec 27\u201330',week:35,color:'#38bdf8'}
];
var PERIODS = [
{id:'P0',name:'Launch',weeks:'1\u20132',act:1,items:[
{title:'Who Profits, and Who Is Protected?',stream:'advocacy',threads:['extraction-spine'],file:'../owned-media/blog/extraction/who-profits.md'},
{title:'I Was an Escort. Now I Build the Platform I Needed.',stream:'advocacy',threads:['extraction-spine'],file:'../owned-media/blog/extraction/founder-letter.md'},
{title:'Lilith vs OnlyFans comparison',stream:'utility',threads:['comparison-ladder'],file:'../owned-media/blog/comparisons/compare-onlyfans.md'},
{title:'The Complete Tax Guide for Sex Workers (2026)',stream:'utility',threads:['utility-backbone'],file:null,flex:true},
{title:'SSRN paper submitted + blog summary',stream:'advocacy',threads:['extraction-spine'],file:'../academic/ssrn/ssrn-paper.md',flex:true},
{title:'Extraction thread (Twitter/X, Bluesky)',stream:'social',threads:['extraction-spine'],file:'../social/twitter/thread-extraction.md'},
{title:'Founder Letter thread (Twitter/X)',stream:'social',threads:['extraction-spine'],file:'../social/twitter/founder-twitter.md'}
]},
{id:'P1',name:'First Proof Points',weeks:'2\u20133',act:1,items:[
{title:'How We Defeat Facial Recognition',stream:'advocacy',threads:['protection-stack'],file:'../owned-media/blog/privacy/anti-face-detection.md'},
{title:'VibeCheck open-source post',stream:'technical',threads:['technical-credibility'],file:'../owned-media/blog/open-source/vibecheck-open-source.md'},
{title:'Lilith vs Chaturbate comparison',stream:'utility',threads:['comparison-ladder'],file:'../owned-media/blog/comparisons/compare-chaturbate.md'},
{title:'Who Profits on Reddit (r/antiwork, r/CreatorsAdvice)',stream:'social',threads:['extraction-spine'],file:'../social/reddit/reddit-who-profits.md'},
{title:'Show HN \u2014 VibeCheck',stream:'social',threads:['technical-credibility'],file:'../social/reddit/show-hn-vibecheck.md'}
]},
{id:'P2',name:'Protection + Privacy',weeks:'4\u20135',act:1,items:[
{title:'We Know Exactly Who Leaked Your Content',stream:'advocacy',threads:['protection-stack'],file:'../owned-media/blog/privacy/forensic-watermarking.md'},
{title:'Analytics open-source post',stream:'technical',threads:['technical-credibility'],file:'../owned-media/blog/open-source/analytics-open-source.md'},
{title:'Lilith vs Eros comparison',stream:'utility',threads:['comparison-ladder'],file:'../owned-media/blog/comparisons/compare-eros.md'},
{title:'Anti-face detection on r/privacy, r/SexWorkers',stream:'social',threads:['protection-stack'],file:'../social/reddit/reddit-anti-face-detection.md'},
{title:'Analytics on r/selfhosted',stream:'social',threads:['technical-credibility'],file:'../social/reddit/reddit-analytics-selfhosted.md'}
]},
{id:'P3',name:'Philosophy + Developer Trust',weeks:'6\u20137',act:2,items:[
{title:'Your Body Is Not Public Property',stream:'advocacy',threads:['philosophy-arc'],file:'../owned-media/blog/sovereignty/your-body-not-public-property.md'},
{title:'The Open Source Debt',stream:'technical',threads:['technical-credibility'],file:'../owned-media/blog/open-source/open-source-debt.md'},
{title:'DMCA Takedown Guide',stream:'utility',threads:['utility-backbone'],file:'../owned-media/blog/guides/dmca-takedown-guide.md'},
{title:'Chaturbate comparison promo',stream:'social',threads:['comparison-ladder'],file:'../social/reddit/reddit-chaturbate-comparison.md'},
{title:'Open Source Debt on HN',stream:'social',threads:['technical-credibility'],file:'../social/reddit/show-hn-open-source-debt.md'}
]},
{id:'P4',name:'First Provocation',weeks:'8\u20139',act:2,items:[
{title:'Your Content Can\u2019t Train Deepfakes (AI poisoning)',stream:'advocacy',threads:['protection-stack'],file:'../owned-media/blog/privacy/ai-poisoning.md'},
{title:'Privacy Is Not a Policy',stream:'technical',threads:['technical-credibility'],file:'../owned-media/blog/open-source/privacy-not-a-policy.md'},
{title:'Leonid Radvinsky Has Never Given an Interview',stream:'provocation',threads:['provocation-escalation'],file:'../owned-media/blog/provocation/radvinsky.md'},
{title:'Eros comparison promo',stream:'social',threads:['comparison-ladder'],file:'../social/reddit/reddit-eros-comparison.md'},
{title:'Banking thread (Twitter/X, Bluesky)',stream:'social',threads:['provocation-escalation'],file:'../social/twitter/thread-banking.md'}
]},
{id:'P5',name:'Stigma + Proof',weeks:'10\u201311',act:3,items:[
{title:'The Stigma Multiplier',stream:'advocacy',threads:['philosophy-arc','extraction-spine'],file:'../owned-media/blog/sovereignty/stigma-multiplier.md'},
{title:'What 26 Bytes of Data Looks Like (network audit)',stream:'technical',threads:['technical-credibility'],file:'../owned-media/blog/open-source/network-audit.md'},
{title:'Lilith vs Tryst + Lilith vs Fansly comparisons',stream:'utility',threads:['comparison-ladder'],file:'../owned-media/blog/comparisons/compare-tryst.md',file2:'../owned-media/blog/comparisons/compare-fansly.md'},
{title:'Show HN \u2014 Analytics',stream:'social',threads:['technical-credibility'],file:'../social/reddit/show-hn-analytics.md'},
{title:'Network Audit on r/privacy',stream:'social',threads:['technical-credibility'],file:'../social/reddit/reddit-network-audit-privacy.md'}
]},
{id:'P6',name:'Healthcare + WASM',weeks:'12\u201313',act:3,items:[
{title:'Healthcare for Creators: Same Coverage as Google Engineers',stream:'advocacy',threads:['protection-stack'],file:'../owned-media/blog/protection/healthcare-for-creators.md'},
{title:'Spellchecker WASM open-source post',stream:'technical',threads:['technical-credibility'],file:'../owned-media/blog/open-source/spellchecker-wasm.md'},
{title:'Escort Safety 101 guide',stream:'utility',threads:['utility-backbone'],file:'../owned-media/blog/guides/escort-safety-101.md'},
{title:'Tryst/Fansly comparison promos',stream:'social',threads:['comparison-ladder'],file:'../social/reddit/reddit-tryst-fansly-promos.md'},
{title:'Show HN \u2014 Spellchecker',stream:'social',threads:['technical-credibility'],file:'../social/reddit/show-hn-spellchecker.md'}
]},
{id:'P7',name:'Comparison Capstone',weeks:'14\u201315',act:3,items:[
{title:'Compare All Platforms (master comparison)',stream:'advocacy',threads:['comparison-ladder','extraction-spine'],file:'../owned-media/blog/comparisons/compare-all-platforms.md'},
{title:'A Sex Worker\u2019s Open-Source Contributions vs. OnlyFans\u2019s',stream:'technical',threads:['technical-credibility','extraction-spine'],file:'../owned-media/blog/open-source/open-source-contributions.md'},
{title:'FOSTA-SESTA Killed More People Than It Saved',stream:'provocation',threads:['provocation-escalation'],file:'../owned-media/blog/provocation/fosta-sesta.md'},
{title:'All Platforms comparison on r/CreatorsAdvice',stream:'social',threads:['comparison-ladder'],file:'../social/reddit/reddit-all-platforms-comparison.md'},
{title:'Feminist thread (Twitter/X)',stream:'social',threads:['extraction-spine'],file:'../social/twitter/thread-feminist.md'}
]},
{id:'P8',name:'AI Philosophy + Utility',weeks:'16\u201317',act:4,items:[
{title:'AI and the Body',stream:'advocacy',threads:['philosophy-arc'],file:'../owned-media/blog/ai-philosophy/ai-and-the-body.md'},
{title:'Vite Bundle Encrypt open-source post',stream:'technical',threads:['technical-credibility'],file:'../owned-media/blog/open-source/vite-bundle-encrypt.md'},
{title:'How to Price Your Services guide',stream:'utility',threads:['utility-backbone'],file:'../owned-media/blog/guides/how-to-price-services.md'},
{title:'Show HN \u2014 Vite Bundle Encrypt',stream:'social',threads:['technical-credibility'],file:'../social/reddit/show-hn-vite-bundle-encrypt.md'},
{title:'LinkedIn #1: \u201c$37.6M per employee. 15x Apple.\u201d',stream:'social',threads:['extraction-spine'],file:'../social/linkedin/linkedin-01-revenue-per-employee.md'}
]},
{id:'P9',name:'Deepening',weeks:'18\u201319',act:4,items:[
{title:'The Harm-Reduction Fallacy',stream:'advocacy',threads:['philosophy-arc'],file:'../owned-media/blog/ai-philosophy/harm-reduction-fallacy.md'},
{title:'UI Sound Effects open-source post',stream:'technical',threads:['technical-credibility'],file:'../owned-media/blog/open-source/ui-sound-effects.md'},
{title:'Your Bank Decided You Don\u2019t Deserve an Account',stream:'provocation',threads:['provocation-escalation'],file:'../owned-media/blog/provocation/banking.md'},
{title:'Vite Encrypt on r/webdev, r/netlify',stream:'social',threads:['technical-credibility'],file:'../social/reddit/reddit-vite-encrypt-webdev.md'},
{title:'Banking provocation on r/antiwork',stream:'social',threads:['provocation-escalation'],file:'../social/reddit/reddit-banking-antiwork.md'}
]},
{id:'P10',name:'Permanence + Security',weeks:'20\u201321',act:5,items:[
{title:'Built to Outlast (permanent software)',stream:'advocacy',threads:['philosophy-arc','extraction-spine'],file:'../owned-media/blog/permanent-software/built-to-outlast.md'},
{title:'Gov Detection open-source post',stream:'technical',threads:['technical-credibility'],file:'../owned-media/blog/open-source/gov-detection.md'},
{title:'Setting Up an LLC for Adult Work guide',stream:'utility',threads:['utility-backbone'],file:'../owned-media/blog/guides/llc-for-adult-work.md'},
{title:'Show HN \u2014 Gov Detection',stream:'social',threads:['technical-credibility'],file:'../social/reddit/show-hn-gov-detection.md'},
{title:'LinkedIn #2: \u201cI built surgical robots for 20 years\u201d',stream:'social',threads:['technical-credibility'],file:'../social/linkedin/linkedin-02-surgical-robots.md'}
]},
{id:'P11',name:'Infrastructure + Labor',weeks:'22\u201323',act:5,items:[
{title:'Healthcare for Creators (human work)',stream:'advocacy',threads:['extraction-spine'],file:'../owned-media/blog/human-work/healthcare-human-work.md'},
{title:'TypeORM pgcrypto open-source post',stream:'technical',threads:['technical-credibility'],file:'../owned-media/blog/open-source/pgcrypto.md'},
{title:'OnlyFans Has 42 Employees and 4.6M Workers',stream:'provocation',threads:['provocation-escalation','extraction-spine'],file:'../owned-media/blog/provocation/onlyfans-employees.md'},
{title:'pgcrypto on r/PostgreSQL',stream:'social',threads:['technical-credibility'],file:'../social/reddit/reddit-pgcrypto-postgresql.md'},
{title:'What to Do When Your Bank Closes Your Account',stream:'utility',threads:['utility-backbone'],file:'../owned-media/blog/guides/bank-account-closed.md'}
]},
{id:'P12',name:'Cooperative Structure',weeks:'24\u201325',act:6,items:[
{title:'A Proposal for the Future Structuring of Corporations',stream:'advocacy',threads:['philosophy-arc','extraction-spine'],file:'../owned-media/blog/cooperative-future/proposal-corporations.md'},
{title:'Service Orchestrator open-source post',stream:'technical',threads:['technical-credibility'],file:'../owned-media/blog/open-source/service-orchestrator.md'},
{title:'The Nordic Model Killed 10 French Sex Workers in 6 Months',stream:'provocation',threads:['provocation-escalation'],file:'../owned-media/blog/provocation/nordic-model.md'},
{title:'Cooperative-future thread (Twitter)',stream:'social',threads:['philosophy-arc'],file:'../social/twitter/thread-cooperative-future.md'},
{title:'LinkedIn #3: \u201c46% of creators lost their bank account\u201d',stream:'social',threads:['provocation-escalation'],file:'../social/linkedin/linkedin-03-creators-bank-account.md'}
]},
{id:'P13',name:'Cooperative Deepening + AI',weeks:'26\u201327',act:6,items:[
{title:'Why Every Platform Has an Expiration Date',stream:'advocacy',threads:['philosophy-arc'],file:'../owned-media/blog/cooperative-future/platform-expiration.md'},
{title:'MCP Tools open-source post',stream:'technical',threads:['technical-credibility'],file:'../owned-media/blog/open-source/mcp-tools.md'},
{title:'Crypto for Creators guide',stream:'utility',threads:['utility-backbone'],file:'../owned-media/blog/guides/crypto-for-creators.md'},
{title:'Show HN \u2014 MCP Tools',stream:'social',threads:['technical-credibility'],file:'../social/reddit/show-hn-mcp-tools.md'},
{title:'MCP Tools on r/ClaudeAI',stream:'social',threads:['technical-credibility'],file:'../social/reddit/reddit-mcp-tools-claudeai.md'}
]},
{id:'P14',name:'Structure + Provocation',weeks:'28\u201329',act:6,items:[
{title:'The Operating System, Not the Operator',stream:'advocacy',threads:['philosophy-arc'],file:'../owned-media/blog/cooperative-future/operating-system-not-operator.md'},
{title:'Pipeline Framework open-source post',stream:'technical',threads:['technical-credibility'],file:'../owned-media/blog/open-source/pipeline-framework.md'},
{title:'I Built Surgical Robots for 20 Years',stream:'provocation',threads:['provocation-escalation'],file:'../owned-media/blog/provocation/surgical-robots-credibility.md'},
{title:'Pipeline Framework on r/MachineLearning',stream:'social',threads:['technical-credibility'],file:'../social/reddit/reddit-pipeline-machinelearning.md'},
{title:'LinkedIn #4: \u201c4-layer protection\u201d',stream:'social',threads:['protection-stack'],file:'../social/linkedin/linkedin-04-face-detection.md'}
]},
{id:'P15',name:'Slutology + ML Teaser',weeks:'30\u201331',act:7,items:[
{title:'Cheating Is Wrong Because It\u2019s Lying (slutology #1)',stream:'advocacy',threads:['philosophy-arc'],file:'../owned-media/blog/slutology/cheating-is-lying.md'},
{title:'Your Legal Rights as a Sex Worker guide',stream:'utility',threads:['utility-backbone'],file:'../owned-media/blog/guides/legal-rights-sex-worker.md'},
{title:'What Chaturbate\u2019s 50% Take Rate Buys You: Nothing',stream:'provocation',threads:['provocation-escalation','extraction-spine'],file:'../owned-media/blog/provocation/chaturbate-take-rate.md'},
{title:'Slutology thread (Twitter)',stream:'social',threads:['philosophy-arc'],file:'../social/twitter/thread-slutology.md'},
{title:'r/LocalLLaMA teaser \u2014 \u201cSomething bigger is coming\u201d',stream:'social',threads:['technical-credibility'],file:'../social/reddit/reddit-localllama-teaser.md'}
]},
{id:'P16',name:'Slutology Capstone + Crown Jewel',weeks:'32\u201333',act:7,items:[
{title:'The Exploration Gap + Sexology Studies It (slutology capstone)',stream:'advocacy',threads:['philosophy-arc','extraction-spine'],file:'../owned-media/blog/slutology/exploration-gap.md',file2:'../owned-media/blog/slutology/sexology-studies-it.md'},
{title:'How We Run 12 ML Models on One GPU: Model Boss',stream:'technical',threads:['technical-credibility'],file:'../owned-media/blog/open-source/model-boss.md'},
{title:'Mastercard Decides Who Gets to Work',stream:'provocation',threads:['provocation-escalation'],file:'../owned-media/blog/provocation/mastercard.md'},
{title:'Show HN \u2014 Model Boss',stream:'social',threads:['technical-credibility'],file:'../social/reddit/show-hn-model-boss.md'},
{title:'r/LocalLLaMA \u2014 Model Boss',stream:'social',threads:['technical-credibility'],file:'../social/reddit/reddit-localllama-model-boss.md'}
]},
{id:'P17',name:'Afterglow + Bridge',weeks:'34\u201335',act:8,items:[
{title:'The GPU Poverty Problem (Model Boss companion)',stream:'technical',threads:['technical-credibility'],file:'../owned-media/blog/open-source/gpu-poverty.md'},
{title:'LinkedIn #5: \u201cThe cooperative economy is coming to adult\u201d',stream:'social',threads:['philosophy-arc'],file:'../social/linkedin/linkedin-05-cooperative-economy.md'},
{title:'Model Boss on r/selfhosted',stream:'social',threads:['technical-credibility'],file:'../social/reddit/reddit-model-boss-selfhosted.md'},
{title:'GPU Poverty on dev.to',stream:'social',threads:['technical-credibility'],file:'../social/devto/devto-gpu-poverty.md'},
{title:'Model Boss on dev.to (series capstone)',stream:'social',threads:['technical-credibility'],file:'../social/devto/devto-model-boss-capstone.md'}
]}
];
// Assign IDs
PERIODS.forEach(function(p, pi) {
p.items.forEach(function(item, ii) {
item.id = p.id + '-' + ii;
item.periodIdx = pi;
});
});
// ============================================================
// HELPERS
// ============================================================
function svgEl(tag, attrs) {
var el = document.createElementNS(NS, tag);
if (attrs) {
var keys = Object.keys(attrs);
for (var k = 0; k < keys.length; k++) el.setAttribute(keys[k], attrs[keys[k]]);
}
return el;
}
function hexToRgb(hex) {
return parseInt(hex.slice(1,3),16)+','+parseInt(hex.slice(3,5),16)+','+parseInt(hex.slice(5,7),16);
}
function hexAlpha(hex, a) { return 'rgba('+hexToRgb(hex)+','+a+')'; }
function toRoman(n) { return ['I','II','III','IV','V','VI','VII','VIII'][n-1] || String(n); }
function makeBadge(text, bgColor, fgColor, borderColor) {
var span = document.createElement('span');
span.className = 'tt-badge';
span.textContent = text;
span.style.background = bgColor;
span.style.color = fgColor;
if (borderColor) span.style.border = '1px solid ' + borderColor;
return span;
}
// ============================================================
// SVG LAYOUT
// ============================================================
var GRID_LEFT = 88, GRID_RIGHT = 1485;
var ARC_BASE = 158;
var GRID_TOP = 172, GRID_BOTTOM = 545;
var COL_W = (GRID_RIGHT - GRID_LEFT) / 18;
var ROW_H = (GRID_BOTTOM - GRID_TOP) / 5;
var ITEM_W = 36, ITEM_H = 15;
function colX(i) { return GRID_LEFT + i * COL_W + COL_W / 2; }
function rowY(si) { return GRID_TOP + si * ROW_H + ROW_H / 2; }
function weekToX(w) { return GRID_LEFT + ((w - 1) / 34) * (GRID_RIGHT - GRID_LEFT); }
// ============================================================
// BUILD STORY MAP
// ============================================================
function buildStoryMap() {
var svg = document.getElementById('story-map');
// Grid lines
for (var ci = 0; ci <= 18; ci++) {
svg.appendChild(svgEl('line', {
x1: GRID_LEFT + ci * COL_W, y1: GRID_TOP - 2,
x2: GRID_LEFT + ci * COL_W, y2: GRID_BOTTOM, 'class': 'grid-line'
}));
}
for (var ri = 0; ri <= 5; ri++) {
svg.appendChild(svgEl('line', {
x1: GRID_LEFT, y1: GRID_TOP + ri * ROW_H,
x2: GRID_RIGHT, y2: GRID_TOP + ri * ROW_H, 'class': 'grid-line'
}));
}
// Stream row labels
STREAM_ORDER.forEach(function(s, si) {
var t = svgEl('text', {
x: GRID_LEFT - 8, y: rowY(si) + 4,
'text-anchor': 'end', 'font-size': '9', fill: STREAM_COLORS[s],
'font-weight': '600', 'letter-spacing': '0.03em'
});
t.textContent = STREAM_LABELS[s].slice(0, 5).toUpperCase();
svg.appendChild(t);
});
// Period column labels
PERIODS.forEach(function(p, i) {
var t = svgEl('text', {
x: colX(i), y: GRID_TOP - 8,
'text-anchor': 'middle', 'font-size': '8.5', fill: '#777',
'font-weight': '700', 'font-family': "'JetBrains Mono', monospace"
});
t.textContent = p.id;
svg.appendChild(t);
});
// Act dividers
var boundaries = [2, 4, 7, 9, 11, 14, 16];
boundaries.forEach(function(after) {
var dx = GRID_LEFT + (after + 1) * COL_W;
svg.appendChild(svgEl('line', {
x1: dx, y1: 6, x2: dx, y2: GRID_BOTTOM, 'class': 'act-divider'
}));
});
// Act labels
var actSpans = [[0,2],[3,4],[5,7],[8,9],[10,11],[12,14],[15,16],[17,17]];
actSpans.forEach(function(span, ai) {
var cx = (colX(span[0]) + colX(span[1])) / 2;
var t = svgEl('text', {
x: cx, y: 14, 'text-anchor': 'middle', 'font-size': '8',
fill: '#c084fc', 'font-weight': '800', opacity: '0.7',
'font-family': "'JetBrains Mono', monospace"
});
t.textContent = 'ACT ' + toRoman(ai + 1);
svg.appendChild(t);
});
// Narrative thread arcs (inserted before items so items are on top)
var arcGroup = svgEl('g', {opacity: '0.4'});
svg.appendChild(arcGroup);
THREADS.forEach(function(thread) {
var periodSet = {};
PERIODS.forEach(function(p, pi) {
p.items.forEach(function(item) {
if (item.threads.indexOf(thread.id) >= 0) periodSet[pi] = true;
});
});
var pis = Object.keys(periodSet).map(Number).sort(function(a,b){return a-b;});
for (var ai = 0; ai < pis.length - 1; ai++) {
var i1 = pis[ai], i2 = pis[ai + 1];
var x1 = colX(i1), x2 = colX(i2);
var h = Math.min(135, Math.max(14, (i2 - i1) * 10));
svg.appendChild(svgEl('path', {
d: 'M '+x1+' '+ARC_BASE+' C '+x1+' '+(ARC_BASE-h)+', '+x2+' '+(ARC_BASE-h)+', '+x2+' '+ARC_BASE,
fill: 'none', stroke: thread.color,
'stroke-width': thread.id === 'technical-credibility' ? '1.5' : '2',
'stroke-dasharray': thread.dash || 'none',
'stroke-linecap': 'round', 'class': 'thread-arc', 'data-thread': thread.id
}));
}
});
// Arc group is already appended after grid lines/labels.
// Items will be appended next, so z-order is: grid lines < arcs < items.
// Place items
PERIODS.forEach(function(period, pi) {
var byStream = {};
period.items.forEach(function(item) {
if (!byStream[item.stream]) byStream[item.stream] = [];
byStream[item.stream].push(item);
});
STREAM_ORDER.forEach(function(stream, si) {
var items = byStream[stream] || [];
var cx = colX(pi), cy = rowY(si);
var n = items.length;
if (n === 0) return;
var spacing = n <= 2 ? 22 : (n <= 3 ? 16 : 11);
var startOff = -(n - 1) * spacing / 2;
items.forEach(function(item, idx) {
var ix = cx + startOff + idx * spacing;
item._x = ix;
item._y = cy;
var g = svgEl('g', {'class': 'item-rect', 'data-id': item.id, 'data-threads': item.threads.join(',')});
g.appendChild(svgEl('rect', {
x: ix - ITEM_W/2, y: cy - ITEM_H/2, width: ITEM_W, height: ITEM_H,
rx: 3, ry: 3, fill: STREAM_COLORS[item.stream], opacity: '0.85',
stroke: item.flex ? '#fbbf24' : 'none',
'stroke-width': item.flex ? '1.5' : '0',
'stroke-dasharray': item.flex ? '3,2' : ''
}));
svg.appendChild(g);
});
});
});
// Conference markers
CONFERENCES.forEach(function(conf) {
var cx = weekToX(conf.week), cy = GRID_TOP - 1, d = 5;
svg.appendChild(svgEl('polygon', {
points: cx+','+(cy-d)+' '+(cx+d)+','+cy+' '+cx+','+(cy+d)+' '+(cx-d)+','+cy,
fill: conf.color, opacity: '0.8'
}));
var t = svgEl('text', {
x: cx, y: cy - d - 3, 'text-anchor': 'middle', 'font-size': '6.5',
fill: conf.color, 'font-weight': '600', opacity: '0.75'
});
t.textContent = conf.name;
svg.appendChild(t);
});
}
// ============================================================
// BUILD SPARKLINE
// ============================================================
function buildSparkline() {
var svg = document.getElementById('cadence-sparkline');
var barW = 30, gap = 10, maxH = 40, baseline = 55;
PERIODS.forEach(function(p, i) {
var count = p.items.length;
var h = (count / 7) * maxH;
var x = 8 + i * (barW + gap);
var isLaunch = i === 0;
svg.appendChild(svgEl('rect', {
x: x, y: baseline - h, width: barW, height: h,
rx: 3, ry: 3, fill: isLaunch ? '#c084fc' : '#444',
opacity: isLaunch ? '1' : '0.55'
}));
var ct = svgEl('text', {
x: x + barW/2, y: baseline - h - 4, 'text-anchor': 'middle',
'font-size': '9', fill: isLaunch ? '#c084fc' : '#777',
'font-weight': '600', 'font-family': "'JetBrains Mono', monospace"
});
ct.textContent = count;
svg.appendChild(ct);
var pt = svgEl('text', {
x: x + barW/2, y: baseline + 11, 'text-anchor': 'middle',
'font-size': '7', fill: '#555', 'font-family': "'JetBrains Mono', monospace"
});
pt.textContent = p.id;
svg.appendChild(pt);
});
}
// ============================================================
// BUILD ACT CARDS
// ============================================================
function buildActCards() {
var container = document.getElementById('act-cards');
ACTS.forEach(function(act) {
var card = document.createElement('div');
card.className = 'act-card';
var header = document.createElement('div');
header.className = 'act-card-header';
var badge = document.createElement('span');
badge.className = 'act-badge';
badge.textContent = 'Act ' + toRoman(act.num);
header.appendChild(badge);
var name = document.createElement('span');
name.className = 'act-name';
name.textContent = act.name;
header.appendChild(name);
var periods = document.createElement('span');
periods.className = 'act-periods-label';
periods.textContent = act.periods.join(', ');
header.appendChild(periods);
var chevron = document.createElement('span');
chevron.className = 'act-chevron';
chevron.textContent = '\u25be';
header.appendChild(chevron);
card.appendChild(header);
var body = document.createElement('div');
body.className = 'act-card-body';
var thesis = document.createElement('p');
thesis.className = 'act-thesis';
thesis.textContent = act.thesis;
body.appendChild(thesis);
// Active threads
var activeThreadIds = {};
act.periods.forEach(function(pid) {
var period = PERIODS.find(function(p){return p.id===pid;});
if (period) period.items.forEach(function(item) {
item.threads.forEach(function(tid) { activeThreadIds[tid] = true; });
});
});
var threadsRow = document.createElement('div');
threadsRow.className = 'act-threads-row';
THREADS.forEach(function(t) {
if (activeThreadIds[t.id]) {
var pill = document.createElement('span');
pill.className = 'act-thread-pill';
pill.style.background = hexAlpha(t.color, 0.15);
pill.style.color = t.color;
pill.style.border = '1px solid ' + hexAlpha(t.color, 0.35);
pill.textContent = t.name;
threadsRow.appendChild(pill);
}
});
body.appendChild(threadsRow);
// Items by period
act.periods.forEach(function(pid) {
var period = PERIODS.find(function(p){return p.id===pid;});
if (!period) return;
var groupLabel = document.createElement('div');
groupLabel.className = 'act-period-label';
groupLabel.textContent = pid + ': ' + period.name + ' (Weeks ' + period.weeks + ')';
body.appendChild(groupLabel);
period.items.forEach(function(item) {
var row = document.createElement('div');
row.className = 'act-period-item';
var sBadge = document.createElement('span');
sBadge.className = 'stream-badge ' + item.stream;
sBadge.textContent = STREAM_LABELS[item.stream];
row.appendChild(sBadge);
if (item.file) {
var link = document.createElement('a');
link.href = item.file;
link.textContent = item.title;
row.appendChild(link);
} else {
var txt = document.createElement('span');
txt.textContent = item.title;
row.appendChild(txt);
}
if (item.flex) {
var ft = document.createElement('span');
ft.className = 'flex-tag';
ft.textContent = 'FLEX';
row.appendChild(ft);
}
body.appendChild(row);
});
});
var xref = document.createElement('div');
xref.className = 'act-xref';
xref.textContent = act.xref;
body.appendChild(xref);
card.appendChild(body);
header.addEventListener('click', function() { card.classList.toggle('expanded'); });
container.appendChild(card);
});
}
// ============================================================
// BUILD PERIOD GRID
// ============================================================
function buildPeriodGrid() {
var container = document.getElementById('period-grid');
PERIODS.forEach(function(period) {
var card = document.createElement('div');
card.className = 'period-card';
var header = document.createElement('div');
header.className = 'period-header';
var num = document.createElement('span');
num.className = 'period-num';
num.textContent = period.id;
header.appendChild(num);
var weeks = document.createElement('span');
weeks.className = 'period-weeks';
weeks.textContent = 'Weeks ' + period.weeks;
header.appendChild(weeks);
var title = document.createElement('span');
title.className = 'period-title';
title.textContent = period.name;
header.appendChild(title);
card.appendChild(header);
var items = document.createElement('div');
items.className = 'period-items';
period.items.forEach(function(item) {
var row = document.createElement('div');
row.className = 'period-item ' + item.stream;
var content = document.createElement('div');
content.className = 'period-item-content';
var nameDiv = document.createElement('div');
nameDiv.className = 'period-item-name';
if (item.file) {
var link = document.createElement('a');
link.href = item.file;
link.textContent = item.title;
nameDiv.appendChild(link);
if (item.file2) {
nameDiv.appendChild(document.createTextNode(' + '));
var link2 = document.createElement('a');
link2.href = item.file2;
link2.textContent = '(companion)';
nameDiv.appendChild(link2);
}
} else {
nameDiv.textContent = item.title;
}
if (item.flex) {
var ft = document.createElement('span');
ft.className = 'flex-tag';
ft.textContent = 'FLEX';
nameDiv.appendChild(ft);
}
content.appendChild(nameDiv);
var badge = document.createElement('span');
badge.className = 'stream-badge ' + item.stream;
badge.textContent = STREAM_LABELS[item.stream];
content.appendChild(badge);
row.appendChild(content);
items.appendChild(row);
});
card.appendChild(items);
container.appendChild(card);
});
}
// ============================================================
// INTERACTIVITY
// ============================================================
function setupInteractions() {
var svg = document.getElementById('story-map');
var wrapper = document.getElementById('story-map-wrapper');
var tooltip = document.getElementById('sm-tooltip');
var detailPanel = document.getElementById('detail-panel');
var ttTitle = tooltip.querySelector('.tt-title');
var ttBadges = tooltip.querySelector('.tt-badges');
var activeItemId = null;
var itemLookup = {};
PERIODS.forEach(function(p) {
p.items.forEach(function(item) { itemLookup[item.id] = item; });
});
// Hover
svg.addEventListener('mouseover', function(e) {
var g = e.target.closest('.item-rect');
if (!g) return;
var threadStr = g.getAttribute('data-threads');
if (!threadStr) return;
var threads = threadStr.split(',');
svg.classList.add('highlighting');
var rects = svg.querySelectorAll('.item-rect');
for (var i = 0; i < rects.length; i++) {
var rt = (rects[i].getAttribute('data-threads') || '').split(',');
var match = false;
for (var t = 0; t < threads.length; t++) {
if (rt.indexOf(threads[t]) >= 0) { match = true; break; }
}
if (match) rects[i].classList.add('hl');
}
var arcs = svg.querySelectorAll('.thread-arc');
for (var a = 0; a < arcs.length; a++) {
if (threads.indexOf(arcs[a].getAttribute('data-thread')) >= 0) arcs[a].classList.add('hl');
}
// Tooltip content (safe DOM construction)
var item = itemLookup[g.getAttribute('data-id')];
if (item) {
ttTitle.textContent = item.title;
while (ttBadges.firstChild) ttBadges.removeChild(ttBadges.firstChild);
ttBadges.appendChild(makeBadge(
STREAM_LABELS[item.stream],
hexAlpha(STREAM_COLORS[item.stream], 0.25),
STREAM_COLORS[item.stream]
));
item.threads.forEach(function(tid) {
var thread = THREAD_MAP[tid];
if (thread) {
ttBadges.appendChild(makeBadge(thread.name, hexAlpha(thread.color, 0.2), thread.color));
}
});
var rect = g.getBoundingClientRect();
var wr = wrapper.getBoundingClientRect();
tooltip.style.left = (rect.left + rect.width/2 - wr.left) + 'px';
tooltip.style.top = (rect.top - wr.top - 10) + 'px';
tooltip.style.transform = 'translate(-50%, -100%)';
tooltip.classList.add('visible');
}
});
svg.addEventListener('mouseout', function(e) {
var g = e.target.closest('.item-rect');
if (!g) return;
if (e.relatedTarget && e.relatedTarget.closest && e.relatedTarget.closest('.item-rect')) return;
svg.classList.remove('highlighting');
var hls = svg.querySelectorAll('.hl');
for (var i = 0; i < hls.length; i++) hls[i].classList.remove('hl');
tooltip.classList.remove('visible');
});
// Click: detail panel (safe DOM construction)
svg.addEventListener('click', function(e) {
var g = e.target.closest('.item-rect');
if (!g) {
detailPanel.classList.remove('visible');
activeItemId = null;
return;
}
var itemId = g.getAttribute('data-id');
if (itemId === activeItemId) {
detailPanel.classList.remove('visible');
activeItemId = null;
return;
}
activeItemId = itemId;
var item = itemLookup[itemId];
if (!item) return;
var period = PERIODS[item.periodIdx];
// Clear panel
while (detailPanel.firstChild) detailPanel.removeChild(detailPanel.firstChild);
// Close button
var closeBtn = document.createElement('button');
closeBtn.className = 'dp-close';
closeBtn.setAttribute('aria-label', 'Close');
closeBtn.textContent = '\u00d7';
closeBtn.addEventListener('click', function() {
detailPanel.classList.remove('visible');
activeItemId = null;
});
detailPanel.appendChild(closeBtn);
// Title
var titleDiv = document.createElement('div');
titleDiv.className = 'dp-title';
titleDiv.textContent = item.title;
detailPanel.appendChild(titleDiv);
// Meta badges
var meta = document.createElement('div');
meta.className = 'dp-meta';
var streamBadge = document.createElement('span');
streamBadge.className = 'dp-badge';
streamBadge.style.background = hexAlpha(STREAM_COLORS[item.stream], 0.25);
streamBadge.style.color = STREAM_COLORS[item.stream];
streamBadge.textContent = STREAM_LABELS[item.stream];
meta.appendChild(streamBadge);
var periodBadge = document.createElement('span');
periodBadge.className = 'dp-badge';
periodBadge.style.background = 'rgba(192,132,252,0.15)';
periodBadge.style.color = '#c084fc';
periodBadge.textContent = period.id + ': ' + period.name;
meta.appendChild(periodBadge);
var weekBadge = document.createElement('span');
weekBadge.className = 'dp-badge';
weekBadge.style.background = 'rgba(255,255,255,0.06)';
weekBadge.style.color = '#888';
weekBadge.textContent = 'Weeks ' + period.weeks;
meta.appendChild(weekBadge);
detailPanel.appendChild(meta);
// Thread badges
var threadsDiv = document.createElement('div');
threadsDiv.className = 'dp-threads';
var threadsLabel = document.createElement('div');
threadsLabel.className = 'dp-threads-label';
threadsLabel.textContent = 'Narrative threads:';
threadsDiv.appendChild(threadsLabel);
var threadMeta = document.createElement('div');
threadMeta.className = 'dp-meta';
item.threads.forEach(function(tid) {
var t = THREAD_MAP[tid];
if (t) {
var tb = document.createElement('span');
tb.className = 'dp-badge';
tb.style.background = hexAlpha(t.color, 0.18);
tb.style.color = t.color;
tb.style.border = '1px solid ' + hexAlpha(t.color, 0.35);
tb.textContent = t.name;
threadMeta.appendChild(tb);
}
});
threadsDiv.appendChild(threadMeta);
detailPanel.appendChild(threadsDiv);
// File links
if (item.file) {
var linkDiv = document.createElement('div');
linkDiv.className = 'dp-link';
var a = document.createElement('a');
a.href = item.file;
a.textContent = item.file;
linkDiv.appendChild(a);
detailPanel.appendChild(linkDiv);
}
if (item.file2) {
var linkDiv2 = document.createElement('div');
linkDiv2.className = 'dp-link';
linkDiv2.style.marginTop = '0.25rem';
var a2 = document.createElement('a');
a2.href = item.file2;
a2.textContent = item.file2;
linkDiv2.appendChild(a2);
detailPanel.appendChild(linkDiv2);
}
// Flex note
if (item.flex) {
var flexNote = document.createElement('div');
flexNote.className = 'dp-flex-note';
flexNote.textContent = 'This item is marked FLEX \u2014 can shift to P1 without narrative impact. Tax Guide benefits from thesis essays being indexed first; SSRN benefits from processing time before press outreach.';
detailPanel.appendChild(flexNote);
}
detailPanel.classList.add('visible');
});
}
// ============================================================
// INIT
// ============================================================
buildStoryMap();
buildSparkline();
buildActCards();
buildPeriodGrid();
setupInteractions();
})();
</script>
</body>
</html>