{"id":24,"date":"2026-04-22T11:35:25","date_gmt":"2026-04-22T11:35:25","guid":{"rendered":"https:\/\/twainstudio.dev\/?page_id=24"},"modified":"2026-04-22T11:46:15","modified_gmt":"2026-04-22T11:46:15","slug":"grammar-ghost","status":"publish","type":"page","link":"https:\/\/twainstudio.dev\/?page_id=24","title":{"rendered":"Grammar Ghost"},"content":{"rendered":"\n<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n<meta charset=\"UTF-8\">\n<meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">\n<title>T.W.A.I.N. Editor \u2014 Voice-Aware Writing Assistant \u00b7 Rebellion Command<\/title>\n<link href=\"https:\/\/fonts.googleapis.com\/css2?family=Cinzel:wght@400;600;700;900&#038;family=Oswald:wght@300;400;500;600;700&#038;family=Crimson+Text:ital,wght@0,400;0,600;1,400&#038;display=swap\" rel=\"stylesheet\">\n<style>\n*{box-sizing:border-box;margin:0;padding:0;}\n:root{\n  --navy:#060d18;--navy2:#0a1628;--navy3:#0d1e35;--navy4:#080f1e;\n  --gold:#c9a84c;--gold2:#e8c97a;--gold-dim:rgba(201,168,76,0.1);\n  --orange:#FF4500;--orange-dim:rgba(255,69,0,0.1);--orange-glow:rgba(255,69,0,0.22);\n  --cream:#f0e6c8;--slate:#4a5d7a;--slate2:#8a9ab5;\n  --green:#1D9E75;--green-dark:#0F6E56;--green-dim:rgba(29,158,117,0.1);\n  --paper:#f5f0e8;--paper2:#ede8da;--ink:#18120a;--ink2:#2c2318;\n  --flag-passive:rgba(255,165,0,0.18);\n  --flag-cliche:rgba(255,69,0,0.18);\n  --flag-drift:rgba(124,80,212,0.18);\n  --flag-grammar:rgba(220,53,69,0.18);\n  --flag-show:rgba(29,158,117,0.18);\n}\nhtml,body{height:100%;margin:0;padding:0;}\nbody{background:var(--navy);color:var(--cream);font-family:'Crimson Text',serif;display:flex;flex-direction:column;height:100%;overflow:hidden;}\n::-webkit-scrollbar{width:4px;height:4px;}\n::-webkit-scrollbar-thumb{background:rgba(201,168,76,0.25);border-radius:2px;}\n\n\/* TOP BAR *\/\n.topbar{flex-shrink:0;height:50px;background:var(--navy4);border-bottom:1px solid rgba(201,168,76,0.3);display:flex;align-items:center;justify-content:space-between;padding:0 16px;position:relative;z-index:100;}\n.topbar::after{content:'';position:absolute;bottom:-1px;left:0;right:0;height:1px;background:linear-gradient(90deg,transparent,var(--orange) 30%,var(--gold) 50%,var(--orange) 70%,transparent);}\n.tb-brand{font-family:'Cinzel',serif;font-weight:900;font-size:13px;letter-spacing:.12em;color:var(--cream);}\n.tb-brand span{color:var(--gold);}\n.tb-brand em{color:var(--orange);font-style:normal;}\n.tb-badge{font-family:'Oswald',sans-serif;font-size:9px;letter-spacing:.25em;text-transform:uppercase;color:var(--green);background:var(--green-dim);border:1px solid rgba(29,158,117,0.3);padding:3px 10px;border-radius:20px;}\n.tb-center{display:flex;align-items:center;gap:14px;}\n.tb-stat{font-family:'Oswald',sans-serif;font-size:10px;letter-spacing:.08em;color:var(--slate2);}\n.tb-stat span{color:var(--gold);font-weight:600;}\n.tb-right{display:flex;align-items:center;gap:6px;}\n.tb-btn{font-family:'Oswald',sans-serif;font-size:9px;letter-spacing:.1em;text-transform:uppercase;background:transparent;border:1px solid rgba(201,168,76,0.2);color:var(--slate2);padding:5px 11px;border-radius:2px;cursor:pointer;transition:all .15s;white-space:nowrap;}\n.tb-btn:hover{color:var(--cream);border-color:rgba(201,168,76,0.5);}\n.tb-btn.gold{color:var(--gold);background:var(--gold-dim);border-color:rgba(201,168,76,0.35);}\n.tb-btn.orange{color:var(--orange);border-color:rgba(255,69,0,0.3);}\n.tb-btn.green{color:var(--green);border-color:rgba(29,158,117,0.3);background:var(--green-dim);}\n\n\/* LEAD MAGNET BANNER *\/\n.lm-banner{flex-shrink:0;background:linear-gradient(135deg,rgba(255,69,0,0.12),rgba(201,168,76,0.08));border-bottom:1px solid rgba(201,168,76,0.15);padding:8px 20px;display:flex;align-items:center;justify-content:space-between;gap:12px;}\n.lm-text{font-family:'Oswald',sans-serif;font-size:10px;letter-spacing:.06em;color:var(--cream);}\n.lm-text strong{color:var(--gold);}\n.lm-cta{font-family:'Oswald',sans-serif;font-size:9px;letter-spacing:.12em;text-transform:uppercase;padding:5px 14px;border-radius:2px;background:var(--orange);color:#fff;border:none;cursor:pointer;white-space:nowrap;box-shadow:0 2px 10px var(--orange-glow);transition:all .15s;}\n.lm-cta:hover{background:#ff5a1a;}\n.lm-dismiss{font-family:'Oswald',sans-serif;font-size:9px;color:var(--slate);background:none;border:none;cursor:pointer;padding:0 4px;opacity:.6;}\n.lm-dismiss:hover{opacity:1;}\n\n\/* MODULE NAV *\/\n.module-nav{flex-shrink:0;height:32px;background:var(--navy4);border-bottom:1px solid rgba(201,168,76,0.1);display:flex;align-items:center;padding:0 14px;gap:2px;}\n.mod-link{font-family:'Oswald',sans-serif;font-size:9px;letter-spacing:.1em;text-transform:uppercase;padding:4px 10px;border-radius:2px;color:var(--slate);border:1px solid transparent;transition:all .15s;text-decoration:none;display:flex;align-items:center;gap:4px;}\n.mod-link:hover{color:var(--cream);}\n.mod-link.active{color:var(--gold);border-color:rgba(201,168,76,0.2);background:var(--gold-dim);}\n.mod-num{color:var(--orange);font-size:8px;font-weight:700;}\n.mod-arrow{color:rgba(201,168,76,0.2);margin:0 2px;}\n.mod-spacer{flex:1;}\n\n\/* MAIN LAYOUT *\/\n.editor-layout{flex:1;display:flex;min-height:0;}\n\n\/* LEFT \u2014 VOICE PANEL *\/\n.voice-panel{width:230px;min-width:230px;background:var(--navy3);display:flex;flex-direction:column;border-right:1px solid rgba(201,168,76,0.08);overflow:hidden;}\n.vp-header{flex-shrink:0;padding:12px 14px;border-bottom:1px solid rgba(201,168,76,0.1);background:rgba(6,13,24,0.5);}\n.vp-title{font-family:'Cinzel',serif;font-size:11px;font-weight:700;letter-spacing:.12em;text-transform:uppercase;color:var(--gold);margin-bottom:2px;}\n.vp-sub{font-family:'Oswald',sans-serif;font-size:8px;letter-spacing:.12em;text-transform:uppercase;color:var(--slate);}\n.vp-body{flex:1;overflow-y:auto;padding:12px;}\n.vp-section{margin-bottom:14px;}\n.vp-sec-title{font-family:'Oswald',sans-serif;font-size:8px;letter-spacing:.2em;text-transform:uppercase;color:var(--orange);margin-bottom:8px;padding-bottom:4px;border-bottom:1px solid rgba(255,69,0,0.12);}\n\n\/* ARCHETYPE SELECTOR *\/\n.arch-cards{display:flex;flex-direction:column;gap:4px;}\n.arch-mini{padding:7px 10px;border-radius:2px;cursor:pointer;border:1px solid rgba(201,168,76,0.15);background:transparent;transition:all .15s;display:flex;align-items:center;gap:8px;}\n.arch-mini:hover{border-color:rgba(201,168,76,0.4);background:var(--gold-dim);}\n.arch-mini.selected{border-color:var(--orange);background:var(--orange-dim);}\n.arch-mini-name{font-family:'Oswald',sans-serif;font-size:10px;font-weight:600;letter-spacing:.06em;text-transform:uppercase;color:var(--cream);}\n.arch-mini.selected .arch-mini-name{color:var(--orange);}\n.arch-mini-role{font-family:'Oswald',sans-serif;font-size:8px;color:var(--slate2);}\n\n\/* CUSTOM VOICE *\/\n.custom-voice-area{background:rgba(255,255,255,0.02);border:1px solid rgba(201,168,76,0.12);border-radius:2px;padding:7px 9px;font-family:'Oswald',sans-serif;font-size:10px;color:var(--cream);line-height:1.6;min-height:60px;}\n.custom-voice-area:empty::before{content:'Your voice profile will appear here after calibration...';color:var(--slate);font-style:italic;}\n\n\/* FLAG LEGEND *\/\n.flag-legend{display:flex;flex-direction:column;gap:4px;}\n.flag-item{display:flex;align-items:center;gap:7px;font-family:'Oswald',sans-serif;font-size:9px;letter-spacing:.04em;color:var(--slate2);}\n.flag-dot{width:10px;height:10px;border-radius:2px;flex-shrink:0;}\n.flag-dot.passive{background:rgba(255,165,0,0.5);}\n.flag-dot.cliche{background:rgba(255,69,0,0.5);}\n.flag-dot.drift{background:rgba(124,80,212,0.5);}\n.flag-dot.grammar{background:rgba(220,53,69,0.5);}\n.flag-dot.show{background:rgba(29,158,117,0.5);}\n.flag-toggle{margin-left:auto;width:28px;height:14px;border-radius:7px;background:rgba(255,255,255,0.08);border:none;cursor:pointer;position:relative;transition:background .15s;flex-shrink:0;}\n.flag-toggle.on{background:var(--green);}\n.flag-toggle::after{content:'';position:absolute;top:2px;left:2px;width:10px;height:10px;border-radius:50%;background:#fff;transition:transform .15s;}\n.flag-toggle.on::after{transform:translateX(14px);}\n\n\/* SCORE STRIP *\/\n.score-strip{flex-shrink:0;padding:10px 14px;border-top:1px solid rgba(201,168,76,0.08);background:rgba(6,13,24,0.5);}\n.score-row{display:flex;justify-content:space-between;align-items:center;margin-bottom:5px;}\n.score-lbl{font-family:'Oswald',sans-serif;font-size:8px;letter-spacing:.12em;text-transform:uppercase;color:var(--slate);}\n.score-val{font-family:'Cinzel',serif;font-size:14px;font-weight:700;}\n.score-val.green{color:var(--green);}\n.score-val.gold{color:var(--gold);}\n.score-val.orange{color:var(--orange);}\n.mini-bar-track{height:2px;background:rgba(255,255,255,0.05);border-radius:1px;margin-bottom:4px;}\n.mini-bar-fill{height:2px;border-radius:1px;transition:width .8s ease;}\n\n\/* KEY STRIP *\/\n.key-strip{flex-shrink:0;padding:8px 12px;border-top:1px solid rgba(201,168,76,0.08);}\n.key-lbl{font-family:'Oswald',sans-serif;font-size:8px;letter-spacing:.1em;text-transform:uppercase;color:rgba(201,168,76,0.4);margin-bottom:3px;}\n.key-inp{width:100%;background:rgba(255,255,255,0.03);border:1px solid rgba(201,168,76,0.15);border-radius:2px;color:var(--cream);font-family:'Oswald',sans-serif;font-size:10px;padding:5px 7px;outline:none;}\n.key-inp:focus{border-color:rgba(201,168,76,0.4);}\n.key-inp::placeholder{color:#1a2a1a;}\n\n\/* CENTER \u2014 DOCUMENT EDITOR *\/\n.doc-pane{flex:1;background:var(--paper);display:flex;flex-direction:column;min-width:0;overflow:hidden;}\n.doc-toolbar{flex-shrink:0;height:36px;background:var(--paper2);border-bottom:1px solid rgba(24,18,10,0.12);display:flex;align-items:center;padding:0 12px;gap:6px;}\n.doc-tool-btn{font-family:'Oswald',sans-serif;font-size:9px;letter-spacing:.08em;text-transform:uppercase;background:transparent;border:1px solid transparent;border-radius:2px;color:rgba(24,18,10,0.5);padding:4px 8px;cursor:pointer;transition:all .12s;}\n.doc-tool-btn:hover{background:rgba(24,18,10,0.06);border-color:rgba(24,18,10,0.15);color:var(--ink2);}\n.doc-tool-btn.active{background:rgba(255,69,0,0.1);border-color:rgba(255,69,0,0.3);color:var(--orange);}\n.doc-tool-div{width:1px;height:16px;background:rgba(24,18,10,0.12);margin:0 3px;}\n.doc-tool-spacer{flex:1;}\n.doc-tool-stat{font-family:'Oswald',sans-serif;font-size:8px;color:rgba(24,18,10,0.35);letter-spacing:.06em;}\n.doc-tool-stat span{color:var(--ink2);font-weight:600;}\n.analyze-btn{font-family:'Oswald',sans-serif;font-size:9px;font-weight:700;letter-spacing:.1em;text-transform:uppercase;background:var(--orange);color:#fff;border:none;padding:5px 14px;border-radius:2px;cursor:pointer;box-shadow:0 2px 8px var(--orange-glow);transition:all .15s;}\n.analyze-btn:hover{background:#ff5a1a;}\n.analyze-btn:disabled{background:var(--slate);box-shadow:none;cursor:not-allowed;}\n\n\/* DOCUMENT AREA *\/\n.doc-wrap{flex:1;overflow-y:auto;position:relative;min-height:0;}\n#doc-area{min-height:100%;padding:48px 64px;font-family:'Crimson Text',serif;font-size:15px;line-height:1.9;color:var(--ink);outline:none;caret-color:var(--orange);}\n#doc-area:empty::before{content:attr(data-placeholder);color:rgba(24,18,10,0.25);font-style:italic;pointer-events:none;}\n\n\/* HIGHLIGHT STYLES \u2014 applied via spans injected into contenteditable *\/\n#doc-area .hl-passive{background:var(--flag-passive);border-bottom:2px solid orange;cursor:pointer;border-radius:2px;}\n#doc-area .hl-cliche{background:var(--flag-cliche);border-bottom:2px solid var(--orange);cursor:pointer;border-radius:2px;}\n#doc-area .hl-drift{background:var(--flag-drift);border-bottom:2px solid #7c50d4;cursor:pointer;border-radius:2px;}\n#doc-area .hl-grammar{background:var(--flag-grammar);border-bottom:2px dotted #dc3545;cursor:pointer;border-radius:2px;}\n#doc-area .hl-show{background:var(--flag-show);border-bottom:2px solid var(--green);cursor:pointer;border-radius:2px;}\n#doc-area .hl-passive:hover,#doc-area .hl-cliche:hover,#doc-area .hl-drift:hover,#doc-area .hl-grammar:hover,#doc-area .hl-show:hover{opacity:.75;}\n#doc-area .hl-active{outline:2px solid var(--gold);outline-offset:1px;}\n\n\/* SUGGESTION TOOLTIP *\/\n.suggestion-tooltip{position:fixed;background:var(--navy3);border:1px solid var(--gold);border-radius:3px;padding:10px 12px;max-width:280px;z-index:500;box-shadow:0 8px 24px rgba(0,0,0,0.6);display:none;}\n.tt-type{font-family:'Oswald',sans-serif;font-size:8px;letter-spacing:.15em;text-transform:uppercase;color:var(--orange);margin-bottom:4px;}\n.tt-issue{font-family:'Oswald',sans-serif;font-size:10px;color:var(--cream);line-height:1.5;margin-bottom:8px;}\n.tt-rewrite{font-family:'Crimson Text',serif;font-style:italic;font-size:12px;color:var(--green);border-left:2px solid var(--green);padding-left:8px;margin-bottom:8px;line-height:1.5;}\n.tt-btns{display:flex;gap:5px;}\n.tt-btn{font-family:'Oswald',sans-serif;font-size:8px;letter-spacing:.08em;text-transform:uppercase;padding:4px 10px;border-radius:2px;cursor:pointer;border:none;transition:all .15s;}\n.tt-btn.accept{background:var(--green-dim);color:var(--green);border:1px solid rgba(29,158,117,0.3);}\n.tt-btn.accept:hover{background:rgba(29,158,117,0.2);}\n.tt-btn.dismiss{background:transparent;color:var(--slate2);border:1px solid rgba(74,93,122,0.25);}\n.tt-btn.dismiss:hover{color:var(--cream);}\n.tt-btn.rewrite{background:var(--orange-dim);color:var(--orange);border:1px solid rgba(255,69,0,0.3);}\n.tt-btn.rewrite:hover{background:rgba(255,69,0,0.2);}\n\n\/* DOC STATUS BAR *\/\n.doc-status{flex-shrink:0;height:24px;background:var(--paper2);border-top:1px solid rgba(24,18,10,0.1);display:flex;align-items:center;padding:0 14px;gap:16px;}\n.doc-status-item{font-family:'Oswald',sans-serif;font-size:8px;letter-spacing:.1em;text-transform:uppercase;color:rgba(24,18,10,0.35);}\n.doc-status-item span{color:rgba(24,18,10,0.6);}\n\n\/* RIGHT \u2014 SUGGESTIONS PANEL *\/\n.suggestions-panel{width:290px;min-width:290px;background:var(--navy2);display:flex;flex-direction:column;border-left:2px solid var(--gold);overflow:hidden;}\n.sp-header{flex-shrink:0;padding:10px 12px;background:rgba(6,13,24,0.6);border-bottom:1px solid rgba(201,168,76,0.12);display:flex;align-items:center;justify-content:space-between;}\n.sp-title{font-family:'Cinzel',serif;font-size:11px;font-weight:700;letter-spacing:.12em;text-transform:uppercase;color:var(--gold);}\n.sp-count{font-family:'Oswald',sans-serif;font-size:9px;letter-spacing:.08em;color:var(--slate2);}\n.eng-row{flex-shrink:0;display:flex;align-items:center;gap:8px;padding:5px 12px;border-bottom:1px solid rgba(201,168,76,0.08);}\n.eng-light{width:7px;height:7px;border-radius:50%;background:#1a2a1a;transition:all .3s;flex-shrink:0;}\n.eng-light.on{background:var(--green);box-shadow:0 0 8px var(--green);}\n.eng-light.busy{background:#0088ff;box-shadow:0 0 8px #0088ff;animation:ep .7s ease-in-out infinite;}\n.eng-light.error{background:#ff3333;}\n@keyframes ep{0%,100%{opacity:1;}50%{opacity:.3;}}\n.eng-name{font-family:'Oswald',sans-serif;font-size:9px;letter-spacing:.1em;color:var(--slate2);text-transform:uppercase;}\n.eng-status{font-family:'Oswald',sans-serif;font-size:8px;color:var(--slate);flex:1;text-align:right;}\n\n\/* SUGGESTION CARDS *\/\n.suggestions-list{flex:1;min-height:0;overflow-y:auto;padding:10px;}\n.sugg-card{background:rgba(255,255,255,0.02);border:1px solid rgba(201,168,76,0.1);border-radius:3px;padding:10px 12px;margin-bottom:7px;cursor:pointer;transition:all .15s;}\n.sugg-card:hover{border-color:rgba(201,168,76,0.3);background:var(--gold-dim);}\n.sugg-card.passive{border-left:3px solid orange;}\n.sugg-card.cliche{border-left:3px solid var(--orange);}\n.sugg-card.drift{border-left:3px solid #7c50d4;}\n.sugg-card.grammar{border-left:3px solid #dc3545;}\n.sugg-card.show{border-left:3px solid var(--green);}\n.sugg-type{font-family:'Oswald',sans-serif;font-size:8px;letter-spacing:.12em;text-transform:uppercase;color:var(--slate);margin-bottom:3px;}\n.sugg-type.passive{color:orange;}\n.sugg-type.cliche{color:var(--orange);}\n.sugg-type.drift{color:#9b77e0;}\n.sugg-type.grammar{color:#ff6b6b;}\n.sugg-type.show{color:var(--green);}\n.sugg-original{font-family:'Crimson Text',serif;font-size:12px;color:var(--cream);line-height:1.5;margin-bottom:5px;}\n.sugg-fix{font-family:'Crimson Text',serif;font-style:italic;font-size:12px;color:var(--green);border-left:2px solid var(--green);padding-left:7px;line-height:1.5;margin-bottom:6px;}\n.sugg-btns{display:flex;gap:4px;}\n.sugg-btn{font-family:'Oswald',sans-serif;font-size:8px;letter-spacing:.06em;text-transform:uppercase;padding:3px 8px;border-radius:2px;cursor:pointer;border:none;transition:all .15s;}\n.sugg-btn.accept{background:var(--green-dim);color:var(--green);border:1px solid rgba(29,158,117,0.3);}\n.sugg-btn.accept:hover{background:rgba(29,158,117,0.2);}\n.sugg-btn.dismiss{background:transparent;color:var(--slate2);border:1px solid rgba(74,93,122,0.2);}\n.sugg-btn.dismiss:hover{color:var(--cream);}\n.sugg-btn.rewrite{background:var(--orange-dim);color:var(--orange);border:1px solid rgba(255,69,0,0.25);}\n.sugg-btn.rewrite:hover{background:rgba(255,69,0,0.18);}\n\n.sp-empty{padding:30px 14px;text-align:center;font-family:'Oswald',sans-serif;font-size:9px;letter-spacing:.1em;text-transform:uppercase;color:var(--slate);}\n.sp-empty-icon{font-size:28px;opacity:.2;margin-bottom:10px;}\n\n\/* QUICK ACTIONS *\/\n.quick-actions{flex-shrink:0;padding:6px 10px;border-bottom:1px solid rgba(201,168,76,0.08);display:flex;gap:4px;flex-wrap:wrap;}\n.qa-btn{font-family:'Oswald',sans-serif;font-size:8px;letter-spacing:.06em;text-transform:uppercase;padding:4px 8px;border-radius:2px;cursor:pointer;border:1px solid;background:transparent;transition:all .15s;}\n.qa-btn.grn{border-color:rgba(29,158,117,0.3);color:var(--green);}\n.qa-btn.grn:hover{background:var(--green-dim);}\n.qa-btn.gld{border-color:rgba(201,168,76,0.25);color:var(--gold);}\n.qa-btn.gld:hover{background:var(--gold-dim);}\n.qa-btn.red{border-color:rgba(255,69,0,0.35);color:var(--orange);}\n.qa-btn.red:hover{background:var(--orange-dim);}\n.qa-btn.sl{border-color:rgba(138,154,181,0.3);color:var(--slate2);}\n.qa-btn.sl:hover{background:rgba(138,154,181,0.08);}\n\n\/* ACCEPT ALL STRIP *\/\n.accept-strip{flex-shrink:0;padding:7px 10px;border-top:1px solid rgba(201,168,76,0.08);display:flex;gap:5px;}\n.accept-all-btn{flex:1;padding:7px;border-radius:2px;font-family:'Oswald',sans-serif;font-size:9px;font-weight:600;letter-spacing:.1em;text-transform:uppercase;cursor:pointer;border:none;transition:all .15s;background:var(--green-dim);color:var(--green);border:1px solid rgba(29,158,117,0.3);}\n.accept-all-btn:hover{background:rgba(29,158,117,0.2);}\n.clear-btn{padding:7px 12px;border-radius:2px;font-family:'Oswald',sans-serif;font-size:9px;letter-spacing:.1em;text-transform:uppercase;cursor:pointer;background:transparent;color:var(--slate2);border:1px solid rgba(74,93,122,0.2);}\n.clear-btn:hover{color:var(--cream);}\n\n\/* UPGRADE MODAL *\/\n.upgrade-modal{position:fixed;inset:0;background:rgba(4,8,18,0.96);z-index:9000;display:none;align-items:center;justify-content:center;}\n.upgrade-box{background:var(--navy3);border:1px solid var(--gold);border-top:3px solid var(--orange);width:min(500px,90vw);padding:32px;box-shadow:0 24px 64px rgba(0,0,0,0.8);}\n.ub-eyebrow{font-family:'Oswald',sans-serif;font-size:9px;letter-spacing:.25em;text-transform:uppercase;color:var(--orange);margin-bottom:10px;}\n.ub-title{font-family:'Cinzel',serif;font-size:22px;font-weight:900;color:var(--gold);margin-bottom:10px;line-height:1.2;}\n.ub-text{font-family:'Oswald',sans-serif;font-size:11px;line-height:1.8;color:var(--cream);margin-bottom:20px;}\n.ub-features{display:flex;flex-direction:column;gap:6px;margin-bottom:22px;}\n.ub-feat{font-family:'Oswald',sans-serif;font-size:10px;letter-spacing:.04em;color:var(--cream);display:flex;align-items:center;gap:8px;}\n.ub-feat::before{content:'';width:6px;height:6px;border-radius:50%;background:var(--green);flex-shrink:0;}\n.ub-btns{display:flex;gap:8px;}\n.ub-btn-primary{flex:1;padding:12px;background:var(--orange);color:#fff;font-family:'Cinzel',serif;font-weight:700;font-size:12px;letter-spacing:.12em;border:none;cursor:pointer;border-radius:2px;box-shadow:0 4px 14px var(--orange-glow);transition:all .15s;}\n.ub-btn-primary:hover{background:#ff5a1a;}\n.ub-btn-secondary{padding:12px 18px;background:transparent;color:var(--slate2);font-family:'Oswald',sans-serif;font-size:10px;letter-spacing:.1em;text-transform:uppercase;border:1px solid rgba(74,93,122,0.3);cursor:pointer;border-radius:2px;}\n.ub-btn-secondary:hover{color:var(--cream);}\n<\/style>\n<\/head>\n<body>\n\n<!-- TOP BAR -->\n<div class=\"topbar\">\n  <div style=\"display:flex;align-items:center;gap:12px;\">\n    <div class=\"tb-brand\">Rebellion <span>Command<\/span> \u00b7 T.W.A.I.<em>N<\/em>.<\/div>\n    <div class=\"tb-badge\">Editor \u2014 Free Tool<\/div>\n  <\/div>\n  <div class=\"tb-center\">\n    <div class=\"tb-stat\">Words <span id=\"stat-words\">0<\/span><\/div>\n    <div class=\"tb-stat\">Issues <span id=\"stat-issues\">0<\/span><\/div>\n    <div class=\"tb-stat\">Voice <span id=\"stat-voice\">Not set<\/span><\/div>\n  <\/div>\n  <div class=\"tb-right\">\n    <button class=\"tb-btn\" onclick=\"clearAll()\">Clear<\/button>\n    <button class=\"tb-btn gold\" onclick=\"exportClean()\">Export Clean<\/button>\n    <button class=\"tb-btn orange\" onclick=\"openUpgradeModal()\">Upgrade to Genesis<\/button>\n  <\/div>\n<\/div>\n\n<!-- LEAD MAGNET BANNER -->\n<div class=\"lm-banner\" id=\"lm-banner\">\n  <div class=\"lm-text\">\n    <strong>T.W.A.I.N. Editor is free.<\/strong> Upgrade to Genesis to keep your Voice Profile permanently, unlock the Forge, The Pulse, and the full Sovereign Writing Cockpit.\n  <\/div>\n  <button class=\"lm-cta\" onclick=\"openUpgradeModal()\">Get Full Access<\/button>\n  <button class=\"lm-dismiss\" onclick=\"dismissBanner()\">\u00d7<\/button>\n<\/div>\n\n<!-- MODULE NAV -->\n<div class=\"module-nav\">\n  <a class=\"mod-link\" href=\"twain-genesis.html\"><span class=\"mod-num\">01<\/span> Genesis<\/a>\n  <span class=\"mod-arrow\">\u203a<\/span>\n  <a class=\"mod-link active\" href=\"twain-editor.html\"><span class=\"mod-num\">\u270e<\/span> Editor<\/a>\n  <span class=\"mod-arrow\">\u203a<\/span>\n  <a class=\"mod-link\" href=\"twain-forge.html\"><span class=\"mod-num\">02<\/span> Forge<\/a>\n  <span class=\"mod-arrow\">\u203a<\/span>\n  <a class=\"mod-link\" href=\"twain-pulse.html\"><span class=\"mod-num\">03<\/span> Pulse<\/a>\n  <span class=\"mod-arrow\">\u203a<\/span>\n  <a class=\"mod-link\" href=\"twain-cover-studio.html\"><span class=\"mod-num\">04<\/span> Cover Studio<\/a>\n  <div class=\"mod-spacer\"><\/div>\n  <span style=\"font-family:'Oswald',sans-serif;font-size:8px;letter-spacing:.12em;text-transform:uppercase;color:var(--slate);\">Rebellion Command Ecosystem<\/span>\n<\/div>\n\n<!-- EDITOR LAYOUT -->\n<div class=\"editor-layout\">\n\n  <!-- LEFT \u2014 VOICE PANEL -->\n  <div class=\"voice-panel\">\n    <div class=\"vp-header\">\n      <div class=\"vp-title\">Voice Profile<\/div>\n      <div class=\"vp-sub\">Acoustic Fingerprint<\/div>\n    <\/div>\n    <div class=\"vp-body\">\n\n      <div class=\"vp-section\">\n        <div class=\"vp-sec-title\">Archetype<\/div>\n        <div class=\"arch-cards\">\n          <div class=\"arch-mini selected\" data-arch=\"sovereign\" onclick=\"selectArch(this)\">\n            <div>\n              <div class=\"arch-mini-name\">Sovereign<\/div>\n              <div class=\"arch-mini-role\">Direct \u00b7 Authoritative<\/div>\n            <\/div>\n          <\/div>\n          <div class=\"arch-mini\" data-arch=\"storyteller\" onclick=\"selectArch(this)\">\n            <div>\n              <div class=\"arch-mini-name\">Storyteller<\/div>\n              <div class=\"arch-mini-role\">Narrative \u00b7 Warm<\/div>\n            <\/div>\n          <\/div>\n          <div class=\"arch-mini\" data-arch=\"strategist\" onclick=\"selectArch(this)\">\n            <div>\n              <div class=\"arch-mini-name\">Strategist<\/div>\n              <div class=\"arch-mini-role\">Analytical \u00b7 Structured<\/div>\n            <\/div>\n          <\/div>\n          <div class=\"arch-mini\" data-arch=\"firestarter\" onclick=\"selectArch(this)\">\n            <div>\n              <div class=\"arch-mini-name\">Firestarter<\/div>\n              <div class=\"arch-mini-role\">Provocative \u00b7 Punchy<\/div>\n            <\/div>\n          <\/div>\n        <\/div>\n      <\/div>\n\n      <div class=\"vp-section\">\n        <div class=\"vp-sec-title\">Active Voice Profile<\/div>\n        <div class=\"custom-voice-area\" id=\"voice-display\"><\/div>\n      <\/div>\n\n      <div class=\"vp-section\">\n        <div class=\"vp-sec-title\">Flag Types<\/div>\n        <div class=\"flag-legend\">\n          <div class=\"flag-item\">\n            <div class=\"flag-dot passive\"><\/div>\n            Passive Voice\n            <button class=\"flag-toggle on\" id=\"tog-passive\" onclick=\"toggleFlag('passive',this)\"><\/button>\n          <\/div>\n          <div class=\"flag-item\">\n            <div class=\"flag-dot cliche\"><\/div>\n            Cliche Language\n            <button class=\"flag-toggle on\" id=\"tog-cliche\" onclick=\"toggleFlag('cliche',this)\"><\/button>\n          <\/div>\n          <div class=\"flag-item\">\n            <div class=\"flag-dot drift\"><\/div>\n            Voice Drift\n            <button class=\"flag-toggle on\" id=\"tog-drift\" onclick=\"toggleFlag('drift',this)\"><\/button>\n          <\/div>\n          <div class=\"flag-item\">\n            <div class=\"flag-dot grammar\"><\/div>\n            Grammar\n            <button class=\"flag-toggle on\" id=\"tog-grammar\" onclick=\"toggleFlag('grammar',this)\"><\/button>\n          <\/div>\n          <div class=\"flag-item\">\n            <div class=\"flag-dot show\"><\/div>\n            Show vs Tell\n            <button class=\"flag-toggle on\" id=\"tog-show\" onclick=\"toggleFlag('show',this)\"><\/button>\n          <\/div>\n        <\/div>\n      <\/div>\n\n    <\/div>\n\n    <div class=\"score-strip\">\n      <div class=\"score-row\">\n        <span class=\"score-lbl\">Voice Match<\/span>\n        <span class=\"score-val gold\" id=\"score-voice\">\u2014<\/span>\n      <\/div>\n      <div class=\"mini-bar-track\"><div class=\"mini-bar-fill\" id=\"bar-voice\" style=\"width:0%;background:var(--gold);\"><\/div><\/div>\n      <div class=\"score-row\">\n        <span class=\"score-lbl\">Active Verbs<\/span>\n        <span class=\"score-val green\" id=\"score-active\">\u2014<\/span>\n      <\/div>\n      <div class=\"mini-bar-track\"><div class=\"mini-bar-fill\" id=\"bar-active\" style=\"width:0%;background:var(--green);\"><\/div><\/div>\n      <div class=\"score-row\">\n        <span class=\"score-lbl\">Clarity<\/span>\n        <span class=\"score-val gold\" id=\"score-clarity\">\u2014<\/span>\n      <\/div>\n      <div class=\"mini-bar-track\"><div class=\"mini-bar-fill\" id=\"bar-clarity\" style=\"width:0%;background:var(--gold);\"><\/div><\/div>\n    <\/div>\n\n    <div class=\"key-strip\">\n      <div class=\"key-lbl\">Groq API Key (for AI rewrites)<\/div>\n      <input type=\"password\" class=\"key-inp\" id=\"groq-key\" placeholder=\"gsk_... for AI-powered suggestions\" oninput=\"saveKey()\" onblur=\"saveKey()\">\n      <div id=\"key-status\" style=\"font-family:Oswald,sans-serif;font-size:7px;color:rgba(201,168,76,0.4);margin-top:3px;letter-spacing:.06em;\">Optional \u2014 enables AI rewrites<\/div>\n    <\/div>\n  <\/div>\n\n  <!-- CENTER \u2014 DOCUMENT -->\n  <div class=\"doc-pane\">\n    <div class=\"doc-toolbar\">\n      <button class=\"doc-tool-btn\" onclick=\"applyFmt('bold')\"><strong>B<\/strong><\/button>\n      <button class=\"doc-tool-btn\" onclick=\"applyFmt('italic')\"><em>I<\/em><\/button>\n      <div class=\"doc-tool-div\"><\/div>\n      <button class=\"doc-tool-btn\" onclick=\"applyBlock('h1')\">H1<\/button>\n      <button class=\"doc-tool-btn\" onclick=\"applyBlock('h2')\">H2<\/button>\n      <button class=\"doc-tool-btn\" onclick=\"applyBlock('p')\">P<\/button>\n      <div class=\"doc-tool-div\"><\/div>\n      <button class=\"doc-tool-btn\" id=\"btn-highlights\" onclick=\"toggleHighlights()\" title=\"Toggle highlights\">Highlights<\/button>\n      <button class=\"doc-tool-btn\" onclick=\"clearHighlights()\">Clear Marks<\/button>\n      <div class=\"doc-tool-div\"><\/div>\n      <button class=\"doc-tool-btn\" onclick=\"triggerUpload()\" title=\"Upload manuscript file (.txt, .docx, .html, .md)\" style=\"color:var(--gold);border-color:rgba(201,168,76,0.3);\">\u2b06 Upload<\/button>\n      <input type=\"file\" id=\"manuscript-upload\" accept=\".txt,.html,.htm,.md,.docx\" style=\"display:none\" onchange=\"handleManuscriptUpload(event)\">\n      <div class=\"doc-tool-spacer\"><\/div>\n      <div class=\"doc-tool-stat\">Words <span id=\"toolbar-wc\">0<\/span><\/div>\n      <div class=\"doc-tool-div\"><\/div>\n      <button class=\"doc-tool-btn\" onclick=\"undoLast()\" title=\"Undo last apply (Ctrl+Z)\">\u21a9 Undo<\/button>\n      <button class=\"analyze-btn\" id=\"analyze-btn\" onclick=\"runAnalysis()\">Analyze Text<\/button>\n    <\/div>\n    <!-- UPLOAD DROP ZONE OVERLAY -->\n    <div id=\"drop-overlay\" style=\"display:none;position:absolute;inset:0;background:rgba(6,13,24,0.92);z-index:200;align-items:center;justify-content:center;flex-direction:column;gap:12px;border:3px dashed var(--gold);border-radius:4px;\">\n      <div style=\"font-family:'Cinzel',serif;font-size:22px;color:var(--gold);\">Drop Manuscript Here<\/div>\n      <div style=\"font-family:'Oswald',sans-serif;font-size:11px;letter-spacing:.15em;text-transform:uppercase;color:var(--slate2);\">.txt \u00b7 .docx \u00b7 .html \u00b7 .md supported<\/div>\n    <\/div>\n    <div class=\"doc-wrap\" style=\"position:relative\">\n      <div id=\"doc-area\"\n        contenteditable=\"true\"\n        data-placeholder=\"Paste or type your writing here. Select your Voice Archetype, then hit Analyze Text \u2014 I will flag passive voice, cliches, voice drift, grammar issues, and show-vs-tell violations, all tuned to your specific voice.\"\n        oninput=\"onDocInput()\"\n        spellcheck=\"true\">\n      <\/div>\n    <\/div>\n    <div class=\"doc-status\">\n      <div class=\"doc-status-item\">Words <span id=\"ds-words\">0<\/span><\/div>\n      <div class=\"doc-status-item\">Sentences <span id=\"ds-sents\">0<\/span><\/div>\n      <div class=\"doc-status-item\">Issues <span id=\"ds-issues\">0<\/span><\/div>\n      <div class=\"doc-status-item\">Archetype <span id=\"ds-arch\">Sovereign<\/span><\/div>\n    <\/div>\n  <\/div>\n\n  <!-- RIGHT \u2014 SUGGESTIONS -->\n  <div class=\"suggestions-panel\">\n    <div class=\"sp-header\">\n      <div class=\"sp-title\">Suggestions<\/div>\n      <div class=\"sp-count\" id=\"sp-count\">0 issues<\/div>\n    <\/div>\n    <div class=\"eng-row\">\n      <div class=\"eng-light\" id=\"eng-light\"><\/div>\n      <div class=\"eng-name\" id=\"eng-name\">Ready<\/div>\n      <div class=\"eng-status\" id=\"eng-status\">Enter Groq key for AI rewrites<\/div>\n    <\/div>\n    <div class=\"quick-actions\">\n      <button class=\"qa-btn grn\" onclick=\"acceptAll('passive')\">Fix Passive<\/button>\n      <button class=\"qa-btn red\" onclick=\"acceptAll('cliche')\">Fix Cliches<\/button>\n      <button class=\"qa-btn gld\" onclick=\"rewriteInVoice()\">Rewrite in Voice<\/button>\n      <button class=\"qa-btn sl\" onclick=\"checkRhythm()\">Rhythm Check<\/button>\n    <\/div>\n    <div class=\"suggestions-list\" id=\"suggestions-list\">\n      <div class=\"sp-empty\">\n        <div class=\"sp-empty-icon\">\u270e<\/div>\n        Paste your text and hit Analyze Text to see voice-aware suggestions.\n      <\/div>\n    <\/div>\n    <div class=\"accept-strip\">\n      <button class=\"accept-all-btn\" onclick=\"acceptAllSuggestions()\">Accept All Suggestions<\/button>\n      <button class=\"clear-btn\" onclick=\"clearAll()\">Clear<\/button>\n    <\/div>\n  <\/div>\n\n<\/div>\n\n<!-- SUGGESTION TOOLTIP -->\n<div class=\"suggestion-tooltip\" id=\"sugg-tooltip\">\n  <div class=\"tt-type\" id=\"tt-type\">Passive Voice<\/div>\n  <div class=\"tt-issue\" id=\"tt-issue\">Issue description<\/div>\n  <div class=\"tt-rewrite\" id=\"tt-rewrite\">Suggested rewrite<\/div>\n  <div class=\"tt-btns\">\n    <button class=\"tt-btn accept\" onclick=\"acceptFromTooltip()\">Accept<\/button>\n    <button class=\"tt-btn rewrite\" onclick=\"requestRewrite()\">AI Rewrite<\/button>\n    <button class=\"tt-btn dismiss\" onclick=\"hideTooltip()\">Dismiss<\/button>\n  <\/div>\n<\/div>\n\n<!-- UPGRADE MODAL -->\n<div class=\"upgrade-modal\" id=\"upgrade-modal\">\n  <div class=\"upgrade-box\">\n    <div class=\"ub-eyebrow\">T.W.A.I.N. by Rebellion Command<\/div>\n    <div class=\"ub-title\">The Editor is just the doorway.<\/div>\n    <div class=\"ub-text\">\n      You have been editing with a free voice-aware tool. Genesis is the full Sovereign Writing Cockpit \u2014 where the Voice Profile you just set persists permanently, the Story Bible keeps A.A. consistent across every chapter, and the multi-model brain rotation gives you four different creative perspectives on demand.\n    <\/div>\n    <div class=\"ub-features\">\n      <div class=\"ub-feat\">Sovereign Writing Cockpit \u2014 full editor with persistent Voice Profile<\/div>\n      <div class=\"ub-feat\">Story Bible \u2014 A.A. never forgets your characters, locations, or rules<\/div>\n      <div class=\"ub-feat\">Multi-model brain \u2014 Llama 70B, Qwen 72B, Llama 4 Scout<\/div>\n      <div class=\"ub-feat\">Anti-writer&#8217;s block \u2014 three Sledgehammer options on demand<\/div>\n      <div class=\"ub-feat\">Manuscript Forge \u2014 print-ready formatting with front matter<\/div>\n      <div class=\"ub-feat\">The Pulse \u2014 full commercial stress test against 2024-2026 bestseller norms<\/div>\n    <\/div>\n    <div class=\"ub-btns\">\n      <button class=\"ub-btn-primary\" onclick=\"window.open('https:\/\/rebellioncommand.com','_blank')\">Start with T.W.A.I.N. \u2014 $9.99\/month<\/button>\n      <button class=\"ub-btn-secondary\" onclick=\"closeUpgradeModal()\">Keep Using Free Editor<\/button>\n    <\/div>\n  <\/div>\n<\/div>\n\n<script>\n\/\/ ================================================================\n\/\/ T.W.A.I.N. EDITOR v2 \u2014 VOICE-AWARE WRITING ASSISTANT\n\/\/ ================================================================\n\nvar E = {\n  selectedArch: 'sovereign',\n  voiceProfile: null,\n  suggestions: [],       \/\/ each item gets a unique .id; dismissed items kept with .dismissed=true\n  activeFlags: { passive: true, cliche: true, drift: true, grammar: true, show: true },\n  highlightsOn: true,\n  activeSuggIdx: -1,\n  conversation: [],\n  snapshots: []          \/\/ undo stack\n};\n\nvar ARCHETYPES = {\n  sovereign: {\n    name: 'Sovereign',\n    tone: 'Direct, authoritative, commanding',\n    sentenceStyle: 'Short declarative sentences. No filler. Active voice.',\n    forbid: 'Never use: perhaps, might, could potentially, lengthy qualifications.',\n    cliches: ['in today\\'s world','at the end of the day','think outside the box','move the needle','circle back','deep dive','level up','game changer','paradigm shift','bleeding edge'],\n    passiveIndicators: ['was ','were ','been ','being ','is being','are being','was being','has been','have been','had been'],\n    rhythm: 'Short. Punchy. Varied but weighted toward brief.'\n  },\n  storyteller: {\n    name: 'Storyteller',\n    tone: 'Warm, narrative, emotionally present',\n    sentenceStyle: 'Varied length. Sensory details. Present the scene, not the summary.',\n    forbid: 'Never use: abstract nouns in place of action. Never summarize when you can show.',\n    cliches: ['her heart raced','time stood still','butterflies in her stomach','a chill ran down','blood ran cold','eyes like','as if time had stopped','the world fell away'],\n    passiveIndicators: ['was ','were ','been ','being ','had been','was felt','was seen','was heard'],\n    rhythm: 'Flowing. Mix of long and short. Rhythm like breath.'\n  },\n  strategist: {\n    name: 'Strategist',\n    tone: 'Analytical, structured, evidence-grounded',\n    sentenceStyle: 'Medium length. Clear logic chain. Evidence then implication.',\n    forbid: 'Never use: vague claims, emotional appeals without data, weasel words.',\n    cliches: ['studies show','experts agree','it goes without saying','needless to say','the fact of the matter','at this point in time','due to the fact that'],\n    passiveIndicators: ['was found','were determined','has been shown','is considered','are believed','was established'],\n    rhythm: 'Methodical. Framework first, evidence second, so what third.'\n  },\n  firestarter: {\n    name: 'Firestarter',\n    tone: 'Provocative, confrontational, disruptive',\n    sentenceStyle: 'Short bursts. Fragments. Challenge first, explain later.',\n    forbid: 'Never use: gentle qualifiers, softening language, both-sides hedging.',\n    cliches: ['the truth is','let\\'s be honest','here\\'s the thing','the reality is','make no mistake','plain and simple','bottom line'],\n    passiveIndicators: ['was ','were ','been ','is being','are being','has been'],\n    rhythm: 'Staccato. Burst. Pause. Burst again.'\n  }\n};\n\n\/\/ ================================================================\n\/\/ INIT\n\/\/ ================================================================\nwindow.addEventListener('load', function() {\n  var key = sessionStorage.getItem('twain_groq_key') || localStorage.getItem('twain_groq_key') || '';\n  if (key) {\n    document.getElementById('groq-key').value = key;\n    document.getElementById('key-status').textContent = '\u2713 Key loaded';\n    document.getElementById('key-status').style.color = 'var(--green)';\n  }\n  try {\n    var vp = localStorage.getItem('twain_voice_profile');\n    if (vp) {\n      E.voiceProfile = JSON.parse(vp);\n      renderVoiceProfile(E.voiceProfile);\n      document.getElementById('stat-voice').textContent = E.voiceProfile.name || 'Custom';\n      document.getElementById('ds-arch').textContent = E.voiceProfile.name || 'Custom';\n    } else {\n      selectArch(document.querySelector('.arch-mini.selected'));\n    }\n  } catch(e) {\n    selectArch(document.querySelector('.arch-mini.selected'));\n  }\n  setLight('on', 'Ready');\n  updateWordCount();\n});\n\n\/\/ ================================================================\n\/\/ KEY MANAGEMENT\n\/\/ ================================================================\nfunction saveKey() {\n  var key = document.getElementById('groq-key').value.trim();\n  if (key) {\n    localStorage.setItem('twain_groq_key', key);\n    sessionStorage.setItem('twain_groq_key', key);\n    document.getElementById('key-status').textContent = '\u2713 Key saved';\n    document.getElementById('key-status').style.color = 'var(--green)';\n  } else {\n    localStorage.removeItem('twain_groq_key');\n    sessionStorage.removeItem('twain_groq_key');\n    document.getElementById('key-status').textContent = 'Optional \u2014 enables AI rewrites';\n    document.getElementById('key-status').style.color = 'rgba(201,168,76,0.4)';\n  }\n}\n\nfunction getKey() {\n  var f = document.getElementById('groq-key').value.trim();\n  if (f) return f;\n  var s = sessionStorage.getItem('twain_groq_key') || localStorage.getItem('twain_groq_key') || '';\n  if (s) document.getElementById('groq-key').value = s;\n  return s;\n}\n\n\/\/ ================================================================\n\/\/ ARCHETYPE SELECTION\n\/\/ ================================================================\nfunction selectArch(card) {\n  document.querySelectorAll('.arch-mini').forEach(function(c) { c.classList.remove('selected'); });\n  card.classList.add('selected');\n  E.selectedArch = card.dataset.arch;\n  E.voiceProfile = ARCHETYPES[E.selectedArch];\n  renderVoiceProfile(E.voiceProfile);\n  document.getElementById('stat-voice').textContent = E.voiceProfile.name;\n  document.getElementById('ds-arch').textContent = E.voiceProfile.name;\n}\n\nfunction renderVoiceProfile(profile) {\n  var el = document.getElementById('voice-display');\n  el.innerHTML =\n    '<strong style=\"color:var(--gold);font-family:Oswald,sans-serif;font-size:9px;\">' + (profile.name || 'Custom') + '<\/strong><br>' +\n    '<span style=\"font-family:Oswald,sans-serif;font-size:9px;color:var(--slate2);\">' + (profile.tone || '') + '<\/span><br><br>' +\n    '<span style=\"font-family:Oswald,sans-serif;font-size:9px;color:var(--slate);\">' + (profile.sentenceStyle || '') + '<\/span>';\n}\n\n\/\/ ================================================================\n\/\/ ANALYSIS ENGINE\n\/\/ ================================================================\nvar _suggId = 0;\nfunction nextId() { return ++_suggId; }\n\nasync function runAnalysis() {\n  var text = document.getElementById('doc-area').innerText || '';\n  if (!text.trim() || text.trim().length < 30) return;\n  var key = getKey();\n  document.getElementById('analyze-btn').disabled = true;\n  setLight('busy', 'Analyzing...');\n  E.suggestions = [];\n\n  var profile = E.voiceProfile || ARCHETYPES[E.selectedArch];\n\n  \/\/ Pass 1 \u2014 local rules (instant)\n  var local = runLocalChecks(text, profile);\n  local.forEach(function(s) { s.id = nextId(); });\n  E.suggestions = E.suggestions.concat(local);\n  renderSuggestions();\n  if (E.highlightsOn) applyHighlights();\n\n  \/\/ Pass 2 \u2014 AI (if key present)\n  if (key) {\n    var ai = await runAIAnalysis(text, profile, key);\n    if (ai &#038;&#038; ai.length) {\n      ai.forEach(function(s) { s.id = nextId(); });\n      E.suggestions = E.suggestions.concat(ai);\n      renderSuggestions();\n      if (E.highlightsOn) applyHighlights();\n    }\n  }\n\n  updateScores(text, profile);\n  document.getElementById('analyze-btn').disabled = false;\n  setLight('on', 'Ready');\n}\n\nfunction runLocalChecks(text, profile) {\n  var suggestions = [];\n  var sentences = text.split(\/[.!?]+\/).filter(function(s) { return s.trim().length > 5; });\n\n  sentences.forEach(function(sent, idx) {\n    var trimmed = sent.trim().toLowerCase();\n\n    if (E.activeFlags.passive && profile.passiveIndicators) {\n      profile.passiveIndicators.forEach(function(ind) {\n        if (trimmed.indexOf(ind) !== -1) {\n          suggestions.push({\n            type: 'passive',\n            original: sent.trim().substring(0, 100),\n            issue: 'Passive construction detected. ' + profile.name + ' voice demands active verbs.',\n            fix: 'Rewrite with an active verb \u2014 who is doing the action?',\n            sentenceText: sent.trim()\n          });\n        }\n      });\n    }\n\n    if (E.activeFlags.cliche && profile.cliches) {\n      profile.cliches.forEach(function(cliche) {\n        if (trimmed.indexOf(cliche.toLowerCase()) !== -1) {\n          suggestions.push({\n            type: 'cliche',\n            original: sent.trim().substring(0, 100),\n            issue: 'Cliche detected: \"' + cliche + '\". This phrase has lost all impact.',\n            fix: 'Say what you actually mean. Be specific. Own the thought.',\n            sentenceText: sent.trim(),\n            clichePhrase: cliche\n          });\n        }\n      });\n    }\n  });\n\n  \/\/ Rhythm \u2014 long sentence clusters\n  var longCount = sentences.filter(function(s) { return s.trim().split(' ').length > 25; }).length;\n  if (longCount > 3 && E.activeFlags.drift) {\n    suggestions.push({\n      type: 'drift',\n      original: 'Multiple long sentences in sequence (' + longCount + ' found)',\n      issue: 'Sentence length has become monotonous. ' + profile.name + ' rhythm: ' + profile.rhythm,\n      fix: 'Break at least two of these long sentences. Vary the beat.',\n      sentenceText: null\n    });\n  }\n\n  return suggestions;\n}\n\nasync function runAIAnalysis(text, profile, key) {\n  var sysPrompt = 'You are a voice-aware writing editor for T.W.A.I.N. by Rebellion Command. ' +\n    'The author uses the ' + profile.name + ' voice archetype.\\n' +\n    'VOICE RULES: ' + profile.sentenceStyle + '\\nNEVER: ' + profile.forbid + '\\n\\n' +\n    'Analyze for: show-dont-tell violations, voice drift, grammar issues.\\n' +\n    'Return JSON array: [{\"type\":\"show|drift|grammar\",\"original\":\"exact quoted text max 80 chars\",\"issue\":\"brief explanation\",\"fix\":\"one-sentence fix\"}]\\n' +\n    'Return ONLY the JSON array. If no issues return [].';\n  try {\n    setLight('busy', 'AI scan...');\n    var res = await fetch('https:\/\/api.groq.com\/openai\/v1\/chat\/completions', {\n      method: 'POST',\n      headers: { 'Authorization': 'Bearer ' + key, 'Content-Type': 'application\/json' },\n      body: JSON.stringify({\n        model: 'llama-3.1-8b-instant',\n        max_tokens: 700,\n        temperature: 0.3,\n        messages: [\n          { role: 'system', content: sysPrompt },\n          { role: 'user', content: 'Analyze:\\n\\n' + text.substring(0, 2500) }\n        ]\n      })\n    });\n    var data = await res.json();\n    if (!res.ok) return null;\n    var raw = data.choices[0].message.content.trim().replace(\/```json|```\/g, '').trim();\n    var parsed = JSON.parse(raw);\n    return Array.isArray(parsed) ? parsed.map(function(s) {\n      s.sentenceText = s.original || null;\n      return s;\n    }) : [];\n  } catch(e) { return null; }\n}\n\n\/\/ ================================================================\n\/\/ RENDER SUGGESTIONS\n\/\/ ================================================================\nfunction renderSuggestions() {\n  var filtered = E.suggestions.filter(function(s) { return !s.dismissed && E.activeFlags[s.type]; });\n  var list = document.getElementById('suggestions-list');\n  document.getElementById('sp-count').textContent = filtered.length + ' issue' + (filtered.length !== 1 ? 's' : '');\n  document.getElementById('stat-issues').textContent = filtered.length;\n  document.getElementById('ds-issues').textContent = filtered.length;\n\n  if (filtered.length === 0) {\n    list.innerHTML = '<div class=\"sp-empty\"><div class=\"sp-empty-icon\">\u2713<\/div>No issues found for this voice profile. Clean writing.<\/div>';\n    return;\n  }\n\n  list.innerHTML = filtered.map(function(s) {\n    var hasAiRewrite = !!s.aiRewrite;\n    return '<div class=\"sugg-card ' + s.type + '\" id=\"sc-' + s.id + '\" onclick=\"focusSuggestion(' + s.id + ')\">' +\n      '<div class=\"sugg-type ' + s.type + '\">' + typeLabel(s.type) + '<\/div>' +\n      '<div class=\"sugg-original\">' + escHtml(s.original) + '<\/div>' +\n      (hasAiRewrite\n        ? '<div class=\"sugg-fix\">' + escHtml(s.aiRewrite) + '<\/div>'\n        : '<div class=\"sugg-fix\">' + escHtml(s.fix) + '<\/div>') +\n      '<div class=\"sugg-btns\">' +\n        (hasAiRewrite && s.sentenceText\n          ? '<button class=\"sugg-btn accept\" onclick=\"applySugg(event,' + s.id + ')\">Apply Fix<\/button>'\n          : '') +\n        '<button class=\"sugg-btn rewrite\" onclick=\"aiRewriteSugg(event,' + s.id + ')\">AI Fix<\/button>' +\n        '<button class=\"sugg-btn dismiss\" onclick=\"dismissSugg(event,' + s.id + ')\">Dismiss<\/button>' +\n      '<\/div>' +\n    '<\/div>';\n  }).join('');\n}\n\nfunction typeLabel(type) {\n  return { passive:'Passive Voice', cliche:'Cliche', drift:'Voice Drift', grammar:'Grammar', show:'Show vs Tell' }[type] || type;\n}\n\nfunction escHtml(str) {\n  if (!str) return '';\n  return String(str).replace(\/&\/g,'&amp;').replace(\/<\/g,'&lt;').replace(\/>\/g,'&gt;');\n}\n\nfunction getSuggById(id) {\n  return E.suggestions.filter(function(s) { return s.id === id; })[0] || null;\n}\n\nfunction dismissSugg(event, id) {\n  event.stopPropagation();\n  var s = getSuggById(id);\n  if (s) s.dismissed = true;\n  renderSuggestions();\n  applyHighlights();\n}\n\n\/\/ ================================================================\n\/\/ INLINE HIGHLIGHTS \u2014 real span injection\n\/\/ ================================================================\nfunction applyHighlights() {\n  if (!E.highlightsOn) return;\n  var docArea = document.getElementById('doc-area');\n\n  \/\/ Save caret position\n  var savedSel = saveSelection(docArea);\n\n  \/\/ Strip existing highlight spans cleanly\n  docArea.querySelectorAll('span[data-hl]').forEach(function(span) {\n    var parent = span.parentNode;\n    while (span.firstChild) parent.insertBefore(span.firstChild, span);\n    parent.removeChild(span);\n  });\n  docArea.normalize();\n\n  \/\/ Only highlight active, non-dismissed suggestions with real sentence text\n  var toHL = E.suggestions.filter(function(s) {\n    return !s.dismissed && E.activeFlags[s.type] && s.sentenceText && s.sentenceText.length > 4;\n  });\n\n  toHL.forEach(function(s) {\n    \/\/ Find and wrap the phrase in the DOM\n    var phrase = s.clichePhrase\n      ? s.clichePhrase\n      : s.sentenceText.substring(0, 60);\n    highlightPhrase(docArea, phrase, 'hl-' + s.type, s.id);\n  });\n\n  \/\/ Restore caret\n  if (savedSel) restoreSelection(docArea, savedSel);\n  document.getElementById('btn-highlights').classList.add('active');\n}\n\nfunction highlightPhrase(container, phrase, cls, suggId) {\n  if (!phrase || phrase.length < 3) return;\n  var walker = document.createTreeWalker(container, NodeFilter.SHOW_TEXT, null, false);\n  var node;\n  var phraseLower = phrase.toLowerCase();\n  while ((node = walker.nextNode())) {\n    var idx = node.nodeValue.toLowerCase().indexOf(phraseLower);\n    if (idx !== -1) {\n      var before = node.nodeValue.substring(0, idx);\n      var match  = node.nodeValue.substring(idx, idx + phrase.length);\n      var after  = node.nodeValue.substring(idx + phrase.length);\n      var span = document.createElement('span');\n      span.className = cls;\n      span.setAttribute('data-hl', suggId);\n      span.setAttribute('title', typeLabel(cls.replace('hl-','')) + ' \u2014 click suggestion card to focus');\n      span.textContent = match;\n      var parent = node.parentNode;\n      var ref = node.nextSibling;\n      parent.removeChild(node);\n      if (before) parent.insertBefore(document.createTextNode(before), ref);\n      parent.insertBefore(span, ref);\n      if (after) parent.insertBefore(document.createTextNode(after), ref);\n      break; \/\/ one highlight per suggestion\n    }\n  }\n}\n\nfunction clearHighlights() {\n  var docArea = document.getElementById('doc-area');\n  docArea.querySelectorAll('span[data-hl]').forEach(function(span) {\n    var parent = span.parentNode;\n    while (span.firstChild) parent.insertBefore(span.firstChild, span);\n    parent.removeChild(span);\n  });\n  docArea.normalize();\n  document.getElementById('btn-highlights').classList.remove('active');\n}\n\nfunction toggleHighlights() {\n  E.highlightsOn = !E.highlightsOn;\n  if (E.highlightsOn) applyHighlights();\n  else clearHighlights();\n}\n\n\/\/ ================================================================\n\/\/ CARET SAVE \/ RESTORE (so highlights don't lose cursor position)\n\/\/ ================================================================\nfunction saveSelection(container) {\n  try {\n    var sel = window.getSelection();\n    if (!sel || sel.rangeCount === 0) return null;\n    var range = sel.getRangeAt(0);\n    var preRange = range.cloneRange();\n    preRange.selectNodeContents(container);\n    preRange.setEnd(range.startContainer, range.startOffset);\n    return { start: preRange.toString().length, end: preRange.toString().length + range.toString().length };\n  } catch(e) { return null; }\n}\n\nfunction restoreSelection(container, saved) {\n  try {\n    if (!saved) return;\n    var charIndex = 0;\n    var range = document.createRange();\n    range.setStart(container, 0);\n    range.collapse(true);\n    var walker = document.createTreeWalker(container, NodeFilter.SHOW_TEXT, null, false);\n    var node, found = false;\n    while ((node = walker.nextNode()) &#038;&#038; !found) {\n      var next = charIndex + node.length;\n      if (!found &#038;&#038; saved.start >= charIndex && saved.start <= next) {\n        range.setStart(node, saved.start - charIndex);\n        range.setEnd(node, saved.start - charIndex);\n        found = true;\n      }\n      charIndex = next;\n    }\n    var sel = window.getSelection();\n    sel.removeAllRanges();\n    sel.addRange(range);\n  } catch(e) {}\n}\n\n\/\/ ================================================================\n\/\/ FOCUS SUGGESTION \u2014 scroll + pulse highlight in doc\n\/\/ ================================================================\nfunction focusSuggestion(id) {\n  var s = getSuggById(id);\n  if (!s) return;\n  \/\/ Pulse the highlight span\n  var span = document.querySelector('span[data-hl=\"' + id + '\"]');\n  if (span) {\n    span.classList.add('hl-active');\n    span.scrollIntoView({ behavior: 'smooth', block: 'center' });\n    setTimeout(function() { span.classList.remove('hl-active'); }, 1500);\n  }\n  \/\/ Highlight the card\n  document.querySelectorAll('.sugg-card').forEach(function(c) { c.style.borderColor = ''; });\n  var card = document.getElementById('sc-' + id);\n  if (card) {\n    card.style.borderColor = 'var(--gold)';\n    card.scrollIntoView({ behavior: 'smooth', block: 'nearest' });\n  }\n  E.activeSuggIdx = id;\n}\n\n\/\/ ================================================================\n\/\/ APPLY FIX \u2014 replace sentence text in doc\n\/\/ ================================================================\nfunction applySugg(event, id) {\n  event.stopPropagation();\n  var s = getSuggById(id);\n  if (!s || !s.sentenceText || !s.aiRewrite) return;\n  \/\/ Push undo snapshot\n  pushSnapshot();\n  var docArea = document.getElementById('doc-area');\n  \/\/ Try to find and replace the span first (cleanest)\n  var span = document.querySelector('span[data-hl=\"' + id + '\"]');\n  if (span) {\n    \/\/ Replace the parent sentence text \u2014 find the containing block\n    var block = span;\n    while (block &#038;&#038; block.parentNode !== docArea) block = block.parentNode;\n    if (block) {\n      var blockText = block.innerText || block.textContent || '';\n      var newText = blockText.replace(s.sentenceText.substring(0, 60), s.aiRewrite);\n      block.innerText = newText;\n    }\n  } else {\n    \/\/ Fallback: replace in raw innerText\n    var full = docArea.innerText;\n    var idx = full.indexOf(s.sentenceText.substring(0, 50));\n    if (idx !== -1) {\n      var updated = full.substring(0, idx) + s.aiRewrite + full.substring(idx + s.sentenceText.length);\n      docArea.innerText = updated;\n    }\n  }\n  s.dismissed = true;\n  renderSuggestions();\n  applyHighlights();\n  updateWordCount();\n}\n\nfunction pushSnapshot() {\n  var snap = document.getElementById('doc-area').innerHTML;\n  E.snapshots.push(snap);\n  if (E.snapshots.length > 20) E.snapshots.shift();\n}\n\nfunction undoLast() {\n  if (E.snapshots.length === 0) return;\n  document.getElementById('doc-area').innerHTML = E.snapshots.pop();\n  updateWordCount();\n}\n\n\/\/ ================================================================\n\/\/ AI REWRITE PER SUGGESTION\n\/\/ ================================================================\nasync function aiRewriteSugg(event, id) {\n  event.stopPropagation();\n  var key = getKey();\n  if (!key) { alert('Enter your Groq API key to use AI rewrites.'); return; }\n  var s = getSuggById(id);\n  if (!s) return;\n  var profile = E.voiceProfile || ARCHETYPES[E.selectedArch];\n  setLight('busy', 'Rewriting...');\n  var card = document.getElementById('sc-' + id);\n  if (card) card.style.opacity = '0.5';\n  try {\n    var res = await fetch('https:\/\/api.groq.com\/openai\/v1\/chat\/completions', {\n      method: 'POST',\n      headers: { 'Authorization': 'Bearer ' + key, 'Content-Type': 'application\/json' },\n      body: JSON.stringify({\n        model: 'llama-3.1-8b-instant',\n        max_tokens: 200,\n        temperature: 0.55,\n        messages: [{\n          role: 'user',\n          content: 'Rewrite in ' + profile.name + ' voice (' + profile.tone + '). Fix the ' + typeLabel(s.type) + ' issue. Return ONLY the rewritten text, nothing else.\\n\\nOriginal: ' + s.original\n        }]\n      })\n    });\n    var data = await res.json();\n    s.aiRewrite = data.choices[0].message.content.trim();\n    setLight('on', 'Ready');\n  } catch(e) {\n    setLight('error', 'Error');\n  }\n  if (card) card.style.opacity = '';\n  renderSuggestions();\n}\n\n\/\/ ================================================================\n\/\/ ACCEPT ALL \/ DISMISS ALL\n\/\/ ================================================================\nfunction acceptAllSuggestions() {\n  E.suggestions.forEach(function(s) { s.dismissed = true; });\n  renderSuggestions();\n  clearHighlights();\n}\n\nfunction acceptAll(type) {\n  E.suggestions.forEach(function(s) { if (s.type === type) s.dismissed = true; });\n  renderSuggestions();\n  applyHighlights();\n}\n\n\/\/ ================================================================\n\/\/ SCORES\n\/\/ ================================================================\nfunction updateScores(text, profile) {\n  var sentences = text.split(\/[.!?]+\/).filter(function(s) { return s.trim().length > 5; });\n  var total = sentences.length;\n  if (total === 0) return;\n\n  var passiveCount = 0;\n  sentences.forEach(function(s) {\n    var lower = s.toLowerCase();\n    (profile.passiveIndicators || []).forEach(function(ind) {\n      if (lower.indexOf(ind) !== -1) passiveCount++;\n    });\n  });\n  var activeRatio = Math.round(((total - passiveCount) \/ total) * 100);\n  var issueCount = E.suggestions.filter(function(s) { return !s.dismissed; }).length;\n  var voiceMatch = Math.max(0, Math.round(((total - issueCount) \/ total) * 100));\n  var avgLen = sentences.reduce(function(sum, s) { return sum + s.trim().split(' ').length; }, 0) \/ total;\n  var clarityScore = Math.round(Math.max(0, Math.min(100, 100 - (avgLen - 15) * 2)));\n\n  setScore('score-voice',  'bar-voice',  voiceMatch,   80, 60);\n  setScore('score-active', 'bar-active', activeRatio,  65, 50, '%');\n  setScore('score-clarity','bar-clarity',clarityScore,  70, 50);\n}\n\nfunction setScore(valId, barId, val, greenT, goldT, suffix) {\n  var el  = document.getElementById(valId);\n  var bar = document.getElementById(barId);\n  var cls = val >= greenT ? 'green' : val >= goldT ? 'gold' : 'orange';\n  if (el)  { el.textContent = val + (suffix||'%'); el.className = 'score-val ' + cls; }\n  if (bar) { setTimeout(function() {\n    bar.style.width = val + '%';\n    bar.style.background = val >= greenT ? 'var(--green)' : val >= goldT ? 'var(--gold)' : 'var(--orange)';\n  }, 100); }\n}\n\n\/\/ ================================================================\n\/\/ QUICK ACTIONS\n\/\/ ================================================================\nasync function rewriteInVoice() {\n  var key = getKey();\n  if (!key) { openUpgradeModal(); return; }\n  var text = document.getElementById('doc-area').innerText || '';\n  if (!text.trim()) return;\n  var profile = E.voiceProfile || ARCHETYPES[E.selectedArch];\n  setLight('busy', 'Rewriting...');\n  try {\n    var res = await fetch('https:\/\/api.groq.com\/openai\/v1\/chat\/completions', {\n      method: 'POST',\n      headers: { 'Authorization': 'Bearer ' + key, 'Content-Type': 'application\/json' },\n      body: JSON.stringify({\n        model: 'llama-3.3-70b-versatile',\n        max_tokens: 800,\n        temperature: 0.6,\n        messages: [{ role: 'user', content: 'Rewrite in ' + profile.name + ' voice. Rules: ' + profile.sentenceStyle + ' NEVER: ' + profile.forbid + '\\n\\nKeep all ideas. Return ONLY the rewritten text.\\n\\nOriginal:\\n' + text.substring(0, 1500) }]\n      })\n    });\n    var data = await res.json();\n    var rewrite = data.choices[0].message.content.trim();\n    var list = document.getElementById('suggestions-list');\n    var div = document.createElement('div');\n    var rid = nextId();\n    div.className = 'sugg-card show';\n    div.id = 'sc-' + rid;\n    div.innerHTML = '<div class=\"sugg-type show\">Full Rewrite \u2014 ' + profile.name + ' Voice<\/div>' +\n      '<div class=\"sugg-fix\">' + escHtml(rewrite) + '<\/div>' +\n      '<div class=\"sugg-btns\"><button class=\"sugg-btn accept\" onclick=\"applyFullRewrite(this,\\'' + encodeURIComponent(rewrite) + '\\')\">Apply Rewrite<\/button><\/div>';\n    list.insertBefore(div, list.firstChild);\n    setLight('on', 'Ready');\n  } catch(e) { setLight('error', 'Error'); }\n}\n\nfunction applyFullRewrite(btn, encoded) {\n  pushSnapshot();\n  var text = decodeURIComponent(encoded);\n  document.getElementById('doc-area').innerText = text;\n  updateWordCount();\n  btn.textContent = 'Applied \u2713';\n  btn.style.pointerEvents = 'none';\n}\n\nasync function checkRhythm() {\n  var key = getKey();\n  if (!key) { openUpgradeModal(); return; }\n  var text = document.getElementById('doc-area').innerText || '';\n  if (!text.trim()) return;\n  var profile = E.voiceProfile || ARCHETYPES[E.selectedArch];\n  setLight('busy', 'Checking rhythm...');\n  try {\n    var res = await fetch('https:\/\/api.groq.com\/openai\/v1\/chat\/completions', {\n      method: 'POST',\n      headers: { 'Authorization': 'Bearer ' + key, 'Content-Type': 'application\/json' },\n      body: JSON.stringify({\n        model: 'llama-3.1-8b-instant',\n        max_tokens: 300,\n        temperature: 0.4,\n        messages: [{ role: 'user', content: 'Analyze rhythm and sentence length variety for a ' + profile.name + ' writer. Expected: ' + profile.rhythm + '\\n\\nRate rhythm 1-10. Where does it break down? One specific fix. Be brief.\\n\\nText:\\n' + text.substring(0, 1500) }]\n      })\n    });\n    var data = await res.json();\n    var reply = data.choices[0].message.content.trim();\n    var list = document.getElementById('suggestions-list');\n    var div = document.createElement('div');\n    div.className = 'sugg-card passive';\n    div.innerHTML = '<div class=\"sugg-type passive\">Rhythm Analysis<\/div><div class=\"sugg-original\">' + escHtml(reply) + '<\/div>';\n    list.insertBefore(div, list.firstChild);\n    setLight('on', 'Ready');\n  } catch(e) { setLight('error', 'Error'); }\n}\n\n\/\/ ================================================================\n\/\/ FLAG TOGGLES\n\/\/ ================================================================\nfunction toggleFlag(type, btn) {\n  E.activeFlags[type] = !E.activeFlags[type];\n  btn.classList.toggle('on', E.activeFlags[type]);\n  renderSuggestions();\n  if (E.highlightsOn) applyHighlights();\n}\n\n\/\/ ================================================================\n\/\/ EDITOR UTILS\n\/\/ ================================================================\nfunction applyFmt(cmd) {\n  document.getElementById('doc-area').focus();\n  try { document.execCommand(cmd, false, null); } catch(e) {}\n}\n\nfunction applyBlock(tag) {\n  document.getElementById('doc-area').focus();\n  try { document.execCommand('formatBlock', false, tag); } catch(e) {}\n}\n\nfunction onDocInput() {\n  updateWordCount();\n}\n\nfunction updateWordCount() {\n  var text = document.getElementById('doc-area').innerText || '';\n  var words = text.trim() ? text.trim().split(\/\\s+\/).length : 0;\n  var sents = text.split(\/[.!?]+\/).filter(function(s) { return s.trim().length > 5; }).length;\n  document.getElementById('stat-words').textContent = words.toLocaleString();\n  document.getElementById('toolbar-wc').textContent = words.toLocaleString();\n  document.getElementById('ds-words').textContent = words.toLocaleString();\n  document.getElementById('ds-sents').textContent = sents;\n}\n\nfunction exportClean() {\n  var text = document.getElementById('doc-area').innerHTML;\n  var html = '<!DOCTYPE html><html><head><meta charset=\"UTF-8\"><title>TWAIN Export<\/title>' +\n    '<style>body{font-family:\"Crimson Text\",Georgia,serif;font-size:13pt;line-height:1.8;max-width:6in;margin:2in auto;color:#18120a;}h1,h2{font-family:Cinzel,serif;}<\/style>' +\n    '<\/head><body>' + text + '<\/body><\/html>';\n  var a = document.createElement('a');\n  a.href = URL.createObjectURL(new Blob([html], { type: 'text\/html' }));\n  a.download = 'TWAIN-Export.html';\n  a.click();\n}\n\nfunction clearAll() {\n  if (!confirm('Clear all text and suggestions?')) return;\n  pushSnapshot();\n  document.getElementById('doc-area').innerHTML = '';\n  E.suggestions = [];\n  renderSuggestions();\n  clearHighlights();\n  updateWordCount();\n  setScore('score-voice','bar-voice',0,80,60);\n  setScore('score-active','bar-active',0,65,50,'%');\n  setScore('score-clarity','bar-clarity',0,70,50);\n  document.getElementById('sp-count').textContent = '0 issues';\n}\n\nfunction hideTooltip() {\n  var t = document.getElementById('sugg-tooltip');\n  if (t) t.style.display = 'none';\n}\n\n\/\/ Keyboard shortcuts\ndocument.addEventListener('keydown', function(e) {\n  if (e.key === 'Escape') { closeUpgradeModal(); hideTooltip(); }\n  if ((e.ctrlKey || e.metaKey) && e.key === 'z' && !e.shiftKey) {\n    \/\/ Only intercept if not focused in doc-area (let browser handle native undo there)\n    if (document.activeElement !== document.getElementById('doc-area')) {\n      e.preventDefault(); undoLast();\n    }\n  }\n});\n\n\/\/ ================================================================\n\/\/ MODAL\n\/\/ ================================================================\nfunction openUpgradeModal()  { document.getElementById('upgrade-modal').style.display = 'flex'; }\nfunction closeUpgradeModal() { document.getElementById('upgrade-modal').style.display = 'none'; }\nfunction dismissBanner()     { document.getElementById('lm-banner').style.display = 'none'; }\ndocument.addEventListener('click', function(e) {\n  if (e.target === document.getElementById('upgrade-modal')) closeUpgradeModal();\n});\n\n\/\/ ================================================================\n\/\/ MANUSCRIPT UPLOAD\n\/\/ ================================================================\nfunction triggerUpload() {\n  document.getElementById('manuscript-upload').click();\n}\n\nfunction handleManuscriptUpload(event) {\n  var file = event.target.files[0];\n  if (!file) return;\n  var ext = file.name.split('.').pop().toLowerCase();\n  var reader = new FileReader();\n\n  if (ext === 'txt' || ext === 'md') {\n    reader.onload = function(e) {\n      pushSnapshot();\n      var text = e.target.result;\n      \/\/ Convert plain text to paragraphs\n      var html = text.split(\/\\n\\n+\/).map(function(p) {\n        return '<p>' + escHtml(p.replace(\/\\n\/g, ' ').trim()) + '<\/p>';\n      }).filter(function(p) { return p !== '<p><\/p>'; }).join('');\n      document.getElementById('doc-area').innerHTML = html || '<p>' + escHtml(text) + '<\/p>';\n      updateWordCount();\n      clearHighlights();\n      showUploadSuccess(file.name);\n    };\n    reader.readAsText(file);\n\n  } else if (ext === 'html' || ext === 'htm') {\n    reader.onload = function(e) {\n      pushSnapshot();\n      var parser = new DOMParser();\n      var doc = parser.parseFromString(e.target.result, 'text\/html');\n      \/\/ Extract body content only, strip scripts\/styles\n      var body = doc.body;\n      body.querySelectorAll('script,style,nav,header,footer').forEach(function(el) { el.remove(); });\n      document.getElementById('doc-area').innerHTML = body.innerHTML;\n      updateWordCount();\n      clearHighlights();\n      showUploadSuccess(file.name);\n    };\n    reader.readAsText(file);\n\n  } else if (ext === 'docx') {\n    reader.onload = function(e) {\n      \/\/ Use mammoth.js if available, otherwise extract raw XML text\n      if (typeof mammoth !== 'undefined') {\n        mammoth.convertToHtml({ arrayBuffer: e.target.result }).then(function(result) {\n          pushSnapshot();\n          document.getElementById('doc-area').innerHTML = result.value;\n          updateWordCount();\n          clearHighlights();\n          showUploadSuccess(file.name);\n        });\n      } else {\n        \/\/ Fallback \u2014 extract raw text from docx zip\n        extractDocxText(e.target.result, file.name);\n      }\n    };\n    reader.readAsArrayBuffer(file);\n\n  } else {\n    alert('Unsupported file type. Please upload .txt, .md, .html, or .docx files.');\n  }\n\n  \/\/ Reset input so same file can be re-uploaded\n  event.target.value = '';\n}\n\nfunction extractDocxText(buffer, filename) {\n  \/\/ Basic docx text extraction without mammoth\n  \/\/ docx is a zip \u2014 try to find word\/document.xml\n  try {\n    var bytes = new Uint8Array(buffer);\n    var str = '';\n    for (var i = 0; i < bytes.length; i++) {\n      if (bytes[i] > 31 && bytes[i] < 127) str += String.fromCharCode(bytes[i]);\n    }\n    \/\/ Extract text between XML tags\n    var text = str.replace(\/<w:t[^>]*>\/g, ' ').replace(\/<[^>]+>\/g, '').replace(\/\\s+\/g, ' ').trim();\n    if (text.length > 50) {\n      pushSnapshot();\n      var html = '<p>' + text.split(\/\\.  +\/).map(function(s) { return escHtml(s.trim()); }).join('.<\/p><p>') + '<\/p>';\n      document.getElementById('doc-area').innerHTML = html;\n      updateWordCount();\n      clearHighlights();\n      showUploadSuccess(filename);\n    } else {\n      showUploadError('Could not read .docx content. Try saving as .txt first.');\n    }\n  } catch(e) {\n    showUploadError('Error reading .docx file. Try saving as .txt first.');\n  }\n}\n\nfunction showUploadSuccess(filename) {\n  setLight('on', 'Loaded');\n  \/\/ Show a brief toast notification\n  var toast = document.createElement('div');\n  toast.style.cssText = 'position:fixed;bottom:24px;left:50%;transform:translateX(-50%);background:var(--green-dark);color:#fff;font-family:Oswald,sans-serif;font-size:11px;letter-spacing:.1em;text-transform:uppercase;padding:10px 20px;border-radius:3px;z-index:9999;box-shadow:0 4px 16px rgba(0,0,0,0.4);animation:fadeIn .2s ease;';\n  toast.textContent = '\u2713 Loaded: ' + filename;\n  document.body.appendChild(toast);\n  setTimeout(function() { toast.remove(); }, 3000);\n}\n\nfunction showUploadError(msg) {\n  var toast = document.createElement('div');\n  toast.style.cssText = 'position:fixed;bottom:24px;left:50%;transform:translateX(-50%);background:#8b0000;color:#fff;font-family:Oswald,sans-serif;font-size:11px;letter-spacing:.1em;padding:10px 20px;border-radius:3px;z-index:9999;box-shadow:0 4px 16px rgba(0,0,0,0.4);';\n  toast.textContent = '\u2717 ' + msg;\n  document.body.appendChild(toast);\n  setTimeout(function() { toast.remove(); }, 4000);\n}\n\n\/\/ DRAG AND DROP support\nvar docWrap = document.querySelector('.doc-wrap');\nvar dropOverlay = document.getElementById('drop-overlay');\n\nif (docWrap && dropOverlay) {\n  docWrap.addEventListener('dragover', function(e) {\n    e.preventDefault();\n    dropOverlay.style.display = 'flex';\n  });\n  docWrap.addEventListener('dragleave', function(e) {\n    if (!docWrap.contains(e.relatedTarget)) {\n      dropOverlay.style.display = 'none';\n    }\n  });\n  docWrap.addEventListener('drop', function(e) {\n    e.preventDefault();\n    dropOverlay.style.display = 'none';\n    var file = e.dataTransfer.files[0];\n    if (file) {\n      handleManuscriptUpload({ target: { files: [file], value: '' } });\n    }\n  });\n}\n\n\/\/ ================================================================\n\/\/ ENGINE LIGHT\n\/\/ ================================================================\nfunction setLight(state, name) {\n  document.getElementById('eng-light').className = 'eng-light ' + state;\n  document.getElementById('eng-name').textContent = name || '';\n  document.getElementById('eng-status').textContent = state === 'busy' ? 'Processing...' : state === 'error' ? 'Check key' : 'Ready';\n}\n<\/script>\n<\/body>\n<\/html>\n","protected":false},"excerpt":{"rendered":"<p>T.W.A.I.N. Editor \u2014 Voice-Aware Writing Assistant \u00b7 Rebellion Command Rebellion Command \u00b7 T.W.A.I.N. Editor \u2014 Free Tool Words 0 Issues 0 Voice Not set Clear Export Clean Upgrade to Genesis T.W.A.I.N. Editor is free. Upgrade to Genesis to keep your Voice Profile permanently, unlock the Forge, The Pulse, and the full Sovereign Writing Cockpit. Get [&hellip;]<\/p>\n","protected":false},"author":1,"featured_media":0,"parent":0,"menu_order":0,"comment_status":"closed","ping_status":"closed","template":"","meta":{"_themeisle_gutenberg_block_has_review":false,"footnotes":""},"class_list":["post-24","page","type-page","status-publish","hentry"],"_links":{"self":[{"href":"https:\/\/twainstudio.dev\/index.php?rest_route=\/wp\/v2\/pages\/24","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/twainstudio.dev\/index.php?rest_route=\/wp\/v2\/pages"}],"about":[{"href":"https:\/\/twainstudio.dev\/index.php?rest_route=\/wp\/v2\/types\/page"}],"author":[{"embeddable":true,"href":"https:\/\/twainstudio.dev\/index.php?rest_route=\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/twainstudio.dev\/index.php?rest_route=%2Fwp%2Fv2%2Fcomments&post=24"}],"version-history":[{"count":2,"href":"https:\/\/twainstudio.dev\/index.php?rest_route=\/wp\/v2\/pages\/24\/revisions"}],"predecessor-version":[{"id":26,"href":"https:\/\/twainstudio.dev\/index.php?rest_route=\/wp\/v2\/pages\/24\/revisions\/26"}],"wp:attachment":[{"href":"https:\/\/twainstudio.dev\/index.php?rest_route=%2Fwp%2Fv2%2Fmedia&parent=24"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}