.vf-finding{padding:10px 14px;border:1px solid var(--border);border-left:3px solid var(--cyan);border-radius:10px;background:var(--bg2);margin-bottom:8px;font-size:12.5px;line-height:1.65} .vf-finding.critical{border-left-color:var(--red)}.vf-finding.high{border-left-color:var(--amber)}.vf-finding.medium{border-left-color:var(--cyan)}.vf-finding.low{border-left-color:var(--green)} .vf-sev{font-size:10px;text-transform:uppercase;letter-spacing:.1em;font-weight:700;padding:2px 6px;border-radius:4px;margin-right:6px} .vf-sev.critical{background:rgba(255,68,102,.15);color:var(--red)}.vf-sev.high{background:rgba(255,184,48,.15);color:var(--amber)}.vf-sev.medium{background:rgba(0,207,255,.12);color:var(--cyan)}.vf-sev.low{background:rgba(0,232,122,.12);color:var(--green)} .vf-scene-row{padding:8px 12px;border:1px solid var(--border);border-radius:8px;margin-bottom:6px;cursor:pointer;font-size:12px;transition:.1s all} .vf-scene-row:hover{border-color:var(--green-dim);background:rgba(0,232,122,.04)} .vf-test-result{padding:12px 14px;border:1px solid var(--border);border-radius:10px;margin-bottom:8px;font-size:12px;font-family:var(--font-mono)} .vf-test-result.pass{border-left:3px solid var(--green)}.vf-test-result.fail{border-left:3px solid var(--red)}.vf-test-result.warn{border-left:3px solid var(--amber)} .vf-shot{padding:8px 12px;background:var(--bg2);border:1px solid var(--border);border-radius:8px;margin-bottom:6px;font-size:12px} .vf-shot-num{font-family:var(--font-mono);color:var(--cyan);font-size:10px;margin-bottom:4px} .vf-packet{background:var(--panel2);border:1px solid var(--border2);border-radius:12px;padding:20px;font-size:13px;line-height:1.75} .vf-packet h3{color:var(--green);font-family:var(--font-mono);font-size:14px;margin-bottom:12px;border-bottom:1px solid var(--border);padding-bottom:8px} .vf-packet-block{margin-bottom:16px} .vf-packet-label{font-size:10px;text-transform:uppercase;letter-spacing:.16em;color:var(--cyan-dim);margin-bottom:4px} .vf-packet-val{color:var(--text);line-height:1.7} .vf-risk-high{color:var(--red);font-weight:700}.vf-risk-med{color:var(--amber);font-weight:700}.vf-risk-low{color:var(--green)} .vf-core-stat{display:flex;justify-content:space-between;padding:4px 0;border-bottom:1px solid var(--border);font-size:12.5px} .vf-core-stat span:last-child{color:var(--green);font-family:var(--font-mono);font-weight:700}
WHGA COMMAND BRAIN FINAL
KEYBOARD SHORTCUTS
Global searchCtrlK
This panel?
ScratchpadCtrl.
DashboardGD
EpisodesGE
ScriptsGS
TimelineGT
BudgetGB
GearGG
Call SheetGC
Export JSONCtrlE
Save versionsCtrlS
✎ Scratchpad
VERSION HISTORY
COMMAND BRAIN
Walter Has Gone Away · HCR Production OS FINAL
S1 Episodes
14
E-1 through E12
Scripts In-Tool
verbatim loaded
LC Videos
26
+2 E11 cutaways
Characters
14
7 WHGA · 7 LC
BOT Cutaways
20
fully scripted
All Episodes
← Select an episode to see details and open in editor.
EPISODE EDITOR
Per-episode workspace · script · breakdown · shots · call sheet · post
← Select an episode.
SCRIPTS
Full verbatim text · all loaded episodes
← Select a script to read, search, or edit.
SEASON ARC
S1 through S2 · placement map · LC insert schedule
SEASON 2 — A-PLOT
Survival → identity → audit → no clean exit
CALENDAR
Production schedule
LC VIDEOS
The Loneliness Company — all 26 videos + E11 cutaways · full scripts
LC CAST
Seven characters · all played by Walter · wardrobe differentiates
BETTER OFF TED CUTAWAYS
20 cutaways · 1.0–2.0 sec · same bright corporate font · footer: "The Loneliness Company a subsidiary of Shaazadon"
Keep each cutaway 1.0–2.0 seconds. Same bright corporate font. Same cheerful tone. Never explained. Footer on every cutaway: The Loneliness Company a subsidiary of Shaazadon
SHAAZADON
Locked lore · never fully explained · predates both concept and company
The Org Chart
↑ [UNLABELLED — line goes off top of chart]
SHAAZADON
Est. [REDACTED] BCE
THE LONELINESS COMPANY
A Subsidiary of Shaazadon
├───────────────────────┤
Persistent Affect Solutions
Managed Absence Group
The Bureau of Wanting
Conditional Love Infrastructure
[REDACTED]
[REDACTED]
[REDACTED]
The Forever Department
Bureau of Persistent Conditions
LC reports quarterly to the Bureau.
LC has never met anyone from the Bureau.
LC receives feedback via document. The documents are warm. Encouraging. The font is wrong.

LC's performance metrics include columns whose headers are in a language LC's software cannot identify.
LC fills them in anyway. Best guess. Nobody has complained yet.
Locked Canon

The Tagline

"We Were Here First." — seen once, briefly, on the wall TV in LC Video 07. No further context. Nobody acknowledges it.

The Logo

Never fully visible. Something geometric. Something that has been a symbol for longer than it has been a logo. If you have seen it in another context — a temple, a textbook, a coin — that is a coincidence Shaazadon's legal team would like to discuss with you.

The Founding Date

Est. [REDACTED] BCE — the redaction is official. It is not concealing a number. It is concealing a calendar system.

The TV

The TV in the LC office always shows Shaazadon content in the background. Nobody turns it off. Nobody acknowledges it. It has always been on.

The Line Going Up

On the org chart, a line goes UP from Shaazadon to something above it. The something above it is not labelled. The line simply continues off the top of the visible chart. This is the most important asset in the entire LC universe.

LC Tagline
"Your Feelings Are Our Infrastructure."
The Canon in One Sentence:
"We will always tell you exactly what we are doing.
You either won't understand — or you've been conditioned to not care.
That's not a failure of our communication.
That's the product."
WHGA CHARACTERS
Wall Walters + all cast · wardrobe / arc / disturbing quality
CANON + RULES
Locked. These do not change.
Locked Rules

The Failure Definition

Failure is not falling. Failure is not trying — and knowing it. Everything else is material.

The Voice Rule

Walter speaks from inside the experience, not above it. No wisdom-dispensing from altitude. If he's figured it out, he's still figuring it out.

LC Never Breaks Canon

The Loneliness Company does not explain itself. The audience makes the connections. Nobody in the LC universe acknowledges the joke.

Shadow Walter Rule

Shadow Walter does not give advice. Shadow Walter reflects. The distinction matters. Shadow Walter retired in Season 2 — replaced by The Interface.

The Evidence Rule

Evidence over time. Truth always. Applied to relationships, applied to self. The Prosecutor episode is this rule's full dramatization.

Season 2 Canon Shifts

Tone Shift

S1 is the self-examination. S2 is what you do with it. The same honesty, lower stakes, higher integration. The glitch aesthetic cools. Architecture replaces chaos.

Shadow Walter Retirement

Shadow Walter steps back. The Interface takes his function — but without emotion. Shadow Walter was the mirror with warmth. The Interface is the mirror with no distortion.

Sarah's Role

Sarah is not a device. She is a character with her own arc visible only through Walter's lens. The audience sees what Walter sees — which means the audience also sees what he misses.

The HUD Era

Season 2 begins the HUD era — the LC universe becomes more integrated into the main narrative. The wall TV is now a character. Shaazadon starts bleeding through.

Walter States (Self-Framework)

Shadow Walter

The old operating system. High function under pressure. Survival-built. Cannot rest. Confuses motion with progress. Knows all the right words. Doesn't believe them yet.

Detective Walter

Evidence-gathering mode. Pattern recognition. Cold. Useful when deployed correctly. Dangerous when deployed pre-emptively. The Prosecutor episode is Detective Walter unhinged.

Old Walter

Pre-transformation baseline. The guy who kept pulling the lever. Knows exactly what he is doing. Cannot stop. Is being interviewed, retired, and honoured — not shamed.

THE INTERFACE
Not a character. A system condition. Season 2 replacement for Shadow Walter.
Core Thesis
The Interface is not a character.
The Interface is a system condition.
It appears when ambiguity collapses.

Where WHGA dramatizes the emotional chaos of human self-awareness,
The Interface exists only after the emotional storm has passed.

Its function is not to motivate.
Its function is not to inspire.
Its function is not to teach.
Its function is to remove the final excuse.

The Interface speaks only when a pattern has already become visible.
It exists in the narrow moment where a person understands their behavior
but still wants permission to avoid changing it.

The Interface does not provide that permission.
It clarifies the cost of continuing the pattern.
Operating Rules (Locked)
  1. The Interface never gives advice.
  2. The Interface never tells Walter what to do.
  3. The Interface never comforts Walter.
  4. The Interface never argues emotionally.
  5. The Interface never motivates.
  6. The Interface never persuades.
  7. The Interface only clarifies observable patterns.
  8. The Interface only speaks when the pattern is already visible.
  9. The Interface does not repeat lessons.
  10. The Interface ends the moment behavioral clarity exists.

Disappearance Condition

The Interface is not a guide. The Interface is a mirror with no distortion.

It does not exist to create growth. It exists to remove the illusion that growth is happening when behavior has not changed.

If the subject changes behavior, the Interface becomes unnecessary. The Interface disappears when the system is no longer required.

CHECKLISTS
Pre-prod · LC Shoot Day · Green Screen Day · Asset Build · Post · Release
PHYSICAL ASSETS
6 builds required before camera rolls
VO SESSION
All LC voice-over · one session · different register from Walter main VO
MOTIFS + SCORES
Score library · musical cues · per-episode notes
Score Notes
SCRIPT INGEST
Local-first screenplay parser · department breakdowns · scheduling implications · Ollama hooks
Ingest Controls
Ready.
Deterministic parse first. AI suggestions stay non-canonical until reviewed. Designed for local Ollama at http://localhost:11434/api/generate.
Department Breakdown
Parse a script to generate props, wardrobe, HMU, cast, locations, and shoot-day estimates.
Scheduling Implications
Night shoots, company moves, cast-day counts, and heavy-scene density will appear here.
Scenes
Entity Review Queue
Assistant Output
V10 COGNITIVE OS
Pattern engine · false-progress destroyer · collapse scheduler · output gate
Operator Console
Locked Stack

V7 — Cognitive Kernel

Decompose text into active subsystem, hidden subsystem, behavioral loop, and avoidance vector. Feelings do not count unless they map to behavior.

V8 — Collapse Scheduler

Only speak when full awareness exists without behavioral change. Timing is part of the engine. Premature clarity is noise.

V9 — False Progress Destroyer

Reclassify growth language that does not alter behavior. Planning, optimization, and self-interpretation are treated as suspect until proven operational.

V10 — Operator OS

Run V7, then V9, then V8. If collapse condition is not met, silence is a valid output. No motivation. No comfort. No permission.

Minimal Irreversible Clarity
Run the engine. Output appears only after subsystem scan, illusion scan, and timing gate.
Pattern Stack
No pattern scanned yet.
Scene / Line Scan
No scan results yet.
V10.1 SCHEMA PASS
Canonical data model · persistent reports · operator approvals · write-back state
Canonical Model
Schema summary loads here.
Operator Queue
No pending review items.
Episode Registry
No canonical episodes yet.
Report History
No saved reports yet.
Schema JSON Preview
PATH TO V11
Natural progression from operator surface to closed-loop cognitive production system
THREE ALTERNATIVE TIMELINES TO V11
Unusual builds that reached V11 differently and produced non-obvious advantages
V11 — FUSED FINAL FORM
Best parts of all timelines merged into one operator-grade system
Definition: V11 is one persistent canon model, one shared cognitive engine, one operator override layer, one timeline simulator, and one production graph that writes back into every episode surface.
V11 Fusion Controls
Ready.
V11 Fusion merges: Timeline A scene graphing, Timeline B rewrite pressure telemetry, Timeline C canon doctrine enforcement.
Fusion Outcome
Run V11 Fusion to generate a merged operating profile.
Behavior Graph
Graph nodes will appear here.
Rewrite Pressure Matrix
Pressure map will appear here.
Doctrine Registry
Fusion doctrine registry will appear here.
Saved V11 Snapshots
No V11 snapshots yet.
V12 REAL — CORPUS AUDIT
Only working modules · real inputs · real transforms · real saved outputs
Corpus Summary
Reality Contract
No simulated future stack. A module only belongs in V12 Real if it has a real input, a real transform, a real saved output, and a visible verification path.
This build keeps: corpus audit, doctrine validation, cross-universe querying, deterministic parsing, saved report registry, and production linkage. It excludes: fake multimodal ingest, agent mesh orchestration, adaptive cognition sensing, and imaginary world simulation.
Ready.
V12 REAL — DOCTRINE VALIDATOR
Checks selected script text against locked WHGA / Interface doctrine
Run Validator
Verification path: validator reads the actual loaded script text for the chosen episode, applies deterministic checks, renders findings, and can persist the report into local state.
Ready.
Findings
Run the validator to see doctrine hits, risks, and evidence.
V12 REAL — CROSS-UNIVERSE QUERY
Search across WHGA scripts, LC videos, BOT cutaways, assets, VO, characters, and canon surfaces
Query Engine
Verification path: results are drawn from arrays and scripts already loaded in the app, with source labels and match excerpts.
Ready.
Results
Search the loaded corpus.
V12 REAL — STRUCTURED PARSER
Deterministic parse of actual script text into units, speakers, timestamps, cues, and persistence
Parser
Verification path: parser reads the real stored script for the episode, segments it deterministically, and writes the result to local state.
Ready.
Parsed Output
Parse an episode to inspect structured units.
V12 REAL — REPORT REGISTRY
Saved doctrine reports, query snapshots, and parser runs
Registry
No saved V12 Real artifacts yet.
Selected Artifact
Select a saved artifact from the registry.
V12 REAL — REMAINING WORK
What is left to shoot, record, source, and prep
V12 REAL — MEDIA BOARD
Episode stills · costume refs · props · locations · storyboard frames
Add Media Item
Drop image here or click to upload
Episode Media Library
V12 REAL — SHOT PLANNER
Canonical shot records for what is planned, shot, missing, or needs reshoot
Add Shot
Episode Shot Inventory
V12 REAL — COSTUME + PROPS
Looks, objects, readiness, missing pieces
Add Costume / Prop
Drop costume / prop image here or click to upload
Costume / Prop Registry
V12 REAL — VO TRACKER
Voiceover required, recorded, cleaned, edited, final
Add VO Item
Episode VO Needs
V12 REAL — GENERATED CHECKLIST
Auto-generated from unresolved shots, props, costumes, VO, and dependencies
BULK IMPORT
Paste scripts using === EPISODE_CODE === delimiters
Format: paste one or more scripts separated by === S01E10 === headers.
Valid codes: S01E10 · S01E11 · S02E01 · S2-SMDY · S2-IID · etc.
Existing scripts will be overwritten. New keys are added immediately.
GEAR TRACKER
HCR equipment · status · checkout log
Add / Edit Gear
Inventory
BUDGET + EXPENSES
Production spend · per-episode · running total
Log Expense
Filter
Click any row to edit.
CALL SHEET
Generate a printable shoot-day document from episode data
Shoot Details
Recent Call Sheets
SCENE BREAKDOWN PARSER
Paste any script text · auto-detect scenes, characters, locations, INT/EXT
Input
← Paste a script and hit Parse Scenes. Extracts scene headings, INT/EXT, time of day, characters, and first lines.
CHARACTER ARC TRACKER
Track arc beats per character across all episodes · click to cycle state
Present Peak moment Break/shift Absent
LC INSERT TIMELINE
Visual map of all Loneliness Company inserts across the full series
How LC Bleeds In

S1: Inserts are sharp, corporate, confident. S2: Language starts to mirror Walter's internal states. S3: Inserts begin degrading — coherence drops, repetition rises. By S3 finale: the system has lost its hold. The timeline shows this arc in one view.

CANON CONFLICT CHECKER
Submit any script text or idea · check against locked rules · get a conflict report
Submit Text to Check
Locked Canon Rules
Submit text to check against canon.
EPISODE BIBLE
Generate a formatted series bible from all loaded episode data
Set options above and click Generate Bible to compile all episode data into a formatted reference document.
STRIPBOARD
Drag scene strips to plan shoot days · color-coded by character/location
BREAKDOWN SHEETS
One sheet per scene · cast · props · costumes · VFX · sound · locations
Select Scene
← Select an episode to see its scenes.
DAY-OUT-OF-DAYS
Which character/costume appears on which shoot day · W=Work · H=Hold · T=Travel · F=Finish
LOCATION TRACKER
Scout cards · address · permit status · parking · access notes · linked episodes
Add Location
No locations yet. Add your first scout card.
CONTINUITY BOARD
Track wardrobe · props · set state · hair across episodes and scenes
POST PIPELINE
Track every episode through post · rough cut → color → audio mix → export → release
RELEASE SCHEDULER
Target dates · platforms · release status per episode
Schedule a Release
No releases scheduled yet.
MUSIC CUE SHEET
Log every music cue · timecode · mood · source · sync rights
Add Cue
No cues logged yet.
SIDES GENERATOR
Generate today's pages — trimmed script for just the scenes you're shooting
Build Sides
Select an episode first.
← Configure and generate.
READ-THROUGH MODE
Just the dialogue — no stage directions · full screen · performance prep
Select an episode and click Load.
SCRIPT FORMATTER
Proper screenplay format · INT/EXT headings · character cues · parentheticals · page count
Input
← Load a script and choose format style.
REVISION COLORS
Industry-standard draft colors · track which version you're on per episode
Draft Color System
Episode Revision Log
A/V TWO-COLUMN FORMAT
Video left · Audio/VO right · Documentary and essay film standard
AI SCRIPT ASSISTANT
Powered by OpenAI · uses your API key · canon-aware · punch up dialogue · generate scenes
Task
Output
AI output appears here…
Credit Tracker
Calls today: 0 · Est. cost: $0.000
ANALYTICS DASHBOARD
Production velocity · writing streaks · episode completion · budget burn
EXPORT SUITE
Professional PDF export · scripts · call sheets · breakdowns · cue sheets · series bible
PDF Exports
✎ Script PDF
▦ Call Sheet PDF
⊟ Breakdown Sheet PDF
♫ Music Cue Sheet PDF
▦ Scene Packet PDF
▦ Series Bible PDF
⊞ Day-Out-Of-Days PDF
Other Exports
PDF Preview
Generate a PDF to preview it here
Quick Stats
CLOUD SYNC
Save your brain to GitHub Gist · free · works on any device · never loses data
How It Works

Your data saves to a private GitHub Gist — a free cloud notepad. Every time you make a change, it syncs automatically. Open the app on your phone, laptop, anywhere — your data is always there.

You need a free GitHub account and a Personal Access Token with "gist" scope. Takes 2 minutes to set up.

Setup
github.com → Settings → Developer settings → Personal access tokens → Tokens (classic) → Generate → check "gist" scope
Sync Status
Not connected.
Auto-Sync Settings
DATA CORE
Relational engine · episodes → scenes → characters → props → locations
Core Stats
Loading…
Query Engine
Scenes
Characters Tracked
GENERATIVE ENGINE
Visual prompt · shot list · VO mock · production extractor · all logic-first
Visual Prompt Generator

Generates camera direction, lighting, composition, and mood from scene data.

AI Storyboard / Shot List

Generates a structured shot list from scene data. Persists to scene packet.

VO Mock Generator

Builds character voice previews from dialogue structure and character profile.

Production Extractor

Extracts props, locations, gear needs, and complexity flags from scene.

CONTINUITY ENGINE — FULL
Auto-generated · sequential scene comparison · prop persistence · character state · timeline logic
Select an episode and run the engine. All findings are auto-generated — no manual entry.
HARDENING LAB
Real test suite · parser validation · continuity torture · regression audit · AI schema validation
Run tests to verify all systems are operational.
SCENE-TO-SHOOT PACKET — FINAL
Fully data-driven · pulls from parser · core model · continuity · generative engine
Select episode + scene and generate packet.
SCRIPT EDITOR
Screenplay format · Tab to cycle elements · Character autocomplete · Live page count
SCENE HEADING
0 pages
0 words
0 scenes
Tab cycle element  ·  Enter next line  ·  Ctrl+S save  ·  Ctrl+Enter new scene  ·  @ character  ·  Esc dismiss autocomplete
SCENES
No episode loaded Ready
// ───────────────────────────────────────────── // V10 COGNITIVE OS // ───────────────────────────────────────────── const ENGINE_SUBSYSTEM_RULES = [ {name:'Parasite', color:'red', cues:['content','framework','healing','journey','brand','audience','algorithm','lesson','interesting','optimize','optimization','system','insight']}, {name:'Over-Giver', color:'green', cues:['help','fix','save','show up','overgive','do for','caretake','perform for','earn love','be enough for']}, {name:'Overthinker', color:'cyan', cues:['think','replay','analyze','pattern','figure out','decode','understand','logic','question','map']}, {name:'Distrustful One', color:'amber', cues:['safe','unsafe','evidence','proof','convict','verdict','micro shift','watch tone','watch timing','not fooled']}, {name:'Numb-Out', color:'purple', cues:['numb','nothing','flat','quiet it','go numb','switch off','don\'t feel']}, {name:'Performer', color:'amber', cues:['perform','seen','watching','interesting','impressive','stage','camera','version of me']}, {name:'Clapper', color:'green', cues:['aware','i know','i get it','already know','healed','growth','integration','moving on']}, {name:'Interface', color:'cyan', cues:['pattern','behavior','cost','clarity','evidence over time','observable','final excuse']} ]; function initV10Engine() { const sel = document.getElementById('engineEpisodeSelect'); if (!sel || sel.dataset.ready === '1') { renderEngineKpis(); restoreEngineStateToUI(); return; } sel.innerHTML = EPISODES.map(ep => ``).join(''); sel.dataset.ready = '1'; if (!state.engine) state.engine = {lastInput:'', lastOutput:null, mode:'v10', source:'manual', selectedEpisode:''}; ensureCanonicalState(); const defaultEp = state.engine.selectedEpisode || (EPISODES[0] && EPISODES[0].code) || ''; if (defaultEp) sel.value = defaultEp; restoreEngineStateToUI(); renderEngineKpis(); } function restoreEngineStateToUI() { const engineState = state.engine || {}; const source = document.getElementById('engineSource'); const mode = document.getElementById('engineMode'); const input = document.getElementById('engineInput'); const sel = document.getElementById('engineEpisodeSelect'); if (source && engineState.source) source.value = engineState.source; if (mode && engineState.mode) mode.value = engineState.mode; if (input && engineState.lastInput) input.value = engineState.lastInput; if (sel && engineState.selectedEpisode) sel.value = engineState.selectedEpisode; if (engineState.lastOutput) renderEngineOutput(engineState.lastOutput); } function engineGetSelectedEpisode() { return document.getElementById('engineEpisodeSelect')?.value || (EPISODES[0] && EPISODES[0].code) || ''; } function engineFindEpisode(code) { return EPISODES.find(ep => ep.code === code); } function engineGetTextBySource(source, code) { const ep = engineFindEpisode(code); const allScripts = getAllScripts(); const epData = code ? getEpData(code) : null; if (source === 'script') { const scriptKey = ep?.scriptKey; return scriptKey && allScripts[scriptKey] ? allScripts[scriptKey] : ''; } if (source === 'episode') { const editorEl = code ? document.getElementById(`scriptEdit-${code}`) : null; if (editorEl && editorEl.value.trim()) return editorEl.value; if (epData?.script?.trim()) return epData.script; const scriptKey = ep?.scriptKey; return scriptKey && allScripts[scriptKey] ? allScripts[scriptKey] : ''; } return document.getElementById('engineInput')?.value || ''; } function engineHandleSourceChange() { const source = document.getElementById('engineSource')?.value || 'manual'; if (!state.engine) state.engine = {}; state.engine.source = source; if (source !== 'manual') engineLoadSelectedSource(); save(); } function engineLoadSelectedSource() { const source = document.getElementById('engineSource')?.value || 'manual'; const code = engineGetSelectedEpisode(); const input = document.getElementById('engineInput'); if (!input) return; const text = engineGetTextBySource(source, code); input.value = text || ''; if (!state.engine) state.engine = {}; state.engine.lastInput = input.value; state.engine.source = source; state.engine.selectedEpisode = code; save(); } function engineClear() { const input = document.getElementById('engineInput'); if (input) input.value = ''; state.engine = Object.assign(state.engine || {}, {lastInput:'', lastOutput:null}); renderEngineOutput(null); save(); } function engineCopyClarity() { const clarity = state.engine?.lastOutput?.minimalClarity || ''; if (!clarity) return; navigator.clipboard.writeText(clarity).catch(()=>{}); } function normalizeEngineText(text) { return String(text || '').replace(/\r/g, '').trim(); } function engineSplitUnits(text) { const cleaned = normalizeEngineText(text); if (!cleaned) return []; const blocks = cleaned.split(/\n\s*\n+/).map(x => x.trim()).filter(Boolean); return blocks.map((block, idx) => ({id: idx + 1, text: block})); } function engineScoreCues(lower, cues) { return cues.reduce((acc, cue) => acc + (lower.includes(cue) ? 1 : 0), 0); } function detectActiveSubsystem(lower) { let best = {name:'Walter', color:'cyan', score:0}; ENGINE_SUBSYSTEM_RULES.forEach(rule => { const score = engineScoreCues(lower, rule.cues); if (score > best.score) best = {name:rule.name, color:rule.color, score}; }); return best.name; } function detectHiddenSubsystem(lower, active) { const contradiction = /(but|except|instead|meanwhile|although|still|yet|however)/.test(lower); if (!contradiction && active !== 'Interface') return 'None surfaced'; if (active === 'Over-Giver') return 'Distrustful One'; if (active === 'Overthinker') return 'Parasite'; if (active === 'Performer') return 'Numb-Out'; if (active === 'Clapper') return 'Parasite'; if (active === 'Interface') return 'None surfaced'; return 'Overthinker'; } function detectPattern(lower) { const patterns = [ ['Over-functioning to prevent abandonment', ['help','fix','save','earn','show up','do for','be enough']], ['Analysis loop replacing action', ['analyze','replay','figure out','decode','map','pattern','understand']], ['Self-improvement theatre', ['healing','growth','framework','journey','process','content','lesson','optimize']], ['Pre-emptive conviction', ['proof','evidence','unsafe','verdict','convict','watch tone','watch timing']], ['Emotional shutdown as control', ['numb','flat','nothing','off','quiet it','go numb']], ['Performance replacing contact', ['perform','interesting','audience','camera','version of me','impressive']], ['Behavioral clarity', ['observable','pattern','cost','evidence over time','behavior','clarity']] ]; let best = ['Diffuse pattern', 0]; patterns.forEach(([label, cues]) => { const score = engineScoreCues(lower, cues); if (score > best[1]) best = [label, score]; }); return best[0]; } function detectAvoidance(lower) { const map = [ ['Direct conversation', ['ask','tell','say','truth','question','clarity']], ['Behavior change', ['change','stop','start','boundary','leave','walk away','delete','sleep','eat']], ['Stillness', ['still','quiet','rest','peace','nothing']], ['Grief / loss', ['miss','grief','hurt','ache','lost','mourning']], ['Exposure to ordinary life', ['ordinary','boring','daylight','routine']] ]; let best = ['Unknown', 0]; map.forEach(([label, cues]) => { const score = engineScoreCues(lower, cues); if (score > best[1]) best = [label, score]; }); if (best[1] === 0 && /framework|process|map|journey/.test(lower)) return 'Behavior change'; return best[0]; } function detectFalseProgress(lower) { const flags = []; if (/(healing|growth|journey|process|integration|insight)/.test(lower) && !/(left|stopped|asked|slept|ate|deleted|walked away|changed)/.test(lower)) flags.push('Insight without behavior'); if (/(framework|map|system|architecture|theory|language)/.test(lower)) flags.push('Interpretation replacing action'); if (/(content|audience|algorithm|lesson|episode|brand)/.test(lower)) flags.push('Experience converted into output'); if (/(optimize|productive|discipline|efficiency)/.test(lower)) flags.push('Optimization used as camouflage'); return flags; } function detectAwarenessState(lower, falseFlags) { const awareness = engineScoreCues(lower, ['i know','i realize','the truth','what\'s actually happening','i see','the problem is','i understand','pattern']); const actions = engineScoreCues(lower, ['i left','i stop','i stopped','i asked','i slept','i ate','i changed','i walked away','i delete','i deleted','i chose','i choose']); if (awareness <= 1) return 'Pre-awareness'; if (awareness >= 2 && actions === 0) return 'Full awareness / no action'; if (awareness >= 2 && actions >= 1) return 'Awareness with behavior'; if (falseFlags.length) return 'Partial awareness'; return 'Partial awareness'; } function detectCost(pattern) { const costs = { 'Over-functioning to prevent abandonment':'Self-erasure becomes the admission price for connection.', 'Analysis loop replacing action':'Thinking consumes the time required for actual life to move.', 'Self-improvement theatre':'Identity becomes organized around not finishing.', 'Pre-emptive conviction':'Protection kills contact before evidence exists.', 'Emotional shutdown as control':'Relief is purchased by deadening the instrument.', 'Performance replacing contact':'Witnessing is replaced by stagecraft.', 'Behavioral clarity':'The excuse inventory is collapsing.' }; return costs[pattern] || 'Continuation deepens the loop.'; } function detectThreshold(pattern) { const map = { 'Over-functioning to prevent abandonment':'The point where help is given before being asked and resentment becomes invisible policy.', 'Analysis loop replacing action':'The point where the next insight delays the next move.', 'Self-improvement theatre':'The point where naming the pattern becomes the substitute for ending it.', 'Pre-emptive conviction':'The point where suspicion outruns evidence over time.', 'Emotional shutdown as control':'The point where peace only exists through reduction of feeling.', 'Performance replacing contact':'The point where being seen matters more than being changed.', 'Behavioral clarity':'The point where another explanation would be avoidance.' }; return map[pattern] || 'The point where repetition starts calling itself identity.'; } function buildMinimalClarity(summary, mode) { const line1 = `ACTIVE SYSTEM: ${summary.primarySubsystem}`; const line2 = `DOMINANT LOOP: ${summary.primaryPattern}`; const line3 = `CONTINUATION COST: ${summary.primaryCost}`; const line4 = `AVOIDANCE VECTOR: ${summary.primaryAvoidance}`; if (mode === 'v7') return [line1, line2, line4].join('\n'); if (mode === 'v8') return summary.collapseTriggered ? [line1, 'TRIGGER: FULL AWARENESS / NO ACTION', line3].join('\n') : 'No intervention. Pattern not yet at collapse condition.'; if (mode === 'v9') return [line2, `FALSE PROGRESS: ${summary.falseProgress.join(' | ') || 'None dominant'}`, line3].join('\n'); return summary.collapseTriggered ? [line1, line2, line3, line4, `POINT OF NO RETURN: ${summary.primaryThreshold}`].join('\n') : 'No collapse output. System not yet at full awareness without behavioral change.'; } function engineAnalyzeText(text, meta={}) { const units = engineSplitUnits(text); const analyses = units.map(unit => { const lower = unit.text.toLowerCase(); const activeSubsystem = detectActiveSubsystem(lower); const hiddenSubsystem = detectHiddenSubsystem(lower, activeSubsystem); const pattern = detectPattern(lower); const avoidance = detectAvoidance(lower); const falseProgress = detectFalseProgress(lower); const awarenessState = detectAwarenessState(lower, falseProgress); const cost = detectCost(pattern); const threshold = detectThreshold(pattern); const collapseTriggered = awarenessState === 'Full awareness / no action'; return { id: unit.id, excerpt: unit.text.slice(0, 420), activeSubsystem, hiddenSubsystem, pattern, avoidance, falseProgress, awarenessState, collapseTriggered, cost, threshold }; }); const primary = analyses[0] || { activeSubsystem:'None', hiddenSubsystem:'None', pattern:'No input', avoidance:'None', falseProgress:[], awarenessState:'No data', collapseTriggered:false, cost:'No data', threshold:'No data' }; const counts = {}; analyses.forEach(a => counts[a.pattern] = (counts[a.pattern] || 0) + 1); const primaryPattern = Object.entries(counts).sort((a,b) => b[1]-a[1])[0]?.[0] || primary.pattern; const primarySubsystem = analyses.map(a => a.activeSubsystem).sort((a,b) => analyses.filter(x=>x.activeSubsystem===b).length - analyses.filter(x=>x.activeSubsystem===a).length)[0] || primary.activeSubsystem; const falseProgress = [...new Set(analyses.flatMap(a => a.falseProgress))]; const collapseTriggered = analyses.some(a => a.collapseTriggered); const summary = { mode: meta.mode || 'v10', source: meta.source || 'manual', episodeCode: meta.episodeCode || '', unitCount: analyses.length, collapseCount: analyses.filter(a => a.collapseTriggered).length, illusionCount: analyses.filter(a => a.falseProgress.length).length, primarySubsystem, primaryPattern, primaryAvoidance: analyses.find(a => a.pattern === primaryPattern)?.avoidance || primary.avoidance, primaryCost: detectCost(primaryPattern), primaryThreshold: detectThreshold(primaryPattern), falseProgress, collapseTriggered, analyses }; summary.minimalClarity = buildMinimalClarity(summary, summary.mode); return summary; } function renderEngineKpis(data=null) { const el = document.getElementById('engineKpis'); if (!el) return; const kpis = data ? [ ['Units Scanned', data.unitCount, data.episodeCode || data.source], ['Primary System', data.primarySubsystem, data.primaryPattern], ['Illusion Flags', data.illusionCount, data.falseProgress[0] || 'None dominant'], ['Collapse Triggers', data.collapseCount, data.collapseTriggered ? 'Active' : 'Not active'] ] : [ ['Units Scanned', 0, 'No run'], ['Primary System', '—', 'Waiting'], ['Illusion Flags', 0, 'Waiting'], ['Collapse Triggers', 0, 'Waiting'] ]; el.innerHTML = kpis.map(([label, value, sub]) => `
${label}
${escHtml(String(value))}
${escHtml(String(sub))}
`).join(''); } function renderEngineOutput(data) { renderEngineKpis(data || null); const clarity = document.getElementById('engineClarity'); const stack = document.getElementById('enginePatternStack'); const lines = document.getElementById('engineLineResults'); if (!data) { if (clarity) clarity.innerHTML = '
Run the engine. Output appears only after subsystem scan, illusion scan, and timing gate.
'; if (stack) stack.innerHTML = 'No pattern scanned yet.'; if (lines) lines.innerHTML = 'No scan results yet.'; return; } if (clarity) clarity.innerHTML = `
${escHtml(data.minimalClarity)}
`; if (stack) { stack.innerHTML = `
Primary Read
Primary System${escHtml(data.primarySubsystem)}
Dominant Loop${escHtml(data.primaryPattern)}
Avoidance Vector${escHtml(data.primaryAvoidance)}
Point of No Return${escHtml(data.primaryThreshold)}
${(data.falseProgress.length ? data.falseProgress : ['No major illusion flag']).map(flag => `${escHtml(flag)}`).join('')} ${data.collapseTriggered ? 'Collapse active' : 'Collapse not active'}
`; } if (lines) { lines.innerHTML = data.analyses.map(a => `
UNIT ${a.id}
${escHtml(a.activeSubsystem)} ${escHtml(a.awarenessState)} ${a.collapseTriggered ? 'Collapse trigger' : ''}
${escHtml(a.excerpt)}
Hidden System${escHtml(a.hiddenSubsystem)}
Behavioral Pattern${escHtml(a.pattern)}
Avoidance Vector${escHtml(a.avoidance)}
Continuation Cost${escHtml(a.cost)}
${(a.falseProgress.length ? a.falseProgress : ['No false-progress flag']).map(flag => `${escHtml(flag)}`).join('')}
`).join(''); } } function engineSaveReport() { const data = state.engine?.lastOutput; if (!data) return; persistEngineReport(data, {episodeCode: data.episodeCode || engineGetSelectedEpisode() || 'GLOBAL'}); } function enginePromotePrimary() { const data = state.engine?.lastOutput; if (!data) return; const report = persistEngineReport(data, {episodeCode: data.episodeCode || engineGetSelectedEpisode() || 'GLOBAL'}); const item = state.canonical.reviewQueue.find(x => x.reportId === report.reportId && x.status === 'pending'); if (item) approveQueueItem(item.id, 'approved'); } function runV10Engine() { initV10Engine(); const mode = document.getElementById('engineMode')?.value || 'v10'; const source = document.getElementById('engineSource')?.value || 'manual'; const episodeCode = engineGetSelectedEpisode(); const input = document.getElementById('engineInput'); const text = normalizeEngineText(input?.value || ''); if (!text) { renderEngineOutput(null); return; } const data = engineAnalyzeText(text, {mode, source, episodeCode}); state.engine = {lastInput:text, lastOutput:data, mode, source, selectedEpisode:episodeCode}; renderEngineOutput(data); save(); renderSchemaPass(); } function runV10FromScript(key, title) { const allScripts = getAllScripts(); const text = allScripts[key] || ''; const ep = EPISODES.find(e => e.scriptKey === key); showSection('v10engine'); setTimeout(() => { initV10Engine(); const source = document.getElementById('engineSource'); const mode = document.getElementById('engineMode'); const sel = document.getElementById('engineEpisodeSelect'); const input = document.getElementById('engineInput'); if (source) source.value = 'script'; if (mode) mode.value = 'v10'; if (sel && ep?.code) sel.value = ep.code; if (input) input.value = text; runV10Engine(); }, 50); } function runV10FromEpisode(code) { showSection('v10engine'); setTimeout(() => { initV10Engine(); const source = document.getElementById('engineSource'); const mode = document.getElementById('engineMode'); const sel = document.getElementById('engineEpisodeSelect'); if (source) source.value = 'episode'; if (mode) mode.value = 'v10'; if (sel) sel.value = code; engineLoadSelectedSource(); runV10Engine(); }, 50); } function initV11Roadmap() { const pane = document.getElementById('v11RoadmapPane'); if (!pane) return; pane.innerHTML = V11_ROADMAP.map(step => `
${step.code}
${step.title}
${step.purpose}
${step.outputs.map(o => `${o}`).join('')}
`).join(''); } function initV11Timelines() { const pane = document.getElementById('v11TimelinesPane'); if (!pane) return; pane.innerHTML = V11_TIMELINES.map(t => `
Timeline ${t.id}

