document.addEventListener('DOMContentLoaded', function() { var CBLE = { extractLines: function(t) { return t.split('\n').map(function(l) { return l.trim(); }).filter(function(l) { return l.length > 0; }); }, groupIntoParagraphBeats: function(lines) { var beats = []; var current = ''; for (var i = 0; i < lines.length; i++) { current += (current ? ' ' : '') + lines[i]; if (current.length > 800) { beats.push(current.trim()); current = ''; } } if (current.trim()) beats.push(current.trim()); while (beats.length > 28) { var merged = []; for (var i = 0; i < beats.length; i += 2) { if (i + 1 < beats.length) merged.push(beats[i] + ' ' + beats[i + 1]); else merged.push(beats[i]); } beats = merged; } return beats; }, vocabularyMap: { "Opening Image": { commandment: "Setup", valueFrom: "life", valueTo: "calm", emotion: "peace" }, "Inciting Incident": { commandment: "Inciting Incident", valueFrom: "stable", valueTo: "unstable", emotion: "curiosity" }, "Catalyst": { commandment: "Inciting Incident", valueFrom: "safety", valueTo: "danger", emotion: "fear" }, "Debate": { commandment: "Progressive Complication", valueFrom: "hope", valueTo: "despair", emotion: "grief" }, "Turning Point": { commandment: "Progressive Complication", valueFrom: "expected", valueTo: "unexpected", emotion: "surprise" }, "Break Into Two": { commandment: "Progressive Complication", valueFrom: "despair", valueTo: "decision", emotion: "determination" }, "B Story": { commandment: "Progressive Complication", valueFrom: "order", valueTo: "chaos", emotion: "love" }, "Midpoint": { commandment: "Crisis", valueFrom: "despair", valueTo: "hope", emotion: "determination" }, "Crisis": { commandment: "Crisis", valueFrom: "certain", valueTo: "uncertain", emotion: "tension" }, "All Is Lost": { commandment: "Crisis", valueFrom: "hope", valueTo: "despair", emotion: "loss" }, "Finale": { commandment: "Climax", valueFrom: "danger", valueTo: "safety", emotion: "victory" }, "Climax": { commandment: "Climax", valueFrom: "inactive", valueTo: "active", emotion: "determination" }, "Final Image": { commandment: "Resolution", valueFrom: "safety", valueTo: "peace", emotion: "peace" }, "Resolution": { commandment: "Resolution", valueFrom: "unresolved", valueTo: "resolved", emotion: "relief" } }, inferSceneType: function(beat, i, total, prevBeat, nextBeat) { var t = beat.toLowerCase(); var p = i / Math.max(total - 1, 1); var prev = prevBeat ? prevBeat.toLowerCase() : ''; var next = nextBeat ? nextBeat.toLowerCase() : ''; // Determine arc position var arcPos = ""; if (i === 0) arcPos = "Opening"; else if (i === total - 1) arcPos = "Closing"; else if (p < 0.20) arcPos = "Early"; else if (p < 0.40) arcPos = "Building"; else if (p < 0.55) arcPos = "Middle"; else if (p < 0.75) arcPos = "Late"; else arcPos = "Final"; // Determine dramatic function (StoryGrid Commandment) var func = ""; if (i === 0) func = "Image"; else if (i === total - 1) func = "Image"; else if (/but then|suddenly|without warning|new enemy|new threat|new wave|appeared|emerged|stepped forward|protocol|orders[^.]*changed|redeploy|abandoned|left him|left us|betrayal|fake|article|headline/.test(t)) func = "Catalyst"; else if (/unexpected|surprise|instead|however|unfortunately|twist|realized the truth|then something|just then/.test(t)) func = "Turning Point"; else if (/can'?t|shouldn'?t|wasn'?t sure|didn'?t know|maybe|perhaps|what if|afraid|fear|hesitat|froze|stumbled|staggered|not like this|not again|shame|regret/.test(t)) func = "Debate"; else if (/either|or|choice|decide|dilemma|whether|if he|if she|best bad|irreconcilable|what now|how could/.test(t)) func = "Crisis"; else if (/overwhelmed|swallowed|crushed|buried|drowning|fading|too many|too strong|can'?t save|won'?t survive|dropped to one knee|barely|broken|heartbroken/.test(t)) func = "All Is Lost"; else if (/chooses|decides|acts|strikes|jumps|commits|refuses|accepts|grabs|lunges|roared|charged|slammed|threw|dove|bolted|didn'?t hesitate|without thinking|no going back/.test(t)) func = "Break Into Two"; else if (/(love|hug|embrace|kiss|hand[^.]*shoulder|arm[^.]*around)/.test(t)) func = "B Story"; else if (/(mom|dad|ma |pa |grandma|grandpa|grandson|family|together)/.test(t) && /(said|whispered|smiled|looked|watched|chuckled)/.test(t)) func = "B Story"; else if (/proud|love you|believe in you|not alone|never alone/.test(t)) func = "B Story"; else if (p > 0.35 && p < 0.65 && (/rescue|arrived|came|appeared|gate|opened|breach|breakthrough|realized|understood|finally saw|finally knew|relief/.test(t))) func = "Midpoint"; else if (p > 0.65 && (/final|last stand|last push|ultimate|climax|final battle|hold fast|hold the line|storm|thunder|coming|hunting|patient/.test(t))) func = "Finale"; else if (/result|outcome|aftermath|consequence|now that|because of|led to|silence|stood|still|quiet|settled|faded/.test(t)) func = "Resolution"; else if (p < 0.25) func = "Setup"; else if (p < 0.50) func = "Complication"; else if (p < 0.75) func = "Crisis"; else func = "Climax"; if (arcPos === "Opening") return "Opening Image"; if (arcPos === "Closing") return "Final Image"; return func; }, inferSpeaker: function(t) { var l = t.toLowerCase(); if (t.match(/\b(tao|his|he)\b.*\b(thought|felt|realized|knew|believed|understood|saw)\b/)) return 'tao'; if (t.match(/\b(she|her|blossom)\b.*\b(knew|understood|decided|chose|refused|felt)\b/)) return 'blossom'; if (t.match(/[""][^""]*[""]/) && (l.includes('tao') || l.includes(' my ') || l.includes(' me ') || l.includes(" i "))) return 'tao'; if (l.includes('tif') && !l.includes('tao') && !l.includes('beautiful') && !l.includes('tify')) return 'tif'; if (l.includes('flip') && !l.includes('tao')) return 'flip'; if (l.includes('zip')) return 'zip'; if (l.includes('li wei') || l.includes('liwei')) return 'liwei'; if (l.includes('tao')) return 'tao'; if (l.includes('magpie')) return 'magpie'; if (l.includes('tinting')) return 'tinting'; if (l.match(/\bmy\b|\bme\b|\bi\b/) && l.length < 300) return 'tao'; return 'narrator'; }, inferEffect: function(t) { var lower = t.toLowerCase(); // Value shifts from StoryGrid if (/died|killed|murdered|sacrificed|corpse|last breath/.test(lower)) return '"Life hung in the balance."'; if (/threatened|attacked|ambushed|hunted|trapped|danger|not safe/.test(lower)) return '"Nowhere was safe now."'; if (/chosen|summoned|called upon|assigned|selected|singled out/.test(lower)) return '"Chosen. For better or worse."'; if (/failed|lost everything|gave up|surrendered|crushed|despair|hopeless/.test(lower)) return '"Hope died in that moment."'; if (/hope|spark|maybe|just maybe|chance|possibility|dawn|new day/.test(lower)) return '"A spark in the darkness."'; if (/betrayed|abandoned|rejected|humiliated|scorned|liar|fake|wasn'?t real/.test(lower)) return '"Betrayal cut deeper than any blade."'; if (/love|connection|together|united|joined|embrace|kiss|held|arms around/.test(lower)) return '"Connection, finally."'; if (/collapsed|erupted|exploded|shattered|unraveled|chaos|madness|mayhem/.test(lower)) return '"The world unraveled."'; if (/order|plan|strategy|formation|organized|gathered|assembled/.test(lower)) return '"A plan emerged."'; if (/mastered|unlocked|awakened|transformed|leveled up|stronger|power/.test(lower)) return '"Power awakened."'; if (/understood|finally saw|realized the truth|learned the lesson|wisdom/.test(lower)) return '"The truth changed everything."'; if (/not alone|never again|stood together|family|team|crew|united/.test(lower)) return '"Not alone. Never again."'; if (/charged|roared|faced|confronted|stood up|defied|no more running|courage/.test(lower)) return '"Courage answered the call."'; // Chapter-specific if (lower.includes('flowers') && lower.includes('chocolate')) return '"Flowers. Chocolate. Legendary."'; if (lower.includes('rappel') || lower.includes('midnight')) return '"A girl doesn\'t rappel down walls for a handshake."'; if (lower.includes('article') || lower.includes('headline') || lower.includes('lucan')) return '"My heart sank."'; if (lower.includes('outgrow') || lower.includes('first crush')) return '"Sometimes you outgrow your first crush."'; if (lower.includes('girl stuff') || lower.includes('hardest fight')) return '"Girl stuff. Hardest fight you\'ll ever face."'; if (lower.includes('pancake heart') || lower.includes('steamroller')) return '"She ran over my heart with a steamroller."'; if (lower.includes('gonna hurt') || lower.includes('century')) return '"Gonna hurt for a while, kiddo."'; if (lower.includes('no do-overs') || lower.includes('someone pays')) return '"No do-overs. Someone pays the price."'; if (lower.includes('storm isn\'t waiting')) return '"The storm isn\'t waiting."'; if (lower.includes('colliding') || lower.includes('misstep')) return '"One misstep. Someone gets hurt."'; if (lower.includes('light changed') || lower.includes('sunlight')) return '"Something was terribly wrong."'; if (lower.includes('coiling') || lower.includes('defied weather')) return '"The clouds were coiling, twisting."'; if (lower.includes('that\'s not thunder')) return '"That\'s not thunder."'; if (lower.includes('laughter') && lower.includes('devoid of warmth')) return '"Cold laughter from nowhere."'; if (lower.includes('stay close')) return '"Stay close."'; if (lower.includes('coming for him') || lower.includes('trap centuries')) return '"This time, it was coming for him."'; if (lower.includes('learning the hard way')) return '"Learning the hard way."'; if (lower.includes('we love you') && lower.includes('grandson')) return '"We love you, grandson."'; if (lower.includes('note') && lower.includes('raccoon')) return '"Raccoon was supposed to give you the note."'; if (lower.includes('beat me') && lower.includes('drill')) return '"Beat me in this drill and I\'ll tell you."'; return '"The value shifted."'; }, generateStoryEvent: function(t, type) { var e = { "Opening Image": "The world before", "Final Image": "After the end", "Catalyst": "Everything changes", "Debate": "The choice", "Break Into Two": "The journey", "B Story": "Inner needs", "Midpoint": "Turning point", "All Is Lost": "Darkest moment", "Finale": "Climax", "Turning Point": "The unexpected complication", "Crisis": "The best bad choice", "Climax": "The decision and action", "Resolution": "The outcome" }; return e[type] || type; }, getPlaceholderImage: function(i) { var imgs = ['https://koru-imprint.com/wp-content/uploads/2026/03/9.png', 'https://koru-imprint.com/wp-content/uploads/2026/03/8.png', 'https://koru-imprint.com/wp-content/uploads/2026/03/2.png']; return imgs[i % imgs.length]; }, sceneColors: { "Opening Image": "#6f6f6f", "Final Image": "#6f6f6f", "Catalyst": "#ff6b35", "Inciting Incident": "#ff6b35", "Debate": "#9b59b6", "Turning Point": "#9b59b6", "Break Into Two": "#3498db", "Complication": "#9b59b6", "B Story": "#e74c6f", "Midpoint": "#ffd700", "All Is Lost": "#ff4444", "Crisis": "#ff4444", "Finale": "#2ecc71", "Climax": "#ffd700", "Setup": "#6f6f6f", "Resolution": "#2ecc71" }, assignVocabulary: function(beats) { var self = this, result = []; for (var i = 0; i < beats.length; i++) { var prevBeat = i > 0 ? beats[i-1] : null; var nextBeat = i < beats.length-1 ? beats[i+1] : null; var type = self.inferSceneType(beats[i], i, beats.length, prevBeat, nextBeat); var vocab = self.vocabularyMap[type] || { commandment: "Beat", valueFrom: "?", valueTo: "?", emotion: "neutral" }; result.push({ beat: i + 1, sceneType: type, commandment: vocab.commandment || "Beat", valueFrom: vocab.valueFrom || "?", valueTo: vocab.valueTo || "?", emotion: vocab.emotion || "neutral", speaker: self.inferSpeaker(beats[i]), cause: beats[i], effect: self.inferEffect(beats[i]), storyEvent: self.generateStoryEvent(beats[i], type), imageUrl: self.getPlaceholderImage(i) }); } return result; }, renderFull: function(data) { if (!data || !data.length) return '
No beats
'; var h = ''; for (var i = 0; i < data.length; i++) { var d = data[i], c = this.sceneColors[d.sceneType] || "#6f6f6f", sc = 'speaker-' + (d.speaker || 'narrator'); h += '