// iPhone onboarding — fully interactive 8-step funnel. // Shared chrome + step bodies. All controls wired to parent state via props. const M = window.MS_TOKENS; const { useState, useEffect, useRef } = React; // ----------------------------------------------------------------------------- // Shared libraries (exported so App can derive defaults/labels) // ----------------------------------------------------------------------------- const TOPICS_LIBRARY = [ "AI", "Longevity", "Health", "Geopolitics", "Research", "Nutrition", "Exercise", "Markets", "Biotech", "Climate", "Space", "Policy", ]; const SOURCES_LIBRARY = [ { id: "ft", name: "Financial Times", mark: "FT", bg: "#FFF1E5", fg: "#000" }, { id: "econ", name: "The Economist", mark: "TE", bg: "#E3120B", fg: "#fff" }, { id: "nature", name: "Nature", mark: "N", bg: "#006633", fg: "#fff" }, { id: "wiki", name: "Wikipedia", mark: "W", bg: "#fff", fg: "#111" }, { id: "arxiv", name: "arXiv", mark: "aX", bg: "#b31b1b", fg: "#fff" }, { id: "reuters", name: "Reuters", mark: "R", bg: "#FA6400", fg: "#fff" }, { id: "nejm", name: "NEJM", mark: "NE", bg: "#cc0000", fg: "#fff" }, { id: "bloomb", name: "Bloomberg", mark: "B", bg: "#000", fg: "#fff" }, { id: "mittr", name: "MIT TR", mark: "MIT", bg: "#A41F35", fg: "#fff" }, { id: "guard", name: "Guardian", mark: "G", bg: "#052962", fg: "#fff" }, { id: "strat", name: "Stratechery", mark: "ST", bg: "#1a1a1a", fg: "#fff" }, { id: "semafor", name: "Semafor", mark: "Sm", bg: "#1f1f1f", fg: "#fff" }, ]; const DEFAULT_SOURCE_IDS = ["ft", "econ", "nature", "wiki", "reuters", "mittr"]; const STYLES_LIBRARY = [ { id: "original", name: "Original", italic: "Signal" }, { id: "editorial", name: "Editorial" }, { id: "developer", name: "Developer" }, { id: "research", name: "Research" }, { id: "custom", name: "Custom", locked: true }, ]; const HOURS = [4, 5, 6, 7, 8, 9, 10]; const MINUTES = [0, 15, 30, 45]; const DAY_LABELS = ["M", "T", "W", "T", "F", "S", "S"]; const DAY_NAMES_FULL = ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"]; const MONTH_ABBR = ["Jan","Feb","Mar","Apr","May","Jun","Jul","Aug","Sep","Oct","Nov","Dec"]; const pad2 = (n) => String(n).padStart(2, "0"); // ----------------------------------------------------------------------------- // Shared chrome // ----------------------------------------------------------------------------- function MScreen({ step, total = 8, children, scroll = true, dark = false }) { const bg = dark ? M.ink : M.paper; const fg = dark ? M.paper : M.ink; return (
{!dark &&
}
Millennium Signal
{pad2(step)} / {pad2(total)}
{Array.from({ length: total }).map((_, i) => (
))}
{children}
); } function MKicker({ num, children }) { return (
{num && {num}} {children}
); } function MHed({ children, italic, size = 32 }) { return (

{children}{italic && {italic}}

); } function MDeck({ children }) { return

{children}

; } function MBtn({ children, primary = true, arrow = true, amber, onClick, disabled }) { const bg = disabled ? M.rule2 : (amber ? M.amber : (primary ? M.ink : "transparent")); const fg = disabled ? M.mid : (amber ? M.ink : (primary ? M.paper : M.ink)); return ( ); } // ============================================================================= // 01 · Entry — collect email + choose Default or Custom // ============================================================================= const EMAIL_RE = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; function MStep01({ email, onSetEmail, onPick }) { const valid = EMAIL_RE.test(email || ""); const tryPick = (mode) => { if (!valid) return; onPick(mode); }; return (
How we begin Where to send your Your inbox is your account. Change anything later by reply.
▸ Step one · your email {valid ? "✓ READY" : "REQUIRED"}
onSetEmail(e.target.value)} placeholder="you@example.com" style={{ width: "100%", marginTop: 10, padding: "10px 0", fontFamily: M.display, fontSize: 24, letterSpacing: -0.5, border: "none", borderBottom: `2px solid ${valid ? M.ink : M.amber}`, background: "transparent", outline: "none", color: M.ink, transition: "border-color 200ms", }} />
{valid ? "Looks good. Pick a path below — we'll send your first edition to this address." : "We'll send your full briefing here. Reply to any edition to change anything."}
— then choose how —
tryPick("default")} style={{ margin: "8px 20px 0", border: `1px solid ${M.rule}`, background: M.paper2, padding: 18, cursor: valid ? "pointer" : "not-allowed", opacity: valid ? 1 : 0.5, }}>
RECOMMENDED 60 SEC
The Default
Our editor's curation. Daily at 07:00.
▸ 6 trusted sources
▸ Original style
▸ Editable any time
{ e.stopPropagation(); tryPick("default"); }}>Use the Default
tryPick("custom")} style={{ margin: "12px 20px 24px", border: `1px solid ${M.ink}`, background: M.ink, color: M.paper, padding: 18, cursor: valid ? "pointer" : "not-allowed", opacity: valid ? 1 : 0.55, }}> ~ 3 MIN
Custom
Shape it yourself — topics, sources, style, cadence.
); } // ============================================================================= // 02 · Topics — text input + voice + library chips // ============================================================================= function MStep02({ topics, onAddTopic, onRemoveTopic }) { const [draft, setDraft] = useState(""); const [recording, setRecording] = useState(false); const recogRef = useRef(null); const inputRef = useRef(null); const commit = () => { const t = draft.trim(); if (t && !topics.includes(t)) onAddTopic(t); setDraft(""); }; const toggleVoice = () => { const SR = window.SpeechRecognition || window.webkitSpeechRecognition; if (!SR) { alert("Voice input not supported in this browser. Try Chrome or Safari on iOS."); inputRef.current?.focus(); return; } if (recording) { recogRef.current?.stop(); setRecording(false); return; } const r = new SR(); r.lang = "en-US"; r.interimResults = true; r.continuous = false; r.onresult = (e) => { const txt = Array.from(e.results).map(x => x[0].transcript).join(" "); setDraft(txt); }; r.onend = () => setRecording(false); r.onerror = () => setRecording(false); recogRef.current = r; setRecording(true); r.start(); }; return (
What you want to know Write it, Three inputs, one canvas. Use any or all.
▸ Your topics · {topics.length}
{topics.length === 0 && ( Nothing yet. Type, speak, or pick below. )} {topics.map(t => ( onRemoveTopic(t)} style={{ display: "inline-flex", alignItems: "center", gap: 5, background: M.ink, color: M.paper, padding: "6px 12px", fontFamily: M.display, fontSize: 13, fontWeight: 700, borderRadius: 999, cursor: "pointer", userSelect: "none", }}> {t}× ))}
setDraft(e.target.value)} onKeyDown={e => { if (e.key === "Enter") commit(); }} placeholder="Type a topic…" style={{ flex: 1, fontFamily: M.display, fontSize: 18, letterSpacing: -0.4, borderBottom: `1.5px solid ${M.ink}`, paddingBottom: 3, border: "none", borderBottomWidth: 1.5, borderBottomStyle: "solid", borderBottomColor: M.ink, background: "transparent", outline: "none", color: M.ink, minWidth: 0, }} />
{recording && (
● LISTENING · tap to stop
)}
— or start from the library —
{TOPICS_LIBRARY.map(t => { const sel = topics.includes(t); return ( sel ? onRemoveTopic(t) : onAddTopic(t)} style={{ padding: "6px 12px", borderRadius: 999, fontFamily: M.display, fontSize: 13, cursor: "pointer", userSelect: "none", border: `1px solid ${sel ? M.ink : M.rule}`, background: sel ? M.ink : "transparent", color: sel ? M.paper : M.ink, fontWeight: sel ? 700 : 400, }}> {t}{sel && } ); })}
); } // ============================================================================= // 03 · Sources // ============================================================================= function MStep03({ sources, onToggleSource, onPickForMe }) { const selCount = sources.length; const coverage = selCount === 0 ? "—" : selCount < 3 ? "Narrow" : selCount < 7 ? "Broad" : "Very broad"; const trust = selCount === 0 ? "—" : selCount < 3 ? "B" : selCount < 6 ? "A" : "A+"; const desc = (() => { if (selCount === 0) return "pick at least one"; const tags = new Set(); sources.forEach(id => { if (["ft","econ","bloomb"].includes(id)) tags.add("fin"); if (["nature","arxiv","nejm","mittr"].includes(id)) tags.add("research"); if (["reuters","guard","semafor"].includes(id)) tags.add("news"); if (["wiki","strat"].includes(id)) tags.add("analysis"); }); return Array.from(tags).slice(0, 2).join(" · ") || "mixed"; })(); return (
Whom you trust The voices that Pick a few, pick many, or let us choose.
{SOURCES_LIBRARY.map(s => { const sel = sources.includes(s.id); return ( onToggleSource(s.id)} style={{ display: "inline-flex", alignItems: "center", gap: 7, padding: "5px 12px 5px 5px", background: sel ? M.ink : M.paper, color: sel ? M.paper : M.ink, border: `1px solid ${sel ? M.ink : M.rule}`, borderRadius: 999, fontFamily: M.display, fontSize: 12.5, fontWeight: sel ? 700 : 400, cursor: "pointer", userSelect: "none", }}> {s.mark} {s.name} {sel && } ); })} Select for me
{[ ["SELECTED", String(selCount), `of ${SOURCES_LIBRARY.length}`], ["COVERAGE", coverage, desc], ["TRUST", trust, "weighted"], ].map(([l,v,s]) => (
{l}
{v}
{s}
))}
); } // ----------------------------------------------------------------------------- // Tiny style previews (unchanged visual) // ----------------------------------------------------------------------------- function OriginalMini() { return (
▸ VOL 01 · 022● LIVE
Fusion's quiet breakthrough
› net-energy confirmed at NIF
read ▸ Nature · 14 min
); } function EditorialMini() { return (
The Signal · Thursday
Fusion Crosses Threshold
a correspondent's note
Livermore reported a third net-energy shot this week, marking the clearest sign yet that…
); } function DeveloperMini() { return (
# signal.md
## Fusion breakthrough
> net-energy × 3 at NIF
- [x] confirmed
- [ ] commercial path
[source](nature.com) 14m
$ next ▸
); } function ResearchMini() { return (
Brief · 24 Apr 2026
Fusion net-energy, repeated
— 3rd successful shot[1]
— 3.15 MJ yield[2]
— peer review pending
[1] Nature [2] LLNL brief
); } function LockedMini() { return (
Your style
Premium · soon
); } const STYLE_MINIS = { original: , editorial: , developer: , research: , custom: , }; // ============================================================================= // 04 · Style (tap-to-select carousel) // ============================================================================= function MStep04({ style, onPickStyle }) { return (
How it looks in your inbox The form of intelligence Same signal. Four voices. Tap to select.
{STYLES_LIBRARY.map((s, i) => { const sel = style === s.id; return (
{ if (!s.locked) onPickStyle(s.id); }} style={{ minWidth: 220, marginLeft: i === 0 ? 20 : 0, marginRight: i === STYLES_LIBRARY.length - 1 ? 20 : 0, border: `1.5px solid ${sel ? M.ink : M.rule}`, background: M.paper2, outline: sel ? `3px solid ${M.amber}` : "none", outlineOffset: 2, scrollSnapAlign: "start", position: "relative", opacity: s.locked ? 0.7 : 1, cursor: s.locked ? "not-allowed" : "pointer", flexShrink: 0, }}> {s.locked && (
◐ PREMIUM
)}
{STYLE_MINIS[s.id]}
{s.name}{s.italic && {s.italic}}
{sel &&
✓ SELECTED
}
); })}
{STYLES_LIBRARY.map(s => (
))}
); } // ============================================================================= // 05 · Frequency — segmented + day grid + live preview // ============================================================================= function computeNextEditions(days, hour, minute, count = 4) { if (!days || days.length === 0) return []; const out = []; const now = new Date(); const start = new Date(now); start.setSeconds(0, 0); for (let off = 0; out.length < count && off < 30; off++) { const d = new Date(start); d.setDate(d.getDate() + off); const dow = (d.getDay() + 6) % 7; if (!days.includes(dow)) continue; d.setHours(hour, minute, 0, 0); if (off === 0 && d <= now) continue; out.push(d); } return out; } function MStep05({ frequency, days, hour, minute, onSetFrequency, onToggleDay }) { const editions = computeNextEditions(days, hour, minute); const sendCount = days.length; const weekdays = days.every(d => d < 5) && sendCount === 5; const desc = sendCount === 0 ? "no sends yet" : sendCount === 7 ? "every day" : weekdays ? "weekends off" : `${sendCount} send${sendCount === 1 ? "" : "s"}/wk`; return (
Your rhythm Daily, or once Tap a segment; toggle days below.
{["daily", "weekly"].map(f => { const sel = frequency === f; return (
onSetFrequency(f)} style={{ flex: 1, background: sel ? M.ink : "transparent", color: sel ? M.paper : M.mid, padding: "12px 0", textAlign: "center", fontFamily: M.display, fontSize: 16, fontWeight: sel ? 700 : 400, cursor: "pointer", userSelect: "none", textTransform: "capitalize", }}> {f} {sel && }
); })}
{DAY_LABELS.map((label, i) => { const sel = days.includes(i); const today = new Date(); const dowToday = (today.getDay() + 6) % 7; const offset = (i - dowToday + 7) % 7; const date = new Date(today); date.setDate(today.getDate() + offset); return (
onToggleDay(i)} style={{ aspectRatio: "1/1.1", cursor: "pointer", userSelect: "none", background: sel ? M.ink : M.paper, color: sel ? M.paper : M.ink, border: `1px solid ${sel ? M.ink : M.rule}`, display: "flex", flexDirection: "column", alignItems: "center", justifyContent: "center", }}>
{label}
{date.getDate()}
{sel ? "SEND" : "—"}
); })}
▸ {sendCount} send{sendCount === 1 ? "" : "s"} · {desc}
▸ Next {editions.length} edition{editions.length === 1 ? "" : "s"}
{editions.length === 0 ? (
Select at least one day.
) : (
{editions.map(d => { const dow = (d.getDay() + 6) % 7; const label = `${DAY_NAMES_FULL[dow]} ${d.getDate()} ${MONTH_ABBR[d.getMonth()]}`; const time = `${pad2(d.getHours())}:${pad2(d.getMinutes())}`; return (
{label} {time}
); })}
)}
); } // ============================================================================= // 06 · Time — tap-to-select hour + minute // ============================================================================= function MStep06({ hour, minute, evening, onSetHour, onSetMinute, onToggleEvening }) { return (
When it arrives The hour it Tap to choose. One send, or add an evening digest.
{pad2(hour)}:{pad2(minute)}
local · {Intl.DateTimeFormat().resolvedOptions().timeZone}
{HOURS.map(h => { const sel = h === hour; return (
onSetHour(h)} style={{ padding: "8px 0", textAlign: "center", cursor: "pointer", userSelect: "none", fontFamily: M.display, fontSize: sel ? 30 : 20, fontWeight: sel ? 700 : 400, color: sel ? M.ink : M.mid, background: sel ? `rgba(184,117,26,0.1)` : "transparent", borderTop: sel ? `1px solid ${M.ink}` : "none", borderBottom: sel ? `1px solid ${M.ink}` : "none", letterSpacing: -1, }}> {pad2(h)}
); })}
:
{MINUTES.map(m => { const sel = m === minute; return (
onSetMinute(m)} style={{ padding: "8px 0", textAlign: "center", cursor: "pointer", userSelect: "none", fontFamily: M.display, fontSize: sel ? 30 : 20, fontWeight: sel ? 700 : 400, color: sel ? M.ink : M.mid, background: sel ? `rgba(184,117,26,0.1)` : "transparent", borderTop: sel ? `1px solid ${M.ink}` : "none", borderBottom: sel ? `1px solid ${M.ink}` : "none", letterSpacing: -1, }}> {pad2(m)}
); })}
▸ {evening ? "Evening on" : "Add evening"}
A closing note at 17:30.
{evening ? "✓" : "+"}
); } // ============================================================================= // 07 · Processing — auto-advance // ============================================================================= function MStep07({ onDone, sources }) { const [progress, setProgress] = useState(0); useEffect(() => { const start = Date.now(); const dur = 2800; const tick = setInterval(() => { const p = Math.min(1, (Date.now() - start) / dur); setProgress(p); if (p >= 1) { clearInterval(tick); setTimeout(() => onDone && onDone(), 200); } }, 50); return () => clearInterval(tick); }, []); const srcLabels = (sources || []).slice(0, 4).map(id => { const s = SOURCES_LIBRARY.find(x => x.id === id); return s?.name || id; }); return (
{[40,72,104,136,160].map((r,i) => ( ))} {Array.from({ length: 18 }).map((_, i) => { const a = (i / 18) * 2 * Math.PI; return ; })} {srcLabels[0] &&
{srcLabels[0]}
} {srcLabels[1] &&
{srcLabels[1]}
} {srcLabels[2] &&
{srcLabels[2]}
} {srcLabels[3] &&
{srcLabels[3]}
}
▸ PHASE {pad2(Math.min(4, Math.ceil(progress * 4)))} / 04 · SYNTHESIS
Listening for the hum.
[0705Z] ✓ {Math.max(1, Math.floor(progress * 6))} patterns detected
[0705Z] ▸ forming headline
); } // ============================================================================= // 08 · Confirmation // ============================================================================= function MStep08({ prefs, onFinish, sendStatus, sendError }) { const styleMeta = STYLES_LIBRARY.find(s => s.id === prefs.style) || STYLES_LIBRARY[0]; const topicsPreview = prefs.topics.length === 0 ? "—" : prefs.topics.slice(0, 2).join(" · ") + (prefs.topics.length > 2 ? " · …" : ""); const sourcesPreview = (() => { const names = prefs.sources.map(id => SOURCES_LIBRARY.find(s => s.id === id)?.name).filter(Boolean); if (names.length === 0) return "—"; return names.slice(0, 2).join(" · ") + (names.length > 2 ? " · …" : ""); })(); const dayDesc = (() => { const n = prefs.days.length; if (n === 0) return "no days"; if (n === 7) return "every day"; if (n === 5 && prefs.days.every(d => d < 5)) return "Mon–Fri"; return prefs.days.map(d => DAY_NAMES_FULL[d].slice(0,2)).join(" "); })(); const tz = Intl.DateTimeFormat().resolvedOptions().timeZone; return (
It's set You'll receive your
✦ Subscription · №{String(Math.floor(Math.random() * 900 + 100))} ✦
The Signal, for you
{[ ["Frequency", prefs.frequency === "daily" ? "Daily" : "Weekly", dayDesc], ["Delivery", `${pad2(prefs.hour)}:${pad2(prefs.minute)}`, tz.split("/").pop().replace("_"," ")], ["Style", styleMeta.name, styleMeta.italic ? styleMeta.italic.toLowerCase() : "—"], ["Topics", String(prefs.topics.length), topicsPreview], ["Sources", String(prefs.sources.length), sourcesPreview], ["Edition", "Ready", "on demand"], ].map(([l,v,s], i) => (
{l}
{v}
{s}
))}
Edit any of this, any time →
✦ {sendStatus === "sending" ? "Queueing your research…" : sendStatus === "error" ? "Did not queue" : "Run your signal"}
{sendStatus === "sending" ? <>Researching now… : <>Real research. PDF in your inbox. }
Going to {prefs.email || "your inbox"} · A4 Blueprint PDF · arrives in 10–20 min
{sendError && (
{sendError}
)}
{sendStatus === "sending" ? "Queueing…" : sendStatus === "error" ? "Try again" : "Generate my signal"}
); } Object.assign(window, { MStep01, MStep02, MStep03, MStep04, MStep05, MStep06, MStep07, MStep08, MScreen, MKicker, MHed, MDeck, MBtn, MS_TOPICS_LIBRARY: TOPICS_LIBRARY, MS_SOURCES_LIBRARY: SOURCES_LIBRARY, MS_DEFAULT_SOURCE_IDS: DEFAULT_SOURCE_IDS, MS_STYLES_LIBRARY: STYLES_LIBRARY, });