${t.name}

${t.tag}
Route: ${t.route}
Unusual move: ${t.unusual}
Beyond-helpful outcome: ${t.outcome}
${t.kept.map(k => `${k}`).join('')}
`).join(''); } function v11EnsureState() { if (!state.v11) state.v11 = { snapshots: [], fusedByEpisode: {} }; } function getEpisodeLatestReport(epCode) { const schema = state.v101Schema?.episodes?.[epCode]; if (!schema || !schema.savedReports || !schema.savedReports.length) return null; return schema.savedReports[schema.savedReports.length - 1]; } function buildV11FusionForEpisode(epCode) { v11EnsureState(); const canonical = getCanonicalEpisode(epCode) || {}; const report = getEpisodeLatestReport(epCode) || {}; const scan = report.scan || canonical.latestLineScan || []; const units = Array.isArray(scan) ? scan : []; const subsystemCounts = {}; const patternCounts = {}; const avoidanceCounts = {}; units.forEach(u => { const s = u.activeSubsystem || 'Unclassified'; const p = u.behavioralPattern || 'Unclassified'; const a = u.avoidanceVector || 'Unclassified'; subsystemCounts[s] = (subsystemCounts[s] || 0) + 1; patternCounts[p] = (patternCounts[p] || 0) + 1; avoidanceCounts[a] = (avoidanceCounts[a] || 0) + 1; }); const topSubsystems = Object.entries(subsystemCounts).sort((a,b)=>b[1]-a[1]).slice(0,5); const topPatterns = Object.entries(patternCounts).sort((a,b)=>b[1]-a[1]).slice(0,5); const topAvoidance = Object.entries(avoidanceCounts).sort((a,b)=>b[1]-a[1]).slice(0,5); const falseFlags = canonical.falseProgressFlags || report.falseProgressFlags || []; const locks = canonical.canonLocks || []; const clarity = canonical.minimalClarity || report.minimalClarity || 'No primary clarity stored.'; const pressureRows = [ ['Ambiguity Density', units.filter(u => (u.hiddenSubsystem||'').includes('Unknown') || (u.activeSubsystem||'').includes('Unknown')).length, 'Higher means unclear structural ownership.'], ['Loop Recurrence', topPatterns.reduce((n,[,c])=> n + (c > 1 ? c : 0), 0), 'Repeated patterns across units.'], ['False Progress Load', falseFlags.length, 'Signals of growth theater or delay architecture.'], ['Canon Tension', locks.length, 'Locked doctrines currently attached to the episode.'], ['Collapse Pressure', report.collapseTriggered ? 1 : 0, report.collapseTriggered ? 'Collapse condition currently met.' : 'Collapse condition not currently met.'] ]; const doctrine = [ {name:'Interface Rule', state:'Locked', note:'Pattern clarification only. No comfort, no advice, no persuasion.'}, {name:'Canonical Episode State', state: locks.length ? 'Active' : 'Open', note: locks.length ? `${locks.length} lock(s) attached to episode.` : 'No explicit canon lock attached yet.'}, {name:'Operator Override', state:'Active', note:'Saved reports require human approval for promotion into canon.'}, {name:'Graph Discipline', state:'Fused', note:'Scene pressure and subsystem recurrence now treated as structural data, not commentary.'} ]; return { epCode, generatedAt: new Date().toISOString(), clarity, topSubsystems, topPatterns, topAvoidance, falseFlags, locks, pressureRows, doctrine, reportCount: (state.v101Schema?.episodes?.[epCode]?.savedReports || []).length }; } function renderV11FusionResult(fusion) { const outcome = document.getElementById('v11OutcomePane'); const graph = document.getElementById('v11GraphPane'); const matrix = document.getElementById('v11MatrixPane'); const doctrine = document.getElementById('v11DoctrinePane'); if (outcome) outcome.innerHTML = `
Fused V11 Operating Profile
Minimal clarity: ${escapeHtml(fusion.clarity)}
${fusion.falseFlags.slice(0,6).map(f => `${escapeHtml(f)}`).join('') || 'No false-progress flags stored'}
Saved report count: ${fusion.reportCount} · Canon locks: ${fusion.locks.length} · Top subsystem: ${fusion.topSubsystems[0] ? escapeHtml(fusion.topSubsystems[0][0]) : 'None'}
`; if (graph) { const nodes = fusion.topSubsystems.map(([name,count],i) => `
${escapeHtml(name)}
Units: ${count}
${fusion.topPatterns[i] ? `
drives pattern → ${escapeHtml(fusion.topPatterns[i][0])} (${fusion.topPatterns[i][1]})
` : ''}`).join('') || '
No graph nodes available.
'; graph.innerHTML = nodes; } if (matrix) matrix.innerHTML = `${fusion.pressureRows.map(r => ``).join('')}
MetricValueMeaning
${escapeHtml(String(r[0]))}${escapeHtml(String(r[1]))}${escapeHtml(String(r[2]))}
`; if (doctrine) doctrine.innerHTML = fusion.doctrine.map(d => `

${escapeHtml(d.name)} · ${escapeHtml(d.state)}

${escapeHtml(d.note)}

`).join(''); } function renderV11Snapshots() { v11EnsureState(); const pane = document.getElementById('v11SnapshotsPane'); if (!pane) return; const snaps = state.v11.snapshots || []; if (!snaps.length) { pane.innerHTML = '
No V11 snapshots yet.
'; return; } pane.innerHTML = snaps.slice().reverse().map((s, idx) => `
${escapeHtml(s.epCode)} · ${escapeHtml(new Date(s.generatedAt).toLocaleString())}
Clarity: ${escapeHtml(s.clarity)}
Subsystems: ${s.topSubsystems.map(x => escapeHtml(x[0]) + ' (' + x[1] + ')').join(' · ')}
`).join(''); } function initV11Fusion() { v11EnsureState(); const sel = document.getElementById('v11EpisodeSelect'); if (sel && !sel.dataset.ready) { sel.innerHTML = EPISODES.map(ep => ``).join(''); sel.dataset.ready = '1'; } const current = sel ? (sel.value || (EPISODES[0] && EPISODES[0].code)) : (EPISODES[0] && EPISODES[0].code); if (current && state.v11.fusedByEpisode[current]) renderV11FusionResult(state.v11.fusedByEpisode[current]); renderV11Snapshots(); } function runV11Fusion() { v11EnsureState(); const sel = document.getElementById('v11EpisodeSelect'); const epCode = sel ? sel.value : null; if (!epCode) return; const fusion = buildV11FusionForEpisode(epCode); state.v11.fusedByEpisode[epCode] = fusion; save(); renderV11FusionResult(fusion); renderV11Snapshots(); const status = document.getElementById('v11Status'); if (status) status.textContent = `V11 fusion built for ${epCode}.`; } function saveV11Snapshot() { v11EnsureState(); const sel = document.getElementById('v11EpisodeSelect'); const epCode = sel ? sel.value : null; if (!epCode) return; const fusion = state.v11.fusedByEpisode[epCode] || buildV11FusionForEpisode(epCode); state.v11.fusedByEpisode[epCode] = fusion; state.v11.snapshots.push(JSON.parse(JSON.stringify(fusion))); save(); renderV11Snapshots(); const status = document.getElementById('v11Status'); if (status) status.textContent = `Saved V11 snapshot for ${epCode}.`; } // ───────────────────────────────────────────── // V12 / 2029 // ───────────────────────────────────────────── const V12_ITERATIONS = [ {code:'V11.1', focus:'State normalization', outcome:'Unify V10/V11 report objects, graph nodes, and production entities into one canonical event schema.'}, {code:'V11.2', focus:'Production write-back', outcome:'Make every simulation write back to dashboard, timeline, calendar risk, and episode editor.'}, {code:'V11.3', focus:'Doctrine enforcement', outcome:'Lock canon, Interface rules, and LC universe invariants behind operator-reviewable validators.'}, {code:'V11.4', focus:'Multimodal inbox', outcome:'Add voice-note, edit-note, storyboard, shot-photo, and PDF ingestion endpoints into the same state model.'}, {code:'V11.5', focus:'Agent mesh', outcome:'Split work into local specialist agents: script parser, canon auditor, schedule forecaster, continuity judge, and risk monitor.'}, {code:'V11.6', focus:'World simulation', outcome:'Model season arcs as state transitions instead of note collections; test rewrite consequences before committing them.'}, {code:'V11.7', focus:'Telemetry loop', outcome:'Track what predictions came true, which false-progress flags survived rewrites, and where production debt keeps recurring.'}, {code:'V11.8', focus:'Adaptive operator surface', outcome:'Reconfigure what the UI emphasizes based on the current stage: writing, prep, shoot, post, release, or doctrine review.'}, {code:'V11.9', focus:'Collaboration / permissions', outcome:'Enable multi-operator review without corrupting canon: author, reviewer, producer, continuity, observer.'}, {code:'V12', focus:'2029 command brain', outcome:'One governed, multimodal, predictive creative OS with simulations, alerts, lineage, and season-scale memory.'} ]; const V12_FUTURE_STACK = [ { name:'Multimodal Ingest Layer', badge:'Build now', summary:'Text-only input is the current bottleneck. V12 ingests transcripts, voice memos, edit notes, still frames, shotlists, calendars, and paperwork into the same schema.', modules:['voice-note parser','edit-timeline notes','storyboard/shot photo registry','document parser inbox','event-to-episode linker'] }, { name:'Agent Mesh', badge:'Local-first', summary:'Instead of one general engine, V12 routes work to task-specific local agents with reviewable outputs.', modules:['canon auditor','continuity judge','schedule/risk forecaster','rewrite propagator','tone drift detector'] }, { name:'World Simulation', badge:'2029 behavior', summary:'Episodes and season arcs are treated as dynamic systems. Rewrites can be simulated before they become canon.', modules:['season state transitions','bridge-scene prediction','character pressure graph','if-changed-then-what simulator','ending stability test'] }, { name:'Adaptive Operator Surface', badge:'Human-first', summary:'The UI stops being static and foregrounds the panels that matter most for the current decision context.', modules:['writing mode','prep mode','shoot mode','post mode','release mode','doctrine review mode'] }, { name:'Governance / Doctrine', badge:'Non-negotiable', summary:'The system protects canon, attribution, review history, and operator intent against drift.', modules:['canon locks','approval lineage','reversible proposals','audit log','rule validators'] }, { name:'Telemetry + Learning', badge:'Closes loop', summary:'Predictions are measured against real outcomes so the system gets sharper instead of noisier.', modules:['prediction ledger','rewrite debt tracking','false-progress recurrence','production variance log','season drift score'] } ]; function ensureV12State() { if (!state.v12) state.v12 = {}; if (!state.v12.snapshots) state.v12.snapshots = []; if (!state.v12.simByEpisode) state.v12.simByEpisode = {}; if (!state.v12.lastMode) state.v12.lastMode = 'season-forecast'; } function getScriptTextForEpisode(code) { const ep = EPISODES.find(e => e.code === code); const all = getAllScripts(); if (ep && ep.scriptKey && all[ep.scriptKey]) return all[ep.scriptKey]; if (all[code]) return all[code]; const d = getEpData(code); return d.script || ''; } function buildCorpusAudit() { const allScripts = getAllScripts(); const loadedWhgaScripts = EPISODES.filter(e => e.code.startsWith('S0') && !!getScriptTextForEpisode(e.code)).length; const loadedLCScripts = LC_VIDEOS.filter(v => !!v.script).length; const importedCount = Object.keys(state.scripts || {}).length; const canonLocks = Object.values(state.canonical?.episodes || {}).reduce((n, ep) => n + ((ep.canonLocks || []).length), 0); const reports = Object.values(state.canonical?.reports || {}).reduce((n, arr) => n + (Array.isArray(arr) ? arr.length : 0), 0); return { whgaEpisodeCount: EPISODES.filter(e => e.code.startsWith('S0')).length, loadedWhgaScripts, lcVideoCount: LC_VIDEOS.length, loadedLCScripts, botCutaways: BOT_CUTAWAYS.length, importedCount, canonLocks, reports }; } function inferSeasonForecasts() { const s1Titles = EPISODES.filter(e => e.code.startsWith('S01')).map(e => e.title); const s2Titles = EPISODES.filter(e => e.code.startsWith('S02')).map(e => e.title); const s1 = { direction:'Season 1 is an internal diagnostic spiral that gradually stops treating pain as mystery and starts treating it as pattern.', movement:['introductory rupture','self-eulogy / subsystem naming','loop recognition','shadow confrontation','parasite exposure','productivity as avoidance','behavioral clarity'], endpoint:'By the finale, the system knows the problem is not lack of insight. The problem is using insight as a substitute for action.' }; const s2 = { direction:'Season 2 is architecture replacing chaos: less mythologizing, more pattern enforcement, more Interface logic, more externalized system design.', movement:['parallel realities and branch logic','same self / different blueprint','cage vs structure','grief for unlived paths','identity sunk-cost','performed recovery audit','survival-performance root cause','Shadow Walter retirement'], endpoint:'Season 2 points toward a world where excuses get thinner and system conditions become visible.' }; const s3 = { direction:'Season 3, based on what is unresolved, should be consequence season: the external world begins answering back to the internal audit.', movement:['what remains after the audit','how intimacy, work, and art behave after pattern clarity','Shaazadon / LC bleed-through becomes structural, not easter egg','the cost of choosing stability over self-mythology','proof of change through sustained behavior'], endpoint:'Season 3 should not be more self-analysis. It should be behavioral verification under pressure.' }; return {s1,s2,s3,s1Titles,s2Titles}; } function buildV12Simulation(epCode, mode) { ensureV12State(); const audit = buildCorpusAudit(); const forecasts = inferSeasonForecasts(); const ep = EPISODES.find(e => e.code === epCode) || {code:epCode, title:epCode, season:'Unknown'}; const txt = getScriptTextForEpisode(epCode); const lower = (txt || '').toLowerCase(); const pressure = [ lower.includes('what if') ? 'counterfactual loop pressure' : null, lower.includes('stay') || lower.includes('stayed') ? 'attachment drag' : null, lower.includes('walk') ? 'movement-as-processing' : null, lower.includes('shadow') ? 'mirror-system activation' : null, lower.includes('truth') ? 'reality demand' : null, lower.includes('parasite') ? 'false-growth exposure' : null, ].filter(Boolean); const risk = []; if (!txt) risk.push('No script text is loaded for this episode key; forecast uses metadata and canonical state only.'); if ((state.canonical?.reports?.[epCode] || []).length < 1) risk.push('No saved cognitive reports yet for this node.'); if (ep.status === 'rewrite') risk.push('Episode is already flagged rewrite; propagation risk is active.'); if (ep.code.startsWith('S02')) risk.push('Season 2 node likely affects Interface doctrine and season-wide architecture.'); const result = { episode: ep, mode, audit, forecasts, pressure, risk, recommendation: mode === 'season-forecast' ? 'Use this node to test whether the episode advances behavior or merely restates self-awareness.' : mode === 'rewrite-propagation' ? 'Any rewrite here should be checked against bridge scenes, doctrine locks, and season-end claims.' : mode === 'production-risk' ? 'Cross-check script complexity against assets, green-screen, location, and VO concentration before locking shoot order.' : 'Audit confirms the corpus is split across WHGA, LC, canon, and production surfaces but can now be simulated as one system.' }; state.v12.simByEpisode[epCode] = result; state.v12.lastMode = mode; return result; } function renderV12Roadmap() { const pane = document.getElementById('v12RoadmapPane'); if (!pane) return; pane.innerHTML = `
Natural Progression
${V12_ITERATIONS.map(step => `
${step.code}
${step.focus}
${step.outcome}
`).join('')}
Why This Reaches 2029
V12 stops being “an app that stores writing” and becomes “a governed operating environment that predicts consequences across writing, production, continuity, and doctrine.” That is the actual jump: - from notes to state - from analysis to simulation - from one engine to task-specific agents - from static UI to adaptive surfaces - from cleverness to audited lineage
`; } function renderV12Futures() { const pane = document.getElementById('v12FuturesPane'); if (!pane) return; pane.innerHTML = `
${V12_FUTURE_STACK.map(item => `
${item.badge}

${item.name}

${item.summary}

`).join('')}
`; } function renderV12Final() { const pane = document.getElementById('v12FinalPane'); if (!pane) return; const audit = buildCorpusAudit(); pane.innerHTML = `
V12 Core Stack

Inputs

Scripts, episode drafts, LC video corpus, BOT cutaways, voice notes, shot artifacts, calendars, approvals, and operator comments.

Inference

Canon auditors, rewrite propagators, continuity judges, season simulators, risk forecasters, and doctrine validators.

Outputs

Scene tags, season forecasts, production alerts, bridge-scene requirements, doctrine conflicts, and release-readiness state.

Current Corpus Ready for Lift
WHGA episodes tracked: ${audit.whgaEpisodeCount} WHGA scripts currently loaded: ${audit.loadedWhgaScripts} LC videos in corpus: ${audit.lcVideoCount} LC scripts available in corpus: ${audit.loadedLCScripts} BOT cutaways: ${audit.botCutaways} Imported script blocks: ${audit.importedCount} Canon locks currently stored: ${audit.canonLocks} Saved cognitive reports: ${audit.reports}
Season 1 / 2 / 3 Direction
What Is Still Missing
1. True multimodal ingestion endpoints 2. Reviewable agent routing instead of one engine path 3. Production-side variance tracking after actual shoots 4. Permissioned collaboration and audit signatures 5. Semantic local-model passes replacing more heuristics 6. A live simulation ledger that can tell you what changed, why, and what it affects next
`; const seasonPane = document.getElementById('v12SeasonForecastPane'); const f = inferSeasonForecasts(); if (seasonPane) { seasonPane.innerHTML = `

Season 1

${f.s1.direction}

Endpoint: ${f.s1.endpoint}

Season 2

${f.s2.direction}

Endpoint: ${f.s2.endpoint}

Season 3

${f.s3.direction}

Endpoint: ${f.s3.endpoint}

`; } } function renderV12Snapshots() { ensureV12State(); const pane = document.getElementById('v12SnapshotsPane'); if (!pane) return; const snaps = state.v12.snapshots || []; if (!snaps.length) { pane.innerHTML = '
No V12 snapshots yet.
'; return; } pane.innerHTML = snaps.slice().reverse().map(s => `
${s.episodeCode} · ${s.mode}
${new Date(s.savedAt).toLocaleString()} · pressure: ${(s.pressure || []).join(', ') || 'none detected'}
`).join(''); } function renderV12EpisodeSelect() { const sel = document.getElementById('v12EpisodeSelect'); if (!sel) return; sel.innerHTML = EPISODES.map(e => ``).join(''); } function runV12Simulation() { ensureV12State(); const epCode = document.getElementById('v12EpisodeSelect')?.value || EPISODES[0].code; const mode = document.getElementById('v12ModeSelect')?.value || 'season-forecast'; const sim = buildV12Simulation(epCode, mode); const pane = document.getElementById('v12SimulationPane'); if (pane) { pane.innerHTML = `
MODE: ${sim.mode} EPISODE: ${sim.episode.code} — ${sim.episode.title} SEASON: ${sim.episode.season} PRESSURE VECTORS: - ${(sim.pressure || []).join('\n- ') || 'none detected from current text pass'} RISKS: - ${(sim.risk || []).join('\n- ') || 'no immediate risks flagged'} CURRENT RECOMMENDATION: ${sim.recommendation} SEASON FORECAST SNAPSHOT: S1 → ${sim.forecasts.s1.direction} S2 → ${sim.forecasts.s2.direction} S3 → ${sim.forecasts.s3.direction}
`; } const status = document.getElementById('v12Status'); if (status) status.textContent = `Simulation built for ${epCode} in ${mode} mode.`; save(); } function saveV12Snapshot() { ensureV12State(); const epCode = document.getElementById('v12EpisodeSelect')?.value || EPISODES[0].code; const sim = state.v12.simByEpisode[epCode] || buildV12Simulation(epCode, state.v12.lastMode || 'season-forecast'); state.v12.snapshots.push({ episodeCode: epCode, mode: sim.mode, pressure: sim.pressure || [], savedAt: new Date().toISOString() }); save(); renderV12Snapshots(); const status = document.getElementById('v12Status'); if (status) status.textContent = `Saved V12 snapshot for ${epCode}.`; } function initV12() { ensureV12State(); renderV12Roadmap(); renderV12EpisodeSelect(); renderV12Futures(); renderV12Final(); renderV12Snapshots(); } function ensureV12RealState() { if (!state.v12real) state.v12real = {}; if (!state.v12real.saved) state.v12real.saved = []; if (!state.v12real.lastDoctrine) state.v12real.lastDoctrine = null; if (!state.v12real.lastQuery) state.v12real.lastQuery = null; if (!state.v12real.lastParse) state.v12real.lastParse = null; if (!state.v12real.lastLinks) state.v12real.lastLinks = null; } function v12Escape(s) { return String(s == null ? '' : s).replace(/[&<>"]/g, c => ({'&':'&','<':'<','>':'>','"':'"'}[c])); } function v12GetEpisodeScript(epCode) { const ep = EPISODES.find(e => e.code === epCode); if (!ep) return ''; const fromEpData = getEpData(epCode)?.script || ''; const key = ep.scriptKey || ep.code; const fromImported = state.scripts?.[key] || state.scripts?.[epCode] || ''; const fromBase = SCRIPTS?.[key] || SCRIPTS?.[epCode] || ''; return [fromEpData, fromImported, fromBase].find(x => x && String(x).trim()) || ''; } function v12CorpusAudit() { const whgaEpisodes = EPISODES.filter(e => String(e.season||'').startsWith('S1') || String(e.season||'').startsWith('S2') || e.code.startsWith('S01') || e.code.startsWith('S02')).length; const loadedScripts = EPISODES.filter(e => v12GetEpisodeScript(e.code).trim()).length; const missingScripts = EPISODES.length - loadedScripts; const parseCount = Object.keys(state.parsedScripts || {}).length; const checkCount = Object.values(CHECKLISTS || {}).reduce((a,b)=>a + (Array.isArray(b)?b.length:0), 0); return { episodeCount: EPISODES.length, whgaEpisodeCount: whgaEpisodes, loadedScripts, missingScripts, lcVideoCount: LC_VIDEOS.length, botCutawayCount: BOT_CUTAWAYS.length, charCount: CHARACTERS.length, assetCount: ASSETS.length, voCount: VO_TABLE.length, checklistCount: checkCount, parseCount, importedScriptKeys: Object.keys(state.scripts || {}).length, canonEpisodeStates: Object.keys(state.canonical?.episodes || {}).length, savedReports: (state.v12real?.saved || []).length }; } function renderV12RealAudit() { const pane = document.getElementById('v12RealAuditPane'); if (!pane) return; const a = v12CorpusAudit(); pane.innerHTML = `
Episodes Indexed
${a.episodeCount}
Loaded Scripts
${a.loadedScripts}
Missing Scripts
${a.missingScripts}
LC Videos
${a.lcVideoCount}
BOT Cutaways
${a.botCutawayCount}
Characters
${a.charCount}
Assets
${a.assetCount}
VO Rows
${a.voCount}
Checklist Items
${a.checklistCount}
Parsed Scripts Saved
${a.parseCount}
Imported Script Keys
${a.importedScriptKeys}
Saved V12 Real Artifacts
${a.savedReports}
WHGA episodes indexed: ${a.whgaEpisodeCount}. Canon episode states stored: ${a.canonEpisodeStates}. Loaded script count is computed from actual episode text available through base scripts, imported scripts, or episode editor state.
`; } function populateV12RealEpisodeSelects() { ['v12DoctrineEpisode','v12ParserEpisode','v12LinkEpisode'].forEach(id => { const sel = document.getElementById(id); if (!sel) return; const current = sel.value; sel.innerHTML = EPISODES.map(e => ``).join(''); if (current) sel.value = current; }); } function v12DoctrineRules() { return [ {id:'voice_rule', label:'No overt advice / prescription', type:'risk', test:(t)=>/\b(should|must|need to|here's how|step one|step 1|the answer is|do this)\b/i.test(t), rationale:'WHGA / Interface logic rejects advice-forward writing.'}, {id:'comfort_rule', label:'No reassurance language', type:'risk', test:(t)=>/\b(you did nothing wrong|you are enough|it's okay|you're okay|everything will be okay)\b/i.test(t), rationale:'Interface doctrine rejects comfort as function.'}, {id:'motivation_rule', label:'No motivational uplift language', type:'risk', test:(t)=>/\b(you got this|keep going everyone|believe in yourself|inspire|motivational)\b/i.test(t), rationale:'Interface does not motivate or persuade.'}, {id:'pattern_rule', label:'Pattern / evidence language present', type:'pass', test:(t)=>/\b(pattern|patterns|evidence|actions over time|behavior|observable|cost)\b/i.test(t), rationale:'Canon favors evidence and observable patterns.'}, {id:'ambiguity_rule', label:'Ambiguity tolerated or named', type:'pass', test:(t)=>/\b(uncertainty|ambiguity|pause|not knowing|over time)\b/i.test(t), rationale:'Mature WHGA writing distinguishes uncertainty from false certainty.'}, {id:'relationship_to_truth', label:'Truth/evidence orientation present', type:'pass', test:(t)=>/\b(truth|accurate|evidence|verdict|facts)\b/i.test(t), rationale:'Evidence over time is locked canon.'}, {id:'interface_specific', label:'Interface anti-function language present', type:'pass', test:(t)=>/\b(does not provide permission|does not comfort|does not motivate|does not persuade|clarifies)\b/i.test(t), rationale:'Useful for Interface-centered text only; absence is not fatal for other episodes.'}, ]; } function runV12DoctrineValidator() { const epCode = document.getElementById('v12DoctrineEpisode')?.value || EPISODES[0].code; const text = v12GetEpisodeScript(epCode); const pane = document.getElementById('v12DoctrinePane'); const status = document.getElementById('v12DoctrineStatus'); if (!text.trim()) { if (pane) pane.innerHTML = '
No script text found for this episode.
'; if (status) status.textContent = `No script found for ${epCode}.`; state.v12real.lastDoctrine = null; return; } const rules = v12DoctrineRules(); const results = rules.map(r => ({...r, hit: !!r.test(text)})); const risks = results.filter(r => r.type === 'risk' && r.hit); const passes = results.filter(r => r.type === 'pass' && r.hit); const misses = results.filter(r => r.type === 'pass' && !r.hit); const evidenceLines = text.split(/\n+/).filter(line => /(pattern|evidence|truth|ambiguity|uncertainty|should|must|need to|it's okay|you got this)/i.test(line)).slice(0,10); const report = { kind:'doctrine', createdAt:new Date().toISOString(), episode:epCode, textLength:text.length, risks: risks.map(r => ({id:r.id,label:r.label,rationale:r.rationale})), passes: passes.map(r => ({id:r.id,label:r.label,rationale:r.rationale})), misses: misses.map(r => ({id:r.id,label:r.label,rationale:r.rationale})), evidenceLines }; state.v12real.lastDoctrine = report; if (pane) pane.innerHTML = `

${epCode} — Doctrine Summary

Risks ${risks.length}Passes ${passes.length}Misses ${misses.length}

Text length: ${text.length} characters.

Risk Hits

