1433 lines
59 KiB
HTML
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 — Story Map</h1>
|
|
<p class="subtitle">18 periods (P0–P17) · 7 narrative threads · 8 acts · 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–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–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–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–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–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–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–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) — 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–P17)</div>
|
|
<div class="period-grid" id="period-grid"></div>
|
|
|
|
<div class="timestamp">Story Map — 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>
|