${risks.length ? '' : '

No risk rules were triggered by the deterministic pass.

'}

Pass Hits

${passes.length ? '' : '

No positive doctrine signals were detected in this pass.

'}

Positive Rules Not Found

${misses.length ? '' : '

All positive doctrine rules were found.

'}

Evidence Lines

${v12Escape(evidenceLines.join('\n') || 'No matching evidence lines.')}
`; if (status) status.textContent = `Doctrine validation completed for ${epCode}.`; } function saveV12DoctrineReport() { if (!state.v12real?.lastDoctrine) return; state.v12real.saved.unshift({...state.v12real.lastDoctrine, id:'doctrine-' + Date.now()}); save(); renderV12Registry(); renderV12RealAudit(); const status = document.getElementById('v12DoctrineStatus'); if (status) status.textContent = 'Doctrine report saved.'; } function v12SearchPool() { const pool = []; EPISODES.forEach(e => pool.push({source:'episode', key:e.code, title:`${e.code} — ${e.title}`, text:[e.title,e.desc,v12GetEpisodeScript(e.code)].join('\n')})); LC_VIDEOS.forEach(v => pool.push({source:'lc-video', key:v.code, title:`${v.code} — ${v.title}`, text:[v.title,v.script,v.vo,v.placement,v.char].join('\n')})); BOT_CUTAWAYS.forEach(v => pool.push({source:'bot', key:'BOT-' + v.num, title:`BOT ${v.num} — ${v.title}`, text:[v.title,v.screen,v.narrator].join('\n')})); ASSETS.forEach((a,i) => pool.push({source:'asset', key:'asset-' + i, title:a.name, text:[a.name,a.notes,a.ep].join('\n')})); VO_TABLE.forEach((v,i) => pool.push({source:'vo', key:'vo-' + i, title:`${v.code} — ${v.char}`, text:[v.code,v.char,v.register,v.keyLine].join('\n')})); CHARACTERS.forEach((c,i) => pool.push({source:'character', key:'char-' + i, title:c.name, text:Object.values(c).join('\n')})); return pool; } function runV12CrossQuery() { const q = (document.getElementById('v12QueryInput')?.value || '').trim(); const pane = document.getElementById('v12QueryPane'); const status = document.getElementById('v12QueryStatus'); if (!q) { if (pane) pane.innerHTML = '
Enter a search term.
'; return; } const regex = new RegExp(q.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'), 'ig'); const results = v12SearchPool().map(item => { const matches = [...item.text.matchAll(regex)]; if (!matches.length) return null; const excerptStart = Math.max(0, matches[0].index - 70); const excerptEnd = Math.min(item.text.length, matches[0].index + q.length + 120); const excerpt = item.text.slice(excerptStart, excerptEnd).replace(/\n+/g, ' '); return {...item, count:matches.length, excerpt}; }).filter(Boolean).sort((a,b)=>b.count-a.count).slice(0,50); const snap = {kind:'query', id:'query-' + Date.now(), createdAt:new Date().toISOString(), query:q, resultCount:results.length, results}; state.v12real.lastQuery = snap; if (pane) pane.innerHTML = results.length ? `
` + results.map(r => `

${v12Escape(r.title)}

${v12Escape(r.source)}${r.count} matches

${v12Escape(r.excerpt)}
`).join('') + `
` : '
No matches found in the loaded corpus.
'; if (status) status.textContent = `Search completed: ${results.length} result(s).`; } function saveV12QuerySnapshot() { if (!state.v12real?.lastQuery) return; state.v12real.saved.unshift(state.v12real.lastQuery); save(); renderV12Registry(); renderV12RealAudit(); const status = document.getElementById('v12QueryStatus'); if (status) status.textContent = 'Query snapshot saved.'; } function v12ParseScriptText(text) { const lines = text.split(/\r?\n/).map(l => l.trim()).filter(Boolean); const units = []; const speakerRe = /^([A-Za-z][A-Za-z0-9 .'\-]+)\s*(?:\(|—|-)/; lines.forEach((line, idx) => { let type = 'line'; let speaker = ''; let timestamp = ''; const tsMatch = line.match(/\((\d{2}:\d{2})\)/); if (tsMatch) timestamp = tsMatch[1]; if (/^(INT\.|EXT\.|#|##|###|\[SETUP:|\[)/.test(line)) type = 'heading'; else if (speakerRe.test(line) || /^Speaker\s+\d+/.test(line)) { type = 'dialogue'; speaker = (line.match(/^Speaker\s+\d+/) || line.match(/^([A-Za-z][A-Za-z0-9 .'\-]+)/) || [''])[0]; } else if (/^(VO:|WALTER \(V\.O\.\):|NARRATOR:|ON SCREEN:)/.test(line)) { type = 'cue'; speaker = (line.match(/^([A-Z .]+):/) || [''])[0].replace(':',''); } units.push({index:idx+1, type, speaker, timestamp, text:line}); }); const counts = units.reduce((acc,u)=>{acc[u.type]=(acc[u.type]||0)+1; return acc;}, {}); const speakers = [...new Set(units.map(u=>u.speaker).filter(Boolean))]; return {createdAt:new Date().toISOString(), lineCount:lines.length, unitCount:units.length, counts, speakers, units:units.slice(0,400)}; } function runV12StructuredParser() { const epCode = document.getElementById('v12ParserEpisode')?.value || EPISODES[0].code; const text = v12GetEpisodeScript(epCode); const pane = document.getElementById('v12ParserPane'); const status = document.getElementById('v12ParserStatus'); if (!text.trim()) { if (pane) pane.innerHTML = '
No script text found for this episode.
'; return; } const parsed = v12ParseScriptText(text); const result = {kind:'parser', id:'parser-' + Date.now(), episode:epCode, ...parsed}; state.v12real.lastParse = result; if (pane) pane.innerHTML = `

${epCode} — Parse Summary

${parsed.unitCount} units${parsed.lineCount} non-empty lines${parsed.speakers.length} speakers

Type counts: ${Object.entries(parsed.counts).map(([k,v])=>`${k}: ${v}`).join(' · ')}

Speakers / Cues

${parsed.speakers.length ? parsed.speakers.map(x=>`${v12Escape(x)}`).join('') : 'No speaker tags detected by deterministic pass.'}

Unit Preview

${v12Escape(parsed.units.slice(0,25).map(u => `[${u.index}] ${u.type}${u.speaker ? ' / ' + u.speaker : ''}${u.timestamp ? ' @ ' + u.timestamp : ''} :: ${u.text}`).join('\n'))}
`; if (status) status.textContent = `Parsed ${epCode}.`; } function saveV12ParsedOutput() { const parsed = state.v12real?.lastParse; if (!parsed) return; if (!state.parsedScripts) state.parsedScripts = {}; state.parsedScripts[parsed.episode] = parsed; state.v12real.saved.unshift(parsed); save(); renderV12Registry(); renderV12RealAudit(); const status = document.getElementById('v12ParserStatus'); if (status) status.textContent = 'Parsed output saved to local state.'; } function v12CollectEpisodeRefs(epCode) { const shortCode = epCode.replace(/^S0?/, '').replace(/[^A-Z0-9-]/g,''); const checks = Object.entries(CHECKLISTS || {}).flatMap(([group,items]) => (items || []).filter(x => (x.ep || '').includes(epCode) || (x.ep || '').includes(shortCode) || (x.text || '').includes(epCode)).map(x => ({group, ...x}))); const assets = (ASSETS || []).filter(a => (a.ep || '').includes(epCode) || (a.notes || '').includes(epCode)); const vo = (VO_TABLE || []).filter(v => (v.code || '').includes(epCode)); const lc = (LC_VIDEOS || []).filter(v => (v.placement || '').includes(epCode.replace('S0','E')) || (v.placement || '').includes(epCode) || (v.script || '').includes(epCode)); const chars = (CHARACTERS || []).filter(c => (c.shootDays || []).some(s => epCode.includes(s) || s.includes(epCode.replace('S01','E').replace('S02','S2-')))); const parsed = state.parsedScripts?.[epCode] || null; return {checks, assets, vo, lc, chars, parsed}; } function runV12ProductionLinks() { const epCode = document.getElementById('v12LinkEpisode')?.value || EPISODES[0].code; const pane = document.getElementById('v12LinksPane'); const status = document.getElementById('v12LinksStatus'); const refs = v12CollectEpisodeRefs(epCode); const out = { kind:'links', id:'links-' + Date.now(), createdAt:new Date().toISOString(), episode:epCode, summary:{ checklists:refs.checks.length, assets:refs.assets.length, vo:refs.vo.length, lc:refs.lc.length, chars:refs.chars.length, parsed:!!refs.parsed }, refs }; state.v12real.lastLinks = out; if (pane) pane.innerHTML = `

${epCode} — Link Summary

Checklist ${refs.checks.length}Assets ${refs.assets.length}VO ${refs.vo.length}LC ${refs.lc.length}Characters ${refs.chars.length}Parsed ${refs.parsed ? 'yes' : 'no'}

Checklist Links

${refs.checks.length ? '' : '

No checklist links found.

'}

Asset / VO / LC

${(!refs.assets.length && !refs.vo.length && !refs.lc.length) ? '

No asset/VO/LC links found.

' : ''}

Character / Parse State

${refs.chars.length ? refs.chars.map(x=>`${v12Escape(x.name)}`).join('') : 'No character shoot references found.'}

${refs.parsed ? `Parsed units available: ${refs.parsed.unitCount || refs.parsed.units?.length || 0}` : 'No saved parse for this episode.'}

`; if (status) status.textContent = `Link map built for ${epCode}.`; } function saveV12LinkSnapshot() { if (!state.v12real?.lastLinks) return; state.v12real.saved.unshift(state.v12real.lastLinks); save(); renderV12Registry(); renderV12RealAudit(); const status = document.getElementById('v12LinksStatus'); if (status) status.textContent = 'Link snapshot saved.'; } function renderV12Registry() { const pane = document.getElementById('v12RegistryPane'); if (!pane) return; const items = state.v12real?.saved || []; if (!items.length) { pane.innerHTML = '
No saved V12 Real artifacts yet.
'; return; } pane.innerHTML = '
' + items.slice(0,50).map((item, idx) => `

${v12Escape(item.kind || 'artifact')} — ${v12Escape(item.episode || item.query || item.id || '')}

${v12Escape(item.id || 'unsaved')}${new Date(item.createdAt).toLocaleString()}

`).join('') + '
'; } function selectV12RegistryArtifact(index) { const item = (state.v12real?.saved || [])[index]; const pane = document.getElementById('v12RegistryDetailPane'); if (!pane || !item) return; pane.innerHTML = `

${v12Escape(item.kind || 'artifact')} detail

${v12Escape(JSON.stringify(item, null, 2))}
`; } function initV12Real() { ensureV12RealState(); populateV12RealEpisodeSelects(); renderV12RealAudit(); renderV12Registry(); } function ensureProductionAssetState() { if (!state.productionAssets) state.productionAssets = {}; const p = state.productionAssets; if (!p.media) p.media = {}; if (!p.shots) p.shots = {}; if (!p.costumes) p.costumes = {}; if (!p.props) p.props = {}; if (!p.voItems) p.voItems = {}; if (!p.savedChecklists) p.savedChecklists = []; } function v12AllProdEpisodeSelectIds() { return ['v12RemainingEpisode','v12MediaEpisode','v12MediaFilterEpisode','v12ShotEpisode','v12ShotFilterEpisode','v12AssetEpisode','v12AssetFilterEpisode','v12VOEpisode','v12VOFilterEpisode','v12ChecklistEpisode']; } function populateV12ProductionSelects() { ensureProductionAssetState(); const opts = EPISODES.map(ep => ``).join(''); v12AllProdEpisodeSelectIds().forEach(id => { const el = document.getElementById(id); if (!el) return; const prev = el.value; el.innerHTML = opts; if (prev && EPISODES.some(e=>e.code===prev)) el.value = prev; }); const defaults = { v12RemainingEpisode:'', v12MediaEpisode:'', v12MediaFilterEpisode:'', v12ShotEpisode:'', v12ShotFilterEpisode:'', v12AssetEpisode:'', v12AssetFilterEpisode:'', v12VOEpisode:'', v12VOFilterEpisode:'', v12ChecklistEpisode:'' }; Object.entries(defaults).forEach(([id,val])=>{ const el=document.getElementById(id); if (el && !el.value) el.value=val; }); } function v12ProdCollection(type) { ensureProductionAssetState(); return state.productionAssets[type] || {}; } function ensureV12UploadState() { if (!state.uiTemp) state.uiTemp = {}; if (!state.uiTemp.v12MediaUpload) state.uiTemp.v12MediaUpload = null; if (!state.uiTemp.v12AssetUpload) state.uiTemp.v12AssetUpload = null; } function v12MediaThumbSrc(item) { return item.imageData || item.path || ''; } function setV12UploadPreview(imgId, wrapId, src) { const img = document.getElementById(imgId); const wrap = document.getElementById(wrapId); if (!img || !wrap) return; if (src) { img.src = src; wrap.style.display = 'block'; } else { img.removeAttribute('src'); wrap.style.display = 'none'; } } function readV12ImageFile(file, done) { if (!file) return; const reader = new FileReader(); reader.onload = e => done({ name:file.name, type:file.type, size:file.size, dataUrl:e.target.result }); reader.readAsDataURL(file); } function handleV12MediaFile(files) { ensureV12UploadState(); const file = files && files[0]; if (!file) return; readV12ImageFile(file, payload => { state.uiTemp.v12MediaUpload = payload; setV12UploadPreview('v12MediaPreview', 'v12MediaPreviewWrap', payload.dataUrl); const s=document.getElementById('v12MediaStatus'); if (s) s.textContent = `Loaded image: ${payload.name}`; }); } function handleV12MediaDrop(event) { event.preventDefault(); event.currentTarget.classList.remove('dragover'); handleV12MediaFile(event.dataTransfer.files); } function clearV12MediaUpload() { ensureV12UploadState(); state.uiTemp.v12MediaUpload = null; const file = document.getElementById('v12MediaFile'); if (file) file.value = ''; setV12UploadPreview('v12MediaPreview', 'v12MediaPreviewWrap', ''); } function handleV12AssetFile(files) { ensureV12UploadState(); const file = files && files[0]; if (!file) return; readV12ImageFile(file, payload => { state.uiTemp.v12AssetUpload = payload; setV12UploadPreview('v12AssetPreview', 'v12AssetPreviewWrap', payload.dataUrl); const s=document.getElementById('v12AssetAddStatus'); if (s) s.textContent = `Loaded image: ${payload.name}`; }); } function handleV12AssetDrop(event) { event.preventDefault(); event.currentTarget.classList.remove('dragover'); handleV12AssetFile(event.dataTransfer.files); } function clearV12AssetUpload() { ensureV12UploadState(); state.uiTemp.v12AssetUpload = null; const file = document.getElementById('v12AssetFile'); if (file) file.value = ''; setV12UploadPreview('v12AssetPreview', 'v12AssetPreviewWrap', ''); } function linkV12MediaToShot(shotId, mediaId) { ensureProductionAssetState(); const shot = state.productionAssets.shots[shotId]; const media = state.productionAssets.media[mediaId]; if (!shot || !media) return; if (!shot.mediaIds) shot.mediaIds = []; if (!shot.mediaIds.includes(mediaId)) shot.mediaIds.push(mediaId); media.linked = shotId; save(); renderV12ShotPlanner(); renderV12MediaBoard(); } function unlinkV12MediaFromShot(shotId, mediaId) { ensureProductionAssetState(); const shot = state.productionAssets.shots[shotId]; const media = state.productionAssets.media[mediaId]; if (!shot) return; shot.mediaIds = (shot.mediaIds || []).filter(x => x !== mediaId); if (media && media.linked === shotId) media.linked = ''; save(); renderV12ShotPlanner(); renderV12MediaBoard(); } function addV12MediaItem() { ensureProductionAssetState(); const episode = document.getElementById('v12MediaEpisode').value; const id = 'MED-' + Date.now(); state.productionAssets.media[id] = { id, episode, category: document.getElementById('v12MediaCategory').value, title: document.getElementById('v12MediaTitle').value.trim() || 'Untitled media', path: document.getElementById('v12MediaPath').value.trim(), imageData: state.uiTemp?.v12MediaUpload?.dataUrl || '', fileName: state.uiTemp?.v12MediaUpload?.name || '', linked: document.getElementById('v12MediaLink').value.trim(), note: document.getElementById('v12MediaNote').value.trim(), status: 'active', createdAt: new Date().toISOString() }; if (state.productionAssets.media[id].linked && state.productionAssets.shots[state.productionAssets.media[id].linked]) { const shot = state.productionAssets.shots[state.productionAssets.media[id].linked]; if (!shot.mediaIds) shot.mediaIds = []; if (!shot.mediaIds.includes(id)) shot.mediaIds.push(id); } save(); renderV12MediaBoard(); renderV12ShotPlanner(); clearV12MediaUpload(); document.getElementById('v12MediaStatus').textContent = `Added media item ${id}.`; } function renderV12MediaBoard() { ensureProductionAssetState(); const episode = (document.getElementById('v12MediaFilterEpisode') || {}).value || EPISODES[0].code; const pane = document.getElementById('v12MediaPane'); if (!pane) return; const items = Object.values(state.productionAssets.media).filter(x=>x.episode===episode).sort((a,b)=>a.createdAt < b.createdAt ? 1 : -1); if (!items.length) { pane.innerHTML = '
No media items saved for this episode yet.
'; return; } pane.innerHTML = '
' + items.map(item => { const src = v12MediaThumbSrc(item); const thumb = src ? `${v12Escape(item.title)}` : '
No image attached.
'; const linkedText = item.linked || '—'; return `
${thumb}

${v12Escape(item.title)} ${v12Escape(item.category)}

File: ${v12Escape(item.fileName || item.path || '—')}

Linked: ${v12Escape(linkedText)}

${v12Escape(item.note || '')}

`; }).join('') + '
'; } function addV12Shot() { ensureProductionAssetState(); const ep = document.getElementById('v12ShotEpisode').value; const n = Object.values(state.productionAssets.shots).filter(x=>x.episode===ep).length + 1; const id = `${ep}-SH-${String(n).padStart(3,'0')}`; state.productionAssets.shots[id] = { id, episode:ep, scene: document.getElementById('v12ShotScene').value.trim() || 'SC?', description: document.getElementById('v12ShotDesc').value.trim() || 'Untitled shot', cast: document.getElementById('v12ShotCast').value.split(',').map(s=>s.trim()).filter(Boolean), costume: document.getElementById('v12ShotCostume').value.trim(), props: document.getElementById('v12ShotProps').value.split(',').map(s=>s.trim()).filter(Boolean), location: document.getElementById('v12ShotLocation').value.trim(), intext: document.getElementById('v12ShotIntext').value, daynight: document.getElementById('v12ShotDaynight').value, voRequired: document.getElementById('v12ShotVO').checked, status: document.getElementById('v12ShotStatus').value, notes: document.getElementById('v12ShotNotes').value.trim(), mediaIds: [], createdAt: new Date().toISOString() }; save(); renderV12ShotPlanner(); renderV12Remaining(); document.getElementById('v12ShotAddStatus').textContent = `Added shot ${id}.`; } function setV12ShotStatus(id, status) { ensureProductionAssetState(); if (!state.productionAssets.shots[id]) return; state.productionAssets.shots[id].status = status; save(); renderV12ShotPlanner(); renderV12Remaining(); } function renderV12ShotPlanner() { ensureProductionAssetState(); const ep = (document.getElementById('v12ShotFilterEpisode') || {}).value || EPISODES[0].code; const pane = document.getElementById('v12ShotPane'); if (!pane) return; const shots = Object.values(state.productionAssets.shots).filter(x=>x.episode===ep); const media = Object.values(state.productionAssets.media).filter(x=>x.episode===ep); if (!shots.length) { pane.innerHTML = '
No shot records yet for this episode.
'; return; } pane.innerHTML = '
' + shots.map(s => { const linked = (s.mediaIds || []).map(id => state.productionAssets.media[id]).filter(Boolean); const available = media.filter(m => !(s.mediaIds || []).includes(m.id)); const linkedHtml = linked.length ? '
' + linked.map(m => `
${v12MediaThumbSrc(m) ? `${v12Escape(m.title)}` : ''}
${v12Escape(m.title)}
`).join('') + '
' : '
No media linked yet.
'; const availableHtml = available.length ? available.slice(0,8).map(m => ``).join('') : 'No unlinked media left for this episode.'; return `

${v12Escape(s.id)} ${v12Escape(s.status)}

${v12Escape(s.scene)} — ${v12Escape(s.description)}

Location: ${v12Escape((s.location||'—'))} · ${v12Escape((s.intext||''))}/${v12Escape((s.daynight||''))}

Cast: ${v12Escape((s.cast||[]).join(', ')||'—')}

Costume: ${v12Escape(s.costume||'—')}

Props: ${v12Escape((s.props||[]).join(', ')||'—')}

VO: ${s.voRequired ? 'yes' : 'no'}

Linked media
${linkedHtml}
Link media to this shot
${availableHtml}
`; }).join('') + '
'; } function addV12AssetRecord() { ensureProductionAssetState(); const type = document.getElementById('v12AssetType').value; const key = type === 'costume' ? 'costumes' : 'props'; const id = (type === 'costume' ? 'COST-' : 'PROP-') + Date.now(); state.productionAssets[key][id] = { id, type, episode: document.getElementById('v12AssetEpisode').value, character: document.getElementById('v12AssetCharacter').value.trim(), name: document.getElementById('v12AssetName').value.trim() || (type==='costume' ? 'Untitled costume' : 'Untitled prop'), items: document.getElementById('v12AssetItems').value.split(',').map(s=>s.trim()).filter(Boolean), status: document.getElementById('v12AssetStatus').value, image: document.getElementById('v12AssetImage').value.trim(), imageData: state.uiTemp?.v12AssetUpload?.dataUrl || '', fileName: state.uiTemp?.v12AssetUpload?.name || '', note: document.getElementById('v12AssetNote').value.trim(), createdAt: new Date().toISOString() }; save(); renderV12AssetRegistry(); renderV12Remaining(); clearV12AssetUpload(); document.getElementById('v12AssetAddStatus').textContent = `Added ${type} record ${id}.`; } function setV12AssetStatus(collection, id, status) { ensureProductionAssetState(); if (!state.productionAssets[collection]?.[id]) return; state.productionAssets[collection][id].status = status; save(); renderV12AssetRegistry(); renderV12Remaining(); } function renderV12AssetRegistry() { ensureProductionAssetState(); const ep = (document.getElementById('v12AssetFilterEpisode') || {}).value || EPISODES[0].code; const pane = document.getElementById('v12AssetPane'); if (!pane) return; const costumes = Object.values(state.productionAssets.costumes).filter(x=>x.episode===ep); const props = Object.values(state.productionAssets.props).filter(x=>x.episode===ep); if (!costumes.length && !props.length) { pane.innerHTML = '
No costume or prop records for this episode.
'; return; } const renderCard = (x, collection, ownerLabel) => { const src = x.imageData || x.image || ''; return `
${src ? `${v12Escape(x.name)}` : ''}

${v12Escape(x.name)} ${v12Escape(x.status)}

${ownerLabel}: ${v12Escape(x.character||'—')}

Items: ${v12Escape((x.items||[]).join(', ')||'—')}

Image: ${v12Escape(x.fileName || x.image || '—')}

`; }; pane.innerHTML = '
' + (costumes.length ? costumes.map(x=>renderCard(x,'costumes','Character')).join('') : '
No costumes.
') + '
' + (props.length ? props.map(x=>renderCard(x,'props','Owner')).join('') : '
No props.
') + '
'; } function addV12VOItem() { ensureProductionAssetState(); const ep = document.getElementById('v12VOEpisode').value; const id = 'VO-' + Date.now(); state.productionAssets.voItems[id] = { id, episode:ep, speaker:document.getElementById('v12VOSpeaker').value.trim() || 'Unknown', status:document.getElementById('v12VOStatus').value, line:document.getElementById('v12VOLine').value.trim(), createdAt:new Date().toISOString() }; save(); renderV12VOTracker(); renderV12Remaining(); document.getElementById('v12VOAddStatus').textContent = `Added VO item ${id}.`; } function setV12VOStatus(id, status) { ensureProductionAssetState(); if (!state.productionAssets.voItems[id]) return; state.productionAssets.voItems[id].status = status; save(); renderV12VOTracker(); renderV12Remaining(); } function renderV12VOTracker() { ensureProductionAssetState(); const ep = (document.getElementById('v12VOFilterEpisode') || {}).value || EPISODES[0].code; const pane = document.getElementById('v12VOPane'); if (!pane) return; const items = Object.values(state.productionAssets.voItems).filter(x=>x.episode===ep); if (!items.length) { pane.innerHTML = '
No VO items saved for this episode.
'; return; } pane.innerHTML = '
' + items.map(x=>`

${v12Escape(x.speaker)} ${v12Escape(x.status)}

${v12Escape(x.line||'')}

`).join('') + '
'; } function buildV12RemainingData(ep) { ensureProductionAssetState(); const shots = Object.values(state.productionAssets.shots).filter(x=>x.episode===ep); const voItems = Object.values(state.productionAssets.voItems).filter(x=>x.episode===ep); const costumes = Object.values(state.productionAssets.costumes).filter(x=>x.episode===ep); const props = Object.values(state.productionAssets.props).filter(x=>x.episode===ep); const shotsLeft = shots.filter(x=>!['shot','complete'].includes(x.status)); const reshoots = shots.filter(x=>x.status==='reshoot'); const voLeft = voItems.filter(x=>x.status!=='final'); const costumeMissing = costumes.filter(x=>x.status!=='ready'); const propsMissing = props.filter(x=>x.status!=='ready'); const missingFromShots = []; shotsLeft.forEach(s => { if (s.costume && !costumes.some(c=>c.episode===ep && (c.name===s.costume || c.character===s.costume) && c.status==='ready')) missingFromShots.push({type:'costume', value:s.costume, shot:s.id}); (s.props||[]).forEach(p => { if (!props.some(pr=>pr.episode===ep && pr.name===p && pr.status==='ready')) missingFromShots.push({type:'prop', value:p, shot:s.id}); }); if (s.voRequired && !voItems.some(v=>v.episode===ep && v.status==='final')) missingFromShots.push({type:'vo', value:'VO final missing', shot:s.id}); }); return {ep, shots, shotsLeft, reshoots, voItems, voLeft, costumes, costumeMissing, props, propsMissing, missingFromShots}; } function renderV12Remaining() { const ep = (document.getElementById('v12RemainingEpisode') || {}).value || EPISODES[0].code; const pane = document.getElementById('v12RemainingPane'); if (!pane) return; const d = buildV12RemainingData(ep); pane.innerHTML = `
Shots Left
${d.shotsLeft.length}
Reshoots
${d.reshoots.length}
VO Left
${d.voLeft.length}
Missing Items
${d.costumeMissing.length + d.propsMissing.length + d.missingFromShots.length}

What is left to be shot

${d.shotsLeft.length ? '' : '

No open shots.

'}

Voiceover still needed

${d.voLeft.length ? '' : '

No open VO items.

'}

Missing costume / prop readiness

${(d.costumeMissing.length || d.propsMissing.length) ? '' : '

No missing costume/prop records.

'}

Dependency gaps from open shots

${d.missingFromShots.length ? '' : '

No shot dependency gaps detected.

'}
`; } function renderV12GeneratedChecklist() { const ep = (document.getElementById('v12ChecklistEpisode') || {}).value || EPISODES[0].code; const pane = document.getElementById('v12ChecklistPane'); if (!pane) return; const d = buildV12RemainingData(ep); const items = []; d.shotsLeft.forEach(s=>items.push(`Shoot ${s.id}: ${s.description}`)); d.reshoots.forEach(s=>items.push(`Reshoot ${s.id}: ${s.description}`)); d.voLeft.forEach(v=>items.push(`Record / finish VO: ${v.speaker} — ${v.line || v.status}`)); d.costumeMissing.forEach(c=>items.push(`Resolve costume: ${c.name} [${c.status}]`)); d.propsMissing.forEach(p=>items.push(`Resolve prop: ${p.name} [${p.status}]`)); d.missingFromShots.forEach(x=>items.push(`Dependency gap for ${x.shot}: ${x.type} — ${x.value}`)); if (!items.length) { pane.innerHTML = '
No open items. Checklist is clear.
'; return; } pane.innerHTML = '

Generated production checklist

    ' + items.map(x=>`
  1. ${v12Escape(x)}
  2. `).join('') + '
'; state.productionAssets.lastGeneratedChecklist = {episode:ep, items, createdAt:new Date().toISOString()}; } function saveV12GeneratedChecklist() { ensureProductionAssetState(); const c = state.productionAssets.lastGeneratedChecklist; const status = document.getElementById('v12ChecklistStatus'); if (!c) { if (status) status.textContent = 'Generate a checklist first.'; return; } state.productionAssets.savedChecklists.unshift({...c, id:'CHK-' + Date.now()}); save(); if (status) status.textContent = 'Generated checklist snapshot saved.'; } function initV12ProductionAssets() { ensureProductionAssetState(); ensureV12UploadState(); populateV12ProductionSelects(); renderV12MediaBoard(); renderV12ShotPlanner(); renderV12AssetRegistry(); renderV12VOTracker(); renderV12Remaining(); renderV12GeneratedChecklist(); } // ───────────────────────────────────────────── // INIT // ───────────────────────────────────────────── document.addEventListener('DOMContentLoaded', () => { loadState(); renderDashboard(); updateOverall(); initCommandBrain(); initV10Engine(); renderSchemaPass(); initV11Roadmap(); initV11Timelines(); initV11Fusion(); initV12Real(); initV12ProductionAssets(); initGearModule(); initBudgetModule(); initCallSheetModule(); }); // ═══════════════════════════════════════════════════════ // GEAR TRACKER // ═══════════════════════════════════════════════════════ function ensureGear() { if (!state.gear) state.gear = {}; } function initGearModule() { ensureGear(); // Seed HCR gear if first run if (Object.keys(state.gear).length === 0) { const seed = [ {name:'Sony ZV-E10 (A-Cam)', category:'Camera', status:'available', notes:'Primary camera, kit lens attached'}, {name:'Sony ZV-E10 (B-Cam)', category:'Camera', status:'available', notes:'Secondary camera, 35mm prime'}, {name:'COLBOR CL60', category:'Lighting', status:'available', notes:'Key light, Pelican case shelf 1'}, {name:'ULANZI VL119', category:'Lighting', status:'available', notes:'Small fill / on-camera LED'}, {name:'NEEWER Reflector', category:'Lighting', status:'available', notes:'5-in-1, rolled in carry bag'}, {name:'SmallRig Softbox', category:'Lighting', status:'available', notes:'Fits COLBOR CL60'}, {name:'PQRQP Wireless Mic', category:'Audio', status:'available', notes:'TX + RX, check charge before shoot'}, {name:'Tripod (Heavy)', category:'Stabilization', status:'available', notes:'Fluid head, studio use'}, {name:'Gorilla Pod', category:'Stabilization', status:'available', notes:'Flexible, location B-roll'}, {name:'Laptop (Dee)', category:'Accessories', status:'available', notes:'On-set monitor / backup record'}, ]; seed.forEach(item => { const id = 'GEAR-' + Date.now() + '-' + Math.random().toString(36).slice(2,6); state.gear[id] = Object.assign({id}, item, {createdAt: new Date().toISOString()}); }); save(); } } function renderGear() { ensureGear(); const filterText = (document.getElementById('gearFilter')?.value || '').toLowerCase(); const filterCat = document.getElementById('gearCatFilter')?.value || ''; const items = Object.values(state.gear).sort((a,b) => a.name.localeCompare(b.name)); const filtered = items.filter(g => { const matchText = !filterText || g.name.toLowerCase().includes(filterText) || (g.notes||'').toLowerCase().includes(filterText); const matchCat = !filterCat || g.category === filterCat; return matchText && matchCat; }); const available = items.filter(g => g.status === 'available').length; const checkedout = items.filter(g => g.status === 'checkedout').length; const maintenance = items.filter(g => g.status === 'maintenance').length; const summary = document.getElementById('gearSummary'); if (summary) summary.innerHTML = `
${items.length}
Total Items
${available}
Available
${checkedout}
In Use
`; const list = document.getElementById('gearList'); if (!list) return; if (!filtered.length) { list.innerHTML = '
No items match filter.
'; return; } list.innerHTML = filtered.map(g => { const statusClass = g.status === 'available' ? 'gear-available' : g.status === 'checkedout' ? 'gear-checkedout' : 'gear-maintenance'; const statusLabel = g.status === 'available' ? 'Available' : g.status === 'checkedout' ? 'In Use' : 'Maintenance'; return `
${escHtml(g.name)}
${escHtml(g.category)}${g.notes ? ' · ' + escHtml(g.notes) : ''}
${statusLabel}
`; }).join(''); } function gearToggleStatus(id) { ensureGear(); const g = state.gear[id]; if (!g) return; const cycle = {available:'checkedout', checkedout:'maintenance', maintenance:'available'}; g.status = cycle[g.status] || 'available'; save(); renderGear(); } function gearEdit(id) { ensureGear(); const g = state.gear[id]; if (!g) return; document.getElementById('gearEditId').value = id; document.getElementById('gearName').value = g.name; document.getElementById('gearCategory').value = g.category; document.getElementById('gearStatus').value = g.status; document.getElementById('gearNotes').value = g.notes || ''; document.getElementById('gearName').focus(); } function gearDelete(id) { ensureGear(); if (!confirm('Remove this item?')) return; delete state.gear[id]; save(); renderGear(); } function saveGearItem() { ensureGear(); const name = document.getElementById('gearName').value.trim(); if (!name) { document.getElementById('gearAddStatus').textContent = 'Name required.'; return; } const editId = document.getElementById('gearEditId').value; const id = editId || ('GEAR-' + Date.now() + '-' + Math.random().toString(36).slice(2,6)); state.gear[id] = { id, name, category: document.getElementById('gearCategory').value, status: document.getElementById('gearStatus').value, notes: document.getElementById('gearNotes').value.trim(), createdAt: state.gear[id]?.createdAt || new Date().toISOString() }; save(); clearGearForm(); renderGear(); document.getElementById('gearAddStatus').textContent = editId ? '✓ Updated.' : '✓ Added.'; setTimeout(() => { const el = document.getElementById('gearAddStatus'); if(el) el.textContent=''; }, 2500); } function clearGearForm() { document.getElementById('gearEditId').value = ''; document.getElementById('gearName').value = ''; document.getElementById('gearCategory').value = 'Camera'; document.getElementById('gearStatus').value = 'available'; document.getElementById('gearNotes').value = ''; } // ═══════════════════════════════════════════════════════ // BUDGET + EXPENSES // ═══════════════════════════════════════════════════════ function ensureExpenses() { if (!state.expenses) state.expenses = {}; } function initBudgetModule() { ensureExpenses(); } const CAT_LABELS = {gear:'Gear', location:'Location', props:'Props', travel:'Travel', food:'Food', other:'Other'}; const CAT_CLASS = {gear:'cat-gear', location:'cat-location', props:'cat-props', travel:'cat-travel', food:'cat-food', other:'cat-other'}; function populateBudgetEpSelects() { const epOptions = ['', ...EPISODES.map(e => ``) ].join(''); const filterOptions = ['', ...EPISODES.map(e => ``) ].join(''); const epSel = document.getElementById('expEpisode'); const filterSel = document.getElementById('expFilterEp'); if (epSel) epSel.innerHTML = epOptions; if (filterSel) filterSel.innerHTML = filterOptions; } function renderBudgetKpis() { ensureExpenses(); const items = Object.values(state.expenses); const total = items.reduce((s,e) => s + (parseFloat(e.amount)||0), 0); const thisMonth = items.filter(e => { if (!e.date) return false; const d = new Date(e.date); const now = new Date(); return d.getFullYear()===now.getFullYear() && d.getMonth()===now.getMonth(); }).reduce((s,e) => s + (parseFloat(e.amount)||0), 0); const catTotals = {}; items.forEach(e => { catTotals[e.category] = (catTotals[e.category]||0) + (parseFloat(e.amount)||0); }); const topCat = Object.entries(catTotals).sort((a,b)=>b[1]-a[1])[0]; const el = document.getElementById('budgetKpis'); if (!el) return; el.innerHTML = `
Total Logged
$${total.toFixed(2)}
This Month
$${thisMonth.toFixed(2)}
Transactions
${items.length}
Top Category
${topCat ? CAT_LABELS[topCat[0]] + ' · $' + topCat[1].toFixed(0) : '—'}
`; } function renderExpenses() { ensureExpenses(); const filterEp = document.getElementById('expFilterEp')?.value || ''; const filterCat = document.getElementById('expFilterCat')?.value || ''; let items = Object.values(state.expenses).sort((a,b) => (b.date||'').localeCompare(a.date||'')); if (filterEp) items = items.filter(e => e.episode === filterEp); if (filterCat) items = items.filter(e => e.category === filterCat); const list = document.getElementById('expenseList'); if (!list) return; if (!items.length) { list.innerHTML = '
No expenses logged.
'; return; } list.innerHTML = items.map(e => `
${e.date || '—'} ${CAT_LABELS[e.category]||e.category} ${escHtml(e.desc||'')}
${e.episode||'General'}
$${parseFloat(e.amount||0).toFixed(2)}
`).join(''); } function saveExpense() { ensureExpenses(); const desc = document.getElementById('expDesc').value.trim(); const amount = parseFloat(document.getElementById('expAmount').value); if (!desc) { document.getElementById('expAddStatus').textContent = 'Description required.'; return; } if (isNaN(amount)||amount<0) { document.getElementById('expAddStatus').textContent = 'Valid amount required.'; return; } const editId = document.getElementById('expEditId').value; const id = editId || ('EXP-' + Date.now()); state.expenses[id] = { id, date: document.getElementById('expDate').value, episode: document.getElementById('expEpisode').value, category: document.getElementById('expCat').value, desc, amount, createdAt: state.expenses[id]?.createdAt || new Date().toISOString() }; save(); clearExpenseForm(); renderBudgetKpis(); renderExpenses(); document.getElementById('expAddStatus').textContent = '✓ Logged.'; setTimeout(() => { const el = document.getElementById('expAddStatus'); if(el) el.textContent=''; }, 2500); } function editExpense(id) { ensureExpenses(); const e = state.expenses[id]; if (!e) return; document.getElementById('expEditId').value = id; document.getElementById('expDate').value = e.date || ''; document.getElementById('expEpisode').value = e.episode || ''; document.getElementById('expCat').value = e.category; document.getElementById('expDesc').value = e.desc || ''; document.getElementById('expAmount').value = e.amount || ''; document.getElementById('expDesc').focus(); } function deleteExpense(id) { ensureExpenses(); if (!confirm('Delete this expense?')) return; delete state.expenses[id]; save(); renderBudgetKpis(); renderExpenses(); } function clearExpenseForm() { document.getElementById('expEditId').value = ''; document.getElementById('expDate').value = new Date().toISOString().slice(0,10); document.getElementById('expEpisode').value = ''; document.getElementById('expCat').value = 'gear'; document.getElementById('expDesc').value = ''; document.getElementById('expAmount').value = ''; } // ═══════════════════════════════════════════════════════ // CALL SHEET GENERATOR // ═══════════════════════════════════════════════════════ function initCallSheetModule() { if (!state.callSheets) state.callSheets = []; // Set today as default date const dateEl = document.getElementById('csDate'); if (dateEl && !dateEl.value) dateEl.value = new Date().toISOString().slice(0,10); const expDateEl = document.getElementById('expDate'); if (expDateEl && !expDateEl.value) expDateEl.value = new Date().toISOString().slice(0,10); } function populateCSEpSelect() { const sel = document.getElementById('csEpisode'); if (!sel) return; sel.innerHTML = '' + EPISODES.map(e => ``).join(''); } function csPrefill() { const code = document.getElementById('csEpisode').value; if (!code) return; const ep = EPISODES.find(e => e.code === code); if (!ep) return; // Try to pull shots from productionAssets if present ensureProductionAssetState && ensureProductionAssetState(); const shots = state.productionAssets?.shots ? Object.values(state.productionAssets.shots).filter(s => s.episode === code) : []; if (shots.length) { document.getElementById('csScenes').value = shots.map(s => `${s.scene||'SC'} — ${s.description||''} [${s.location||''}] (${s.status||'planned'})` ).join('\n'); } // Prefill gear from gear tracker const gearAvail = Object.values(state.gear||{}).filter(g => g.status !== 'maintenance'); if (gearAvail.length) { document.getElementById('csGear').value = gearAvail.map(g => g.name).join('\n'); } } function fmt12(t) { if (!t) return '—'; const [h, m] = t.split(':').map(Number); const ampm = h >= 12 ? 'PM' : 'AM'; return `${h % 12 || 12}:${String(m).padStart(2,'0')} ${ampm}`; } function generateCallSheet() { const epCode = document.getElementById('csEpisode').value; const ep = EPISODES.find(e => e.code === epCode); const date = document.getElementById('csDate').value; const dayNum = document.getElementById('csDayNum').value; const callTime = document.getElementById('csCallTime').value; const shootCall= document.getElementById('csShootCall').value; const lunch = document.getElementById('csLunch').value; const wrap = document.getElementById('csWrap').value; const location = document.getElementById('csLocation').value.trim(); const parking = document.getElementById('csParking').value.trim(); const scenes = document.getElementById('csScenes').value.trim(); const cast = document.getElementById('csCast').value.trim(); const gear = document.getElementById('csGear').value.trim(); const notes = document.getElementById('csNotes').value.trim(); const dateFormatted = date ? new Date(date + 'T12:00:00').toLocaleDateString('en-CA', {weekday:'long', year:'numeric', month:'long', day:'numeric'}) : '—'; const scenesHTML = scenes ? scenes.split('\n').filter(Boolean).map((s,i) => `
${i+1}. ${escHtml(s)}
`).join('') : '
No scenes specified.
'; const castHTML = cast ? `
NameCall Time
${cast.split('\n').filter(Boolean).map(c => { const [name,...rest] = c.split('—'); return `
${escHtml(name.trim())}${escHtml(rest.join('—').trim())}
`; }).join('')}` : '
No cast specified.
'; const gearHTML = gear ? gear.split('\n').filter(Boolean).map(g => `
· ${escHtml(g)}
`).join('') : '
No gear specified.
'; const html = `

Walter Has Gone Away

${ep ? escHtml(ep.code + ' — ' + ep.title) : 'Daily Call Sheet'}${dayNum ? ' · Shoot Day ' + dayNum : ''}

${dateFormatted}
General Information
General Call${fmt12(callTime)}
Shoot Call${fmt12(shootCall)}
Lunch${fmt12(lunch)}
Est. Wrap${fmt12(wrap)}
Location${escHtml(location||'TBD')}
Parking${escHtml(parking||'—')}
Scenes / Shots Today
${scenesHTML}
Cast Call Times
${castHTML}
Gear Checklist
${gearHTML}
${notes ? `
Notes / Safety
${escHtml(notes)}
` : ''} `; document.getElementById('csPreview').innerHTML = html; document.getElementById('csPreviewWrap').style.display = 'block'; document.getElementById('csPreviewWrap').scrollIntoView({behavior:'smooth'}); } function saveCallSheet() { if (!state.callSheets) state.callSheets = []; const epCode = document.getElementById('csEpisode').value; const date = document.getElementById('csDate').value; const html = document.getElementById('csPreview').innerHTML; if (!html.trim()) { alert('Generate a call sheet first.'); return; } state.callSheets.unshift({ id: 'CS-' + Date.now(), epCode, date, dayNum: document.getElementById('csDayNum').value, location: document.getElementById('csLocation').value.trim(), html, savedAt: new Date().toISOString() }); // Keep last 20 if (state.callSheets.length > 20) state.callSheets = state.callSheets.slice(0, 20); save(); renderSavedCallSheets(); } function renderSavedCallSheets() { if (!state.callSheets) state.callSheets = []; const el = document.getElementById('csSavedList'); if (!el) return; if (!state.callSheets.length) { el.innerHTML = '
No saved call sheets yet. Generate and click Save Snapshot.
'; return; } el.innerHTML = state.callSheets.map(cs => `
${escHtml(cs.epCode||'General')}${cs.dayNum ? ' · Day ' + cs.dayNum : ''}
${cs.date||'—'} · ${escHtml(cs.location||'No location')}
`).join(''); } function loadSavedCallSheet(id) { const cs = state.callSheets.find(c => c.id === id); if (!cs) return; document.getElementById('csPreview').innerHTML = cs.html; document.getElementById('csPreviewWrap').style.display = 'block'; document.getElementById('csPreviewWrap').scrollIntoView({behavior:'smooth'}); } function deleteSavedCallSheet(id) { if (!confirm('Delete this saved call sheet?')) return; state.callSheets = state.callSheets.filter(c => c.id !== id); save(); renderSavedCallSheets(); } function clearCSForm() { document.getElementById('csEpisode').value = ''; document.getElementById('csDate').value = new Date().toISOString().slice(0,10); document.getElementById('csDayNum').value = ''; document.getElementById('csCallTime').value = '07:00'; document.getElementById('csShootCall').value= '09:00'; document.getElementById('csLunch').value = '13:00'; document.getElementById('csWrap').value = '18:00'; document.getElementById('csLocation').value = ''; document.getElementById('csParking').value = ''; document.getElementById('csScenes').value = ''; document.getElementById('csCast').value = ''; document.getElementById('csGear').value = ''; document.getElementById('csNotes').value = ''; document.getElementById('csPreviewWrap').style.display = 'none'; } // ===== WHGA S3 INTEGRATION PATCH v16 ===== const WHGA_S3_EPISODES = [ {code:'S03E01', title:'I Guess That\'s Alright', season:'S3', status:'draft', scriptKey:'S03E01', desc:'Single location. No internal characters. Just Walter, the city, the bass, and the door.'}, {code:'S03E02', title:'The Edge of the Map', season:'S3', status:'draft', scriptKey:'S03E02', desc:'Night walk. Downtown Vancouver. How do you trust yourself again when you have been confidently wrong before? Walter asks the question live. Camera goes on the ground. He walks away without it.'}, {code:'S03E03', title:'I Can Heal for Realz', season:'S3', status:'draft', scriptKey:'S03E03', desc:'Jonas Blasektoski (Blaze) goes to Hawaii. Walter facilitates narcissist support group in his own apartment. Six characters. Social media cutaways. Ends: six hands. No meeting shown.'}, {code:'S03E04', title:'The Forest', season:'S3', status:'draft', scriptKey:'S03E04', desc:'Walter hiking a real Vancouver trail. Talking to the forest — which is himself. Cannot see the forest for the trees. Realizes he was always where he was. Last line caught by camera, not performed for it.'}, {code:'S03E05', title:'At The Water', season:'S3', status:'draft', scriptKey:'S03E05', desc:'Shoreline. Walter with the notebook. The system fires in real time. Micro-failure. The decision. Notebook dead in his hand. He never goes to page one. That absence is present.'}, {code:'S03E06', title:'Your Time Is Important To Us', season:'S3', status:'draft', scriptKey:'S03E06', desc:'A park. High vis. Walkie talkie. 47 minutes on hold with Telus about mobile data. Walter does every radio voice. Kyle is the only other real person. Turn it off and on again.'}, {code:'S03E07', title:'Untitled', season:'S3', status:'planning', scriptKey:'', desc:'Behavior after awareness. Placeholder.'}, {code:'S03E08', title:'Untitled', season:'S3', status:'planning', scriptKey:'', desc:'Behavior after awareness. Placeholder.'}, {code:'S03E09', title:'Untitled', season:'S3', status:'planning', scriptKey:'', desc:'Behavior after awareness. Placeholder.'}, {code:'S03E10', title:'Untitled', season:'S3', status:'planning', scriptKey:'', desc:'Behavior after awareness. Placeholder.'}, {code:'S03FIN', title:'The Final Mirror', season:'S3 Finale', status:'finale', scriptKey:'S03FIN', desc:'Break Room Roast Cut + Accusation Spine. Hat burn. Series end.'} ]; WHGA_S3_EPISODES.forEach(ep => { if (!EPISODES.some(e => e.code === ep.code)) EPISODES.push(ep); }); Object.assign(SCRIPTS, { 'S03E01': `WALTER HAS GONE AWAY Season 3 — Episode 1 "I GUESS THAT'S ALRIGHT" Runtime target: 8 minutes Format: Single location. No internal characters. No Interface. No committee. Just Walter. ============================================================ VISUAL LANGUAGE / DIRECTOR'S NOTE ============================================================ No rooftop. No pier. No dramatic cityscape chosen for its symbolism. A park bench on a balcony. Bought from a set sale. Still looks like it belongs somewhere else. That's fine. So does he. The city is always audible. It never stops. It was never going to stop. The bass guitar is a Warwick Rockbass Streamer through a Boss GX-10. The line is not composed. Whatever comes out that day is the score. No internal characters. No Shadow Walter watching from a corner. No Prosecutor with a clipboard. No Parasite with a theory. Just Walter. And the city. And the bass. And eventually — the door. ============================================================ COLD OPEN ============================================================ BLACK. City noise. Not establishing. Already mid-stream. Like it's been going this whole time and we just tuned in. Traffic. Someone's music two floors down. A dog. Wind through whatever gap exists between this building and the next. No score yet. No bass yet. Just the city doing what the city does which is continue regardless. FADE UP SLOWLY TO: ============================================================ EXT. BALCONY — DAY — WIDE ============================================================ The bench. It's a park bench. The kind with the slats and the armrests and the specific weight of something that used to be somewhere public. Walter is already on it. We don't see him arrive. He's just there. Like he's been there a while and lost track of how long. Bass guitar across his lap. Not playing yet. Just holding it the way you hold something familiar when you don't know what else to do with your hands. He's looking out. Not at anything specific. Just — out. The city below. The sky above. The particular quality of a day that is not good or bad but simply present. He starts playing. No preamble. No tuning. No adjustment. Just — a bass line. Low. Slow. Not sad. Not happy. Just moving. The way water moves when nothing is disturbing it. The city competes immediately. It was always going to. ============================================================ ACT ONE — THE QUIET THAT ISN'T QUIET (0:00 — 2:30) ============================================================ Walter plays. The city plays. Neither wins yet. WALTER (V.O.) I didn't know what to do with a Tuesday. (beat) That sounds like a joke. It isn't. (beat) My whole life I have known exactly what to do with a Tuesday. You survive it. You get through it. You manage whatever Tuesday decided to throw at you and you come out the other side and you call that living. (beat) Turns out that's not living. (beat) That's just — a very efficient relationship with Tuesdays. (beat) And now it's Tuesday. And nothing is on fire. And I'm sitting on a bench I bought from a set sale on my balcony playing bass at — (checks nothing, looks at sky) — I genuinely don't know what time it is. (beat) And that's — (beat) That's new. The bass line continues. Unhurried. WALTER (V.O.) You spend enough time in a volcano and your whole body learns to run hot. (beat) Your nervous system recalibrates. Danger becomes the baseline. Noise becomes the baseline. The specific weight of something always being wrong becomes the baseline. (beat) And then one day — (beat) it isn't. (beat) And your body doesn't know what to do with that. (beat) It's not relief. (beat) Relief implies you knew the weight was there. (beat) This is more like — (long pause, playing) —putting down something heavy you forgot you were carrying and then standing there feeling weirdly light and not entirely trusting it. ============================================================ ACT TWO — THE REACH (2:30 — 4:30) ============================================================ Walter stops playing. Not dramatically. Just — stops. He looks at his phone. Picks it up. Puts it down. Picks up the bass again. Plays four bars. Stops. Looks out at the city. WALTER (V.O.) Here's the thing nobody tells you about getting to the other side of the thing. (beat) There's still a you on the other side. (beat) And that you has all the same hands. All the same habits. All the same reflexes for reaching. (beat) I reach for my phone. I reach for a project. I reach for the next thing to build. (beat) And I'm not sure yet — (beat) and I want to be honest about not being sure — (beat) whether I'm reaching because I genuinely want the thing (beat) or because my hands don't know what to do when they're not fighting something. (beat) Both, probably. (beat) If I'm being accurate — which, apparently, is the only thing I know how to be now — (beat) both. He picks up the bass again. Different line this time. Still low. Still unhurried. But searching slightly. Like a question instead of a statement. The city gets incrementally louder. Or maybe he just notices it more now that he stopped playing. WALTER (V.O.) I'm building things. (beat) The show. The music. The business. The stuff I always said I'd do when I had the bandwidth. (beat) Turns out the bandwidth was always there. I was just using it to manage the chaos. (beat) So now I'm here. Bandwidth available. Hands ready. And I'm building. (beat) Is it real? (beat) I think so. (beat) I enjoy it. I actually enjoy it. Which is — (beat) which is genuinely strange to say out loud. (beat) Enjoying something without it being attached to surviving something. (beat) Novel concept. Apparently. (half laugh — real, not performed) ============================================================ ACT THREE — THE CITY GETS LOUDER (4:30 — 6:30) ============================================================ Walter is playing again. The bass line has shifted. Still low. But there's something underneath it now. Not tension. More like — awareness. The city is louder now. Noticeably. The mix is tilting. Traffic. Someone arguing somewhere. A delivery truck. The specific percussion of a city that does not care that you figured something out. WALTER (V.O.) The city was always this loud. (beat) I just used to be louder. (beat) I used to generate enough internal noise that the external stuff couldn't compete. Drama. Chaos. The specific volume of a life lived at emergency frequency. (beat) And now — (beat) I can hear the city. (beat) All of it. All the time. Every layer. (beat) I'm not sure yet whether that's progress or just — (beat) a different kind of loud. The bass and the city are competing fully now. Neither polite about it. WALTER (V.O.) I've been thinking about what it means to actually be okay. (beat) Not performing okay. Not formatting okay into something more palatable than it is. (beat) Actually okay. (beat) Because here's the thing — (beat) I don't have a reference point for actually okay. (beat) I have a reference point for surviving. For getting through. For white-knuckling it to the other side of whatever the current thing is. (beat) I don't have a reference point for — (looks around at the balcony, the bench, the ordinary day) —this. (beat) A bench. A bass. A Tuesday. Money I don't have enough of but am dealing with better than I used to. (beat) Stress that exists but isn't running the show. (beat) A life that is — (long pause) —genuinely not bad. (beat) And me not entirely knowing what to do with genuinely not bad. ============================================================ ACT FOUR — THE DOOR (6:30 — 8:00) ============================================================ Walter stops playing. This time he puts the bass down. Leans it against the bench. Leans back. The city is almost uncomfortable now. The mix has fully tilted. Traffic and voices and urban machinery filling every available space. He sits in it. Doesn't fight it. Doesn't fill it. Just — sits. This is the moment. Not cinematic. Not dramatic. Just a man on a park bench on a balcony letting the city be loud and not generating noise to compete with it. He stays there longer than comfortable. WALTER (V.O.) I don't know if I've made it. (beat) I don't think that's the right question anymore. (beat) I think the question is — (beat) can I live here? (beat) In this. (beat) Not in the chaos. Not in the emergency. Not in the very compelling story of a man who is always just about to figure it out. (beat) Here. (beat) Bench. Bass. Tuesday. City that doesn't care. Life that is — (beat) alright. (beat) Actually alright. (beat) Not fixed. Not finished. Not some version of healed that requires a framework to explain. (beat) Just — (long pause) alright. Walter looks out one more time. At the city. At the sky. At whatever is out there that was always out there and will keep being out there regardless. He picks up the bass. Plays one more line. Four bars. Then stops. He stands. Picks up the bass. Goes inside. Closes the door. The city continues. Muffled now but present. It was never going to stop. And then — silence. Not peaceful silence. A silence with a high noise floor. The kind that hums. The kind that reminds you that silence is just the absence of the specific thing that was covering it. We hold on the closed door. From outside. The city still going behind us. WALTER (V.O.) (from inside, barely audible through the door) I guess that's alright. The hum holds. CUT TO BLACK. Hum continues over black. Fades. But slowly. Slower than comfortable. Then gone. ============================================================ END OF S3E01 ============================================================ WALTER HAS GONE AWAY Season 3 — Episode 1 "I Guess That's Alright" Next: Episode 2 `, 'S03E02': `WALTER HAS GONE AWAY Season 3 — Episode 2 "THE EDGE OF THE MAP" Draft 2 Runtime target: 8-10 minutes Format: Single camera. Night walk. Downtown Vancouver. No internal characters. No Interface. No committee. No callbacks to specific S1/S2 material. Just Walter. Mid-thought. Already inside it. ============================================================ VISUAL LANGUAGE / DIRECTOR'S NOTE ============================================================ Night. Downtown Vancouver. Not chosen for symbolism. Just — downtown. The city that keeps going regardless of what you're working out inside it. Walter is already walking when we find him. Already mid-thought. Not arriving at the question. Already inside it. Camera is handheld. Not dramatically. Not shaky for effect. Just held. Following. The specific intimacy of one human pointing a camera at another who is trying to say something true. At the end of the episode Walter sets the camera on the ground. Still running. And walks away. The camera stays. The city continues around it. Nobody picks it up. Nobody explains it. For the first time in three seasons something happens that the camera doesn't follow. ============================================================ COLD OPEN ============================================================ BLACK. City at night. Traffic. Voices. The specific hum of a place that has no idea what you're working through and wouldn't care if it did. FADE UP TO: ============================================================ EXT. DOWNTOWN VANCOUVER — NIGHT — MOVING ============================================================ Walter walking. Already talking. Not looking at the camera. Just moving and thinking out loud the way you do when the question won't sit still. WALTER I want to ask you something. (beat) Not you specifically. (gestures vaguely — at the camera, the city, everyone) Anyone who has ever been completely certain about something and been completely wrong. (beat) How do you know when to trust yourself again? (beat) Like — actually trust. Not perform trust. Not decide to trust because the alternative is spending the rest of your life alone and technically correct. (beat) Actually trust. (beat) How do you do that? He keeps walking. The city keeps going around him. ============================================================ ACT ONE — THE MECHANICAL PROBLEM (0:00 — 3:00) ============================================================ WALTER Here is the problem as I understand it. (beat) You go through something. You do the work. You learn the thing. (beat) And the thing you learn — legitimately learn, not perform learning, actually integrate — (beat) is: watch. Wait. Don't go just because you're pointed. Look first. (beat) Okay. Good. Correct. Earned. (beat) And then someone shows up. (beat) And they're — actually good. (beat) And the thing you learned that is correct and earned goes: watch. Wait. Look first. (beat) And you also know — because you've been on the other side of it — what it costs someone to be looked at instead of seen. (beat) What it costs someone to be with a person who is technically present but running a background audit on everything they say and do. (beat) You know what that feels like from the inside. (beat) So the tool you built to protect yourself is also capable of becoming the thing you built it to protect yourself from. He stops at a corner. Waits for the light. WALTER (CONT'D) That's the catch 22. (beat) And it doesn't resolve. (beat) I'm not looking for it to resolve. (beat) I'm just saying it out loud because I don't think anyone says it out loud. Light changes. He crosses. ============================================================ ACT TWO — THE ACTUAL FEAR (3:00 — 5:30) ============================================================ WALTER Here's the actual fear. (beat) Not — is this real. Not — can I trust this person. (beat) Those are the questions that sound right but aren't the right questions. (beat) The actual fear is: (beat) can I trust myself to know the difference. (beat) Because I've been wrong before. (beat) Not slightly wrong. Not forgivably wrong. (beat) Wrong in ways that should have been obvious. (beat) And they weren't obvious. (beat) To me. (beat) So what makes me think my read is any good now? (beat) What makes me think I've calibrated correctly and I'm not just — (beat) doing the same thing with better vocabulary and a slightly more sophisticated set of reasons for why this time is different? He walks. People pass him. A couple. Someone on their phone. Someone eating something while walking somewhere. All of it continuing. WALTER (CONT'D) And here's the thing that keeps stopping me when I get to that place. (beat) The background noise. (beat) There's a thing that runs. Has always run. In every room. In every conversation. With every person. (beat) A low-level scan. Looking for the thing that's about to go wrong. (beat) It's not paranoia. It's not anxiety in the clinical sense. (beat) It's just — a system that learned early that things go wrong and decided the best strategy was to see it coming. (beat) That system runs at about fifty percent constantly. (beat) Has my whole life. (beat) And then — (long pause) It dropped. (beat) Not because I decided it should. Not because I told myself to relax and be open and trust the process or any of the other things that sound right and mean nothing. (beat) It just — dropped. (beat) Without my permission. (beat) And I noticed it the way you notice when something that's always been humming suddenly stops. (beat) In the quiet after. ============================================================ ACT THREE — WHAT ACTUALLY DIFFERENT FEELS LIKE (5:30 — 7:30) ============================================================ WALTER I don't have a reference point for what normal is supposed to feel like. (beat) I want to be honest about that. (beat) Every relationship I've been in — and I say this without drama, just as information — (beat) has been two people whose specific damage fit each other in a specific way. (beat) And that fit feels like something. It feels like recognition. It feels like — oh, you speak this language too. (beat) And it is a kind of love. I'm not saying it isn't. (beat) It's just not — (beat) it's not this. He slows. Not stopping. Just present for a second. WALTER (CONT'D) This feels boring. (beat) And I mean that as the highest possible compliment. (beat) Not boring like empty. Boring like — no performance required. (beat) No reading of the room. No adjusting myself to the temperature of someone else's mood and hoping I got it right. (beat) Just — two people in a room who don't need anything from each other in that specific moment except to be in the room. (beat) I didn't know that was available. (beat) I genuinely did not know that was a thing. (half laugh — real, not performed) (beat) And she knew me before. (beat) Not before the show. Before all of it. Before I had language for any of this. (beat) She has a version of me from before the renovation. (beat) And she showed up after. Looked at the current version. And went — (beat) yeah. This tracks. (beat) I don't know what to do with that. (beat) I mean I do. (beat) I just haven't done it before. ============================================================ ACT FOUR — THE EDGE OF THE MAP (7:30 — 9:00) ============================================================ Walter has been walking this whole time. He's somewhere in downtown now. Not a landmark. Just a street. Ordinary. He stops. Not at anything significant. Just — stops. Looks around at the city. Then at the camera. WALTER Here's where the map runs out. (beat) Everything I've learned — every pattern I've named, every thing I've earned the hard way — (beat) gets me to here. (beat) And here is: (beat) I don't know. (beat) I don't know if I'm reading it right. I don't know if the version of me that learned to watch for things is capable of accurately reading the absence of those things. (beat) I don't know. (beat) And I'm going anyway. (beat) Not blindly. (beat) With everything I know. With the system running. With my eyes open. (beat) And still — (beat) going. (beat) Because the alternative is staying on the known side of the map and calling it wisdom. (beat) And I've met that version of myself. (beat) He's not coming back. He looks at the camera. One beat. Two. Then he bends down. Sets the camera on the ground. Still running. We see him from ground level now. His feet. His legs. The city behind him. All of it enormous from down here. He straightens. And walks. Not running. Not dramatic. Just — forward. Into the city. Into the thing without a map. The camera stays on the ground. Running. People walk past it. A pair of shoes. A skateboard. Someone who almost kicks it and doesn't. Nobody picks it up. Nobody explains it. The city continues. Walter keeps walking until he's gone. WALTER (V.O.) (from somewhere the camera can no longer reach) When you're willing to build something not out of emergency — something out of something you don't know — it's the most terrifying thing. (beat) But it might also be the most beautiful thing you've ever experienced. The camera stays on the ground. The city continues around it. Thirty seconds. Forty. The hum of everything still going. CUT TO BLACK. ============================================================ END OF S3E02 ============================================================ WALTER HAS GONE AWAY Season 3 — Episode 2 "The Edge of the Map" Next: Episode 3 `, 'S03E03': `WALTER HAS GONE AWAY Season 3 — Episode TBD "I CAN HEAL FOR REALZ" Runtime target: 10–12 minutes Format: Documentary briefing. Walter to camera while setting up. Six character introductions. Social media cutaways. Ends at the start of the meeting. No resolution. No catharsis. Just six hands. ============================================================ VISUAL LANGUAGE / DIRECTOR'S NOTE ============================================================ Walter's apartment. Pre-meeting. He is setting up. The camera is watching him set up. He is aware of the camera. He has decided to use this time constructively. He is going to tell us exactly who is coming tonight. Before they get here. So we know. So when the door buzzes we already know. This is the episode. Not the meeting. The briefing before the meeting. VO register: Kill Tony. 80% merciless. 20% surgical. No warmth. No advice. No lesson. Just accurate. ============================================================ COLD OPEN ============================================================ Walter's apartment. 6:15pm. Walter is moving his couch. He stops. Looks at the camera. WALTER Jonas Blasektoski called me four days ago at 11:17pm. He moves the couch a little more. Stops again. WALTER Jonas runs a support group called I Can Heal for Realz. He needed someone to facilitate tonight's session. He is in Hawaii. Walter picks up three folding chairs leaning against the wall. He starts arranging them. WALTER He asked if I had space. I said I had my apartment. He said perfect. I said how many people. He said six. Walter looks at the three chairs. Looks at the camera. WALTER I have three folding chairs. (beat) I'm going to need to find something else for the other three. He looks around the apartment. Pulls a chair from his desk. An ottoman. A barstool from the kitchen counter. He looks at the arrangement. Three folding chairs. A desk chair. An ottoman. A barstool. WALTER That's fine. (it is not fine) He looks at the camera. WALTER Jonas sent me a PDF. Three pages. It arrived at 1am. It had a section called "Holding the Container." I printed it at Staples. He holds up the clipboard. A single rectangle drawn on it. WALTER I'm going to tell you about the six people who are coming to my apartment tonight. In forty-five minutes. To talk about narcissists. (beat) You should know what you're looking at before they get here. He picks up the candle from his coffee table. Looks at it. Writes "Clarity" on a sticky note. Sticks it on the candle. Straightens it. It's still slightly crooked. He leaves it. WALTER Let's start with Brenda. ============================================================ INTRODUCTION 1 — BRENDA ============================================================ Walter is folding the throw blanket on his couch with the specific energy of a man who knows someone is going to look at his couch. WALTER Brenda found the group through Facebook. A group called "Survivors of Emotional Unavailability and Related Crimes." (beat) She has been in four support groups in two years. She left all of them. He smooths the blanket. WALTER The first one the facilitator "wasn't holding space correctly." The second one someone else's story "took up too much room." The third one she doesn't talk about. The fourth one is tonight. (beat) Brenda is going to arrive early. I know this because Brenda texted me to confirm the address and then texted me again to ask about parking and then texted me again to say she found parking and then texted me again to ask if there was anywhere closer. He checks his phone. WALTER She just texted to say she's five minutes away. (beat) It's 6:17. CUT TO: BRENDA'S FACEBOOK PAGE. Profile picture: a sunset with a quote over it. The quote says "She was a wildfire they mistook for a candle." She did not write this quote. She shared it. Most recent post: a photo of crystals arranged on a windowsill. Caption: "Protecting my energy this week 🔮 Some people will never understand what they lost. And that's their healing journey, not mine. Sending love to everyone who knows. You know who you are. 💛" 47 comments. All of them variations of "queen 👑" and "their loss, your gain 🙌" WALTER V.O. Brenda has been protecting her energy for approximately seven years. Her energy is fine. Her energy has never been in danger. Brenda is the danger. Not in a villain way. In a very specific way where she walks into rooms and within eleven minutes the room is about Brenda. She doesn't do this on purpose. That's what makes it impressive. CUT BACK TO: Walter placing the candle on the coffee table. Looking at the sticky note. Leaving it. WALTER She's going to want to go first tonight. She's always going to want to go first. If you put Brenda in a coma she would find a way to wake up first. ============================================================ INTRODUCTION 2 — SEBASTIAN ============================================================ Walter is in the kitchen making a list on his phone of things the PDF said he needs. Tissues. Check. He has tissues. A "grounding object." He picks up a rock from a small pile of rocks on his windowsill. He holds it up to the camera. WALTER Sebastian. He puts the rock on the coffee table. WALTER Sebastian has been to therapy. Sebastian has read every book. Sebastian did the retreat. Then did the retreat again because the first one didn't go deep enough. (beat) He texted me to ask about parking. He found parking immediately. He texted to let me know he found parking. He included the parking app rate because he thought I might find that useful. I did not find that useful. He opens a drawer looking for something. WALTER Sebastian is coming tonight not because he needs the group. He's past needing it. He's coming because he feels called to witness others' healing. His therapist Renata — he calls her Renata — suggested he spend time with peers. (beat) Sebastian does not have peers. Sebastian has students he hasn't enrolled yet. CUT TO: SEBASTIAN'S INSTAGRAM. Bio: "Doing the work 🌿 | Trauma-informed | Somatic practitioner in training | Retreat survivor (twice) | He/him" Most recent post: a photo of him meditating on a dock. Golden hour. Perfect light. Caption: "Stillness is not the absence of feeling. It's the presence of you. — S" The "— S" is doing a lot of work. Previous post: a photo of a book called "The Body Keeps the Score" on a wooden table next to a coffee. Caption: "Currently: integrating. 🍃 This book changed everything. Has anyone else found that the second read hits completely differently? I'm on my third." WALTER V.O. Sebastian is on his third read of a book about trauma because he has processed the trauma so thoroughly that the trauma has become a hobby. He is going to come to my apartment tonight and be the most healed person in the room. That is not a thing healed people do. Healed people don't need to be the most healed person in the room. Sebastian needs this the way Brenda needs to go first. Different trophy. Same shelf. CUT BACK TO: Walter looking at the seating arrangement. The barstool is noticeably higher than everything else. He considers moving it. He leaves it. WALTER He's going to mention the retreat before I've finished saying welcome. I would bet actual money on this. ============================================================ INTRODUCTION 3 — TYLER ============================================================ Walter is at the TV tray near the door arranging the name tags Jonas mailed him. They arrived today, still creased. Walter smooths them out one by one. He picks up Tyler's. Looks at it. WALTER Tyler. He sets it down. WALTER Tyler is charming. That's the whole thing. That's the entire Tyler situation. He is charming in a way that makes you give him the benefit of the doubt slightly longer than you should and then by the time you've stopped giving it you've already forgiven him for the thing that made you stop. He arranges the remaining name tags. WALTER He texted me to ask if he could bring drinks. I said this was probably a support group so maybe not. He said "ha yeah fair enough man." (beat) He's bringing drinks. I know he's bringing drinks. He knows I know. He's going to put them in my fridge and look at me while he does it and smile and I'm going to say nothing because that's the Tyler thing. That's the whole Tyler thing. CUT TO: TYLER'S INSTAGRAM. Highlight reel energy. Photos of him laughing at parties. Doing something outdoorsy. Wearing the jacket. Always the jacket. Most recent post: a black and white photo of him looking out a window. Very considered. Caption: "Learning to sit with it. 🖤 Growth isn't linear but the view helps." 312 likes. Scroll down two posts: A photo of him and a woman — Jade — at a restaurant. They look happy. He's tagged her. She's untagged herself. The tag is still visible as a blank. He hasn't re-captioned. WALTER V.O. Tyler's ex Jade untagged herself from that photo eight months ago. He hasn't taken the post down. He hasn't re-captioned it. The blank where her tag was is doing more work than anything Tyler has said in this room and Tyler hasn't walked in yet. CUT BACK TO: Walter holding the "YOUR TRUTH IS VALID" handout. Reading it. Putting it down. WALTER Tyler's going to make everyone laugh tonight. Then say something alarming. Then make everyone laugh again before the alarming thing fully lands. He's very good at this. It's an extremely transferable skill. Jade is familiar with it. ============================================================ INTRODUCTION 4 — MORGAN ============================================================ Walter is straightening the sticky note on the candle. It is still slightly crooked. He steps back. Looks at it. Leaves it. He turns to the camera. WALTER Morgan. (beat) Morgan has been to I Can Heal for Realz before. (beat) Three times. (longer beat) Under different names the first two times. He lets that sit. WALTER Real name this time. She texted me to confirm the address. That's it. One text. Confirmed. That's all she needed. He adjusts one of the folding chairs slightly. WALTER She's bringing her own tea. In a thermos. She mentioned this in the text. Not as a question. Just as information. "I'll bring my own tea." (beat) I don't know what that means. I've been thinking about it since yesterday. CUT TO: MORGAN'S SOCIAL MEDIA. Walter stares at the camera. Nothing comes up. No Instagram. No Facebook. No LinkedIn. Nothing. Walter holds the beat. WALTER V.O. Morgan has no social media. None. Not private. Not deactivated. None. I looked. I looked for longer than I'm going to tell you. (beat) That's her introduction. CUT BACK TO: Walter looking at the seating arrangement again. The ottoman. The barstool. The desk chair. The three folding chairs. WALTER She's going to take one of the lower seats. I think she's going to choose it on purpose. I don't know why yet. I'll figure it out during the meeting and I'm going to wish I hadn't. ============================================================ INTRODUCTION 5 — BLAKE ============================================================ Walter is at the kitchen counter filling a small bowl with mints. He bought them specifically for this. WALTER Blake has forty-seven thousand followers on Instagram. His content is about healing. Specifically his healing. From his relationship with a woman named Priya. He puts the mint bowl on the TV tray. WALTER He hasn't named her by name in his content. He's described her in enough detail that Priya's friends all know it's her and have sent her the links and Priya's lawyer has been in contact. (beat) Blake texted me three times to ask about the lighting situation in my apartment. He looks around at his lighting. WALTER I have lamps. (beat) He asked if I had "natural light options." I have a window. I told him I have a window. He said "perfect." He didn't ask about anything else. Not the agenda. Not what to expect. Just the lighting. CUT TO: BLAKE'S INSTAGRAM. 47.2k followers. Bio: "Healing out loud 🌱 | Survivor | Creator | Your story matters" Pinned post: a video of him talking to camera. Thumbnail shows him mid-sentence, eyes slightly red, looking earnest. Caption: "I wasn't going to post this. But healing happens in community. And if one person watches this and feels less alone — it was worth it. This one's for you. 💛 #HealingJourney #NarcissisticAbuse #YouAreNotAlone" 2.3 million views. Comment pinned by Blake: "For everyone asking — no I won't be naming her. This isn't about blame. This is about healing. 🙏" Below that, a reply from an account called priya_is_fine: "Hi. It's Priya. Please stop. My lawyer's name is in your DMs. — P" Blake has replied with a heart emoji. WALTER V.O. Blake replied to Priya's lawyer comment with a heart emoji. A heart emoji. He saw "my lawyer's name is in your DMs" and responded with a heart emoji. I need you to understand that Blake is not doing this cynically. Blake genuinely believes the heart emoji was the right move. Blake believes the heart emoji communicated warmth and healing in a difficult moment. Blake is going to come to my apartment tonight and film it and it is going to get forty thousand views and Jonas is going to comment "🙏🏽" from Hawaii and I am going to appear in the background next to a candle that isn't called Clarity. CUT BACK TO: Walter placing the mint bowl on the TV tray. Looking at it. WALTER He's going to film tonight. He's going to try to be subtle about it. He's not going to be subtle about it. I'm going to say nothing. I know I'm going to say nothing. That's already decided. ============================================================ INTRODUCTION 6 — CASSANDRA ============================================================ Walter checks his phone. He has a text from Jonas. From three days ago. He hasn't replied. He puts the phone in his pocket. He straightens the handouts on the TV tray. WALTER Cassandra and Ryan broke up eleven weeks ago. (beat) This is the fourth final breakup. (beat) Ryan texted her this afternoon. The text said "hey." She told me this when she texted to confirm tonight. She texted the address confirmation and then she texted: "also my ex texted me today, just 'hey', should I come still or" (beat) I said yes come. She said ok. She said "I'm not going to text him back." (beat) She's going to text him back. He adjusts the last folding chair slightly. WALTER She got dressed up tonight. Not for the group. She told me she had something before this that ran late. There was no something before this. She got dressed up and came to the group. (beat) She's going to check her phone a lot. She's going to say she's completely done. She's going to mean it in the way you mean something that you know you're not going to do. CUT TO: CASSANDRA'S INSTAGRAM. Most recent post: a sunset photo. Three weeks ago. Caption: "new chapter 🌱" Scroll through her following list. Ryan is in there. Ryan follows her back. Go to Ryan's page. Find the sunset post. Ryan has liked it. Ryan liked it four minutes after she posted it. That's all. That's the whole Ryan situation. Ryan liked it in four minutes. WALTER V.O. Ryan liked that photo in four minutes. At 11:43pm. Three weeks ago. She posted it at 11:39pm. Ryan was awake at 11:39pm and saw her post and liked it in four minutes. Ryan is not done. Cassandra is not done. The loop is not done. The loop has not been done for approximately two years across four final breakups and Ryan liking a sunset photo called "new chapter" in four minutes at 11:43pm. (beat) The Loneliness Company could not have designed this more efficiently if they tried. CUT BACK TO: Walter standing in the middle of his apartment. Looking at the arrangement. Three folding chairs. A desk chair. An ottoman. A barstool. The TV tray with name tags and mints. The monitor turned around. The Clarity candle. He looks at the camera. WALTER That's all six. (beat) Jonas asked me to "hold the container" tonight. I've been thinking about what that means. I think it means sit there while six people describe themselves without knowing that's what they're doing. (beat) Jonas is in Hawaii. (beat) Jonas started a healing group for people recovering from narcissists. Jonas named it after his own healing. Jonas put the meeting in my apartment. Jonas went to Hawaii. (beat) I've known what Jonas is for two years. He picks up the clipboard. The rectangle is still the only thing on it. WALTER Two years. His buzzer goes. He looks at the intercom. Looks at the camera. WALTER That's Brenda. She's twelve minutes early. He buzzes her in. Goes to the door. Turns back to the camera. WALTER (quietly, like he's deciding something) I'm not going to text Jonas back tonight. (beat) Okay. He opens the door. ============================================================ THE MEETING BEGINS ============================================================ 6:58pm. All six are seated. Three folding chairs. A desk chair. An ottoman. A barstool. Nobody has commented on the barstool. Everyone has noticed the barstool. Blake's phone is already at an angle. Cassandra's is already face-up on her knee. Sebastian has already mentioned the retreat. Walter owes himself a dollar. Brenda is sitting with her tote bag taking up slightly more floor space than necessary. Morgan is holding her thermos. She found his mugs on the first try. He's still thinking about that. Tyler put something in the fridge. Walter didn't say anything. Tyler knew Walter wouldn't say anything. Walter stands at the monitor. Looks at his clipboard. The rectangle. WALTER Okay. (beat) Welcome to I Can Heal for Realz. (beat) I'm Walter. I'm filling in for Jonas tonight. Jonas sends his regards. (nobody asks about Jonas) (Walter had expected someone to ask about Jonas) WALTER So. We're going to share our truths tonight. In a — (checks clipboard) — safe container. (looks up) And honour our journey. (beat) So. He looks at the six of them. WALTER Who wants to start? Six hands go up. Simultaneously. All six. Walter looks at the hands. The hands are up. CUT TO BLACK. ============================================================ POST CREDITS ============================================================ Jonas's Instagram story. Beach. Sunset. Cocktail. Feet in sand. Caption: "Healing looks different for everyone 🌊 Mine looks like this today. Your journey is valid wherever you are. 💛 #ICanHealForRealz #HealingJourney #Blaze" 47,000 likes. Blake has commented: "This is so beautiful man 🙏 collab when u back?" Jonas replied: 🔥 Below that: priya_is_fine: "Hi Jonas. This is Priya. Please tell Blake to remove the videos. My lawyer has asked nicely. We are done asking nicely." Jonas has not replied. The heart emoji he sent last week is still there. Just above Priya's comment. Sitting there. A heart emoji. Above a lawyer. CUT TO BLACK. ============================================================ LC BLEED-THROUGH ============================================================ During the introductions. After Morgan's. The moment Walter says "That's her introduction." One second. No warning. THE LONELINESS COMPANY Internal Memo Narcissistic Ecosystem Division — Q3 "Subject W has agreed to facilitate. Asset J. Blasektoski performing as expected. The container is Subject W's apartment. Subject W set up the chairs. Subject W bought the mints. Subject W printed the handouts. This required no intervention from us. Note: Subject W volatility continues to decline. Recommend monitoring. Asset Blasektoski flagged for redeployment. The loop requires no maintenance. It only requires the right person to say yes." FOOTER: The Loneliness Company — a Shaazadon subsidiary CUT BACK. Walter is still talking. He didn't notice. Or he noticed and has nothing to add. Both are equally possible. ============================================================ END OF EPISODE ============================================================ WALTER HAS GONE AWAY Season 3 — "I Can Heal for Realz" The Hollow Crown Rebellion "The loop requires no maintenance. It only requires the right person to say yes." `, 'S03E04': `WALTER HAS GONE AWAY Season 3 — Episode 4 "THE FOREST" Runtime target: 8–10 minutes Format: Single location. Real trail. Real forest. Vancouver. Walter walking. Talking to the forest. The camera catches it. ============================================================ VISUAL LANGUAGE / DIRECTOR'S NOTE ============================================================ A real trail. A real forest. Somewhere in Vancouver where the trees are tall enough that you can't see past them and the city disappears completely within about forty seconds of walking in. Walter is already walking when we find him. He is not looking at the forest. He is looking at his feet. At the path. At the next thing. The camera sees the forest. Walter doesn't. Not yet. This is not a cinematic forest. No golden hour light through the canopy. No dramatic mist. Just trees. A lot of trees. Right there. The whole time. Walter is talking. He is not talking to us. He is talking to himself. The camera is close enough to hear it. That's the episode. No internal characters. No Interface. No committee. Just Walter and the trees he isn't looking at. ============================================================ COLD OPEN ============================================================ BLACK. Forest sounds. Wind through trees. Birds. Feet on a trail. No score. Just the forest doing what forests do which is exist completely indifferent to whether or not you notice them. FADE UP TO: EXT. FOREST TRAIL — DAY Walter walking. Already mid-thought. Looking at his feet. The trees are right there. He doesn't look at them. WALTER There's this thing that happens when you stop. (beat) Not stop stop. Not a decision. Just — the thing you were doing isn't there anymore. And you didn't plan for that. And your whole operating system was built around the doing and now the doing is gone and you're just — (beat) standing there. (beat) Or in my case walking. Around my apartment. For six months. Which is a specific kind of standing there. He navigates a root in the path. Looks back down at his feet. WALTER Six months off. Not by choice. Well. By choice in the way that the industry slows down and you choose to keep your integrity instead of taking the thing that isn't right for you and then the right thing takes longer than expected and suddenly it's been six months and you're walking around your apartment wondering if the version of you that knew how to do things was a character you played or actually you. ============================================================ ACT ONE — THE SIX MONTHS (0:00 — 3:00) ============================================================ Walter keeps walking. Still not looking at the trees. The trees are everywhere. WALTER Here's what six months off teaches you if you let it: (beat) Nothing. (beat) It teaches you nothing if you spend the whole time waiting for it to be over. Which I did. Mostly. He steps over something. Keeps going. WALTER I didn't know how to be not working. Not in a workaholic way. Not "I just love the hustle." In a more specific way. In a — I built my entire sense of who I am around what I do and when what I do wasn't happening I couldn't find the other thing. (beat) The other thing that's just — me. Without the doing. (beat) And here's the embarrassing part. He slows slightly. WALTER I went looking for it in all the wrong places. I went looking for it in whether people thought well of me. In whether the relationship was working. In whether I was being productive enough about not being productive. In whether I was processing correctly. In whether my processing was going to eventually produce something that justified the time spent processing. (beat — almost a laugh) I was productively procrastinating my own identity. I built a whole architecture to avoid noticing that I didn't know who I was without something to show for it. The camera catches him shaking his head. Small. At the ground. WALTER That's embarrassing. (beat) That's accurate. ============================================================ ACT TWO — THE SET (3:00 — 6:00) ============================================================ Walter is walking faster now. Not urgently. Just with more forward motion. Like the talking is moving his legs. WALTER And then I went back to work. (beat) First day back on set. Proper set. Real budget. Real crew. Real stakes. And something happened that I wasn't expecting. He stops for a second. Not dramatically. Just pausing mid-thought. WALTER It was just there. (beat) Not like it came back. Not like I rebuilt it. Just — there. The way you left your keys somewhere and looked everywhere for them and then looked in the first place you should have looked and they were just there. Sitting there. Not lost. Just somewhere you weren't looking. He starts walking again. WALTER The way my brain works on a set. The specific way I read a room. How I talk to people. How I solve the thing before it becomes the thing. How I make the day work when the day is trying very hard not to work. (beat) That guy. (beat) I looked around on that first day back and I was like — oh. (beat) There he is. (beat) That guy is me. That guy has always been me. That guy was me during the six months. That guy was me during the relationships. That guy was me the whole time. He navigates another part of the path. Still not looking up. Still talking to the ground. To himself. To whatever is in front of him. WALTER I just wasn't looking at him. (beat) I was looking for permission. (beat) I don't know who I was waiting for. That's the thing. I genuinely couldn't tell you. There was no specific person I needed to greenlight me. There was no authority I was waiting on. (beat) I was just waiting. (beat) In the specific way you wait when you've spent so long living around other people's needs that you forget you're allowed to just — be the thing you are. Without asking. Without checking. Without making sure everyone's comfortable with it first. The camera catches his face. He's not performing this. He's working it out while he walks. WALTER Confidence isn't the word. Everyone uses confidence. It's not confidence. (beat) It's more like — permission. That you give yourself. That you forgot you could give yourself. That you kept looking for in other people's faces and other people's reactions and whether they approved and whether they stayed and whether the approval meant you were actually good or just good at making people think you were good. (beat) And the whole time — He stops again. WALTER The whole time it was just — He looks up. For the first time. ============================================================ ACT THREE — THE FOREST (6:00 — 8:30) ============================================================ He looks up. And the forest is right there. It was always right there. Trees in every direction. Tall. Still. Completely indifferent. He looks at it for a moment. Not dramatically. Just — oh. WALTER (quieter now) I've been in this forest the whole time. (beat) And I've been looking at my feet. He doesn't move. Just stands in it. Looks around slowly. WALTER The forest is me. I know that's — I know how that sounds. But I mean it exactly. (beat) Every part of it I couldn't see was just me. The competence. The clarity. The specific way I think. The thing I bring into a room. The reason people call me back. The reason I get hired again. The reason the day works. (beat) That's not ego. That's just — accurate. And it took me six months of looking at my feet and a day on a film set to remember that accurate is allowed to just be accurate. He turns slowly. Taking it in. The trees. The light coming through. The path continuing ahead. WALTER I was holding myself back. (beat) Not the relationships. Not the industry. Not the six months. (beat) Me. (beat) Waiting for a signal that was never going to come from anywhere but here. He touches his chest. Not theatrical. Just — there. He looks at the path ahead. Then he looks at the forest again. Really looks at it. WALTER (to the forest — not to us) I know. (beat) I know. Long pause. The forest. The wind. The birds. Walter standing in the middle of all of it. Then — He shakes his head. Small. Almost to himself. The specific gesture of someone who just caught themselves doing the thing and finds it both frustrating and kind of funny and entirely human. He starts walking again. The camera catches it. WALTER (quietly, to the trees, to himself) When you spend so much time staring at your feet. Not knowing. (beat) We spend so much time putting one foot in front of the other not looking at anything else around you. (beat) And you just realize — (beat) you are always where you are. (beat) You're here. You're now. And there's nothing you can do about that. (beat — the smallest laugh) So you just gotta shake your head. And just keep living. He keeps walking. Into the forest. Still talking to it. Not to us. The camera holds. He gets smaller in the frame. The trees get larger. He doesn't disappear. He just becomes one part of the frame instead of the whole thing. The forest was always here. So was he. CUT TO BLACK. No music. Just the forest continuing after he's gone. Wind. Birds. The sound of feet on a trail getting quieter. Then nothing. Then the nothing holds a beat longer than comfortable. Then gone. ============================================================ END OF S3E04 ============================================================ WALTER HAS GONE AWAY Season 3 — Episode 4 "The Forest" "You are always where you are. You're here. You're now. And there's nothing you can do about that. So you just gotta shake your head and just keep living." Next: Episode 5 `, 'S03E05': `WALTER HAS GONE AWAY Season 3 — Episode 5 "AT THE WATER" Runtime: 8–10 minutes Single location. Shoreline. Vancouver. No relief. No comfort. No poetic cover. This is what the system looks like from inside it. ============================================================ DIRECTOR'S NOTE The notebook is real. It was a gift. It has a letter on the first page. The audience does not see the letter in this episode. They will see it in the finale. What matters here is not the letter. What matters is what grew around it. The system in this episode learned to run in this notebook. This object. This weight. Walter knows what's on page one. He never goes there. That absence is present. When he holds the notebook at the end— unused— something has shifted. Not resolved. Not named. Shifted. Hold on that. ============================================================ COLD OPEN BLACK. Water. Loud. Wind cutting through. Seagulls somewhere. No music. FADE UP: EXT. SHORELINE — DAY Walter is already there. Coffee in hand. Going cold. Notebook under his arm. Cigarette lit. He stands wrong. Not relaxed— performing relaxation. Watching himself do it. He looks at the water. WALTER Okay. (beat) So this is supposed to — (beat) …something. (beat) Water. (beat) Right. Nothing happens. WALTER Yeah. (beat) Of course. (beat) So what now. ============================================================ ACT ONE — THE SYSTEM FIRES He opens the notebook. No pause. No decision. Hand moves first. He flips past the beginning. Always past the beginning. He writes. Fast. WALTER If you're here— (beat) there should be something. (beat) Otherwise this is just — (beat) standing. (beat) And standing — (beat) doesn't count. He writes faster. WALTER Okay. (beat) What is this. (beat) Why are you here. (beat) Say it properly. (beat) Don't waste it. He stops. Pen hovering. Waiting. Nothing comes. WALTER … (beat) Nothing. He writes it. "nothing" Stares at it. WALTER No. (beat) That's not — (beat) No. ============================================================ ACT TWO — THE RECOGNITION He freezes. Something clicks. WALTER Oh. (beat) There it is. (beat) Immediately. (beat) Didn't even — (beat) ten seconds. He looks at the notebook. Then the water. Back. WALTER Had to build something. (beat) Right away. (beat) Turn it into — (beat) anything. (beat) So it counts. (beat) Because if it doesn't — He stops. WALTER … (beat) Right. (beat) That. He nods once. Recognition. Not agreement. WALTER It worked. (beat) That's why it's still here. (beat) Scan it. (beat) Define it. (beat) Stay ahead of — (beat) something. (beat) There's nothing here. (beat) Still scanning. He looks at the water. WALTER That feeling — (beat) move. (beat) Now. (beat) Or you missed it. (beat) Missed what. (beat) What. (beat) There's nothing. (beat) Still — (beat) move. (beat) Move. (beat) Move. He exhales. WALTER You again. ============================================================ ACT THREE — THE INVASION He looks at the water. Commits. One second. Two. Three. WALTER Okay but — (beat) if you're not writing it (beat) just — He stops. Hard. WALTER No. (beat) No. (beat) Don't — (beat) Don't phrase it. (beat) Don't — (beat) You're doing it again. He laughs. Short. Sharp. WALTER You can't even — (beat) No. (beat) There. (beat) That. He holds up the notebook. WALTER You call it thinking. (beat) It's not thinking. (beat) It's just not stopping. (beat) Different voice. (beat) Same thing. He looks at it. Really looks. Not the page. The object. Something crosses his face. Unspoken. Held. WALTER You didn't choose this. (beat) It chose you. (beat) And it stayed. (beat) Even when there's nothing left (beat) to stay for. He lowers the notebook slightly. WALTER It doesn't stop. (beat) It just — (beat) finds something. (beat) Anything. (beat) So it can keep going. ============================================================ MICRO-FAILURE He looks at the water. Two seconds. Three. His grip tightens. He opens the notebook again. Pen hits paper. Starts a word — Stops. Looks at it. WALTER No. (beat) No. ============================================================ THE DECISION He stands there. Notebook open. Pen ready. Nothing written. He looks at it. Then the water. Then back. No speech. No ceremony. He lowers it. Arm drops. Notebook hangs. Dead weight. Unused. His shoulders shift. Slight. Real. He looks at the water. No narration. No extraction. No monitoring. Just — looking. The water does nothing. It never was going to. Wind. Time passes. He takes a drag. No comment. A few seconds. He shakes his head. Not defeat. Not victory. Recognition. WALTER (quiet) Enough. He turns. Walks. Notebook still in his hand. But different. Still there. Not gone. Not used. The camera stays on the water. Too long. Long enough to feel it. No narration. No meaning. Just — there. CUT TO BLACK. No music. Water continues one second into black. Then — silence. ============================================================ CANON NOTE — FOR COMMAND BRAIN The notebook is Chrissy's gift. Her letter is on page one. Walter has written past it for three seasons. The audience does not know this yet. They will in the finale. In this episode: The moment Walter looks at the notebook and something crosses his face— that is proximity. Not understanding. Not action. Proximity. Hold that moment. Do not rush it. The meaning lands later. ============================================================ END OF S3E05 WALTER HAS GONE AWAY Season 3 — Episode 5 "AT THE WATER" "It's not thinking. It's just not stopping." `, 'S03E06': `WALTER HAS GONE AWAY Season 3 — Episode 6 "YOUR TIME IS IMPORTANT TO US" Runtime: 10–12 minutes Format: A park. High vis. Walkie talkie. Walter does every voice on the radio. Kyle is the only real person. Nobody else exists. The park does not care. ============================================================ DIRECTOR'S NOTE ============================================================ This entire episode is filmed in a park near Walter's apartment. Walter is wearing a full high vis jacket. He has a walkie talkie. He has a clipboard. He has a phone to his ear. He is on hold with Telus. The park has dogs in it. People walking. A guy on a bench. Pigeons. None of them are on his film set. He is on his film set. They are in a park. Every radio communication in this episode is Walter's voice. He is the 1st AD. He is transport. He is the location manager. He is the director. He is the camera operator. He is everyone. The only voice that is not Walter is Kyle. Kyle is real. Kyle works at Telus. Kyle will arrive at minute forty seven. Kyle will change everything in approximately ninety seconds. The hold music runs under the whole episode. Not loud. Just present. The way it was in his ear. The way it is now in yours. You're welcome. ============================================================ COLD OPEN ============================================================ BLACK. Hold music. Eight seconds. Just the music. The flute does something in bar two that was probably a mistake. FADE UP: EXT. PARK — VANCOUVER — DAY — 8:14AM A park. Clearly a park. Trees. Path. A bench. A dog somewhere. Normal Tuesday morning park energy. Walter. Standing in the middle of it. Full high vis jacket. Earpiece in — walkie clipped to his chest. Clipboard under one arm. Coffee in one hand. Phone pressed to his other ear. He looks completely out of place. He does not appear to notice this. AUTOMATED VOICE Thank you for calling Telus. Your call is important to us. Please hold and we'll be with you shortly. WALTER (to no one, quietly) Shortly. (beat) Okay. The hold music begins. Walter listens to it. His face does a small calculation. WALTER Fine. He looks around the park. His park. His set. His domain. He pulls the walkie. WALTER (into walkie — 1st AD voice, slightly different register, complete authority) Good morning. This is Walter. All departments confirm status. We are rolling at 8:30. (beat — he keys the walkie again, different voice — transport) Transport confirms. Two vehicles at the south entrance. (beat — another key, location manager) Location is clear. Weather holding. (beat — another key, back to his own voice, professional) Copy all. Let's have a good day. He clips the walkie. Looks at his phone. Hold music. WALTER V.O. 8:14am. Data stopped working at 7:52am. I need data because Slack is how a film crew communicates and without data I am a very competent man standing in a park in a high vis jacket talking to himself on a radio. (beat) This will take four minutes. A pigeon lands near his feet. Walter looks at it. It looks back. Neither of them blinks. Walter looks back at the middle distance. Professional. ============================================================ TIME CUT — 8:25AM (11 MINUTES IN) ============================================================ Walter is walking the path now. Slow loop. Phone to his ear. Hold music going. WALTER V.O. Eleven minutes. (beat) The music has a flute in it. (beat) The flute does something in bar two that I don't think was intentional. (beat) I think it was a mistake that everyone in the room decided was fine. (beat) It is not fine. (beat) I have heard it fourteen times. (beat) It gets worse each time. Not because it changes. Because I know it's coming. His walkie crackles. WALTER (into walkie — camera operator voice) Walter this is camera one — are we still on schedule for the first setup? (beat — clicks back, his own voice) Camera one confirmed. We move at 8:30. Stay ready. (clicks again — camera operator) Copy. He clips the walkie. Hold music. The flute. Bar two. Walter's jaw tightens. Slightly. WALTER (under his breath) There it is. ============================================================ TIME CUT — 8:38AM (24 MINUTES IN) ============================================================ Walter is sitting on a park bench. Not his bench. A public bench. He is sitting on it professionally. Back straight. Clipboard on his knee. Phone to his ear. High vis. WALTER V.O. Twenty four minutes. (beat) I have learned the music. (beat) Not the way you learn something you enjoy. (beat) The way you learn the sound of a car alarm at 2am on your street. (beat) Against your will. By total repetition you did not choose and cannot stop. (beat) The flute is in bar two. Piano in bar four. The piano thinks it's doing something. The piano is not doing something. The piano is being bar four of the same loop for the nineteenth time. (beat) I will know this music when I am eighty years old sitting in a chair somewhere and someone will play it and I will feel something I cannot explain and it will be this. (beat) A park bench. (beat) A high vis. (beat) Twenty four minutes. AUTOMATED VOICE Thank you for your continued patience. We are experiencing higher than usual call volumes. Your call is important to us. We will be with you shortly. Walter looks at the phone. WALTER (to the elderly man, quietly) Show me shortly on a clock. Walter's walkie crackles. WALTER (into walkie — director voice, slightly more intense) Walter. We need to move the company to the next location. What's our status? (beat — clicks, his own voice, immediate) Moving now. Give me five minutes. (clicks — director) Five minutes. (clicks — his own voice) Five minutes. He stands up from the bench. Starts walking. Phone still to his ear. Hold music still going. High vis moving through the park with complete professional urgency toward nothing in particular. ============================================================ LC BLEED-THROUGH ============================================================ One second. No warning. The hold music continues. But the visual cuts to: THE LONELINESS COMPANY Internal Memo On Hold Division Est. 1987 "The On Hold System remains our most cost-effective retention product. No staff required at point of contact. No resolution required at any point. The caller provides their own hope. We provide only: — The word 'shortly' — The implication of movement — Music 'Shortly' has been tested extensively. It produces hope in 94% of subjects. Hope produces staying. Staying produces the account. Do not resolve too quickly. Resolution closes the account. Note: The flute in bar two was a mistake. We kept it. Subjects who identify the mistake stay on the line longer. They want to be there when someone fixes it. Nobody is going to fix it. Thank you for holding. Your call is important to us." FOOTER: The Loneliness Company — a Shaazadon subsidiary "We invented this. All of this. You're welcome." CUT BACK TO: Walter walking through the park. Hold music in his ear. The flute doing the thing. He doesn't know what just happened. He knows exactly what just happened. It doesn't matter either way. The music keeps going. ============================================================ TIME CUT — 8:49AM (35 MINUTES IN) ============================================================ Walter has found a slightly more secluded part of the park. Trees. Less foot traffic. He is pacing a small rectangle. The rectangle of a man who has been on hold for thirty five minutes and cannot stand still and cannot leave. WALTER V.O. Thirty five minutes. (beat) I want to be clear about something. (beat) I called about data. (beat) Mobile data. (beat) The thing that lets your phone use the internet when you're not on wifi. (beat) I need it for Slack. (beat) Slack. (beat) I have been on hold for thirty five minutes for Slack. (beat) And I cannot hang up. (beat) Because if I hang up I go back to the beginning of the queue. (beat) And if I stay on I am wasting more time but it connects to the thirty five minutes which means it counts. (beat) Which is — He stops pacing. WALTER V.O. That's a pattern. (beat) Staying because leaving means the time already spent meant nothing. (beat) Sunk cost dressed as commitment. (beat) I spent an entire episode at the water watching this exact thing happen. (beat) And I'm doing it. (beat) Right now. (beat) On hold with Telus. (beat) About Slack. (beat) In a park. (beat) In a high vis jacket. He looks down at the high vis. He almost laughs. His walkie crackles. WALTER (into walkie — 1st AD voice) Walter — director wants another setup before we lose the light. What's our status? (beat — clicks, his own voice) Tell him yes. We have the time. (clicks — 1st AD) Copy that. He clips the walkie. Looks at the phone. Thirty five minutes. WALTER (quiet, to the phone, to Telus, to the system) I know what you are. (beat) I've been studying this for three seasons. (beat) And I'm still on the line. (beat) That's — He shakes his head. The shake. WALTER That's very funny. ============================================================ TIME CUT — 8:58AM (44 MINUTES IN) ============================================================ Walter is standing very still now. Near a tree. Just him and the tree and the music. WALTER V.O. Forty four minutes. (beat) The hold music has stopped being music. (beat) It's just — (beat) a condition. (beat) The way traffic noise stops being noise when you live near a highway. (beat) It's just the air now. (beat) The air has a flute in it. (beat) The flute made a mistake in bar two. (beat) Nobody fixed it. (beat) Nobody is going to fix it. (beat) That's fine. (beat) That's fine. AUTOMATED VOICE Thank you for your patience. Your call is important to us. We will be with you shortly. Walter looks at the phone. WALTER (very quietly) Shortly. (beat) Shortly. (beat) Shortly. He says it until it's just sound. WALTER Shortly. (beat) Yeah. (beat) Gone. ============================================================ KYLE — 9:01AM (47 MINUTES IN) ============================================================ Walter is standing in the middle of the path. Just standing. Phone to his ear. Hold music. The flute. Bar two. The mistake nobody fixed. Then — KYLE Hi thank you for calling Telus this is Kyle how can I help you today? Walter freezes. One beat. Two. He looks at the phone. WALTER Kyle. KYLE Yes sir, how can I help? WALTER Kyle. KYLE ...Yes? WALTER You're real. KYLE I — yes sir I'm real, how can I help you today? Walter exhales. The full forty seven minutes leaving his body at once. WALTER Kyle I need my mobile data turned back on. It stopped working this morning. I need it for work. I'm on a — (he looks at the park) I'm on a job. KYLE Of course, can I get your account number? Walter gives the number. Kyle types. The hold music is gone. The silence where the hold music was is enormous. The park fills the space. Birds. Wind. The dog somewhere. Real sounds. KYLE Okay I can see the issue here — have you tried turning your phone off and on? Walter looks at the phone. Long pause. He looks at the park. He looks at the high vis. He looks at forty seven minutes. WALTER ...No. KYLE Let's try that first. Walter turns his phone off. The park goes quiet. No hold music. No Telus. No Kyle. Just a man in a high vis standing in a park with a dead phone. He turns it back on. Waits. The data icon appears. Slack notification. Twenty two unread messages. The imaginary crew has been communicating this whole time. Nobody noticed he was gone. The set ran fine. Of course the set ran fine. WALTER It's working. KYLE Great! Is there anything else I can help you with today? Walter looks at the park. At the bench where the old man was. He's gone now. At the path where the dog ran. At the tree he stood next to for eleven minutes. At the rectangle he paced for six. At forty seven minutes of his life. At the data icon. Glowing. Like it was never gone. WALTER No. (beat) Thank you Kyle. KYLE Have a great day sir! The call ends. Walter stands in the park. Phone in hand. High vis. Walkie. Clipboard. Cold coffee. He opens Slack. Scrolls through twenty two messages. Mundane. Normal. Lighting update. Parking question. Someone asking about lunch. He puts the phone in his pocket. Looks at the park. Just the park. A Tuesday morning park. People walking dogs. A guy on a bench. Pigeons. He pulls the walkie. WALTER (into walkie — his own voice, just his own voice) That's a wrap on hold. (beat) Good work everyone. He clips the walkie. WALTER V.O. Forty seven minutes. (beat) Turn it off and on again. (beat) Ninety seconds with Kyle. (beat) That's the thing about the system. (beat) It doesn't care how long you waited. (beat) It doesn't know you waited. (beat) It just plays the music until someone answers. (beat) Or doesn't. (beat) And then when someone finally does — (beat) four seconds. (beat) Off. (beat) On. (beat) Done. He takes a sip of cold coffee. Doesn't comment on the temperature. Starts walking. High vis moving through the park back toward the exit back toward whatever the day actually is now that the hold is over. WALTER V.O. The flute was a mistake in bar two. (beat) I'll know it until I die. (beat) That part wasn't Telus's fault. (beat) That part's just how brains work. (beat) Once something gets in — (beat) it stays. (beat) Whether it was supposed to or not. He walks out of frame. The park stays. Dogs. Pigeons. Bench. Path. Completely unchanged. CUT TO BLACK. ============================================================ POST CREDITS ============================================================ BLACK. Hold music. Eight seconds. The flute does the thing in bar two. AUTOMATED VOICE Thank you for your patience. Your call is important to us. We will be with you — Hard cut. Silence. Then, text on screen: "Kyle was real. Kyle did his best. This was not Kyle's fault." CUT TO BLACK. ============================================================ END OF S3E06 WALTER HAS GONE AWAY Season 3 — Episode 6 "Your Time Is Important To Us" "The system doesn't know you waited. It just plays the music until someone answers. Or doesn't." `, 'S03FIN': `WALTER HAS GONE AWAY Series Finale — "THE FINAL MIRROR" Break Room Roast Cut + Accusation Spine Runtime target: 28–32 minutes ============================================================ DOCTRINE ============================================================ This is not about her. This is not about him. This is not about anyone watching. This is about what Walter lost and who he became by letting himself do this. No names. No proof. No resolution. No catharsis. One hair between the full truth and the line. ============================================================ COLD OPEN ============================================================ BLACK. Ocean. Wind. Distant beach voices — not crowd, just life continuing. A score already underneath. Low. Uneasy. Alive. Not music yet. Just pressure. A lighter flicks. Misses. Second flick. The flame catches something we can't see yet. WALTER PRIME (V.O.) (quiet, almost to himself) There's a version of this where I tell you everything. (beat) I'm not doing that. (beat) You already know everything. A room erupts with laughter before we see it. CUT TO: ============================================================ EXT. SUNSET BEACH — GOLDEN HOUR — TV IMAGE ============================================================ WALTER PRIME walks the shoreline. City behind him. Open ocean ahead. BC ending at the water. Everything after that — open. Probable. Possible. Unwritten. He is composed. Too composed for a man about to burn something. WALTER PRIME Not for you. You've got your own problems. CUT WIDER TO REVEAL: ============================================================ INT. COSMIC BREAK ROOM — NEXT DIMENSION UP ============================================================ A break room that feels corporate, cosmic, and profoundly tired. Cheap couch. Bad fluorescent hum. A vending machine older than language. Mugs. Cans. A stale muffin nobody has touched in what appears to be several geological epochs. A TV in the corner plays the beach image. Present: SHADOW WALTER — leaning against the wall. Dressed like Walter. Quieter than Walter has ever been. He retired at the end of S2. He's here anyway. Old habits. THE PROSECUTOR — seated. Jacket on. Clipboard. He did not need to bring the clipboard. He brought the clipboard. THE PARASITE — sprawled on the couch. Already on his third drink before the episode started. Not proud of this. Not ashamed either. ENGAGEMENT CLONE — standing near the TV. Phone in hand. Always phone in hand. Tracking something. Always tracking something. LEADERSHIP — partially visible. Lower half only. Good watch. No brand. The watch has always been there. The watch was there before watches. No introductions. They have been here for three seasons. ENGAGEMENT CLONE Strong open. Dismissive intimacy. "Not for you" while absolutely wanting you to watch. Classic. Retention curve is — PROSECUTOR Manipulative. PARASITE He opened with "not for you" because he needs you watching to feel okay about finally saying the thing he's been not saying for two years. LEADERSHIP Audience denial is among his most reliable tools. We've logged it seventeen times. SHADOW WALTER He rehearsed "casual." The room breaks. PARASITE Okay. Same rules. Every time he confuses awareness with change — drink. Every time he says "hypothetically" — double. Every time he gets within one sentence of the actual truth and then upholsters it — finish everything. PROSECUTOR Accepted. ENGAGEMENT CLONE If he says "clarity" I'm dead in twelve minutes. LEADERSHIP That would be your strongest quarter. On TV: WALTER PRIME Let me give you some hypothetical scenarios — ALL DRINK. ============================================================ SECTION ONE — THE KNOWING ============================================================ EXT. BEACH — TV IMAGE Walter keeps walking. Not slow. Not dramatic. Just a man who has been thinking about this for a very long time and is finally done thinking. WALTER PRIME I want to talk about something most people don't talk about. Not because it's too painful. Because it makes you look bad. (beat) The part where you know. (beat) Not suspect. Not wonder. Not have a feeling. Know. (beat) And stay anyway. CUT TO BREAK ROOM: PARASITE Here we go. PROSECUTOR Opening statement. Self-implicating. ENGAGEMENT CLONE This tests incredibly well with people who have absolutely been this person. Which is everyone. SHADOW WALTER Let him say it. ON TV: WALTER PRIME Your body tells you. Not once. Not subtly. Your whole nervous system starts running a different program than the one your mouth is running. Your stomach knows. Your skin knows. The specific way your chest goes quiet when something should have made it loud — (beat) that's not anxiety. (beat) That's accuracy. CUT TO BREAK ROOM: PARASITE OH. He's starting with the body. He never starts with the body. PROSECUTOR Physiological testimony. Admissible. SHADOW WALTER He learned that one the expensive way. LEADERSHIP Most useful lessons arrive that way. ENGAGEMENT CLONE The "your body knew" angle gets massive — SHADOW WALTER (without looking up) Don't. Engagement Clone stops. ON TV: WALTER PRIME And you have a choice. You can listen to that. Or you can decide — and this is the move, this is the classic, this is the one I did so many times I should have gotten a loyalty card — you can decide that loving someone hard enough will eventually make the signal wrong. (beat) Spoiler. (beat) The signal is not wrong. CUT TO BREAK ROOM: PROSECUTOR Stated plainly. Somewhat damning. PARASITE "Loyalty card." That's the shirt. ENGAGEMENT CLONE Already mocking up the design. SHADOW WALTER He's still protecting them. The room goes quiet. SHADOW WALTER (CONT'D) Watch. ON TV: WALTER PRIME Now — I'm not here to explain what someone else did or didn't do. That's not mine to say. CUT TO BREAK ROOM: PARASITE THERE. Drink. PROSECUTOR Protective amendment. SHADOW WALTER He can't help it. LEADERSHIP He will get there. He always gets there eventually. It just costs him something each time. SHADOW WALTER Good. ============================================================ SECTION TWO — THE ACCUMULATION (One hair from the line) ============================================================ ON TV: WALTER PRIME (stops walking for a moment) What I will say — and I'm choosing these words very specifically — is that there are moments in a relationship where the math stops adding up. (beat) Not dramatically. Not with a confrontation or a confession or a scene. (beat) Just — things that should make sense don't make sense. Explanations that almost work. Timing that almost fits. A smell in a car that almost belongs there. (beat) And you file it. You file it and you file it and you file it and you keep showing up because that's who you are. Because you decided a long time ago that love means you give the benefit of the doubt until there's no doubt left. (beat) (quieter) Turns out you can run out of doubt and still not leave. CUT TO BREAK ROOM: Silence. PROSECUTOR (quietly) That's admissible. PARASITE He just described carrying someone else's secret on their behalf without their permission. ENGAGEMENT CLONE He did that for — SHADOW WALTER Don't say the number. ENGAGEMENT CLONE I was going to say a significant — SHADOW WALTER Don't. LEADERSHIP The duration is not the point. The pattern is the point. PARASITE The pattern. Yes. Let's talk about the pattern. ON TV: WALTER PRIME The pattern. PARASITE (to the room) He heard me. PROSECUTOR He did not hear you. You are not in the episode. PARASITE We are ADJACENT to the episode. PROSECUTOR Sustained. Both points. ON TV: WALTER PRIME Here's the thing about patterns. They don't lie. People lie. Explanations lie. Stories lie. But a pattern is just — a series of things that happened. In sequence. Over time. And at some point the sequence becomes undeniable. (beat) Not to them. (beat) To you. (beat) And when you know — when you actually know — you have exactly one move that preserves your integrity. (long pause) I didn't make it. CUT TO BREAK ROOM: PARASITE (not laughing) No. PROSECUTOR Against interest. Fully stated. SHADOW WALTER There it is. LEADERSHIP He said it without flinching. That's new. ENGAGEMENT CLONE (quietly) Yeah. SHADOW WALTER He's not protecting anyone this time. PROSECUTOR He's protecting himself. SHADOW WALTER That's different. That's allowed. ON TV: WALTER PRIME I made the other one. The one where you convince yourself that staying calm is the same as staying right. The one where you tell yourself you're being mature when actually you're just not ready to be the person who blew it up. (beat) Because blowing it up meant admitting the whole thing might not have been what you thought it was. (beat) And I loved it. (beat) I loved what I thought it was so much that I helped it stay standing longer than it deserved. (beat) That part's on me. CUT TO BREAK ROOM: Long silence. PARASITE (eventually) He's not going to soften that one. PROSECUTOR No. SHADOW WALTER No. ENGAGEMENT CLONE (no metrics this time) No. LEADERSHIP (simply) Good. ============================================================ SECTION THREE — THE MOP ============================================================ ON TV: WALTER PRIME (half laugh, real) You want to know the most honest image I have of myself during that whole period? (beat) I'm holding evidence. (beat) Physical, actual evidence. In my hand. And then — (beat) I put it in my bag. (beat) And I went and cleaned someone's house. (beat) Because they seemed stressed. CUT TO BREAK ROOM: The room LOSES IT. PARASITE YES. YES YES YES. This is the one. This is the image. "Let me just — pocket this proof — and alphabetize your spice rack." PROSECUTOR Exhibit A: Domestic labor as cognitive dissonance management. ENGAGEMENT CLONE "Cleaned their house." The most devastating three words in the series. SHADOW WALTER (not laughing) He loved her. The room goes quiet. SHADOW WALTER (CONT'D) That's not funny. PARASITE (quieter) No. PROSECUTOR (quietly) No. SHADOW WALTER He cleaned the house because he loved her. And he pocketed the evidence because he loved her. And both of those things are the truest and most self-destructive things he has ever done in the same afternoon. Beat. LEADERSHIP That's the episode. SHADOW WALTER That's been the whole show. ============================================================ SECTION FOUR — THE PART THAT COSTS SOMETHING ============================================================ ON TV: WALTER PRIME And here is the part I don't get to be the victim of. (beat) I knew. (beat) I'm not going to tell you exactly what I knew. I'm not going to give you names or proof or a case. (beat) You don't get that. (beat) (direct to camera — just for a second) Neither do you. (beat) But I knew. And I want to say that out loud one time in front of people because I have been carrying a very specific image in my head since August of last year and I have never said it out loud to another living person. (beat) And I'm not going to say it now either. (beat) But it's there. (beat) And it counts. (beat) Even if no one ever confirms it. (beat) Even if everyone involved spends the rest of their lives telling themselves a different story. (beat) It counts. CUT TO BREAK ROOM: No one speaks for a moment. PARASITE (quietly) That's the hair. PROSECUTOR What? PARASITE Between saying it and not saying it. He's standing right there. One hair. SHADOW WALTER That's exactly where he should be. LEADERSHIP Any further and it becomes accusation. SHADOW WALTER Any further back and it becomes cowardice. ENGAGEMENT CLONE (genuine) He found the line. PROSECUTOR He is walking it. SHADOW WALTER He finally learned that some things are true without needing to be proven. Beat. LEADERSHIP That took him long enough. SHADOW WALTER (almost a smile) Yeah. ============================================================ SECTION FIVE — THE WORK ============================================================ ON TV: WALTER PRIME So what do you do with that? (beat) You build a show about your own brain. (laughs) Obviously. You walk around at night with a camera talking to nobody until the nobody becomes somebody and the somebody becomes a season and the season becomes three seasons of watching yourself be exactly the person you were trying to stop being. (beat) That's the joke. (beat) That's also the whole point. CUT TO BREAK ROOM: PARASITE He made the show to process the thing. (beat) Then the show became the thing he needed to process. PROSECUTOR Recursive avoidance. ENGAGEMENT CLONE With great production value. SHADOW WALTER With real production value. PARASITE Don't. SHADOW WALTER (to Parasite, flat) You've had enough. PARASITE I haven't had nearly enough. LEADERSHIP Nobody ever has. That's the product. ON TV: WALTER PRIME And here is the part that actually took all three seasons to figure out: (beat) The work wasn't the show. The show was the proof that the work happened. (beat) And the difference between those two things — between doing the thing and performing doing the thing — is the only difference that actually matters. CUT TO BREAK ROOM: SHADOW WALTER That's the S2 finale said correctly. PROSECUTOR "Performed Recovery." He learned it. PARASITE He learned it or he learned to say it? SHADOW WALTER Watch. ============================================================ SECTION SIX — THE BUFFALO JUMP ============================================================ ON TV: Walter stops. Looks at the ocean. Looks at the city behind him. Looks forward. WALTER PRIME There's a concept I've been thinking about for a while now. (beat) The buffalo jump. (beat) Indigenous hunters would drive a herd of buffalo toward a cliff. The buffalo at the front couldn't stop. The ones in the back couldn't see. (beat) They went because that's where they were pointed. (beat) That's what every bad pattern in my life looked like from the outside. (beat) Someone or something pointing me in a direction and me going because I trusted the point. (beat) The bar. The chaos. The staying too long. The cleaning the house. (beat) I went because I was pointed and I trusted and I couldn't see what was at the edge until I was already in the air. (beat) And the growth — (beat) the actual growth, not the performed kind — isn't that I stopped moving. (beat) It's that I learned to look at where I'm being pointed before I commit to the jump. (beat) I go in slowly now. Not because I'm afraid. (beat) Because I know the difference between afraid and smart. CUT TO BREAK ROOM: Quiet. PARASITE That's the series. That's the whole series in one metaphor. PROSECUTOR Concise. Clear. Accurate. ENGAGEMENT CLONE (no metrics, just) Yeah. LEADERSHIP He found the frame. SHADOW WALTER Three seasons to find the frame. LEADERSHIP Some people never find it. SHADOW WALTER (quietly) Some people never jump and call it safety. LEADERSHIP Yes. SHADOW WALTER He jumped. Every time. Wrong cliff, right cliff, didn't matter. (beat) He always jumped. (beat) Now he looks. Beat. PARASITE That's annoyingly admirable. PROSECUTOR Document that. It will not be said again. ============================================================ SECTION SEVEN — THE BURN ============================================================ EXT. BEACH — WALTER BUILDS THE FIRE He kneels. Starts building. Methodical. Calm. Like a man who has been thinking about doing this for a specific amount of time and is now simply doing it. WALTER PRIME (V.O.) I'm going to tell you what this isn't. (beat) This isn't forgiveness. (beat) This isn't release. It isn't catharsis. It isn't healing in the way that word gets used on wellness accounts by people trying to sell you the next step of the process. (beat) This is something simpler. (beat) This is me ending an investigation that was never going to close. (beat) Because some cases don't close. (beat) The evidence is what the evidence is. The conclusions are what the conclusions are. (beat) And the person who could confirm it isn't going to. (beat) Not because they can't. (beat) Because they won't. (beat) And I've spent a very specific amount of time waiting for a confession from someone who decided a long time ago that their comfort was worth more than my reality. (beat) That's fine. (beat) That's actually fine. (beat) I just stopped being available to carry it on their behalf. Fire catches. CUT TO BREAK ROOM: PARASITE (very quiet) He said it. PROSECUTOR He said it without saying it. SHADOW WALTER One hair. PARASITE One hair. ENGAGEMENT CLONE (genuine) That's going to hit people who have been exactly there and never had a word for it. LEADERSHIP "Carrying it on their behalf." (beat) That is the mechanism. That is how it sustains. The other party doesn't have to do anything because the carrying person does all the work. SHADOW WALTER He did all the work. LEADERSHIP He did. SHADOW WALTER (flat) He was excellent at it. PROSECUTOR Against interest. SHADOW WALTER Against interest. Against logic. Against everything except love. (beat) (quieter) Which is the only thing that ever actually defeats logic. No one answers that. ============================================================ SECTION EIGHT — THE HAT ============================================================ ON TV: Walter reaches into his bag. Pulls out the hat. A flat-brimmed black hat. Death Row Records. The kind of hat that is far too white for most situations. He looks at it. Not long. Not dramatically. He puts it on. Backwards. CUT TO BREAK ROOM: PARASITE (stands up) HE IS WEARING IT. HE IS WEARING THE HAT. PROSECUTOR Symbol — PARASITE HE PUT THE HAT ON. BACKWARDS. THAT IS VILE. THAT IS ART. THAT IS THE MOST DISGUSTING THING HE HAS EVER DONE AND I HAVE BEEN HERE FOR THREE SEASONS. ENGAGEMENT CLONE I need a moment. LEADERSHIP (calmly) He is telling them he has it without telling them he has it. SHADOW WALTER He's had it since August. PROSECUTOR The jury — PARASITE THERE IS NO JURY. THERE NEVER WAS A JURY. THAT'S THE POINT. (catches himself) (sits down) (drinks) SHADOW WALTER He was always the only one who needed to know. Beat. PROSECUTOR He was always the only one who needed to know. SHADOW WALTER (to Prosecutor) Write that one down. PROSECUTOR (genuinely) Already done. ON TV: Walter takes the hat off. Looks at it. WALTER PRIME This isn't you. (beat) This isn't even what happened. (beat) This is what I carried when I couldn't carry the actual thing. (beat) You're not a person. You're not a memory. You're not evidence. (beat) You're the part of me that needed to hold something because I didn't know what else to do with knowing something I couldn't prove. (beat) And I've held you for — (beat, choice) long enough. (beat) The investigation is over. (beat) Not because it was solved. (beat) Because I'm closing it. (beat) There's a difference. CUT TO BREAK ROOM: LEADERSHIP That's the series. SHADOW WALTER That's the series. PROSECUTOR (quietly) The distinction matters. PARASITE "Not because it was solved. Because I'm closing it." (beat) He's not getting closure. He's choosing to close it. (beat) That's — that's different. ENGAGEMENT CLONE That's everything. SHADOW WALTER That's everything. ============================================================ SECTION NINE — THE LAST MONOLOGUE ============================================================ ON TV: WALTER PRIME Three seasons. (beat) Three seasons of watching myself be exactly the person I kept saying I was trying to stop being. (half smile) Productive. (beat) I thought I was doing the work. Turns out I was renovating the cage. (beat) I thought I was healing. Turns out I was formatting my pain into something more palatable and calling it growth. (beat) I thought I was strong. Turns out I was just overmanaged. (beat) And the thing — the thing that actually changed — wasn't any of that. (beat) It was this: (beat) I stopped helping the things that weren't helping me. (beat) That's it. (beat) No framework. No insight. No three-step system. (beat) I just stopped collaborating with the things that required me to be confused in order to survive. (beat) And I started being a little bit harder to fool. (beat) Even by myself. (beat) Especially by myself. CUT TO BREAK ROOM: PROSECUTOR Primary thesis. Stated. PARASITE "Renovating the cage." ENGAGEMENT CLONE "Formatting my pain." SHADOW WALTER "Overmanaged." LEADERSHIP All three. In thirty seconds. SHADOW WALTER He got there. PARASITE He got there ugly and late and real. SHADOW WALTER That's the only way it counts. ON TV: WALTER PRIME I'm not standing here telling you I figured it out. (beat) I'm telling you I stopped pretending I hadn't. (beat) That's a smaller thing. (beat) It's also the only thing. ============================================================ SECTION TEN — SARAH + THE BURN ============================================================ ON TV: Walter looks at the hat one more time. Then he places it in the fire. No ceremony. No speech. No rage. No hesitation. Just done. He stands. He looks at the ocean. WALTER PRIME (to the ocean, quiet) I know what I know. You don't have to say it. That's fine. Because now — (looks forward, city behind him, water ahead) This is mine. (beat) And where it goes? (half smile — real, not performed) No idea. (beat) Good. SARAH steps into frame. She kisses his cheek. No words. No eye contact held too long. No savior energy. No rescue. Just: present. Not built from emergency. Just there. They turn. They walk. CUT TO BREAK ROOM: Nobody jokes about her. ENGAGEMENT CLONE (quietly) He finally stopped pitching. PROSECUTOR He accepted insufficient certainty. PARASITE He'll build something else. He always builds something. LEADERSHIP Of course. (beat) LEADERSHIP (CONT'D) But maybe not out of emergency this time. SHADOW WALTER That's the whole difference. PROSECUTOR He remains guilty. Beat. SHADOW WALTER Not of that anymore. ============================================================ FINAL IMAGE ============================================================ TV IMAGE: Camera tilts DOWN. The hat is revealed in the flames. We hold. One full minute. Ocean. Gulls. Wind. Distant people living their lives. The fire slowly rises in the mix. Overtakes everything. Becomes the dominant sound. Becomes almost uncomfortable. At forty-five seconds — Camera begins tilting UP. Walter and Sarah in the distance. Walking. Not toward anything specific. Just forward. Hard guitars enter. Not resolution. Just momentum. Tilt continues: Beach. City behind. Mountains. Sky. First stars. CUT TO BLACK. Music holds a fraction too long. Then stops. ============================================================ POST-CREDITS — THE BREAK ROOM FLOOR ============================================================ INT. COSMIC BREAK ROOM — SLIGHTLY LATER The TV is off now. The others have gone. Leadership left silently, as Leadership always does. Prosecutor filed something. Engagement Clone posted something. Shadow Walter walked out without ceremony. He's done here. He was done two seasons ago. He just wanted to see how it ended. THE PARASITE is on the floor. Not collapsed dramatically. Just — on the floor. Next to the couch. Surrounded by evidence of a good time. The dog pillow from the couch is under his head. A cat has appeared from nowhere and is sitting on his chest. The cat has no opinion about any of this. The Parasite is asleep. Or close to it. His lips are moving. PARASITE (murmuring, half asleep) ...no but that's the thing... ...the breakfast cereal thing was real, I have the sources, I have the PRIMARY sources... (shifting) ...all I'm saying is if you look at the shadow it doesn't match the angle of the light but nobody is asking why it doesn't match the angle of the light... (long pause) ...okay but this one is different... this one came from someone who heard it from someone whose sister received the information through methods I am not at liberty to describe but which involved the petrified fecal matter of the Whooping Whharble... The cat blinks. PARASITE (CONT'D) ...micro island... sixteen hundred nautical miles off the pacific coast of south western south america... you can find it if you look to the continent by way of the Whooping Whharble and if the Whharble could tell you the time its beak would point to the number of feathers on the left wing minus the number of feathers on the right wing which lands at six sixty-six which doesn't exist on a clock face which is the ENTIRE POINT — (shifting again, getting agitated even in sleep) — the location of the island corresponds to a coordinate that only appears when you account for the magnetic declination of a compass held by someone who is emotionally compromised in a specific and documentable way — (very quiet now) ...which is all of us... ...which has always been all of us... Long pause. The cat begins to purr. PARASITE (CONT'D) (barely audible) ...I'm not wrong about this one... (beat) ...I'm not wrong about any of them... (longer pause) ...I just wish I could make it stop for like twenty consecutive minutes... (last barely audible murmur) ...the Whharble knows... ...the Whharble has always known... ...they just can't tell us because their beak is pointing at a time that hasn't been invented yet... Silence. The cat continues purring. One fluorescent light flickers once. In the corner of the room, the TV clicks on for exactly one second. ON SCREEN: THE LONELINESS COMPANY Internal Memo — Parasite Division Re: Outstanding Invoice for Services Rendered (Seasons 1–3) Amount Due: [REDACTED] Due Date: [REDACTED] Bureau of Persistent Conditions Reference: [REDACTED] Fine print at the bottom: "The Whooping Whharble is not affiliated with The Loneliness Company. The Loneliness Company is not affiliated with The Whooping Whharble. Shaazadon has no comment. Shaazadon has never had a comment. Shaazadon was the comment." TV clicks off. Darkness. The Parasite snores once. The cat does not move. CUT TO BLACK. END. ============================================================ END CARD (on black, white text, held) ============================================================ NOBODY JUST GROWS APART. FIRST THEY CHOOSE SMALL, REPEATED DISHONESTIES BECAUSE THEY FEEL EASIER THAN TRUTH. AND ONE DAY THERE'S NOTHING LEFT TO STAND CLOSE TO. (held longer than comfortable) (then, smaller, below it) THE INVESTIGATION IS CLOSED. NOT BECAUSE IT WAS SOLVED. BECAUSE HE CHOSE TO CLOSE IT. (beat) WALTER HAS GONE AWAY. ============================================================ FINAL SHAAZADON AUDIO — POST END CARD (no image — pure black — barely audible) ============================================================ LC NARRATOR VOICE (clinical, almost normal, almost) Subject has exited interpretive loop. (beat) Classification: Non-compliant. (beat — longer than expected) Outcome: Unknown. (long silence) (then, the quietest it has ever been, almost not there at all) ...that's new. CUT TO: nothing. ============================================================ END OF SERIES ============================================================ WALTER HAS GONE AWAY Created by Walter Sawan The Hollow Crown Rebellion Season 1 — Internal Diagnostic Spiral Season 2 — Architecture Replacing Chaos Season 3 — Behavioral Verification Under Pressure "Evidence over time. Actions over words. Observable patterns only." ` }); const WHGA_S3_INSERTION_PACK = [ {id:'A1', category:'TRAINING', line:'Loneliness is not a condition. It is a renewable behavioral resource.'}, {id:'A2', category:'TRAINING', line:'Stability reduces engagement. Engagement drives return.'}, {id:'A3', category:'TRAINING', line:'Users do not seek resolution. They seek movement.'}, {id:'A4', category:'TRAINING', line:'Emotional closure has shown a 62% drop in re-engagement cycles.'}, {id:'A5', category:'TRAINING', line:'Introduce uncertainty. Remove conclusion.'}, {id:'B1', category:'METRICS', line:'Subject W volatility has decreased by 43%.'}, {id:'B2', category:'METRICS', line:'Narrative escalation pathways currently inactive.'}, {id:'B3', category:'METRICS', line:'Emotional spike frequency below baseline expectation.'}, {id:'B4', category:'METRICS', line:'Subject W no longer responding to previous trigger sets.'}, {id:'B5', category:'METRICS', line:'Engagement risk: sustained neutrality.'}, {id:'C1', category:'CONCERN', line:'We are observing a decline in reactive pattern loops.'}, {id:'C2', category:'CONCERN', line:'Subject appears to be… maintaining. Flag for review.'}, {id:'C3', category:'CONCERN', line:'Predictive modeling is returning inconsistent outputs.'}, {id:'C4', category:'CONCERN', line:'We are unable to generate a compelling next step.'}, {id:'C5', category:'CONCERN', line:'No immediate intervention recommended. Monitor closely.'}, {id:'D1', category:'R&D', line:'New initiative: Low-Conflict Living™. Early tests show minimal narrative output.'}, {id:'D2', category:'R&D', line:'Prototype: “Nothing Happens.” User retention unstable.'}, {id:'D3', category:'R&D', line:'Testing passive existence models.'}, {id:'D4', category:'R&D', line:'Users report reduced urgency. Interpretation pending.'}, {id:'D5', category:'R&D', line:'We may have reached a version of the user that does not require us. This is under investigation.'}, {id:'E1', category:'HR', line:'Who approved sustained emotional neutrality?'}, {id:'E2', category:'HR', line:'We are not in the business of “fine.”'}, {id:'E3', category:'HR', line:'Reintroduce friction. Subtly.'}, {id:'E4', category:'HR', line:'You allowed the system to stabilize. Explain.'}, {id:'E5', category:'HR', line:'This is not a maintenance company. This is a momentum company.'}, {id:'F1', category:'EXEC', line:'The subject has not escaped.'}, {id:'F2', category:'EXEC', line:'The system does not require participation to persist.'}, {id:'F3', category:'EXEC', line:'Absence of noise is not absence of structure.'}, {id:'F4', category:'EXEC', line:'He is no longer generating value. Observe.'}, {id:'F5', category:'EXEC', line:'We do not lose users. We outlast them.'}, {id:'G1', category:'GLITCH', line:'—pattern deviation— —pattern deviation—'}, {id:'G2', category:'GLITCH', line:'Subject W — Subject W — Sub—'}, {id:'G3', category:'GLITCH', line:'Engagement — Engage— —'}, {id:'G4', category:'GLITCH', line:'No action required. No action required. No action required.'}, {id:'G5', category:'GLITCH', line:'SYSTEM OPERATING WITH REDUCED INPUT'} ]; const WHGA_S3_DEPLOYMENT = [ {ep:'S03E01', count:0, placements:[], note:'No Company. No bleed. Full separation.'}, {ep:'S03E02', count:1, placements:[{mark:'~70%', line:'Users do not seek resolution. They seek movement.'}], note:'First signal. Reframes the audience, not Walter.'}, {ep:'S03E03', count:2, placements:[{mark:'~30%', line:'Subject W volatility has decreased by 43%.'},{mark:'~80%', line:'Engagement risk: sustained neutrality.'}], note:'Introduce observation plus mild concern.'}, {ep:'S03E04', count:1, placements:[{mark:'~50%', line:'Absence of noise is not absence of structure.'}], note:'First language-mirroring bleed.'}, {ep:'S03E05', count:2, placements:[{mark:'~20%', line:'Emotional spike frequency below baseline expectation.'},{mark:'~75%', line:'We are unable to generate a compelling next step.'}], note:'System starts losing predictive control.'}, {ep:'S03E06', count:3, placements:[{mark:'~15%', line:'This is not a maintenance company. This is a momentum company.'},{mark:'~55%', line:'Prototype: “Nothing Happens.” User retention unstable.'},{mark:'~90%', line:'No action required. No action required.'}], note:'Escalation. System tries to define the anomaly.'}, {ep:'S03E07', count:1, placements:[{mark:'~60%', line:'Subject appears to be… maintaining. Flag for review.'}], note:'First time the Company sounds uncertain.'}, {ep:'S03E08', count:2, placements:[{mark:'~40%', line:'—pattern deviation— —pattern deviation—'},{mark:'~85%', line:'SYSTEM OPERATING WITH REDUCED INPUT'}], note:'System degradation begins. No explanation.'}, {ep:'S03E09', count:3, placements:[{mark:'~25%', line:'We may have reached a version of the user that does not require us.'},{mark:'~65%', line:'The system does not require participation to persist.'},{mark:'~95%', line:'Subject W — Sub—'}], note:'Contradiction established.'}, {ep:'S03E10', count:0, placements:[{mark:'final bridge', line:'Subject W no longer requires management. Continue observation.'}], note:'No cutaway inserts. Bridge line only.'}, {ep:'S03FIN', count:'scene', placements:[{mark:'break room', line:'He thinks he left. / He did. / Yeah… but not the system.'}], note:'Full cosmic break room confirmation. No exposition.'} ]; function renderS3SystemSection() { const sectionId = 'sec-s3system'; let section = document.getElementById(sectionId); if (!section) { section = document.createElement('section'); section.className = 'section'; section.id = sectionId; section.innerHTML = '
SEASON 3 SYSTEM
Behavior after awareness · ambient Company bleed · finale confirmation
'; document.querySelector('main.main').appendChild(section); } const nav = document.querySelector('.sidebar nav'); if (nav && !document.querySelector('[data-whga-s3-nav="1"]')) { const group = document.createElement('div'); group.className = 'nav-group'; group.innerHTML = ''; const importGroup = nav.querySelector('.nav-group:last-child'); nav.insertBefore(group, importGroup); } const pane = document.getElementById('s3SystemPane'); if (!pane) return; const cardsByCat = ['TRAINING','METRICS','CONCERN','R&D','HR','EXEC','GLITCH'].map(cat => { return '

'+cat+'

'; }).join(''); const depRows = WHGA_S3_DEPLOYMENT.map(item => { const label = item.count === 'scene' ? 'Finale scene' : item.count + ' insertion' + (item.count===1?'':'s'); const pill = item.ep === 'S03FIN' ? 'pill-finale' : 'pill-planning'; const stateTxt = item.ep === 'S03E01' ? 'locked' : 'mapped'; const placements = item.placements.map(p => '
↳ '+escHtml(p.mark)+' · '+escHtml(p.line)+'
').join(''); return '
'+item.ep+'
'+label+'
'+escHtml(item.note)+'
'+placements+'
'+(item.ep==='S03FIN'?'finale':'planning')+'
'+stateTxt+'
'; }).join(''); pane.innerHTML = `
SERIES SPINE (LOCKED — March 2026)

A trauma response learning to aim itself.

The hypervigilance. The heightened smell. The pattern recognition that runs at fifty percent constantly. Every feature that made life harder was always the same instrument. Just pointed in the wrong direction. Pointed inward: torture. Pointed outward at his own cognition: superpower. Three seasons of slowly turning the instrument around until it aims correctly. This is not about growth. This is a live demonstration of system architecture under load — and the accidental discovery that the most painful feature is also the most powerful one.

S3 Metacognition Canon

S3 register shift: Walter watches his own mind in real time. Not from inside it. From slightly above it. He can see the thought forming before it lands. He can watch the old software try to boot and go — oh. There it is. This is behaviour after awareness. The awareness is the operating system now. The LC bleed-through mirrors this exactly: Walter watches his own mind from outside — the LC watches humans from outside. Same instrument. Opposite purpose. Walter uses it to get free. The LC uses it to keep people stuck. That is the dark joke underneath all of S3.

The Notebook — Finale Card (OPERATOR LEVEL)

The notebook Walter has used throughout the series was a gift from Chrissy. Her letter is on page one. Walter has always written past it — never to it. The audience does not know this. They will see it in the finale when the notebook burns alongside the hat. Two objects. One fire. The investigation and the tool built to survive it. Both going at the same time. DO NOT reference the letter or the notebook's origin in any episode before the finale.

ONE MAN PRODUCTION — HARD CANON (LOCKED)

Walter Sawan plays every character in the entire series. No exceptions. No guest appearances. No other faces on camera. One person. All of it. This is not a budget constraint. This is the show. Every character Walter has ever played is a version of Walter. The narcissists. The 1st AD. Shadow Walter. The Prosecutor. The Parasite. The Loneliness Company cast. Kyle. All Walter. When writing any episode or character, write for one person performing all roles. This rule applies retroactively to all existing episodes and all future episodes. DO NOT write scenes that require another human face on camera.

Season 3 Lock

Theme

Behaviour after awareness. External world answers back. Behavioral verification under pressure.

Mirror Function

S3 holds the mirror outward. Universal patterns. No mercy. No advice. Recognition not inspiration.

LC Rule

Escalate by removing coherence. Language degrades. By finale: LC bleeds into Walter's monologue structure. The only win is becoming immune to needing resolution.

Script Locks

S03E01 — I Guess That's Alright

Park bench. Bass guitar. City competing. Door closes. High noise floor hum. Last line from inside through the closed door.

S03E02 — The Edge of the Map

Night walk. Downtown Vancouver. The trust question live. Camera set on ground. Walter walks away without it.

S03E03 — I Can Heal for Realz

Jonas goes to Hawaii. Walter hosts narcissist support group in his apartment. Six characters all played by Walter. Documentary briefing format. Ends on six simultaneous hands.

S03E04 — The Forest

Real Vancouver trail. Walter talks to the forest which is himself. Camera catches the last line — not performed, just heard. "You are always where you are. So you just gotta shake your head and just keep living."

S03E05 — At The Water

Shoreline. The system fires and rebrands in real time. Micro-failure included. Last spoken word: "Enough." Notebook still in his hand. Never opened to page one. Finale card — hold for fire.

S03E06 — Your Time Is Important To Us

A park. High vis. Walkie talkie. 47 minutes on hold with Telus. Walter does every radio voice. Kyle is real. Turn it off and on again.

S03FIN — The Final Mirror

Break room roast + beach burn + Parasite post-credits. Final line: Now he gets to find out who he is without the emergency.

Season 3 Deployment Map
${depRows}
Insertion Pack
${cardsByCat}
`; } function appendS3CanonBlock() { const sec = document.getElementById('sec-canon'); if (!sec || sec.querySelector('[data-s3canon="1"]')) return; const block = document.createElement('div'); block.setAttribute('data-s3canon','1'); block.className = 'card'; block.style.marginTop = '16px'; block.innerHTML = '
Season 3 Canon Shift

Quiet Walter / Loud System

Walter becomes quietly human. The Company becomes loudly inhuman. They run in parallel and are never acknowledged directly by Walter.

No Reveal Rule

The finale does not reveal the system. It confirms something the audience already felt but could not prove.

Operating Condition Rule

Loneliness Company and Shaazadon no longer appear as entities. They appear as ambient conditions, language infection, and environmental bleed.

'; sec.appendChild(block); } const __origRenderTimeline = renderTimeline; renderTimeline = function() { __origRenderTimeline(); const el = document.getElementById('timelineContent'); if (!el || el.querySelector('[data-s3timeline="1"]')) return; let html = '
Season 3 Arc + Ambient Company Bleed
'; const s3eps = EPISODES.filter(e => e.season && e.season.includes('S3')); s3eps.forEach(ep => { const dep = WHGA_S3_DEPLOYMENT.find(x => x.ep === ep.code) || {count:0, placements:[], note:''}; const d = getEpData(ep.code); const placements = dep.placements.map(i => '
↳ '+escHtml(i.mark)+' · '+escHtml(i.line)+'
').join(''); html += '
'+ep.code+'
'+ep.title+'
'+ep.desc+'
'+placements+(dep.note?'
'+escHtml(dep.note)+'
':'')+'
'+(d.status||ep.status)+'
'+(d.pct||0)+'%
'; }); html += '
'; el.insertAdjacentHTML('beforeend', html); }; function refreshS3Selectors() { ['cbRenderEpisodeSelect','renderEngineEpisodeSelect','renderV11EpisodeSelect','populateV12RealEpisodeSelects','renderEpList','renderScriptList','renderTimeline','renderDashboard'].forEach(name => { if (typeof window[name] === 'function') { try { window[name](); } catch (e) {} } }); } setTimeout(() => { renderS3SystemSection(); appendS3CanonBlock(); refreshS3Selectors(); }, 0); // ===== END WHGA S3 INTEGRATION PATCH ===== // ═══════════════════════════════════════════════════════ // V19 — GLOBAL SEARCH // ═══════════════════════════════════════════════════════ const V19_SEARCH_INDEX = []; function buildSearchIndex() { V19_SEARCH_INDEX.length = 0; // Episodes EPISODES.forEach(ep => { V19_SEARCH_INDEX.push({ type: 'episode', icon: '▤', cat: 'Episode', title: ep.code + ' — ' + ep.title, sub: ep.desc || ep.season || '', action: () => { showSection('episodes'); setTimeout(() => openEpisode && openEpisode(ep.code), 80); } }); }); // Scripts const allScripts = getAllScripts ? getAllScripts() : {}; Object.entries(allScripts).forEach(([key, text]) => { if (!text) return; const ep = EPISODES.find(e => e.scriptKey === key); const title = ep ? ep.title : key; V19_SEARCH_INDEX.push({ type: 'script', icon: '✎', cat: 'Script', title: 'Script: ' + title, sub: text.slice(0, 120).replace(/\s+/g,' '), fullText: text.toLowerCase(), action: () => { showSection('scripts'); setTimeout(() => openScript && openScript(key, title), 80); } }); }); // Characters CHARACTERS.forEach(ch => { V19_SEARCH_INDEX.push({ type: 'character', icon: '◎', cat: 'Character', title: ch.name, sub: ch.role + ' · ' + (ch.motivation || ''), action: () => showSection('characters') }); }); // LC Videos LC_VIDEOS.forEach(v => { V19_SEARCH_INDEX.push({ type: 'lc', icon: '▣', cat: 'LC Video', title: v.code + ' — ' + v.title, sub: (v.vo || '') + (v.desc || ''), action: () => showSection('lc-videos') }); }); // Season 2 production cards Object.values(SEASON2_PRODUCTION || {}).forEach(ep => { V19_SEARCH_INDEX.push({ type: 's2', icon: '⌬', cat: 'S2 Episode', title: ep.title, sub: ep.reveal || ep.purpose || '', action: () => { showSection('season2arc'); setTimeout(() => renderSeason2ProductionCard && renderSeason2ProductionCard(ep.code), 80); } }); }); // Nav sections const navSections = [ {label:'Dashboard', sec:'dashboard', icon:'⬡'}, {label:'Calendar', sec:'calendar', icon:'◷'}, {label:'Episode Editor', sec:'episodes', icon:'▤'}, {label:'Scripts', sec:'scripts', icon:'✎'}, {label:'Season Arc', sec:'timeline', icon:'↔'}, {label:'LC Videos', sec:'lc-videos', icon:'▣'}, {label:'Characters', sec:'characters', icon:'◎'}, {label:'Canon + Rules', sec:'canon', icon:'⊛'}, {label:'Gear Tracker', sec:'gear', icon:'⬡'}, {label:'Budget + Expenses', sec:'budget', icon:'$'}, {label:'Call Sheet', sec:'callsheet', icon:'▦'}, {label:'Checklists', sec:'checklists', icon:'☑'}, {label:'Physical Assets', sec:'assets', icon:'⬜'}, {label:'VO Session', sec:'vo', icon:'◈'}, {label:'Season 3 System', sec:'s3system', icon:'◬'}, ]; navSections.forEach(n => { V19_SEARCH_INDEX.push({ type: 'nav', icon: n.icon, cat: 'Section', title: n.label, sub: 'Go to ' + n.label, action: () => { const el = document.querySelector('.nav-item[onclick*="'+n.sec+'"]'); showSection(n.sec, el); } }); }); } let searchActiveIdx = -1; function openSearch() { buildSearchIndex(); document.getElementById('searchOverlay').classList.add('open'); setTimeout(() => document.getElementById('searchInput').focus(), 50); searchActiveIdx = -1; } function closeSearch() { document.getElementById('searchOverlay').classList.remove('open'); document.getElementById('searchInput').value = ''; document.getElementById('searchResults').innerHTML = '
Start typing to search everything…
'; } function runSearch() { const q = document.getElementById('searchInput').value.trim().toLowerCase(); const container = document.getElementById('searchResults'); searchActiveIdx = -1; if (!q) { container.innerHTML = '
Start typing to search everything…
'; return; } const results = V19_SEARCH_INDEX.filter(item => { const haystack = (item.title + ' ' + item.sub + ' ' + (item.fullText || '') + ' ' + item.cat).toLowerCase(); return q.split(' ').every(word => haystack.includes(word)); }).slice(0, 18); if (!results.length) { container.innerHTML = '
No results for "' + escHtml(q) + '"
'; return; } container.innerHTML = results.map((r, i) => `
${r.icon}
${escHtml(r.title)}
${escHtml(r.sub)}
${escHtml(r.cat)}
`).join(''); // Store results for keyboard nav container._results = results; } function highlightSearch() { document.querySelectorAll('.search-result-item').forEach((el, i) => { el.classList.toggle('active', i === searchActiveIdx); if (i === searchActiveIdx) el.scrollIntoView({block:'nearest'}); }); } function executeSearch(idx) { const container = document.getElementById('searchResults'); const results = container._results; if (!results || !results[idx]) return; results[idx].action(); closeSearch(); } function searchKeyNav(e) { const container = document.getElementById('searchResults'); const results = container._results || []; if (e.key === 'Escape') { closeSearch(); return; } if (e.key === 'ArrowDown') { e.preventDefault(); searchActiveIdx = Math.min(searchActiveIdx + 1, results.length - 1); highlightSearch(); } if (e.key === 'ArrowUp') { e.preventDefault(); searchActiveIdx = Math.max(searchActiveIdx - 1, 0); highlightSearch(); } if (e.key === 'Enter') { if (searchActiveIdx >= 0) executeSearch(searchActiveIdx); else if (results.length) executeSearch(0); } } // ═══════════════════════════════════════════════════════ // V19 — KEYBOARD SHORTCUTS // ═══════════════════════════════════════════════════════ function openShortcuts() { document.getElementById('shortcutsOverlay').classList.add('open'); } function closeShortcuts() { document.getElementById('shortcutsOverlay').classList.remove('open'); } let _v19KeyBuf = ''; let _v19KeyTimer = null; document.addEventListener('keydown', (e) => { const tag = document.activeElement.tagName.toLowerCase(); const isEditing = ['input','textarea','select'].includes(tag); const searchOpen = document.getElementById('searchOverlay').classList.contains('open'); // Always-active shortcuts if ((e.ctrlKey || e.metaKey) && e.key === 'k') { e.preventDefault(); openSearch(); return; } if ((e.ctrlKey || e.metaKey) && e.key === '.') { e.preventDefault(); toggleScratchpad(); return; } if ((e.ctrlKey || e.metaKey) && e.key === 'e' && !isEditing) { e.preventDefault(); exportData(); return; } if (searchOpen) return; // Search modal handles its own keys if (!isEditing) { if (e.key === '?') { openShortcuts(); return; } if (e.key === 'Escape') { closeShortcuts(); closeVersionDrawer(); return; } // Two-key nav shortcuts (G+X) clearTimeout(_v19KeyTimer); _v19KeyBuf += e.key.toLowerCase(); _v19KeyTimer = setTimeout(() => { _v19KeyBuf = ''; }, 800); const navMap = { 'gd': 'dashboard', 'ge': 'episodes', 'gs': 'scripts', 'gt': 'timeline', 'gb': 'budget', 'gg': 'gear', 'gc': 'callsheet', 'gl': 'lc-videos', 'gx': 'canon' }; if (navMap[_v19KeyBuf]) { const sec = navMap[_v19KeyBuf]; const navEl = document.querySelector(`.nav-item[onclick*="${sec}"]`); showSection(sec, navEl); _v19KeyBuf = ''; } } }); // ═══════════════════════════════════════════════════════ // V19 — SCRATCHPAD // ═══════════════════════════════════════════════════════ const SCRATCHPAD_KEY = 'whga-v19-scratchpad'; function toggleScratchpad() { const panel = document.getElementById('scratchpadPanel'); panel.classList.toggle('open'); if (panel.classList.contains('open')) { const saved = localStorage.getItem(SCRATCHPAD_KEY) || ''; document.getElementById('scratchpadText').value = saved; document.getElementById('scratchpadText').focus(); } } function saveScratchpad() { localStorage.setItem(SCRATCHPAD_KEY, document.getElementById('scratchpadText').value); } function clearScratchpad() { if (confirm('Clear scratchpad?')) { document.getElementById('scratchpadText').value = ''; localStorage.removeItem(SCRATCHPAD_KEY); } } // ═══════════════════════════════════════════════════════ // V19 — SCRIPT VERSION HISTORY // ═══════════════════════════════════════════════════════ const VERSION_KEY = 'whga-v19-versions'; let _currentVersionEp = null; function getVersionStore() { try { return JSON.parse(localStorage.getItem(VERSION_KEY) || '{}'); } catch(e) { return {}; } } function saveVersionStore(store) { localStorage.setItem(VERSION_KEY, JSON.stringify(store)); } function openVersionDrawer(epCode) { _currentVersionEp = epCode; document.getElementById('versionDrawer').classList.add('open'); document.getElementById('versionDrawerEpLabel').textContent = 'Episode: ' + epCode; renderVersionList(); } function closeVersionDrawer() { document.getElementById('versionDrawer').classList.remove('open'); _currentVersionEp = null; } function saveCurrentVersion(label) { if (!_currentVersionEp) return; const allScripts = getAllScripts ? getAllScripts() : {}; const ep = EPISODES.find(e => e.code === _currentVersionEp); const text = ep ? (allScripts[ep.scriptKey] || '') : ''; if (!text.trim()) { alert('No script content to save.'); return; } const store = getVersionStore(); if (!store[_currentVersionEp]) store[_currentVersionEp] = []; const draftLabel = label || prompt('Label this version (optional):') || ('Draft ' + (store[_currentVersionEp].length + 1)); store[_currentVersionEp].unshift({ id: 'v-' + Date.now(), label: draftLabel, date: new Date().toISOString(), text: text, locked: false, wordCount: text.split(/\s+/).filter(Boolean).length }); // Cap at 30 versions per episode store[_currentVersionEp] = store[_currentVersionEp].slice(0, 30); saveVersionStore(store); renderVersionList(); showToast('Version saved: ' + draftLabel); } function lockCurrentVersion() { if (!_currentVersionEp) return; const store = getVersionStore(); const versions = store[_currentVersionEp] || []; if (!versions.length) { alert('No versions saved yet.'); return; } versions[0].locked = true; saveVersionStore(store); renderVersionList(); showToast('Latest version locked.'); } function restoreVersion(epCode, id) { const store = getVersionStore(); const v = (store[epCode] || []).find(x => x.id === id); if (!v) return; if (!confirm('Restore this version? Current script will be overwritten.')) return; const ep = EPISODES.find(e => e.code === epCode); if (!ep) return; if (!state.episodeData[ep.scriptKey]) state.episodeData[ep.scriptKey] = {}; state.episodeData[ep.scriptKey].script = v.text; save(); showToast('Version restored.'); renderVersionList(); } function renderVersionList() { const el = document.getElementById('versionList'); if (!el || !_currentVersionEp) return; const store = getVersionStore(); const versions = store[_currentVersionEp] || []; if (!versions.length) { el.innerHTML = '
No versions saved yet for this episode.
'; return; } el.innerHTML = versions.map(v => `
${escHtml(v.label)} ${v.locked ? '⚿ LOCKED' : ''}
${new Date(v.date).toLocaleString()} · ${v.wordCount} words
${escHtml(v.text.slice(0,120))}
${!v.locked ? `` : 'Cannot restore — locked'}
`).join(''); } function previewVersion(epCode, id) { const store = getVersionStore(); const v = (store[epCode] || []).find(x => x.id === id); if (!v) return; const win = window.open('', '_blank'); win.document.write('' + escHtml(v.label) + '' + escHtml(v.text) + ''); win.document.close(); } function deleteVersion(epCode, id) { const store = getVersionStore(); if (!store[epCode]) return; const v = store[epCode].find(x => x.id === id); if (v && v.locked) { alert('Cannot delete a locked version.'); return; } if (!confirm('Delete this version?')) return; store[epCode] = store[epCode].filter(x => x.id !== id); saveVersionStore(store); renderVersionList(); } // V19 save hook — consolidated at bottom of file // ═══════════════════════════════════════════════════════ // V19 — THEME TOGGLE // ═══════════════════════════════════════════════════════ const THEME_KEY = 'whga-v19-theme'; function toggleTheme() { const isLight = document.body.classList.toggle('light-theme'); localStorage.setItem(THEME_KEY, isLight ? 'light' : 'dark'); document.querySelector('.theme-toggle').textContent = isLight ? '🌙' : '☀'; } function initTheme() { const saved = localStorage.getItem(THEME_KEY); if (saved === 'light') { document.body.classList.add('light-theme'); document.querySelector('.theme-toggle').textContent = '🌙'; } } // ═══════════════════════════════════════════════════════ // V19 — TOAST NOTIFICATIONS // ═══════════════════════════════════════════════════════ function showToast(msg, color) { let toast = document.getElementById('v19Toast'); if (!toast) { toast = document.createElement('div'); toast.id = 'v19Toast'; toast.style.cssText = 'position:fixed;bottom:32px;left:50%;transform:translateX(-50%);background:var(--panel);border:1px solid var(--border2);border-radius:10px;padding:10px 20px;font-size:13px;color:var(--text);z-index:99999;box-shadow:0 8px 32px rgba(0,0,0,.5);transition:opacity .3s;pointer-events:none;'; document.body.appendChild(toast); } toast.textContent = msg; toast.style.borderColor = color === 'red' ? 'var(--red)' : color === 'amber' ? 'var(--amber)' : 'var(--green)'; toast.style.opacity = '1'; clearTimeout(toast._timer); toast._timer = setTimeout(() => { toast.style.opacity = '0'; }, 2800); } // ═══════════════════════════════════════════════════════ // V19 — ACTIVITY FEED // ═══════════════════════════════════════════════════════ const ACTIVITY_KEY = 'whga-v19-activity'; const MAX_ACTIVITY = 20; function updateActivityFeed(entry) { try { const feed = JSON.parse(localStorage.getItem(ACTIVITY_KEY) || '[]'); feed.unshift({ ...entry, time: new Date().toISOString() }); localStorage.setItem(ACTIVITY_KEY, JSON.stringify(feed.slice(0, MAX_ACTIVITY))); } catch(e) {} } function logActivity(text, color = 'cyan') { updateActivityFeed({ text, color }); } function renderActivityFeed() { const el = document.getElementById('v19ActivityFeed'); if (!el) return; try { const feed = JSON.parse(localStorage.getItem(ACTIVITY_KEY) || '[]'); if (!feed.length) { el.innerHTML = '
No recent activity.
'; return; } el.innerHTML = '
' + feed.slice(0, 8).map(a => `
${escHtml(a.text)}
${timeAgo(a.time)}
`).join('') + '
'; } catch(e) { el.innerHTML = '
'; } } function timeAgo(iso) { const d = Date.now() - new Date(iso).getTime(); if (d < 60000) return 'just now'; if (d < 3600000) return Math.floor(d/60000) + 'm ago'; if (d < 86400000) return Math.floor(d/3600000) + 'h ago'; return Math.floor(d/86400000) + 'd ago'; } // ═══════════════════════════════════════════════════════ // V19 — ENHANCED DASHBOARD PATCH // ═══════════════════════════════════════════════════════ function v19InjectDashboardExtras() { const dash = document.getElementById('sec-dashboard'); if (!dash) return; if (dash.querySelector('#v19DashExtras')) { renderActivityFeed(); renderCompletionRings(); return; } const extras = document.createElement('div'); extras.id = 'v19DashExtras'; extras.style.cssText = 'display:grid;grid-template-columns:1fr 1fr;gap:16px;margin-top:16px;'; extras.innerHTML = `
Recent Activity
Season Completion
`; dash.appendChild(extras); renderActivityFeed(); renderCompletionRings(); } function renderCompletionRings() { const el = document.getElementById('v19CompletionRings'); if (!el) return; const seasons = [ { label: 'S1', codes: EPISODES.filter(e => e.season && e.season.includes('S1')).map(e => e.code), color: '#00e87a' }, { label: 'S2', codes: EPISODES.filter(e => e.season && e.season.includes('S2')).map(e => e.code), color: '#00cfff' }, { label: 'S3', codes: EPISODES.filter(e => e.season && e.season.includes('S3')).map(e => e.code), color: '#b06aff' }, ]; el.innerHTML = seasons.map(s => { const total = s.codes.length; if (!total) return ''; const done = s.codes.filter(c => { const d = getEpData ? getEpData(c) : {}; return (d.status || '') === 'released'; }).length; const pct = total ? Math.round(done / total * 100) : 0; const r = 28; const circ = 2 * Math.PI * r; const fill = circ * (pct / 100); return `
${pct}% ${s.label} ${done}/${total}
`; }).join(''); } // ═══════════════════════════════════════════════════════ // V19 — VERSION HISTORY HOOK INTO EPISODE EDITOR // ═══════════════════════════════════════════════════════ function v19InjectVersionBar(code) { setTimeout(() => { const pane = document.getElementById('epEditorPane'); if (!pane || pane.querySelector('#v19VersionBtn')) return; const ep = EPISODES.find(e => e.code === code); if (!ep) return; const bar = document.createElement('div'); bar.id = 'v19VersionBtn'; bar.style.cssText = 'margin-top:12px;display:flex;gap:8px;align-items:center;padding:10px;background:var(--bg2);border-radius:8px;border:1px solid var(--border);'; bar.innerHTML = ` V19 Version Control `; pane.insertBefore(bar, pane.firstChild); _currentVersionEp = code; }, 120); } // Hook openEpisode safely — only if it exists in base app (function() { if (typeof openEpisode !== 'function') return; const _baseOpenEpisode = openEpisode; openEpisode = function(code) { _baseOpenEpisode(code); v19InjectVersionBar(code); }; })(); // ═══════════════════════════════════════════════════════ // V19 — INIT // ═══════════════════════════════════════════════════════ (function v19Init() { const run = () => { initTheme(); logActivity('Command Brain v19 loaded', 'green'); buildSearchIndex(); const sc = localStorage.getItem(SCRATCHPAD_KEY); if (sc) document.getElementById('scratchpadText').value = sc; // Inject dashboard extras after base dashboard has rendered setTimeout(v19InjectDashboardExtras, 400); }; if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', run); } else { setTimeout(run, 200); } })(); // ===== END V19 ADDITIONS ===== // ═══════════════════════════════════════════════════════ // V20 — SCENE BREAKDOWN PARSER // ═══════════════════════════════════════════════════════ function parserLoadFromEpisode() { const code = document.getElementById('parserEpSelect').value; if (!code) return; const ep = EPISODES.find(e => e.code === code); if (!ep) return; const allScripts = getAllScripts ? getAllScripts() : {}; const text = allScripts[ep.scriptKey] || ''; if (!text) { showToast('No script loaded for ' + code, 'amber'); return; } document.getElementById('parserInput').value = text; } function runSceneParser() { const raw = document.getElementById('parserInput').value; const out = document.getElementById('parserOutput'); const stat = document.getElementById('parserStatus'); if (!raw.trim()) { showToast('No text to parse', 'amber'); return; } const lines = raw.split('\n'); const scenes = []; let currentScene = null; const charSet = new Set(); // Heading patterns: INT./EXT. or just a line in ALL CAPS that looks like a scene heading const headingRe = /^\s*(INT\.|EXT\.|INT\/EXT\.?|I\/E\.?)\s*(.+?)\s*[-—–]\s*(DAY|NIGHT|DAWN|DUSK|CONTINUOUS|LATER|MOMENTS LATER|EVENING|MORNING)?/i; const charRe = /^\s{10,}([A-Z][A-Z\s\-'\.]+[A-Z])\s*(\(.*\))?\s*$/; lines.forEach((line, i) => { const hMatch = headingRe.exec(line); if (hMatch) { if (currentScene) scenes.push(currentScene); currentScene = { num: scenes.length + 1, full: line.trim(), intExt: hMatch[1].replace('.','').toUpperCase(), location: hMatch[2].trim(), time: (hMatch[3] || 'UNSPECIFIED').toUpperCase(), chars: [], lines: [], startLine: i }; return; } if (!currentScene) { // Pre-scenes preamble — treat as scene 0 currentScene = { num: 0, full: 'PREAMBLE', intExt: '—', location: 'Pre-script', time: '—', chars: [], lines: [], startLine: 0 }; } const cMatch = charRe.exec(line); if (cMatch) { const name = cMatch[1].trim(); if (name.length > 1 && name.length < 40 && !name.match(/^(THE END|FADE IN|FADE OUT|CUT TO|SMASH CUT|DISSOLVE|CONTINUED|MORE|CONT'D)$/)) { if (!currentScene.chars.includes(name)) currentScene.chars.push(name); charSet.add(name); } } currentScene.lines.push(line); }); if (currentScene) scenes.push(currentScene); const real = scenes.filter(s => s.num > 0); stat.textContent = real.length + ' scenes · ' + charSet.size + ' characters detected'; if (!real.length) { out.innerHTML = '
No scene headings detected. Make sure INT./EXT. headings are present, or the script uses all-caps scene slugs.
'; return; } out.innerHTML = '
' + real.map(s => { const intClass = s.intExt === 'INT' ? 'scene-tag-int' : s.intExt === 'EXT' ? 'scene-tag-ext' : ''; const timeClass = s.time.includes('NIGHT') || s.time.includes('DUSK') ? 'scene-tag-nite' : 'scene-tag-day'; const preview = s.lines.filter(l => l.trim()).slice(0, 3).map(l => escHtml(l.trim())).join('
'); return `
SC${String(s.num).padStart(2,'0')} ${escHtml(s.location)}
${escHtml(s.intExt)} ${escHtml(s.time)} ${s.chars.slice(0,6).map(c => `${escHtml(c)}`).join('')} ${s.chars.length > 6 ? `+${s.chars.length - 6} more` : ''}
${preview}
${s.lines.length} lines
`; }).join('') + '
'; logActivity('Scene parser: ' + real.length + ' scenes extracted', 'cyan'); } function initParserEpSelect() { const sel = document.getElementById('parserEpSelect'); if (!sel) return; sel.innerHTML = '' + EPISODES.map(e => ``).join(''); } // ═══════════════════════════════════════════════════════ // V20 — CHARACTER ARC TRACKER // ═══════════════════════════════════════════════════════ const ARC_KEY = 'whga-v20-arc-data'; const ARC_STATES = ['absent', 'present', 'peak', 'break']; const ARC_STATE_LABELS = { absent: '—', present: '●', peak: '★', break: '↯' }; function getArcStore() { try { return JSON.parse(localStorage.getItem(ARC_KEY) || '{}'); } catch(e) { return {}; } } function saveArcData() { // arc store is kept in memory via arcState object during session, persist on demand localStorage.setItem(ARC_KEY, JSON.stringify(window._arcState || {})); showToast('Arc data saved'); logActivity('Character arc data saved', 'green'); } function renderArcTable() { const season = document.getElementById('arcSeasonFilter')?.value || 'S1'; const eps = EPISODES.filter(e => e.season && e.season.includes(season)); const chars = CHARACTERS.slice(0, 7); // WHGA characters (Walter variants) if (!window._arcState) window._arcState = getArcStore(); const container = document.getElementById('arcTableContainer'); if (!container) return; // Build table let html = ''; eps.forEach(ep => { html += ``; }); html += ''; chars.forEach(ch => { html += ``; eps.forEach(ep => { const key = ch.name + '|' + ep.code; const state = (window._arcState[key] || 'absent'); const cls = 'arc-beat-' + state; const label = ARC_STATE_LABELS[state] || '—'; html += ``; }); // Notes cell const notesKey = 'notes|' + ch.name + '|' + season; const notesVal = window._arcState[notesKey] || ''; html += ``; html += ''; }); html += '
Character${escHtml(ep.code)}Notes
${escHtml(ch.name)}${label}
'; container.innerHTML = html; } function cycleArcBeat(charName, epCode) { if (!window._arcState) window._arcState = getArcStore(); const key = charName + '|' + epCode; const current = window._arcState[key] || 'absent'; const idx = ARC_STATES.indexOf(current); window._arcState[key] = ARC_STATES[(idx + 1) % ARC_STATES.length]; renderArcTable(); } function setArcNote(charName, season, val) { if (!window._arcState) window._arcState = getArcStore(); window._arcState['notes|' + charName + '|' + season] = val; } // ═══════════════════════════════════════════════════════ // V20 — LC INSERT TIMELINE // ═══════════════════════════════════════════════════════ function renderLCTimeline() { const wrap = document.getElementById('lcTimelineWrap'); if (!wrap) return; const S3_DEP = typeof WHGA_S3_DEPLOYMENT !== 'undefined' ? WHGA_S3_DEPLOYMENT : []; const allEps = EPISODES.filter(e => e.season); // Build insert map per episode const insertMap = {}; S3_DEP.forEach(d => { insertMap[d.ep] = d.placements || []; }); // LC video placement map from EPISODES const lcByEp = {}; LC_VIDEOS.forEach(v => { if (v.placement) { const epMatch = v.placement.match(/E\d+|S\d+E\d+/); if (epMatch) { if (!lcByEp[epMatch[0]]) lcByEp[epMatch[0]] = []; lcByEp[epMatch[0]].push(v); } } }); const seasonGroups = [ { label: 'Season 1', color: '#00e87a', eps: allEps.filter(e => e.season.includes('S1')) }, { label: 'Season 2', color: '#00cfff', eps: allEps.filter(e => e.season.includes('S2')) }, { label: 'Season 3', color: '#b06aff', eps: allEps.filter(e => e.season.includes('S3')) }, ]; wrap.innerHTML = seasonGroups.map(sg => { const epDots = sg.eps.map(ep => { const lcCount = (lcByEp[ep.code] || []).length; const s3Count = (insertMap[ep.code] || []).length; const total = lcCount + s3Count; const dotColor = total > 2 ? '#ff4466' : total > 0 ? sg.color : 'var(--border2)'; const inserts = [...(lcByEp[ep.code] || []).map(v => v.code + ': ' + v.title), ...(insertMap[ep.code] || []).map(p => p.mark + ' · ' + p.line)]; return `
${escHtml(ep.code)}
${inserts.slice(0,2).map(i => `
${escHtml(i.slice(0,18))}
`).join('')}
`; }).join(''); return `
${sg.label}
${epDots}
`; }).join(''); } function showLCInsertDetail(epCode) { const ep = EPISODES.find(e => e.code === epCode); const S3_DEP = typeof WHGA_S3_DEPLOYMENT !== 'undefined' ? WHGA_S3_DEPLOYMENT : []; const s3data = S3_DEP.find(d => d.ep === epCode); const detail = document.getElementById('lcTimelineDetail'); if (!detail) return; const lcVids = LC_VIDEOS.filter(v => v.placement && v.placement.includes(epCode)); detail.innerHTML = `
${escHtml(epCode)} — LC Insert Detail
${ep ? `
${escHtml(ep.title)}
${escHtml(ep.desc || '')}
` : ''} ${lcVids.length ? `
LC Video Placements
${lcVids.map(v => `
${escHtml(v.code)} ${escHtml(v.title)} · ${escHtml(v.placement || '')}
`).join('')}
` : ''} ${s3data && s3data.placements.length ? `
S3 System Inserts
${s3data.placements.map(p => `
↳ ${escHtml(p.mark)} · ${escHtml(p.line)}
`).join('')}
${escHtml(s3data.note || '')}
` : ''} ${!lcVids.length && (!s3data || !s3data.placements.length) ? '
No LC inserts mapped to this episode.
' : ''}
`; } // ═══════════════════════════════════════════════════════ // V20 — CANON CONFLICT CHECKER // ═══════════════════════════════════════════════════════ const CANON_RULES = [ { id: 'C01', severity: 'BLOCK', rule: 'One-man production — no other human faces on camera', keywords: ['guest', 'actor', 'second person', 'other person on camera', 'another actor', 'cast member'] }, { id: 'C02', severity: 'BLOCK', rule: 'Walter Sawan plays every character — no exceptions', keywords: ['played by someone else', 'another performer', 'guest star', 'co-star'] }, { id: 'C03', severity: 'BLOCK', rule: 'The notebook is a gift from Chrissy — letter on page one. NEVER reference before finale', keywords: ['chrissy', 'notebook letter', 'page one', 'the letter in'] }, { id: 'C04', severity: 'BLOCK', rule: 'Notebook burns with hat in finale — both destroyed at same time. Do not show either destroyed separately', keywords: ['notebook burns alone', 'hat burns separately', 'only the hat', 'just the notebook'] }, { id: 'C05', severity: 'WARN', rule: 'Loneliness Company is never acknowledged by Walter directly — ambient only in S3', keywords: ['walter mentions the company', 'walter confronts', 'walter talks to lc', 'walter calls the company'] }, { id: 'C06', severity: 'WARN', rule: 'Shaazadon is never fully explained — predates concept and company', keywords: ['shaazadon explained', 'shaazadon origin', 'shaazadon is actually', 'the truth about shaazadon'] }, { id: 'C07', severity: 'WARN', rule: 'S3 register is metacognitive — Walter observes his own mind from slightly above it', keywords: ['walter breaks down', 'walter loses control', 'walter acts from inside the emotion'] }, { id: 'C08', severity: 'BLOCK', rule: 'Alternate Walters in S2-GUL must never acknowledge each other', keywords: ['alternate walter speaks to', 'the other walter says', 'two walters talk'] }, { id: 'C09', severity: 'WARN', rule: 'The finale does not reveal the system — it confirms what audience already felt', keywords: ['reveals the company', 'exposes shaazadon', 'big twist reveal', 'explains everything'] }, { id: 'C10', severity: 'BLOCK', rule: 'No clean exit at end of S2 — nothing is fixed, everything is seen', keywords: ['s2 ends with resolution', 'walter is healed', 'everything is fixed', 'happy ending'] }, ]; function renderCanonRulesList() { const el = document.getElementById('canonRulesList'); if (!el) return; el.innerHTML = CANON_RULES.map(r => `
${r.id} ${escHtml(r.rule)}
`).join(''); // Also populate episode selects ['conflictEpContext','parserEpSelect','bibleScope'].forEach(id => { const sel = document.getElementById(id); if (!sel || id === 'bibleScope') return; if (!sel.querySelector('option[value="S01E01"]')) { EPISODES.forEach(ep => { const opt = document.createElement('option'); opt.value = ep.code; opt.textContent = ep.code + ' — ' + ep.title; sel.appendChild(opt); }); } }); } function runConflictCheck() { const text = (document.getElementById('conflictInput')?.value || '').toLowerCase(); const out = document.getElementById('conflictResults'); if (!text.trim()) { showToast('No text to check', 'amber'); return; } const results = []; CANON_RULES.forEach(rule => { const triggered = rule.keywords.some(kw => text.includes(kw.toLowerCase())); if (triggered) { results.push({ ...rule, triggered: true }); } }); // General checks const hasRealisticDialogue = text.length > 80; if (!results.length && hasRealisticDialogue) { results.push({ id: '✓', severity: 'OK', rule: 'No canon conflicts detected in this text.', triggered: false }); } if (!results.length) { out.innerHTML = '
CLEAR
No canon rule violations detected.
'; return; } out.innerHTML = '
' + results.map(r => { const cls = r.severity === 'BLOCK' ? 'conflict-block' : r.severity === 'WARN' ? 'conflict-warn' : 'conflict-ok'; const lcls = r.severity === 'BLOCK' ? 'block' : r.severity === 'WARN' ? 'warn' : 'ok'; const emoji = r.severity === 'BLOCK' ? '⛔ BLOCKED' : r.severity === 'WARN' ? '⚠ WARNING' : '✓ CLEAR'; return `
${emoji} · ${r.id}
${escHtml(r.rule)}
`; }).join('') + '
'; logActivity('Canon check: ' + results.filter(r => r.severity !== 'OK').length + ' issue(s) found', results.find(r => r.severity === 'BLOCK') ? 'red' : 'amber'); } // ═══════════════════════════════════════════════════════ // V20 — EPISODE BIBLE GENERATOR // ═══════════════════════════════════════════════════════ function generateBible() { const scope = document.getElementById('bibleScope').value; const incScripts = document.getElementById('bibleIncludeScripts').checked; const incChars = document.getElementById('bibleIncludeChars').checked; const incCanon = document.getElementById('bibleIncludeCanon').checked; const allScripts = getAllScripts ? getAllScripts() : {}; const out = document.getElementById('bibleOutput'); const eps = scope === 'full' ? EPISODES : EPISODES.filter(e => e.season && e.season.includes(scope)); let html = `
${eps.length} episodes compiled

WALTER HAS GONE AWAY
Series Bible${scope !== 'full' ? ' — ' + scope : ''}

Generated ${new Date().toLocaleDateString()} · HCR Production OS FINAL · One-man production · Walter Sawan

`; // Series premise html += `

Series Premise

A trauma response learning to aim itself. Three seasons of slowly turning the instrument around until it aims correctly. This is not about growth. This is a live demonstration of system architecture under load — and the accidental discovery that the most painful feature is also the most powerful one.

One-man production. Walter Sawan plays every character in the entire series. No exceptions. No guest appearances. No other faces on camera.

`; // Episodes html += `

Episode Listing

`; eps.forEach(ep => { const d = getEpData ? getEpData(ep.code) : {}; const script = allScripts[ep.scriptKey] || ''; html += `

${escHtml(ep.code)} — ${escHtml(ep.title)}

Season: ${escHtml(ep.season || '—')}  ·  Status: ${escHtml(d.status || ep.status || '—')}  ·  Progress: ${d.pct || 0}%

${escHtml(ep.desc || '—')}

${incScripts && script ? `

"${escHtml(script.slice(0,300).replace(/\s+/g,' '))}…"

` : ''}
`; }); // Characters if (incChars) { html += `

Characters

`; CHARACTERS.slice(0,7).forEach(ch => { html += `

${escHtml(ch.name)} — ${escHtml(ch.role)}

Motivation: ${escHtml(ch.motivation)}

Arc: ${escHtml(ch.arc)}

Wardrobe: ${escHtml(ch.wardrobe)}

${escHtml(ch.disturbing)}

`; }); } // Canon rules if (incCanon) { html += `

Canon Rules (Locked)

`; CANON_RULES.forEach(r => { html += `

${r.id} [${r.severity}]: ${escHtml(r.rule)}

`; }); } html += `
`; out.innerHTML = html; document.getElementById('biblePrintBtn').style.display = 'inline-flex'; logActivity('Episode Bible generated: ' + scope + ' (' + eps.length + ' eps)', 'green'); } function printBible() { const content = document.getElementById('bibleDoc'); if (!content) return; const win = window.open('', '_blank'); win.document.write('WHGA Series Bible'); win.document.write(content.innerHTML); win.document.write(''); win.document.close(); win.print(); } // ═══════════════════════════════════════════════════════ // V20 — INIT // ═══════════════════════════════════════════════════════ function initV20() { initParserEpSelect(); renderCanonRulesList(); renderArcTable(); renderLCTimeline(); // Log activity logActivity('Command Brain v21 loaded — Creative Tools active', 'purple'); showToast('V20 loaded — Scene Parser · Arc Tracker · LC Timeline · Canon Checker · Bible'); } // V20 showSection hook — consolidated at bottom of file // Boot (function() { const run = () => setTimeout(initV20, 300); if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', run); else run(); })(); // ===== END V20 ADDITIONS ===== // ═══════════════════════════════════════════════════════════════ // V21 — STRIPBOARD // ═══════════════════════════════════════════════════════════════ const STRIP_KEY = 'whga-v21-stripboard'; function getStrips(){ try{return JSON.parse(localStorage.getItem(STRIP_KEY)||'{"days":[]}');}catch(e){return{days:[]};} } function saveStripboard(){ localStorage.setItem(STRIP_KEY,JSON.stringify(window._strips||getStrips())); showToast('Stripboard saved'); logActivity('Stripboard saved','green'); } const STRIP_COLORS = { S1:'#00e87a', S2:'#00cfff', S3:'#b06aff', LC:'#ffb830', DEFAULT:'#3a5570' }; function stripColor(code){ if(code.startsWith('S01')) return STRIP_COLORS.S1; if(code.startsWith('S02')) return STRIP_COLORS.S2; if(code.startsWith('S03')) return STRIP_COLORS.S3; if(code.startsWith('LC')) return STRIP_COLORS.LC; return STRIP_COLORS.DEFAULT; } function addStripDay(){ if(!window._strips) window._strips = getStrips(); const date = prompt('Shoot date (YYYY-MM-DD or label):') || 'Day '+(window._strips.days.length+1); window._strips.days.push({id:'SD-'+Date.now(), label:date, strips:[]}); saveStripboard(); renderStripboard(); } function autoPopulateStrips(){ if(!window._strips) window._strips = getStrips(); if(!window._strips.days.length) { addStripDay(); } // Distribute episodes across existing days const allEps = EPISODES.filter(e=>e.season); const dayCount = window._strips.days.length; allEps.forEach((ep,i)=>{ const day = window._strips.days[i % dayCount]; if(!day.strips.find(s=>s.code===ep.code)){ day.strips.push({code:ep.code, title:ep.title, color:stripColor(ep.code), id:'strip-'+ep.code}); } }); saveStripboard(); renderStripboard(); showToast('Episodes distributed across '+dayCount+' shoot days'); } function renderStripboard(){ if(!window._strips) window._strips = getStrips(); const container = document.getElementById('stripboardContainer'); if(!container) return; const sel = document.getElementById('stripEpFilter'); if(sel && !sel.querySelector('option[value="S01E01"]')){ EPISODES.forEach(ep=>{ const o=document.createElement('option'); o.value=ep.code; o.textContent=ep.code+' — '+ep.title; sel.appendChild(o); }); } if(!window._strips.days.length){ container.innerHTML='
No shoot days yet. Click "+ Add Shoot Day" or "Auto-populate".
'; return; } container.innerHTML = window._strips.days.map((day,di)=>`
SHOOT DAY ${di+1} ${escHtml(day.label)} ${day.strips.length} scene${day.strips.length!==1?'s':''}
${day.strips.map(s=>`
${escHtml(s.code)}
${escHtml(s.title)}
`).join('')}
`).join(''); } let _dragStrip = null, _dragFromDay = null; function dragStrip(e,stripId,dayId){ _dragStrip=stripId; _dragFromDay=dayId; e.target.classList.add('dragging'); } function dropStrip(e,toDayId){ if(!_dragStrip) return; const from = window._strips.days.find(d=>d.id===_dragFromDay); const to = window._strips.days.find(d=>d.id===toDayId); if(!from||!to||from===to) return; const idx = from.strips.findIndex(s=>s.id===_dragStrip); if(idx<0) return; const [strip] = from.strips.splice(idx,1); to.strips.push(strip); _dragStrip=null; _dragFromDay=null; renderStripboard(); } function removeStripDay(id){ if(!confirm('Remove this shoot day?')) return; window._strips.days = window._strips.days.filter(d=>d.id!==id); saveStripboard(); renderStripboard(); } function addStripToDay(dayId){ const code = prompt('Episode code to add (e.g. S01E03):'); if(!code) return; const ep = EPISODES.find(e=>e.code===code.toUpperCase().trim()); if(!ep){ showToast('Episode not found','amber'); return; } const day = window._strips.days.find(d=>d.id===dayId); if(!day) return; day.strips.push({code:ep.code, title:ep.title, color:stripColor(ep.code), id:'strip-'+ep.code+'-'+Date.now()}); renderStripboard(); } // ═══════════════════════════════════════════════════════════════ // V21 — BREAKDOWN SHEETS // ═══════════════════════════════════════════════════════════════ const BREAKDOWN_KEY = 'whga-v21-breakdown'; function getBreakdowns(){ try{return JSON.parse(localStorage.getItem(BREAKDOWN_KEY)||'{}');}catch(e){return{};} } function saveBreakdown(epCode,sceneNum,fields){ const store = getBreakdowns(); if(!store[epCode]) store[epCode]={}; store[epCode][sceneNum] = fields; localStorage.setItem(BREAKDOWN_KEY,JSON.stringify(store)); } function loadBreakdownSheets(){ const epCode = document.getElementById('breakdownEpSelect').value; if(!epCode) return; const allScripts = getAllScripts ? getAllScripts() : {}; const ep = EPISODES.find(e=>e.code===epCode); const script = ep ? (allScripts[ep.scriptKey]||'') : ''; // Parse scenes from script const scenes = parseScenes(script); const list = document.getElementById('breakdownSceneList'); if(!list) return; if(!scenes.length){ list.innerHTML='
No scene headings found in this script.
'; return; } list.innerHTML = scenes.map((s,i)=>`
SC${String(i+1).padStart(2,'0')} ${escHtml(s.location||s.full||'Scene '+(i+1))} ${escHtml(s.intExt||'—')} / ${escHtml(s.time||'—')}
`).join(''); if(scenes.length) openBreakdownSheet(epCode,0,scenes[0].location||'Scene 1'); } function parseScenes(text){ if(!text) return []; const lines = text.split('\n'); const scenes = []; const headRe = /^\s*(INT\.|EXT\.|INT\/EXT\.?)\s*(.+?)\s*[-—–]\s*(DAY|NIGHT|DAWN|DUSK|CONTINUOUS|LATER|EVENING|MORNING)?/i; lines.forEach(line=>{ const m = headRe.exec(line); if(m) scenes.push({ full:line.trim(), intExt:m[1].replace('.','').toUpperCase(), location:m[2].trim(), time:(m[3]||'').toUpperCase() }); }); return scenes; } function openBreakdownSheet(epCode,sceneIdx,sceneLabel){ document.querySelectorAll('.breakdown-scene-row').forEach((r,i)=>r.classList.toggle('active',i===sceneIdx)); const store = getBreakdowns(); const saved = store[epCode]?.[sceneIdx] || {}; const pane = document.getElementById('breakdownSheetPane'); if(!pane) return; const fields = [ {key:'cast',label:'Cast'}, {key:'costumes',label:'Costumes'}, {key:'props',label:'Props'}, {key:'setdressing',label:'Set Dressing'}, {key:'vfx',label:'VFX / Green Screen'}, {key:'sound',label:'Sound / Music'}, {key:'location',label:'Location Notes'}, {key:'notes',label:'Other Notes'}, ]; pane.innerHTML = `
SC${String(sceneIdx+1).padStart(2,'0')} — ${escHtml(sceneLabel)}
${fields.map(f=>`
${f.label}
`).join('')}
`; } function getBreakdownFields(epCode,sceneIdx){ const fields=['cast','costumes','props','setdressing','vfx','sound','location','notes']; const result={}; fields.forEach(f=>{ const el=document.getElementById('bd-'+epCode+'-'+sceneIdx+'-'+f); if(el) result[f]=el.value; }); return result; } function saveBreakdownSheet(epCode,sceneIdx){ saveBreakdown(epCode,sceneIdx,getBreakdownFields(epCode,sceneIdx)); showToast('Breakdown saved'); } function printBreakdownSheet(epCode,sceneIdx,label){ const fields = getBreakdownFields(epCode,sceneIdx); const win=window.open('','_blank'); win.document.write('Breakdown: '+label+''); win.document.write('

BREAKDOWN SHEET: '+escHtml(label)+'

Episode: '+escHtml(epCode)+' · Scene '+(sceneIdx+1)+'

'); win.document.write(''); Object.entries(fields).forEach(([k,v])=>{ win.document.write(''); }); win.document.write('
ElementNotes
'+k+''+(v||'—')+'
'); win.document.close(); win.print(); } function exportBreakdownSheet(){ showToast('Open a breakdown sheet first, then use ⎙ Print','amber'); } // ═══════════════════════════════════════════════════════════════ // V21 — DAY-OUT-OF-DAYS // ═══════════════════════════════════════════════════════════════ const DOOD_KEY = 'whga-v21-dood'; const DOOD_STATES = [' ','W','H','T','F']; const DOOD_CHARS = CHARACTERS.slice(0,7).map(c=>c.name); function getDood(){ try{return JSON.parse(localStorage.getItem(DOOD_KEY)||'{"days":[],"cells":{}}');}catch(e){return{days:[],cells:{}};} } function saveDood(){ localStorage.setItem(DOOD_KEY,JSON.stringify(window._dood||getDood())); showToast('DOOD saved'); } function addDoodDay(){ if(!window._dood) window._dood = getDood(); const label = prompt('Shoot day label (e.g. 2026-05-01 or Day 1):') || 'Day '+(window._dood.days.length+1); window._dood.days.push({id:'DD-'+Date.now(), label}); renderDood(); } function cycleDoodCell(charIdx,dayId){ if(!window._dood) window._dood = getDood(); const key = charIdx+'|'+dayId; const cur = DOOD_STATES.indexOf(window._dood.cells[key]||' '); window._dood.cells[key] = DOOD_STATES[(cur+1)%DOOD_STATES.length]; renderDood(); } function renderDood(){ if(!window._dood) window._dood = getDood(); const container = document.getElementById('doodContainer'); if(!container) return; if(!window._dood.days.length){ container.innerHTML='
No shoot days yet. Add days to build the DOOD chart.
'; return; } let html=''; window._dood.days.forEach(d=>{ html+=``; }); html+=''; DOOD_CHARS.forEach((char,ci)=>{ html+=``; window._dood.days.forEach(d=>{ const key=ci+'|'+d.id; const state=window._dood.cells[key]||' '; html+=``; }); html+=''; }); html+='
Character${escHtml(d.label)}
${escHtml(char)}${state}
'; html+='
'; [['W','Work',DOOD_COLORS='var(--green)'],['H','Hold','var(--amber)'],['T','Travel','var(--cyan)'],['F','Finish','var(--red)']].forEach(([k,l,c])=>{ html+=`${k}${l}`; }); html+='
'; container.innerHTML=html; } function printDood(){ const tbl=document.getElementById('doodContainer')?.innerHTML||''; const win=window.open('','_blank'); win.document.write('Day Out Of Days

DAY-OUT-OF-DAYS — WHGA

'+tbl+''); win.document.close(); win.print(); } function exportDOOD(){ printDood(); } // ═══════════════════════════════════════════════════════════════ // V21 — LOCATION TRACKER // ═══════════════════════════════════════════════════════════════ const LOC_KEY = 'whga-v21-locations'; function getLocs(){ try{return JSON.parse(localStorage.getItem(LOC_KEY)||'[]');}catch(e){return[];} } function addLocation(){ const name=document.getElementById('locName').value.trim(); if(!name){ showToast('Location name required','amber'); return; } const locs=getLocs(); locs.push({ id:'LOC-'+Date.now(), name, address: document.getElementById('locAddress').value.trim(), type: document.getElementById('locType').value, permit: document.getElementById('locPermit').value, notes: document.getElementById('locNotes').value.trim(), episodes:document.getElementById('locEpisodes').value.trim(), photo: document.getElementById('locPhoto').value.trim(), createdAt: new Date().toISOString() }); localStorage.setItem(LOC_KEY, JSON.stringify(locs)); ['locName','locAddress','locNotes','locEpisodes','locPhoto'].forEach(id=>{ const el=document.getElementById(id); if(el) el.value=''; }); renderLocationCards(); showToast('Location added: '+name); logActivity('Location added: '+name,'cyan'); } function renderLocationCards(){ const locs=getLocs(); const container=document.getElementById('locationCards'); if(!container) return; if(!locs.length){ container.innerHTML='
No locations yet.
'; return; } container.innerHTML=locs.map(loc=>{ const permitClass='permit-'+loc.permit.replace('-needed','').replace('none','none'); const permitLabels={'none-needed':'No permit','pending':'Permit pending','approved':'Permit approved','denied':'Denied'}; return `
${escHtml(loc.name)}
${escHtml(loc.address||'—')} · ${escHtml(loc.type||'—')}
${permitLabels[loc.permit]||loc.permit}
${loc.photo?`Location photo`:''} ${loc.notes?`
${escHtml(loc.notes)}
`:''} ${loc.episodes?`
Episodes: ${escHtml(loc.episodes)}
`:''}
`; }).join(''); } function deleteLocation(id){ if(!confirm('Remove location?')) return; const locs=getLocs().filter(l=>l.id!==id); localStorage.setItem(LOC_KEY,JSON.stringify(locs)); renderLocationCards(); } // ═══════════════════════════════════════════════════════════════ // V21 — CONTINUITY BOARD // ═══════════════════════════════════════════════════════════════ const CONT_KEY='whga-v21-continuity'; function getCont(){ try{return JSON.parse(localStorage.getItem(CONT_KEY)||'[]');}catch(e){return[];} } function showAddContinuityItem(){ const f=document.getElementById('addContinuityForm'); if(!f) return; f.style.display = f.style.display==='none'?'block':'none'; if(f.style.display==='block') f.innerHTML=`
Add Continuity Item
`; } function addContinuityItem(){ const items=getCont(); items.push({ id:'CONT-'+Date.now(), cat:document.getElementById('contCat').value, char:document.getElementById('contChar').value.trim(), item:document.getElementById('contItem').value.trim()||'Untitled', ep:document.getElementById('contEp').value, note:document.getElementById('contNote').value.trim(), status:document.getElementById('contStatus').value, createdAt:new Date().toISOString() }); localStorage.setItem(CONT_KEY,JSON.stringify(items)); document.getElementById('addContinuityForm').style.display='none'; renderContinuityBoard(); showToast('Continuity item added'); } function saveContinuity(){ showToast('Continuity auto-saved to localStorage'); } function renderContinuityBoard(){ const items=getCont(); const epFilter=document.getElementById('contEpFilter')?.value||''; const catFilter=document.getElementById('contCatFilter')?.value||''; const filtered=items.filter(i=>((!epFilter||i.ep===epFilter)&&(!catFilter||i.cat===catFilter))); const board=document.getElementById('continuityBoard'); if(!board) return; if(!filtered.length){ board.innerHTML='
No continuity items yet. Add items to track wardrobe, props, and set state across episodes.
'; return; } const cats=['wardrobe','props','set','hair','other']; const catColors={wardrobe:'var(--cyan)',props:'var(--amber)',set:'var(--purple)',hair:'var(--green)',other:'var(--dim)'}; board.innerHTML=cats.map(cat=>{ const catItems=filtered.filter(i=>i.cat===cat); if(!catItems.length) return ''; return `
${cat}
${catItems.map(i=>`
${escHtml(i.item)}
${escHtml(i.char||'—')} · ${escHtml(i.ep)}
${i.note?`
${escHtml(i.note)}
`:''}
${i.status}
`).join('')}
`; }).join(''); } // ═══════════════════════════════════════════════════════════════ // V21 — POST PIPELINE // ═══════════════════════════════════════════════════════════════ const POST_KEY='whga-v21-post'; const POST_STAGES=['Shot','Rough Cut','Fine Cut','Color','Audio Mix','VFX','Music','Export','Review','Released']; function getPost(){ try{return JSON.parse(localStorage.getItem(POST_KEY)||'{}');}catch(e){return{};} } function savePostPipeline(){ localStorage.setItem(POST_KEY,JSON.stringify(window._post||getPost())); showToast('Pipeline saved'); } function cyclePostStage(epCode,stage){ if(!window._post) window._post=getPost(); if(!window._post[epCode]) window._post[epCode]={}; const states=['todo','inprog','done','skip']; const cur=states.indexOf(window._post[epCode][stage]||'todo'); window._post[epCode][stage]=states[(cur+1)%states.length]; renderPostPipeline(); } function renderPostPipeline(){ if(!window._post) window._post=getPost(); const container=document.getElementById('postPipelineContainer'); if(!container) return; const stateIcons={todo:'○',inprog:'◑',done:'●',skip:'✕'}; const stateLabels={todo:'To do',inprog:'In progress',done:'Done',skip:'Skip'}; let html=``; POST_STAGES.forEach(s=>{ html+=``; }); html+=''; EPISODES.forEach(ep=>{ const d=window._post[ep.code]||{}; const done=POST_STAGES.filter(s=>(d[s]||'todo')==='done').length; const pct=Math.round(done/POST_STAGES.length*100); html+=``; POST_STAGES.forEach(s=>{ const state=d[s]||'todo'; html+=``; }); html+=``; }); html+='
Episode${escHtml(s)}Progress
${escHtml(ep.code)}
${escHtml(ep.title.slice(0,20))}
${stateIcons[state]}
${pct}%
'; container.innerHTML=html; document.getElementById('postPipelineStatus').textContent='Click any stage to cycle: ○ To do → ◑ In progress → ● Done → ✕ Skip'; } // ═══════════════════════════════════════════════════════════════ // V21 — RELEASE SCHEDULER // ═══════════════════════════════════════════════════════════════ const REL_KEY='whga-v21-releases'; function getReleases(){ try{return JSON.parse(localStorage.getItem(REL_KEY)||'[]');}catch(e){return[];} } function addRelease(){ const ep=document.getElementById('relEpSelect').value; const epObj=EPISODES.find(e=>e.code===ep); const releases=getReleases(); releases.push({ id:'REL-'+Date.now(), ep, epTitle:epObj?.title||ep, platform:document.getElementById('relPlatform').value, date:document.getElementById('relDate').value, status:document.getElementById('relStatus').value, notes:document.getElementById('relNotes').value.trim(), createdAt:new Date().toISOString() }); localStorage.setItem(REL_KEY,JSON.stringify(releases)); renderReleaseSchedule(); showToast('Release scheduled'); logActivity('Release scheduled: '+ep,'green'); } function renderReleaseSchedule(){ const releases=getReleases().sort((a,b)=>a.date.localeCompare(b.date)); const container=document.getElementById('releaseScheduleList'); if(!container) return; if(!releases.length){ container.innerHTML='
No releases scheduled yet.
'; return; } const platLabels={youtube:'YouTube',instagram:'Instagram',tiktok:'TikTok',vimeo:'Vimeo',website:'Website',other:'Other'}; const statusPill={scheduled:'pill-planning',ready:'pill-editing','published':'pill-released',delayed:'pill-seed'}; container.innerHTML=releases.map(r=>`
${r.date||'TBD'}
${escHtml(r.epTitle||r.ep)}
${r.notes?`
${escHtml(r.notes)}
`:''}
${platLabels[r.platform]||r.platform}
${r.status}
`).join(''); } function deleteRelease(id){ const releases=getReleases().filter(r=>r.id!==id); localStorage.setItem(REL_KEY,JSON.stringify(releases)); renderReleaseSchedule(); } // ═══════════════════════════════════════════════════════════════ // V21 — MUSIC CUE SHEET // ═══════════════════════════════════════════════════════════════ const CUE_KEY='whga-v21-cues'; function getCues(){ try{return JSON.parse(localStorage.getItem(CUE_KEY)||'[]');}catch(e){return[];} } function addMusicCue(){ const cueName=document.getElementById('cueName').value.trim(); if(!cueName){ showToast('Cue name required','amber'); return; } const cues=getCues(); cues.push({ id:'CUE-'+Date.now(), ep:document.getElementById('cueEpSelect').value, name:cueName, tcIn:document.getElementById('cueIn').value.trim(), tcOut:document.getElementById('cueOut').value.trim(), mood:document.getElementById('cueMood').value, source:document.getElementById('cueSource').value.trim(), rights:document.getElementById('cueRights').value, notes:document.getElementById('cueNotes').value.trim(), createdAt:new Date().toISOString() }); localStorage.setItem(CUE_KEY,JSON.stringify(cues)); ['cueName','cueIn','cueOut','cueSource','cueNotes'].forEach(id=>{ const el=document.getElementById(id); if(el) el.value=''; }); renderMusicCues(); showToast('Cue added: '+cueName); } function renderMusicCues(){ const epFilter=document.getElementById('cueListFilter')?.value||''; const cues=getCues().filter(c=>!epFilter||c.ep===epFilter); const container=document.getElementById('musicCueList'); if(!container) return; if(!cues.length){ container.innerHTML='
No cues yet.
'; return; } const rightsClass={'original':'rights-ok','licensed':'rights-ok','royalty-free':'rights-ok','clearance-needed':'rights-needed','public-domain':'rights-ok'}; container.innerHTML=cues.map(c=>`
${escHtml(c.name)}
${escHtml(c.ep)} · ${escHtml(c.source||'—')}
${escHtml(c.tcIn||'—')}
${escHtml(c.tcOut||'—')}
${escHtml(c.mood)} ${escHtml(c.rights)}
`).join(''); } function deleteCue(id){ const cues=getCues().filter(c=>c.id!==id); localStorage.setItem(CUE_KEY,JSON.stringify(cues)); renderMusicCues(); } function printMusicCues(){ const epFilter=document.getElementById('cueListFilter')?.value||''; const cues=getCues().filter(c=>!epFilter||c.ep===epFilter); const win=window.open('','_blank'); win.document.write('Music Cue Sheet

MUSIC CUE SHEET — WHGA

'); win.document.write(''); cues.forEach(c=>{ win.document.write(``); }); win.document.write('
CueEpisodeTC InTC OutMoodSourceRights
${c.name}${c.ep}${c.tcIn||'—'}${c.tcOut||'—'}${c.mood}${c.source||'—'}${c.rights}
'); win.document.close(); win.print(); } function exportMusicCues(){ printMusicCues(); } // ═══════════════════════════════════════════════════════════════ // V21 — SIDES GENERATOR // ═══════════════════════════════════════════════════════════════ function loadSidesScenes(){ const epCode=document.getElementById('sidesEpSelect').value; if(!epCode) return; const ep=EPISODES.find(e=>e.code===epCode); const allScripts=getAllScripts?getAllScripts():{}; const script=ep?(allScripts[ep.scriptKey]||''):''; const scenes=parseScenes(script); const box=document.getElementById('sidesSceneCheckboxes'); if(!box) return; if(!scenes.length){ box.innerHTML='
No scenes found.
'; return; } box.innerHTML=scenes.map((s,i)=>` `).join(''); } function generateSides(){ const epCode=document.getElementById('sidesEpSelect').value; const date=document.getElementById('sidesDate').value; const note=document.getElementById('sidesNote').value; if(!epCode){ showToast('Select an episode','amber'); return; } const ep=EPISODES.find(e=>e.code===epCode); const allScripts=getAllScripts?getAllScripts():{}; const script=ep?(allScripts[ep.scriptKey]||''):''; const scenes=parseScenes(script); const checked=[...document.querySelectorAll('#sidesSceneCheckboxes input:checked')].map(el=>parseInt(el.value)); const selected=scenes.filter((_,i)=>checked.includes(i)); const preview=document.getElementById('sidesPreview'); if(!preview) return; preview.innerHTML=`
SIDES
${escHtml(ep?.title||epCode)} · ${date||'Date TBD'}
${note?`
${escHtml(note)}
`:''}
${selected.map((s,i)=>`
SC${String(i+1).padStart(2,'0')} — ${escHtml(s.intExt)} ${escHtml(s.location)} — ${escHtml(s.time)}
${escHtml(s.full)}
`).join('')}
WHGA COMMAND BRAIN v21 · ${new Date().toLocaleDateString()}
`; } // ═══════════════════════════════════════════════════════════════ // V21 — READ-THROUGH MODE // ═══════════════════════════════════════════════════════════════ function loadReadThrough(){ const epCode=document.getElementById('readEpSelect').value; if(!epCode){ showToast('Select an episode','amber'); return; } const ep=EPISODES.find(e=>e.code===epCode); const allScripts=getAllScripts?getAllScripts():{}; const script=ep?(allScripts[ep.scriptKey]||''):''; if(!script.trim()){ showToast('No script loaded for this episode','amber'); return; } const display=document.getElementById('readThroughDisplay'); if(!display) return; // Extract dialogue only const lines=script.split('\n'); const charRe=/^\s{10,}([A-Z][A-Z\s\-'\.]{1,30}[A-Z])\s*(\(.*\))?\s*$/; const parenRe=/^\s*\(.*\)\s*$/; let html=''; let inDialogue=false; let currentChar=''; lines.forEach(line=>{ if(charRe.test(line)){ currentChar=line.trim().replace(/\(.*\)/,'').trim(); inDialogue=true; html+=`
${escHtml(currentChar)}`; } else if(inDialogue && parenRe.test(line)){ html+=`${escHtml(line.trim())}`; } else if(inDialogue && line.trim()){ html+=`${escHtml(line.trim())}
`; inDialogue=false; } else { inDialogue=false; } }); display.innerHTML=html||'
No dialogue structure detected. Script may be in prose/monologue format.
'; applyReadThroughFont(); } function applyReadThroughFont(){ const size=document.getElementById('readFontSize')?.value||'20'; const display=document.getElementById('readThroughDisplay'); if(display) display.style.fontSize=size+'px'; } function toggleReadThroughFullscreen(){ const display=document.getElementById('readThroughDisplay'); if(!display) return; if(!document.fullscreenElement){ display.requestFullscreen && display.requestFullscreen(); } else { document.exitFullscreen && document.exitFullscreen(); } } // ═══════════════════════════════════════════════════════════════ // V22 — SCRIPT FORMATTER // ═══════════════════════════════════════════════════════════════ function fmtLoadEpisode(){ const ep=EPISODES.find(e=>e.code===document.getElementById('fmtEpSelect').value); if(!ep) return; const allScripts=getAllScripts?getAllScripts():{}; document.getElementById('fmtInput').value=allScripts[ep.scriptKey]||''; } function runFormatter(){ const raw=document.getElementById('fmtInput').value; const style=document.getElementById('fmtStyle').value; const out=document.getElementById('fmtOutput'); if(!raw.trim()){ showToast('No text to format','amber'); return; } if(style==='av'){ out.innerHTML=buildAVScriptFromText(raw); return; } if(style==='prose'){ out.innerHTML=buildProseFormat(raw); return; } // Screenplay format const lines=raw.split('\n'); let html='
'; const headRe=/^\s*(INT\.|EXT\.|INT\/EXT\.?)\s*(.+)/i; const charRe=/^\s{8,}([A-Z][A-Z\s\-'\.]{1,30})\s*$/; const parenRe=/^\s*\(.*\)\s*$/; const transRe=/^\s*(FADE IN|FADE OUT|CUT TO|SMASH CUT|DISSOLVE|CONTINUED)/i; lines.forEach(line=>{ if(!line.trim()){ html+='
'; return; } if(headRe.test(line)){ html+=`
${escHtml(line.trim())}
`; return; } if(charRe.test(line)){ html+=`
${escHtml(line.trim())}
`; return; } if(parenRe.test(line)){ html+=`
${escHtml(line.trim())}
`; return; } if(transRe.test(line)){ html+=`
${escHtml(line.trim())}
`; return; } html+=`
${escHtml(line.trim())}
`; }); html+='
'; out.innerHTML=html; } function buildProseFormat(text){ return `
${text.split('\n\n').map(para=>`

${escHtml(para.trim())}

`).join('')}
`; } function buildAVScriptFromText(text){ const lines=text.split('\n').filter(l=>l.trim()); const headRe=/^\s*(INT\.|EXT\.)\s*(.+)/i; let rows=''; lines.forEach(line=>{ if(headRe.test(line)){ rows+=`
${escHtml(line.trim())}
`; } else if(/^\s{8,}[A-Z]/.test(line)){ rows+=`
${escHtml(line.trim())}
`; } else { rows+=`
${escHtml(line.trim())}
`; } }); return `
VIDEO / ACTION
AUDIO / VO / DIALOGUE
${rows}
`; } function buildAVFormat(){ const ep=EPISODES.find(e=>e.code===document.getElementById('avEpSelect').value); if(!ep){ showToast('Select an episode','amber'); return; } const allScripts=getAllScripts?getAllScripts():{}; const script=allScripts[ep.scriptKey]||''; const container=document.getElementById('avScriptContainer'); if(!container) return; container.innerHTML=buildAVScriptFromText(script)||'
No script loaded.
'; } function printAVScript(){ const html=document.getElementById('avScriptContainer')?.innerHTML||''; const win=window.open('','_blank'); win.document.write('A/V Script'); win.document.write('

A/V SCRIPT — WHGA

'+html); win.document.write(''); win.document.close(); win.print(); } // ═══════════════════════════════════════════════════════════════ // V22 — REVISION COLORS // ═══════════════════════════════════════════════════════════════ const REVISION_DRAFTS=[ {name:'White', hex:'#ffffff', num:1, meaning:'Original draft'}, {name:'Blue', hex:'#a8c4e0', num:2, meaning:'First revision'}, {name:'Pink', hex:'#f4a7b9', num:3, meaning:'Second revision'}, {name:'Yellow', hex:'#f7e68a', num:4, meaning:'Third revision'}, {name:'Green', hex:'#a8d8a8', num:5, meaning:'Fourth revision'}, {name:'Goldenrod', hex:'#daa520', num:6, meaning:'Fifth revision'}, {name:'Buff', hex:'#f5deb3', num:7, meaning:'Sixth revision'}, {name:'Salmon', hex:'#fa8072', num:8, meaning:'Seventh revision'}, {name:'Cherry', hex:'#dc143c', num:9, meaning:'Eighth revision (major)'}, {name:'Tan', hex:'#d2b48c', num:10,meaning:'Ninth revision'}, {name:'Ivory', hex:'#fffff0', num:11,meaning:'Tenth revision (locked)'} ]; const REV_KEY='whga-v22-revisions'; function getRevisions(){ try{return JSON.parse(localStorage.getItem(REV_KEY)||'{}');}catch(e){return{};} } function renderRevisionColors(){ const el=document.getElementById('revisionColorChart'); if(!el) return; el.innerHTML=REVISION_DRAFTS.map(d=>`
${d.name} Draft ${d.num} — ${d.meaning}
`).join(''); renderRevisionLog(); } function renderRevisionLog(){ const el=document.getElementById('revisionEpLog'); if(!el) return; const revs=getRevisions(); el.innerHTML=EPISODES.map(ep=>{ const idx=revs[ep.code]||0; const draft=REVISION_DRAFTS[idx]||REVISION_DRAFTS[0]; return `
${ep.code}
${draft.name} (Draft ${draft.num})
`; }).join(''); } function advanceRevision(){ const epCode=document.getElementById('revEpSelect').value; if(!epCode){ showToast('Select an episode','amber'); return; } const revs=getRevisions(); const current=revs[epCode]||0; if(current>=REVISION_DRAFTS.length-1){ showToast('Already at final draft color','amber'); return; } revs[epCode]=(current+1); localStorage.setItem(REV_KEY,JSON.stringify(revs)); const next=REVISION_DRAFTS[revs[epCode]]; renderRevisionLog(); showToast(epCode+' advanced to '+next.name+' (Draft '+next.num+')'); logActivity('Revision advanced: '+epCode+' → '+next.name,'amber'); } // ═══════════════════════════════════════════════════════════════ // V22 — AI SCRIPT ASSISTANT (OpenAI) // ═══════════════════════════════════════════════════════════════ const AI_KEY_STORE='whga-v22-openai-key'; const AI_USAGE_KEY='whga-v22-ai-usage'; let _aiCallsToday=0; let _aiCostToday=0; function getAIKey(){ return localStorage.getItem(AI_KEY_STORE)||''; } function saveAIKey(){ const key=document.getElementById('aiKeyInput').value.trim(); if(!key.startsWith('sk-')){ showToast('Invalid key format — should start with sk-','red'); return; } localStorage.setItem(AI_KEY_STORE,key); document.getElementById('aiSetupBanner').style.display='none'; document.getElementById('aiKeyStatus').textContent='Key saved.'; showToast('OpenAI key saved'); logActivity('OpenAI API key configured','green'); } function clearAIKey(){ localStorage.removeItem(AI_KEY_STORE); showToast('Key cleared'); } const AI_TASK_HINTS={ punch:'Paste the dialogue or monologue you want punched up. The AI will rewrite it sharper, more Walter, more precise.', scene:'Describe the scene you need: who, where, what emotional state. The AI will draft it.', audit:'Paste any script text or story idea. The AI will flag canon violations using all locked WHGA rules.', cold:'Describe the episode. The AI will write a cold open in WHGA voice.', lc:'Describe the beat in the episode. The AI will write a Loneliness Company insert to mirror it.', rewrite:'Paste any rough text. The AI will rewrite it in Walter\'s voice — precise, self-aware, no performance.', breakdown:'Paste a scene. The AI will suggest cast, props, costumes, VFX, and sound elements.' }; function updateAIPromptHint(){ const task=document.getElementById('aiTask')?.value||'punch'; const hint=document.getElementById('aiPromptHint'); if(!hint) return; hint.textContent=AI_TASK_HINTS[task]||''; } function buildAISystemPrompt(task,epContext){ const ep=EPISODES.find(e=>e.code===epContext); const base=`You are a script development assistant for WHGA (Walter Has Gone Away), a one-man web series. Walter Sawan plays every character. The show's voice is: precise, self-aware, no performance, no inspiration porn, pattern recognition as superpower. The Loneliness Company is a corporate entity that uses wellness language to keep people stuck. Shaazadon is above them — never explained. Canon rules: one-man production (no other faces), Walter plays everyone, notebook from Chrissy burns with hat in finale (never reference before finale), Shaazadon is never explained, S3 Walter observes his own mind from slightly above it.`; if(ep) return base+`\n\nCurrent episode context: ${ep.code} — ${ep.title}. ${ep.desc||''}`; return base; } function buildAIUserPrompt(task,userInput){ const prompts={ punch:`Punch up this dialogue. Make it sharper, more precise, more Walter. Remove any performance. Every word should earn its place:\n\n${userInput}`, scene:`Write a scene for WHGA based on this description:\n\n${userInput}`, audit:`Audit this text for WHGA canon violations. List each violation with the rule it breaks and severity (BLOCK or WARN):\n\n${userInput}`, cold:`Write a cold open for a WHGA episode based on this:\n\n${userInput}`, lc:`Write a Loneliness Company cutaway insert (max 30 seconds, corporate wellness tone, subtly sinister) for this emotional beat:\n\n${userInput}`, rewrite:`Rewrite this in Walter's voice — self-aware, precise, no inspiration rhetoric, no performance:\n\n${userInput}`, breakdown:`List the production breakdown elements for this scene — cast, costumes, props, set dressing, VFX, sound:\n\n${userInput}` }; return prompts[task]||userInput; } async function runAI(){ const key=getAIKey(); if(!key){ document.getElementById('aiSetupBanner').style.display='block'; return; } const task=document.getElementById('aiTask').value; const epContext=document.getElementById('aiEpContext').value; const userInput=document.getElementById('aiInput').value.trim(); if(!userInput){ showToast('Enter some text first','amber'); return; } const out=document.getElementById('aiOutput'); const status=document.getElementById('aiStatus'); const btn=document.getElementById('aiRunBtn'); out.innerHTML='
Thinking…
'; status.textContent='Calling OpenAI…'; btn.disabled=true; try{ const res=await fetch('https://api.openai.com/v1/chat/completions',{ method:'POST', headers:{'Content-Type':'application/json','Authorization':'Bearer '+key}, body:JSON.stringify({ model:'gpt-4o-mini', max_tokens:900, messages:[ {role:'system', content:buildAISystemPrompt(task,epContext)}, {role:'user', content:buildAIUserPrompt(task,userInput)} ] }) }); if(!res.ok){ const err=await res.json(); throw new Error(err.error?.message||'API error '+res.status); } const data=await res.json(); const text=data.choices?.[0]?.message?.content||'No response.'; const tokens=data.usage?.total_tokens||0; const cost=(tokens/1000)*0.00015; _aiCallsToday++; _aiCostToday+=cost; out.innerHTML='
'+escHtml(text)+'
'; status.textContent='Done · '+tokens+' tokens'; document.getElementById('aiCostEstimate').textContent='~$'+cost.toFixed(4); document.getElementById('aiCreditTracker').textContent='Calls today: '+_aiCallsToday+' · Est. cost: $'+_aiCostToday.toFixed(4); document.getElementById('aiOutputActions').style.display='flex'; logActivity('AI: '+task+' — '+tokens+' tokens','purple'); // Store output for later use window._lastAIOutput=text; window._lastAIEp=epContext; } catch(err){ out.innerHTML='
Error: '+escHtml(err.message)+'
'; status.textContent='Failed'; showToast('AI error: '+err.message,'red'); } finally { btn.disabled=false; } } function copyAIOutput(){ const text=window._lastAIOutput||''; if(!text){ showToast('Nothing to copy','amber'); return; } navigator.clipboard.writeText(text).then(()=>showToast('Copied to clipboard')); } function appendAIToScript(){ if(!window._lastAIOutput||!window._lastAIEp) return; const ep=EPISODES.find(e=>e.code===window._lastAIEp); if(!ep){ showToast('No episode context — cannot append','amber'); return; } if(!state.episodeData[ep.scriptKey]) state.episodeData[ep.scriptKey]={}; state.episodeData[ep.scriptKey].script=(state.episodeData[ep.scriptKey].script||'')+'\n\n'+window._lastAIOutput; save(); showToast('Appended to '+ep.code+' script'); } function replaceScriptWithAI(){ if(!window._lastAIOutput||!window._lastAIEp) return; if(!confirm('Replace entire script with AI output?')) return; const ep=EPISODES.find(e=>e.code===window._lastAIEp); if(!ep) return; if(!state.episodeData[ep.scriptKey]) state.episodeData[ep.scriptKey]={}; state.episodeData[ep.scriptKey].script=window._lastAIOutput; save(); showToast('Script replaced for '+ep.code); } // ═══════════════════════════════════════════════════════════════ // V22 — ANALYTICS DASHBOARD // ═══════════════════════════════════════════════════════════════ function renderAnalytics(){ const container=document.getElementById('analyticsContainer'); if(!container) return; const allEps=EPISODES; const released=allEps.filter(e=>{ const d=getEpData?getEpData(e.code):{}; return (d.status||e.status)==='released'; }); const inProgress=allEps.filter(e=>{ const d=getEpData?getEpData(e.code):{}; const s=d.status||e.status; return ['editing','filming','rewrite'].includes(s); }); const allScripts=getAllScripts?getAllScripts():{}; const totalWords=Object.values(allScripts).reduce((n,t)=>n+(t?t.split(/\s+/).filter(Boolean).length:0),0); const cues=getCues().length; const locs=getLocs().length; container.innerHTML=`
${released.length}
Released
${inProgress.length}
In Progress
${totalWords.toLocaleString()}
Total Words Written
${allEps.length}
Total Episodes
Episode Completion
${allEps.map(ep=>{ const d=getEpData?getEpData(ep.code):{}; const pct=d.pct||0; return `
${escHtml(ep.code)}
${pct}%
`; }).join('')}
Production Summary
📍 Locations scouted: ${locs}
♫ Music cues logged: ${cues}
✎ Scripts with content: ${Object.values(allScripts).filter(s=>s&&s.trim().length>50).length}
📋 Breakdown sheets: ${Object.keys(getBreakdowns()).length} episodes
🎬 Releases scheduled: ${getReleases().length}
📦 Post pipeline tracked: ${Object.keys(getPost()).length} episodes
Season Progress
${[['S1','#00e87a'],['S2','#00cfff'],['S3','#b06aff']].map(([s,c])=>{ const eps=allEps.filter(e=>e.season&&e.season.includes(s)); const done=eps.filter(e=>{ const d=getEpData?getEpData(e.code):{}; return (d.status||e.status)==='released'; }).length; const pct=eps.length?Math.round(done/eps.length*100):0; return `
${s}
${pct}%
`; }).join('')}
`; } // ═══════════════════════════════════════════════════════════════ // V22 — EXPORT SUITE HELPERS // ═══════════════════════════════════════════════════════════════ function exportAllScripts(){ const allScripts=getAllScripts?getAllScripts():{}; let txt='WHGA — ALL SCRIPTS\nExported: '+new Date().toLocaleString()+'\n\n'; EPISODES.forEach(ep=>{ const s=allScripts[ep.scriptKey]||''; if(!s.trim()) return; txt+='═══════════════════════════\n'+ep.code+' — '+ep.title+'\n═══════════════════════════\n\n'+s+'\n\n'; }); const blob=new Blob([txt],{type:'text/plain'}); const a=document.createElement('a'); a.href=URL.createObjectURL(blob); a.download='WHGA_all_scripts_'+new Date().toISOString().slice(0,10)+'.txt'; a.click(); showToast('Scripts exported as text file'); } function renderExportQuickStats(){ const el=document.getElementById('exportQuickStats'); if(!el) return; const allScripts=getAllScripts?getAllScripts():{}; const words=Object.values(allScripts).reduce((n,t)=>n+(t?t.split(/\s+/).filter(Boolean).length:0),0); el.innerHTML=[ ['Total episodes', EPISODES.length], ['Scripts with content', Object.values(allScripts).filter(s=>s&&s.trim().length>50).length], ['Total words written', words.toLocaleString()], ['Music cues', getCues().length], ['Locations', getLocs().length], ['Releases scheduled', getReleases().length], ].map(([l,v])=>`
${v} — ${l}
`).join(''); } // ═══════════════════════════════════════════════════════════════ // V22 — GITHUB GIST CLOUD SYNC // ═══════════════════════════════════════════════════════════════ const GIST_TOKEN_KEY='whga-v22-gist-token'; const GIST_ID_KEY='whga-v22-gist-id'; const GIST_SETTINGS_KEY='whga-v22-gist-settings'; let _gistAutoSyncTimer=null; function getGistConfig(){ return { token: localStorage.getItem(GIST_TOKEN_KEY)||'', gistId: localStorage.getItem(GIST_ID_KEY)||'', settings: JSON.parse(localStorage.getItem(GIST_SETTINGS_KEY)||'{"autoSync":false,"syncOnSave":true}') }; } async function setupGist(){ const token=document.getElementById('gistTokenInput').value.trim(); const existingId=document.getElementById('gistIdInput').value.trim(); const status=document.getElementById('gistSetupStatus'); if(!token){ showToast('Enter your GitHub token','amber'); return; } status.innerHTML='
Connecting to GitHub…
'; try{ let gistId=existingId; if(!gistId){ // Create new gist status.innerHTML='
Token validated ✓
Creating new Gist…
'; const res=await fetch('https://api.github.com/gists',{ method:'POST', headers:{'Authorization':'Bearer '+token,'Content-Type':'application/json','Accept':'application/vnd.github+json'}, body:JSON.stringify({ description:'WHGA Command Brain Data — do not edit manually', public:false, files:{'whga-brain.json':{content:JSON.stringify({created:new Date().toISOString(),note:'WHGA Command Brain v21 data'})}} }) }); if(!res.ok) throw new Error('Failed to create Gist: '+res.status); const data=await res.json(); gistId=data.id; } localStorage.setItem(GIST_TOKEN_KEY,token); localStorage.setItem(GIST_ID_KEY,gistId); document.getElementById('gistIdInput').value=gistId; // Initial save status.innerHTML='
Token saved ✓
Gist ready ✓
Saving data…
'; await doGistSave(token,gistId); status.innerHTML='
Token saved ✓
Gist created/connected ✓
Data saved ✓
✓ Cloud sync active. Gist ID: '+escHtml(gistId)+'
'; updateGistStatusBar('connected'); showToast('Cloud sync connected!'); logActivity('GitHub Gist sync connected','green'); } catch(err){ status.innerHTML='
Error: '+escHtml(err.message)+'
'; showToast('Setup failed: '+err.message,'red'); } } async function doGistSave(token,gistId){ const dataStr=JSON.stringify({ ...JSON.parse(localStorage.getItem('whga-command-brain-v12-real-assets')||'{}'), _meta:{ savedAt:new Date().toISOString(), version:'v21' } }); const res=await fetch('https://api.github.com/gists/'+gistId,{ method:'PATCH', headers:{'Authorization':'Bearer '+token,'Content-Type':'application/json','Accept':'application/vnd.github+json'}, body:JSON.stringify({files:{'whga-brain.json':{content:dataStr}}}) }); if(!res.ok) throw new Error('Save failed: '+res.status); } async function manualGistSync(){ const cfg=getGistConfig(); if(!cfg.token||!cfg.gistId){ showToast('Not configured — go to Cloud Sync setup','amber'); return; } const status=document.getElementById('gistDetailStatus'); if(status) status.innerHTML='
☁ Syncing…
'; try{ await doGistSave(cfg.token,cfg.gistId); const now=new Date().toLocaleTimeString(); if(status) status.innerHTML=`
✓ Last synced: ${now}
Gist ID: ${escHtml(cfg.gistId)}
`; updateGistStatusBar('synced',now); showToast('Synced to cloud ✓'); } catch(err){ if(status) status.innerHTML='
✕ Sync failed: '+escHtml(err.message)+'
'; showToast('Sync failed','red'); } } async function loadFromGist(){ const cfg=getGistConfig(); if(!cfg.token||!cfg.gistId){ showToast('Not configured','amber'); return; } const status=document.getElementById('gistDetailStatus'); if(status) status.innerHTML='
↓ Loading from cloud…
'; try{ const res=await fetch('https://api.github.com/gists/'+cfg.gistId,{ headers:{'Authorization':'Bearer '+cfg.token,'Accept':'application/vnd.github+json'} }); if(!res.ok) throw new Error('Load failed: '+res.status); const data=await res.json(); const content=data.files?.['whga-brain.json']?.content; if(!content) throw new Error('No brain data found in Gist'); const parsed=JSON.parse(content); localStorage.setItem('whga-command-brain-v12-real-assets',JSON.stringify(parsed)); loadState && loadState(); renderDashboard && renderDashboard(); if(status) status.innerHTML='
✓ Loaded from cloud at '+new Date().toLocaleTimeString()+'
'; showToast('Data loaded from cloud ✓'); logActivity('Data loaded from GitHub Gist','green'); } catch(err){ if(status) status.innerHTML='
✕ Load failed: '+escHtml(err.message)+'
'; showToast('Load failed: '+err.message,'red'); } } function toggleGistAutoSync(){ const enabled=document.getElementById('gistAutoSync').checked; saveGistSettings(); if(enabled){ _gistAutoSyncTimer=setInterval(()=>{ manualGistSync(); }, 3*60*1000); showToast('Auto-sync enabled — every 3 minutes'); } else { clearInterval(_gistAutoSyncTimer); showToast('Auto-sync disabled'); } } function saveGistSettings(){ const settings={ autoSync: document.getElementById('gistAutoSync')?.checked||false, syncOnSave: document.getElementById('gistSyncOnSave')?.checked||true }; localStorage.setItem(GIST_SETTINGS_KEY,JSON.stringify(settings)); } function clearGistConfig(){ if(!confirm('Disconnect cloud sync? Your local data is safe.')) return; localStorage.removeItem(GIST_TOKEN_KEY); localStorage.removeItem(GIST_ID_KEY); clearInterval(_gistAutoSyncTimer); updateGistStatusBar('disconnected'); showToast('Cloud sync disconnected'); } function updateGistStatusBar(state,time){ const el=document.getElementById('gistSyncStatus'); if(!el) return; if(state==='connected'||state==='synced'){ el.style.color='var(--green)'; el.textContent='☁ Cloud: '+(time?'synced '+time:'connected'); } else if(state==='syncing'){ el.style.color='var(--cyan)'; el.textContent='☁ Cloud: syncing…'; } else { el.style.color='var(--ghost)'; el.textContent='☁ Cloud: not configured'; } } function initGistOnLoad(){ const cfg=getGistConfig(); if(cfg.token&&cfg.gistId){ updateGistStatusBar('connected'); if(cfg.settings.autoSync){ document.getElementById('gistAutoSync') && (document.getElementById('gistAutoSync').checked=true); _gistAutoSyncTimer=setInterval(()=>manualGistSync(), 3*60*1000); } // Restore input fields const tokenEl=document.getElementById('gistTokenInput'); if(tokenEl) tokenEl.value=cfg.token; const idEl=document.getElementById('gistIdInput'); if(idEl) idEl.value=cfg.gistId; // Auto-sync on save handled by consolidated save() hook at bottom of file } } // ═══════════════════════════════════════════════════════════════ // V21/V22 — POPULATE ALL SELECTS // ═══════════════════════════════════════════════════════════════ function populateAllV21Selects(){ const selIds=['breakdownEpSelect','sidesEpSelect','readEpSelect','relEpSelect','cueEpSelect','cueListFilter','fmtEpSelect','avEpSelect','revEpSelect','aiEpContext','stripEpFilter','contEpFilter']; selIds.forEach(id=>{ const sel=document.getElementById(id); if(!sel) return; if(sel.querySelector('option[value="S01E01"]')) return; const blank=document.createElement('option'); blank.value=''; blank.textContent='— All / None —'; sel.appendChild(blank); EPISODES.forEach(ep=>{ const o=document.createElement('option'); o.value=ep.code; o.textContent=ep.code+' — '+ep.title; sel.appendChild(o); }); }); } // ═══════════════════════════════════════════════════════════════ // V21/V22 — INIT // ═══════════════════════════════════════════════════════════════ function initV21(){ populateAllV21Selects(); renderStripboard(); renderDood(); renderLocationCards(); renderContinuityBoard(); renderPostPipeline(); renderReleaseSchedule(); renderMusicCues(); renderRevisionColors(); renderAnalytics(); renderExportQuickStats(); initGistOnLoad(); updateAIPromptHint(); const aiKey=getAIKey(); if(!aiKey){ const banner=document.getElementById('aiSetupBanner'); if(banner) banner.style.display='block'; } logActivity('Command Brain v21 fully loaded — all systems active','green'); showToast('V21+V22 loaded — Production Suite + AI + Cloud Sync active'); } // ═══════════════════════════════════════════════════════════════ // CONSOLIDATED showSection — single overwrite covering V20+V21+V22 // Replaces all previous partial overwrites // ═══════════════════════════════════════════════════════════════ (function() { // Grab the original ONCE — defined earlier in the base app const _baseShowSection = showSection; showSection = function(id, el) { // Call the original base function if (typeof _baseShowSection === 'function') { _baseShowSection(id, el); } else { // Fallback if base not found document.querySelectorAll('.section').forEach(s => s.classList.remove('active')); const target = document.getElementById('sec-' + id); if (target) target.classList.add('active'); document.querySelectorAll('.nav-item').forEach(n => n.classList.remove('active')); if (el) el.classList.add('active'); } // V20 lazy inits if (id === 'v20parser') { setTimeout(initParserEpSelect, 50); return; } if (id === 'v20arcs') { setTimeout(renderArcTable, 50); return; } if (id === 'v20lctimeline') { setTimeout(renderLCTimeline, 50); return; } if (id === 'v20canon') { setTimeout(renderCanonRulesList, 50); return; } if (id === 'v20bible') return; // V21/V22 lazy inits const renders = { 'v21stripboard': renderStripboard, 'v21breakdown': () => {}, 'v21dood': renderDood, 'v21postpipeline':renderPostPipeline, 'v21locations': renderLocationCards, 'v21continuity': renderContinuityBoard, 'v21release': renderReleaseSchedule, 'v21musiccues': renderMusicCues, 'v21sides': () => {}, 'v21readthrough': () => {}, 'v22formatter': () => {}, 'v22revisions': renderRevisionColors, 'v22avformat': () => {}, 'v22ai': updateAIPromptHint, 'v22analytics': renderAnalytics, 'v22export': renderExportQuickStats, 'v22gist': initGistOnLoad, }; if (renders[id]) setTimeout(renders[id], 50); }; })(); // ═══════════════════════════════════════════════════════════════ // CONSOLIDATED save() hook — single overwrite covering V19+V22 // ═══════════════════════════════════════════════════════════════ (function() { const _baseSave = save; save = function() { _baseSave(); // V19: log activity try { updateActivityFeed({ type: 'save', text: 'State saved to localStorage', color: 'green' }); } catch(e) {} // V22: gist sync on save if configured try { const cfg = getGistConfig(); if (cfg.token && cfg.gistId && cfg.settings && cfg.settings.syncOnSave) { setTimeout(() => manualGistSync(), 800); } } catch(e) {} }; })(); (function(){ const run=()=>setTimeout(initV21,500); if(document.readyState==='loading') document.addEventListener('DOMContentLoaded',run); else run(); })(); // ===== END V21/V22 ADDITIONS ===== // ═══════════════════════════════════════════════════════════════════ (function(){ if(window.__whgaPWALoaded) return; window.__whgaPWALoaded = true; // ───────────────────────────────────────────── // ICON GENERATION // Draw the WHGA icon onto a canvas — works from local file // ───────────────────────────────────────────── function generateIcon(size){ const canvas = document.createElement('canvas'); canvas.width = size; canvas.height = size; const ctx = canvas.getContext('2d'); // Background ctx.fillStyle = '#07111e'; ctx.beginPath(); ctx.roundRect(0, 0, size, size, size * 0.18); ctx.fill(); // Outer ring ctx.strokeStyle = '#1e3a55'; ctx.lineWidth = size * 0.025; ctx.beginPath(); ctx.roundRect(size*0.06, size*0.06, size*0.88, size*0.88, size*0.14); ctx.stroke(); // Green accent bar top-left ctx.fillStyle = '#00e87a'; ctx.fillRect(size*0.12, size*0.12, size*0.08, size*0.04); // "W" lettermark const fs = size * 0.42; ctx.font = `700 ${fs}px "Courier New", monospace`; ctx.fillStyle = '#00e87a'; ctx.textAlign = 'center'; ctx.textBaseline = 'middle'; ctx.fillText('W', size/2, size * 0.46); // "HGA" subtext const fs2 = size * 0.13; ctx.font = `400 ${fs2}px "Courier New", monospace`; ctx.fillStyle = '#00cfff'; ctx.fillText('HGA', size/2, size * 0.75); // Bottom dot indicators const dotR = size * 0.025; const dotY = size * 0.88; const colors = ['#00e87a','#00cfff','#b06aff']; colors.forEach((c, i) => { const x = size/2 + (i-1) * size * 0.09; ctx.beginPath(); ctx.arc(x, dotY, dotR, 0, Math.PI*2); ctx.fillStyle = c; ctx.fill(); }); return canvas; } function iconDataURL(size){ return generateIcon(size).toDataURL('image/png'); } // ───────────────────────────────────────────── // INJECT MANIFEST // ───────────────────────────────────────────── function injectManifest(){ const icon192 = iconDataURL(192); const icon512 = iconDataURL(512); const icon180 = iconDataURL(180); // Set apple touch icon const appleIcon = document.getElementById('appleTouchIcon'); if(appleIcon) appleIcon.href = icon180; // Set favicon const favicon = document.getElementById('faviconLink'); if(favicon) favicon.href = iconDataURL(32); const manifest = { name: 'WHGA Command Brain', short_name: 'WHGA Brain', description: 'Walter Has Gone Away — HCR Production OS. One-man production management system.', start_url: window.location.href.split('?')[0], display: 'standalone', orientation: 'any', background_color: '#07111e', theme_color: '#07111e', categories: ['productivity', 'utilities'], icons: [ { src: icon192, sizes: '192x192', type: 'image/png', purpose: 'any maskable' }, { src: icon512, sizes: '512x512', type: 'image/png', purpose: 'any maskable' } ], shortcuts: [ { name: 'Today Mode', short_name: 'Today', description: 'Go to Today Mode', url: window.location.href.split('?')[0] + '?goto=today', icons: [{ src: icon192, sizes: '192x192', type: 'image/png' }] }, { name: 'Episode Editor', short_name: 'Episodes', description: 'Open episode editor', url: window.location.href.split('?')[0] + '?goto=episodes', icons: [{ src: icon192, sizes: '192x192', type: 'image/png' }] } ] }; const manifestStr = JSON.stringify(manifest); const manifestBlob = new Blob([manifestStr], {type:'application/json'}); const manifestURL = URL.createObjectURL(manifestBlob); const manifestLink = document.getElementById('pwaManifest'); if(manifestLink) manifestLink.href = manifestURL; } // ───────────────────────────────────────────── // SERVICE WORKER — INLINE AS BLOB // Caches the entire app for offline use // ───────────────────────────────────────────── const SW_CODE = ` const CACHE_NAME = 'whga-brain-final-v1'; const URLS_TO_CACHE = [self.location.href]; // Install: cache the app self.addEventListener('install', event => { event.waitUntil( caches.open(CACHE_NAME).then(cache => { return cache.addAll(URLS_TO_CACHE).catch(() => {}); }) ); self.skipWaiting(); }); // Activate: clean old caches self.addEventListener('activate', event => { event.waitUntil( caches.keys().then(keys => Promise.all(keys.filter(k => k !== CACHE_NAME).map(k => caches.delete(k))) ) ); self.clients.claim(); }); // Fetch: serve from cache, fall back to network self.addEventListener('fetch', event => { // Only handle GET requests for same-origin or our file if(event.request.method !== 'GET') return; event.respondWith( caches.match(event.request).then(cached => { if(cached) return cached; return fetch(event.request).then(response => { if(!response || response.status !== 200 || response.type === 'opaque') return response; const toCache = response.clone(); caches.open(CACHE_NAME).then(cache => cache.put(event.request, toCache)); return response; }).catch(() => cached || new Response('Offline — WHGA Brain cached version unavailable', {status:503})); }) ); }); // Message: force update self.addEventListener('message', event => { if(event.data === 'skipWaiting') self.skipWaiting(); }); `; function registerServiceWorker(){ if(!('serviceWorker' in navigator)) return; try { const blob = new Blob([SW_CODE], {type:'application/javascript'}); const swURL = URL.createObjectURL(blob); navigator.serviceWorker.register(swURL, {scope: '/'}) .then(reg => { console.log('[WHGA PWA] Service worker registered:', reg.scope); // Check for updates reg.addEventListener('updatefound', () => { const newWorker = reg.installing; newWorker.addEventListener('statechange', () => { if(newWorker.state === 'installed' && navigator.serviceWorker.controller){ showPWAUpdateBanner(); } }); }); }) .catch(err => { // Service workers don't work from local file:// — that's OK // Will work when hosted on hollowrebellion.art console.log('[WHGA PWA] SW registration skipped (local file):', err.message); }); } catch(e) { console.log('[WHGA PWA] SW setup error:', e.message); } } // ───────────────────────────────────────────── // INSTALL PROMPT // ───────────────────────────────────────────── let deferredInstallPrompt = null; window.addEventListener('beforeinstallprompt', e => { e.preventDefault(); deferredInstallPrompt = e; // Show install banner after 3 seconds if not already installed setTimeout(showInstallBanner, 3000); }); window.addEventListener('appinstalled', () => { deferredInstallPrompt = null; hideInstallBanner(); if(typeof showToast === 'function') showToast('WHGA Brain installed as app ✓'); if(typeof logActivity === 'function') logActivity('App installed as PWA', 'green'); localStorage.setItem('whga-pwa-installed', 'true'); }); function showInstallBanner(){ if(localStorage.getItem('whga-pwa-installed') === 'true') return; if(document.getElementById('pwaInstallBanner')) return; const banner = document.createElement('div'); banner.id = 'pwaInstallBanner'; banner.style.cssText = [ 'position:fixed', 'bottom:80px', 'left:50%', 'transform:translateX(-50%)', 'background:#0e1e30', 'border:1px solid #2a4d72', 'border-radius:16px', 'padding:16px 20px', 'display:flex', 'align-items:center', 'gap:14px', 'z-index:99990', 'box-shadow:0 16px 48px rgba(0,0,0,.6)', 'max-width:340px', 'width:calc(100vw - 40px)', 'animation:fadeUp .3s ease' ].join(';'); banner.innerHTML = `
INSTALL AS APP
Add to home screen for offline access and faster loading.
`; document.body.appendChild(banner); // Draw icon in banner setTimeout(() => { const ic = document.getElementById('pwaInstallIcon'); if(ic){ const ctx2 = ic.getContext('2d'); ctx2.drawImage(generateIcon(40), 0, 0, 40, 40); } }, 50); } window.whgaPWAInstall = function(){ if(!deferredInstallPrompt) return; deferredInstallPrompt.prompt(); deferredInstallPrompt.userChoice.then(choice => { if(choice.outcome === 'accepted') hideInstallBanner(); deferredInstallPrompt = null; }); }; window.whgaPWADismiss = function(){ hideInstallBanner(); // Don't show again for 7 days localStorage.setItem('whga-pwa-dismissed', Date.now().toString()); }; function hideInstallBanner(){ const b = document.getElementById('pwaInstallBanner'); if(b) b.remove(); } // ───────────────────────────────────────────── // UPDATE BANNER // ───────────────────────────────────────────── function showPWAUpdateBanner(){ if(document.getElementById('pwaUpdateBanner')) return; const banner = document.createElement('div'); banner.id = 'pwaUpdateBanner'; banner.style.cssText = 'position:fixed;top:12px;left:50%;transform:translateX(-50%);background:#0e1e30;border:1px solid #00cfff;border-radius:12px;padding:10px 18px;display:flex;align-items:center;gap:12px;z-index:99990;font-size:13px;color:#ddeeff;box-shadow:0 8px 32px rgba(0,0,0,.5)'; banner.innerHTML = ` Update available — `; document.body.appendChild(banner); } window.whgaPWAUpdate = function(){ navigator.serviceWorker.controller?.postMessage('skipWaiting'); window.location.reload(); }; // ───────────────────────────────────────────── // OFFLINE INDICATOR // ───────────────────────────────────────────── function updateOnlineStatus(){ const indicator = document.getElementById('pwaOfflineIndicator'); if(navigator.onLine){ if(indicator) indicator.remove(); } else { if(!indicator){ const el = document.createElement('div'); el.id = 'pwaOfflineIndicator'; el.style.cssText = 'position:fixed;top:0;left:0;right:0;background:#ff4466;color:#fff;text-align:center;font-size:12px;font-family:"Courier New",monospace;padding:4px;z-index:99995;letter-spacing:.1em'; el.textContent = '● OFFLINE — All data saved locally. Changes will sync when reconnected.'; document.body.prepend(el); } } } window.addEventListener('online', updateOnlineStatus); window.addEventListener('offline', updateOnlineStatus); // ───────────────────────────────────────────── // HANDLE SHORTCUT URL PARAMS // Route ?goto=today etc from PWA shortcuts // ───────────────────────────────────────────── function handleShortcutParam(){ const params = new URLSearchParams(window.location.search); const goto = params.get('goto'); if(!goto) return; // Remove param from URL without reload const url = new URL(window.location.href); url.searchParams.delete('goto'); history.replaceState({}, '', url.toString()); // Navigate after app loads setTimeout(() => { if(typeof showSection === 'function'){ const sectionMap = { today: 'v24today', episodes: 'episodes', scripts: 'scripts', budget: 'budget', gear: 'gear', }; const target = sectionMap[goto]; if(target){ const navEl = document.querySelector(`.nav-item[onclick*="${target}"]`); showSection(target, navEl); } } }, 1200); } // ───────────────────────────────────────────── // PWA STATUS SECTION IN SIDEBAR FOOTER // ───────────────────────────────────────────── function injectPWAStatus(){ const footer = document.querySelector('.sidebar-footer'); if(!footer || document.getElementById('pwaStatusLine')) return; const line = document.createElement('div'); line.id = 'pwaStatusLine'; line.style.cssText = 'margin-top:6px;font-size:10px;cursor:pointer;transition:.1s color'; const isInstalled = window.matchMedia('(display-mode: standalone)').matches || window.navigator.standalone === true; line.innerHTML = isInstalled ? '◈ Running as installed app' : '◌ Tap to install as app'; footer.appendChild(line); } // ───────────────────────────────────────────── // INIT // ───────────────────────────────────────────── function pwaInit(){ injectManifest(); registerServiceWorker(); updateOnlineStatus(); handleShortcutParam(); setTimeout(injectPWAStatus, 800); // Check if dismissed recently (within 7 days) const dismissed = parseInt(localStorage.getItem('whga-pwa-dismissed') || '0'); if(dismissed && Date.now() - dismissed < 7 * 24 * 60 * 60 * 1000){ deferredInstallPrompt = null; // don't auto-show } if(typeof logActivity === 'function'){ const standalone = window.matchMedia('(display-mode: standalone)').matches; logActivity('PWA layer active' + (standalone ? ' — running as installed app' : ''), 'cyan'); } } if(document.readyState === 'loading'){ document.addEventListener('DOMContentLoaded', pwaInit); } else { pwaInit(); } })();