// DoView Board Engine V1.4.4 // Public release: 2026-08-25 // Runtime engine for standalone DoView Boards. // Generated boards are active HTML/JavaScript files. Treat them like executable web content. // See CHANGELOG.md for release history and security notes. let EMBEDDED_STATE = null; // engine-wide read-only flag. When a board is loaded from an embedded state that carries isReadonly:true (created via the Create Read-Only Copy function), IS_READONLY is set in init() and editing UI is disabled. This is a separate distributed copy for sharing an agreed board; the normal editable board is unaffected. let IS_READONLY = false; const _ENGINE_SRC = (typeof document !== 'undefined' && document.currentScript) ? document.currentScript.textContent : ''; let _doviewInitCalled = false; const DOVIEW_JSON_FORMAT = 'doview-board-json'; const DOVIEW_SCHEMA_VERSION = 'V1.3.9'; const DOVIEW_ENGINE_VERSION = 'V1.4.4'; const DoView = (function() { function sanitizeAISettingsForStorage(settings) { settings = settings || {}; return { endpoint: settings.endpoint || '', model: settings.model || 'gpt-4o' }; } function cleanupStoredAIKey() { try { const s = localStorage.getItem('doview_ai_settings'); if (!s) return {}; const parsed = JSON.parse(s) || {}; const clean = sanitizeAISettingsForStorage(parsed); localStorage.setItem('doview_ai_settings', JSON.stringify(clean)); return clean; } catch (e) { try { localStorage.removeItem('doview_ai_settings'); } catch (e2) {} return {}; } } const CSS = ` :root{--orange:#F5A623;--od:#d4880a;--text:#1e293b;--text2:#64748b;--bg:#f8fafc;--bdr:#e2e8f0;--white:#fff;--board-scale:1.35;--popup-scale:1.25} *{box-sizing:border-box;margin:0;padding:0} body{font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Helvetica,sans-serif;background:var(--bg);color:var(--text);font-size:14px;display:flex;flex-direction:column;min-height:100vh} .hdr{background:var(--orange);padding:13px 28px 12px;display:flex;flex-direction:column;gap:8px;flex-shrink:0} .hdr-top{display:flex;justify-content:space-between;align-items:flex-start;gap:18px;width:100%} .hdr-nav-line{display:flex;align-items:center;gap:16px;width:100%;flex-wrap:wrap} .hdr-left{display:flex;align-items:center;gap:18px;flex-wrap:wrap} /* the title is wrapped in a small column container so the optional Read-Only Copy label can sit on its own line directly under the title without disturbing the existing flex layout of the top utility links to the right. align-self:center keeps the wrap visually centered with the row of utility links. */ .hdr-title-wrap{display:flex;flex-direction:column;align-items:flex-start;align-self:center} .hdr-title{color:#fff;font-weight:700;font-size:19px;line-height:1.2} .hdr-binfo{color:rgba(255,255,255,0.9);font-size:14px;cursor:pointer;text-decoration:underline;text-underline-offset:2px;white-space:nowrap} .hdr-binfo:hover{color:#fff} /* the new Get training top utility link is rendered as an element with class .hdr-binfo so it shares the look of the other top utility links. The browser-default :visited color for anchors would otherwise turn it purple after the user has visited the target page; this rule keeps anchors with class .hdr-binfo the same color in all link states so the top utility links stay visually consistent. */ a.hdr-binfo,a.hdr-binfo:visited,a.hdr-binfo:active,a.hdr-binfo:link{color:rgba(255,255,255,0.9)} a.hdr-binfo:hover{color:#fff} .hdr-sep{width:1px;height:18px;background:rgba(255,255,255,0.55);display:inline-block;flex:0 0 auto} .hdr-right{display:flex;align-items:center;justify-content:flex-end;gap:10px;min-width:0;flex-wrap:wrap} .hdr-brand-line{display:flex;align-items:baseline;gap:7px;white-space:nowrap} .hdr-mode-badge-row{display:flex;align-items:center;justify-content:flex-end;gap:8px;flex-wrap:wrap;max-width:100%} .hdr-presentation-pill{display:inline-flex;align-items:center;justify-content:center;background:rgba(255,255,255,0.18);color:#fff;border:1.5px solid rgba(255,255,255,0.88);border-radius:999px;padding:6px 12px;font-size:12px;font-weight:700;cursor:pointer;white-space:nowrap;line-height:1.2} .hdr-presentation-pill:hover{background:rgba(255,255,255,0.28)} .hdr-tagline{color:#fff;font-size:13px;font-weight:600;letter-spacing:2px;text-transform:uppercase;opacity:0.88} .hdr-version{color:rgba(255,255,255,0.58);font-size:10.5px;font-weight:500;letter-spacing:.3px;text-transform:none} .hdr-latest,.hdr-latest:visited,.hdr-latest:active,.hdr-latest:link{color:rgba(255,255,255,0.66);font-size:10.5px;font-weight:500;letter-spacing:.3px;text-transform:none;text-decoration:underline;text-underline-offset:2px} .hdr-latest:hover{color:#fff} .hdr-brand{color:rgba(255,255,255,0.7);font-size:12px} .page-info-bar{padding:8px 22px;font-size:14px;color:var(--text2);background:#fff;border-bottom:1px solid var(--bdr);flex-shrink:0} .page-info-bar span{font-weight:600;color:var(--text)} .pi-link{color:var(--text2);cursor:pointer;font-size:13.5px;font-weight:500} .pi-link:hover{color:var(--text)} .pi-move{color:var(--text2);cursor:pointer;font-size:17px;text-decoration:none;margin-right:4px;vertical-align:middle} .pi-move:hover{color:var(--text)} .pi-move.pi-disabled{opacity:0.3;cursor:default} .pi-move.pi-disabled:hover{color:var(--text2)} .pi-undo{color:var(--text2);cursor:pointer;font-size:15.5px;font-weight:500} .pi-undo:hover{color:var(--text)} .pi-undo.pi-disabled{opacity:0.3;cursor:default} .info-modal-ta{width:100%;border:1px solid var(--bdr);border-radius:7px;padding:11px;font-size:14px;font-family:inherit;resize:vertical;min-height:200px;background:#f8fafc;line-height:1.6} .info-modal-ta:focus{outline:none;border-color:var(--orange)} .info-helper{font-size:13px;color:var(--text2);margin-bottom:12px;line-height:1.55;font-style:italic} .linklike-btn{border:0;background:transparent;color:var(--od);text-decoration:underline;text-underline-offset:2px;cursor:pointer;font:inherit;padding:0}.linklike-btn:hover{color:#7c4700} .tab-bar{display:none;background:#fff;border-bottom:1px solid var(--bdr);overflow-x:auto;white-space:nowrap;flex-shrink:0;scrollbar-width:auto;scrollbar-color:#94a3b8 #f1f5f9} /* top page-tab scrollbar made noticeably thicker and easier to click/drag. */ .tab-bar::-webkit-scrollbar{height:14px}.tab-bar::-webkit-scrollbar-track{background:#f1f5f9;border-radius:7px}.tab-bar::-webkit-scrollbar-thumb{background:#94a3b8;border-radius:7px;border:2px solid #f1f5f9;min-width:40px}.tab-bar::-webkit-scrollbar-thumb:hover{background:#64748b} .tabs{display:inline-flex;padding:0 18px;gap:0} .tab{padding:11px 20px;font-size:14px;font-weight:500;cursor:pointer;border-bottom:3px solid transparent;color:var(--text2);white-space:nowrap;transition:color 0.15s,border-color 0.15s;user-select:none;display:inline-flex;align-items:center;line-height:1.3} .tab:hover{color:var(--text)} .tab.active{color:var(--text);font-weight:600} .main-wrap{flex:1;display:flex;flex-direction:column;overflow:hidden} .main{padding:20px;flex:1;overflow-y:auto;zoom:var(--board-scale);position:relative} .ov-final{border:1px solid var(--bdr);background:#fff;border-radius:12px;padding:18px 28px;max-width:460px;margin:0 auto 18px;cursor:pointer;text-align:center;transition:box-shadow 0.15s;position:relative} .ov-final:hover{box-shadow:0 4px 12px rgba(15,23,42,0.10)} .ov-final h3{color:var(--od);font-size:15px;font-weight:700;margin-bottom:5px} .ov-final p{color:var(--text2);font-size:11.5px} .ov-divider{border:none;border-top:1px solid var(--bdr);margin:10px 0 8px} .sp-grid{display:grid;grid-template-columns:repeat(3,1fr);gap:14px} .sp-grid.compact{grid-template-columns:repeat(auto-fill,minmax(150px,1fr));gap:8px;margin-top:8px} .ov-top-actions{display:flex;justify-content:center;align-items:center;gap:8px;margin:-2px 0 8px;flex-wrap:wrap} .ov-mode-toggle{display:flex;justify-content:center;gap:6px;margin:0;align-items:center} .ov-mode-btn,.ov-add-page-pill{padding:6px 14px;border-radius:20px;font-size:11.5px;font-weight:500;cursor:pointer;border:1px solid #cbd5e1;background:#f1f5f9;color:#1e293b;transition:background 0.15s;line-height:1.2} .ov-mode-btn{font-weight:500} .ov-mode-btn.active{background:#e2e8f0;color:#1e293b;border-color:#cbd5e1;font-weight:700} .ov-add-page-pill:hover,.ov-mode-btn:hover{background:#e2e8f0;color:#1e293b} .sp-tile{position:relative;border-radius:10px;padding:16px;min-height:92px;cursor:pointer;box-shadow:0 1px 2px rgba(0,0,0,0.06);transition:transform 0.15s} .sp-tile.with-ov-meta{padding:18px 16px 30px 42px;min-height:108px} .sp-tile.compact{border-radius:8px;padding:8px 9px 16px;min-height:44px} .sp-tile:hover{transform:translateY(-2px);box-shadow:0 4px 12px rgba(0,0,0,0.1)} .sp-tile.compact:hover{transform:translateY(-1px);box-shadow:0 2px 8px rgba(0,0,0,0.08)} .sp-tile h4{font-size:13px;line-height:1.28;margin-bottom:6px;color:var(--text);font-weight:700} .sp-tile.compact h4{font-size:10.5px;line-height:1.25;margin-bottom:2px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;font-weight:600} .sp-tile.compact .ov-compact-meta{display:flex;align-items:center;justify-content:space-between;gap:8px;margin-top:4px;font-size:10px;color:var(--text2);min-width:0} .ov-compact-badge{font-weight:700;white-space:nowrap} .ov-compact-level{margin-left:auto;text-align:right;white-space:nowrap;font-weight:600;color:#334155} .sp-tile p{font-size:11px;color:var(--text2);line-height:1.35} .ov-card-tags{display:block;margin-top:6px;font-size:11px;line-height:1.3;color:var(--text2);cursor:pointer;min-height:15px} .ov-card-tags:hover{text-decoration:underline;text-underline-offset:2px} body.is-readonly .ov-card-tags{cursor:default;text-decoration:none} .ov-card-tags .doview-tag{font-size:10px;margin-left:0;margin-right:5px;color:#475569;opacity:.88} .ov-card-tag-edit{display:inline-flex;align-items:center;border:1px dashed #cbd5e1;border-radius:999px;padding:2px 7px;font-size:10px;font-weight:700;color:#64748b;background:rgba(255,255,255,.55)} .ov-card-color-btn{position:absolute;left:9px;bottom:8px;z-index:4} .ov-tag-popover{min-width:230px;max-width:280px} .ov-tag-row{display:flex;align-items:center;gap:5px;flex-wrap:wrap;margin-bottom:7px;font-size:11px;color:var(--text2)} .ov-tag-actions{display:flex;gap:5px;align-items:center;flex-wrap:wrap} .sp-tile-plus{border:1px solid #cbd5e1;background:#f1f5f9;border-radius:20px;min-height:0;display:flex;align-items:center;justify-content:center;font-size:11.5px;font-weight:500;color:#1e293b;cursor:pointer;transition:background 0.15s;padding:8px 14px} .sp-tile-plus.compact{min-height:0;border-radius:20px;font-size:11.5px;padding:7px 12px} .sp-tile-plus:hover{background:#e2e8f0;color:#1e293b} .ov-reorder-btn{position:absolute;left:50%;transform:translateX(-50%);bottom:8px;border:1px solid #cbd5e1;background:#f8fafc;color:#1e293b;border-radius:999px;padding:3px 7px;font-size:10.5px;font-weight:700;cursor:default;line-height:1.1;display:inline-flex;align-items:center;gap:4px;white-space:nowrap} .ov-reorder-btn:hover{background:#f8fafc} .ov-reorder-arrow{border:0;background:transparent;color:#1e293b;font-size:12px;font-weight:800;line-height:1;cursor:pointer;padding:1px 2px}.ov-reorder-arrow:hover{color:#0f172a}.ov-reorder-arrow.ov-reorder-disabled{opacity:.28;cursor:default;pointer-events:none} .sp-tile.compact .ov-reorder-btn{bottom:5px;padding:2px 6px;font-size:10px} .drill-tri{position:absolute;bottom:8px;right:8px;width:0;height:0;border-left:6px solid transparent;border-right:6px solid transparent;border-top:8px solid #F5A623;opacity:0.7} .ov-hint{text-align:center;color:var(--text2);font-size:11px;margin-top:18px;font-style:italic} .ov-final-clean{background:#fff!important;border:1px solid var(--bdr)!important;box-shadow:0 1px 2px rgba(15,23,42,.08)!important}.ov-final-clean h3,.ov-final-clean p{color:#F5A623!important} .back-btn{display:inline-flex;align-items:center;gap:5px;background:none;border:1px solid var(--bdr);border-radius:6px;padding:6px 13px;cursor:pointer;font-size:11.5px;color:var(--text2);margin-bottom:16px;transition:background 0.15s} .back-btn:hover{background:var(--bdr)} .board-scroll{overflow-x:auto;padding-bottom:4px} .ttw{display:flex;align-items:stretch;min-width:max-content;gap:0} .sidebar{width:26px;flex-shrink:0;background:var(--orange);border:1px solid var(--od);display:flex;align-items:center;justify-content:center;border-radius:4px} .sidebar span{writing-mode:vertical-rl;transform:rotate(180deg);color:#fff;font-weight:700;font-size:10px;letter-spacing:2px;text-transform:uppercase} .cols-row{display:flex;align-items:center;flex:1;padding:0 24px} .col-wrap{display:flex;flex-direction:column;flex-shrink:0;align-self:flex-start} .col-h{font-size:10px;font-weight:600;text-transform:uppercase;color:var(--text2);letter-spacing:0.5px;margin-bottom:8px;text-align:center;min-height:28px;display:flex;align-items:flex-start;justify-content:center;padding:0 4px;width:174px} .col-h.last-col{} .col-boxes{display:flex;flex-direction:column;gap:12px;align-items:stretch} .col-arrow{width:24px;flex-shrink:0;display:flex;justify-content:center} .box{border-radius:7px;padding:8px 25px 8px 36px;cursor:pointer;position:relative;width:174px;min-height:68px;border:1px solid;transition:box-shadow 0.15s,opacity 0.2s;overflow:visible} .box:hover{box-shadow:0 2px 8px rgba(0,0,0,0.12)} .box.last-col{font-weight:700} .box.grey-state{} .box-txt{font-size:12px;line-height:1.45;color:var(--text);display:-webkit-box;-webkit-line-clamp:5;-webkit-box-orient:vertical;overflow:hidden} .box.last-col .box-txt{} /* on-box traffic-light dot is clickable to open a small chooser. */ .tl-dot{position:absolute;top:6px;right:6px;width:14px;height:14px;border-radius:50%;cursor:pointer;z-index:3} .tl-dot.tl-ring{box-shadow:0 0 0 1.5px #FFFFFF} .tl-dot.tl-ring:hover{box-shadow:0 0 0 1.5px #FFFFFF,0 0 0 3.5px rgba(0,0,0,0.18)} .tl-dot:not(.tl-ring):hover{box-shadow:0 0 0 2px rgba(0,0,0,0.18)} .tl-empty{background:#fff!important;border:1px dashed #cbd5e1!important;box-shadow:none!important;opacity:.78} .tl-hidden{background:transparent!important;border-color:transparent!important;box-shadow:none!important;opacity:0} .tl-empty:hover{border-color:#94a3b8!important;background:#f8fafc!important;box-shadow:0 1px 4px rgba(15,23,42,0.14)!important} /* on-box "edit" label removed. Class kept hidden for backward compatibility. */ .edit-lbl{display:none} /* on-box priority badge is clickable to open a small chooser. */ /* priority badge neutral intensity, placeholder, and chooser contrast. */ .pri-badge{position:absolute;top:4px;left:4px;width:22px;height:18px;border-radius:999px;display:flex;align-items:center;justify-content:center;font-size:10.5px;font-weight:700;line-height:1;letter-spacing:.1px;box-sizing:border-box;background:#f8fafc;color:#334155;border:1px solid #cbd5e1;cursor:pointer} .pri-badge.pri-a{background:#0f172a;color:#fff;border-color:#0f172a} .pri-badge.pri-b{background:#334155;color:#fff;border-color:#334155} .pri-badge.pri-c{background:#64748b;color:#fff;border-color:#64748b} .pri-badge.pri-d{background:#cbd5e1;color:#1e293b;border-color:#cbd5e1} .pri-badge.pri-e{background:#e2e8f0;color:#334155;border-color:#cbd5e1} .pri-badge.pri-bau{width:30px;background:#f8fafc;color:#334155;border-color:#94a3b8;font-size:9.5px} .pri-badge.pri-empty{background:#fff;color:transparent;border:1px dashed #cbd5e1;opacity:.78} .pri-badge.pri-hidden{background:transparent;color:transparent;border-color:transparent;opacity:0} .tt-cnt-hotspot{width:20px;height:22px} .how-box{padding-right:30px} .how-box .box-txt{padding-right:6px} .pri-badge:hover{border-color:#94a3b8} .pri-opt{min-width:30px;height:24px;border-radius:999px;border:1px solid #cbd5e1;background:#f8fafc;font-weight:700;cursor:pointer;line-height:1;color:#334155} .pri-opt.pri-a{background:#0f172a;color:#fff;border-color:#0f172a}.pri-opt.pri-b{background:#334155;color:#fff;border-color:#334155}.pri-opt.pri-c{background:#64748b;color:#fff;border-color:#64748b}.pri-opt.pri-d{background:#cbd5e1;color:#1e293b;border-color:#cbd5e1}.pri-opt.pri-e{background:#e2e8f0;color:#334155;border-color:#cbd5e1}.pri-opt.pri-bau{background:#f8fafc;color:#334155;border-color:#94a3b8}.pri-opt-clear{min-width:44px;color:#334155;background:#fff}.pri-opt.opt-active{outline:2px solid #94a3b8;outline-offset:1px} /* lightweight on-box chooser popup for traffic light and priority. */ .box-chooser{position:absolute;background:#fff;border:1px solid #cbd5e1;border-radius:8px;box-shadow:0 6px 18px rgba(0,0,0,0.18);padding:8px 10px;z-index:1000;display:flex;flex-direction:column;gap:6px;min-width:128px} .box-chooser-hdr{font-size:10.5px;font-weight:600;color:var(--text2);text-transform:uppercase;letter-spacing:0.4px;margin-bottom:2px} .box-chooser-row{display:flex;gap:6px;flex-wrap:wrap;align-items:center} .box-chooser .tl-opt{width:18px;height:18px;box-sizing:border-box;border-radius:50%;cursor:pointer;border:0;box-shadow:0 0 0 1.5px #FFFFFF;transition:transform 0.1s;flex-shrink:0} .box-chooser .tl-opt.opt-active{transform:scale(1.12)} .box-chooser .pri-opt{min-width:30px;height:24px;border-radius:5px;border:1px solid var(--bdr);background:#fff;cursor:pointer;font-size:10.5px;font-weight:700;padding:0 5px;transition:border-color 0.1s} .box-chooser .pri-opt.opt-active{border:2px solid var(--text)} .box-chooser .pri-opt-clear{font-size:10px;font-weight:500;color:var(--text2);min-width:auto;padding:0 7px} .box-chooser .pri-opt.pri-a{background:#0f172a;color:#fff;border-color:#0f172a}.box-chooser .pri-opt.pri-b{background:#334155;color:#fff;border-color:#334155}.box-chooser .pri-opt.pri-c{background:#64748b;color:#fff;border-color:#64748b}.box-chooser .pri-opt.pri-d{background:#cbd5e1;color:#1e293b;border-color:#cbd5e1}.box-chooser .pri-opt.pri-e{background:#e2e8f0;color:#334155;border-color:#cbd5e1}.box-chooser .pri-opt.pri-bau{background:#f8fafc;color:#334155;border-color:#94a3b8}.box-chooser .pri-opt.pri-opt-clear{background:#fff;color:#334155} .box-drill{position:absolute;bottom:3px;right:3px;width:0;height:0;border-left:5px solid transparent;border-right:5px solid transparent;border-top:7px solid #F5A623;opacity:0.7} .box-jump{position:absolute;bottom:4px;right:4px;width:14px;height:14px;background:#fef3c7;border:1px solid #fde68a;border-radius:3px;display:flex;align-items:center;justify-content:center;cursor:pointer;z-index:6} .box-jump::after{content:'';width:0;height:0;border-left:5px solid #F5A623;border-top:3px solid transparent;border-bottom:3px solid transparent} .box-jump-tri{position:absolute;bottom:3px;right:3px;width:0;height:0;border-left:6px solid transparent;border-right:6px solid transparent;border-top:8px solid #F5A623;opacity:0.85;cursor:pointer;z-index:6} .box-jump-tri:hover{opacity:1} .fo-list{max-width:580px;margin:0 auto;display:flex;flex-direction:column;gap:10px} .fo-actions-row .tt-box-actions-wrap{flex:0 0 auto;margin-left:0} .fo-list .under-box-wrap{width:100%} .fo-list .under-box-block{width:100%} .fo-box{border-radius:8px;padding:13px 29px 13px 40px;cursor:pointer;position:relative;background:#fff;border:1px solid #d1d5db;transition:box-shadow 0.15s} .fo-box:hover{box-shadow:0 2px 10px rgba(0,0,0,0.1)} .fo-box .box-txt{font-weight:700;color:#1e293b;font-size:12px} .ep{background:#fff;border:1px solid var(--bdr);border-radius:12px;padding:15px 18px;margin:0 0 12px} .ep-hdr{display:flex;justify-content:space-between;align-items:flex-start;margin-bottom:10px} .ep-title{font-weight:600;font-size:12.5px;max-width:80%;line-height:1.4} .ep-close{background:none;border:none;cursor:pointer;font-size:17px;color:var(--text2);padding:0 4px;line-height:1;flex-shrink:0} .ep-entries{margin-bottom:10px;font-size:12px;line-height:1.6} .ep-row{display:flex;align-items:flex-start;gap:5px;margin-bottom:3px;flex-wrap:wrap} .ep-lbl{font-weight:600;color:var(--text2);flex-shrink:0;font-size:11px} .ep-lbl.note-lbl{color:#6366f1} .ep-txt{min-width:0} .ep-txt a{color:var(--od);word-break:break-all}.ep-txt a:hover{text-decoration:underline} .ep-edit{background:none;border:none;cursor:pointer;font-size:12px;color:var(--text2);padding:0 3px;flex-shrink:0} .ep-controls{display:flex;align-items:center;gap:10px;margin-bottom:10px;flex-wrap:wrap} .ep-cl{font-size:10.5px;color:var(--text2)} .tl-dots-row{display:flex;gap:7px} .tl-dot-btn{width:18px;height:18px;box-sizing:border-box;border-radius:50%;cursor:pointer;border:0;box-shadow:0 0 0 1.5px #FFFFFF;transition:transform 0.1s;flex-shrink:0} .tl-dot-btn.dot-active{transform:scale(1.12)} /* two intermediate traffic-light states rendered as vertically split circles. greenYellow = left half green, right half yellow; yellowRed = left half yellow, right half red. Used everywhere a single-color fill would otherwise be set inline (the on-box .tl-dot, the details-pane .tl-dot-btn picker, and the on-box .tl-opt chooser). The split renders as a half-width linear gradient with hard stops at 50%, so the dot still reads as one circle at small sizes. The class is applied via a small switch in the rendering helpers; existing bare 'green'/'yellow'/'red'/'grey' values remain rendered as solid fills via their inline backgrounds. */ .tl-split-gy{background:linear-gradient(90deg,#2ECC71 0 50%,#F5A623 50% 100%)!important} .tl-split-yr{background:linear-gradient(90deg,#F5A623 0 50%,#E74C3C 50% 100%)!important} /* compact preset row for the per-box Border color control in the details pane. Sits next to Status (traffic light) and Priority. Each swatch is a small circle with the preset color as its background plus a thin neutral outline; the Default option uses a transparent fill with a dashed neutral outline so it reads clearly as "no custom border". The active preset gets a stronger inner border so it stands out without changing layout. Read-only mode hides the whole .ep-controls row's editing affordances via existing rules; see body.is-readonly .ep-struct{display:none} which already hides the entry panel structural area, and the per-box border color stays visible because it is rendered on the box itself. */ .bc-row{display:flex;gap:6px;flex-wrap:wrap} .bc-btn{width:18px;height:18px;border-radius:50%;cursor:pointer;border:1.5px solid var(--bdr);transition:transform 0.1s,border-color 0.1s;flex-shrink:0;padding:0;background-clip:padding-box} .bc-btn:hover{transform:scale(1.1)} .bc-btn.bc-default{background:#fff;border:1.5px dashed var(--text2)} .bc-btn.bc-active{border:2px solid #64748b;box-shadow:0 0 0 2px rgba(100,116,139,.18);transform:scale(1.08)} /* small read-only display block placed above editable textareas in the link / measure / EQ / board info / page info popups. Renders the saved text via linkify() so URLs become clickable in display/view mode. Shown only when the saved field has content. The textarea below remains the edit surface; this block does not replace it. Styling is intentionally subtle — a softly-bordered block with a slightly recessed background — so users can tell at a glance which is the live (clickable) display and which is the (editable) textarea. */ .meq-detail-display{font-size:11.5px;line-height:1.5;color:var(--text);background:#f8fafc;border:1px solid var(--bdr);border-radius:6px;padding:6px 9px;margin-bottom:5px;white-space:pre-wrap;word-break:break-word} .meq-detail-display a{color:var(--od);word-break:break-all} .meq-detail-display-label{font-size:9.5px;font-weight:600;color:var(--text2);text-transform:uppercase;letter-spacing:0.5px;margin-bottom:3px} .pri-row{display:flex;gap:5px;flex-wrap:wrap} .pri-btn{width:32px;height:25px;border-radius:5px;border:1px solid var(--bdr);background:none;cursor:pointer;font-size:10.5px;font-weight:700;transition:border-color 0.1s;color:#334155} .pri-btn.pri-a{background:#0f172a;color:#fff;border-color:#0f172a}.pri-btn.pri-b{background:#334155;color:#fff;border-color:#334155}.pri-btn.pri-c{background:#64748b;color:#fff;border-color:#64748b}.pri-btn.pri-d{background:#cbd5e1;color:#1e293b;border-color:#cbd5e1}.pri-btn.pri-e{background:#e2e8f0;color:#334155;border-color:#cbd5e1}.pri-btn.pri-bau{background:#f8fafc;color:#334155;border-color:#94a3b8} .pri-btn.pri-active{outline:2px solid var(--text);outline-offset:1px} .ep-add{display:flex;gap:7px;margin-bottom:8px;align-items:center;flex-wrap:wrap} .pill{padding:5px 11px;border-radius:20px;border:1px solid var(--bdr);background:none;cursor:pointer;font-size:11px;font-weight:500;transition:all 0.15s} .pill.pill-sf{background:#dbeafe;border-color:#93c5fd;color:#1e40af} .pill.pill-nt{background:#ede9fe;border-color:#a5b4fc;color:#4338ca} .pill.pill-ai{background:#fff8ed;border-color:var(--orange);color:var(--od)} .ep-ta{width:100%;border:1px solid var(--bdr);border-radius:7px;padding:7px 10px;font-size:12px;font-family:inherit;resize:vertical;min-height:58px;transition:border-color 0.15s} .ep-ta:focus{outline:none;border-color:var(--orange)} .ep-acts{display:flex;gap:8px;margin-top:7px} .save-btn{background:var(--orange);color:#fff;border:none;border-radius:6px;padding:7px 18px;font-size:12px;font-weight:600;cursor:pointer;transition:background 0.15s} .save-btn:hover{background:var(--od)} .save-btn:disabled{opacity:0.55;cursor:not-allowed} .ctrl-bar{padding:8px 14px;background:#fff;border-top:1px solid var(--bdr);display:flex;align-items:center;gap:7px;flex-wrap:nowrap;overflow-x:auto;overflow-y:hidden;-webkit-overflow-scrolling:touch;scrollbar-width:thin;flex-shrink:0;position:sticky;bottom:0;z-index:100} .cbtn{padding:6px 12px;border-radius:20px;font-size:14px;line-height:1.15;min-height:34px;font-weight:500;cursor:pointer;border:1px solid;transition:all 0.15s;white-space:nowrap;flex:0 0 auto} .cbtn-or{background:var(--orange);color:#fff;border-color:var(--od)}.cbtn-or:hover{background:var(--od)} .cbtn-dk{background:#1e293b;color:#fff;border-color:#0f172a}.cbtn-dk:hover{background:#374151} .cbtn-ol{background:none;color:var(--text);border-color:var(--bdr)}.cbtn-ol:hover{background:var(--bg)} .cbtn-disabled,.cbtn:disabled{background:#e5e7eb!important;color:#64748b!important;border-color:#cbd5e1!important;cursor:not-allowed!important;box-shadow:none!important}.cbtn-disabled:hover,.cbtn:disabled:hover{background:#e5e7eb!important;color:#64748b!important} .status-bar{margin-left:auto;font-size:12.5px;color:var(--text2);white-space:nowrap;flex:0 0 auto} .mobile-chat-btn{display:none} .src-link{cursor:pointer;text-decoration:underline}.src-link:hover{color:var(--text)} .attr{text-align:center;padding:12px 22px;font-size:12.5px;color:var(--text2);border-top:1px solid var(--bdr);background:#fff;flex-shrink:0} .attr a{color:var(--od);text-decoration:none}.attr a:hover{text-decoration:underline} .attr-or{color:var(--orange)!important;cursor:pointer}.attr-or:hover{text-decoration:underline} .modal-ov{display:none;position:fixed;inset:0;background:rgba(0,0,0,0.5);backdrop-filter:blur(4px);z-index:2000;align-items:center;justify-content:center;padding:20px} .modal-ov.open{display:flex} .modal-box{background:#fff;border-radius:16px;padding:28px;max-width:660px;width:100%;max-height:82vh;overflow-y:auto} /* popup top heading reduced from 18px to 11px so it reads as only modestly larger than the popup body text rather than a visually dominant banner. The .modal-box h3 rule is the main popup heading rule; it applies at popup-scale (currently 1.75), so the final rendered heading is slightly smaller than body text in absolute size but no longer dominates the popup. Kept surgical — no other heading rules changed, no layout/spacing changed, margin-bottom preserved. */ .modal-box h3{font-size:11px;font-weight:700;margin-bottom:14px} .modal-close{background:var(--orange);color:#fff;border:none;border-radius:8px;padding:9px 22px;cursor:pointer;font-size:14px;font-weight:600;margin-top:16px} .save-workflow-intro{font-size:14px;line-height:1.65;color:var(--text2);margin-bottom:10px} .save-options{display:grid;grid-template-columns:1fr;gap:12px;margin-top:12px} .save-option{border:1px solid var(--bdr);border-radius:10px;background:#fff;padding:14px 15px} .save-option.primary{border-color:#fed7aa;background:#fffaf0} .save-option.disabled{opacity:.55;background:#f8fafc} .save-option h4{font-size:14px;font-weight:700;color:var(--text);margin-bottom:2px} .save-option-sub{font-size:12px;color:var(--text2);font-style:italic;margin-bottom:8px} .save-option p{font-size:13px;line-height:1.55;color:var(--text2);margin-bottom:8px} .save-option-actions{display:flex;align-items:center;gap:8px;flex-wrap:wrap;margin-top:8px} .save-option-actions .cbtn{font-size:13px;padding:7px 14px} .save-option-note{font-size:11.5px;color:var(--text2);line-height:1.45} .save-current-file{font-size:12.5px;color:var(--text);background:#f8fafc;border:1px solid #e2e8f0;border-radius:7px;padding:7px 9px;margin:8px 0 4px;word-break:break-word} .save-result{display:none;border:1px solid var(--bdr);border-radius:10px;background:#f8fafc;padding:14px 15px;margin-top:12px} .save-result h4{font-size:14px;font-weight:700;color:var(--text);margin:0 0 8px} .save-result p{font-size:13.5px;line-height:1.6;color:var(--text2);margin-bottom:8px} .save-result .save-result-main{color:var(--text)} .save-result .save-result-main strong{font-weight:700} .save-result-file{font-size:13px;color:var(--text);background:#fff;border:1px solid #e2e8f0;border-radius:7px;padding:7px 9px;margin-top:8px;word-break:break-word} .save-result-actions{display:flex;gap:8px;flex-wrap:wrap;margin-top:10px} .warn-txt{font-size:14px;color:var(--text2);background:#fef9c3;border:1px solid #fde68a;border-radius:6px;padding:10px 12px;margin-bottom:14px} .src-list{list-style:none;padding:0} .src-list li{padding:8px 0;border-bottom:1px solid var(--bdr);font-size:14px} .src-list li:last-child{border-bottom:none} .src-list a{color:var(--od);word-break:break-all} .upd-ta{width:100%;border:1px solid var(--bdr);border-radius:7px;padding:11px;font-size:12.5px;font-family:monospace;resize:vertical;height:260px;background:#f8fafc} .copy-btn{background:#1e293b;color:#fff;border:none;border-radius:8px;padding:9px 19px;cursor:pointer;font-size:14px;font-weight:600;margin-top:12px} .copy-btn.copied{background:#22c55e} .chat-fab{position:fixed;bottom:64px;right:clamp(14px,2vw,22px);background:linear-gradient(135deg,#F5A623,#e09000);color:#fff;font-weight:600;font-size:14px;padding:11px 18px;border-radius:24px;border:none;cursor:pointer;box-shadow:0 4px 14px rgba(245,166,35,0.4);z-index:1000;transition:transform 0.15s,box-shadow 0.15s} .chat-fab:hover{transform:scale(1.06);box-shadow:0 6px 18px rgba(245,166,35,0.5)} .chat-panel{position:fixed;bottom:154px;right:clamp(14px,2vw,22px);width:500px;max-width:calc(100vw - 44px);height:620px;max-height:calc(100vh - 165px);background:#fff;border:1px solid var(--bdr);border-radius:16px;box-shadow:0 8px 32px rgba(0,0,0,0.15);z-index:1001;display:none;flex-direction:column;overflow:hidden} .chat-panel.open{display:flex} .chat-hdr{background:linear-gradient(135deg,#F5A623,#e09000);padding:15px 17px;display:flex;justify-content:space-between;align-items:center} .chat-hdr-l h4{color:#fff;font-size:16.5px;font-weight:700} .chat-hdr-l small{color:rgba(255,255,255,0.85);font-size:12.5px} .chat-hdr-r{display:flex;gap:7px} .chat-hbtn{width:34px;height:34px;border-radius:50%;background:rgba(255,255,255,0.25);border:none;cursor:pointer;font-size:15px;display:flex;align-items:center;justify-content:center;color:#fff} .chat-msgs{flex:1;overflow-y:auto;padding:16px;display:flex;flex-direction:column;gap:10px} .chat-m{max-width:86%;padding:10px 13px;border-radius:11px;font-size:13.5px;line-height:1.55} .chat-m.user{align-self:flex-end;background:#fff8ed;border:1px solid #fde68a} .chat-m.asst{align-self:flex-start;background:#f1f5f9;border:1px solid #e2e8f0} .chat-m.sys{align-self:center;color:var(--text2);font-size:12px;font-style:italic} .chat-in-area{padding:13px;border-top:1px solid var(--bdr);display:flex;gap:8px} .chat-in{flex:1;border:1px solid var(--bdr);border-radius:8px;padding:8px 11px;font-size:13.5px;font-family:inherit;resize:none} .chat-in:focus{outline:none;border-color:var(--orange)} .chat-send{background:var(--orange);color:#fff;border:none;border-radius:8px;padding:8px 14px;cursor:pointer;font-size:14px;font-weight:600} .chat-settings-pane{flex:1;overflow-y:auto;padding:16px;display:none;flex-direction:column;gap:12px} .chat-settings-pane.open{display:flex} .set-fld{display:flex;flex-direction:column;gap:4px} .set-fld label{font-size:12.5px;font-weight:600;color:var(--text2)} .set-fld input{border:1px solid var(--bdr);border-radius:7px;padding:7px 10px;font-size:13.5px} .set-save{background:var(--orange);color:#fff;border:none;border-radius:8px;padding:8px 17px;cursor:pointer;font-size:14px;font-weight:600} .set-back{color:var(--orange);font-size:13px;cursor:pointer;text-decoration:underline} .disclaim{font-size:11.5px;color:var(--text2);background:#f1f5f9;border-radius:6px;padding:8px;line-height:1.55;font-style:italic} .help-txt{font-size:12.5px;color:var(--text2);line-height:1.55} @keyframes pulse{0%,100%{opacity:1}50%{opacity:0.35}} /* selected/current-box emphasis strengthened modestly so the eye is drawn to the selected box, not to secondary grey linked boxes. Thicker dark border + subtle halo + gentle lift via shadow. Kept conservative; distinction with inspect-home (3px #1e293b + stronger shadow) and reveal-active (2px #475569, no halo) is preserved. Grey linked / inspect-linked boxes remain modestly styled as before. */ .box.box-selected{border:2px solid #1e293b!important;box-shadow:0 0 0 2px rgba(30,41,59,0.10),0 2px 8px rgba(0,0,0,0.10)!important;z-index:3} .how-box.box-selected{border:2px solid #1e293b!important;box-shadow:0 0 0 2px rgba(30,41,59,0.10),0 2px 8px rgba(0,0,0,0.10)!important;z-index:3} .fo-box.box-selected{border:2px solid #1e293b!important;box-shadow:0 0 0 2px rgba(30,41,59,0.10),0 2px 8px rgba(0,0,0,0.10)!important;z-index:3} .box.reveal-out::before{content:'';position:absolute;left:-1px;top:50%;transform:translateY(-50%);width:0;height:0;border-top:7px solid transparent;border-bottom:7px solid transparent;border-left:8px solid #475569;z-index:2} .box.reveal-in::after{content:'';position:absolute;right:-9px;top:50%;transform:translateY(-50%);width:0;height:0;border-top:7px solid transparent;border-bottom:7px solid transparent;border-left:8px solid #475569;z-index:2} .box.reveal-out-neg::before,.box.reveal-in-neg::after{border-left-color:#b91c1c!important} .box.reveal-active{border:2px solid #475569!important} .box.reveal-out::before,.box.reveal-in::after{cursor:pointer} /* tooltip-bearing hover overlays for the small black reveal/link arrows. The arrows themselves are CSS pseudo-elements (::before / ::after) and cannot carry a title attribute, so these transparent overlay spans sit over the arrow area and carry the side-specific title tooltips. The overlays also cover the optional link traffic-light dot and remain outside the count-badge zone. */ .reveal-arrow-hover-out{position:absolute;left:-18px;top:50%;transform:translateY(-50%);width:30px;height:18px;z-index:4;cursor:pointer;background:transparent} .reveal-arrow-hover-in{position:absolute;right:-18px;top:50%;transform:translateY(-50%);width:30px;height:18px;z-index:4;cursor:pointer;background:transparent} .reveal-arrow-hover-out,.reveal-arrow-hover-in{overflow:visible} .link-tl-dot{position:absolute;top:50%;width:10px;height:10px;border-radius:50%;transform:translateY(-50%);box-shadow:0 0 0 1.5px #fff;z-index:10;pointer-events:auto} .reveal-arrow-hover-out .link-tl-dot{left:3px} .reveal-arrow-hover-in .link-tl-dot{left:-3px;right:auto} .link-hover-pop{display:none} .link-hover-portal{display:none;position:fixed;top:0;left:0;box-sizing:border-box;width:max-content;max-width:min(360px,calc(100vw - 16px));min-width:90px;padding:7px 10px;background:#fff;color:var(--text);border:1px solid var(--bdr);border-radius:6px;box-shadow:0 4px 14px rgba(15,23,42,0.16);font-size:12px;line-height:1.45;white-space:normal;overflow:hidden;z-index:3000;pointer-events:auto} .link-hover-portal.open{display:block} .link-hover-text{display:-webkit-box;-webkit-box-orient:vertical;-webkit-line-clamp:5;max-height:calc(1.45em * 5);overflow:hidden;white-space:pre-wrap;overflow-wrap:anywhere} .display-code-style{font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,'Liberation Mono',monospace;background:#f8fafc;border:1px solid #dbe3ee;border-radius:6px;white-space:pre-wrap;overflow-wrap:anywhere;word-break:normal} .link-hover-text.display-code-style{display:block;-webkit-line-clamp:unset;max-height:calc(1.45em * 8);padding:5px 6px;overflow:auto} .link-hover-tl-dot{position:static;display:inline-block;width:.83em;height:.83em;border-radius:50%;margin-right:.42em;vertical-align:-.08em;box-shadow:0 0 0 1.5px #fff} .how-box.reveal-highlight{border:2px solid #475569!important} .box.inspect-home{border:3px solid #1e293b!important;box-shadow:0 0 0 2px rgba(30,41,59,0.15)} .box.inspect-linked{border:2px solid #94a3b8!important;background:rgba(148,163,184,0.08)!important} .how-box.inspect-home{border:3px solid #1e293b!important;box-shadow:0 0 0 2px rgba(30,41,59,0.15)} .how-box.inspect-linked{border:2px solid #94a3b8!important;background:rgba(148,163,184,0.08)!important} .fo-box.inspect-home{border:3px solid #1e293b!important;box-shadow:0 0 0 2px rgba(30,41,59,0.15)} .fo-box.inspect-linked{border:2px solid #94a3b8!important;background:rgba(148,163,184,0.08)!important} .detail-box{width:174px;background:#fff;border:1px solid #d1d5db;border-radius:0 0 7px 7px;margin-top:-1px;padding:5px 7px;font-size:10.5px;line-height:1.4;color:var(--text);max-height:72px;overflow-y:auto;cursor:text;outline:none;word-break:break-word} .detail-box:focus{border-color:var(--orange)} .under-box-block-main.editable{cursor:pointer} .under-box-block-main.editable:hover{background:transparent;text-decoration:underline;text-underline-offset:1px} .under-box-edit-ta{width:100%;box-sizing:border-box;border:1px solid var(--bdr);border-radius:6px;padding:5px 7px;font-size:10.5px;line-height:1.4;font-family:inherit;resize:vertical;min-height:54px;background:#fff;color:var(--text)} .under-box-edit-ta:focus{outline:none;border-color:var(--orange)} .under-box-edit-actions{display:flex;gap:6px;margin-top:5px} .under-box-edit-actions button{font-size:10px;padding:3px 8px;border-radius:5px;border:1px solid var(--bdr);background:#fff;cursor:pointer} .under-box-edit-actions .save{background:var(--orange);border-color:var(--od);color:#fff} .box-display-preview{width:100%;box-sizing:border-box;border:1px solid var(--bdr);border-radius:7px;background:#f8fafc;padding:8px 10px;min-height:58px;max-height:128px;overflow:hidden;white-space:pre-wrap;word-break:break-word;font-size:11.5px;line-height:1.45;color:var(--text)} .box-display-preview.display-code-style{max-height:160px;overflow:auto;word-break:normal;overflow-wrap:anywhere} .box-display-preview.editable{cursor:pointer} .box-display-preview.editable:hover{border-color:var(--orange);background:#fffaf0} .box-display-empty{color:var(--text2);font-style:italic} .box-display-modal .modal-box{max-width:calc(1200px / var(--popup-scale));padding:16px} .link-display-modal .modal-box{max-width:calc(1200px / var(--popup-scale));padding:16px} .box-display-textarea{width:100%;box-sizing:border-box;border:1px solid var(--bdr);border-radius:8px;background:#f8fafc;padding:10px 12px;min-height:260px;max-height:calc(60vh / var(--popup-scale));resize:vertical;font-size:13.5px;line-height:1.55;font-family:inherit;color:var(--text)} .box-display-textarea.code-style-active{font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,'Liberation Mono',monospace;white-space:pre-wrap;overflow-wrap:anywhere;word-break:normal} .box-display-textarea:focus{outline:none;border-color:var(--orange);box-shadow:0 0 0 1px rgba(245,166,35,0.16)} .box-display-actions,.meq-detail-actions{display:flex;gap:8px;flex-wrap:wrap} .display-text-edit-notice{display:none;margin-top:7px;font-size:11.5px;color:var(--text2);line-height:1.35} .display-text-edit-notice.on{display:block} .meq-detail-actions-top{margin:0 0 10px 0} .meq-detail-actions-bottom{margin-top:12px} .pill.pill-n1{background:#fef3c7;border-color:#fde68a;color:#92400e} .pill.pill-n2{background:#d1fae5;border-color:#6ee7b7;color:#065f46} .pill.pill-n3{background:#ffe4e6;border-color:#fecdd3;color:#9f1239} .thinking-dots{display:inline-flex;gap:4px;padding:6px 0}.thinking-dots span{width:6px;height:6px;border-radius:50%;background:var(--text2);animation:pulse 1.2s infinite} .thinking-dots span:nth-child(2){animation-delay:0.2s}.thinking-dots span:nth-child(3){animation-delay:0.4s} .pulsing{animation:pulse 0.9s infinite} .save-toast{position:fixed;bottom:80px;left:50%;transform:translateX(-50%);color:#fff;font-size:12.5px;padding:12px 20px;border-radius:10px;box-shadow:0 4px 14px rgba(0,0,0,0.2);z-index:3000;transition:opacity 0.3s;cursor:pointer} .save-toast.fade{opacity:0} .ep-struct{display:flex;gap:7px;margin-top:10px;padding-top:10px;border-top:1px solid var(--bdr);flex-wrap:wrap} .ep-sbtn{padding:4px 10px;border-radius:5px;font-size:10px;font-weight:500;cursor:pointer;border:1px solid var(--bdr);background:none;color:var(--text2);transition:all 0.15s} .ep-sbtn:hover{background:var(--bg);color:var(--text)} .ep-sbtn:disabled{opacity:0.35;cursor:not-allowed;background:none;color:var(--text2)} .ep-sbtn.ep-del{color:#dc2626;border-color:#fecaca}.ep-sbtn.ep-del:hover{background:#fef2f2} .info-link{color:rgba(255,255,255,0.9);font-size:13px;cursor:pointer;text-decoration:underline;margin-left:14px} .info-link:hover{color:#fff} .page-info-bar{padding:7px 22px;font-size:14px;color:var(--text2);background:#fff;border-bottom:1px solid var(--bdr);flex-shrink:0} .page-info-bar .pi-link{color:var(--text2);cursor:pointer;font-weight:500} .page-info-bar .pi-link:hover{color:var(--text)} .info-ta{width:100%;border:1px solid var(--bdr);border-radius:7px;padding:11px;font-size:14px;font-family:inherit;resize:vertical;min-height:200px;line-height:1.6} .info-ta:focus{outline:none;border-color:var(--orange)} .ptc-ov{display:none;position:fixed;inset:0;background:rgba(0,0,0,0.35);backdrop-filter:blur(2px);z-index:2500;align-items:center;justify-content:center;padding:20px} .ptc-ov.open{display:flex} .ptc-box{background:#fff;border-radius:14px;padding:26px 28px;max-width:400px;width:100%;box-shadow:0 8px 32px rgba(0,0,0,0.18)} .ptc-box h3{font-size:16.5px;font-weight:700;margin-bottom:16px;color:var(--text)} .ptc-opt{display:flex;align-items:center;gap:12px;padding:13px 16px;border-radius:10px;border:1px solid var(--bdr);cursor:pointer;margin-bottom:10px;transition:background 0.15s,border-color 0.15s} .ptc-opt:hover{background:#f8fafc;border-color:#94a3b8} .ptc-dot{width:12px;height:12px;border-radius:50%;flex-shrink:0} .ptc-lbl{font-size:14px;font-weight:600;color:var(--text)} .ptc-desc{font-size:12px;color:var(--text2);margin-top:2px} .ptc-cancel{text-align:center;margin-top:8px;font-size:13px;color:var(--text2);cursor:pointer} .ptc-cancel:hover{color:var(--text)} .tab-type-dot{width:6px;height:6px;border-radius:50%;display:inline-block;margin-right:5px;vertical-align:middle;flex-shrink:0} .how-bar{height:36px;background:#64748b;border-radius:6px;display:flex;align-items:center;padding:0 14px;margin-bottom:14px;gap:12px} .how-bar span{color:#fff;font-weight:700;font-size:11px;letter-spacing:2.5px;text-transform:uppercase} .how-bar .page-bar-title{letter-spacing:0;text-transform:none;font-size:12px;font-weight:600} .tt-bar{height:36px;border-radius:6px;display:flex;align-items:center;padding:0 14px;margin-bottom:14px;gap:12px} .tt-bar span{color:#fff;font-weight:700;font-size:11px;letter-spacing:2.5px;text-transform:uppercase} .tt-bar .page-bar-title{letter-spacing:0;text-transform:none;font-size:12px;font-weight:600} /* modest selected-box actions controls in the This–Then and How page bars. */ .tt-box-actions-wrap{display:inline-flex;align-items:center;gap:7px;margin-left:2px;min-width:0;flex:1 1 auto} .tt-box-action-menu{font-size:11px;border:1px solid rgba(255,255,255,0.45);background:rgba(255,255,255,0.95);color:#1e293b;border-radius:18px;padding:4px 8px;max-width:180px;cursor:pointer} .tt-box-action-menu:disabled{opacity:0.7;cursor:not-allowed;color:#64748b} .tt-selected-box-label{color:rgba(255,255,255,0.94);font-size:11px;font-weight:600;letter-spacing:0;text-transform:none;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;max-width:260px;min-width:0} body.is-readonly .tt-box-actions-wrap{display:none!important} .doc-bar{height:36px;background:#8b5cf6;border-radius:6px;display:flex;align-items:center;padding:0 14px;margin-bottom:14px;gap:12px} .doc-bar span{color:#fff;font-weight:700;font-size:11px;letter-spacing:2.5px;text-transform:uppercase} .doc-bar .page-bar-title{letter-spacing:0;text-transform:none;font-size:12px;font-weight:600} .how-grid{display:flex;flex-wrap:wrap;row-gap:34px;column-gap:12px;padding:0 8px} .how-box{border-radius:7px;padding:8px 10px 8px 36px;cursor:pointer;position:relative;width:174px;min-height:68px;border:1px solid #d1d5db;background:#fff;transition:box-shadow 0.15s,opacity 0.2s;overflow:visible} .how-box{padding-right:30px} .how-box .box-txt{padding-right:4px} .how-box:hover{box-shadow:0 2px 8px rgba(0,0,0,0.12)} .how-box.grey-state{} .how-hid{position:absolute;bottom:3px;left:5px;font-size:8px;color:var(--text2);font-weight:600;letter-spacing:0.3px} .how-add-wrap{margin-bottom:12px;padding:0 8px} .how-add-btn{padding:6px 14px;border-radius:20px;font-size:11.5px;font-weight:500;cursor:pointer;border:1px solid #cbd5e1;background:#f1f5f9;color:#1e293b;transition:background 0.15s} .how-add-btn:hover{background:#e2e8f0} .doc-page{max-width:800px;margin:0 auto;padding:0 16px} .doc-toolbar{display:flex;flex-wrap:wrap;gap:3px;padding:6px 8px;background:#f8fafc;border:1px solid var(--bdr);border-bottom:none;border-radius:8px 8px 0 0} .doc-tbtn{width:30px;height:28px;border:1px solid transparent;border-radius:4px;background:none;cursor:pointer;font-size:13px;color:var(--text);display:flex;align-items:center;justify-content:center;transition:background 0.12s} .doc-tbtn:hover{background:#e2e8f0} .doc-tbtn.active{background:#dbeafe;border-color:#93c5fd} .doc-tbtn-sep{width:1px;background:var(--bdr);margin:4px 3px;flex-shrink:0} .doc-sel{height:28px;border:1px solid var(--bdr);border-radius:4px;background:#fff;font-size:11px;padding:0 6px;cursor:pointer;color:var(--text)} .doc-editor{min-height:420px;border:1px solid var(--bdr);border-radius:0 0 8px 8px;padding:18px 22px;background:#fff;font-size:13px;line-height:1.7;color:var(--text);outline:none;overflow-y:auto} .doc-editor:focus{border-color:var(--orange)} .doc-editor h1{font-size:20px;font-weight:700;margin:16px 0 8px} .doc-editor h2{font-size:17px;font-weight:700;margin:14px 0 6px} .doc-editor h3{font-size:14px;font-weight:600;margin:12px 0 4px} .doc-editor ul,.doc-editor ol{padding-left:24px;margin:6px 0} .doc-editor li{margin:3px 0} .doc-editor blockquote{border-left:3px solid var(--orange);padding:6px 14px;margin:8px 0;background:#fff8ed;border-radius:0 6px 6px 0;color:var(--text2)} .doc-editor a{color:var(--od);text-decoration:underline} .tt-link-section{margin-top:10px;padding-top:10px;border-top:1px solid var(--bdr)} .tt-link-hdr{font-size:11px;font-weight:600;color:var(--text2);margin-bottom:6px} .tt-link-row{display:flex;align-items:center;gap:6px;font-size:11px;margin-bottom:4px;padding:3px 0} .tt-link-box{max-width:120px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;font-size:10.5px;color:var(--text)} .tt-link-pg{font-size:9px;color:var(--text2)} .tt-link-arrow{cursor:pointer;font-weight:700;color:var(--orange);padding:2px 4px;border-radius:3px;transition:background 0.15s;user-select:none} .tt-link-arrow:hover{background:#fef3c7} .tt-link-rm{cursor:pointer;color:#dc2626;font-size:10px;margin-left:auto;padding:0 3px} .tt-link-rm:hover{text-decoration:underline} .tt-link-add{display:flex;gap:6px;margin-top:6px;align-items:center} .tt-link-sel{flex:1;border:1px solid var(--bdr);border-radius:5px;padding:4px 6px;font-size:11px;font-family:inherit} .tt-link-abtn{padding:4px 10px;border-radius:5px;font-size:10px;font-weight:500;cursor:pointer;border:1px solid var(--bdr);background:none;color:var(--text2);transition:all 0.15s} .tt-link-abtn:hover{background:var(--bg);color:var(--text)} .tt-cnt{position:absolute;font-size:8px;font-weight:700;color:#334155;cursor:pointer;z-index:4;line-height:1;min-width:16px;height:16px;border-radius:999px;background:#e2e8f0;border:1px solid #cbd5e1;display:flex;align-items:center;justify-content:center;box-shadow:0 1px 2px rgba(15,23,42,0.10);padding:0 3px;box-sizing:border-box;white-space:nowrap} .tt-cnt:hover{filter:brightness(.96)} .tt-cnt.tt-cnt-empty{min-width:12px;width:12px;height:12px;border-radius:50%;background:#fff;border:1.5px solid #cbd5e1;color:transparent;box-shadow:0 1px 2px rgba(0,0,0,0.10);padding:0} .tt-cnt.tt-cnt-empty:hover{background:#f8fafc;filter:none;box-shadow:0 2px 6px rgba(15,23,42,0.16)} .tt-cnt.tt-pol-positive{background:#e2e8f0;color:#334155;border-color:#cbd5e1} .tt-cnt.tt-pol-negative{background:#f1dada;color:#7f1d1d;border-color:#e7bcbc} .tt-cnt.tt-pol-mixed{background:linear-gradient(90deg,#e2e8f0 0 50%,#f1dada 50% 100%);color:#334155;border-color:#cbd5e1} /* hidden This–Then count/link drag hotspots are slightly smaller and sit a little lower, matching left and right side geometry while staying on the box borders and clear of link-arrow/page-jump affordances. */ .tt-cnt-hotspot{position:absolute;z-index:3;width:18px;height:20px;border-radius:999px;cursor:pointer;background:transparent;display:block} .tt-cnt-hotspot:hover{background:rgba(100,116,139,0.12);box-shadow:0 2px 8px rgba(15,23,42,0.18)} .reveal-arrow-hover-out,.reveal-arrow-hover-in{z-index:8} .tt-cnt-hotspot.tt-cnt-l{left:-11px;top:72%} .tt-cnt-hotspot.tt-cnt-r{right:-11px;top:72%} .reveal-arrow-hover-out.tt-neg,.reveal-arrow-hover-in.tt-neg{color:#b85f5f!important} .tt-rel-neg{color:#b85f5f!important;font-weight:700} .tt-rel-pos{color:var(--text);font-weight:700} .tt-flick{animation:ttFlick .26s ease-in-out 1} @keyframes ttFlick{0%{transform:translateX(0)}25%{transform:translateX(-2px) rotate(-.4deg)}55%{transform:translateX(2px) rotate(.4deg)}100%{transform:translateX(0)}} .tt-cnt-l{left:-8px;top:72%;transform:translateY(-50%)} .tt-cnt-r{right:-8px;top:72%;transform:translateY(-50%)} .tt-cnt-b{bottom:-7px;left:50%;transform:translateX(-50%)} .ttl-list{font-size:12px;line-height:1.7;max-height:300px;overflow-y:auto} .ttl-item{padding:6px 8px;border-bottom:1px solid var(--bdr);cursor:pointer;border-radius:4px} .ttl-item:last-child{border-bottom:none} .ttl-item:hover{background:#f8fafc} .ttl-item-title{font-weight:500;color:var(--text)} /* This-Then count-popup rows keep text in the main column and align info/delete controls in stable action columns within each popup column. */ .ttl-item-tight-actions{display:grid!important;grid-template-columns:minmax(0,1fr) auto auto;align-items:start;column-gap:6px;max-width:100%;width:100%;flex-wrap:nowrap} .ttl-item-tight-actions .ttl-link-main{min-width:0;max-width:100%;cursor:pointer;overflow-wrap:normal;word-break:normal} .ttl-item-tight-actions .ttl-item-title{overflow-wrap:normal;word-break:normal} .ttl-item-tight-actions .link-info-btn,.ttl-item-tight-actions .tt-link-abtn{justify-self:center;flex:0 0 auto} .ttl-item-tight-actions .tt-link-abtn{margin-left:0!important} .ttl-item-page{font-size:10px;color:var(--text2)} .doc-read{min-height:420px;border:1px solid var(--bdr);border-radius:8px;padding:18px 22px;background:#fff;font-size:13px;line-height:1.7;color:var(--text);overflow-y:auto} .doc-read h1{font-size:20px;font-weight:700;margin:16px 0 8px}.doc-read h2{font-size:17px;font-weight:700;margin:14px 0 6px}.doc-read h3{font-size:14px;font-weight:600;margin:12px 0 4px} .doc-read ul,.doc-read ol{padding-left:24px;margin:6px 0}.doc-read li{margin:3px 0} .doc-read blockquote{border-left:3px solid var(--orange);padding:6px 14px;margin:8px 0;background:#fff8ed;border-radius:0 6px 6px 0;color:var(--text2)} .doc-read a{color:var(--od);text-decoration:underline} .doc-edit-bar{display:flex;gap:8px;margin-bottom:10px} .doc-edit-btn{padding:6px 16px;border-radius:6px;font-size:11.5px;font-weight:600;cursor:pointer;border:1px solid var(--bdr);background:#fff;color:var(--text);transition:all 0.15s} .doc-edit-btn:hover{background:var(--bg);border-color:#94a3b8} .doc-save-bar{display:flex;gap:8px;margin-top:10px} .doc-save-btn{padding:6px 16px;border-radius:6px;font-size:11.5px;font-weight:600;cursor:pointer;border:none;color:#fff;background:var(--orange);transition:background 0.15s} .doc-save-btn:hover{background:var(--od)} .doc-cancel-btn{padding:6px 16px;border-radius:6px;font-size:11.5px;font-weight:600;cursor:pointer;border:1px solid var(--bdr);background:#fff;color:var(--text);transition:all 0.15s} .doc-cancel-btn:hover{background:var(--bg)} .doc-clone{background:#f1f5f9;border:1px solid #cbd5e1;border-radius:6px;padding:8px 12px;margin:8px 0;cursor:pointer;font-size:12.5px;line-height:1.5;color:var(--text);position:relative;transition:background 0.15s} .doc-clone:hover{background:#e2e8f0} .doc-clone::before{content:'';display:inline-block;width:6px;height:6px;border-radius:50%;background:#64748b;margin-right:6px;vertical-align:middle;flex-shrink:0} .doc-clone-label{font-size:9px;color:#7c3aed;font-weight:600;text-transform:uppercase;letter-spacing:0.3px;margin-right:4px} .doc-clone-broken{background:#fef2f2;border-color:#fecaca;color:#991b1b;font-style:italic} .doc-clone-broken::before{background:#ef4444} .doc-clone-broken .doc-clone-label{color:#dc2626} .doc-clone-rm{position:absolute;top:4px;right:6px;font-size:11px;color:var(--text2);cursor:pointer;padding:2px 4px;border-radius:3px;line-height:1} .doc-clone-rm:hover{color:#ef4444;background:#fef2f2} .clone-chooser{display:flex;flex-direction:column;gap:10px} .clone-chooser label{font-size:11px;font-weight:600;color:var(--text2)} .clone-chooser select{width:100%;border:1px solid var(--bdr);border-radius:6px;padding:7px 10px;font-size:12px;font-family:inherit;background:#fff} .clone-chooser select:focus{outline:none;border-color:var(--orange)} .how-link-section{margin-top:8px;padding-top:8px;border-top:1px solid var(--bdr)} .how-link-hdr{font-size:11px;font-weight:600;color:var(--text2);margin-bottom:6px} .how-link-row{display:flex;align-items:center;gap:6px;font-size:11px;margin-bottom:4px;padding:3px 0} .how-link-box{max-width:120px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;font-size:10.5px;color:var(--text)} .how-link-pg{font-size:9px;color:var(--text2)} .how-link-rm{cursor:pointer;color:#dc2626;font-size:10px;margin-left:auto;padding:0 3px} .how-link-rm:hover{text-decoration:underline} .how-link-add{display:flex;gap:6px;margin-top:6px;align-items:center} .how-link-sel{flex:1;border:1px solid var(--bdr);border-radius:5px;padding:4px 6px;font-size:11px;font-family:inherit} .how-link-abtn{padding:4px 10px;border-radius:5px;font-size:10px;font-weight:500;cursor:pointer;border:1px solid var(--bdr);background:none;color:var(--text2);transition:all 0.15s} .how-link-abtn:hover{background:var(--bg);color:var(--text)} .meq-section{margin-top:10px;padding-top:10px;border-top:1px solid var(--bdr)} .meq-hdr{font-size:10.5px;font-weight:600;color:var(--text);margin-bottom:6px} .meq-item{display:flex;align-items:center;gap:6px;margin-bottom:4px;font-size:10.5px} .meq-item-id{color:var(--text2);font-weight:600;font-size:9.5px;min-width:36px} .meq-item-title{color:var(--od);cursor:pointer;text-decoration:underline;text-underline-offset:2px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;flex:1} .meq-item-title:hover{color:var(--text)} .meq-item-rm{color:var(--text2);cursor:pointer;font-size:12px;padding:0 3px}.meq-item-rm:hover{color:#ef4444} .meq-add{display:flex;gap:4px;margin-top:4px;align-items:center} .meq-sel{flex:1;font-size:10.5px;padding:4px 6px;border:1px solid var(--bdr);border-radius:5px;background:#fff} .meq-abtn{padding:4px 10px;border-radius:5px;font-size:10px;font-weight:500;cursor:pointer;border:1px solid var(--bdr);background:none;color:var(--text2);transition:all 0.15s} .meq-abtn:hover{background:var(--bg);color:var(--text)} .meq-footer{display:flex;gap:8px;margin-top:6px;flex-wrap:wrap} .meq-footer-link{font-size:10px;color:var(--od);cursor:pointer;text-decoration:underline;text-underline-offset:2px} .meq-footer-link:hover{color:var(--text)} .meq-detail-modal .modal-box{max-width:520px} .meq-detail-field{margin-bottom:10px} .meq-detail-field label{display:block;font-size:11px;font-weight:600;color:var(--text);margin-bottom:3px} .meq-detail-field input,.meq-detail-field textarea{width:100%;border:1px solid var(--bdr);border-radius:6px;padding:7px 10px;font-size:12px;font-family:inherit;background:#f8fafc} .meq-detail-field input:focus,.meq-detail-field textarea:focus{outline:none;border-color:var(--orange)} .meq-detail-field textarea{resize:vertical;min-height:60px;line-height:1.5} .meq-linked-boxes{margin-top:8px} .meq-linked-box{font-size:10.5px;color:var(--od);cursor:pointer;margin-bottom:3px;text-decoration:underline;text-underline-offset:2px} .meq-linked-box:hover{color:var(--text)} .meq-reg-item{display:flex;align-items:center;gap:8px;padding:6px 0;border-bottom:1px solid var(--bdr);cursor:pointer} .meq-reg-item:hover{background:#f8fafc} .meq-reg-id{font-weight:600;color:var(--text2);font-size:10px;min-width:44px} .meq-reg-title{flex:1;font-size:11.5px;color:var(--text)} .meq-reg-status{font-size:10px;color:var(--text2)} .meq-reg-toolbar{display:flex;gap:8px;margin-bottom:10px} .meq-reg-btn{padding:5px 12px;border-radius:6px;font-size:11px;font-weight:500;cursor:pointer;border:1px solid var(--bdr);background:#fff;color:var(--text)} .meq-reg-btn:hover{background:var(--bg);border-color:var(--od)} .view-btn{color:var(--text2);cursor:pointer;font-size:13px;font-weight:500;margin-left:10px} .view-btn:hover{color:var(--text)} .view-panel-body{display:flex;flex-direction:column;gap:7px} .view-chk{display:flex;align-items:center;gap:9px;font-size:13.5px;color:var(--text);padding:4px 0} .view-chk input{margin:0} .view-chk label{cursor:pointer;user-select:none} .view-chk.disabled{opacity:.48} .view-chk input:disabled,.view-chk-inline input:disabled{cursor:not-allowed} .view-chk-row{display:flex;align-items:center;gap:18px;flex-wrap:wrap;font-size:13.5px;color:var(--text);padding:4px 0} .view-chk-main,.view-chk-inline{display:inline-flex;align-items:center;gap:9px;cursor:pointer;user-select:none} .view-chk-main input,.view-chk-inline input{margin:0} .view-chk-inline{font-size:12.5px;color:var(--text2)} .view-chk-row.disabled .view-chk-inline{opacity:.48} .view-chk-hint{font-size:11px;color:var(--text2);font-style:italic;margin:-3px 0 4px 24px} .under-box-wrap{display:flex;flex-direction:column;gap:2px;margin-top:3px;border:0;border-radius:0;background:transparent;overflow:visible} .under-box-block{width:174px;background:transparent;border:0;padding:2px 2px;font-size:9.5px;line-height:1.4;color:var(--text);max-height:60px;overflow-y:auto;word-break:break-word} .under-box-block + .under-box-block{border-top:0} .under-box-block-measures{border-left:3px solid #94a3b8} .under-box-block-eq{border-left:3px solid #5eead4} .under-box-block-main{border-left:0} .under-box-block-main.display-code-style{background:#f8fafc;border:1px solid #dbe3ee;border-radius:5px;padding:5px 6px;max-height:78px;white-space:pre-wrap;overflow-wrap:anywhere;word-break:normal} .under-box-block-main.show-full-display-text{max-height:none;overflow:visible} .under-box-block-label{font-size:10px;font-weight:500;color:var(--text2);letter-spacing:normal;margin-bottom:1px} .under-box-block .ub-clickable{color:var(--text);cursor:pointer;text-decoration:none;display:flex;align-items:flex-start;gap:3px;min-width:0} .under-box-block .ub-clickable:hover{color:var(--od);text-decoration:underline;text-underline-offset:1px} .ub-text{min-width:0;overflow-wrap:anywhere} .ub-tl-dot{display:inline-block;width:.78em;height:.78em;border-radius:50%;box-shadow:0 0 0 1px #fff;flex:0 0 auto;margin-top:.24em} .how-cnt-bottom{position:absolute;bottom:-8px;left:50%;transform:translateX(-50%);font-size:8px;font-weight:700;color:#475569;cursor:pointer;z-index:4;line-height:1;min-width:16px;height:16px;border-radius:3px;background:#fff;border:1.5px solid #94a3b8;display:flex;align-items:center;justify-content:center;padding:0 4px;box-sizing:border-box} .how-cnt-bottom:hover{background:#f8fafc;border-color:#64748b} .why-cnt-top{position:absolute;top:-8px;left:50%;transform:translateX(-50%);font-size:8px;font-weight:700;color:#475569;cursor:pointer;z-index:4;line-height:1;min-width:16px;height:16px;border-radius:3px;background:#fff;border:1.5px solid #94a3b8;display:flex;align-items:center;justify-content:center;padding:0 4px;box-sizing:border-box} .why-cnt-top:hover{background:#f8fafc;border-color:#64748b} .how-how-cnt-bottom{position:absolute;bottom:-8px;left:50%;transform:translateX(-50%);font-size:8px;font-weight:700;color:#475569;cursor:pointer;z-index:4;line-height:1;min-width:16px;height:16px;border-radius:3px;background:#fff;border:1.5px solid #94a3b8;display:flex;align-items:center;justify-content:center;padding:0 4px;box-sizing:border-box} .how-how-cnt-bottom:hover{background:#f8fafc;border-color:#64748b} .how-lateral-sq{position:absolute;right:-7px;top:42%;transform:translateY(-50%);width:12px;height:12px;border:1.5px solid #94a3b8;border-radius:2px;cursor:pointer;z-index:4;background:#fff;display:flex;align-items:center;justify-content:center;transition:background 0.15s,border-color 0.15s} .how-lateral-sq:hover{border-color:#64748b;background:#f8fafc} .how-lateral-sq.has-links{background:#64748b;border-color:#475569} .how-lateral-sq.has-links:hover{background:#475569} /* TT lateral-link affordance moved UP on the right border (from middle to near the top) so it no longer clashes with the middle-right area used by This–Then link arrows, the tt-cnt-r circle, or the jump/drill triangles in the lower-right. */ .tt-lateral-sq{position:absolute;right:-7px;top:16px;width:12px;height:12px;border:1.5px solid #94a3b8;border-radius:2px;cursor:pointer;z-index:4;background:#fff;display:flex;align-items:center;justify-content:center;transition:background 0.15s,border-color 0.15s} .tt-lateral-sq:hover{border-color:#64748b;background:#f8fafc} .tt-lateral-sq.has-links{background:#64748b;border-color:#475569} .tt-lateral-sq.has-links:hover{background:#475569} .src-ref-box{position:absolute;top:6px;right:58px;max-width:260px;min-width:170px;padding:6px 28px 10px 12px;overflow:visible;border-radius:6px;border:1px solid;font-size:12px;line-height:1.35;z-index:50;box-shadow:0 2px 8px rgba(0,0,0,0.12);background:#fff;user-select:none} .src-ref-box .tt-cnt-l{left:-8px;top:50%} .src-ref-box .tt-cnt-r{right:-8px;top:50%} .src-ref-box .tt-lateral-sq{right:-7px;top:50%;transform:translateY(-50%)} .src-ref-label{font-size:8.5px;font-weight:600;color:var(--text2);text-transform:uppercase;letter-spacing:0.45px;margin-bottom:2px;line-height:1.3;white-space:normal} .src-ref-content{cursor:pointer;color:var(--text);font-size:12px;line-height:1.45;display:-webkit-box;-webkit-box-orient:vertical;-webkit-line-clamp:5;max-height:calc(1.45em * 5);overflow:hidden;white-space:normal;overflow-wrap:anywhere} .src-ref-content:hover{text-decoration:underline} .src-ref-close{position:absolute;top:3px;right:5px;font-size:14px;color:var(--text2);cursor:pointer;line-height:1;padding:0 3px;user-select:none} .src-ref-close:hover{color:#ef4444} .view-panel-btnrow{display:flex;gap:8px;margin-top:12px;padding-top:10px;border-top:1px solid var(--bdr);flex-wrap:wrap} .view-panel-btn{padding:5px 12px;border-radius:6px;font-size:11.5px;font-weight:500;cursor:pointer;border:1px solid var(--bdr);background:#fff;color:var(--text);transition:all 0.15s} .view-panel-btn:hover{background:var(--bg);border-color:#94a3b8} .view-sep{height:1px;background:#e5e7eb;margin:4px 0} .link-reg-item{display:flex;align-items:flex-start;gap:8px;padding:7px 0;border-bottom:1px solid var(--bdr);cursor:pointer;font-size:11px;line-height:1.35} .link-reg-item:hover{background:#f8fafc} .link-reg-type{font-size:9px;font-weight:600;color:#fff;padding:2px 6px;border-radius:3px;white-space:nowrap;flex-shrink:0;margin-top:1px} .link-reg-type.tt-type{background:#F5A623} .link-reg-type.how-type{background:#60a5fa} /* Cross-Link badge for Cross-Link structural links in the top Links list. Distinct slate color so it reads as a separate structural-link category from This–Then and How. The wording "Cross-Link" is used in place of the older Other links wording, which was intentionally avoided as too vague. */ .link-reg-type.nud-type{background:#94a3b8} .link-reg-main{flex:1;min-width:0;display:flex;flex-wrap:wrap;align-items:baseline;gap:3px 5px} .link-reg-rel{font-weight:700;font-style:normal;color:var(--text);white-space:nowrap} .link-reg-arrow{color:var(--text2);font-size:10px;flex-shrink:0} .link-reg-box{font-size:10.8px;color:var(--text)} .link-reg-pg{font-size:10px;color:var(--text2);white-space:nowrap} .link-reg-snippet{font-size:9.5px;color:var(--text2);font-style:italic;margin-left:6px} .link-info-btn{cursor:pointer;font-size:10px;color:var(--text2);padding:0 3px;flex-shrink:0;text-decoration:underline;text-underline-offset:2px} .link-info-btn:hover{color:var(--od)} .link-detail-top{display:flex;align-items:center;gap:10px;margin:12px 0 12px;padding:1px 0} .link-detail-tl{width:18px;height:18px;border-radius:50%;border:0;padding:0;cursor:pointer;box-shadow:0 0 0 1.5px #fff;flex:0 0 auto} .link-detail-tl:disabled{cursor:default} .link-detail-tl-label{font-size:10.5px;color:var(--text2)} .tt-cnt-empty{position:absolute;font-size:8px;font-weight:700;color:transparent;cursor:pointer;z-index:4;line-height:1;width:14px;height:14px;border-radius:50%;background:#fff;border:1.5px solid #cbd5e1;display:flex;align-items:center;justify-content:center} .tt-cnt-empty:hover{border-color:#94a3b8;background:#f8fafc} .link-drag-line{position:fixed;left:0;top:0;width:0;height:0;border-top:2px dashed #64748b;transform-origin:0 0;z-index:9999;pointer-events:none;opacity:.85} .link-drag-target{outline:2px solid #64748b!important;outline-offset:3px} .link-dragging{cursor:crosshair!important} .link-dragging *{user-select:none!important} .src-ref-affordances{position:absolute;inset:0;height:auto;margin:0;pointer-events:none;overflow:visible} .src-ref-affordances .tt-cnt,.src-ref-affordances .tt-cnt-empty,.src-ref-affordances .how-cnt-bottom,.src-ref-affordances .how-cnt-bottom-empty,.src-ref-affordances .why-cnt-top,.src-ref-affordances .why-cnt-top-empty,.src-ref-affordances .how-how-cnt-bottom,.src-ref-affordances .how-lateral-sq,.src-ref-affordances .tt-lateral-sq{z-index:60;pointer-events:auto} .why-cnt-top-empty{position:absolute;top:-8px;left:50%;transform:translateX(-50%);font-size:8px;font-weight:700;color:transparent;cursor:pointer;z-index:4;line-height:1;min-width:14px;height:14px;border-radius:3px;background:#fff;border:1.5px solid #cbd5e1;display:flex;align-items:center;justify-content:center;padding:0 2px} .why-cnt-top-empty:hover{border-color:#94a3b8;background:#f8fafc} .how-cnt-bottom-empty{position:absolute;bottom:-8px;left:50%;transform:translateX(-50%);font-size:8px;font-weight:700;color:transparent;cursor:pointer;z-index:4;line-height:1;min-width:14px;height:14px;border-radius:3px;background:#fff;border:1.5px solid #cbd5e1;display:flex;align-items:center;justify-content:center;padding:0 2px} .how-cnt-bottom-empty:hover{border-color:#94a3b8;background:#f8fafc} /* ghost/context affordances with no existing links stay visually empty rather than using the filled count style. */ .src-ref-affordances .tt-cnt-empty{background:#fff!important;color:transparent!important;border:1.5px solid #cbd5e1!important} .src-ref-affordances .why-cnt-top-empty,.src-ref-affordances .how-cnt-bottom-empty{background:#fff!important;color:transparent!important;border:1.5px solid #cbd5e1!important} .src-ref-affordances .how-lateral-sq:not(.has-links),.src-ref-affordances .tt-lateral-sq:not(.has-links){background:#fff!important} .src-ref-box .how-lateral-sq{right:-7px;top:50%;transform:translateY(-50%)} .src-ref-box .why-cnt-top,.src-ref-box .why-cnt-top-empty{top:-8px;left:50%;transform:translateX(-50%)} .src-ref-box .how-cnt-bottom,.src-ref-box .how-cnt-bottom-empty,.src-ref-box .how-how-cnt-bottom{bottom:-8px;left:50%;transform:translateX(-50%)} /* stable first-render placement for ghost/context This–Then side affordances. These rules deliberately override the normal-box 68% positioning so ghost affordances sit midway on their own panel immediately, before any subsequent interaction/re-render. */ .src-ref-box .src-ref-affordances .tt-cnt-l,.src-ref-box .src-ref-affordances .tt-cnt-empty.tt-cnt-l{left:-8px!important;right:auto!important;top:50%!important;bottom:auto!important;transform:translateY(-50%)!important} .src-ref-box .src-ref-affordances .tt-cnt-r,.src-ref-box .src-ref-affordances .tt-cnt-empty.tt-cnt-r{right:-8px!important;left:auto!important;top:50%!important;bottom:auto!important;transform:translateY(-50%)!important} /* backward guard for malformed empty side classes created by the earlier replacement bug. */ .src-ref-box .src-ref-affordances .tt-cnt-empty-l{left:-8px!important;right:auto!important;top:50%!important;bottom:auto!important;transform:translateY(-50%)!important} .src-ref-box .src-ref-affordances .tt-cnt-empty-r{right:-8px!important;left:auto!important;top:50%!important;bottom:auto!important;transform:translateY(-50%)!important} /* custom tooltip. Replaces native title-based tooltips so they appear faster and are larger and easier to read. Kept compact, unobtrusive, and stable. The tooltip system (see DV_TIPS script below) intercepts the native title attribute on hover, hides the native tooltip, and shows this styled element after a short delay. */ .dv-tip{position:fixed;z-index:9999;background:#475569;color:#fff;padding:7px 11px;border-radius:6px;font-size:12.5px;font-weight:500;line-height:1.45;max-width:280px;box-shadow:0 3px 12px rgba(15,23,42,0.20);pointer-events:none;display:none;white-space:normal;letter-spacing:0.1px} /* read-only copy styling. The .is-readonly class is applied to when the board is loaded from an embedded state carrying isReadonly:true. It hides edit-oriented controls throughout the UI while preserving navigation, search, print, view settings, and other viewing functions. Keep modest \u2014 a small outline pill label near the board title, dimmed/hidden buttons, and hidden chat. Non-alarmist styling. */ /* Read-only copy label was previously rendered inside .hdr-right above the tagline as a translucent outline pill. */ /* the Read-Only Copy label is moved back to the left side of the header, rendered left-justified directly under the board title inside .hdr-title-wrap. The styling is intentionally modest \u2014 small, low-contrast, single line, no pill, no uppercase spacing, no warning-banner appearance \u2014 so it reads as a quiet mode indicator rather than a security guarantee. The honest tooltip clarifies that read-only copies are a UI-only restriction and not security protection. */ /* the Read-Only label is restyled as a modest pill and moved inline with the top utility links (alongside Board info, Measures, \u2026, Get training) rather than sitting under the title. The label text is "Read-Only Board" and reads as a status pill, not a clickable link or a warning banner. The font size is slightly larger than the previous tiny label so it stands out modestly without dominating; the subtle white outline + transparent background and small horizontal padding give it pill shape; cursor:help signals hover reveals the tooltip; user-select:none makes it feel like a status, not text. The honest tooltip remains unchanged ("Editing is disabled through the board interface. This is not security protection; someone with technical skills could still edit the HTML."). Display is gated by body.is-readonly so editable boards never show it. */ .ro-label{display:none;font-size:12px;color:#1e293b;font-weight:700;line-height:1.2;white-space:nowrap;padding:6px 12px;letter-spacing:0.1px;text-transform:none;background:#fff7ed;border:1.5px solid #fed7aa;border-radius:999px;cursor:help;user-select:none;align-self:center} body.is-readonly .ro-label{display:inline-flex;align-items:center;justify-content:center} /* in read-only Presentation View, the read-only status pill is also shown in the presentation top bar immediately to the left of Normal View. */ .presentation-bar .ro-label{margin-right:2px;color:#1e293b;background:#fff7ed;border-color:#fed7aa} .tag-validation{display:none;color:#b91c1c;background:#fef2f2;border:1px solid #fecaca;border-radius:6px;padding:7px 9px;font-size:12px;line-height:1.35;margin-top:7px;font-weight:600} body.is-readonly .chat-fab,body.is-readonly .chat-panel,body.is-readonly .mobile-chat-btn{display:none!important} body.is-readonly #saveBtn2,body.is-readonly #newBoardBtn,body.is-readonly #createReadOnlyCopyBtn{display:none!important} body.is-readonly .how-add-btn{display:none!important} body.is-readonly .ep-struct{display:none!important} /* Read-only copies keep the details panel viewable while hiding edit-oriented controls. */ body.is-readonly .ep-controls-edit,body.is-readonly .ep-add,body.is-readonly #epTa,body.is-readonly .ep-acts,body.is-readonly .tt-link-add{display:none!important} body.is-readonly .tab-add-btn{display:none!important} body.is-readonly .pi-link[onclick*="movePageLeft"],body.is-readonly .pi-link[onclick*="confirmDeletePage"],body.is-readonly .pi-undo,body.is-readonly .pi-link[onclick*="openPageInfo"]{display:none!important} /* Keep Page info clickable (view-only) — editing is gated in savePageInfo. Hide move/delete/undo. */ body.is-readonly .pi-link[onclick*="openPageInfo"]{display:inline!important} body.is-readonly .how-level-controls{display:none!important} body.is-readonly .ep-edit{display:none!important} body.is-readonly .doc-edit-bar,body.is-readonly .doc-save-bar,body.is-readonly .doc-toolbar{display:none!important} body.is-readonly #srcModal button[onclick*="addNewSourceRow"],body.is-readonly #srcModal button[onclick*="saveSources"]{display:none!important} /* button[onclick="saveViewPanel()"] is intentionally NOT hidden in read-only mode \u2014 changing View is a display/viewing change, not a content edit, and saveViewPanel() now runs in read-only mode (saveState() is a no-op in read-only so nothing persists past the session). The other Save-style buttons remain hidden because they would edit board content. */ body.is-readonly button[onclick="saveBoardInfo()"],body.is-readonly button[onclick="savePageInfo()"],body.is-readonly button[onclick="saveMeasureDetail()"],body.is-readonly button[onclick="saveEQDetail()"],body.is-readonly button[onclick="saveLinkDetail()"],body.is-readonly button[onclick="doInsertClone()"],body.is-readonly button[onclick="saveTopRightText()"],body.is-readonly button[onclick="clearTopRightText()"],body.is-readonly button[onclick="saveBoxDisplayTextPopup()"],body.is-readonly button[onclick="saveLinkDisplayTextPopup()"],body.is-readonly button[onclick="saveMEQDisplayTextEditor()"]{display:none!important} /* in read-only copies, Board info / Page info open in rendered view mode only. The Edit and Cancel buttons (which would expose the textarea + Save) are hidden, so the popup shows only the rendered view of the saved text plus the Close button. URLs in the rendered view remain clickable. */ body.is-readonly button[onclick="boardInfoStartEdit()"],body.is-readonly button[onclick="boardInfoCancelEdit()"],body.is-readonly button[onclick="pageInfoStartEdit()"],body.is-readonly button[onclick="pageInfoCancelEdit()"]{display:none!important} body.is-readonly .meq-reg-toolbar,body.is-readonly .meq-footer,body.is-readonly .meq-add,body.is-readonly .how-link-add,body.is-readonly .ep-sbtn.ep-del,body.is-readonly .meq-item-rm,body.is-readonly .tt-link-rm,body.is-readonly .how-link-rm,body.is-readonly .doc-clone-rm,body.is-readonly .src-ref-close{display:none!important} body.is-readonly .pri-badge,body.is-readonly .tl-dot{cursor:default} /* Keep dots/badges displayed (view-only) \u2014 their click-to-edit popups are gated in openTLChooser / openPriChooser. */ body.is-readonly .box-chooser{display:none!important} /* in a read-only copy, popup textareas and text inputs are made non-editable via the readOnly attribute set by applyReadonlyToModal() (called from openModal). These CSS rules back that up with visual styling that reads as non-editable at a glance \u2014 dimmed background, default (non-text) cursor, neutralised focus ring \u2014 so the UI does not imply editability when none is possible. Contenteditable regions (such as the doc-page editor, should it ever render in read-only mode) are also visually locked down. Search/filename inputs are excluded via the .ro-keep-editable escape hatch so viewing/filtering inputs still feel live. */ body.is-readonly .modal-ov textarea[readonly],body.is-readonly .modal-ov input[type="text"][readonly]{background:#eef2f6!important;cursor:default!important;color:var(--text)} body.is-readonly .modal-ov textarea[readonly]:focus,body.is-readonly .modal-ov input[type="text"][readonly]:focus{outline:none!important;border-color:var(--bdr)!important;box-shadow:none!important} body.is-readonly .modal-ov [contenteditable="false"]{cursor:default} body.is-readonly .modal-ov .ro-keep-editable{background:#f8fafc!important;cursor:text!important} /* coherent non-box UI scaling. The board area (.main) already uses zoom:var(--board-scale) so boxes and on-board chrome (page bars, under-box details, etc.) scale together. Previously, the non-box UI/chrome sat outside .main at 1x, so header/tab-bar/page-info-bar/control-bar/attribution and all popups/modals/tooltip/save-toast/chat elements felt disproportionately small once the board was scaled. Each of these containers now participates in the same scaling logic via zoom:var(--board-scale), so the whole interface scales together. For fixed-size popups/modals/tooltip/chat panel, max-width/max-height/width/height are divided by --board-scale so their visible on-screen footprint stays consistent with pre-fix behavior while internal text scales coherently. The on-board box-chooser popup is appended inside #main and therefore keeps inheriting .main's zoom as before — no change needed. CSS zoom is already in use for .main, so relying on zoom here is consistent with the existing approach and avoids scattering many per-element font-size tweaks. */ /* Main popup/modal scaling is decoupled from board scale so popup text remains readable while the board is zoomed. */ /* main popup/modal width base increased from 820px to 1200px (still divided by --popup-scale) so the full popup is visible without sideways truncation and right-side controls such as Add are reachable without horizontal scrolling. Only .modal-box max-width changes here — --popup-scale itself is unchanged, .meq-detail-modal .modal-box and .ptc-box keep their existing max-width bases, and the popup heading size change lives with the .modal-box h3 rule earlier in the CSS. */ .hdr{zoom:var(--board-scale)} .tab-bar{zoom:var(--board-scale)} .page-info-bar{zoom:var(--board-scale)} .ctrl-bar{zoom:1} .attr{zoom:var(--board-scale)} .modal-box{zoom:var(--popup-scale);max-width:calc(1200px / var(--popup-scale));max-height:calc(82vh / var(--popup-scale))} .meq-detail-modal .modal-box{max-width:calc(520px / var(--popup-scale))} .ptc-box{zoom:var(--popup-scale);max-width:calc(400px / var(--popup-scale))} #howPageTypeChooser .ptc-box{max-width:calc(680px / var(--popup-scale));max-height:calc(86vh / var(--popup-scale));overflow-y:auto} .dv-tip{zoom:var(--board-scale);max-width:calc(280px / var(--board-scale))} .save-toast{zoom:var(--board-scale)} .chat-fab{zoom:1} .chat-panel{zoom:var(--board-scale);width:calc(500px / var(--board-scale));max-width:calc((100vw - 44px) / var(--board-scale));height:calc(620px / var(--board-scale));max-height:calc((100vh - 165px) / var(--board-scale))} @media (max-width:700px), ((orientation: landscape) and (max-height:520px) and (max-width:950px)){ .ctrl-bar{zoom:1;flex-wrap:nowrap;overflow-x:auto;overflow-y:hidden;-webkit-overflow-scrolling:touch;gap:7px;padding:8px 10px;align-items:center;scrollbar-width:auto} .ctrl-bar::-webkit-scrollbar{height:8px}.ctrl-bar::-webkit-scrollbar-track{background:#f1f5f9}.ctrl-bar::-webkit-scrollbar-thumb{background:#cbd5e1;border-radius:999px} .ctrl-bar .cbtn{flex:0 0 auto;font-size:14px;line-height:1.2;padding:7px 12px;min-height:36px} .ctrl-bar .status-bar{margin-left:0;flex:0 0 auto;font-size:12px;white-space:nowrap;padding-right:6px} .mobile-chat-btn{display:inline-flex;align-items:center;justify-content:center} .chat-fab{display:none!important} .chat-panel{zoom:1;left:10px;right:10px;bottom:58px;width:auto;max-width:none;height:min(520px,calc(100vh - 82px));max-height:calc(100vh - 82px);border-radius:12px} } /* inner detail/edit popup content width fix. Previously the outer .modal-box was widened and the inline detail/entry panel (.ep) was rendered inside #epWrap inside .main, but the inner .ep panel had no explicit width and some inner rows (.ep-add, .ep-struct, .meq-add) could push right-side controls such as Add off the visible area, requiring horizontal scrolling. The fix targets only the inner edit/detail popup content: .ep is forced to use the available width with width:100% and max-width:none; .ep-ta is reinforced at width:100% (already so); .ep-add and .ep-struct keep their existing flex-wrap:wrap and now also have max-width:100% so they wrap rather than overflow; .meq-add (the picker+Add row inside Measure/Evaluation Question/Link detail modals) gets flex-wrap:wrap + max-width:100% for the same reason; .meq-section (parent of meq-add and meq-item rows) is constrained to max-width:100%. No outer modal width change here — that lives with the .modal-box rule earlier. No popup-scale change. No board-scale change. No schema, state, link, read-only, or interaction changes. */ .ep{width:100%;max-width:none} .ep-ta{width:100%} .ep-add{flex-wrap:wrap;max-width:100%} .ep-struct{flex-wrap:wrap;max-width:100%} .meq-add{flex-wrap:wrap;max-width:100%} .meq-section{max-width:100%} /* This–Then / How link popup overflow. The .ep / detail-panel rules above targeted those classes, but the remaining overflow problem was specifically the This–Then / How link popup, where the inner row containing the dropdown/select and the Add button could still push the Add button off to the right and require horizontal scrolling. The fix targets the actual link-popup rows (the .tt-link-add and .how-link-add rows used by buildTTLinkPicker, buildHowActingOnPicker, buildHowLinkTTPicker, buildHowLinkHowPicker, buildLateralHowOnTTPopupPicker, etc., plus the .meq-add picker+Add rows used inside the Link detail popup buildLinkMeasuresSection / buildLinkEQSection): .tt-link-add and .how-link-add now have flex-wrap:wrap + max-width:100% (parallel to the .ep-add / .ep-struct / .meq-add rules) so they wrap rather than overflow at narrow widths; .tt-link-section and .how-link-section are constrained to max-width:100% as their containers; and .tt-link-sel, .how-link-sel, and .meq-sel get min-width:0 + max-width:100% so the select itself can shrink below its widest option text rather than forcing the row wider than its container — without min-width:0, a flex item's default min-width:auto resolves to min-content and prevents shrink even with flex:1 + flex-shrink:1. No outer modal width change; no popup-scale change beyond the default board-scale change in :root; no schema, state, link, read-only, or interaction changes. */ .tt-link-add{flex-wrap:wrap;max-width:100%} .how-link-add{flex-wrap:wrap;max-width:100%} .tt-link-section{max-width:100%} .how-link-section{max-width:100%} .tt-link-sel{min-width:0;max-width:100%} .how-link-sel{min-width:0;max-width:100%} .meq-sel{min-width:0;max-width:100%} /* board-level Top right text styling for the page-info bar. The page-info bar's left content stays as-is in #pageInfoBarLeft; #pageInfoBarRight floats to the right and right-aligns the optional Top right text or the modest "+ Add top right text" affordance shown when no Top right text exists. The Top right text is plain text only (no rich text, no links). Truncated with ellipsis and capped at a sensible max-width so long text does not crowd out page controls on narrow viewports. In read-only copies the text is visible (via the same .pi-right-text class) but the click handler is gated by IS_READONLY so it cannot be edited. The "+ Add top right text" affordance is hidden in read-only copies via body.is-readonly. */ .pi-bar-flex{display:flex;align-items:center;gap:6px;width:100%} .pi-bar-left{flex:1;min-width:0} .pi-bar-right{margin-left:auto;display:flex;align-items:center;gap:6px;flex-shrink:0;text-align:right;justify-content:flex-end} .pi-right-text{font-size:13.5px;font-style:italic;color:var(--text2);max-width:320px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;cursor:pointer;padding:1px 4px;border-radius:4px} .pi-right-text:hover{color:var(--text);background:#f1f5f9} body.is-readonly .pi-right-text{cursor:default} body.is-readonly .pi-right-text:hover{color:var(--text2);background:transparent} .pi-right-add{font-size:12px;color:var(--text2);cursor:pointer;font-style:italic;opacity:0.7;padding:1px 4px;border-radius:4px} .pi-right-add:hover{opacity:1;color:var(--text);background:#f1f5f9} body.is-readonly .pi-right-add{display:none!important} /* Board-level Official DoView Badge block. Text-only; no logo or icon is embedded. Remove the generated badge block for non-official, forked, rebranded, or substantially modified builds unless authorised in writing by Dr Paul Duignan or the DoView® trademark owner. */ .hdr-badge{display:inline-flex;flex-direction:column;align-items:center;justify-content:center;background:var(--orange);color:#fff;border:1.5px solid #fff;border-radius:8px;padding:5px 12px;margin-top:5px;font-weight:700;line-height:1.25;cursor:pointer;text-align:center;letter-spacing:0.2px} .hdr-badge:hover{background:#e09000} .hdr-badge-l1{font-size:13px;font-weight:700} .hdr-badge-l2{font-size:10.5px;font-weight:600;opacity:0.95;margin-top:1px} .badge-modal-body{font-size:13.5px;line-height:1.6;color:var(--text)} .badge-modal-body p{margin-bottom:10px} .badge-modal-body a{color:var(--od);text-decoration:underline;word-break:break-all} .badge-modal-body a:hover{color:var(--text)} /* quiet user-defined visual tags. */ .doview-tag{font-size:0.72em;color:#777;opacity:0.75;font-weight:600;white-space:nowrap;margin-left:0.35em} .tag-manager-row{display:flex;align-items:center;gap:6px;flex-wrap:wrap;border-bottom:1px solid var(--bdr);padding:7px 0} .tag-manager-row input{border:1px solid var(--bdr);border-radius:6px;padding:5px 7px;font-size:12px;font-family:inherit} /* standard editable form-field styling for tag fields, pasted-page rename fields, and compact text/select controls where practical. */ .tag-short-input,.tag-label-input,.tag-desc-input,#pastePageName,.tt-link-sel,.how-link-sel,.meq-sel,.meq-detail-field input,.meq-detail-field textarea{border:1px solid var(--bdr);border-radius:7px;background:#f8fafc;font-family:inherit;transition:border-color 0.15s,box-shadow 0.15s} .tag-short-input:focus,.tag-label-input:focus,.tag-desc-input:focus,#pastePageName:focus,.tt-link-sel:focus,.how-link-sel:focus,.meq-sel:focus,.meq-detail-field input:focus,.meq-detail-field textarea:focus{outline:none;border-color:var(--orange);box-shadow:0 0 0 1px rgba(245,166,35,0.16)} .tag-short-input{width:70px;text-transform:uppercase}.tag-label-input{flex:1;min-width:180px}.tag-desc-input{flex:2;min-width:220px}.tag-short-input,.tag-label-input,.tag-desc-input{font-size:10.5px;line-height:1.25}.meq-detail-field label,.tag-manager-row label{font-size:11px;font-weight:600;line-height:1.25} .tag-small-btn{border:1px solid var(--bdr);background:#fff;border-radius:6px;padding:5px 8px;font-size:11px;cursor:pointer;color:var(--text2)}.tag-small-btn:hover{background:#f8fafc;color:var(--text)}.tag-del-btn{color:#b91c1c} .ep-tags{font-size:11px;color:var(--text2);margin:8px 0 10px;padding:6px 0;border-top:1px solid #eef2f7;border-bottom:1px solid #eef2f7;display:flex;align-items:center;gap:6px;flex-wrap:wrap} .ep-tag-pill{font-weight:600;color:#666;opacity:0.8;white-space:nowrap}.ep-tag-rm{cursor:pointer;color:#b91c1c;opacity:0.75;margin-left:2px}.ep-tag-rm:hover{opacity:1} body.is-readonly .tag-edit-only{display:none!important} /* selected-box details pane cleanup. */ /* Presentation View and details-pane polish. */ .ep-secondary-grid{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:10px;align-items:start;margin-top:10px} .ep-secondary-grid .ep-section,.ep-secondary-grid .meq-section{margin-top:0;padding-top:8px;border-top:1px solid #eef2f7;min-width:0} .ep-secondary-grid .how-link-add,.ep-secondary-grid .ep-compact-row{display:flex;align-items:center;gap:6px;flex-wrap:wrap} .ep-secondary-grid select{flex:0 1 170px!important;max-width:190px!important;min-width:0} .ep-secondary-grid .meq-abtn,.ep-secondary-grid .ep-sbtn{white-space:nowrap} @media(max-width:980px){.ep-secondary-grid{grid-template-columns:1fr}.ep-secondary-grid select{flex:1 1 190px!important;max-width:260px!important}} .presentation-statement-row{display:none;background:#fff;border-bottom:1px solid var(--bdr);padding:6px 16px;justify-content:flex-end;align-items:center;gap:10px;min-height:30px;box-sizing:border-box;flex-wrap:wrap} .presentation-statement-text{font-size:12.5px;font-weight:600;color:var(--text2);max-width:min(620px,70vw);overflow:hidden;text-overflow:ellipsis;white-space:nowrap;text-align:right}.presentation-overview-slot{margin-right:auto;display:inline-flex;align-items:center} .presentation-readonly-pill{display:none;font-size:11.5px;font-weight:700;color:#9a5a00;background:#fff7ed;border:1px solid #fed7aa;border-radius:999px;padding:2px 8px;white-space:nowrap} body.is-readonly .presentation-readonly-pill{display:inline-flex} body.presentation-view .presentation-statement-row.has-content{display:flex} .presentation-bar .presentation-brand{margin-left:0} body.presentation-view .chat-fab{bottom:28px} body.presentation-view .chat-panel{bottom:96px;max-height:calc(100vh - 120px)} .ep-section{margin-top:10px;padding-top:8px;border-top:1px solid #eef2f7} .ep-section-hdr{font-size:11px;font-weight:700;color:var(--text);margin-bottom:5px} .ep-quick-row{display:flex;align-items:center;gap:8px;flex-wrap:wrap;font-size:10.5px;color:var(--text2)} .ep-notes-head{display:flex;align-items:center;justify-content:space-between;gap:8px} .ep-notes-summary{max-height:7.5em;overflow-y:auto;border:1px solid #eef2f7;border-radius:7px;background:#f8fafc;padding:5px 7px;margin:5px 0;font-size:10.5px;color:var(--text2);line-height:1.45} .ep-note-line{display:flex;align-items:flex-start;gap:5px;margin:2px 0} .ep-note-label{font-weight:700;color:var(--text);white-space:nowrap} .ep-note-empty{font-style:italic;color:#94a3b8} .ep-add-notes{display:flex;align-items:center;gap:4px;flex-wrap:wrap;margin-top:5px} .ep-add-notes .pill{font-size:10.5px;padding:3px 7px} .ep-compact-row{display:flex;align-items:center;gap:6px;flex-wrap:wrap} .ep-compact-row select,.meq-add select,.how-link-add select{flex:0 1 230px!important;max-width:260px!important} .meq-item{background:#f8fafc;border:1px solid #e2e8f0;border-radius:9px;padding:4px 7px;margin:3px 0;display:flex;align-items:center;gap:5px;line-height:1.3} .meq-item-title{color:var(--text)!important;text-decoration:none!important;cursor:pointer;flex:1;min-width:0;word-break:break-word} .meq-item-id{background:#fff7ed;border:1px solid #fed7aa;border-radius:999px;padding:1px 5px;color:#9a5a00;font-weight:700;font-size:9.5px;white-space:nowrap} .meq-footer{display:flex;align-items:center;gap:8px;flex-wrap:wrap;margin-top:4px} .color-swatch-btn{border:0!important;background:transparent!important;border-radius:999px!important;padding:1px 3px!important} .color-swatch-btn.opt-active,.color-swatch-btn:hover{outline:1px solid #94a3b8;background:#f8fafc!important} .color-swatch{width:14px;height:14px} .color-compact{display:inline-flex;align-items:center;gap:4px;position:relative}.color-swatch-btn{display:inline-flex;align-items:center;gap:3px;border:1px solid var(--bdr);background:#f8fafc;border-radius:999px;padding:2px 6px;font-size:10.5px;cursor:pointer;color:var(--text2);line-height:1.1}.color-swatch-btn:disabled{cursor:default;opacity:.85}.color-swatch{display:inline-block;width:13px;height:13px;border-radius:50%;border:1px solid #94a3b8;background:#fff;vertical-align:middle}.color-swatch.default{background:linear-gradient(135deg,#fff 0%,#fff 46%,#cbd5e1 48%,#cbd5e1 52%,#fff 54%,#fff 100%)} /* one-bar Presentation View and page-control cleanup. */ .pi-pill,.view-btn{display:inline-flex;align-items:center;gap:4px;border:1px solid var(--bdr);background:#f8fafc;border-radius:999px;padding:4px 9px;font-size:12px;font-weight:600;color:var(--text2);cursor:pointer;line-height:1.2;text-decoration:none;vertical-align:middle;margin-right:4px} .pi-pill:hover,.view-btn:hover{background:#eef2f7;color:var(--text)} .pi-primary{background:#fff7ed;border-color:#fed7aa;color:#9a5a00;font-weight:700} .pi-primary:hover{background:#ffedd5;color:#7c4700} .pi-del{color:#b91c1c;border-color:var(--bdr);background:#f8fafc}.pi-del:hover{background:#eef2f7;color:#991b1b} .pi-disabled{opacity:.42;cursor:default}.pi-disabled:hover{background:#f8fafc;color:var(--text2)} .page-step-pill{gap:6px}.page-step-part{cursor:pointer;white-space:nowrap}.page-step-part.pi-disabled{opacity:.42;cursor:default;pointer-events:none}.page-step-sep{color:#cbd5e1} .presentation-bar .page-step-pill{background:#fff7ed;border-color:#fed7aa;color:#111;margin:0}.presentation-bar .page-step-sep{color:#94a3b8} .presentation-bar{display:none;background:var(--orange);color:#fff;align-items:center;justify-content:flex-start;gap:14px;padding:8px 16px;min-height:40px;box-sizing:border-box;box-shadow:0 1px 2px rgba(0,0,0,0.10);flex-wrap:wrap} .presentation-left,.presentation-right{display:flex;align-items:center;gap:7px;min-width:0} .presentation-left{flex:0 0 auto;justify-content:flex-start;flex-wrap:nowrap;white-space:nowrap}.presentation-right{flex:0 0 auto;justify-content:flex-end;font-size:13px;font-weight:700;flex-wrap:nowrap;white-space:nowrap;margin-left:auto} .presentation-title{flex:1 1 220px;text-align:center;font-size:17px;font-weight:800;line-height:1.22;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;min-width:80px;padding:2px 8px} .presentation-brand{opacity:.95;white-space:nowrap;margin-left:0}.presentation-annotation{display:none!important} .presentation-btn,.presentation-bar .view-btn,.presentation-bar .tt-box-action-menu{border:1px solid rgba(255,255,255,.72);background:rgba(255,255,255,.14);color:#fff;border-radius:999px;padding:4px 10px;font-size:12px;font-weight:700;cursor:pointer;font-family:inherit;line-height:1.2;min-height:24px} .presentation-btn:hover,.presentation-bar .view-btn:hover{background:rgba(255,255,255,.24);color:#fff}.presentation-btn-primary{background:#fff7ed;color:#9a5a00;border:1.5px solid #fed7aa;border-radius:999px;padding:6px 13px;font-size:11.5px;font-weight:700;box-shadow:0 1px 0 rgba(0,0,0,.04)}.presentation-btn-primary:hover{background:#ffedd5;color:#7c4700} .presentation-bar .view-btn,.presentation-bar .tt-box-action-menu{border:1px solid #fed7aa;background:#fff7ed;color:#111} .presentation-bar .view-btn:hover{background:#ffedd5;color:#111} .presentation-bar .tt-box-action-menu:disabled{opacity:.68;cursor:default}.presentation-bar .tt-box-actions-wrap{display:inline-flex;align-items:center;flex:0 0 auto;white-space:nowrap}.presentation-bar .pi-move{display:inline-flex;align-items:center;justify-content:center;min-width:24px;height:24px;border:1px solid #fed7aa;background:#fff7ed;color:#111;border-radius:999px;font-size:13px;margin:0;cursor:pointer;text-decoration:none}.presentation-bar .pi-undo{display:inline-flex;align-items:center;justify-content:center;min-width:24px;height:24px;border:1px solid rgba(255,255,255,.72);background:#fff;color:#111;border-radius:999px;font-size:13px;margin:0;cursor:pointer;text-decoration:none}.presentation-bar .pi-disabled{opacity:.42;cursor:default} .presentation-toggle-normal{display:inline-flex;align-items:center;border-color:rgba(255,255,255,.75);background:rgba(255,255,255,.14);color:#fff;text-decoration:none;vertical-align:middle} .presentation-toggle-normal:hover{background:rgba(255,255,255,.24);color:#fff} .back-btn{display:inline-flex;align-items:center;gap:5px;background:#fff7ed;border:1px solid #fed7aa;border-radius:999px;padding:6px 13px;cursor:pointer;font-size:11.5px;font-weight:700;color:#9a5a00;margin-bottom:16px;transition:background 0.15s}.back-btn:hover{background:#ffedd5;color:#7c4700} body.presentation-view .hdr{display:none!important} body.presentation-view .presentation-bar{display:flex} body.presentation-view .tab-bar{display:none!important} body.presentation-view .ctrl-bar{display:none!important} body.presentation-view .attr{display:none!important} body.presentation-view .page-info-bar{display:none!important} body.presentation-view .tt-bar,body.presentation-view .how-bar,body.presentation-view .doc-bar{display:none!important} @media(max-width:760px){.presentation-bar{align-items:flex-start}.presentation-left{order:1;flex-wrap:wrap;white-space:normal}.presentation-right{order:2;margin-left:auto}.presentation-title{text-align:left;order:3;flex:1 1 100%;max-width:100%;padding-left:0}.presentation-annotation{max-width:140px}}@media(max-width:520px){.presentation-right{margin-left:0;flex-wrap:wrap;justify-content:flex-start}.presentation-left{flex:1 1 100%}.presentation-title{order:2}.presentation-right{order:3}.presentation-brand{display:none}} `; function buildHTML(TITLE) { const safeTitle=escHtml(TITLE||'DoView Board'); // The official badge identifies this official package/source and standards-compliant board structure, not review or endorsement of user content. Forked, substantially modified, rebranded, or non-official builds should remove the generated badge block unless authorised in writing by Dr Paul Duignan or the DoView® trademark owner. return `
Read-Only BoardDoView® Board
Read-only
${safeTitle}
Official DoView® Badge
Standards-Compliant Board Structure
Board infoMeasuresEval QuestionsLinksTagsSearchWalk-ThroughGet trainingPrototype OnlyRead-Only Board
Sources \u00B7 Not saved since changes
DoView® Planning — Dr Paul Duignan — doviewplanning.org · Info for developers · Acknowledgments · Disclaimer ·

Add a new page

This\u2013Then Page
Causal logic: what leads to what
How Page
Actions being taken to make it happen
Documentation Page
Supporting material kept inside the board
Cancel

What type of How Page do you want to create?

Vertical Link How Page
Use this for How Boxes that sit in the main Level 1 / Level 2 / Level 3 structure. Boxes on this page can connect up or down using Vertical Links.
Level 1 How Boxes can link up to This\u2013Then Boxes and down to Level 2 How Boxes.

Level 2 or deeper How Boxes can link up to the level above and down to the level below.
Cross-Link How Page
Use this for How Boxes that are not part of the main vertical level structure. Boxes on this page can connect to other How Boxes or This\u2013Then Boxes using Cross-Links where allowed.
No level is assigned to this page.
Cancel

Chat with board

Configure endpoint to use Board Chat
⚙️ AI Connection Settings
Board Chat is optional. The board works without it. Sending a Board Chat message may send board content to the custom AI endpoint you configure, using the API key or session credential you enter for this session. If privacy, confidentiality, security, compliance, API-key handling, or external data sharing is a concern, do not use Board Chat.
API keys are sensitive, session-only, and never saved in board state or localStorage. Remembered settings apply only to endpoint/model. If you do not want to configure Board Chat, you can still use the board manually in a normal Claude chat or another AI chat by putting the board content into that chat and asking questions there. In enterprise, public-sector, regulated, sensitive, or multi-user deployments, disable or govern Board Chat unless an approved custom AI endpoint, backend or proxy, logging, audit, retention, data-handling, and organisational approvals are in place.
← Back to chat
`; } // ─── GLOBALS (set by init) ─── let TITLE, SLUG, SP, FO_DATA, B={}, DEPS={}, currentPage='overview', openBox=null, entryMode='sofar', editingType=null; let revealBox=null, revealType=null; let inspectHome=null, inspectLinkedKeys=[]; let _clickTimer=null; let sources=[], chatMsgs=[], aiSettings={}, chatSettingsOpen=false, undoStack=[], pendingStructural=[]; // sourcesInitialized tracks whether the Sources list has ever had its default seed applied. The seed (a single { title: "Information on DoView Boards", url: "https://doviewplanning.org/doviewboards" } entry) is applied once on init when sources is empty AND sourcesInitialized is false. After seeding the flag is set true and persisted in saveState, so the user's later edits/removals of the default source are respected and the default is NOT re-added on reload. Defaults to false so older saved boards (which lack the flag) still get the seed once when first opened, and only when they have no sources at that time. The flag is part of the saved state going forward; older boards read it as false by default which is backward-compatible. let sourcesInitialized=false; // pendingStateActions queues state-actions when a chat response also includes structural-actions, so a mixed response is presented as ONE OK/Cancel transaction instead of partially applying state-actions before the user has confirmed. State-only responses still apply directly (preserving existing safe behavior). Cancel rolls back via doUndo() against the snapshot taken before any apply. let pendingStateActions=[]; // the default Source seeded on init when a board has no sources and sourcesInitialized is false. Title is shown in the source title field; URL is shown in the source URL field; the existing Sources popup behavior (Open link, edit, save, remove) handles it from there. (the seed title is updated from "Information on DoView Boards" to "Information about DoView Boards" so the default first source for newly-created boards reads "Information about DoView Boards". The URL is unchanged. The seed-once mechanism (sourcesInitialized flag) is unchanged: existing saved boards' Sources are preserved and not overwritten; the default is never re-seeded onto a board the user has already edited or removed it from.) const DEFAULT_SOURCE={title:'Information about DoView Boards',url:'https://doviewplanning.org/doviewboards'}; const CANONICAL_DISCLAIMER_TITLE='Using DoView Boards and Disclaimer'; const CANONICAL_DISCLAIMER_TEXT=`Using DoView Boards and Disclaimer See the disclaimer section below. The DoView Board prompt and the DoView Board built within this DoView Board prototype app are provided free of charge so that anyone can experiment with DoView Boards and explore for themselves how they can answer the 20 key questions that anyone running or overseeing an organization needs to answer. See [https://doviewplanning.org/doviewboards](https://doviewplanning.org/doviewboards). Walk-Through To get a walk-through for using DoView Boards, see [https://doviewplanning.org/walkthrough](https://doviewplanning.org/walkthrough). Accessing Consulting or Training We can provide consulting or training on integrating the use of DoView Boards into your existing planning, implementation and reporting workflows in business, government, or nonprofit settings. See [https://doviewplanning.org/offerings](https://doviewplanning.org/offerings). Adapting the DoView Board Prototype App to your setting This DoView Board is being used within the DoView Board prototype app. It is a free of charge app that runs inside any browser window and can be used for piloting, testing, proof-of-concept and for use where confidential information is not being included and where there is a low security risk. The DoView Board app is an open-source developer project under an Apache-2.0 open-source license. See [https://github.com/doviewplanning/official-doview-boards](https://github.com/doviewplanning/official-doview-boards). The wider DoView Planning methodology and DoView® trademarks are governed separately. See [https://doviewplanning.org/trademarkuse](https://doviewplanning.org/trademarkuse). Because it is an open-source project, any developer can extend, harden, adapt, or include features of the DoView Board app in other planning products or systems to suit specific deployment requirements. For example, adding enhanced stability, scalability, security, authentication, data handling, or integration features needed for production use, sensitive content, or higher-security environments. If you are looking to deploy DoView Boards in a live operational setting, you can work with any developer to tailor the app to your needs. You can also get in touch with us for advice and methodological input into the best way for you to adapt and extend your use of DoView Boards in your particular setting. [https://doviewplanning.org/contact](https://doviewplanning.org/contact). Disclaimer DoView Boards are provided for planning and illustrative purposes only. The content of any DoView Board does not constitute professional advice of any kind, including but not limited to legal, financial, medical, strategic, or organizational advice. No warranty is given as to the accuracy, completeness, or fitness for purpose of any content. The usual precautions for AI-generated material need to be taken. Dr Paul Duignan, DoViewPlanning.org, The Ideas Web Ltd, DoView Corporation Ltd and any associated parties accept no liability whatsoever for any loss, damage, or adverse outcome arising directly or indirectly from the use of, or reliance on, any DoView Board or its contents. Use entirely at your own risk. Please note that this DoView Board is in an interactive HTML file and contains active JavaScript so the board can work. Only open boards from sources you trust. Read-only copies disable editing through the board interface but are not security protection.`; let boardInfo='', pageInfo={}, docContent={}, createdDate='', boardInstanceId=''; // Additive builder metadata for traceability. Older and directly embedded boards omit it and continue to open normally. let builderValidation=null; function builderValidationStatusText(){return builderValidation&&builderValidation.passed===true?'Builder validation: confirmed':'Builder validation: not confirmed';} function builderValidationForExport(){return builderValidation&&typeof builderValidation==='object'?JSON.parse(JSON.stringify(builderValidation)):undefined;} // boardUid is the additive stable unique ID for the board itself, separate from the existing boardInstanceId. Generated once on init() if missing, persists through save / load / Save HTML Board / Copy HTML Board / Create Read-Only Copy / localStorage / DOVIEW-STATE snapshots, and does NOT change when the board is renamed, edited, or copied. Read-only copies preserve the same boardUid as their source (least-disruptive choice). Defaults to '' for older boards; _backfillUids assigns one on first load if missing. let boardUid=''; // board-level Top right text — a short plain-text annotation/disclaimer (e.g. Draft, Illustrative only, Confidential) shown right-justified in the page-info bar across all pages. Defaults to '' for backward compatibility with older saved boards. Plain text only; no rich text; no links. In read-only copies the text is visible but not editable. Persisted with the board via savedState/EMBEDDED_STATE/localStorage and included in DOVIEW-STATE snapshots. let topRightText=''; // board-level display mode. Defaults to Normal View for older boards; persisted as a small additive saved-state field. let presentationView=false; let ttLinks=[], ttLinkNextId=1; let docEditMode={}, docEditBackup={}; let howLinks=[], howLinkNextId=1; let runtimeLinkIndex=null, runtimeLinkIndexDirty=true; let measures=[], measureNextId=1; let evalQuestions=[], eqNextId=1; // board-level user-defined visual tags registry. Tags apply through tagIds references; the editable link-tag UI is limited to This–Then links in this build. let tags=[]; let tagManagerDraft=null; let registryShowTags={measures:true,eq:true,links:true}; // Overview navigation uses a stop-propagating wrapper so selected-box inspect context is not cleared by the Overview board click handler before page navigation. // Overview display preference is session-local only to avoid saved-state churn. let overviewMode='cards'; // transient Overview reorder mode. Session-local only; no saved-state/schema change. let overviewReorderMode=false; // initial viewSettings for newly-created boards now match the simple cleared/all-false Page View state (the same state as pressing Clear all, including tags hidden). Existing saved boards continue to load with their saved viewSettings unchanged \u2014 the savedState restore path uses Object.assign over these initial values, so saved settings always win. The View menu Clear all and Default options remain available with their existing meaning. New pages of a given page type continue to follow the currently operating viewSettings for that page type. The separate Final Outcomes View settings behavior is preserved. let viewSettings={thisThen:{showCounts:false,showTrafficLights:false,showPriorities:false,showHowCounts:false,showMeasures:false,showEvalQuestions:false,showMainText:false,showFullMainText:false,showMainTextCodeStyle:false,showLateralHow:false,showTags:false,showLinkInfoOnHover:false,showLinkInfoCodeStyle:false},how:{showNumbering:false,showTrafficLights:false,showPriorities:false,showWhyCounts:false,showLateralHow:false,showMeasures:false,showEvalQuestions:false,showMainText:false,showFullMainText:false,showMainTextCodeStyle:false,showTags:false},finalOutcomes:{showTrafficLights:false,showPriorities:false,showMeasures:false,showEvalQuestions:false,showMainText:false,showFullMainText:false,showMainTextCodeStyle:false,showTags:false},overview:{showTrafficLights:false,showPriorities:false,showTags:false,showCardColorControls:false}}; // INITIAL_NEW_BOARD_VIEW_SETTINGS is the simple cleared/all-false Page View state used to initialise viewSettings for newly-created boards (and used by resetToStarterBoard). Kept separate from DEFAULT_VIEW_SETTINGS \u2014 DEFAULT_VIEW_SETTINGS is the working defaults that the View menu's Default button still restores. Both constants exist deliberately: Clear all == INITIAL_NEW_BOARD_VIEW_SETTINGS (everything off), Default == DEFAULT_VIEW_SETTINGS (working defaults with Traffic Lights / priorities / etc. on). const INITIAL_NEW_BOARD_VIEW_SETTINGS={thisThen:{showCounts:false,showTrafficLights:false,showPriorities:false,showHowCounts:false,showMeasures:false,showEvalQuestions:false,showMainText:false,showFullMainText:false,showMainTextCodeStyle:false,showLateralHow:false,showTags:false,showLinkInfoOnHover:false,showLinkInfoCodeStyle:false},how:{showNumbering:false,showTrafficLights:false,showPriorities:false,showWhyCounts:false,showLateralHow:false,showMeasures:false,showEvalQuestions:false,showMainText:false,showFullMainText:false,showMainTextCodeStyle:false,showTags:false},finalOutcomes:{showTrafficLights:false,showPriorities:false,showMeasures:false,showEvalQuestions:false,showMainText:false,showFullMainText:false,showMainTextCodeStyle:false,showTags:false},overview:{showTrafficLights:false,showPriorities:false,showTags:false,showCardColorControls:false}}; // defaults used by Restore Defaults // Restore Defaults: How-page Vertical Link counts (showWhyCounts) are ON by default; TT-page Vertical Link counts (showHowCounts) remain ON by default; TT-page This\u2013Then Link counts (showCounts) remain OFF by default; both page types have Cross-Links (showLateralHow) OFF by default; Measures, Evaluation Questions, and Display Text under Boxes remain OFF by default. The TT-page Vertical Link toggle reads "Show Vertical Link counts from How Boxes (to check alignment)" and the TT-page Cross-Link toggle reads "Show Cross-Links to How Boxes"; the How-page Vertical Link toggle reads "Show Vertical Link counts to This\u2013Then Boxes or How Boxes (to check alignment)" and the How-page Cross-Link toggle reads "Show Cross-Links to This\u2013Then Boxes or How Boxes". (DEFAULT_VIEW_SETTINGS is unchanged \u2014 it is still the working defaults that the Default button restores. The initial viewSettings for newly-created boards now uses INITIAL_NEW_BOARD_VIEW_SETTINGS (above) instead of DEFAULT_VIEW_SETTINGS, so new boards open with everything cleared.) const DEFAULT_VIEW_SETTINGS={thisThen:{showCounts:false,showTrafficLights:true,showPriorities:true,showHowCounts:true,showMeasures:false,showEvalQuestions:false,showMainText:false,showFullMainText:false,showMainTextCodeStyle:false,showLateralHow:false,showTags:true,showLinkInfoOnHover:false,showLinkInfoCodeStyle:false},how:{showNumbering:true,showTrafficLights:true,showPriorities:true,showWhyCounts:true,showLateralHow:false,showMeasures:false,showEvalQuestions:false,showMainText:false,showFullMainText:false,showMainTextCodeStyle:false,showTags:true},finalOutcomes:{showTrafficLights:true,showPriorities:true,showMeasures:false,showEvalQuestions:false,showMainText:false,showFullMainText:false,showMainTextCodeStyle:false,showTags:true},overview:{showTrafficLights:false,showPriorities:false,showTags:false,showCardColorControls:false}}; // temporary source-reference box state — transient, not persisted let sourceRefBox=null; // lightweight save-state tracking for the bottom-right status area. Tracks whether the in-browser board has unsaved edits since the last explicit HTML save/export/copy, the time of the last successful HTML save/export/copy, and whether a save is currently in progress. These are transient (not persisted) — after reload the indicator starts from a clean baseline. Edits set isDirty=true via saveState(); a successful current-board HTML save/export/copy clears it and stamps lastSavedTime. let lastSavedTime=null, isDirty=false, isSaving=false, unsavedWarningInstalled=false; let chosenSaveFileHandle=null; // page-navigation history stack for the back/forward arrows in the page info bar. // These are browser-style navigation arrows only; they do not affect page order. Page reordering is a // separate control ("Change page order"). The history is transient and is not persisted with the board. let navHistory=[], navFuture=[]; const MAX_NAV_HIST=30; const MAX_UNDO=20; const COL_COLORS=[{bg:'#dbeafe',bdr:'#93c5fd',tab:'#3b82f6'},{bg:'#f3e8ff',bdr:'#d8b4fe',tab:'#a855f8'},{bg:'#ccfbf1',bdr:'#5eead4',tab:'#0d9488'},{bg:'#fef3c7',bdr:'#fde68a',tab:'#f59e0b'},{bg:'#fce7f3',bdr:'#f9a8d4',tab:'#db2777'},{bg:'#dcfce7',bdr:'#86efac',tab:'#16a34a'},{bg:'#e0e7ff',bdr:'#a5b4fc',tab:'#6366f1'},{bg:'#f1f5f9',bdr:'#cbd5e1',tab:'#64748b'},{bg:'#ffe4e6',bdr:'#fecdd3',tab:'#e11d48'},{bg:'#cffafe',bdr:'#67e8f9',tab:'#0891b2'}]; const DEFAULT_COLOR={bg:'#f1f5f9',bdr:'#cbd5e1',tab:'#64748b'}; const PAGE_TYPE_COLORS={this_then:'#F5A623',how:'#60a5fa',documentation:'#8b5cf6'}; function sanitizeColorValue(value,fallback){var fb=(fallback===undefined||fallback===null)?'':String(fallback);if(value===null||value===undefined)return fb;var s=String(value).trim();if(!s)return fb;var low=s.toLowerCase();if(/[;"'<>\\]/.test(s)||low.indexOf('url(')>=0||low.indexOf('expression(')>=0||low.indexOf('@import')>=0||low.indexOf('javascript:')>=0||low.indexOf('data:')>=0)return fb;if(/^#(?:[0-9a-f]{3}|[0-9a-f]{4}|[0-9a-f]{6}|[0-9a-f]{8})$/i.test(s))return s;if(/^[a-zA-Z][a-zA-Z0-9-]{0,31}$/.test(s))return s;if(/^(?:rgb|rgba|hsl|hsla)\(\s*[-+0-9.%,\s/]+\)$/i.test(s))return s;return fb;} function sanitizeOptionalColorValue(value){return sanitizeColorValue(value,'');} function sanitizeColorObject(value,fallback){var fb=fallback||DEFAULT_COLOR;var v=(value&&typeof value==='object')?value:{};return {bg:sanitizeColorValue(v.bg,fb.bg),bdr:sanitizeColorValue(v.bdr,fb.bdr),tab:sanitizeColorValue(v.tab,fb.tab)};} function sanitizeCustomColorsInMemory(){if(typeof SP!=='undefined'&&Array.isArray(SP)){SP.forEach(function(sp){if(!sp)return;if(sp.color)sp.color=sanitizeColorObject(sp.color,DEFAULT_COLOR);if(sp.overviewCard&&typeof sp.overviewCard.boxColor==='string')sp.overviewCard.boxColor=sanitizeOptionalColorValue(sp.overviewCard.boxColor);});}if(typeof B!=='undefined'&&B){Object.keys(B).forEach(function(k){var b=B[k];if(!b)return;b.borderColor=sanitizeOptionalColorValue(b.borderColor);b.boxColor=sanitizeOptionalColorValue(b.boxColor);});}} function escapeScriptJson(json){return String(json).replace(//g,'\\u003E').replace(/&/g,'\\u0026').replace(/\u2028/g,'\\u2028').replace(/\u2029/g,'\\u2029');} function standaloneBoardTitleHtmlFor(title,suffix){return escHtml(String(title||'DoView Board')+(suffix||''));} function standaloneBoardTitleHtml(suffix){return standaloneBoardTitleHtmlFor(TITLE,suffix);} function defaultNewBoardTitle(){return 'Untitled DoView Board';} function defaultNewBoardSlug(){return 'untitled_doview_board_'+Date.now().toString(36);} function cleanNewBoardTitleInput(value){var title=String(value||'').trim().replace(/\.html$/i,'').trim();return title||defaultNewBoardTitle();} function slugFromBoardName(value){var slug=String(value||'').toLowerCase().replace(/[^a-z0-9]+/g,'_').replace(/^_+|_+$/g,'');return slug;} function timestampedBoardFilename(slug){var d=new Date();var ds=d.getFullYear()+'-'+String(d.getMonth()+1).padStart(2,'0')+'-'+String(d.getDate()).padStart(2,'0');var hr=d.getHours();var mn=String(d.getMinutes()).padStart(2,'0');var ampm=hr>=12?'pm':'am';var hr12=hr%12||12;return String(slug||defaultNewBoardSlug())+'_'+ds+'_'+hr12+'-'+mn+ampm+'.html';} function spColor(sp){return sanitizeColorObject(sp&&sp.color,DEFAULT_COLOR);} function _paletteBgKey(bg){return String(bg||'').trim().toLowerCase();} function _paletteColorForBg(bg){var key=_paletteBgKey(bg);return COL_COLORS.find(function(c){return _paletteBgKey(c.bg)===key;})||null;} function _noteUsedPaletteBg(used,bg){var c=_paletteColorForBg(bg);if(c)used[_paletteBgKey(c.bg)]=true;} function _usedPageCardPaletteBgs(){var used={};(SP||[]).forEach(function(sp){if(!sp)return;_noteUsedPaletteBg(used,spColor(sp).bg);if(sp.overviewCard&&sp.overviewCard.boxColor)_noteUsedPaletteBg(used,sanitizeOptionalColorValue(sp.overviewCard.boxColor));});return used;} function nextAutoPageColor(){var used=_usedPageCardPaletteBgs();var avail=COL_COLORS.find(function(c){return!used[_paletteBgKey(c.bg)];});if(avail)return {bg:avail.bg,bdr:avail.bdr,tab:avail.tab};var idx=((SP&&SP.length)||0)%COL_COLORS.length;var c=COL_COLORS[idx]||DEFAULT_COLOR;return {bg:c.bg,bdr:c.bdr,tab:c.tab};} function ensureOverviewViewSettings(){if(!viewSettings.overview)viewSettings.overview={};var d=INITIAL_NEW_BOARD_VIEW_SETTINGS.overview;Object.keys(d).forEach(function(k){if(typeof viewSettings.overview[k]!=='boolean')viewSettings.overview[k]=d[k];});return viewSettings.overview;} function findPageById(pid){return (SP||[]).find(function(s){return s&&s.id===pid;});} function nextUnusedPageId(extraUsed){var used={};(SP||[]).forEach(function(s){if(s&&s.id)used[s.id]=true;});if(extraUsed){Object.keys(extraUsed).forEach(function(id){if(id)used[id]=true;});}var n=((SP&&SP.length)||0)+1;while(used['p'+n])n++;return 'p'+n;} function hasOverviewPage(){return true;} function isCurrentPageSubpage(){return !!findPageById(currentPage);} function currentPageSupportsPageView(){if(currentPage==='overview'||currentPage==='final')return true;var sp=findPageById(currentPage);if(!sp)return false;var pt=sp.pageType||'this_then';return pt==='this_then'||pt==='how';} function displayIdKey(value){return String(value||'').trim().toLowerCase();} function visibleDisplayId(value,fallback){var v=String(value||'').trim();return v||String(fallback||'').trim();} function findHowBoxRecordByKey(key){var m=String(key||'').match(/^(p\d+)-(H\d+)$/);if(!m)return null;var sp=findPageById(m[1]);if(!sp||sp.pageType!=='how'||!Array.isArray(sp.howBoxes))return null;var hb=sp.howBoxes.find(function(x){return x&&x.id===m[2];});return hb?{sp:sp,hb:hb}:null;} function howBoxDisplayId(hb){return visibleDisplayId(hb&&hb.displayId,hb&&hb.id);} function howBoxDisplayIdForKey(key){var rec=findHowBoxRecordByKey(key);return rec?howBoxDisplayId(rec.hb):'';} function duplicateHowDisplayId(sp,hb,nextId){var want=displayIdKey(nextId);return !!(sp&&Array.isArray(sp.howBoxes)&&want&&sp.howBoxes.some(function(other){return other&&other!==hb&&displayIdKey(howBoxDisplayId(other))===want;}));} function measureDisplayId(m){return visibleDisplayId(m&&m.displayId,displayMeasureId(m&&m.id));} function eqDisplayId(q){return visibleDisplayId(q&&q.displayId,displayEQId(q&&q.id));} function duplicateMeasureDisplayId(m,nextId){var want=displayIdKey(nextId);return !!(want&&measures.some(function(other){return other&&other!==m&&displayIdKey(measureDisplayId(other))===want;}));} function duplicateEQDisplayId(q,nextId){var want=displayIdKey(nextId);return !!(want&&evalQuestions.some(function(other){return other&&other!==q&&displayIdKey(eqDisplayId(other))===want;}));} function shouldShowBackToOverview(){return hasOverviewPage()&¤tPage!=='overview';} function backToOverviewButtonHTML(){return (!presentationView&&shouldShowBackToOverview())?'':'';} function presentationBackToOverviewHTML(){return shouldShowBackToOverview()?'':'';} function canonicalDisclaimerInlineHtml(text){var safe=escHtml(text||'');return safe.replace(/\[([^\]]+)\]\((https?:\/\/[^)]+)\)/g,function(match,label,url){var href=_safeUrl(url);if(!href)return label;return ''+label+'';});} function renderCanonicalDisclaimerHtml(includeTitle){var sectionHeads={'Walk-Through':true,'Accessing Consulting or Training':true,'Adapting the DoView Board Prototype App to your setting':true,Disclaimer:true};return CANONICAL_DISCLAIMER_TEXT.split(/\n{2,}/).map(function(block){var text=block.trim();if(!text)return '';var html=canonicalDisclaimerInlineHtml(text).replace(/\n/g,'
');if(text===CANONICAL_DISCLAIMER_TITLE){return includeTitle?'

'+html+'

':'';}if(sectionHeads[text])return '

'+html+'

';return '

'+html+'

';}).join('');} function overviewDefaultCardColor(sp){if(!sp)return DEFAULT_COLOR;var pt=sp.pageType||'this_then';if(pt==='this_then')return spColor(sp);var base=spColor(sp);return {bg:'#fff',bdr:'#d1d5db',tab:base.tab||DEFAULT_COLOR.tab};} function overviewCardBorderFor(bg,def){var safeBg=sanitizeColorValue(bg,'');var key=String(safeBg||'').toLowerCase();var match=COL_COLORS.find(function(c){return c.bg.toLowerCase()===key;});return match?match.bdr:sanitizeColorValue((def&&def.bdr),DEFAULT_COLOR.bdr);} const PRIORITY_VALUES=['A','B','C','D','E','BAU']; function normalizePriorityValue(value){var p=(value===null||value===undefined)?'':String(value).trim().toUpperCase();return PRIORITY_VALUES.indexOf(p)>=0?p:'';} function serializedPriority(value){return normalizePriorityValue(value);} function finalOutcomeLabelValue(value){if(typeof value==='string')return value;if(value&&typeof value==='object'&&!Array.isArray(value)&&typeof value.label==='string')return value.label;if(value===null||value===undefined)return '';if(typeof value==='object')return '';return String(value);} function normalizedFinalOutcomes(list){return Array.isArray(list)?list.map(finalOutcomeLabelValue):[];} function normalizeFinalOutcomesInMemory(){if(!Array.isArray(FO_DATA))FO_DATA=[];FO_DATA=normalizedFinalOutcomes(FO_DATA);if(typeof B!=='undefined'&&B){FO_DATA.forEach(function(label,i){var b=B['final-b'+i];if(b){var boxLabel=finalOutcomeLabelValue(b.label);b.label=boxLabel||label;}});}} function normalizePrioritiesInMemory(){if(typeof B!=='undefined'&&B){Object.keys(B).forEach(function(k){if(B[k])B[k].priority=normalizePriorityValue(B[k].priority);});}if(typeof SP!=='undefined'&&Array.isArray(SP)){SP.forEach(function(sp){if(sp&&sp.overviewCard)sp.overviewCard.priority=normalizePriorityValue(sp.overviewCard.priority);});}} function normalizeTrafficLightsInMemory(){if(typeof B!=='undefined'&&B){Object.keys(B).forEach(function(k){if(B[k])B[k].light=normalizeTrafficLightValue(B[k].light);});}if(typeof ttLinks!=='undefined'&&Array.isArray(ttLinks)){ttLinks.forEach(function(l){if(l)l.light=normalizeTrafficLightValue(l.light);});}if(typeof measures!=='undefined'&&Array.isArray(measures)){measures.forEach(function(m){if(m&&m.trafficLight!==undefined)m.trafficLight=normalizeTrafficLightValue(m.trafficLight);});}if(typeof evalQuestions!=='undefined'&&Array.isArray(evalQuestions)){evalQuestions.forEach(function(q){if(q&&q.trafficLight!==undefined)q.trafficLight=normalizeTrafficLightValue(q.trafficLight);});}} function ensureOverviewCardMeta(sp){if(!sp)return {light:'grey',priority:'',tagIds:[],boxColor:''};if(!sp.overviewCard)sp.overviewCard={};var m=sp.overviewCard;if(!m.light)m.light='grey';m.priority=normalizePriorityValue(m.priority);if(!Array.isArray(m.tagIds))m.tagIds=[];if(typeof m.boxColor!=='string')m.boxColor='';return m;} function overviewCardMeta(sp){var m=(sp&&sp.overviewCard)||{};return {light:m.light||'grey',priority:normalizePriorityValue(m.priority),tagIds:Array.isArray(m.tagIds)?m.tagIds:[],boxColor:typeof m.boxColor==='string'?sanitizeOptionalColorValue(m.boxColor):''};} function overviewCardColor(sp){var def=overviewDefaultCardColor(sp);var m=overviewCardMeta(sp);if(m.boxColor)return {bg:m.boxColor,bdr:overviewCardBorderFor(m.boxColor,def),tab:def.tab};return def;} const PRI_COLORS={A:'#dc2626',B:'#ea580c',C:'#ca8a04',D:'#2563eb',E:'#4f46e5',BAU:'#334155'}; const STRUCTURAL_CMDS=['addBox','removeBox','renameBox','addColumn','removeColumn','renameColumn','moveColumn','addSubpage','removeSubpage','addFinalOutcome','removeFinalOutcome','deleteMeasure','deleteEQ']; // SUPPORTED_CMDS is the source of truth used by _validateActionSet to reject unsupported action names clearly. Keep it in sync with execAction. const SUPPORTED_CMDS=['setLight','setAllLights','setSubpageLights','addSofar','addNote','addNote1','addNote2','addNote3','setPriority','clearEntries','setDetail','clearDetail','setBoxBorder','clearBoxBorder','setSubpageBorders','clearSubpageBorders','setBoardInfo','setPageInfo','addBox','removeBox','renameBox','addColumn','removeColumn','renameColumn','moveColumn','addSubpage','removeSubpage','addFinalOutcome','removeFinalOutcome','addTTLink','removeTTLink','flipTTLink','addHowLink','removeHowLink','createMeasure','createEQ','linkMeasure','unlinkMeasure','linkEQ','unlinkEQ','deleteMeasure','deleteEQ']; // Minimum argument counts for each supported command, including the command name in parts. const CMD_MIN_PARTS={setLight:3,setAllLights:2,setSubpageLights:3,addSofar:3,addNote:3,addNote1:3,addNote2:3,addNote3:3,setPriority:3,clearEntries:3,setDetail:3,clearDetail:2,setBoxBorder:3,clearBoxBorder:2,setSubpageBorders:3,clearSubpageBorders:2,setBoardInfo:2,setPageInfo:3,addBox:4,removeBox:2,renameBox:3,addColumn:4,removeColumn:3,renameColumn:4,moveColumn:4,addSubpage:2,removeSubpage:2,addFinalOutcome:2,removeFinalOutcome:2,addTTLink:3,removeTTLink:2,flipTTLink:2,addHowLink:3,removeHowLink:2,createMeasure:2,createEQ:2,linkMeasure:3,unlinkMeasure:3,linkEQ:3,unlinkEQ:3,deleteMeasure:2,deleteEQ:2}; // ─── CORE ─── // _genUid returns a stable unique ID with a short type prefix and no network dependency. function _genUid(prefix){var p=prefix||'id';var body;try{if(typeof crypto!=='undefined'&&crypto&&typeof crypto.randomUUID==='function'){body=crypto.randomUUID();}else{body=Date.now().toString(36)+'_'+Math.random().toString(36).slice(2,10);}}catch(_e){body=Date.now().toString(36)+'_'+Math.random().toString(36).slice(2,10);}return p+'_'+body;} // _backfillUids assigns missing stable IDs additively and preserves existing IDs. function _backfillUids(){if(!boardUid)boardUid=_genUid('board');if(typeof SP!=='undefined'&&Array.isArray(SP)){SP.forEach(function(sp){if(!sp)return;if(!sp.uid)sp.uid=_genUid('page');if(sp.overviewCard&&!Array.isArray(sp.overviewCard.tagIds))sp.overviewCard.tagIds=[];if(Array.isArray(sp.cols)){sp.cols.forEach(function(col){if(col&&!col.uid)col.uid=_genUid('col');});}if(Array.isArray(sp.howBoxes)){sp.howBoxes.forEach(function(hb){if(hb&&!hb.uid)hb.uid=_genUid('box');});}});}if(typeof B!=='undefined'&&B){Object.keys(B).forEach(function(k){var b=B[k];if(!b)return;if(!b.uid){b.uid=k.indexOf('final-')===0?_genUid('final'):_genUid('box');}if(!Array.isArray(b.tagIds))b.tagIds=[];});}if(typeof tags!=='undefined'&&Array.isArray(tags)){tags.forEach(function(t){if(t&&!t.id)t.id=_genUid('tag');});}if(typeof ttLinks!=='undefined'&&Array.isArray(ttLinks)){ttLinks.forEach(function(l){if(l&&!l.uid)l.uid=_genUid('link');});}if(typeof howLinks!=='undefined'&&Array.isArray(howLinks)){howLinks.forEach(function(l){if(l&&!l.uid)l.uid=_genUid('link');});}if(typeof measures!=='undefined'&&Array.isArray(measures)){measures.forEach(function(m){if(m&&!m.uid)m.uid=_genUid('measure');});}if(typeof evalQuestions!=='undefined'&&Array.isArray(evalQuestions)){evalQuestions.forEach(function(q){if(q&&!q.uid)q.uid=_genUid('eq');});}} function buildInitialState(){normalizeFinalOutcomesInMemory();SP.forEach(sp=>{if(!sp.uid)sp.uid=_genUid('page');if(Array.isArray(sp.cols)){sp.cols.forEach(function(col){if(!col.uid)col.uid=_genUid('col');});}sp.cols.forEach((col,ci)=>{col.boxes.forEach((label,bi)=>{B[`${sp.id}-c${ci}-b${bi}`]={label,light:'',entries:[],priority:'',hasSubpage:false,detailText:'',borderColor:'',boxColor:'',measures:[],evalQuestions:[],tagIds:[],uid:_genUid('box')};});});if(sp.howBoxes){sp.howBoxes.forEach(hb=>{if(!hb.uid)hb.uid=_genUid('box');const k=`${sp.id}-${hb.id}`;B[k]={label:hb.label,light:'',entries:[],priority:'',hasSubpage:false,detailText:'',borderColor:'',boxColor:'',tagIds:[],uid:hb.uid};});}});FO_DATA.forEach((label,i)=>{B[`final-b${i}`]={label:label,light:'',entries:[],priority:'',hasSubpage:false,detailText:'',borderColor:'',boxColor:'',measures:[],evalQuestions:[],tagIds:[],uid:_genUid('final')};});} function buildDepsMap(){DEPS={};SP.forEach(sp=>{sp.cols.forEach((col,ci)=>{if(ci===0)return;col.boxes.forEach((_,bi)=>{const k=`${sp.id}-c${ci}-b${bi}`;DEPS[k]=sp.cols[ci-1].boxes.map((_,pbi)=>`${sp.id}-c${ci-1}-b${pbi}`);});});});} function cascade(){return;} function markRuntimeLinkIndexDirty(){runtimeLinkIndexDirty=true;} function _idxNewMap(){return Object.create(null);} function _idxPush(map,key,item){if(!key)return;(map[key]||(map[key]=[])).push(item);} function _idxPair(a,b){return String(a)+'\u0001'+String(b);} function rebuildRuntimeLinkIndex(){var idx={pageById:_idxNewMap(),pageByKey:_idxNewMap(),boxTypeByKey:_idxNewMap(),howLevelByKey:_idxNewMap(),visibleKeysByPage:_idxNewMap(),howRecordByKey:_idxNewMap(),ttByFrom:_idxNewMap(),ttByTo:_idxNewMap(),ttByTouch:_idxNewMap(),ttByPair:_idxNewMap(),ttById:_idxNewMap(),howByFrom:_idxNewMap(),howByTo:_idxNewMap(),howByTouch:_idxNewMap(),howByPair:_idxNewMap(),howById:_idxNewMap(),ttLen:Array.isArray(ttLinks)?ttLinks.length:0,howLen:Array.isArray(howLinks)?howLinks.length:0,ttRef:ttLinks,howRef:howLinks};var pages=Array.isArray(SP)?SP:[];pages.forEach(function(sp){if(!sp||!sp.id)return;idx.pageById[sp.id]=sp;if(!idx.visibleKeysByPage[sp.id])idx.visibleKeysByPage[sp.id]=[];var pt=sp.pageType||'this_then';if(pt==='how'&&Array.isArray(sp.howBoxes)){sp.howBoxes.forEach(function(hb){if(!hb||!hb.id)return;var k=sp.id+'-'+hb.id;idx.pageByKey[k]=sp;idx.boxTypeByKey[k]='how';idx.howLevelByKey[k]=(sp.howLevel!==undefined)?sp.howLevel:null;idx.howRecordByKey[k]={sp:sp,hb:hb};if(B&&B[k])idx.visibleKeysByPage[sp.id].push(k);});}else if(pt==='this_then'&&Array.isArray(sp.cols)){sp.cols.forEach(function(col,ci){(col.boxes||[]).forEach(function(_,bi){var k=sp.id+'-c'+ci+'-b'+bi;idx.pageByKey[k]=sp;idx.boxTypeByKey[k]='this_then';if(B&&B[k])idx.visibleKeysByPage[sp.id].push(k);});});}});idx.visibleKeysByPage.final=[];if(Array.isArray(FO_DATA)){FO_DATA.forEach(function(_,i){var k='final-b'+i;idx.boxTypeByKey[k]='final';if(B&&B[k])idx.visibleKeysByPage.final.push(k);});}(Array.isArray(ttLinks)?ttLinks:[]).forEach(function(l){if(!l||!l.from||!l.to)return;_idxPush(idx.ttByFrom,l.from,l);_idxPush(idx.ttByTo,l.to,l);_idxPush(idx.ttByTouch,l.from,l);_idxPush(idx.ttByTouch,l.to,l);idx.ttByPair[_idxPair(l.from,l.to)]=l;if(l.id)idx.ttById[l.id]=l;});(Array.isArray(howLinks)?howLinks:[]).forEach(function(l){if(!l||!l.from||!l.to)return;_idxPush(idx.howByFrom,l.from,l);_idxPush(idx.howByTo,l.to,l);_idxPush(idx.howByTouch,l.from,l);_idxPush(idx.howByTouch,l.to,l);idx.howByPair[_idxPair(l.from,l.to)]=l;if(l.id)idx.howById[l.id]=l;});runtimeLinkIndex=idx;runtimeLinkIndexDirty=false;return idx;} function getRuntimeLinkIndex(){if(runtimeLinkIndex&&!runtimeLinkIndexDirty&&runtimeLinkIndex.ttRef===ttLinks&&runtimeLinkIndex.howRef===howLinks&&runtimeLinkIndex.ttLen===(Array.isArray(ttLinks)?ttLinks.length:0)&&runtimeLinkIndex.howLen===(Array.isArray(howLinks)?howLinks.length:0))return runtimeLinkIndex;return rebuildRuntimeLinkIndex();} function _idxList(map,key){return getRuntimeLinkIndex()[map][key]||[];} function getTTLinksFrom(key){return _idxList('ttByFrom',key);} function getTTLinksTo(key){return _idxList('ttByTo',key);} function getTTLinksTouching(key){return _idxList('ttByTouch',key);} function getHowLinksFrom(key){return _idxList('howByFrom',key);} function getHowLinksTo(key){return _idxList('howByTo',key);} function getHowLinksTouching(key){return _idxList('howByTouch',key);} function getHowLinkExact(from,to){return getRuntimeLinkIndex().howByPair[_idxPair(from,to)]||null;} function _pushUniqueKey(list,key){if(key&&list.indexOf(key)<0)list.push(key);} function linkedKeysForSource(sourceKey){var out=[];if(!sourceKey||!B[sourceKey])return out;if(isTTBox(sourceKey)){getTTLinksFrom(sourceKey).forEach(function(l){_pushUniqueKey(out,l.to);});getTTLinksTo(sourceKey).forEach(function(l){_pushUniqueKey(out,l.from);});getHowLinksTo(sourceKey).forEach(function(l){_pushUniqueKey(out,l.from);});}else if(isHowBox(sourceKey)){getHowLinksFrom(sourceKey).forEach(function(l){_pushUniqueKey(out,l.to);});getHowLinksTo(sourceKey).forEach(function(l){_pushUniqueKey(out,l.from);});}return out;} function parseTTBoxKey(key){var m=String(key||'').match(/^(.+)-c(\d+)-b(\d+)$/);return m?{spId:m[1],ci:parseInt(m[2],10),bi:parseInt(m[3],10)}:null;} function parseHowBoxKey(key){var m=String(key||'').match(/^(.+)-(H\d+)$/);return m?{spId:m[1],howId:m[2]}:null;} function saveState(){ // Read-only copies do not persist edits through the board interface. if(IS_READONLY)return; _backfillUids();ensureTagIdsOnSupportedElements(); normalizePrioritiesInMemory();normalizeTrafficLightsInMemory();normalizeFinalOutcomesInMemory();normalizeBoxMEQRefsInMemory();sanitizeCustomColorsInMemory(); try{const cleanB={};Object.entries(B).forEach(([k,v])=>{cleanB[k]={label:v.label,light:v.light,entries:[...v.entries],priority:serializedPriority(v.priority),hasSubpage:v.hasSubpage,detailText:v.detailText||'',borderColor:v.borderColor||'',boxColor:v.boxColor||'',measures:[...(v.measures||[])],evalQuestions:[...(v.evalQuestions||[])],tagIds:[...(v.tagIds||[])],jumpToPage:v.jumpToPage||'',/* persist box uid + optional provenance fields so save/load is uid-stable. */uid:v.uid||'',sourceUid:v.sourceUid||'',sourceBoardUid:v.sourceBoardUid||'',sourceBoardTitle:v.sourceBoardTitle||'',sourceObjectType:v.sourceObjectType||'',sourceObjectLabelAtCopy:v.sourceObjectLabelAtCopy||'',copiedAt:v.copiedAt||''};});const state={B:cleanB,SP:JSON.parse(JSON.stringify(SP)),FO:[...FO_DATA],boardInfo:boardInfo,pageInfo:JSON.parse(JSON.stringify(pageInfo)),docContent:sanitizedDocContentState(),createdDate:createdDate,boardInstanceId:boardInstanceId,/* persist boardUid alongside boardInstanceId. boardUid is the additive stable unique board ID; boardInstanceId continues to play its existing role unchanged. */boardUid:boardUid||'',ttLinks:JSON.parse(JSON.stringify(ttLinks)),ttLinkNextId:ttLinkNextId,howLinks:JSON.parse(JSON.stringify(howLinks)),howLinkNextId:howLinkNextId,measures:JSON.parse(JSON.stringify(measures)),measureNextId:measureNextId,evalQuestions:JSON.parse(JSON.stringify(evalQuestions)),eqNextId:eqNextId,tags:JSON.parse(JSON.stringify(tags||[])),viewSettings:JSON.parse(JSON.stringify(viewSettings)),topRightText:topRightText||'',presentationView:!!presentationView,/* persist sourcesInitialized so that a user who removed the default source is not re-seeded on reload. */sourcesInitialized:!!sourcesInitialized};localStorage.setItem(`doview-${SLUG}-v1`,JSON.stringify(state));}catch(e){} // Browser-local state is separate from explicit HTML save/export/copy flows. isDirty=true;updateSaveStateDisplay(); } function markBoardCleanAfterExport(){isSaving=false;isDirty=false;lastSavedTime=Date.now();updateSaveStateDisplay();} function resetDirtyTrackingAfterLoad(){isSaving=false;isDirty=false;lastSavedTime=EMBEDDED_STATE?Date.now():null;updateSaveStateDisplay();} function installUnsavedChangesWarning(){if(unsavedWarningInstalled||typeof window==='undefined')return;unsavedWarningInstalled=true;window.addEventListener('beforeunload',function(e){if(IS_READONLY||!isDirty)return;e.preventDefault();e.returnValue='';return '';});} // ─── LINKIFY ─── /* Low-level linkification helper for text that is already HTML-safe. Dangerous schemes are not matched. */ function linkify(text){return text.replace(/(https?:\/\/[^\s<>"')\]]+)/g,'$1').replace(/(mailto:[^\s<>"')\]]+)/g,'$1').replace(/(?"')\]]*)?)/g,'$1');} /* Small central safe-URL helper. It allows ordinary http(s) URLs and bare domains, rejects dangerous schemes, and does not rewrite stored user text. */ function _safeUrl(s){if(s===null||s===undefined)return '';var raw=String(s);if(!raw)return '';/* trim surrounding whitespace including control characters */ var v=raw.replace(/^[\s\u0000-\u001F\u007F]+|[\s\u0000-\u001F\u007F]+$/g,'');if(!v)return '';/* normalise the scheme prefix for danger detection: lowercase + strip embedded whitespace/control chars between scheme letters (defeats e.g. "java\tscript:" or "java\nscript:" tricks). */var lc=v.toLowerCase();var schemePart=lc.split(':')[0]||'';var schemeNoWs=schemePart.replace(/[\s\u0000-\u001F\u007F]+/g,'');var DANGEROUS=['JavaScript','data','vbscript','file','about'];if(DANGEROUS.indexOf(schemeNoWs)>=0)return '';/* if the value already starts with http:// or https://, keep it as-is (after trim). Be lenient about case in the scheme. */if(/^https?:\/\//i.test(v))return v;/* otherwise treat it as a bare domain. Accept things like "example.org", "www.example.org", "example.org/path?q=1". Reject anything that still contains a colon at this point (e.g. "ftp:foo", "weird:thing") because we only want to upgrade plain domains. */if(v.indexOf(':')>=0)return '';/* require something that looks like a host token (letters/digits/dots/hyphens) followed optionally by a path/query/fragment. */if(!/^[A-Za-z0-9._~%-]+(?:\/[^\s<>"']*)?$/.test(v))return '';/* require at least one dot in the host portion so we do not promote single-word junk to a URL. */var hostPart=v.split('/')[0]||'';if(hostPart.indexOf('.')<0)return '';return 'https://'+v;} /* Documentation rich-text sanitizer: narrow internal sanitizer for saved/imported/embedded docContent. */ const DOC_ALLOWED_TAGS={P:1,BR:1,B:1,STRONG:1,I:1,EM:1,U:1,UL:1,OL:1,LI:1,H1:1,H2:1,H3:1,BLOCKQUOTE:1,A:1,DIV:1,SPAN:1}; const DOC_DROP_TAGS={SCRIPT:1,IFRAME:1,OBJECT:1,EMBED:1,STYLE:1,IMG:1,VIDEO:1,AUDIO:1,SOURCE:1,CANVAS:1,LINK:1,META:1,BASE:1,SVG:1,MATH:1}; const DOC_UNWRAP_TAGS={FORM:1,BUTTON:1}; const DOC_CLONE_TYPES={page_title:1,box_title:1,box_main_text:1,measure:1,eval_question:1,link:1}; function isSafeDocCloneType(type){return !!DOC_CLONE_TYPES[String(type||'')];} function isSafeDocCloneKey(key){return /^[A-Za-z0-9_-]+(?:-[A-Za-z0-9_-]+)*$/.test(String(key||''));} function isSafeDocClone(type,key){return isSafeDocCloneType(type)&&isSafeDocCloneKey(key);} function unwrapDocNode(node){var parent=node&&node.parentNode;if(!parent)return;while(node.firstChild){parent.insertBefore(node.firstChild,node);}parent.removeChild(node);} function sanitizeDocElementAttrs(el){ var tag=String(el.tagName||'').toUpperCase(); var href=tag==='A'?el.getAttribute('href'):''; var cls=el.getAttribute('class')||''; var cloneType=el.getAttribute('data-clone-type')||''; var cloneKey=el.getAttribute('data-clone-key')||''; Array.from(el.attributes||[]).forEach(function(attr){el.removeAttribute(attr.name);}); if(tag==='A'){ var safeHref=_safeUrl(href||''); if(safeHref){el.setAttribute('href',safeHref);if(/^https?:\/\//i.test(safeHref)){el.setAttribute('target','_blank');el.setAttribute('rel','noopener noreferrer');}} return; } if(tag==='DIV'&&/\bdoc-clone\b/.test(cls)&&isSafeDocClone(cloneType,cloneKey)){ el.setAttribute('class','doc-clone'); el.setAttribute('data-clone-type',cloneType); el.setAttribute('data-clone-key',cloneKey); while(el.firstChild){el.removeChild(el.firstChild);} return; } } function sanitizeDocHtml(html){ if(html===null||html===undefined)return ''; var src=String(html); if(!src)return ''; var tpl=document.createElement('template'); try{tpl.innerHTML=src;}catch(e){return escHtml(src);} function cleanTree(parent){ Array.from(parent.childNodes).forEach(function(node){ if(node.nodeType===8){node.remove();return;} if(node.nodeType!==1)return; var tag=String(node.tagName||'').toUpperCase(); if(DOC_DROP_TAGS[tag]){node.remove();return;} if(DOC_UNWRAP_TAGS[tag]){cleanTree(node);unwrapDocNode(node);return;} if(!DOC_ALLOWED_TAGS[tag]){cleanTree(node);unwrapDocNode(node);return;} sanitizeDocElementAttrs(node); cleanTree(node); }); } cleanTree(tpl.content); return tpl.innerHTML; } function sanitizeDocContentObject(obj){var out={};if(!obj||typeof obj!=='object')return out;Object.keys(obj).forEach(function(pid){out[pid]=sanitizeDocHtml(obj[pid]);});return out;} function sanitizedDocContentState(){return sanitizeDocContentObject(docContent);} /* safe-linkify helper used by popup display blocks. Escapes raw user text first (so any < > & in the saved value renders literally rather than as HTML), then runs the existing linkify() to mark URLs as clickable. Mirrors the existing pattern used in buildUnderBoxDisplay and renderMd; kept as a small named helper so each display call site reads clearly. Does not alter the stored text — only how it is rendered. */ function linkifyDisplay(text){if(text===null||text===undefined)return '';var s=String(text);if(!s)return '';return linkify(s.replace(/&/g,'&').replace(//g,'>'));} function displayTextForView(text,codeStyle,allowLinks){var s=(text===null||text===undefined)?'':String(text);if(!s.trim())return '';return codeStyle?escHtml(s):(allowLinks?linkifyDisplay(s):escHtml(s));} function getViewForBoxKey(key){if(!key)return null;if(String(key).indexOf('final-b')===0)return viewSettings.finalOutcomes;var pg=getBoxPage(key);if(!pg)return null;return (pg.pageType==='how')?viewSettings.how:viewSettings.thisThen;} function boxDisplayCodeStyleActive(key){var vs=getViewForBoxKey(key);return !!(vs&&vs.showMainTextCodeStyle);} function linkDisplayCodeStyleActive(){return !!(viewSettings.thisThen&&viewSettings.thisThen.showLinkInfoCodeStyle);} function setDisplayTextNoticeAndStyle(textareaId,noticeId,active){var ta=document.getElementById(textareaId);var notice=document.getElementById(noticeId);if(ta)ta.classList.toggle('code-style-active',!!active);if(notice)notice.classList.toggle('on',!!active);} /* build the small read-only display block placed above an editable textarea in the Measure / EQ / Link / Board info / Page info popups. Returns '' when the saved value is empty so the popup stays clean for never-filled-in fields. The label is optional and rendered subtly. */ function buildPopupDisplayBlock(text,label){var s=(text===null||text===undefined)?'':String(text);if(!s.trim())return '';var lbl=label?'
'+escHtml(label)+'
':'';return '
'+lbl+linkifyDisplay(s)+'
';} // ─── RENDER ─── /* tlColor returns a single solid color for a traffic-light state, used wherever a flat fill is appropriate (e.g. AI chat state-text color codes). For the two new mixed states ('greenYellow', 'yellowRed') it returns the dominant left-half color as a sensible fallback when a flat fill is rendered; the visually correct vertically-split rendering is produced by tlSplitClass below, which is applied to the dot/picker/chooser elements. Older bare values 'green'/'yellow'/'red'/'grey' are unchanged. */ function tlColor(l){return{yellow:'#F5A623',green:'#2ECC71',red:'#E74C3C',grey:'#94A3B8',greenYellow:'#2ECC71',yellowRed:'#F5A623'}[l]||'#94A3B8';} /* tlSplitClass returns the CSS class name for the vertically-split rendering of the two new mixed traffic-light states. Returns '' for the four original states so the existing solid-fill rendering continues to apply. The class is appended to dot/picker/chooser elements alongside their inline background, and the .tl-split-gy / .tl-split-yr CSS rules use !important to override the inline solid background with the half-and-half gradient. */ function tlSplitClass(l){return l==='greenYellow'?'tl-split-gy':(l==='yellowRed'?'tl-split-yr':'');} /* tlLabel returns the user-facing label for a traffic-light state, used in tooltips and accessible titles in the picker/chooser. */ function tlLabel(l){return{green:'Green',greenYellow:'Green/yellow',yellow:'Yellow',yellowRed:'Yellow/red',red:'Red',grey:'Grey',unset:'Unset'}[l]||'Unset';} const TRAFFIC_LIGHT_VALUES=['green','greenYellow','yellow','yellowRed','red','grey']; function normalizeTrafficLightValue(value){var v=value===null||value===undefined?'':String(value).trim();return TRAFFIC_LIGHT_VALUES.indexOf(v)>=0?v:'';} function trafficLightIsSet(value){return normalizeTrafficLightValue(value)!=='';} function trafficLightDotHTML(key,value,show,onClick,pageType){var state=normalizeTrafficLightValue(value);var title=state?('Click to change traffic light: '+tlLabel(state)):'Click to set traffic light';var click=onClick||"openTLChooser(event,'"+key+"')";if(!show){return '
';}if(state){var split=tlSplitClass(state);return '
';}var hidden=!pageShouldShowTrafficPlaceholder(pageType);return '
';} function pageBoxKeysForType(pageType){if(pageType==='finalOutcomes'){return FO_DATA.map(function(_,i){return 'final-b'+i;}).filter(function(k){return !!B[k];});}var sp=findPageById(currentPage);if(!sp)return [];if(pageType==='how'){return (sp.howBoxes||[]).map(function(hb){return sp.id+'-'+hb.id;}).filter(function(k){return !!B[k];});}return Object.keys(B||{}).filter(function(k){return k.indexOf(sp.id+'-c')===0;});} function pageHasSelectedTrafficLight(pageType){return pageBoxKeysForType(pageType).some(function(k){return B[k]&&trafficLightIsSet(B[k].light);});} function pageShouldShowTrafficPlaceholder(pageType){return !pageHasSelectedTrafficLight(pageType);} function pageHasSelectedPriority(pageType){return pageBoxKeysForType(pageType).some(function(k){return B[k]&&normalizePriorityValue(B[k].priority);});} function pageShouldShowPriorityPlaceholder(pageType){return !pageHasSelectedPriority(pageType);} function linkTrafficValue(l){return l?normalizeTrafficLightValue(l.light):'';} function linkTrafficDotHTML(l){var state=linkTrafficValue(l);if(!state)return '';var split=tlSplitClass(state);return '';} function linkHoverTrafficIconHTML(l){var state=linkTrafficValue(l);if(!state)return '';var split=tlSplitClass(state);return '';} function linkHoverPopupHTML(l){var raw=(l&&typeof l.mainText==='string')?l.mainText:'';if(!raw.trim())return '';var codeStyle=linkDisplayCodeStyleActive();var text=codeStyle?escHtml(raw):escHtml(raw.trim());return ''+linkHoverTrafficIconHTML(l)+''+text+'';} function linkRevealInnerHTML(l){if(!l||!viewSettings.thisThen.showLinkInfoOnHover)return '';return linkTrafficDotHTML(l)+linkHoverPopupHTML(l);} function linkRevealTitle(defaultTitle,l){return (viewSettings.thisThen.showLinkInfoOnHover&&l&&l.mainText&&String(l.mainText).trim())?'':defaultTitle;} var linkHoverHideTimer=null,linkHoverShowTimer=null,linkHoverAnchor=null; function clearLinkHoverShowTimer(){if(linkHoverShowTimer){clearTimeout(linkHoverShowTimer);linkHoverShowTimer=null;}} function ensureLinkHoverPortal(){var p=document.getElementById('linkHoverPortal');if(p)return p;p=document.createElement('div');p.id='linkHoverPortal';p.className='link-hover-portal';p.onmouseenter=function(){if(linkHoverHideTimer){clearTimeout(linkHoverHideTimer);linkHoverHideTimer=null;}};p.onmouseleave=function(){scheduleHideLinkHoverPopup();};p.onclick=function(ev){if(ev){ev.stopPropagation();ev.preventDefault();}var id=p.dataset.linkId||'';hideLinkHoverPopup();if(id)openLinkDetail(id);};document.body.appendChild(p);return p;} function linkHoverVisibleBounds(){var vw=window.innerWidth||document.documentElement.clientWidth||0;var vh=window.innerHeight||document.documentElement.clientHeight||0;var margin=8;var b={left:margin,top:margin,right:Math.max(margin,vw-margin),bottom:Math.max(margin,vh-margin)};var main=document.getElementById('main');if(main&&main.getBoundingClientRect){var r=main.getBoundingClientRect();var mb={left:Math.max(margin,r.left+margin),top:Math.max(margin,r.top+margin),right:Math.min(vw-margin,r.right-margin),bottom:Math.min(vh-margin,r.bottom-margin)};if(mb.right-mb.left>=70&&mb.bottom-mb.top>=40)b=mb;}return b;} function linkHoverRelationshipRect(anchor,linkId){var ar=anchor.getBoundingClientRect();var rect={left:ar.left,top:ar.top,right:ar.right,bottom:ar.bottom,width:ar.width,height:ar.height};var found=linkId?findLink(linkId):null;if(found&&found.link){['from','to'].forEach(function(k){var box=document.getElementById('bx-'+found.link[k]);if(box&&box.getBoundingClientRect){var br=box.getBoundingClientRect();rect.left=Math.min(rect.left,br.left);rect.top=Math.min(rect.top,br.top);rect.right=Math.max(rect.right,br.right);rect.bottom=Math.max(rect.bottom,br.bottom);}});rect.width=rect.right-rect.left;rect.height=rect.bottom-rect.top;}return rect;} function syncLinkHoverTypography(anchor,portal){if(!portal)return;var boxEl=null;var id=portal.dataset?portal.dataset.linkId:'';var found=id?findLink(id):null;if(found&&found.link){boxEl=document.getElementById('bx-'+found.link.from)||document.getElementById('bx-'+found.link.to);}if(!boxEl&&anchor&&anchor.closest)boxEl=anchor.closest('.box,.how-box,.fo-box');var txtEl=boxEl&&boxEl.querySelector?boxEl.querySelector('.box-txt'):null;if(!txtEl)return;var cs=window.getComputedStyle?window.getComputedStyle(txtEl):null;if(!cs)return;var fontPx=parseFloat(cs.fontSize)||12;var linePx=parseFloat(cs.lineHeight);var ratio=(linePx&&fontPx)?(linePx/fontPx):1.45;var scale=1;if(boxEl.offsetWidth&&boxEl.getBoundingClientRect){var br=boxEl.getBoundingClientRect();scale=br.width/boxEl.offsetWidth;}if(!isFinite(scale)||scale<=0){var main=document.getElementById('main');var mz=main&&window.getComputedStyle?parseFloat(window.getComputedStyle(main).zoom):1;scale=(isFinite(mz)&&mz>0)?mz:1;}scale=Math.max(0.5,Math.min(3,scale));portal.style.fontSize=(fontPx*scale).toFixed(2)+'px';portal.style.lineHeight=String(Math.max(1.1,Math.min(1.8,ratio)));} function positionLinkHoverPopup(){var anchor=linkHoverAnchor;if(!anchor)return;var portal=document.getElementById('linkHoverPortal');if(!portal||!portal.classList.contains('open'))return;var bounds=linkHoverVisibleBounds();var maxW=Math.max(90,Math.min(360,bounds.right-bounds.left));portal.style.maxWidth=maxW+'px';portal.style.visibility='hidden';portal.style.left='0px';portal.style.top='0px';var ar=anchor.getBoundingClientRect();var lr=linkHoverRelationshipRect(anchor,portal.dataset.linkId||'');var pr=portal.getBoundingClientRect();var gap=8;var left=ar.left+(ar.width/2)-(pr.width/2);if(leftbounds.right)left=bounds.right-pr.width;if(leftbounds.bottom){top=aboveTop>=bounds.top?aboveTop:bounds.bottom-pr.height;}if(topbounds.bottom)top=Math.max(bounds.top,bounds.bottom-pr.height);portal.style.left=Math.round(left)+'px';portal.style.top=Math.round(top)+'px';portal.style.visibility='visible';} function showLinkHoverPopup(ev){var anchor=ev&&ev.currentTarget?ev.currentTarget:null;if(!anchor)return;var src=anchor.querySelector('.link-hover-pop');if(!src){hideLinkHoverPopup();return;}clearLinkHoverShowTimer();if(linkHoverHideTimer){clearTimeout(linkHoverHideTimer);linkHoverHideTimer=null;}linkHoverAnchor=anchor;var portal=ensureLinkHoverPortal();portal.innerHTML=src.innerHTML;portal.dataset.linkId=src.dataset.linkId||'';portal.classList.add('open');syncLinkHoverTypography(anchor,portal);if(window.__doviewHideTip)window.__doviewHideTip();positionLinkHoverPopup();} function scheduleShowLinkHoverPopup(ev){var anchor=ev&&ev.currentTarget?ev.currentTarget:null;if(!anchor)return;if(!anchor.querySelector('.link-hover-pop')){hideLinkHoverPopup();return;}if(linkHoverHideTimer){clearTimeout(linkHoverHideTimer);linkHoverHideTimer=null;}if(linkHoverAnchor===anchor){positionLinkHoverPopup();return;}clearLinkHoverShowTimer();linkHoverShowTimer=setTimeout(function(){showLinkHoverPopup({currentTarget:anchor});},125);} function scheduleHideLinkHoverPopup(){clearLinkHoverShowTimer();if(linkHoverHideTimer)clearTimeout(linkHoverHideTimer);linkHoverHideTimer=setTimeout(hideLinkHoverPopup,120);} function hideLinkHoverPopup(){clearLinkHoverShowTimer();if(linkHoverHideTimer){clearTimeout(linkHoverHideTimer);linkHoverHideTimer=null;}var portal=document.getElementById('linkHoverPortal');if(portal){portal.classList.remove('open');portal.innerHTML='';portal.dataset.linkId='';}linkHoverAnchor=null;} function wireLinkHoverPopups(){document.querySelectorAll('.reveal-arrow-hover-out,.reveal-arrow-hover-in').forEach(function(el){el.onmouseenter=scheduleShowLinkHoverPopup;el.onmouseover=function(ev){if(!ev||!ev.relatedTarget||!el.contains(ev.relatedTarget))scheduleShowLinkHoverPopup({currentTarget:el});};el.onmousemove=positionLinkHoverPopup;el.onmouseleave=scheduleHideLinkHoverPopup;el.onmouseout=function(ev){if(!ev||!ev.relatedTarget||!el.contains(ev.relatedTarget))scheduleHideLinkHoverPopup();};el.onfocus=scheduleShowLinkHoverPopup;el.onblur=scheduleHideLinkHoverPopup;});} function priColor(p){p=normalizePriorityValue(p);return {A:'#ffffff',B:'#ffffff',C:'#ffffff',D:'#1e293b',E:'#334155'}[p]||'#334155';} function priBadgeClass(p){p=normalizePriorityValue(p);return p?' pri-'+String(p).toLowerCase().replace(/[^a-z0-9]/g,''):' pri-empty';} function priorityBadgeHTML(key,value,show,pageType){if(!show)return '';var pri=normalizePriorityValue(value);var hidden=!pri&&!pageShouldShowPriorityPlaceholder(pageType);return '
'+escHtml(pri)+'
';} function getRevealClasses(key){function _neg(from,to){var l=ttLinkBetween(from,to);return l&&ttLinkPolarity(l)==='negative';}if(inspectHome){if(key===inspectHome)return ' inspect-home';if(inspectLinkedKeys.indexOf(key)>=0){let cls=' inspect-linked';if(ttLinkBetween(inspectHome,key)){cls+=' reveal-out';if(_neg(inspectHome,key))cls+=' reveal-out-neg';}if(ttLinkBetween(key,inspectHome)){cls+=' reveal-in';if(_neg(key,inspectHome))cls+=' reveal-in-neg';}return cls;}return '';}if(!revealBox)return '';if(revealType==='tt'){if(key===revealBox)return ' reveal-active';let cls='';if(ttLinkBetween(revealBox,key)){cls+=' reveal-out';if(_neg(revealBox,key))cls+=' reveal-out-neg';}if(ttLinkBetween(key,revealBox)){cls+=' reveal-in';if(_neg(key,revealBox))cls+=' reveal-in-neg';}if(cls)cls+=' reveal-active';return cls;}if(revealType==='how'){const rb=revealBox;if(isTTBox(key)){if(getHowLinkExact(rb,key))return ' box-selected';}return '';}return '';} function ttLinkPolarity(l){return l&&l.polarity==='negative'?'negative':'positive';} function ttPolarityRel(l){return ttLinkPolarity(l)==='negative'?'negatively affects':'positively affects';} function ttRelHTML(l){return ttLinkPolarity(l)==='negative'?'negatively affects':'positively affects';} function ttCountInfo(key,direction){var links=direction==='left'?getTTLinksTo(key):getTTLinksFrom(key);var pos=0,neg=0;links.forEach(function(l){if(ttLinkPolarity(l)==='negative')neg++;else pos++;});return {total:links.length,pos:pos,neg:neg,cls:pos&&neg?'tt-pol-mixed':(neg?'tt-pol-negative':'tt-pol-positive')};} function ttCountBadgeHTML(key,direction,showCounts){var info=ttCountInfo(key,direction);var side=direction==='left'?'tt-cnt-l':'tt-cnt-r';var kind=direction==='left'?'tt-left':'tt-right';var title=info.total?(info.total+' '+(direction==='left'?'incoming':'outgoing')+' This–Then links'):(direction==='left'?'No incoming links':'No outgoing links');var click="if(linkDragClickSuppress(event))return;event.stopPropagation();showTTLinksList('"+key+"','"+direction+"')";if(showCounts){if(!info.total)return '';return ''+info.total+'';}return ''; } function ttLinkBetween(from,to){return getRuntimeLinkIndex().ttByPair[_idxPair(from,to)]||null;} function flickBoxes(keys){keys=(keys||[]).filter(Boolean);if(sourceRefBox&&sourceRefBox.sourceKey&&keys.indexOf(sourceRefBox.sourceKey)>=0){var sr=document.getElementById('srcRefBox');if(sr){sr.classList.remove('tt-flick');void sr.offsetWidth;sr.classList.add('tt-flick');}}keys.forEach(function(k){var el=document.getElementById('bx-'+k);if(el){el.classList.remove('tt-flick');void el.offsetWidth;el.classList.add('tt-flick');}});} function getHowRevealClass(key){if(inspectHome){if(key===inspectHome)return ' inspect-home';if(inspectLinkedKeys.indexOf(key)>=0)return ' inspect-linked';return '';}if(!revealBox)return '';if(revealType==='how'){if(key===revealBox)return '';if(getHowLinkExact(revealBox,key)||getHowLinkExact(key,revealBox))return ' reveal-highlight';return '';}if(revealType==='tt'){if(getHowLinkExact(key,revealBox))return ' reveal-highlight';return '';}return '';} function boxHTML(key,boxData,isLast,spColor){const{label,light,priority,hasSubpage,detailText,borderColor,jumpToPage}=boxData;const entries=Array.isArray(boxData.entries)?boxData.entries:[];const hasSofar=entries.some(e=>e.type==='sofar');const editLbl=light==='green'&&hasSofar?'edit':'';const priTxt=normalizePriorityValue(priority);const vs=viewSettings.thisThen;const showPri=vs.showPriorities;const priBadge=priorityBadgeHTML(key,priTxt,showPri,'this_then');const drillHTML=hasSubpage?'
':'';const jumpHTML=jumpToPage?(hasSubpage?`
`:`
`):'';const greyClass=light==='grey'?' grey-state':'';const lastClass=isLast?' last-col':'';const selClass=key===openBox?' box-selected':'';const revCls=getRevealClasses(key);const customFill=sanitizeOptionalColorValue(boxData.boxColor);const customBdr=sanitizeOptionalColorValue(borderColor);const fillColor=customFill||spColor.bg;const bdrColor=customBdr||spColor.bdr;const bdrWidth=customBdr?'2px':'1px';const showTL=vs.showTrafficLights;const tlDot=trafficLightDotHTML(key,light,showTL,"openTLChooser(event,'"+key+"')",'this_then');const showCnts=vs.showCounts;const lBadge=ttCountBadgeHTML(key,'left',showCnts);const rBadge=ttCountBadgeHTML(key,'right',showCnts);const howCnt=getHowCountForTTBox(key);const showHC=vs.showHowCounts;const howBadge=showHC?(howCnt?`${howCnt}`:``):'';const ttLatCnt=getLateralHowCountForTTBox(key);const showTTLat=vs.showLateralHow===true;const ttLatSq=showTTLat?``:'';const underBox=buildUnderBoxDisplay(key,'this_then');/* tooltip-bearing overlays for the small black reveal/link arrows. A box with class reveal-out has a LEFT-side arrow (this box results from the selected box). A box with class reveal-in has a RIGHT-side arrow (this box makes the selected box happen). The overlays carry side-specific tooltips via the DV_TIPS interceptor. */const hasRevealOut=revCls.indexOf('reveal-out')>=0;const hasRevealIn=revCls.indexOf('reveal-in')>=0;const outLink=inspectHome?ttLinkBetween(inspectHome,key):(revealBox?ttLinkBetween(revealBox,key):null);const inLink=inspectHome?ttLinkBetween(key,inspectHome):(revealBox?ttLinkBetween(key,revealBox):null);const revealArrowHovers=(hasRevealOut?''+linkRevealInnerHTML(outLink)+'':'')+(hasRevealIn?''+linkRevealInnerHTML(inLink)+'':'');return `
${priBadge}${tlDot}
${escHtml(label)}${tagHTMLForBox(boxData)}
${drillHTML}${jumpHTML}${lBadge}${rBadge}${howBadge}${ttLatSq}${revealArrowHovers}
${underBox}`;} function howBoxHTML(key,boxData,hId){const{label,light,priority,detailText,borderColor,jumpToPage}=boxData;const entries=Array.isArray(boxData.entries)?boxData.entries:[];const hasSofar=entries.some(e=>e.type==='sofar');const editLbl=light==='green'&&hasSofar?'edit':'';const priTxt=normalizePriorityValue(priority);const vs=viewSettings.how;const showPri=vs.showPriorities!==false;const priBadge=priorityBadgeHTML(key,priTxt,showPri,'how');const greyClass=light==='grey'?' grey-state':'';const selClass=key===openBox?' box-selected':'';const howRevCls=getHowRevealClass(key);const customFill=sanitizeOptionalColorValue(boxData.boxColor);const customBdr=sanitizeOptionalColorValue(borderColor);const fillColor=customFill||'#fff';const bdrColor=customBdr||'#d1d5db';const bdrWidth=customBdr?'2px':'1px';const showNum=vs.showNumbering;const hidEl=showNum?`
${escHtml(hId)}
`:'';const showTL=vs.showTrafficLights!==false;const tlDot=trafficLightDotHTML(key,light,showTL,"openTLChooser(event,'"+key+"')",'how');const jumpHTML=jumpToPage?`
`:'';const whyCnt=getWhyCountForHowBox(key);const showWC=vs.showWhyCounts;const showVertical=howBoxHasVerticalLevel(key);const whyBadge=(showWC&&showVertical)?(whyCnt?`${whyCnt}`:``):'';const h2hCnt=getHowToHowInCount(key);const dnCnt=getDownwardHowCount(key);const h2hBadge=(showWC&&showVertical)?(dnCnt?`${dnCnt}`:``):'';const underBox=buildUnderBoxDisplay(key,'how');const latCnt=getLateralHowCount(key);const showLat=vs.showLateralHow!==false;const latSq=showLat?``:''; return `
${priBadge}${tlDot}
${escHtml(label)}${tagHTMLForBox(boxData)}
${hidEl}${jumpHTML}${whyBadge}${h2hBadge}${latSq}
${underBox}
`;} function renderTabBar(){/* persistent horizontal page-name/tab strip removed from Normal View; Overview remains the main page-navigation surface. The hidden tabBar element is left in the DOM for compatibility but is intentionally not populated. */var el=document.getElementById('tabBar');if(el)el.innerHTML='';} function navTo(id){if(document.activeElement&&document.activeElement.isContentEditable)return; // if an explicit relationship-navigation context (inspectHome) is active and the user is crossing to a different page, create the ghost/context box consistently via the shared helper. This covers border-click reveal continuation across pages and count-affordance cancel/continue cross-page continuation. Ordinary manual tab switching with NO inspect context is unaffected \u2014 this block is gated on inspectHome being set. The heading is the link-inspection heading ('Box on other page linking to highlighted boxes below'); jumps still flow through navFromSourceBox and carry their own 'jump' navType. if(inspectHome&&id!==currentPage){var _ihSrcPid=getSourcePageId(inspectHome);if(_ihSrcPid&&_ihSrcPid!==id){ensureSourceRefBoxForCrossPage(inspectHome,id,'inspect');}} // manual navigation away from a 'returned' source-ref context clears the context box \u2014 the return-to-source highlight is not meant to persist across ordinary manual page switching. if(sourceRefBox&&sourceRefBox.returned&&id!==currentPage){sourceRefBox=null;inspectHome=null;inspectLinkedKeys=[];} // push current page onto the navigation history stack (and clear the forward stack) when explicitly moving to a different page. Used by the back/forward arrows in the page info bar. if(id!==currentPage){navHistory.push(currentPage);if(navHistory.length>MAX_NAV_HIST)navHistory.shift();navFuture=[];} if(currentPage==='overview'&&id!=='overview')overviewReorderMode=false; currentPage=id;openBox=null;editingType=null;document.getElementById('epWrap').style.display='none';render();} // back/forward page-navigation arrows. These are browser-style navigation history only and do NOT affect page order. They do not push to navHistory themselves (they only consume/push through the stacks). function pageOrderIds(){var ids=['overview'];if(SP.length!==1)ids.push('final');SP.forEach(function(s){ids.push(s.id);});return ids;} function currentPageOrderIndex(){return pageOrderIds().indexOf(currentPage);} function pageStepNavHTML(){var ids=pageOrderIds();var idx=ids.indexOf(currentPage);var canPrev=idx>0;var canNext=idx>=0&&idx‹ Previous|Next ›'; } function pageStepNav(ev,dir){if(ev&&ev.stopPropagation)ev.stopPropagation();var ids=pageOrderIds();var idx=ids.indexOf(currentPage);if(idx<0)return;var nextIdx=idx+(dir<0?-1:1);if(nextIdx<0||nextIdx>=ids.length)return;navTo(ids[nextIdx]);} function navBack(ev){if(ev&&ev.stopPropagation)ev.stopPropagation();if(!navHistory.length)return;const prev=navHistory.pop();navFuture.push(currentPage);if(navFuture.length>MAX_NAV_HIST)navFuture.shift();if(inspectHome&&prev!==currentPage){var _ihSrcPid=getSourcePageId(inspectHome);if(_ihSrcPid&&_ihSrcPid!==prev){ensureSourceRefBoxForCrossPage(inspectHome,prev,'inspect');}}currentPage=prev;openBox=null;editingType=null;var _ep=document.getElementById('epWrap');if(_ep)_ep.style.display='none';render();} function navForward(ev){if(ev&&ev.stopPropagation)ev.stopPropagation();if(!navFuture.length)return;const nxt=navFuture.pop();navHistory.push(currentPage);if(navHistory.length>MAX_NAV_HIST)navHistory.shift();if(inspectHome&&nxt!==currentPage){var _ihSrcPid=getSourcePageId(inspectHome);if(_ihSrcPid&&_ihSrcPid!==nxt){ensureSourceRefBoxForCrossPage(inspectHome,nxt,'inspect');}}currentPage=nxt;openBox=null;editingType=null;var _ep=document.getElementById('epWrap');if(_ep)_ep.style.display='none';render();} function tabClick(id){if(_clickTimer){clearTimeout(_clickTimer);_clickTimer=null;}_clickTimer=setTimeout(function(){_clickTimer=null;navTo(id);},250);} function setOverviewMode(mode,ev){if(ev&&ev.stopPropagation)ev.stopPropagation();overviewMode=(mode==='compact')?'compact':'cards';render();} function overviewNavTo(ev,id){if(ev&&ev.stopPropagation)ev.stopPropagation();navTo(id);} function changePageOrderToggle(ev){if(ev&&ev.stopPropagation)ev.stopPropagation();if(IS_READONLY)return;if(overviewReorderMode){overviewReorderMode=false;render();return;}overviewReorderMode=true;if(currentPage!=='overview')navTo('overview');else render();} function renderOverview(){ var isCompact=overviewMode==='compact'; var reorderActive=overviewReorderMode&&!IS_READONLY; var ovs=ensureOverviewViewSettings(); var mode='
'; var addPill=(IS_READONLY||presentationView)?'':''; var topActions='
'+mode+addPill+'
'; var plus=IS_READONLY?'':''; var tiles=SP.map(function(sp,idx){ var meta=overviewCardMeta(sp); var tileColor=overviewCardColor(sp); var cue=sp.pageType==='how'?'HOW':sp.pageType==='documentation'?'DOC':'THIS\u2013THEN'; var levelCue=sp.pageType==='how'?(sp.howLevel?'Level '+sp.howLevel:'No level'):''; var canLeft=idx>0, canRight=idxMove':''; var cueHtml=(isCompact&&!reorderActive)?'
'+cue+''+(levelCue?''+levelCue+'':'')+'
':''; if(isCompact){return '

'+escHtml(sp.label)+'

'+cueHtml+'
'+moveBtn+'
';} var showTL=ovs.showTrafficLights===true; var showPri=ovs.showPriorities===true; var showTags=ovs.showTags===true; var showColor=ovs.showCardColorControls===true&&!IS_READONLY; var hasMeta=showTL||showPri||showTags||showColor; var tlSplit=tlSplitClass(meta.light); var tlDot=showTL?'
':''; var pri=showPri?'
'+(meta.priority||'')+'
':''; var tagContent=tagHTMLForElement(meta)||(!IS_READONLY?'Tags':''); var tagClick=IS_READONLY?'':' onclick="openOverviewCardTags(event,\''+sp.id+'\')" title="Edit tags"'; var tagBlock=showTags?'
'+tagContent+'
':''; var colorBtn=showColor?'':''; return '
'+pri+tlDot+'

'+escHtml(sp.label)+'

'+tagBlock+'
'+colorBtn+moveBtn+'
'; }).join('')+plus; return '

Final Outcomes

The high-level outcomes — click to explore


'+topActions+'
'+tiles+'
'+(isCompact?'':'

This–Then pages show steps and outcomes in the world. How pages show the actions being taken to change them.

'); } function renderFinalOutcomes(){const vs=viewSettings.finalOutcomes;const backBtn=backToOverviewButtonHTML();const addBtn=IS_READONLY?'':'';return backBtn+`

Final Outcomes

${addBtn}${selectedFinalBoxActionsHTML()}
${FO_DATA.map((label,i)=>{const k=`final-b${i}`,b=B[k];if(!b)return '';const pri=normalizePriorityValue(b.priority);const showPri=vs.showPriorities;const pb=priorityBadgeHTML(k,pri,showPri,'finalOutcomes');const showTL=vs.showTrafficLights;const tlDot=trafficLightDotHTML(k,b.light,showTL,"openTLChooser(event,'"+k+"')",'finalOutcomes');const selClass=k===openBox?' box-selected':'';const insClass=inspectHome?(k===inspectHome?' inspect-home':(inspectLinkedKeys.indexOf(k)>=0?' inspect-linked':'')):'';const jumpHTML=b.jumpToPage?`
`:'';const underBox=buildUnderBoxDisplay(k,'finalOutcomes');const fill=sanitizeOptionalColorValue(b.boxColor);const bdr=sanitizeOptionalColorValue(b.borderColor);const fSty=(fill?'background:'+fill+';':'')+(bdr?'border-color:'+bdr+';border-width:2px;':'');return `
${pb}${tlDot}
${escHtml(label)}${tagHTMLForBox(b)}
${jumpHTML}
${underBox}`;}).join('')}
`;} function selectedTTBoxKeyForPage(pid){if(!openBox)return '';var m=openBox.match(/^(p\d+)-c(\d+)-b(\d+)$/);if(m&&m[1]===pid&&B[openBox])return openBox;return '';} function selectedHowBoxKeyForPage(pid){if(!openBox)return '';var m=openBox.match(/^(p\d+)-(H\d+)$/);if(m&&m[1]===pid&&B[openBox])return openBox;return '';} function selectedFinalBoxKey(){if(!openBox)return '';var m=openBox.match(/^final-b(\d+)$/);if(m&&B[openBox])return openBox;return '';} function menuContextLabel(label,max){label=label||'selected box';max=max||54;return label.length>max?label.substring(0,max-1)+'…':label;} function selectedTTBoxActionsHTML(pid){if(IS_READONLY)return '';var key=selectedTTBoxKeyForPage(pid);if(!key)return '';var label=(B[key]&&B[key].label)||'selected box';var shortLabel=menuContextLabel(label);var m=parseTTBoxKey(key);var sp=SP.find(function(s){return s.id===pid;});var ci=m?m.ci:0;var canMoveLeft=sp&&Array.isArray(sp.cols)&&ci>0;var canMoveRight=sp&&Array.isArray(sp.cols)&&ciMove Box Left':'';var moveRightOpt=canMoveRight?'':'';var moveColOpt=canMoveRight?'':'';return '';} function selectedHowBoxActionsHTML(pid){if(IS_READONLY)return '';var key=selectedHowBoxKeyForPage(pid);if(!key)return '';var label=(B[key]&&B[key].label)||'selected How Box';var shortLabel=menuContextLabel(label);return '';} function selectedFinalBoxActionsHTML(){if(IS_READONLY)return '';var key=selectedFinalBoxKey();if(!key)return '';var label=(B[key]&&B[key].label)||'selected box';var shortLabel=menuContextLabel(label);var idx=finalOutcomeIndexFromKey(key);var upOpt=idx>0?'':'';var downOpt=(idx>=0&&idxMove box down':'';return '';} function runSelectedTTBoxAction(sel,pid){if(!sel)return;var action=sel.value;sel.value='';if(!action||IS_READONLY)return;var key=selectedTTBoxKeyForPage(pid);if(!key){alert('Select a This–Then Box first.');render();return;}if(action==='addBoxBelow'){addTTBoxUnderSelected(pid);return;}if(action==='addColumnAfter'){promptAddColumn(key);return;}if(action==='moveColumnRight'){moveSelectedTTColumnRight(key);return;}if(action==='editMainText'){editMainTextForBox(key);return;}if(action==='moveUp'){moveBoxUp(key);return;}if(action==='moveDown'){moveBoxDown(key);return;}if(action==='moveLeft'){moveTTBoxHorizontal(key,'left');return;}if(action==='moveRight'){moveTTBoxHorizontal(key,'right');return;}if(action==='deleteBox'){confirmDeleteBox(key);return;}if(action==='deleteColumn'){confirmDeleteColumn(key);return;}} function runSelectedHowBoxAction(sel,pid){if(!sel)return;var action=sel.value;sel.value='';if(!action||IS_READONLY)return;var key=selectedHowBoxKeyForPage(pid);var m=key&&parseHowBoxKey(key);if(!key||!m){alert('Select a How Box first.');render();return;}var howId=m.howId;if(action==='addHowBoxBelow'){addHowBoxAfter(pid,howId);return;}if(action==='editMainText'){editMainTextForBox(key);return;}if(action==='moveLeft'){moveHowBox(pid,howId,'earlier');return;}if(action==='moveRight'){moveHowBox(pid,howId,'later');return;}if(action==='moveUp'){moveHowBoxVertical(pid,howId,'up');return;}if(action==='moveDown'){moveHowBoxVertical(pid,howId,'down');return;}if(action==='moveFirst'){moveHowBox(pid,howId,'first');return;}if(action==='moveLast'){moveHowBox(pid,howId,'last');return;}if(action==='deleteBox'){deleteHowBox(pid,howId);return;}} function runSelectedFinalBoxAction(sel){if(!sel)return;var action=sel.value;sel.value='';if(!action||IS_READONLY)return;var key=selectedFinalBoxKey();if(!key){alert('Select a final outcome box first.');render();return;}var idx=finalOutcomeIndexFromKey(key);if(idx<0){render();return;}if(action==='addBoxBelow'){addFinalOutcomeBelowSelected();return;}if(action==='moveUp'){moveFinalUp(idx);return;}if(action==='moveDown'){moveFinalDown(idx);return;}if(action==='deleteBox'){confirmDeleteFinal(idx);return;}} function renderSubpage(pid){const sp=SP.find(s=>s.id===pid);if(!sp)return '';const nc=sp.cols.length;const singlePage=SP.length===1;const barColor=singlePage?COL_COLORS[0].tab:spColor(sp).tab;const cols=sp.cols.map((col,ci)=>{const isLast=ci===nc-1;const colColor=singlePage?COL_COLORS[ci%COL_COLORS.length]:spColor(sp);const boxes=col.boxes.map((_,bi)=>{const k=`${sp.id}-c${ci}-b${bi}`;return B[k]?boxHTML(k,B[k],isLast,colColor):'';}).join('');return `
${escHtml(col.h)}
${boxes}
`;});let inner='';cols.forEach((col,i)=>{inner+=col;if(i`;}});const backBtn=backToOverviewButtonHTML();return `${backBtn}
This\u2013Then${escHtml(sp.label)}${presentationView?'':selectedTTBoxActionsHTML(pid)}
${inner}
`;} function moveSelectedTTColumnRight(key){if(IS_READONLY)return;const m=key&&parseTTBoxKey(key);if(!m)return;const spId=m.spId,ci=m.ci,bi=m.bi;const sp=SP.find(s=>s.id===spId);if(!sp||!Array.isArray(sp.cols)||ci>=sp.cols.length-1)return;takeSnapshot();const keyMap={};const leftBoxes=sp.cols[ci].boxes||[];const rightBoxes=sp.cols[ci+1].boxes||[];for(let i=0;is.id===spId);if(!sp)return;takeSnapshot();if(openBox){const m=openBox.match(/^(p\d+)-c(\d+)-b(\d+)$/);if(m&&m[1]===spId){const ci=parseInt(m[2]),bi=parseInt(m[3]);/* Preserve box state and links while positional keys shift. */const oldColLen=sp.cols[ci].boxes.length;const keyMap={};for(let j=bi+1;js.id===pid);if(!sp)return '';const boxes=(sp.howBoxes||[]).map(hb=>{const key=`${sp.id}-${hb.id}`;const boxData=B[key];if(!boxData)return '';return howBoxHTML(key,boxData,howBoxDisplayId(hb));}).join('');const backBtn=backToOverviewButtonHTML();return `${backBtn}
How${escHtml(sp.label)}${presentationView?'':selectedHowBoxActionsHTML(pid)}
${boxes}
`;} function renderDocPage(pid){var sp=SP.find(function(s){return s.id===pid;});if(!sp)return '';var backBtn=backToOverviewButtonHTML();var docBar='
Documentation'+escHtml(sp.label)+'
';var saved=docContent[pid]||'';var safeSaved=sanitizeDocHtml(saved);var resolvedContent=linkifyDocHtml(resolveDocClones(safeSaved));if(docEditMode[pid]){var editContent=resolveDocClonesForEdit(safeSaved);return backBtn+docBar+'
'+( editContent||'

Start writing here...

')+'
';}return backBtn+docBar+'
'+( resolvedContent||'

No content yet. Click Edit to start writing.

')+'
';} function docStartEdit(pid){if(IS_READONLY)return;docEditBackup[pid]=docContent[pid]||'';docEditMode[pid]=true;render();} function docSaveEdit(pid){if(IS_READONLY)return;docEditMode[pid]=false;delete docEditBackup[pid];saveState();render();} function docCancelEdit(pid){if(IS_READONLY)return;docContent[pid]=docEditBackup[pid]||'';docEditMode[pid]=false;delete docEditBackup[pid];saveState();render();} function docExec(cmd,val){document.execCommand(cmd,false,val||null);var el=document.getElementById('docEditor');if(el)el.focus();} function docExecBlock(tag){if(!tag)return;document.execCommand('formatBlock',false,tag);var el=document.getElementById('docEditor');if(el)el.focus();} /* docInsertLink previously took whatever the user typed in the prompt() and passed it directly to document.execCommand('createLink'); a user typing JavaScript:alert(1) would have produced a clickable JavaScript: anchor in the saved Documentation Page content. The entered URL is now resolved through the _safeUrl helper and the createLink command is only run when _safeUrl returns a safe URL. Unsafe / blank URLs are rejected with a small alert so the user knows the link was not created. The contenteditable HTML editor's safe display path (linkifyDocHtml, which walks text nodes, escapes, then linkifies) is unchanged. Stored Documentation Page content is not rewritten. */ function docInsertLink(){var url=prompt('Enter URL:');if(url){var safe=_safeUrl(url);if(safe){document.execCommand('createLink',false,safe);var sel=window.getSelection&&window.getSelection();var node=sel&&sel.anchorNode;var a=node&&node.nodeType===1?node:(node?node.parentElement:null);while(a&&a.tagName!=='A')a=a.parentElement;if(a&&a.tagName==='A'){a.setAttribute('target','_blank');a.setAttribute('rel','noopener noreferrer');}}else{try{alert('That URL was not added because it could not be opened safely. Please use an https:// or http:// URL, or a plain domain like example.org.');}catch(_){}}}var el=document.getElementById('docEditor');if(el)el.focus();} function docSave(pid){if(IS_READONLY)return;var el=document.getElementById('docEditor');if(el){var html=el.innerHTML;html=sanitizeDocHtml(sanitizeDocClones(html));docContent[pid]=html;}saveState();} function docPaste(e){e.preventDefault();var text=e.clipboardData.getData('text/plain');document.execCommand('insertText',false,text);} // ─── DOCUMENTATION PAGE CLONES ─── function resolveCloneText(type,key){ if(type==='page_title'){if(key==='final')return 'Final Outcomes';const sp=SP.find(s=>s.id===key);return sp?sp.label:null;} if(type==='box_title'){const b=B[key];return b?b.label:null;} if(type==='box_main_text'){const b=B[key];return b?(b.label+': '+(b.detailText||'(no display text)')):null;} if(type==='measure'){const m=getMeasure(key);return m?measureDisplayId(m)+' '+m.title:null;} if(type==='eval_question'){const q=getEQ(key);return q?eqDisplayId(q)+' '+q.questionText:null;} if(type==='link'){const l=ttLinks.find(x=>x.id===key)||howLinks.find(x=>x.id===key);if(!l)return null;const fb=B[l.from],tb=B[l.to];if(!fb||!tb)return null;return (fb.label)+' \u2192 '+(tb.label);} return null; } function cloneTypeLabel(type){ if(type==='page_title')return 'Page title';if(type==='box_title')return 'Box title';if(type==='box_main_text')return 'Box Display Text';if(type==='measure')return 'Measure';if(type==='eval_question')return 'Evaluation Question';if(type==='link')return 'Link';return 'Clone'; } function sanitizeDocClones(html){ if(!html)return html; return html.replace(/
]*)>[\s\S]*?<\/div>/g,function(match,attrs){ var typeM=attrs.match(/data-clone-type="([^"]*)"/i);var keyM=attrs.match(/data-clone-key="([^"]*)"/i); if(!typeM||!keyM||!isSafeDocClone(typeM[1],keyM[1]))return ''; return '
'; }); } function ensureBlankTargetRel(root){if(!root||!root.querySelectorAll)return;try{root.querySelectorAll('a[target="_blank"]').forEach(function(a){var rel=(a.getAttribute('rel')||'').toLowerCase().split(/\s+/).filter(Boolean);if(rel.indexOf('noopener')<0)rel.push('noopener');if(rel.indexOf('noreferrer')<0)rel.push('noreferrer');a.setAttribute('rel',rel.join(' '));});}catch(e){}} function linkifyDocHtml(html){if(!html)return html;var d=document.createElement('div');d.innerHTML=html;function walkText(node){if(node.nodeType===3){var t=node.textContent;var linked=linkify(t.replace(/&/g,'&').replace(//g,'>'));if(linked!==t.replace(/&/g,'&').replace(//g,'>')){var span=document.createElement('span');span.innerHTML=linked;node.parentNode.replaceChild(span,node);}}else if(node.nodeType===1&&node.tagName!=='A'&&!node.classList.contains('doc-clone')){var children=Array.from(node.childNodes);children.forEach(walkText);}}walkText(d);ensureBlankTargetRel(d);return d.innerHTML;} function resolveDocClones(html){ if(!html)return html; return html.replace(/
]*)>[\s\S]*?<\/div>/g,function(match,attrs){ var typeM=attrs.match(/data-clone-type="([^"]*)"/);var keyM=attrs.match(/data-clone-key="([^"]*)"/); if(!typeM||!keyM)return match; var type=typeM[1],key=keyM[1];var text=resolveCloneText(type,key); var label=cloneTypeLabel(type); if(text===null)return '
'+escHtml(label)+' (missing) [Source deleted]
'; var clickAction=cloneClickAction(type,key); return '
'+escHtml(label)+' '+escHtml(text)+'
'; }); } function resolveDocClonesForEdit(html){ if(!html)return html; return html.replace(/
]*)>[\s\S]*?<\/div>/g,function(match,attrs){ var typeM=attrs.match(/data-clone-type="([^"]*)"/);var keyM=attrs.match(/data-clone-key="([^"]*)"/); if(!typeM||!keyM)return match; var type=typeM[1],key=keyM[1];var text=resolveCloneText(type,key); var label=cloneTypeLabel(type); if(text===null)return '
'+escHtml(label)+' (missing) [Source deleted] \u2715
'; return '
'+escHtml(label)+' '+escHtml(text)+' \u2715
'; }); } function cloneClickAction(type,key){ if(type==='page_title'){return "navTo('"+key+"')";} if(type==='box_title'||type==='box_main_text'){return "navToBox('"+key+"')";} if(type==='measure'){return "openMeasureDetail('"+key+"')";} if(type==='eval_question'){return "openEQDetail('"+key+"')";} if(type==='link'){return "openLinkDetail('"+key+"')";} return ''; } function removeCloneBlock(el){if(IS_READONLY)return;if(!el)return;var pid=currentPage;el.remove();var editor=document.getElementById('docEditor');if(editor){var html=editor.innerHTML;docContent[pid]=sanitizeDocHtml(sanitizeDocClones(html));saveState();}} var _cloneTargetPage=null; function openCloneChooser(pid){_cloneTargetPage=pid;document.getElementById('cloneSourceType').value='';document.getElementById('cloneStep2').innerHTML='';document.getElementById('cloneInsertBtn').disabled=true;openModal('cloneChooserModal');} function updateCloneChooserStep2(){ var type=document.getElementById('cloneSourceType').value;var s2=document.getElementById('cloneStep2');document.getElementById('cloneInsertBtn').disabled=true; if(!type){s2.innerHTML='';return;} var html=''; if(type==='page_title'){ html=''; }else if(type==='box_title'||type==='box_main_text'){ html=''; }else if(type==='measure'){ html=''; }else if(type==='eval_question'){ html=''; }else if(type==='link'){ html=''; } s2.innerHTML=html;cloneKeyChanged(); } function cloneKeyChanged(){var sel=document.getElementById('cloneSourceKey');document.getElementById('cloneInsertBtn').disabled=!sel||!sel.value;} function doInsertClone(){if(IS_READONLY)return; var type=document.getElementById('cloneSourceType').value; var sel=document.getElementById('cloneSourceKey'); if(!type||!sel||!sel.value)return; var key=sel.value; if(key==='__create_new__'){ closeModal('cloneChooserModal'); if(type==='measure'){var title=prompt('New measure title:');if(!title)return;var id='M'+String(measureNextId).padStart(3,'0');measureNextId++;/* assign stable uid to a Measure created via the clone chooser. sourceUid is intentionally NOT set here — doInsertClone creates a brand-new Measure, not a clone-by-reference. */measures.push({id:id,title:title,mainText:'',notes1:'',notes2:'',notes3:'',trafficLight:'',uid:_genUid('measure')});saveState();key=id;} else if(type==='eval_question'){var qtext=prompt('New evaluation question:');if(!qtext)return;var id='EQ'+String(eqNextId).padStart(3,'0');eqNextId++;/* assign stable uid to an Evaluation Question created via the clone chooser. sourceUid is intentionally NOT set here — doInsertClone creates a brand-new EQ, not a clone-by-reference. */evalQuestions.push({id:id,questionText:qtext,mainText:'',notes1:'',notes2:'',notes3:'',trafficLight:'',uid:_genUid('eq')});saveState();key=id;} else return; }else{closeModal('cloneChooserModal');} var cloneHtml='
'; var editor=document.getElementById('docEditor'); if(editor){ editor.focus(); var temp=document.createElement('div');temp.innerHTML=cloneHtml;var cloneEl=temp.firstChild; var selection=window.getSelection(); if(selection.rangeCount){var range=selection.getRangeAt(0);range.collapse(false);range.insertNode(cloneEl);}else{editor.appendChild(cloneEl);} var html=editor.innerHTML;docContent[_cloneTargetPage]=sanitizeDocHtml(sanitizeDocClones(html));saveState();render(); } } function currentPresentationTitle(){if(currentPage==='overview')return TITLE||'Overview';if(currentPage==='final')return 'Final Outcomes';var sp=SP.find(function(s){return s.id===currentPage;});return sp?(sp.label||'Untitled page'):'Overview';} function presentationContextActionHTML(){if(currentPage==='overview'){return '';}var sp=SP.find(function(s){return s.id===currentPage;});if(sp&&((sp.pageType||'this_then')==='this_then'))return selectedTTBoxActionsHTML(currentPage);if(sp&&sp.pageType==='how')return selectedHowBoxActionsHTML(currentPage);return '';} function updatePresentationBar(){var title=document.getElementById('presentationTitle');if(title)title.textContent=currentPresentationTitle();var row=document.getElementById('presentationStatementRow');var st=document.getElementById('presentationStatementText');if(row&&st){var txt=(topRightText||'').trim();var back=presentationBackToOverviewHTML();st.textContent=txt;st.title=txt;st.style.display=txt?'inline':'none';var old=row.querySelector('.presentation-overview-slot');if(old)old.remove();if(back){var slot=document.createElement('span');slot.className='presentation-overview-slot';slot.innerHTML=back;row.insertBefore(slot,row.firstChild);}row.classList.toggle('has-content',!!txt||!!IS_READONLY||!!back);}var left=document.getElementById('presentationLeft');if(!left)return;var backCls=navHistory.length?'pi-move':'pi-move pi-disabled';var fwdCls=navFuture.length?'pi-move':'pi-move pi-disabled';var navBackBtn='';var navFwdBtn='';var undoClass=undoStack.length?'pi-undo':'pi-undo pi-disabled';var undoBtn='';var viewBtn=currentPageSupportsPageView()?'Page View':'';var stepNav=pageStepNavHTML();left.innerHTML=navBackBtn+navFwdBtn+stepNav+viewBtn+undoBtn+presentationContextActionHTML();} function clearSelectedBoxContext(){if(!openBox)return;closeEntry();revealBox=null;revealType=null;inspectHome=null;inspectLinkedKeys=[];sourceRefBox=null;render();} function presentationDeselectBox(){if(!presentationView||!openBox)return;clearSelectedBoxContext();} function normalDeselectBox(){if(presentationView||!openBox)return;clearSelectedBoxContext();} /* Normal View genuine empty-canvas deselect preserves the current board scroll position while closing the selected-box details pane. */ function normalDeselectBoxPreserveScroll(){if(presentationView||!openBox)return;var main=document.getElementById('main');var sx=main?main.scrollLeft:0;var sy=main?main.scrollTop:0;clearSelectedBoxContext();if(main){var restore=function(){main.scrollLeft=sx;main.scrollTop=sy;};restore();if(typeof queueMicrotask==='function')queueMicrotask(restore);else setTimeout(restore,0);if(typeof requestAnimationFrame==='function')requestAnimationFrame(restore);}} function boardClickAway(ev){if(!openBox)return;if(!ev||!ev.target)return;var t=ev.target;if(t.isContentEditable)return;var interactive='#epWrap,.box,.how-box,.fo-box,.box-chooser,.modal-ov,.modal,.ptc-ov,.chat-panel,.chat-fab,button,a,select,input,textarea,[contenteditable=true],.box-jump,.box-jump-tri,.tl-dot,.pri-badge,.tt-aff,.how-aff,.tt-cnt,.tt-cnt-empty,.tt-cnt-hotspot,.how-cnt-bottom,.how-cnt-bottom-empty,.why-cnt-top,.why-cnt-top-empty,.how-how-cnt-bottom,.how-lateral-sq,.tt-lateral-sq,.link-info-btn,.tt-link-abtn,.ttl-item,.view-btn,.pi-pill,.pi-link,.pi-move,.pi-undo,.presentation-bar,.presentation-statement-row,.presentation-btn,.hdr,.page-info-bar,.control-bar,.src-ref-box,.tag-pill,.tag-chip,.meq-under-box,.box-under-block';if(t.closest&&t.closest(interactive))return;var main=document.getElementById('main');if(main&&(t===main||(main.contains&&main.contains(t)))){if(presentationView)presentationDeselectBox();else normalDeselectBoxPreserveScroll();}} /* Genuine empty-canvas clicks clear selected-box context in Normal View as well as Presentation View; Escape remains Presentation View-only. */ document.addEventListener('keydown',function(ev){if(!presentationView||!openBox)return;if(ev.key!=='Escape')return;var t=ev.target;if(t&&(t.tagName==='INPUT'||t.tagName==='TEXTAREA'||t.tagName==='SELECT'||t.isContentEditable))return;presentationDeselectBox();}); function applyPresentationView(){var on=!!presentationView;if(document&&document.body){document.body.classList.toggle('presentation-view',on);}updatePresentationBar();} function setPresentationView(on){presentationView=!!on;applyPresentationView();saveState();render();} function render(){markRuntimeLinkIndexDirty();getRuntimeLinkIndex();applyPresentationView();renderTabBar();renderPageInfoBar();const b=document.getElementById('board');if(currentPage==='overview')b.innerHTML=renderOverview();else if(currentPage==='final')b.innerHTML=renderFinalOutcomes();else{const sp=SP.find(s=>s.id===currentPage);if(sp&&sp.pageType==='how')b.innerHTML=renderHowPage(currentPage);else if(sp&&sp.pageType==='documentation')b.innerHTML=renderDocPage(currentPage);else b.innerHTML=renderSubpage(currentPage);}updatePresentationBar();updateSaveStateDisplay();if(openBox)renderEntryPanel();else document.getElementById('epWrap').style.display='none';renderSourceRefBox();wireLinkHoverPopups();} // save-state status display replaces the previous complete/yellow/blockers line. States: Saving... (while an explicit HTML save is actively running, if that path is hit), Last saved: [time] (clean state after a successful HTML save), Not saved since changes (dirty state after any edit since the last HTML save), or Not yet saved (initial state). Kept lightweight and unobtrusive \u2014 no toasts, no banners, no modal warnings are added by this fix. Time format uses local locale short time. function updateSaveStateDisplay(){var el=document.getElementById('saveStateTxt');if(!el)return;var txt; // in a read-only copy, the save-state wording must not misleadingly suggest editable unsaved changes. Editing is disabled through the UI, so the indicator shows a plain static label instead of the normal dirty/saved/saving states. No new warning banner or modal is added by this fix \u2014 it stays lightweight. if(IS_READONLY){el.textContent=' \u00B7 Read-only copy';return;} if(isSaving){txt=' \u00B7 Saving\u2026';}else if(isDirty){txt=' \u00B7 Not saved since changes';}else if(lastSavedTime){try{var d=new Date(lastSavedTime);var hr=d.getHours();var mn=String(d.getMinutes()).padStart(2,'0');var ampm=hr>=12?'pm':'am';var hr12=hr%12||12;var today=new Date();var sameDay=d.toDateString()===today.toDateString();var whenStr=sameDay?(hr12+':'+mn+ampm):(d.toLocaleDateString()+' '+hr12+':'+mn+ampm);txt=' \u00B7 Last saved: '+whenStr;}catch(e){txt=' \u00B7 Last saved';}}else{txt=' \u00B7 Not yet saved';}el.textContent=txt;} // kept as a no-op alias so any residual internal callers of updateStatus() remain safe. The complete/yellow/blockers line it used to write is gone by design \u2014 that data is still visible per-box via traffic-light dots, and is still included in the DOVIEW-STATE snapshot summary line and in the board-chat system prompt. function updateStatus(){updateSaveStateDisplay();} // ─── ENTRY PANEL ─── function clickBox(ev,key){if(!ev){ev=window.event;}if(ev&&ev.target&&ev.target.isContentEditable)return; // if the ghost/context box is currently showing and the user clicks a box that is NOT part of the current linked/highlighted navigation context, dismiss it \u2014 it is a temporary navigation-context aid, not a pinned banner. Treated as same context: the source box itself (so the user can click it without losing the banner) and any currently inspectLinkedKeys (so clicking a highlighted linked target does not dismiss). Anything else counts as focus change / new unrelated inspect and clears sourceRefBox and the stale inspect state together so the new click can establish its own reveal/inspect context cleanly. Manual close via the \u2715 control still works via dismissSourceRef. if(sourceRefBox){var _srcRefKey=sourceRefBox.sourceKey;var _keyInCtx=(key===_srcRefKey)||(Array.isArray(inspectLinkedKeys)&&inspectLinkedKeys.indexOf(key)>=0);if(!_keyInCtx){sourceRefBox=null;inspectHome=null;inspectLinkedKeys=[];}} if(_clickTimer){clearTimeout(_clickTimer);_clickTimer=null;}const isTT=isTTBox(key);const isHow=isHowBox(key);if(isTT&&ev&&ev.target){const el=document.getElementById('bx-'+key);if(el){const r=el.getBoundingClientRect();const x=ev.clientX-r.left,y=ev.clientY-r.top; // enlarged, clearly-separated TT reveal-arrow click zone. Middle-left and middle-right regions only. X within 22px of the edge, Y between ~35% and ~60% of the box height, so it stays clearly separate from the top Cross-Link link affordance, the Vertical Link count affordance, the jump triangle zone, and the right-edge outgoing-count circle (each of which absorbs its own clicks via stopPropagation). This check runs BEFORE the border-zone check so a middle-band click in inspect/reveal state reliably opens the link popup instead of entering a new reveal or falling through to the entry panel. const _arrowX=22;const _arrowYMin=r.height*0.35;const _arrowYMax=r.height*0.60;const _inArrowBand=(y>=_arrowYMin&&y<=_arrowYMax); if(_inArrowBand){ if(inspectHome&&inspectLinkedKeys.indexOf(key)>=0&&key!==inspectHome){const outLink=ttLinkBetween(inspectHome,key);const inLink=ttLinkBetween(key,inspectHome);if(outLink&&x<_arrowX){ev.stopPropagation();openLinkDetail(outLink.id);return;}if(inLink&&x>r.width-_arrowX){ev.stopPropagation();openLinkDetail(inLink.id);return;}} if(revealBox&&revealType==='tt'&&key!==revealBox){const outLink=ttLinkBetween(revealBox,key);const inLink=ttLinkBetween(key,revealBox);if(outLink&&x<_arrowX){ev.stopPropagation();openLinkDetail(outLink.id);return;}if(inLink&&x>r.width-_arrowX){ev.stopPropagation();openLinkDetail(inLink.id);return;}} } const bz=10;if(xr.width-bz||yr.height-bz){ev.stopPropagation(); // narrow (bz=10) border zone remains the entry point for border-click reveal mode. Arrow-zone openLinkDetail cases already handled above. // Border click enters inspect mode showing both directions with greyed connected boxes revealBox=null;revealType=null;inspectHome=key;inspectLinkedKeys=linkedKeysForSource(key);openBox=null;editingType=null;document.getElementById('epWrap').style.display='none';/* border-click activation of inspect mode immediately pins the ghost/context box on the source page with the heading 'Showing links for this box on this page'. */setSourceRefBox(key,'inspect');refreshGhostLinkContext(key);render();return;}}}if(isHow&&ev&&ev.target){const el=document.getElementById('bx-'+key);if(el){const r=el.getBoundingClientRect();const x=ev.clientX-r.left,y=ev.clientY-r.top;const bz=10;if(xr.width-bz||yr.height-bz){ev.stopPropagation();revealBox=null;revealType=null;inspectHome=key;inspectLinkedKeys=linkedKeysForSource(key);openBox=null;editingType=null;document.getElementById('epWrap').style.display='none';setSourceRefBox(key,'inspect');refreshGhostLinkContext(key);render();return;}}}_clickTimer=setTimeout(function(){_clickTimer=null;if(isHow){inspectHome=null;inspectLinkedKeys=[];revealBox=null;revealType=null;sourceRefBox=null;}else{inspectHome=null;inspectLinkedKeys=[];revealBox=null;revealType=null;}if(openBox===key){closeEntry();return;}openBox=key;entryMode='sofar';editingType=null;render();},250);} function closeEntry(){openBox=null;editingType=null;document.getElementById('epWrap').style.display='none';} function dblEditBox(event,key){if(IS_READONLY){event.stopPropagation();event.preventDefault();return;}event.stopPropagation();event.preventDefault();if(_clickTimer){clearTimeout(_clickTimer);_clickTimer=null;}const box=B[key];if(!box)return;const el=document.querySelector(`#bx-${key} .box-txt`);if(!el)return;el.style.display='block';el.style.webkitLineClamp='unset';el.style.overflow='visible';el.contentEditable='true';el.focus();const sel=window.getSelection();sel.selectAllChildren(el);el.style.outline='2px solid #F5A623';el.style.borderRadius='3px';el.style.background='#fff';const finish=function(){el.contentEditable='false';el.style.outline='';el.style.background='';el.style.display='';el.style.webkitLineClamp='';el.style.overflow='';const newLabel=el.textContent.trim();if(newLabel!==box.label||newLabel===''){takeSnapshot();box.label=newLabel;const m=parseTTBoxKey(key);if(m){const sp=SP.find(s=>s.id===m.spId);if(sp&&sp.cols[m.ci])sp.cols[m.ci].boxes[m.bi]=newLabel;}const hm=parseHowBoxKey(key);if(hm){const sp=SP.find(s=>s.id===hm.spId);if(sp&&sp.howBoxes){const hb=sp.howBoxes.find(h=>h.id===hm.howId);if(hb)hb.label=newLabel;}}saveState();}render();};el.addEventListener('blur',finish,{once:true});el.addEventListener('keydown',function(e){if(e.key==='Enter'){e.preventDefault();el.blur();}if(e.key==='Escape'){el.textContent=box.label;el.blur();}});} function dblEditColH(event,spId,ci){if(IS_READONLY){event.stopPropagation();event.preventDefault();return;}event.stopPropagation();event.preventDefault();const sp=SP.find(s=>s.id===spId);if(!sp||!sp.cols[ci])return;const el=document.getElementById(`ch-${spId}-c${ci}`);if(!el)return;el.contentEditable='true';el.style.outline='2px solid #F5A623';el.style.borderRadius='3px';el.style.background='#fff';el.style.textTransform='none';el.focus();const sel=window.getSelection();sel.selectAllChildren(el);const oldH=sp.cols[ci].h;const finish=function(){el.contentEditable='false';el.style.outline='';el.style.background='';el.style.textTransform='';const newH=el.textContent.trim();if(newH&&newH!==oldH){takeSnapshot();sp.cols[ci].h=newH;saveState();}render();};el.addEventListener('blur',finish,{once:true});el.addEventListener('keydown',function(e){if(e.key==='Enter'){e.preventDefault();el.blur();}if(e.key==='Escape'){el.textContent=oldH;el.blur();}});} function confirmDeleteBox(key){if(IS_READONLY)return;const box=B[key];if(!box)return;if(!confirm('Delete this box: "'+box.label+'"?'))return;takeSnapshot();const m=parseTTBoxKey(key);if(m){const spId=m.spId,ci=m.ci,bi=m.bi;const sp=SP.find(s=>s.id===spId);if(sp&&sp.cols[ci]&&sp.cols[ci].boxes[bi]!==undefined){/* Preserve box state and links while positional keys shift. */const oldColLen=sp.cols[ci].boxes.length;const keyMap={};keyMap[`${spId}-c${ci}-b${bi}`]='';for(let j=bi+1;js.id===spId);if(sp&&sp.cols[ci]){sp.cols[ci].boxes.push('New box');const bi=sp.cols[ci].boxes.length-1;const k=`${spId}-c${ci}-b${bi}`;B[k]={label:'New box',light:'',entries:[],priority:'',hasSubpage:false,detailText:'',borderColor:'',boxColor:'',/* stable uid at creation. */uid:_genUid('box')};buildDepsMap();cascade();saveState();render();}} function confirmDeleteColumn(key){if(IS_READONLY)return;const m=parseTTBoxKey(key);if(!m)return;const spId=m.spId,ci=m.ci;const sp=SP.find(s=>s.id===spId);if(!sp||!sp.cols[ci])return;if(!confirm('Delete column "'+sp.cols[ci].h+'" and all its boxes?'))return;takeSnapshot();/* Preserve box state and links while positional keys shift. */const oldColCount=sp.cols.length;const keyMap={};for(let cj=0;cjci){keyMap[oldK]=`${spId}-c${cj-1}-b${bi}`;}}}sp.cols.splice(ci,1);rebuildBoxKeys(spId,keyMap);buildDepsMap();cascade();saveState();closeEntry();render();} function promptAddColumn(key){if(IS_READONLY)return;const m=parseTTBoxKey(key);if(!m)return;const spId=m.spId,ci=m.ci;takeSnapshot();const sp=SP.find(s=>s.id===spId);if(sp){const newCi=ci+1;/* Preserve box state and links while positional keys shift. */const oldColCount=sp.cols.length;const keyMap={};for(let cj=newCi;cjs.id===spId);if(!sp||!sp.cols[ci])return;takeSnapshot();/* Preserve box state and links while positional keys shift. */const keyMap={};keyMap[`${spId}-c${ci}-b${bi-1}`]=`${spId}-c${ci}-b${bi}`;keyMap[`${spId}-c${ci}-b${bi}`]=`${spId}-c${ci}-b${bi-1}`;const boxes=sp.cols[ci].boxes;const tmp=boxes[bi];boxes[bi]=boxes[bi-1];boxes[bi-1]=tmp;rebuildBoxKeys(spId,keyMap);buildDepsMap();cascade();saveState();openBox=`${spId}-c${ci}-b${bi-1}`;render();} function moveBoxDown(key){if(IS_READONLY)return;const m=parseTTBoxKey(key);if(!m)return;const spId=m.spId,ci=m.ci,bi=m.bi;const sp=SP.find(s=>s.id===spId);if(!sp||!sp.cols[ci])return;if(bi>=sp.cols[ci].boxes.length-1)return;takeSnapshot();/* Preserve box state and links while positional keys shift. */const keyMap={};keyMap[`${spId}-c${ci}-b${bi}`]=`${spId}-c${ci}-b${bi+1}`;keyMap[`${spId}-c${ci}-b${bi+1}`]=`${spId}-c${ci}-b${bi}`;const boxes=sp.cols[ci].boxes;const tmp=boxes[bi];boxes[bi]=boxes[bi+1];boxes[bi+1]=tmp;rebuildBoxKeys(spId,keyMap);buildDepsMap();cascade();saveState();openBox=`${spId}-c${ci}-b${bi+1}`;render();} function moveTTBoxHorizontal(key,direction){if(IS_READONLY)return;const m=parseTTBoxKey(key);if(!m)return;const sp=SP.find(s=>s.id===m.spId);if(!sp||!Array.isArray(sp.cols)||!sp.cols[m.ci])return;const targetCi=m.ci+(direction==='left'?-1:direction==='right'?1:0);if(targetCi<0||targetCi>=sp.cols.length||targetCi===m.ci)return;const sourceBoxes=sp.cols[m.ci].boxes||[];const targetBoxes=sp.cols[targetCi].boxes||[];if(m.bi<0||m.bi>=sourceBoxes.length)return;const insertBi=Math.min(m.bi,targetBoxes.length);const keyMap={};keyMap[`${m.spId}-c${m.ci}-b${m.bi}`]=`${m.spId}-c${targetCi}-b${insertBi}`;for(let i=m.bi+1;is.id===spId);if(!sp||sp.pageType!=='how')return;takeSnapshot();if(!sp.howBoxes)sp.howBoxes=[];const num=sp.nextHowNum||(sp.howBoxes.length+1);const id='H'+String(num).padStart(3,'0');sp.nextHowNum=num+1;const label='How (action, person, team, unit, organization, capability, \u2026)';/* stable uid on the new How Box record + its B[k] entry. */const _hbUid=_genUid('box');sp.howBoxes.push({id,label,uid:_hbUid});B[`${spId}-${id}`]={label,light:'',entries:[],priority:'',hasSubpage:false,detailText:'',borderColor:'',uid:_hbUid}; saveState();render();} function addHowBoxAfter(spId,howId){if(IS_READONLY)return;const sp=SP.find(s=>s.id===spId);if(!sp||sp.pageType!=='how'||!sp.howBoxes)return;const idx=sp.howBoxes.findIndex(h=>h.id===howId);if(idx<0)return;takeSnapshot();const num=sp.nextHowNum||(sp.howBoxes.length+1);const newId='H'+String(num).padStart(3,'0');sp.nextHowNum=num+1;const label='How (action, person, team, unit, organization, capability, \u2026)';/* stable uid on the new How Box record + its B[k] entry. */const _hbUid=_genUid('box');sp.howBoxes.splice(idx+1,0,{id:newId,label,uid:_hbUid});B[`${spId}-${newId}`]={label,light:'',entries:[],priority:'',hasSubpage:false,detailText:'',borderColor:'',uid:_hbUid}; saveState();render();} function deleteHowBox(spId,howId){if(IS_READONLY)return;const sp=SP.find(s=>s.id===spId);if(!sp||!sp.howBoxes)return;const key=`${spId}-${howId}`;if(!confirm('Delete this box: "'+( B[key]?.label||howId)+'"?'))return;takeSnapshot();const idx=sp.howBoxes.findIndex(h=>h.id===howId);if(idx>=0)sp.howBoxes.splice(idx,1);delete B[key];cleanupHowLinks();saveState();closeEntry();render();} function moveHowBox(spId,howId,direction){if(IS_READONLY)return;const sp=SP.find(s=>s.id===spId);if(!sp||!sp.howBoxes)return;const idx=sp.howBoxes.findIndex(h=>h.id===howId);if(idx<0)return;let newIdx;if(direction==='earlier')newIdx=idx-1;else if(direction==='later')newIdx=idx+1;else if(direction==='first')newIdx=0;else if(direction==='last')newIdx=sp.howBoxes.length-1;else return;if(newIdx<0||newIdx>=sp.howBoxes.length||newIdx===idx)return;takeSnapshot();const item=sp.howBoxes.splice(idx,1)[0];sp.howBoxes.splice(newIdx,0,item);saveState();render();} function moveHowBoxVertical(spId,howId,direction){if(IS_READONLY||direction!=='up'&&direction!=='down')return;const sp=SP.find(s=>s.id===spId);if(!sp||!Array.isArray(sp.howBoxes))return;const grid=document.querySelector('.how-grid');if(!grid)return;const positions=Array.from(grid.children).map(function(wrap){const box=wrap.querySelector&&wrap.querySelector('.how-box[id^="bx-"]');if(!box)return null;const m=parseHowBoxKey(box.id.slice(3));if(!m||m.spId!==spId)return null;const rect=wrap.getBoundingClientRect();return {howId:m.howId,left:Math.round(rect.left),top:Math.round(rect.top)};}).filter(Boolean);const current=positions.find(function(p){return p.howId===howId;});if(!current)return;const sameColumn=positions.filter(function(p){return Math.abs(p.left-current.left)<=2;}).sort(function(a,b){return a.top-b.top;});const visualIdx=sameColumn.findIndex(function(p){return p.howId===howId;});const targetVisualIdx=visualIdx+(direction==='up'?-1:1);if(visualIdx<0||targetVisualIdx<0||targetVisualIdx>=sameColumn.length)return;const idx=sp.howBoxes.findIndex(function(h){return h.id===howId;});const targetIdx=sp.howBoxes.findIndex(function(h){return h.id===sameColumn[targetVisualIdx].howId;});if(idx<0||targetIdx<0||idx===targetIdx)return;takeSnapshot();const tmp=sp.howBoxes[idx];sp.howBoxes[idx]=sp.howBoxes[targetIdx];sp.howBoxes[targetIdx]=tmp;saveState();render();} function finalOutcomeIndexFromKey(key){var m=key&&key.match(/^final-b(\d+)$/);if(!m)return -1;var idx=parseInt(m[1]);return idx>=0&&idx=FO_DATA.length)return;takeSnapshot();const tmp=FO_DATA[i];FO_DATA[i]=FO_DATA[i-1];FO_DATA[i-1]=tmp;const keyMap={};keyMap['final-b'+i]='final-b'+(i-1);keyMap['final-b'+(i-1)]='final-b'+i;rebuildFinalKeysWithMap(keyMap);saveState();openBox='final-b'+(i-1);render();} function moveFinalDown(i){if(IS_READONLY)return;if(i<0||i>=FO_DATA.length-1)return;takeSnapshot();const tmp=FO_DATA[i];FO_DATA[i]=FO_DATA[i+1];FO_DATA[i+1]=tmp;const keyMap={};keyMap['final-b'+i]='final-b'+(i+1);keyMap['final-b'+(i+1)]='final-b'+i;rebuildFinalKeysWithMap(keyMap);saveState();openBox='final-b'+(i+1);render();} function confirmDeleteFinal(i){if(IS_READONLY)return;if(i<0||i>=FO_DATA.length)return;if(!confirm('Delete final outcome: "'+FO_DATA[i]+'"?'))return;takeSnapshot();var oldLen=FO_DATA.length;var keyMap={};keyMap['final-b'+i]='';for(var j=i+1;j=FO_DATA.length)return;const k='final-b'+i;const el=document.querySelector('#bx-'+k+' .box-txt');if(!el)return;el.contentEditable='true';el.focus();var sel=window.getSelection();sel.selectAllChildren(el);el.style.outline='2px solid #F5A623';el.style.borderRadius='3px';el.style.background='#fff';const oldLabel=FO_DATA[i];const finish=function(){el.contentEditable='false';el.style.outline='';el.style.background='';const newLabel=el.textContent.trim();if(newLabel&&newLabel!==oldLabel){takeSnapshot();FO_DATA[i]=newLabel;if(B[k])B[k].label=newLabel;saveState();}render();};el.addEventListener('blur',finish,{once:true});el.addEventListener('keydown',function(e){if(e.key==='Enter'){e.preventDefault();el.blur();}if(e.key==='Escape'){el.textContent=oldLabel;el.blur();}});} function dblEditTab(event,spId){if(IS_READONLY){event.stopPropagation();event.preventDefault();return;}event.stopPropagation();event.preventDefault();if(_clickTimer){clearTimeout(_clickTimer);_clickTimer=null;}const sp=SP.find(function(s){return s.id===spId;});if(!sp)return;var el=document.querySelector('#tabBar [data-spid="'+spId+'"]');if(!el)return;el.contentEditable='true';el.focus();var sel=window.getSelection();sel.selectAllChildren(el);el.style.outline='2px solid #F5A623';el.style.borderRadius='3px';const oldLabel=sp.label;const finish=function(){el.contentEditable='false';el.style.outline='';const newLabel=el.textContent.trim();if(newLabel&&newLabel!==oldLabel){takeSnapshot();sp.label=newLabel;saveState();}render();};el.addEventListener('blur',finish,{once:true});el.addEventListener('keydown',function(e){if(e.key==='Enter'){e.preventDefault();el.blur();}if(e.key==='Escape'){el.textContent=oldLabel;el.blur();}});} // inline-edit the page name shown in the top page bar (tt-bar, how-bar, doc-bar). Edits the same underlying sp.label as the tab, so tab and bar stay in sync. function dblEditPageBarTitle(event,spId){if(IS_READONLY){event.stopPropagation();event.preventDefault();return;}event.stopPropagation();event.preventDefault();if(_clickTimer){clearTimeout(_clickTimer);_clickTimer=null;}const sp=SP.find(function(s){return s.id===spId;});if(!sp)return;var el=event.currentTarget;if(!el)return;el.contentEditable='true';el.focus();var sel=window.getSelection();sel.selectAllChildren(el);el.style.outline='2px solid rgba(255,255,255,0.8)';el.style.background='rgba(255,255,255,0.12)';el.style.borderRadius='3px';el.style.padding='0 4px';const oldLabel=sp.label;const finish=function(){el.contentEditable='false';el.style.outline='';el.style.background='';el.style.padding='';const newLabel=el.textContent.trim();if(newLabel&&newLabel!==oldLabel){takeSnapshot();sp.label=newLabel;saveState();}render();};el.addEventListener('blur',finish,{once:true});el.addEventListener('keydown',function(e){if(e.key==='Enter'){e.preventDefault();el.blur();}if(e.key==='Escape'){el.textContent=oldLabel;el.blur();}});} function dblEditTitle(event){if(IS_READONLY){event.stopPropagation();event.preventDefault();return;}event.stopPropagation();event.preventDefault();const el=document.getElementById('hdrTitle');if(!el)return;el.contentEditable='true';el.focus();var sel=window.getSelection();sel.selectAllChildren(el);el.style.outline='2px solid rgba(255,255,255,0.5)';el.style.borderRadius='3px';const oldTitle=TITLE;const finish=function(){el.contentEditable='false';el.style.outline='';const newTitle=el.textContent.trim();if(newTitle&&newTitle!==oldTitle){takeSnapshot();TITLE=newTitle;document.title=TITLE;saveState();}render();};el.addEventListener('blur',finish,{once:true});el.addEventListener('keydown',function(e){if(e.key==='Enter'){e.preventDefault();el.blur();}if(e.key==='Escape'){el.textContent=oldTitle;el.blur();}});} function addNewPage(){if(IS_READONLY)return;document.getElementById('pageTypeChooser').classList.add('open');} function closePageChooser(){document.getElementById('pageTypeChooser').classList.remove('open');} function closeHowPageTypeChooser(){var el=document.getElementById('howPageTypeChooser');if(el)el.classList.remove('open');} function normalizedVerticalHowLevel(level){if(level===null||level===undefined)return null;var n=parseInt(level,10);return Number.isFinite(n)&&n>0?n:null;} function verticalHowPageAtLevel(level,excludeId){var n=normalizedVerticalHowLevel(level);if(n===null)return null;return SP.find(function(s){return s&&s.id!==excludeId&&s.pageType==='how'&&normalizedVerticalHowLevel(s.howLevel)===n;})||null;} function hasLevelOneHowPage(){return !!verticalHowPageAtLevel(1);} function nextVerticalHowLevel(){var maxLvl=0;SP.forEach(function(s){var n=s&&s.pageType==='how'?normalizedVerticalHowLevel(s.howLevel):null;if(n!==null&&n>maxLvl)maxLvl=n;});return hasLevelOneHowPage()?Math.max(2,maxLvl+1):1;} function hasVerticalHowPageLevel(level){return !!verticalHowPageAtLevel(level);} function hasThisThenPage(){return SP.some(function(s){return s&&(s.pageType||'this_then')==='this_then';});} function missingPreviousVerticalHowLevel(level){level=parseInt(level,10);if(!level||level<=1)return 0;var previous=level-1;return hasVerticalHowPageLevel(previous)?0:previous;} function warnMissingPreviousVerticalHowLevel(level){var previous=missingPreviousVerticalHowLevel(level);if(!previous)return false;alert('Create a Level '+previous+' Vertical Link How Page first.');return true;} function warnDuplicateVerticalHowLevel(level,excludeId){var n=normalizedVerticalHowLevel(level);if(n===null)return false;var existing=verticalHowPageAtLevel(n,excludeId);if(!existing)return false;alert('A Level '+n+' How Page already exists: "'+(existing.label||existing.id)+'". There is only one vertical How Page hierarchy. Use one page per numbered level, or create a Cross-Link How Page.');return true;} function warnNoSelectedTarget(){alert('Choose a target first.');} function buildHowLevelOptionsHTML(){var maxLvl=3;SP.forEach(function(s){var n=s&&s.pageType==='how'?normalizedVerticalHowLevel(s.howLevel):null;if(n!==null&&n>=maxLvl)maxLvl=n+1;});var selected=nextVerticalHowLevel();var html='';for(var lvl=1;lvl<=maxLvl;lvl++){var disabled=hasVerticalHowPageLevel(lvl);html+='';}return html;} function openHowPageTypeChooser(){if(IS_READONLY)return;closePageChooser();var sel=document.getElementById('howPageLevelSelect');if(sel)sel.innerHTML=buildHowLevelOptionsHTML();var notice=document.getElementById('howLevel1Notice');if(notice)notice.style.display=SP.some(function(s){return s&&s.pageType==='how'&&normalizedVerticalHowLevel(s.howLevel)!==null;})?'block':'none';var el=document.getElementById('howPageTypeChooser');if(el)el.classList.add('open');} function confirmCreateVerticalHowPage(){var sel=document.getElementById('howPageLevelSelect');var lvl=sel?parseInt(sel.value,10):nextVerticalHowLevel();if(warnDuplicateVerticalHowLevel(lvl)){openHowPageTypeChooser();return;}if(warnMissingPreviousVerticalHowLevel(lvl))return;closeHowPageTypeChooser();createPageWithType('how',{howLevel:lvl,label:'Vertical Link How Page'});} function confirmCreateCrossLinkHowPage(){closeHowPageTypeChooser();createPageWithType('how',{howLevel:null,label:'Cross-Link How Page'});} function createPageWithType(pageType,opts){if(IS_READONLY)return;opts=opts||{};closePageChooser();const wasSingle=SP.length===1;const avail=nextAutoPageColor();const id=nextUnusedPageId();if(pageType==='how'){var hasExplicitLevel=Object.prototype.hasOwnProperty.call(opts,'howLevel');var requestedLevel=hasExplicitLevel?opts.howLevel:nextVerticalHowLevel();var howLevel=requestedLevel===null?null:normalizedVerticalHowLevel(requestedLevel);if(howLevel===null&&requestedLevel!==null)howLevel=nextVerticalHowLevel();if(warnDuplicateVerticalHowLevel(howLevel))return;if(warnMissingPreviousVerticalHowLevel(howLevel))return;takeSnapshot();const howBoxes=[];for(let i=1;i<=8;i++){const hid='H'+String(i).padStart(3,'0');howBoxes.push({id:hid,label:'How (action, person, team, unit, organization, capability, \u2026)'});}SP.push({id:id,label:opts.label||'How: New page',pageType:'how',color:{bg:avail.bg,bdr:avail.bdr,tab:avail.tab},cols:[],howBoxes:howBoxes,nextHowNum:9,howLevel:howLevel});howBoxes.forEach(hb=>{B[`${id}-${hb.id}`]={label:hb.label,light:'',entries:[],priority:'',hasSubpage:false,detailText:'',borderColor:''};});}else{takeSnapshot();SP.push({id:id,label:(pageType==='documentation'?'Documentation: New page':'This\u2192Then: New page'),pageType:pageType,color:{bg:avail.bg,bdr:avail.bdr,tab:avail.tab},cols:[{h:'New column',boxes:['New box']}]});const k=id+'-c0-b0';B[k]={label:'New box',light:'',entries:[],priority:'',hasSubpage:false,detailText:'',borderColor:''};}if(wasSingle){currentPage='overview';}buildDepsMap();saveState();navTo(id);} function movePageLeft(ev,pid){if(ev&&ev.stopPropagation)ev.stopPropagation();if(IS_READONLY)return;var idx=SP.findIndex(function(s){return s.id===pid;});if(idx<=0)return;takeSnapshot();var tmp=SP[idx];SP[idx]=SP[idx-1];SP[idx-1]=tmp;saveState();render();} function movePageRight(ev,pid){if(ev&&ev.stopPropagation)ev.stopPropagation();if(IS_READONLY)return;var idx=SP.findIndex(function(s){return s.id===pid;});if(idx<0||idx>=SP.length-1)return;takeSnapshot();var tmp=SP[idx];SP[idx]=SP[idx+1];SP[idx+1]=tmp;saveState();render();} function toggleHowLevel(pid){if(IS_READONLY)return;var sp=SP.find(function(s){return s.id===pid;});if(!sp||sp.pageType!=='how')return;takeSnapshot();if(sp.howLevel){sp.howLevel=null;}else{var maxLvl=0;SP.forEach(function(s){if(s.pageType==='how'&&s.howLevel&&s.howLevel>maxLvl)maxLvl=s.howLevel;});sp.howLevel=maxLvl+1;}saveState();render();} function moveHowLevelUp(pid){if(IS_READONLY)return;var sp=SP.find(function(s){return s.id===pid;});if(!sp||sp.pageType!=='how')return;var lvl=sp.howLevel||1;if(lvl<=1)return;var swap=SP.find(function(s){return s.pageType==='how'&&s.howLevel===lvl-1;});takeSnapshot();sp.howLevel=lvl-1;if(swap)swap.howLevel=lvl;saveState();render();} function moveHowLevelDown(pid){if(IS_READONLY)return;var sp=SP.find(function(s){return s.id===pid;});if(!sp||sp.pageType!=='how')return;var lvl=sp.howLevel||1;var howPages=SP.filter(function(s){return s.pageType==='how';});if(lvl>=howPages.length)return;var swap=SP.find(function(s){return s.pageType==='how'&&s.howLevel===lvl+1;});takeSnapshot();sp.howLevel=lvl+1;if(swap)swap.howLevel=lvl;saveState();render();} function confirmDeletePage(pid){if(IS_READONLY)return;var sp=SP.find(function(s){return s.id===pid;});if(!sp)return;if(!confirm('Delete page "'+sp.label+'" and all its boxes?'))return;takeSnapshot();Object.keys(B).filter(function(k){return k.startsWith(pid+'-');}).forEach(function(k){delete B[k];});var idx=SP.findIndex(function(s){return s.id===pid;});SP.splice(idx,1);delete pageInfo[pid];delete docContent[pid];cleanupTTLinks();cleanupHowLinks();buildDepsMap();cascade();saveState();if(SP.length===1){currentPage=SP[0].id;render();}else{navTo('overview');}} function renderEntryPanel(){ const key=openBox;if(!key||!B[key])return;const box=B[key];if(!Array.isArray(box.entries))box.entries=[];const ep=document.getElementById('epWrap');ep.style.display='block'; if(!['note1','note2','note3','note4','note5'].includes(entryMode))entryMode='note1'; const tls=['','green','greenYellow','yellow','yellowRed','red','grey'].map(t=>{const state=normalizeTrafficLightValue(t);const split=tlSplitClass(state);const active=normalizeTrafficLightValue(box.light)===state?' dot-active':'';const cls='tl-dot-btn'+(state?'':' tl-empty')+active+(split?' '+split:'');const bg=state?tlColor(state):'#fff';return `
`;}).join(''); const curPri=normalizePriorityValue(box.priority); const pris=PRIORITY_VALUES.map(p=>``).join(''); function vals(types){return box.entries.filter(e=>types.indexOf(e.type)>=0).map(e=>e.text).filter(Boolean);} function row(label,types,mode){const arr=vals(types);if(!arr.length)return '';return `
${label}:${arr.map(t=>linkifyDisplay(t)).join(' | ')}
`;} const n1R=row('Notes 1',['note1','sofar'],'note1'); const n2R=row('Notes 2',['note2','note'],'note2'); const n3R=row('Notes 3',['note3'],'note3'); const n4R=row('Notes 4',['note4'],'note4'); const n5R=row('Notes 5',['note5'],'note5'); const noteSummary=(n1R||n2R||n3R||n4R||n5R)?'
'+n1R+n2R+n3R+n4R+n5R+'
':'
No notes yet.
'; const noteName=entryMode==='note1'?'Notes 1':entryMode==='note2'?'Notes 2':entryMode==='note3'?'Notes 3':entryMode==='note4'?'Notes 4':'Notes 5'; const ph='Add a note to '+noteName+'...'; const addBtnLabel=(editingType?'Update ':'Add to ')+noteName; const mc={note1:'pill-n1',note2:'pill-n2',note3:'pill-n3',note4:'pill-n1',note5:'pill-n2'}; ep.innerHTML=`
Box: ${escHtml(box.label)}
${buildBoxTagsSection(key)}${buildHowDisplayIdSection(key)}
Quick settings
Status
${tls}
Priority
${pris}
ColorsBox Border
${buildMainTextSection(key)}
Notes
${noteSummary}
${buildJumpToPageSection(key)}${buildMeasuresSection(key)}${buildEQSection(key)}
`; } async function setMode(mode){if(IS_READONLY)return;editingType=null;const ta=document.getElementById('epTa');const ct=ta?ta.value.trim():'';if(ct){await doSaveEntry(ct,mode);if(mode===entryMode){ta.value='';renderEntryPanel();return;}}entryMode=mode;renderEntryPanel();setTimeout(()=>{const t=document.getElementById('epTa');if(t)t.focus();},50);} function editEntries(type){const typeMap={note1:['note1','sofar'],note2:['note2','note'],note3:['note3'],note4:['note4'],note5:['note5']};const types=typeMap[type]||[type];editingType=type;entryMode=type;const box=B[openBox];const entries=box.entries.filter(e=>types.indexOf(e.type)>=0).map(e=>e.text);renderEntryPanel();setTimeout(()=>{const ta=document.getElementById('epTa');if(ta){ta.value=entries.join(' | ');ta.focus();}},50);} // snapshot prior state BEFORE applying a traffic-light change so undo reliably restores the previous value. No-op (and no snapshot) if the value is unchanged. function setTL(key,state){if(IS_READONLY)return;if(!B[key])return;const next=normalizeTrafficLightValue(state);if(normalizeTrafficLightValue(B[key].light)===next){closeBoxChooser();return;}takeSnapshot();B[key].light=next;cascade();saveState();render();closeBoxChooser();} /* apply a preset Border color to a box from the details-pane Border color control. Empty string clears the custom border. Snapshot taken before applying so undo restores the previous value. No-op (and no snapshot) if the value is unchanged. The borderColor field is already plumbed through buildInitialState, saveState, boxHTML, howBoxHTML, rebuildBoxKeys, rebuildFinalKeys, and the AI chat state, so this is purely a UI write into existing state. */ function setBoxBorder(key,color){if(IS_READONLY)return;if(!B[key])return;const newColor=sanitizeOptionalColorValue(color);if((sanitizeOptionalColorValue(B[key].borderColor)||'')===newColor)return;takeSnapshot();B[key].borderColor=newColor;saveState();render();} // snapshot prior state BEFORE applying a priority toggle so undo reliably restores the previous value. No-op (and no snapshot) if the resulting value is unchanged. function setBoxFillColor(key,color){if(IS_READONLY)return;if(!B[key])return;const newColor=sanitizeOptionalColorValue(color);if((sanitizeOptionalColorValue(B[key].boxColor)||'')===newColor)return;takeSnapshot();B[key].boxColor=newColor;saveState();render();} function getBoxFillColorPresets(){return[{key:'',label:'Default'}].concat(COL_COLORS.map(function(c,i){return{key:c.bg,label:'Standard soft fill '+(i+1)};}));} function getBorderColorPresets(){return[{key:'',label:'Default'},{key:'#d97706',label:'Orange'},{key:'#2563eb',label:'Blue'},{key:'#16a34a',label:'Green'},{key:'#ca8a04',label:'Yellow'},{key:'#7c3aed',label:'Purple'},{key:'#dc2626',label:'Red'},{key:'#475569',label:'Slate'}];} function openColorPalette(ev,key,kind){if(ev){ev.stopPropagation();ev.preventDefault();}closeBoxChooser();var box=B[key];if(!box)return;var anchor=ev&&ev.currentTarget?ev.currentTarget:null;if(!anchor)return;var mainEl=document.getElementById('main');if(!mainEl)return;var div=document.createElement('div');div.id='boxChooser';div.className='box-chooser';var presets=(kind==='box'?getBoxFillColorPresets():getBorderColorPresets());var cur=(kind==='box'?(box.boxColor||''):(box.borderColor||'')).toLowerCase();var opts=presets.map(function(p){var act=(p.key||'').toLowerCase()===cur?' opt-active':'';var sw=p.key?'':'';var fn=kind==='box'?'setBoxFillColor':'setBoxBorder';return '';}).join('');div.innerHTML='
'+(kind==='box'?'Box color':'Border color')+'
'+opts+'
';mainEl.appendChild(div);positionBoxChooser(div,anchor);} function setOverviewTL(pid,state){if(IS_READONLY)return;var sp=findPageById(pid);if(!sp)return;var meta=ensureOverviewCardMeta(sp);if(meta.light===state){closeBoxChooser();return;}takeSnapshot();meta.light=state;saveState();render();closeBoxChooser();} function setOverviewPriExact(pid,pri){if(IS_READONLY)return;var sp=findPageById(pid);if(!sp)return;var meta=ensureOverviewCardMeta(sp);var next=normalizePriorityValue(pri);if(meta.priority===next){closeBoxChooser();return;}takeSnapshot();meta.priority=next;saveState();render();closeBoxChooser();} function setOverviewCardFillColor(pid,color){if(IS_READONLY)return;var sp=findPageById(pid);if(!sp)return;var meta=ensureOverviewCardMeta(sp);var next=sanitizeOptionalColorValue(color);if((sanitizeOptionalColorValue(meta.boxColor)||'')===next)return;takeSnapshot();meta.boxColor=next;saveState();render();closeBoxChooser();} function openOverviewTLChooser(ev,pid){if(IS_READONLY){if(ev){ev.stopPropagation();ev.preventDefault();}return;}if(ev){ev.stopPropagation();ev.preventDefault();}closeBoxChooser();var sp=findPageById(pid);if(!sp)return;var meta=ensureOverviewCardMeta(sp);var anchor=ev&&ev.currentTarget?ev.currentTarget:document.getElementById('ov-tl-'+pid);if(!anchor)return;var mainEl=document.getElementById('main');if(!mainEl)return;var div=document.createElement('div');div.id='boxChooser';div.className='box-chooser';var states=['green','greenYellow','yellow','yellowRed','red','grey'];var optsHtml=states.map(function(t){var act=meta.light===t?' opt-active':'';var split=tlSplitClass(t);var cls='tl-opt'+act+(split?' '+split:'');return '
';}).join('');div.innerHTML='
Traffic light
'+optsHtml+'
';mainEl.appendChild(div);positionBoxChooser(div,anchor);} function openOverviewPriChooser(ev,pid){if(IS_READONLY){if(ev){ev.stopPropagation();ev.preventDefault();}return;}if(ev){ev.stopPropagation();ev.preventDefault();}closeBoxChooser();var sp=findPageById(pid);if(!sp)return;var meta=ensureOverviewCardMeta(sp);var anchor=ev&&ev.currentTarget?ev.currentTarget:null;if(!anchor)return;var mainEl=document.getElementById('main');if(!mainEl)return;var div=document.createElement('div');div.id='boxChooser';div.className='box-chooser';var optsHtml=PRIORITY_VALUES.map(function(p){var act=meta.priority===p?' opt-active':'';var cls='pri-'+String(p).toLowerCase();return '';}).join('');var clearAct=!meta.priority?' opt-active':'';optsHtml+='';div.innerHTML='
Priority
'+optsHtml+'
';mainEl.appendChild(div);positionBoxChooser(div,anchor);} function openOverviewColorPalette(ev,pid){if(ev){ev.stopPropagation();ev.preventDefault();}if(IS_READONLY)return;closeBoxChooser();var sp=findPageById(pid);if(!sp)return;var meta=ensureOverviewCardMeta(sp);var anchor=ev&&ev.currentTarget?ev.currentTarget:null;if(!anchor)return;var mainEl=document.getElementById('main');if(!mainEl)return;var div=document.createElement('div');div.id='boxChooser';div.className='box-chooser';var cur=(meta.boxColor||'').toLowerCase();var opts=getBoxFillColorPresets().map(function(p){var act=(p.key||'').toLowerCase()===cur?' opt-active':'';var sw=p.key?'':'';return '';}).join('');div.innerHTML='
Card colour
'+opts+'
';mainEl.appendChild(div);positionBoxChooser(div,anchor);} function openOverviewCardTags(ev,pid){if(ev){ev.stopPropagation();ev.preventDefault();}closeBoxChooser();if(IS_READONLY)return;var sp=findPageById(pid);if(!sp)return;var meta=ensureOverviewCardMeta(sp);var anchor=ev&&ev.currentTarget?ev.currentTarget:null;if(!anchor)return;var mainEl=document.getElementById('main');if(!mainEl)return;var ids=cleanTagIds(meta.tagIds||[]);meta.tagIds=ids;var cur=ids.length?ids.map(function(tid){var t=getTag(tid);if(!t)return '';var sl=normalizeTagShortLabel(t.shortLabel||'');return ''+escHtml(sl)+' ';}).join(' '):'No tags on this card';var pickerId='overviewTagPicker_'+String(pid).replace(/[^A-Za-z0-9_]/g,'_');var opts='';(tags||[]).forEach(function(t){if(!ids.includes(t.id)){opts+='';}});var div=document.createElement('div');div.id='boxChooser';div.className='box-chooser';div.innerHTML='
Tags
'+cur+'
Manage tags
'+buildInlineCreateTag('overviewPage',pid,pickerId,'Create and apply')+'
';mainEl.appendChild(div);positionBoxChooser(div,anchor);} function refreshOverviewCardTagPicker(pid,pickerId){var sel=document.getElementById(pickerId);if(!sel)return;var sp=findPageById(pid);if(!sp)return;var meta=ensureOverviewCardMeta(sp);var ids=cleanTagIds(meta.tagIds||[]);var cur=sel.value;var opts='';(tags||[]).forEach(function(t){if(!ids.includes(t.id)||t.id===cur)opts+='';});sel.innerHTML=opts;if(cur)sel.value=cur;} function addOverviewCardTagFromPicker(ev,pid,pickerId){if(ev){ev.stopPropagation();ev.preventDefault();}if(IS_READONLY)return;refreshOverviewCardTagPicker(pid,pickerId);var sel=document.getElementById(pickerId);var sp=findPageById(pid);if(!sel||!sel.value||!sp)return;var meta=ensureOverviewCardMeta(sp);if(!meta.tagIds.includes(sel.value)){takeSnapshot();meta.tagIds.push(sel.value);saveState();}render();closeBoxChooser();} function removeOverviewCardTag(ev,pid,tagId){if(ev){ev.stopPropagation();ev.preventDefault();}if(IS_READONLY)return;var sp=findPageById(pid);if(!sp)return;var meta=ensureOverviewCardMeta(sp);if(!Array.isArray(meta.tagIds))return;if(meta.tagIds.indexOf(tagId)<0)return;takeSnapshot();meta.tagIds=meta.tagIds.filter(function(x){return x!==tagId;});saveState();render();closeBoxChooser();} function getDoViewColorPresets(){return [{key:'',label:'Default'},{key:'#F5A623',label:'Orange'},{key:'#2563eb',label:'Blue'},{key:'#16a34a',label:'Green'},{key:'#FFFF00',label:'Yellow'},{key:'#dc2626',label:'Red'},{key:'#9333ea',label:'Purple'},{key:'#64748b',label:'Grey'}];} function colorSwatchHTML(color){var safe=sanitizeOptionalColorValue(color);return safe?'':'';} function setPri(key,pri){if(IS_READONLY)return;if(!B[key])return;const target=normalizePriorityValue(pri);const cur=normalizePriorityValue(B[key].priority);const newPri=cur===target?'':target;if(cur===newPri){closeBoxChooser();return;}takeSnapshot();B[key].priority=newPri;saveState();render();closeBoxChooser();} // set priority to an exact value (no toggle) \u2014 used by the on-box priority chooser. Empty pri clears it. Now snapshots before applying so undo reliably restores the previous priority. function setPriExact(key,pri){if(IS_READONLY)return;if(!B[key])return;const newPri=normalizePriorityValue(pri);if(normalizePriorityValue(B[key].priority)===newPri){closeBoxChooser();return;}takeSnapshot();B[key].priority=newPri;saveState();render();closeBoxChooser();} // on-box chooser popups for traffic light and priority. Anchored to the affordance, dismissed on outside click. Choosing a value applies it immediately. The existing details-pane editing of these fields still works as an alternative. function closeBoxChooser(){var existing=document.getElementById('boxChooser');if(existing)existing.remove();} function positionBoxChooser(div,anchorEl){var ar=anchorEl.getBoundingClientRect();var mainEl=document.getElementById('main');var mr=mainEl?mainEl.getBoundingClientRect():{left:0,top:0};var scale=parseFloat(getComputedStyle(document.documentElement).getPropertyValue('--board-scale'))||1;var leftPx=(ar.left-mr.left)/scale+18;var topPx=(ar.top-mr.top)/scale+18;div.style.position='absolute';div.style.left=leftPx+'px';div.style.top=topPx+'px';} function openTLChooser(ev,key){if(IS_READONLY){if(ev){ev.stopPropagation();ev.preventDefault();}return;}if(ev){ev.stopPropagation();ev.preventDefault();}closeBoxChooser();var box=B[key];if(!box)return;var anchor=ev&&ev.currentTarget?ev.currentTarget:document.getElementById('tl-'+key);if(!anchor)return;var mainEl=document.getElementById('main');if(!mainEl)return;var div=document.createElement('div');div.id='boxChooser';div.className='box-chooser';/* on-box chooser offers the standard DoView states plus a true unset option. Grey remains a deliberate selected value, not the unset state. */var cur=normalizeTrafficLightValue(box.light);var states=['','green','greenYellow','yellow','yellowRed','red','grey'];var optsHtml=states.map(function(t){var state=normalizeTrafficLightValue(t);var act=cur===state?' opt-active':'';var split=tlSplitClass(state);var cls='tl-opt'+(state?'':' tl-empty')+act+(split?' '+split:'');var bg=state?tlColor(state):'#fff';return '
';}).join('');div.innerHTML='
Traffic light
'+optsHtml+'
';mainEl.appendChild(div);positionBoxChooser(div,anchor);} function openPriChooser(ev,key){if(IS_READONLY){if(ev){ev.stopPropagation();ev.preventDefault();}return;}if(ev){ev.stopPropagation();ev.preventDefault();}closeBoxChooser();var box=B[key];if(!box)return;var cur=normalizePriorityValue(box.priority);var anchor=ev&&ev.currentTarget?ev.currentTarget:null;if(!anchor){var bxEl=document.getElementById('bx-'+key);if(bxEl){anchor=bxEl.querySelector('.pri-badge');}}if(!anchor)return;var mainEl=document.getElementById('main');if(!mainEl)return;var div=document.createElement('div');div.id='boxChooser';div.className='box-chooser';var optsHtml=PRIORITY_VALUES.map(function(p){var act=cur===p?' opt-active':'';var cls='pri-'+String(p).toLowerCase();return '';}).join('');var clearAct=!cur?' opt-active':'';optsHtml+='';div.innerHTML='
Priority
'+optsHtml+'
';mainEl.appendChild(div);positionBoxChooser(div,anchor);} async function saveEntry(){if(IS_READONLY)return;const ta=document.getElementById('epTa');if(!ta)return;const text=ta.value.trim();if(!text)return;await doSaveEntry(text,entryMode);ta.value='';editingType=null;renderEntryPanel();} async function doSaveEntry(text,mode){const key=openBox;if(!key||!B[key])return;const box=B[key];if(!Array.isArray(box.entries))box.entries=[];const typeMap={note1:'note1',note2:'note2',note3:'note3',note4:'note4',note5:'note5'};const entryType=typeMap[mode]||'note1';const parts=text.split('|').map(s=>s.trim()).filter(Boolean);if(editingType===entryType||editingType===mode){var removeTypes=[entryType];if(entryType==='note1')removeTypes.push('sofar');if(entryType==='note2')removeTypes.push('note');box.entries=box.entries.filter(e=>removeTypes.indexOf(e.type)<0);}parts.forEach(p=>box.entries.push({type:entryType,text:p}));editingType=null;saveState();render();} // ─── CLASSIFY ─── async function classifyText(text){const lo=text.toLowerCase().trim();if(/\b(not yet|later|leave|defer|hold off|come back|closer to|will do later|for later|at the moment|not sure|unsure|don't know yet|check later|wait|postpone|pending|tbd|yellow|keep yellow|stay yellow|make yellow|leave yellow|leave this|do this later|haven't done|have not done|can't do yet|cannot do yet)\b/.test(lo))return 'defer';if(/\b(problem|issue|blocked|blocker|can't|cannot|won't work|stuck|risk|danger|concern|obstacle|stopping|prevents|failed|fail|broken|error)\b/.test(lo))return 'blocker';return 'green';} // ─── UNDO ─── function takeSnapshot(){const snap={B:JSON.parse(JSON.stringify(B)),SP:JSON.parse(JSON.stringify(SP)),FO:JSON.parse(JSON.stringify(FO_DATA)),DEPS:JSON.parse(JSON.stringify(DEPS)),ttLinks:JSON.parse(JSON.stringify(ttLinks)),ttLinkNextId:ttLinkNextId,howLinks:JSON.parse(JSON.stringify(howLinks)),howLinkNextId:howLinkNextId,measures:JSON.parse(JSON.stringify(measures)),measureNextId:measureNextId,evalQuestions:JSON.parse(JSON.stringify(evalQuestions)),eqNextId:eqNextId,boardInfo:boardInfo,pageInfo:JSON.parse(JSON.stringify(pageInfo)),docContent:sanitizedDocContentState()};undoStack.push(snap);if(undoStack.length>MAX_UNDO)undoStack.shift();updateUndoBtn();} // undo is a UI action, not a conversational event \u2014 no chat message is written. The board change itself is enough feedback. function doUndo(){if(IS_READONLY)return;if(!undoStack.length)return;const snap=undoStack.pop();B=snap.B;SP.length=0;snap.SP.forEach(s=>SP.push(s));FO_DATA.length=0;normalizedFinalOutcomes(snap.FO).forEach(f=>FO_DATA.push(f));normalizeFinalOutcomesInMemory();DEPS=snap.DEPS;if(snap.ttLinks){ttLinks=snap.ttLinks;ttLinkNextId=snap.ttLinkNextId;}if(snap.howLinks){howLinks=snap.howLinks;howLinkNextId=snap.howLinkNextId;}if(snap.measures){measures=snap.measures;measureNextId=snap.measureNextId;}if(snap.evalQuestions){evalQuestions=snap.evalQuestions;eqNextId=snap.eqNextId;}if(snap.boardInfo!==undefined)boardInfo=snap.boardInfo;if(snap.pageInfo)pageInfo=snap.pageInfo;if(snap.docContent)docContent=sanitizeDocContentObject(snap.docContent);markRuntimeLinkIndexDirty();cascade();saveState();render();updateUndoBtn();} function updateUndoBtn(){const btn=document.getElementById('undoBtn');if(btn)btn.style.display=undoStack.length?'flex':'none';} // ─── COMMAND PARSER — CRITICAL: Do not modify, replace, or "improve" this function ─── function parseActions(text){const actions=[];const clean=text.replace(/\[ACTION:(.*?)\]/g,(_,m)=>{actions.push(m.split(':'));return '';});return {clean:clean.trim(),actions};} function execAction(parts){const cmd=parts[0]; if(cmd==='setLight'&&parts.length>=3){const k=parts[1],c=normalizeTrafficLightValue(parts[2]);if(B[k])B[k].light=c;cascade();saveState();render();} else if(cmd==='setAllLights'&&parts.length>=2){const c=normalizeTrafficLightValue(parts[1]);Object.keys(B).forEach(k=>B[k].light=c);cascade();saveState();render();} else if(cmd==='setSubpageLights'&&parts.length>=3){const spId=parts[1],c=normalizeTrafficLightValue(parts[2]);Object.keys(B).filter(k=>k.startsWith(spId+'-')).forEach(k=>B[k].light=c);cascade();saveState();render();} else if(cmd==='addSofar'&&parts.length>=3){const k=parts[1],txt=parts.slice(2).join(':');if(B[k])B[k].entries.push({type:'sofar',text:txt});saveState();render();} else if(cmd==='addNote'&&parts.length>=3){const k=parts[1],txt=parts.slice(2).join(':');if(B[k])B[k].entries.push({type:'note',text:txt});saveState();render();} else if(cmd==='setPriority'&&parts.length>=3){const k=parts[1],p=normalizePriorityValue(parts[2]);if(B[k])B[k].priority=p;saveState();render();} else if(cmd==='clearEntries'&&parts.length>=3){const k=parts[1],t=parts[2];if(B[k]){if(t==='all')B[k].entries=[];else B[k].entries=B[k].entries.filter(e=>e.type!==t);}saveState();render();} else if(cmd==='addNote1'&&parts.length>=3){const k=parts[1],txt=parts.slice(2).join(':');if(B[k])B[k].entries.push({type:'note1',text:txt});saveState();render();} else if(cmd==='addNote2'&&parts.length>=3){const k=parts[1],txt=parts.slice(2).join(':');if(B[k])B[k].entries.push({type:'note2',text:txt});saveState();render();} else if(cmd==='addNote3'&&parts.length>=3){const k=parts[1],txt=parts.slice(2).join(':');if(B[k])B[k].entries.push({type:'note3',text:txt});saveState();render();} else if(cmd==='setDetail'&&parts.length>=3){const k=parts[1],txt=parts.slice(2).join(':');if(B[k])B[k].detailText=txt;saveState();render();} else if(cmd==='clearDetail'&&parts.length>=2){const k=parts[1];if(B[k])B[k].detailText='';saveState();render();} else if(cmd==='setBoxBorder'&&parts.length>=3){const k=parts[1],c=sanitizeOptionalColorValue(parts[2]);if(B[k])B[k].borderColor=c;saveState();render();} else if(cmd==='clearBoxBorder'&&parts.length>=2){const k=parts[1];if(B[k])B[k].borderColor='';saveState();render();} else if(cmd==='setSubpageBorders'&&parts.length>=3){const spId=parts[1],c=sanitizeOptionalColorValue(parts[2]);Object.keys(B).filter(k=>k.startsWith(spId+'-')).forEach(k=>B[k].borderColor=c);saveState();render();} else if(cmd==='clearSubpageBorders'&&parts.length>=2){const spId=parts[1];Object.keys(B).filter(k=>k.startsWith(spId+'-')).forEach(k=>B[k].borderColor='');saveState();render();} else if(cmd==='setBoardInfo'&&parts.length>=2){boardInfo=parts.slice(1).join(':');saveState();} else if(cmd==='setPageInfo'&&parts.length>=3){const pid=parts[1];const txt=parts.slice(2).join(':');pageInfo[pid]=txt;saveState();} else if(cmd==='addBox'&&parts.length>=4){const spId=parts[1],ci=parseInt(parts[2]),label=parts.slice(3).join(':');const sp=SP.find(s=>s.id===spId);if(sp&&sp.cols[ci]){sp.cols[ci].boxes.push(label);const bi=sp.cols[ci].boxes.length-1;const k=`${spId}-c${ci}-b${bi}`;/* stable uid at creation. */B[k]={label,light:'',entries:[],priority:'',hasSubpage:false,detailText:'',borderColor:'',boxColor:'',tagIds:[],uid:_genUid('box')};buildDepsMap();cascade();saveState();render();}} else if(cmd==='removeBox'&&parts.length>=2){const k=parts[1];const m=k.match(/^(p\d+)-c(\d+)-b(\d+)$/);if(m){const spId=m[1],ci=parseInt(m[2]),bi=parseInt(m[3]);const sp=SP.find(s=>s.id===spId);if(sp&&sp.cols[ci]&&sp.cols[ci].boxes[bi]!==undefined){/* Preserve box state and links while positional keys shift. */const oldColLen=sp.cols[ci].boxes.length;const keyMap={};keyMap[`${spId}-c${ci}-b${bi}`]='';for(let j=bi+1;j=3){const k=parts[1],nl=parts.slice(2).join(':');if(B[k]){B[k].label=nl;const m=k.match(/^(p\d+)-c(\d+)-b(\d+)$/);if(m){const sp=SP.find(s=>s.id===m[1]);if(sp&&sp.cols[parseInt(m[2])])sp.cols[parseInt(m[2])].boxes[parseInt(m[3])]=nl;}saveState();render();}} else if(cmd==='addColumn'&&parts.length>=4){const spId=parts[1],ci=parseInt(parts[2]),h=parts.slice(3).join(':');const sp=SP.find(s=>s.id===spId);if(sp){/* Preserve box state and links while positional keys shift. */const oldColCount=sp.cols.length;const keyMap={};for(let cj=ci;cj=3){const spId=parts[1],ci=parseInt(parts[2]);const sp=SP.find(s=>s.id===spId);if(sp&&sp.cols[ci]){/* Preserve box state and links while positional keys shift. */const oldColCount=sp.cols.length;const keyMap={};for(let cj=0;cjci){keyMap[oldK]=`${spId}-c${cj-1}-b${bi}`;}}}sp.cols.splice(ci,1);rebuildBoxKeys(spId,keyMap);buildDepsMap();cascade();saveState();render();}} else if(cmd==='renameColumn'&&parts.length>=4){const spId=parts[1],ci=parseInt(parts[2]),nh=parts.slice(3).join(':');const sp=SP.find(s=>s.id===spId);if(sp&&sp.cols[ci]){sp.cols[ci].h=nh;saveState();render();}} else if(cmd==='moveColumn'&&parts.length>=4){const spId=parts[1],fromCi=parseInt(parts[2]),toCi=parseInt(parts[3]);const sp=SP.find(s=>s.id===spId);if(sp&&sp.cols[fromCi]&&toCi>=0&&toCi<=sp.cols.length-1&&fromCi!==toCi){/* Preserve box state and links while positional keys shift. */const oldColCount=sp.cols.length;const keyMap={};for(let cj=0;cjfromCi&&cj<=toCi){newCj=cj-1;}else if(fromCi>toCi&&cj>=toCi&&cj=2){const wasSingle=SP.length===1;const label=parts[1];let pType='this_then';if(parts.length>=3&&!parts[2].startsWith('#')&&['this_then','how','documentation'].includes(parts[2])){pType=parts[2];}let bg,bdr,tab;const colorStart=pType!=='this_then'?3:2;if(parts.length>=colorStart+3){const avail=nextAutoPageColor();bg=sanitizeColorValue(parts[colorStart],avail.bg);bdr=sanitizeColorValue(parts[colorStart+1],avail.bdr);tab=sanitizeColorValue(parts[colorStart+2],avail.tab);}else{const avail=nextAutoPageColor();bg=avail.bg;bdr=avail.bdr;tab=avail.tab;}const id=nextUnusedPageId();/* stable uid on the new page. */const _pageUid=_genUid('page');if(pType==='how'){const howBoxes=[];for(let i=1;i<=8;i++){const hid='H'+String(i).padStart(3,'0');/* stable uid on each starter How Box record. */howBoxes.push({id:hid,label:'How (action, person, team, unit, organization, capability, \u2026)',uid:_genUid('box')});}SP.push({id,label,pageType:'how',color:{bg,bdr,tab},cols:[],howBoxes,nextHowNum:9,howLevel:nextVerticalHowLevel(),uid:_pageUid});howBoxes.forEach(hb=>{B[`${id}-${hb.id}`]={label:hb.label,light:'',entries:[],priority:'',hasSubpage:false,detailText:'',borderColor:'',boxColor:'',tagIds:[],uid:hb.uid};});}else{/* stable uid on the default first column of a new this_then / documentation page. */SP.push({id,label,pageType:pType,color:{bg,bdr,tab},cols:[{h:'First Step',boxes:[],uid:_genUid('col')}],uid:_pageUid});}if(wasSingle){addChatMsg('system','This is now a multi-page board. The overview page has been added and the original page now uses a single color scheme to match the overview tiles.');currentPage='overview';}buildDepsMap();saveState();render();} else if(cmd==='removeSubpage'&&parts.length>=2){const spId=parts[1];const idx=SP.findIndex(s=>s.id===spId);if(idx>=0){Object.keys(B).filter(k=>k.startsWith(spId+'-')).forEach(k=>delete B[k]);SP.splice(idx,1);delete pageInfo[spId];delete docContent[spId];cleanupTTLinks();cleanupHowLinks();buildDepsMap();cascade();saveState();if(SP.length===1){addChatMsg('system','This is now a single-page board. The overview page has been removed and columns now use different colors.');currentPage=SP[0].id;render();}else{render();if(currentPage===spId)navTo('overview');}}} else if(cmd==='addFinalOutcome'&&parts.length>=2){const label=parts.slice(1).join(':');FO_DATA.push(label);const i=FO_DATA.length-1;/* stable uid on the new Final Outcome box. */B[`final-b${i}`]={label,light:'',entries:[],priority:'',hasSubpage:false,detailText:'',borderColor:'',boxColor:'',measures:[],evalQuestions:[],tagIds:[],uid:_genUid('final')};saveState();render();} else if(cmd==='removeFinalOutcome'&&parts.length>=2){const i=parseInt(parts[1]);if(i>=0&&i=3){const from=parts[1],to=parts[2];addTTLink(from,to);} else if(cmd==='removeTTLink'&&parts.length>=2){const id=parts[1];removeTTLink(id);} else if(cmd==='flipTTLink'&&parts.length>=2){const id=parts[1];flipTTLink(id);} else if(cmd==='addHowLink'&&parts.length>=3){const from=parts[1],to=parts[2];addHowLink(from,to);} else if(cmd==='createMeasure'&&parts.length>=2){const title=parts.slice(1).join(':');const id='M'+String(measureNextId).padStart(3,'0');measureNextId++;/* stable uid on the new Measure. */measures.push({id,title,mainText:'',notes1:'',notes2:'',notes3:'',trafficLight:'',uid:_genUid('measure')});saveState();render();} else if(cmd==='createEQ'&&parts.length>=2){const qtext=parts.slice(1).join(':');const id='EQ'+String(eqNextId).padStart(3,'0');eqNextId++;/* stable uid on the new Evaluation Question. */evalQuestions.push({id,questionText:qtext,mainText:'',notes1:'',notes2:'',notes3:'',trafficLight:'',uid:_genUid('eq')});saveState();render();} else if(cmd==='linkMeasure'&&parts.length>=3){const mId=parts[1],boxKey=parts[2];linkMeasureToBox(mId,boxKey);} else if(cmd==='unlinkMeasure'&&parts.length>=3){const mId=parts[1],boxKey=parts[2];unlinkMeasureFromBox(mId,boxKey);} else if(cmd==='linkEQ'&&parts.length>=3){const eqId=parts[1],boxKey=parts[2];linkEQToBox(eqId,boxKey);} else if(cmd==='unlinkEQ'&&parts.length>=3){const eqId=parts[1],boxKey=parts[2];unlinkEQFromBox(eqId,boxKey);} else if(cmd==='deleteMeasure'&&parts.length>=2){const mId=normalizeMeasureIdInput(parts[1]);const m=getMeasure(mId);if(m){const linked=getBoxesLinkedToMeasure(mId);linked.forEach(k=>{if(B[k]&&B[k].measures)B[k].measures=B[k].measures.filter(id=>normalizeMeasureIdInput(id)!==mId);});ttLinks.forEach(function(l){if(l.measures)l.measures=l.measures.filter(function(id){return normalizeMeasureIdInput(id)!==mId;});});measures=measures.filter(x=>x.id!==mId);saveState();render();}} else if(cmd==='deleteEQ'&&parts.length>=2){const eqId=normalizeEQIdInput(parts[1]);const q=getEQ(eqId);if(q){const linked=getBoxesLinkedToEQ(eqId);linked.forEach(k=>{if(B[k]&&B[k].evalQuestions)B[k].evalQuestions=B[k].evalQuestions.filter(id=>normalizeEQIdInput(id)!==eqId);});ttLinks.forEach(function(l){if(l.evalQuestions)l.evalQuestions=l.evalQuestions.filter(function(id){return normalizeEQIdInput(id)!==eqId;});});evalQuestions=evalQuestions.filter(x=>x.id!==eqId);saveState();render();}} else if(cmd==='removeHowLink'&&parts.length>=2){const id=parts[1];removeHowLink(id);} } /* Preserve box state and links while positional keys shift. */ function rebuildBoxKeys(spId,keyMap){markRuntimeLinkIndexDirty();const sp=SP.find(s=>s.id===spId);if(!sp)return;if(keyMap&&typeof keyMap==='object'){const captured={};Object.keys(B).filter(k=>k.indexOf(spId+'-c')===0).forEach(k=>{captured[k]=B[k];delete B[k];});Object.keys(captured).forEach(oldK=>{let newK;if(Object.prototype.hasOwnProperty.call(keyMap,oldK)){newK=keyMap[oldK];}else{newK=oldK;}if(newK===''||newK==null)return;B[newK]=captured[oldK];});sp.cols.forEach((col,ci)=>{col.boxes.forEach((label,bi)=>{const k=`${spId}-c${ci}-b${bi}`;if(!B[k])B[k]={label,light:'',entries:[],priority:'',hasSubpage:false,detailText:'',borderColor:'',boxColor:'',tagIds:[],uid:_genUid('box')};B[k].label=label;});});function _remap(k){if(k&&typeof k==='string'&&k.indexOf(spId+'-c')===0&&Object.prototype.hasOwnProperty.call(keyMap,k)){return keyMap[k];}return k;}if(typeof ttLinks!=='undefined'&&Array.isArray(ttLinks)){ttLinks.forEach(l=>{if(l&&l.from)l.from=_remap(l.from);if(l&&l.to)l.to=_remap(l.to);});ttLinks=ttLinks.filter(l=>l&&l.from&&l.to);}if(typeof howLinks!=='undefined'&&Array.isArray(howLinks)){howLinks.forEach(l=>{if(l&&l.from)l.from=_remap(l.from);if(l&&l.to)l.to=_remap(l.to);});howLinks=howLinks.filter(l=>l&&l.from&&l.to);}if(typeof openBox!=='undefined'&&openBox){const r=_remap(openBox);openBox=(r==='')?null:r;}if(typeof inspectHome!=='undefined'&&inspectHome){const r=_remap(inspectHome);inspectHome=(r==='')?null:r;}if(typeof revealBox!=='undefined'&&revealBox){const r=_remap(revealBox);revealBox=(r==='')?null:r;}if(typeof inspectLinkedKeys!=='undefined'&&Array.isArray(inspectLinkedKeys)){inspectLinkedKeys=inspectLinkedKeys.map(_remap).filter(k=>k);}if(typeof sourceRefBox!=='undefined'&&sourceRefBox&&sourceRefBox.sourceKey){const r=_remap(sourceRefBox.sourceKey);if(r!=='')sourceRefBox.sourceKey=r;}cleanupTTLinks();cleanupHowLinks();return;}const oldB={};Object.keys(B).filter(k=>k.startsWith(spId+'-')).forEach(k=>{oldB[k]=B[k];delete B[k];});sp.cols.forEach((col,ci)=>{col.boxes.forEach((label,bi)=>{const k=`${spId}-c${ci}-b${bi}`;const old=oldB[k];B[k]=old||{label,light:'',entries:[],priority:'',hasSubpage:false,detailText:'',borderColor:'',boxColor:'',tagIds:[]};B[k].label=label;});});cleanupTTLinks();cleanupHowLinks();} function rebuildFinalKeys(){normalizeFinalOutcomesInMemory();const oldB={};Object.keys(B).filter(k=>k.startsWith('final-')).forEach(k=>{oldB[k]=B[k];delete B[k];});FO_DATA.forEach((label,i)=>{const k=`final-b${i}`;const old=oldB[k];B[k]=old||{label:label,light:'',entries:[],priority:'',hasSubpage:false,detailText:'',borderColor:'',boxColor:'',measures:[],evalQuestions:[],tagIds:[]};B[k].label=label;});} function rebuildFinalKeysWithMap(keyMap){markRuntimeLinkIndexDirty();normalizeFinalOutcomesInMemory();const captured={};Object.keys(B).filter(k=>k.startsWith('final-')).forEach(k=>{captured[k]=B[k];delete B[k];});Object.keys(captured).forEach(oldK=>{let newK;if(keyMap&&Object.prototype.hasOwnProperty.call(keyMap,oldK)){newK=keyMap[oldK];}else{newK=oldK;}if(newK===''||newK==null)return;B[newK]=captured[oldK];});FO_DATA.forEach((label,i)=>{const k=`final-b${i}`;if(!B[k])B[k]={label:label,light:'',entries:[],priority:'',hasSubpage:false,detailText:'',borderColor:'',boxColor:'',measures:[],evalQuestions:[],tagIds:[],uid:_genUid('final')};B[k].label=label;if(!Array.isArray(B[k].entries))B[k].entries=[];if(!Array.isArray(B[k].measures))B[k].measures=[];if(!Array.isArray(B[k].evalQuestions))B[k].evalQuestions=[];if(!Array.isArray(B[k].tagIds))B[k].tagIds=[];});function _remap(k){if(k&&typeof k==='string'&&k.startsWith('final-')&&keyMap&&Object.prototype.hasOwnProperty.call(keyMap,k)){return keyMap[k];}return k;}if(typeof openBox!=='undefined'&&openBox){const r=_remap(openBox);openBox=(r==='')?null:r;}if(typeof inspectHome!=='undefined'&&inspectHome){const r=_remap(inspectHome);inspectHome=(r==='')?null:r;}if(typeof revealBox!=='undefined'&&revealBox){const r=_remap(revealBox);revealBox=(r==='')?null:r;}if(typeof inspectLinkedKeys!=='undefined'&&Array.isArray(inspectLinkedKeys)){inspectLinkedKeys=inspectLinkedKeys.map(_remap).filter(k=>k);}if(typeof sourceRefBox!=='undefined'&&sourceRefBox&&sourceRefBox.sourceKey){const r=_remap(sourceRefBox.sourceKey);if(r){sourceRefBox.sourceKey=r;sourceRefBox.sourceLabel=B[r]?B[r].label:sourceRefBox.sourceLabel;}else{sourceRefBox=null;}}} /* detect explicit blank/starter This\u2013Then page requests in the user's chat message. Returns true when the user clearly asks for a blank/starter/empty/shell page rather than a populated one. Used to bypass the populated-page validation so genuinely-blank requests still create a blank/starter page. */ function isBlankPageRequest(msg){if(!msg)return false;return /\b(blank|starter|empty|shell|skeleton)\b/i.test(msg);} /* detect populated This\u2013Then page requests in the user's chat message. Returns true when the user is asking for a populated/filled-in This\u2013Then page about a topic. The trigger phrasings cover the common cases: "populate it", "add DoView this-then boxes", "create a this-then logic for X", "add a this-then page", and "add a page and fill it in". This also includes "full DoView", "complete DoView", "full logic", and "full populated page" so phrasings such as "add a new page called X which includes a full DoView on it" reliably propose a populated This\u2013Then page. It also removes the previous broad "add a page about X" matcher (without an explicit this-then keyword or populated trigger) so a bare "add a page about X" is now classified as ambiguous (see isAmbiguousPageCreationRequest) and asks for the page type instead. It also extends phrase coverage further so common informal phrasings reliably propose a populated This\u2013Then page when the intent is clear: "do/make the full map (for X)", "map this out", "build out (a/this) page", "fill out this page", "build the full logic", and "turn this into a full page". Tight phrasings only \u2014 the broad word "map" by itself, or "section" by itself, do NOT trigger; question-only requests are NOT triggered. An explicit blank/starter request short-circuits to false so a clearly-blank request bypasses populated-page validation. Used by processAIResponse to validate that the AI's proposed structural actions actually populate the new page rather than leaving it as a one-box placeholder. */ function isPopulatedThisThenPageRequest(msg){if(!msg)return false;if(isBlankPageRequest(msg))return false;if(/\bpopulate\b/i.test(msg))return true;if(/\bfill\s+(it|in|out|the\s+page|out\s+this\s+page)\b/i.test(msg))return true;/* "full DoView" / "complete DoView" / "full logic" / "full populated page" all trigger populated This\u2013Then page behavior. */if(/\b(full|complete)\s+doview\b/i.test(msg))return true;if(/\bfull\s+(logic|populated\s+page|populated)\b/i.test(msg))return true;/* "build the full logic" (a non-noun-form alternative to "full logic") also triggers populated. */if(/\bbuild\s+(the\s+|a\s+)?full\s+logic\b/i.test(msg))return true;/* "do the full map" / "make the full map" / "the full map for X" trigger populated. The bare word "map" by itself does NOT trigger \u2014 it must be preceded by "do/make/build the full" or be the verb in "map this out". */if(/\b(do|make|build)\s+(the\s+|a\s+)?full\s+map\b/i.test(msg))return true;if(/\bmap\s+(this|it|that)\s+out\b/i.test(msg))return true;/* "build out (a/this/the) page", "fill out this page", "turn this into a full page" trigger populated. These phrasings are about populating a page, not just creating a shell. */if(/\bbuild\s+out\s+(a\s+|an\s+|the\s+|this\s+)?page\b/i.test(msg))return true;if(/\bturn\s+(this|it|that)\s+into\s+(a\s+)?full\s+page\b/i.test(msg))return true;if(/\b(add|create|make)\s+(a\s+|an\s+)?(new\s+)?this[-\u2013\s]*then\s+page\b/i.test(msg))return true;if(/\bcreate\s+(a\s+|an\s+)?this[-\u2013\s]*then\s+(logic|page|flow)\b/i.test(msg))return true;if(/\badd\s+(doview\s+)?(this[-\u2013\s]*then\s+)?boxes\b/i.test(msg))return true;if(/\badd\s+(a\s+|an\s+)?(new\s+)?page\b.*\bfill\b/i.test(msg))return true;return false;} /* detect generic page-creation phrasings in the user's chat message. Returns true when the user is asking to add/create/make a page (or "add X as a page"). Used together with isExplicitPageType, isBlankPageRequest, and isPopulatedThisThenPageRequest to decide whether a page-creation request is ambiguous and needs the page-type clarification question. Deliberately tight so it does not misfire on rename/delete/edit ("rename this page", "delete this page") or board-level commentary. It also detects "make/add another tab", "set up a section (for X)", "build out (a/this/the) page", and "turn this into a full page" so common informal page-creation phrasings are recognised. Tight phrasings only \u2014 the bare word "section" or "tab" or "map" does NOT trigger; questions like "what does this map say about X" do NOT trigger. */ function isPageCreationRequest(msg){if(!msg)return false;if(/\b(add|create|make)\s+(a\s+|an\s+)([a-z\u2013-]+\s+){0,3}page\b/i.test(msg))return true;if(/\b(add|create|make)\s+(a\s+|an\s+)?new\s+([a-z\u2013-]+\s+){0,3}page\b/i.test(msg))return true;if(/\bnew\s+(this[-\u2013\s]*then|how|documentation|doc)\s+page\b/i.test(msg))return true;if(/\bas\s+a\s+(new\s+)?(this[-\u2013\s]*then|how|documentation|doc)?\s*page\b/i.test(msg))return true;/* "make another tab", "add another tab", "make a new tab" \u2014 page-creation by another name. */if(/\b(make|add|create)\s+(another|a\s+new|a)\s+tab\b/i.test(msg))return true;/* "set up a section (for X)" \u2014 ambiguous page-creation. The bare word "section" without "set up a" does NOT trigger. */if(/\bset\s+up\s+(a\s+|an\s+)?(new\s+)?section\b/i.test(msg))return true;/* "build out a/this/the page", "turn this into a full page" \u2014 also page-creation phrasings (these typically mean populated, but isPopulatedThisThenPageRequest will catch the populated intent and bypass the ambiguity check). */if(/\bbuild\s+out\s+(a\s+|an\s+|the\s+|this\s+)?page\b/i.test(msg))return true;if(/\bturn\s+(this|it|that)\s+into\s+(a\s+)?(full\s+)?page\b/i.test(msg))return true;return false;} /* detect whether the user has explicitly stated which page type they want (This\u2013Then, How, or Documentation). Used by isAmbiguousPageCreationRequest. Includes common variants like "doc page", "this-then page", "this then page", "how page". */ function isExplicitPageType(msg){if(!msg)return false;if(/\bthis[-\u2013\s]*then\s+page\b/i.test(msg))return true;if(/\bhow\s+page\b/i.test(msg))return true;if(/\b(documentation|doc)\s+page\b/i.test(msg))return true;return false;} /* detect ambiguous page-creation requests where the user has asked to add/create/make a page but has NOT specified which page type they want and has NOT clearly implied a populated This\u2013Then page (via "full DoView", "populate it", etc.). Examples that should be classified as ambiguous: "add a page called leadership development", "create a new page about leadership development", "make a page for leadership development", "add leadership development as a page". Examples that are NOT ambiguous: "add a How page called X" (explicit type), "add a blank This\u2013Then page" (explicit type with blank qualifier), "add a complete DoView page about X" (populated trigger), "add a full populated page about X" (populated trigger). Note: a blank request without an explicit page type ("add a starter page called X") is still ambiguous because the page type is missing \u2014 the AI must ask which type before creating. Used by processAIResponse to reject any addSubpage emitted for an ambiguous request before queuing pendingStructural so no page is silently created. */ function isAmbiguousPageCreationRequest(msg){if(!msg)return false;if(!isPageCreationRequest(msg))return false;if(isExplicitPageType(msg))return false;if(isPopulatedThisThenPageRequest(msg))return false;return true;} /* pick the page type from an addSubpage action array. Mirrors the parsing in execAction's addSubpage branch so validation and summary stay consistent with apply behavior. Defaults to this_then. */ function _pageTypeFromAddSubpage(a){if(!a||a[0]!=='addSubpage')return 'this_then';if(a.length>=3 && a[2] && a[2].charAt(0)!=='#' && ['this_then','how','documentation'].indexOf(a[2])>=0)return a[2];return 'this_then';} /* , walk the structural actions array and predict the new page id assigned by execAction's addSubpage branch. New ids use the next unused pN value, not the current page count alone, so deleted pages do not cause id collisions. Returns an array of {action, predictedId, pageType} entries, one per addSubpage in order. */ function _predictNewPageIds(structActions){var out=[];var used={};(SP||[]).forEach(function(sp){if(sp&&sp.id)used[sp.id]=true;});structActions.forEach(function(a){if(a[0]==='addSubpage'){var pType=_pageTypeFromAddSubpage(a);var id=nextUnusedPageId(used);used[id]=true;out.push({action:a,predictedId:id,pageType:pType});}});return out;} /* preflight validation for an entire action set before anything is applied. Performs a dry-run over a working "shadow" model that mirrors the parts of execAction's behavior that affect target resolution: addSubpage adds a new page with the next predicted id (and a default first column "First Step" for this_then / documentation, no columns for how pages); addColumn / removeColumn / moveColumn updates the working column list; addBox / removeBox updates the working box list (and renumbers indices because rebuildBoxKeys does the same on apply); addFinalOutcome / removeFinalOutcome updates the working FO list; createMeasure / createEQ / deleteMeasure / deleteEQ updates the working measure / EQ id list; addTTLink / addHowLink / removeTTLink / removeHowLink updates working link id maps. The validator does NOT execute real actions; it only checks that every action references a valid object (existing in the current state OR created earlier in the same set), that page types are supported (this_then, how, documentation), that column indices are valid for the resolved page, that new labels/headings are non-empty, that addBox actions have valid page+column targets, that the action's parts.length meets CMD_MIN_PARTS for that command, and that the command name appears in SUPPORTED_CMDS. Returns {ok:true} or {ok:false, message:'...'}. The message is plain-language; never a stack trace or raw internal error. */ function _validateActionSet(actions){ if(!Array.isArray(actions)||!actions.length)return {ok:true}; // Working shadow state. We model only the parts of execAction that affect target resolution. var W={ pages:{}, // pid -> {pageType, cols:[{h, boxes:[label,...]}], howIds:[h001,...], finalRm:false} pageOrder:[], // pids in order fo:FO_DATA.slice(), // list of labels (positions used as bI) measures:measures.map(function(m){return m.id;}), eqs:evalQuestions.map(function(q){return q.id;}), ttLinkIds:ttLinks.map(function(l){return l.id;}), howLinkIds:howLinks.map(function(l){return l.id;}) }; // Seed W.pages from current SP SP.forEach(function(sp){ var entry={pageType:sp.pageType||'this_then',cols:[],howIds:[]}; if(entry.pageType==='how'){if(sp.howBoxes)entry.howIds=sp.howBoxes.map(function(h){return h.id;});} else{(sp.cols||[]).forEach(function(col){entry.cols.push({h:col.h,boxes:(col.boxes||[]).slice()});});} W.pages[sp.id]=entry;W.pageOrder.push(sp.id); }); function pageExists(pid){return !!W.pages[pid];} function nextShadowPageId(){var n=((SP&&SP.length)||0)+1;while(W.pages['p'+n])n++;return 'p'+n;} function resolveBoxKey(k){ if(!k)return null; var m=k.match(/^(p\d+)-c(\d+)-b(\d+)$/); if(m){var pid=m[1],ci=parseInt(m[2]),bi=parseInt(m[3]);var pg=W.pages[pid];if(!pg||pg.pageType==='how')return null;if(!pg.cols[ci])return null;if(pg.cols[ci].boxes[bi]===undefined)return null;return {kind:'tt',pid:pid,ci:ci,bi:bi};} var hm=k.match(/^(p\d+)-(H\d{3,})$/); if(hm){var pid2=hm[1],hid=hm[2];var pg2=W.pages[pid2];if(!pg2||pg2.pageType!=='how')return null;if(pg2.howIds.indexOf(hid)<0)return null;return {kind:'how',pid:pid2,hid:hid};} var fm=k.match(/^final-b(\d+)$/); if(fm){var i=parseInt(fm[1]);if(i<0||i>=W.fo.length)return null;return {kind:'final',fi:i};} return null; } for(var idx=0;idxpgC.cols.length)return {ok:false,message:'I could not add that column because the target column index is out of range. No changes were made.'}; if(!hC||!hC.trim())return {ok:false,message:'I could not add that column because the heading was empty. No changes were made.'}; pgC.cols.splice(ciC,0,{h:hC,boxes:[]}); } else if(cmd==='removeColumn'){ var spIdRc=a[1],ciRc=parseInt(a[2]);var pgRc=W.pages[spIdRc];if(!pgRc)return {ok:false,message:'I could not apply that change because the target page could not be found. No changes were made.'}; if(pgRc.pageType==='how')return {ok:false,message:'I could not apply that change because How pages do not use columns. No changes were made.'}; if(isNaN(ciRc)||!pgRc.cols[ciRc])return {ok:false,message:'I could not apply that change because the target column could not be found. No changes were made.'}; pgRc.cols.splice(ciRc,1); } else if(cmd==='renameColumn'){ var spIdRn=a[1],ciRn=parseInt(a[2]),nhRn=a.slice(3).join(':');var pgRn=W.pages[spIdRn];if(!pgRn)return {ok:false,message:'I could not apply that change because the target page could not be found. No changes were made.'}; if(pgRn.pageType==='how')return {ok:false,message:'I could not apply that change because How pages do not use columns. No changes were made.'}; if(isNaN(ciRn)||!pgRn.cols[ciRn])return {ok:false,message:'I could not apply that change because the target column could not be found. No changes were made.'}; if(!nhRn||!nhRn.trim())return {ok:false,message:'I could not apply that change because the new heading was empty. No changes were made.'}; pgRn.cols[ciRn].h=nhRn; } else if(cmd==='moveColumn'){ var spIdMc=a[1],fr=parseInt(a[2]),to=parseInt(a[3]);var pgMc=W.pages[spIdMc];if(!pgMc)return {ok:false,message:'I could not apply that change because the target page could not be found. No changes were made.'}; if(pgMc.pageType==='how')return {ok:false,message:'I could not apply that change because How pages do not use columns. No changes were made.'}; if(isNaN(fr)||isNaN(to)||!pgMc.cols[fr]||to<0||to>=pgMc.cols.length)return {ok:false,message:'I could not apply that change because the target column could not be found. No changes were made.'}; var col=pgMc.cols.splice(fr,1)[0];pgMc.cols.splice(to,0,col); } else if(cmd==='addSubpage'){ var lblS=a[1];if(!lblS||!lblS.trim())return {ok:false,message:'I could not add that page because the page title was empty. No changes were made.'}; var pType='this_then';if(a.length>=3 && a[2] && a[2].charAt(0)!=='#'){if(['this_then','how','documentation'].indexOf(a[2])<0)return {ok:false,message:'I could not add that page because the page type "'+a[2]+'" is not supported. No changes were made.'};pType=a[2];} var newId=nextShadowPageId();var entryS={pageType:pType,cols:[],howIds:[]}; if(pType==='how'){for(var hi=1;hi<=8;hi++){entryS.howIds.push('H'+String(hi).padStart(3,'0'));}} else{entryS.cols.push({h:'First Step',boxes:[]});} W.pages[newId]=entryS;W.pageOrder.push(newId); } else if(cmd==='removeSubpage'){ var rmId=a[1];if(!pageExists(rmId))return {ok:false,message:'I could not apply that change because the target page could not be found. No changes were made.'}; delete W.pages[rmId];var oi=W.pageOrder.indexOf(rmId);if(oi>=0)W.pageOrder.splice(oi,1); } else if(cmd==='addFinalOutcome'){ var fl=a.slice(1).join(':');if(!fl||!fl.trim())return {ok:false,message:'I could not add that final outcome because the label was empty. No changes were made.'}; W.fo.push(fl); } else if(cmd==='removeFinalOutcome'){ var fi=parseInt(a[1]);if(isNaN(fi)||fi<0||fi>=W.fo.length)return {ok:false,message:'I could not apply that change because the target final outcome could not be found. No changes were made.'}; W.fo.splice(fi,1); } else if(cmd==='addTTLink'){ var fk=resolveBoxKey(a[1]);var tk=resolveBoxKey(a[2]);if(!fk||!tk||fk.kind!=='tt'||tk.kind!=='tt')return {ok:false,message:'I could not add that link because one of the linked boxes could not be found. No changes were made.'}; } else if(cmd==='removeTTLink'||cmd==='flipTTLink'){ if(W.ttLinkIds.indexOf(a[1])<0)return {ok:false,message:'I could not apply that change because the target structural link could not be found. No changes were made.'}; if(cmd==='removeTTLink'){var li=W.ttLinkIds.indexOf(a[1]);W.ttLinkIds.splice(li,1);} } else if(cmd==='addHowLink'){ var hfk=resolveBoxKey(a[1]);var htk=resolveBoxKey(a[2]);if(!hfk||!htk)return {ok:false,message:'I could not add that link because one of the linked boxes could not be found. No changes were made.'}; } else if(cmd==='removeHowLink'){ if(W.howLinkIds.indexOf(a[1])<0)return {ok:false,message:'I could not apply that change because the target structural link could not be found. No changes were made.'}; var hli=W.howLinkIds.indexOf(a[1]);W.howLinkIds.splice(hli,1); } else if(cmd==='createMeasure'){ var mt=a.slice(1).join(':');if(!mt||!mt.trim())return {ok:false,message:'I could not create that Measure because the title was empty. No changes were made.'}; W.measures.push('M'+String(W.measures.length+1).padStart(3,'0')); } else if(cmd==='createEQ'){ var qt=a.slice(1).join(':');if(!qt||!qt.trim())return {ok:false,message:'I could not create that Evaluation Question because the question text was empty. No changes were made.'}; W.eqs.push('EQ'+String(W.eqs.length+1).padStart(3,'0')); } else if(cmd==='linkMeasure'||cmd==='unlinkMeasure'){ if(W.measures.indexOf(a[1])<0)return {ok:false,message:'I could not apply that change because the target Measure could not be found. No changes were made.'}; if(!resolveBoxKey(a[2]))return {ok:false,message:'I could not apply that change because the target box could not be found. No changes were made.'}; } else if(cmd==='linkEQ'||cmd==='unlinkEQ'){ if(W.eqs.indexOf(a[1])<0)return {ok:false,message:'I could not apply that change because the target Evaluation Question could not be found. No changes were made.'}; if(!resolveBoxKey(a[2]))return {ok:false,message:'I could not apply that change because the target box could not be found. No changes were made.'}; } else if(cmd==='deleteMeasure'){ if(W.measures.indexOf(a[1])<0)return {ok:false,message:'I could not apply that change because the target Measure could not be found. No changes were made.'}; var dmi=W.measures.indexOf(a[1]);W.measures.splice(dmi,1); } else if(cmd==='deleteEQ'){ if(W.eqs.indexOf(a[1])<0)return {ok:false,message:'I could not apply that change because the target Evaluation Question could not be found. No changes were made.'}; var deqi=W.eqs.indexOf(a[1]);W.eqs.splice(deqi,1); } // Other state-only commands (setAllLights, setBoardInfo) require no target validation beyond the parts.length check. } return {ok:true}; } /* Validate populated This\u2013Then page proposals. Given the structural actions array, count addColumn / removeColumn / addBox actions targeting each predicted new this_then page id, and report any predicted page where the resulting structure would be a one-box placeholder or sparse: fewer than 3 columns total or fewer than 6 boxes. Engine adds 1 default column ("First Stage") when an addSubpage:this_then runs, so total columns = 1 + addColumn(this id) - removeColumn(this id). Total boxes = addBox(this id). This also flags pages whose column headings are mechanical numbered placeholders such as "Stage 1", "Stage 2", "Column 1", "Step 2", "Phase 3" \u2014 these are rejected because column headings must name real causal stages. It also flags pages whose column headings are dominated by the generic logic-model template Inputs / Activities / Outputs (specifically: 2 or more of those generic words used as headings together) so a mechanical "Inputs / Activities / Outputs" page is rejected unless the AI uses a domain-specific shape; single occurrences of those words are still allowed because a domain may genuinely have one step called "Outputs". The default first-column heading "First Stage" is left alone (this is the engine's default until the AI renames it via renameColumn) \u2014 the AI is expected to renameColumn it to a domain-specific heading; if it does not rename it, the count of generic headings still flags the proposal. Returns array of insufficient predicted page descriptors with totalCols, addBoxes, hasGenericHeadings, and a human-readable reason string. */ function _findInsufficientThisThenPageProposals(structActions){var preds=_predictNewPageIds(structActions);var bad=[];var genericRx=/^(stage|step|column|phase)\s*\d+$/i;var genericTemplateRx=/^(inputs?|activities|outputs?)$/i;preds.forEach(function(p){if(p.pageType!=='this_then')return;var pid=p.predictedId;var addCols=structActions.filter(function(a){return a[0]==='addColumn' && a[1]===pid;}).length;var rmCols=structActions.filter(function(a){return a[0]==='removeColumn' && a[1]===pid;}).length;var totalCols=1+addCols-rmCols;var addBoxes=structActions.filter(function(a){return a[0]==='addBox' && a[1]===pid;}).length;/* scan addColumn / renameColumn headings for mechanical numbered placeholders. If any heading targeting this page id matches the generic pattern, flag the proposal. also count Inputs/Activities/Outputs headings; flag when 2 or more occur together. */var hasGenericHeadings=false;var templateCount=0;for(var k=0;k=2;var reason=[];if(totalCols<3)reason.push('only '+totalCols+' column'+(totalCols===1?'':'s'));if(addBoxes<6)reason.push('only '+addBoxes+' box'+(addBoxes===1?'':'es'));if(hasGenericHeadings)reason.push('mechanical numbered column headings');if(hasGenericTemplate)reason.push('generic Inputs / Activities / Outputs template headings');if(reason.length){bad.push({predictedId:pid,label:p.action[1]||'(untitled)',totalCols:totalCols,addBoxes:addBoxes,hasGenericHeadings:hasGenericHeadings,hasGenericTemplate:hasGenericTemplate,reason:reason.join(', ')});}});return bad;} function processAIResponse(fullText,userMsg){const{clean,actions}=parseActions(fullText);if(!actions.length)return clean; /* preflight validation runs first \u2014 before snapshot, before any state-action execution, before showing OK/Cancel. If any action is malformed, has an unsupported command name, or targets a missing object, the WHOLE action set is rejected with a plain-language message and nothing is applied. */ var v=_validateActionSet(actions); if(!v.ok){addChatMsg('system',v.message);return clean;} const stateActions=actions.filter(a=>!STRUCTURAL_CMDS.includes(a[0]));const structActions=actions.filter(a=>STRUCTURAL_CMDS.includes(a[0])); /* if the user's request is an ambiguous page-creation request (no page type specified, no populated trigger), reject any addSubpage actions emitted by the AI. The AI is expected by the prompt to ask the page-type clarification question instead of emitting addSubpage; this engine-side guard is a safety net so a page is never silently created with the wrong type. Other action shapes (state changes, removals, renames, link/measure/EQ actions, etc.) are unaffected by this guard. */ if(structActions.length && userMsg && isAmbiguousPageCreationRequest(userMsg)){var hasAddSubpage=structActions.some(function(a){return a[0]==='addSubpage';});if(hasAddSubpage){addChatMsg('system','Your request did not specify the page type (This\u2013Then page, How page, or Documentation page). The change has not been applied. Please clarify which type of page you want \u2014 see the question above \u2014 and the board chat will then propose the right page.');return clean;}} /* If the user clearly asked for a populated This\u2013Then page (and did not explicitly ask for a blank/starter page), validate that the AI's proposed structural actions would actually create a properly populated page. Reject sparse proposals (fewer than 3 columns or fewer than 6 boxes) or proposals with mechanical numbered column headings or generic Inputs/Activities/Outputs template headings. The change is rejected before queuing pendingStructural so a one-box placeholder or generic-template page is never silently applied. Other action shapes (How pages, Documentation pages, removals, renames, link/measure/EQ actions, etc.) are unaffected. */ if(structActions.length && userMsg && isPopulatedThisThenPageRequest(userMsg)){var insufficient=_findInsufficientThisThenPageProposals(structActions);if(insufficient.length){var listing=insufficient.map(function(x){return '"'+x.label+'" ('+x.reason+')';}).join('; ');addChatMsg('system','It looks like you asked for a populated This\u2013Then page, but the proposed change would not create a properly populated page: '+listing+'. A populated This\u2013Then page should have at least 3 columns and at least 6 boxes, and the column headings must name real causal stages for the domain (not "Phase 1, Phase 2, Phase 3" or a generic "Inputs / Activities / Outputs" template). The change has not been applied. Please ask again with more domain-specific detail, or say "add a blank/starter page about X" if you really want a shell.');return clean;}} /* mixed-transaction handling. State-only responses still apply directly with a snapshot (preserving the existing safe direct-apply path). Structural-only responses still queue pendingStructural and show OK/Cancel. Mixed responses queue BOTH state and structural actions and present them together in one OK/Cancel preview, so the user sees the full change set and Cancel rolls back to a clean state without partial application. */ if(structActions.length && stateActions.length){ takeSnapshot(); pendingStateActions=stateActions; pendingStructural=structActions; showConfirmation({state:stateActions,structural:structActions}); return clean; } if(stateActions.length||structActions.length)takeSnapshot(); stateActions.forEach(a=>execAction(a)); if(structActions.length){pendingStructural=structActions;showConfirmation(structActions);} return clean; } /* showConfirmation now accepts either a string description (legacy) or the structural-actions array. When given the actions array it renders a clean structured summary that groups addSubpage + addColumn / renameColumn / addBox actions by predicted new page id, and shows the page title, the column headings, and the actual box count for each new page. Other structural actions render as one line each. The summary makes it clear what will be added before the user clicks OK. The pendingStructural array (carrying the FULL set of proposed columns and boxes) is unchanged and is still applied in order by confirmStructural on OK. showConfirmation now also accepts a mixed object {state:[...],structural:[...]} so a chat response containing both state and structural actions can be presented in ONE OK/Cancel preview that covers both kinds. */ function showConfirmation(descOrActions){var msgs=document.getElementById('chatMsgs');var div=document.createElement('div');div.className='chat-m sys';var summaryHTML;if(descOrActions && !Array.isArray(descOrActions) && typeof descOrActions==='object' && (descOrActions.state||descOrActions.structural)){summaryHTML=_buildStructuralChangeSummaryHTML(descOrActions.structural||[],descOrActions.state||[]);}else if(Array.isArray(descOrActions)){summaryHTML=_buildStructuralChangeSummaryHTML(descOrActions);}else{summaryHTML='
'+escHtml(String(descOrActions))+'
';}div.innerHTML='\u26a0\ufe0f Change(s) proposed:'+summaryHTML+'';msgs.appendChild(div);msgs.scrollTop=msgs.scrollHeight;} /* Build the structured summary HTML for the confirmation message. addSubpage actions are grouped with their related addColumn / renameColumn / addBox actions (by predicted new page id) into a single readable line per new page that shows the page type, title, column headings and box count. The preview adds a small Populated / Blank-starter status pill for This\u2013Then page proposals so the OK / Cancel preview makes obvious whether a full page is being proposed or only a shell. Unrelated structural actions (removals, renames, etc.) render as one bullet line each, escaped. */ function _buildStructuralChangeSummaryHTML(actions,stateActionsForPreview){ /* per-action friendly preview lines for individual structural and state actions. addSubpage groupings (with addColumn / renameColumn / addBox children) are still rendered as the existing rich grouped line. Other actions \u2014 especially destructive ones \u2014 now render with plain-language wording that names the affected object (page, column, box, final outcome, Measure, EQ, link, association) so the user understands what will happen before clicking OK. Targets are resolved from current state SP / B / FO_DATA / measures / evalQuestions / ttLinks / howLinks; resolution failures fall back to a compact tag-only line. By the time we reach this function, _validateActionSet has already run and rejected unresolvable targets, so resolution should normally succeed. stateActionsForPreview, if provided, lists state-action lines (setLight, setPriority, addSofar, etc.) below the structural lines so a mixed transaction shows ALL changes in ONE preview. State-only responses still apply directly (this function is not used for them). */ function _safe(s){return escHtml(String(s==null?'':s));} function _pageLabelById(pid){var sp=SP.find(function(s){return s.id===pid;});return sp?sp.label:pid;} function _resolveTTBoxLabel(k){var b=B[k];if(b)return b.label;var m=k&&k.match(/^(p\d+)-c(\d+)-b(\d+)$/);if(m){var sp=SP.find(function(s){return s.id===m[1];});if(sp&&sp.cols[parseInt(m[2])]&&sp.cols[parseInt(m[2])].boxes[parseInt(m[3])]!==undefined)return sp.cols[parseInt(m[2])].boxes[parseInt(m[3])];}return k;} function _resolveColumnHeading(spId,ci){var sp=SP.find(function(s){return s.id===spId;});if(sp&&sp.cols[ci])return sp.cols[ci].h;return 'column '+(ci+1);} function _resolveAnyBoxLabel(k){if(!k)return k;if(k.indexOf('final-b')===0){var i=parseInt(k.replace('final-b',''));if(!isNaN(i)&&i>=0&&i=0&&fi0 || boxesAdded.length>0); if(isPopulated){statusPill=' POPULATED';} else{statusPill=' BLANK/STARTER';} } var line='
\u2022 Add new '+escHtml(pTypeLabel)+' page '+escHtml('"'+label+'"')+statusPill; var colHeadings=[]; if(pType==='this_then' || pType==='documentation'){ colHeadings.push(colsRenamed[0]||'First Step'); var sortedAdds=colsAdded.slice().sort(function(x,y){return x.idx-y.idx;}); sortedAdds.forEach(function(c){colHeadings.push(colsRenamed[c.idx]||c.h);}); if(colsRemoved>0)colHeadings=colHeadings.slice(Math.min(colsRemoved,colHeadings.length-0)); }else if(pType==='how'){ colsAdded.forEach(function(c){colHeadings.push(c.h);}); } if(colHeadings.length){line+=' \u2014 '+colHeadings.length+' column'+(colHeadings.length===1?'':'s')+': '+colHeadings.map(function(h){return escHtml(h);}).join(' \u2192 ');} if(pType==='this_then'){line+=' \u2014 '+boxesAdded.length+' box'+(boxesAdded.length===1?'':'es');} else if(pType==='how'){line+=' \u2014 default 8 How Boxes';} else if(pType==='documentation' && boxesAdded.length){line+=' \u2014 '+boxesAdded.length+' box'+(boxesAdded.length===1?'':'es');} line+='
';html+=line;i=j; }else{ html+='
\u2022 '+_renderSingleAction(a)+'
'; i++; } } /* if the response contains state actions alongside structural actions, render them in the same preview so the user confirms the FULL change set in one OK/Cancel. State-only responses still apply directly without ever calling this function. */ if(stateActionsForPreview && stateActionsForPreview.length){ for(var si=0;si'; } } html+='
'; return html; } function confirmStructural(ok){ /* mixed-transaction commit. On OK we apply the queued pendingStateActions first (so they execute against the pre-structural state) then the queued pendingStructural; on Cancel we doUndo() to roll back to the snapshot taken in processAIResponse so neither the state changes nor the structural changes are partially applied. Both queues are cleared either way. */ if(ok){ if(pendingStateActions && pendingStateActions.length){pendingStateActions.forEach(function(a){execAction(a);});} if(pendingStructural && pendingStructural.length){pendingStructural.forEach(function(a){execAction(a);});} }else{ doUndo(); } pendingStateActions=[]; pendingStructural=[]; addChatMsg('system',ok?'Changes applied.':'Changes cancelled.'); } // ─── SOURCES & MODALS ─── // Sources popup is now editable. Each source is rendered as a pair of inputs (title, URL) // with a remove control. Helper text replaces the older yellow warning-banner (.warn-txt) treatment and now // uses standard popup-consistent styling (.info-helper). In display mode the user clicks the URL as a link; // when editing, the row is a plain input. Save persists to the in-memory sources array and to localStorage. // in editable boards, each non-empty URL also gets a small "Open" link beside the URL input so the user can open the saved URL in a new tab without entering edit mode (the URL input remains editable). In read-only copies, source rows render as view-only — the title appears as plain text, the URL appears as a clickable Open link (or as the bare URL if there is no title), and editable inputs / Add source / Save / Remove are hidden. The Close button on the modal remains so the popup can always be dismissed. The sources data shape and the saveSources/addNewSourceRow/removeSourceRow flow are unchanged for editable boards. // Sources URL rendering now passes through _safeUrl before opening or rendering as a clickable link. Safe URLs (https:// / http:// / bare domains promoted to https://) become clickable Open links; unsafe URLs (JavaScript:, data:, vbscript:, blank, malformed) are rejected and the row falls back to plain-text display so a dangerous URL never becomes a clickable link. Generated anchors use rel="noopener noreferrer". Stored user text is not rewritten \u2014 the input still holds whatever the user typed. function openSources(){renderSourcesList();openModal('srcModal');} function renderSourcesList(){var list=document.getElementById('srcList');if(!list)return;if(!sources.length){list.innerHTML='
'+(IS_READONLY?'No sources.':'No sources added yet. Click "Add source" below to add one.')+'
';return;}var html='';sources.forEach(function(s,i){var t=(s&&s.title)||'';var u=(s&&s.url)||'';/* resolve the saved URL through the central safe helper. Empty / dangerous / malformed values yield '' and are not rendered as clickable links. */var safeHref=_safeUrl(u);if(IS_READONLY){/* view-only row for read-only copies. Show title as plain text and URL as a clickable Open link. If only the URL is present, show the URL itself as the link text. (Only render the clickable Open affordance when _safeUrl returns a safe URL; otherwise render the URL as plain text.) */var titleHtml=t?''+escHtml(t)+'':'';var urlHtml='';if(u){if(safeHref){var openText=t?'Open':escHtml(u);urlHtml=''+openText+'';}else{urlHtml=''+escHtml(u)+'';}}else if(!t){urlHtml='(no URL)';}html+='
'+titleHtml+urlHtml+'
';}else{/* editable row gains a small Open link beside the URL input when the URL is non-empty, so the saved URL can be opened in a new tab without entering edit mode. The URL itself remains editable in the input. (the Open affordance is rendered only when _safeUrl returns a safe URL so a dangerous URL is never made clickable, but the URL input itself remains editable so the user can still see and correct what they typed.) */var openLink=safeHref?'Open':'';html+='
' +'' +'' +openLink +'' +'
';}});list.innerHTML=html;} function addNewSourceRow(){if(IS_READONLY)return;saveSourcesFromDOM(/*silent*/true);sources.push({title:'',url:''});renderSourcesList();} function removeSourceRow(idx){if(IS_READONLY)return;saveSourcesFromDOM(/*silent*/true);if(idx<0||idx>=sources.length)return;sources.splice(idx,1);renderSourcesList();} function saveSourcesFromDOM(silent){var rows=document.querySelectorAll('#srcList .src-edit-row');if(!rows||!rows.length){return;}var next=[];rows.forEach(function(row){var t=(row.querySelector('.src-edit-title')||{}).value||'';var u=(row.querySelector('.src-edit-url')||{}).value||'';t=t.trim();u=u.trim();if(!t&&!u)return;next.push({title:t,url:u});});sources=next;try{localStorage.setItem('doview_sources',JSON.stringify(sources));}catch(e){}if(!silent)saveState();} function saveSources(){if(IS_READONLY){closeModal('srcModal');return;}saveSourcesFromDOM(false);renderSourcesList();closeModal('srcModal');render();} function addSource(title,url){sources.push({title,url});try{localStorage.setItem('doview_sources',JSON.stringify(sources));}catch(e){}} /* in read-only mode, popup windows still open and are viewable, but text/content fields inside them must be clearly non-editable. This helper sweeps the modal's textareas, text inputs, and contenteditable regions and disables editability, so typing or deletion inside popup fields is not possible when the change would not persist anyway. Links stay clickable; the Close button and navigation affordances are unaffected. A small .ro-keep-editable escape hatch is respected so viewing/filtering inputs (e.g. the global-search input) stay live. applyReadonlyToModal is called from openModal every time a modal opens, so dynamically-inserted content (Measure detail, EQ detail, Link detail, Sources list, Clone chooser, etc.) is covered after its innerHTML is set. */ function applyReadonlyToModal(modalEl){if(!IS_READONLY||!modalEl)return;try{var tas=modalEl.querySelectorAll('textarea');for(var i=0;i=maxZ)maxZ=z+1;});el.style.zIndex=maxZ;var mb=el.querySelector('.modal-box');if(mb)mb.scrollTop=0;el.scrollTop=0;}catch(e){}applyReadonlyToModal(el);} function closeModal(id){document.getElementById(id).classList.remove('open');} function openInfoDev(){openModal('infoModal');} function openPrototypeOnly(){openModal('prototypeOnlyModal');} function openAck(){openModal('ackModal');} function openDisclaimer(){openModal('disclaimerModal');} /* Board info now opens in rendered view mode by default. The saved text is rendered once with clickable URLs (via buildPopupDisplayBlock); an Edit button toggles to the textarea + Save / Cancel. Empty fields show a modest empty-state line above the Edit button rather than a duplicate textarea + rendered block. In read-only copies, the Edit button is hidden via the existing body.is-readonly button[onclick=...] CSS rule and only the rendered view is visible. URLs in the rendered view remain clickable in both editable and read-only modes. */ function _renderBoardInfoView(){var _bid=document.getElementById('boardInfoDisplay');if(!_bid)return;var s=(boardInfo===null||boardInfo===undefined)?'':String(boardInfo);if(!s.trim()){_bid.innerHTML='
No board info yet.'+(IS_READONLY?'':' Click Edit to add some.')+'
';}else{_bid.innerHTML='
'+linkifyDisplay(s)+'
';}} function openBoardInfo(){_renderBoardInfoView();var v=document.getElementById('boardInfoView');var e=document.getElementById('boardInfoEdit');if(v)v.style.display='';if(e)e.style.display='none';/* always start in view mode; Edit button (hidden in read-only via existing CSS rule on saveBoardInfo's button[onclick]; we hide the Edit button via body.is-readonly #boardInfoEditBtn) switches to edit mode. */openModal('boardInfoModal');} function boardInfoStartEdit(){if(IS_READONLY)return;var v=document.getElementById('boardInfoView');var e=document.getElementById('boardInfoEdit');var ta=document.getElementById('boardInfoTa');if(ta)ta.value=boardInfo||'';if(v)v.style.display='none';if(e)e.style.display='';if(ta){setTimeout(function(){try{ta.focus();}catch(_){}},30);}} function boardInfoCancelEdit(){var v=document.getElementById('boardInfoView');var e=document.getElementById('boardInfoEdit');if(v)v.style.display='';if(e)e.style.display='none';/* cancel does not save; the previous saved text is preserved. Re-render the view block from the unchanged in-memory boardInfo. */_renderBoardInfoView();} function saveBoardInfo(){if(IS_READONLY){closeModal('boardInfoModal');return;}var ta=document.getElementById('boardInfoTa');boardInfo=ta?(ta.value||''):boardInfo;saveState();/* after Save, return to view mode rather than closing the modal so the user can immediately see their saved text rendered with clickable URLs. The modal stays open. */_renderBoardInfoView();var vDiv=document.getElementById('boardInfoView');var eDiv=document.getElementById('boardInfoEdit');if(vDiv)vDiv.style.display='';if(eDiv)eDiv.style.display='none';render();} function _renderPageInfoView(){var _pid=document.getElementById('pageInfoDisplay');if(!_pid)return;var s=pageInfo[currentPage]||'';var spNoteObj=SP.find(function(_s){return _s.id===currentPage;});var srcNote=(spNoteObj&&spNoteObj.sourceNote)?'
'+linkifyDisplay(spNoteObj.sourceNote)+'
':'';if(!s.trim()){_pid.innerHTML=srcNote+'
No page info yet.'+(IS_READONLY?'':' Click Edit to add some.')+'
';}else{_pid.innerHTML=srcNote+'
'+linkifyDisplay(s)+'
';}} function openPageInfo(){var pid=currentPage;var label=pid;if(pid==='overview')label='Overview';else if(pid==='final')label='Final Outcomes';else{var sp=SP.find(function(s){return s.id===pid;});if(sp)label=sp.label;}document.getElementById('pageInfoTitle').textContent='Page info: '+label;_renderPageInfoView();var v=document.getElementById('pageInfoView');var e=document.getElementById('pageInfoEdit');if(v)v.style.display='';if(e)e.style.display='none';openModal('pageInfoModal');} function pageInfoStartEdit(){if(IS_READONLY)return;var v=document.getElementById('pageInfoView');var e=document.getElementById('pageInfoEdit');var ta=document.getElementById('pageInfoTa');if(ta)ta.value=pageInfo[currentPage]||'';if(v)v.style.display='none';if(e)e.style.display='';if(ta){setTimeout(function(){try{ta.focus();}catch(_){}},30);}} function pageInfoCancelEdit(){var v=document.getElementById('pageInfoView');var e=document.getElementById('pageInfoEdit');if(v)v.style.display='';if(e)e.style.display='none';_renderPageInfoView();} function savePageInfo(){if(IS_READONLY){closeModal('pageInfoModal');return;}var ta=document.getElementById('pageInfoTa');pageInfo[currentPage]=ta?(ta.value||''):pageInfo[currentPage];saveState();/* after Save, return to view mode rather than closing the modal so the user can immediately see their saved text rendered with clickable URLs. */_renderPageInfoView();var vDiv=document.getElementById('pageInfoView');var eDiv=document.getElementById('pageInfoEdit');if(vDiv)vDiv.style.display='';if(eDiv)eDiv.style.display='none';} function renderPageInfoBar(){var bar=document.getElementById('pageInfoBar');if(!bar)return;if(presentationView){bar.style.display='none';bar.innerHTML='';return;}var pid=currentPage;var label='';if(pid==='overview')label='Overview';else if(pid==='final')label='Final Outcomes';else{var sp=SP.find(function(s){return s.id===pid;});if(sp)label=sp.label;}if(label){bar.style.display='';var isSubpage=isCurrentPageSubpage(); // far-left page-navigation history arrows (\u2190 / \u2192). These are browser-style navigation only; they do not affect page order. var backCls=navHistory.length?'pi-move':'pi-move pi-disabled'; var fwdCls=navFuture.length?'pi-move':'pi-move pi-disabled'; var navBackBtn='\u2190'; var navFwdBtn='\u2192 '; // page-order control relabelled 'Change page order' and moved to sit AFTER the page name and Page info, BEFORE View. Wording is direction-neutral so future vertical page-picking/reordering remains compatible. var reorderBtn=(!IS_READONLY&&isSubpage)?' Change page order':''; var delBtn=isSubpage?' Delete':'';var undoClass=undoStack.length?'pi-undo':'pi-undo pi-disabled';var undoBtn=' \u21A9';var viewBtn=currentPageSupportsPageView()?' Page View':''; var stepNav=pageStepNavHTML(); var pageInfoBtn=' Page info'; /* second-line page controls are ordered as navigation, Page View, page-order controls, Page info, Delete, Undo. Back to Overview is intentionally omitted from this line; the separate lower Back to Overview button is unchanged. */ var leftHtml=navBackBtn+navFwdBtn+stepNav+viewBtn+reorderBtn+pageInfoBtn+delBtn+undoBtn; if(isSubpage){var _hlsp=SP.find(function(s){return s.id===pid;});if(_hlsp&&_hlsp.pageType==='how'){var hlv=_hlsp.howLevel;var howPages=SP.filter(function(s){return s.pageType==='how';});var lvlText=hlv?'Level '+hlv:'No level';var canUp=hlv&&hlv>1;var canDn=hlv&&hlv '+(hlv?'\u2715':'Set')+'';}} // board-level Top right text rendered right-justified in the page-info bar. When non-empty, the text itself is shown (clickable in editable boards to edit, plain non-clickable in read-only mode). When empty, a modest "+ Add top right text" affordance was previously shown in editable boards only. // when topRightText is empty, the right-hand page-info bar now shows nothing at all in both editable boards and read-only copies. The previous "+ Add top right text" affordance is removed so finished boards do not look unfinished. Top right text is edited through the direct page-info-bar text modal, not through Board info. var rightHtml=''; if(topRightText&&topRightText.trim()){ rightHtml=''+escHtml(topRightText)+''; } bar.innerHTML='
'+leftHtml+'
'+rightHtml+'
'; }else{bar.style.display='none';}} // open the Top right text editor modal. The input is pre-populated with the current value. In read-only copies the modal is opened too (so users can read the text in detail), but its Save / Clear buttons are hidden via the standard CSS rule body.is-readonly button[onclick=...] pattern; for symmetry and safety, the save/clear functions also short-circuit on IS_READONLY. function openTopRightTextModal(){var inp=document.getElementById('topRightTextInput');if(inp)inp.value=topRightText||'';openModal('topRightTextModal');if(!IS_READONLY&&inp){setTimeout(function(){try{inp.focus();inp.select();}catch(_){}},50);}} function saveTopRightText(){if(IS_READONLY){closeModal('topRightTextModal');return;}var inp=document.getElementById('topRightTextInput');var v=inp?(inp.value||''):'';v=v.replace(/[\r\n\t]+/g,' ').trim();takeSnapshot();topRightText=v;saveState();closeModal('topRightTextModal');render();} function clearTopRightText(){if(IS_READONLY){closeModal('topRightTextModal');return;}takeSnapshot();topRightText='';saveState();closeModal('topRightTextModal');render();} // open the non-editable Official DoView Badge Standards-Compliant Board Structure information popup. Modal content is static (declared in buildHTML) and contains only a Close button. (badge wording reordered; popup heading now reads "Official DoView® Badge Standards-Compliant Board Structure".) function openOfficialBadgeModal(){openModal('officialBadgeModal');} function buildStateText(){const total=Object.keys(B).length;const green=Object.values(B).filter(v=>v.light==='green').length;const yellow=Object.values(B).filter(v=>v.light==='yellow').length;const red=Object.values(B).filter(v=>v.light==='red').length;let txt=`DOVIEW-STATE: ${TITLE}\nSlug: ${SLUG}\nSummary: ${green}/${total} green · ${yellow} yellow · ${red} red\n\nSTRUCTURE:\n`;SP.forEach(sp=>{txt+=`\n[${sp.id}] ${sp.label} | type:${sp.pageType||'this_then'}${sp.pageType==='how'&&sp.nextHowNum?' | nextHowNum:'+sp.nextHowNum:''}${sp.pageType==='how'&&sp.howLevel?' | howLevel:'+sp.howLevel:''} | bg:${spColor(sp).bg} bdr:${spColor(sp).bdr} tab:${spColor(sp).tab}\n`;if(sp.pageType==='how'&&sp.howBoxes){sp.howBoxes.forEach(hb=>{const k=`${sp.id}-${hb.id}`;const b=B[k];if(!b)return;const pri=b.priority?` | Pri:${b.priority}`:'';const entries=b.entries.length?' | '+b.entries.map(e=>`[${e.type}] ${e.text}`).join(' | '):'';const dt=b.detailText?` | [detail] ${b.detailText}`:'';const bc=b.borderColor?` | [border] ${b.borderColor}`:'';txt+=` ${k} | ${b.light}${pri} | ${b.label}${entries}${dt}${bc}\n`;});}else{sp.cols.forEach((col,ci)=>{txt+=` Column ${ci+1}: "${col.h}"\n`;col.boxes.forEach((_,bi)=>{const k=`${sp.id}-c${ci}-b${bi}`;const b=B[k];if(!b)return;const pri=b.priority?` | Pri:${b.priority}`:'';const entries=b.entries.length?' | '+b.entries.map(e=>`[${e.type}] ${e.text}`).join(' | '):'';const dt=b.detailText?` | [detail] ${b.detailText}`:'';const bc=b.borderColor?` | [border] ${b.borderColor}`:'';txt+=` ${k} | ${b.light}${pri} | ${b.label}${entries}${dt}${bc}\n`;});});}});txt+=`\nFINAL OUTCOMES:\n`;FO_DATA.forEach((label,i)=>{const k=`final-b${i}`;const b=B[k];if(!b)return;const pri=b.priority?` | Pri:${b.priority}`:'';const entries=b.entries.length?' | '+b.entries.map(e=>`[${e.type}] ${e.text}`).join(' | '):'';const dt=b.detailText?` | [detail] ${b.detailText}`:'';const bc=b.borderColor?` | [border] ${b.borderColor}`:'';txt+=` ${k} | ${b.light}${pri} | ${label}${entries}${dt}${bc}\n`;});if(boardInfo)txt+=`\nBOARD INFO:\n${boardInfo}\n`;if(topRightText)txt+=`\nTOP RIGHT TEXT:\n${topRightText}\n`;const piKeys=Object.keys(pageInfo).filter(k=>pageInfo[k]);if(piKeys.length){txt+=`\nPAGE INFO:\n`;piKeys.forEach(pid=>{let label=pid;if(pid==='overview')label='Overview';else if(pid==='final')label='Final Outcomes';else{const sp=SP.find(s=>s.id===pid);if(sp)label=sp.label;}txt+=` [${label} (${pid})]: ${pageInfo[pid]}\n`;});}const _bstDcKeys=Object.keys(docContent).filter(k=>docContent[k]);if(_bstDcKeys.length){txt+=`\nDOCUMENTATION CONTENT:\n`;_bstDcKeys.forEach(pid=>{let label=pid;const sp=SP.find(s=>s.id===pid);if(sp)label=sp.label;txt+=` [${label} (${pid})]: ${docContent[pid]}\n`;});}if(ttLinks.length){txt+=`\nTHIS-THEN LINKS:\n`;ttLinks.forEach(l=>{const fb=B[l.from];const tb=B[l.to];if(fb&&tb)txt+=` ${l.id} | ${l.from} -> ${l.to} | ${fb.label} -> ${tb.label}${l.mainText?' | main:'+l.mainText:''}${l.notes1?' | n1:'+l.notes1:''}${l.notes2?' | n2:'+l.notes2:''}${l.notes3?' | n3:'+l.notes3:''}${l.measures&&l.measures.length?' | measures:'+l.measures.join(','):''}${l.evalQuestions&&l.evalQuestions.length?' | evalQuestions:'+l.evalQuestions.join(','):''}\n`;});txt+=` nextId: ${ttLinkNextId}\n`;}if(howLinks.length){txt+=`\nHOW LINKS:\n`;howLinks.forEach(l=>{const fb=B[l.from];const tb=B[l.to];if(fb&&tb)txt+=` ${l.id} | ${l.from} -> ${l.to} | ${fb.label} -> ${tb.label}${l.mainText?' | main:'+l.mainText:''}${l.notes1?' | n1:'+l.notes1:''}${l.notes2?' | n2:'+l.notes2:''}${l.notes3?' | n3:'+l.notes3:''}\n`;});txt+=` nextId: ${howLinkNextId}\n`;}if(measures.length){txt+=`\nMEASURES:\n`;measures.forEach(m=>{const cnt=getBoxesLinkedToMeasure(m.id).length;txt+=` ${m.id} | ${m.title} | linked:${cnt}${m.mainText?' | main:'+m.mainText:''}${m.notes1?' | n1:'+m.notes1:''}${m.notes2?' | n2:'+m.notes2:''}${m.notes3?' | n3:'+m.notes3:''}\n`;});txt+=` nextId: ${measureNextId}\n`;}if(evalQuestions.length){txt+=`\nEVALUATION QUESTIONS:\n`;evalQuestions.forEach(q=>{const cnt=getBoxesLinkedToEQ(q.id).length;txt+=` ${q.id} | ${q.questionText} | linked:${cnt}${q.mainText?' | main:'+q.mainText:''}${q.notes1?' | n1:'+q.notes1:''}${q.notes2?' | n2:'+q.notes2:''}${q.notes3?' | n3:'+q.notes3:''}\n`;});txt+=` nextId: ${eqNextId}\n`;}txt+=`\nBOX MEASURES/EQ LINKS:\n`;Object.keys(B).forEach(k=>{const b=B[k];if(b.measures&&b.measures.length)txt+=` ${k} measures: ${b.measures.join(',')}\n`;if(b.evalQuestions&&b.evalQuestions.length)txt+=` ${k} evalQuestions: ${b.evalQuestions.join(',')}\n`;});txt+=`\nVIEW SETTINGS:\n thisThen: showCounts=${viewSettings.thisThen.showCounts} showTrafficLights=${viewSettings.thisThen.showTrafficLights} showPriorities=${viewSettings.thisThen.showPriorities} showHowCounts=${viewSettings.thisThen.showHowCounts} showMeasures=${viewSettings.thisThen.showMeasures} showEvalQuestions=${viewSettings.thisThen.showEvalQuestions} showMainText=${viewSettings.thisThen.showMainText}\n how: showNumbering=${viewSettings.how.showNumbering} showTrafficLights=${viewSettings.how.showTrafficLights} showPriorities=${viewSettings.how.showPriorities} showWhyCounts=${viewSettings.how.showWhyCounts} showLateralHow=${viewSettings.how.showLateralHow!==false} showMeasures=${viewSettings.how.showMeasures} showEvalQuestions=${viewSettings.how.showEvalQuestions} showMainText=${viewSettings.how.showMainText}\n finalOutcomes: showTrafficLights=${viewSettings.finalOutcomes.showTrafficLights} showPriorities=${viewSettings.finalOutcomes.showPriorities} showMeasures=${viewSettings.finalOutcomes.showMeasures} showEvalQuestions=${viewSettings.finalOutcomes.showEvalQuestions} showMainText=${viewSettings.finalOutcomes.showMainText}\n`;txt+=`\n---\nWhen the user types 'redraw doview', rebuild this board using the uploaded doview-board-engine.js file. Reconstruct the full config from the structure above: each [pN] block is a subpage with its colors, each "Column N" is a column with heading, each indented line is a box with its ID, light state, priority, label, and entries. Pass all states via savedState in the DoView.init() config. Include howLinks and howLinkNextId in savedState if HOW LINKS section is present. Include viewSettings in savedState if VIEW SETTINGS section is present.`;return txt;} var _buildStateTextBase=buildStateText; buildStateText=function(){var txt=_buildStateTextBase();txt=txt.replace(/(thisThen:[^\n]*showMainText=[^\n]*)\n/,function(_,line){return line+' showFullMainText='+viewSettings.thisThen.showFullMainText+' showMainTextCodeStyle='+viewSettings.thisThen.showMainTextCodeStyle+' showLinkInfoOnHover='+viewSettings.thisThen.showLinkInfoOnHover+'\n';});txt=txt.replace(/( how:[^\n]*showMainText=[^\n]*)\n/,function(_,line){return line+' showFullMainText='+viewSettings.how.showFullMainText+' showMainTextCodeStyle='+viewSettings.how.showMainTextCodeStyle+'\n';});txt=txt.replace(/( finalOutcomes:[^\n]*showMainText=[^\n]*)\n/,function(_,line){return line+' showFullMainText='+viewSettings.finalOutcomes.showFullMainText+' showMainTextCodeStyle='+viewSettings.finalOutcomes.showMainTextCodeStyle+'\n';});function escRe(s){return String(s).replace(/[.*+?^${}()|[\]\\]/g,'\\$&');}(ttLinks||[]).forEach(function(l){var lt=linkTrafficValue(l);if(!lt)return;var re=new RegExp('(^ '+escRe(l.id)+' \\| [^\\n]*)(\\n)','m');txt=txt.replace(re,function(m,line,nl){return line.indexOf(' | light:')>=0?m:line+' | light:'+lt+nl;});});return txt;}; function saveToClaud(){if(IS_READONLY)return;const txt=buildStateText();if(typeof sendPrompt==='function'){sendPrompt(txt);const btn=document.getElementById('saveBtn2');if(btn){btn.textContent='\u2713 Sent';btn.style.background='#22c55e';btn.style.color='#fff';btn.style.borderColor='#22c55e';setTimeout(()=>{btn.textContent='Update board changes to Main AI Chat';btn.style.background='';btn.style.color='';btn.style.borderColor='';},2000);}}else{document.getElementById('updTa').value=txt;document.getElementById('cpyBtn').textContent='Select All and Copy';document.getElementById('cpyBtn').className='copy-btn';openModal('updModal');}} function copyState(){const ta=document.getElementById('updTa');ta.select();document.execCommand('copy');const btn=document.getElementById('cpyBtn');btn.textContent='✓ Copied!';btn.className='copy-btn copied';setTimeout(()=>{btn.textContent='Select All and Copy';btn.className='copy-btn';},2000);} function getPageObjectKeys(pid){return Object.keys(B||{}).filter(function(k){return k.indexOf(pid+'-')===0;});} function _copySourceMeta(obj,type,label,meta){obj.sourceUid=(obj.uid||obj.sourceUid||'');obj.sourceBoardUid=meta.sourceBoardUid||'';obj.sourceBoardTitle=meta.sourceBoardTitle||'';obj.sourceObjectType=type||'';obj.sourceObjectLabelAtCopy=label||'';obj.copiedAt=meta.copiedAt||'';return obj;} function _cloneForPageClipboard(o){return JSON.parse(JSON.stringify(o||{}));} function _collectPageSupportingContent(boxKeys,links){var mSet={},qSet={};boxKeys.forEach(function(k){var b=B[k];(b&&b.measures||[]).forEach(function(id){mSet[id]=true;});(b&&b.evalQuestions||[]).forEach(function(id){qSet[id]=true;});});(links||[]).forEach(function(l){(l.measures||[]).forEach(function(id){mSet[id]=true;});(l.evalQuestions||[]).forEach(function(id){qSet[id]=true;});});return {measures:(measures||[]).filter(function(m){return mSet[m.id];}).map(_cloneForPageClipboard),evalQuestions:(evalQuestions||[]).filter(function(q){return qSet[q.id];}).map(_cloneForPageClipboard)};} function buildCopiedPagePayload(){_backfillUids();ensureTagIdsOnSupportedElements();var sp=SP.find(function(s){return s.id===currentPage;});if(!sp)return null;var copiedAt=new Date().toISOString();var boxKeys=getPageObjectKeys(sp.id);var keySet={};boxKeys.forEach(function(k){keySet[k]=true;});var boxes={};boxKeys.forEach(function(k){var b=_cloneForPageClipboard(B[k]);_copySourceMeta(b,'box',b.label||'',{sourceBoardUid:boardUid||'',sourceBoardTitle:TITLE||'',copiedAt:copiedAt});boxes[k]=b;});var internalTT=[];var skipped=0;(ttLinks||[]).forEach(function(l){if(keySet[l.from]&&keySet[l.to]){var c=_cloneForPageClipboard(l);_copySourceMeta(c,'this_then_link',(B[l.from]?B[l.from].label:'')+' → '+(B[l.to]?B[l.to].label:''),{sourceBoardUid:boardUid||'',sourceBoardTitle:TITLE||'',copiedAt:copiedAt});internalTT.push(c);}else if(keySet[l.from]||keySet[l.to])skipped++;});var internalHow=[];(howLinks||[]).forEach(function(l){if(keySet[l.from]&&keySet[l.to]){var c=_cloneForPageClipboard(l);_copySourceMeta(c,'how_link',(B[l.from]?B[l.from].label:'')+' → '+(B[l.to]?B[l.to].label:''),{sourceBoardUid:boardUid||'',sourceBoardTitle:TITLE||'',copiedAt:copiedAt});internalHow.push(c);}else if(keySet[l.from]||keySet[l.to])skipped++;});var support=_collectPageSupportingContent(boxKeys,internalTT.concat(internalHow));support.measures.forEach(function(m){_copySourceMeta(m,'measure',m.title||m.id,{sourceBoardUid:boardUid||'',sourceBoardTitle:TITLE||'',copiedAt:copiedAt});});support.evalQuestions.forEach(function(q){_copySourceMeta(q,'evaluation_question',q.questionText||q.id,{sourceBoardUid:boardUid||'',sourceBoardTitle:TITLE||'',copiedAt:copiedAt});});var spCopy=_cloneForPageClipboard(sp);_copySourceMeta(spCopy,'page',sp.label||'',{sourceBoardUid:boardUid||'',sourceBoardTitle:TITLE||'',copiedAt:copiedAt});return {kind:'doview-page-clipboard',payloadVersion:'',page:spCopy,pageInfo:pageInfo[sp.id]||'',docContent:sanitizeDocHtml(docContent[sp.id]||''),boxes:boxes,ttLinks:internalTT,howLinks:internalHow,measures:support.measures,evalQuestions:support.evalQuestions,sourceBoardTitle:TITLE||'',sourceBoardUid:boardUid||'',sourceBoardVersion:'',copiedAt:copiedAt,skippedOutsideLinks:skipped};} function copyCurrentPageForPasting(){if(IS_READONLY)return;var payload=buildCopiedPagePayload();if(!payload){showSaveToast('Only a normal page tab can be copied in this build.','#f59e0b');return;}var txt=JSON.stringify(payload);function ok(){showSaveToast('Page copied for pasting.','#22c55e');}function fail(){showSaveToast('The browser did not allow copying this page to the clipboard.','#f59e0b');}if(navigator.clipboard&&navigator.clipboard.writeText){navigator.clipboard.writeText(txt).then(ok).catch(fail);}else{try{var ta=document.createElement('textarea');ta.value=txt;ta.style.position='fixed';ta.style.left='-9999px';document.body.appendChild(ta);ta.focus();ta.select();var done=document.execCommand('copy');document.body.removeChild(ta);done?ok():fail();}catch(e){fail();}}} function _validateCopiedPagePayload(p){return !!(p&&p.kind==='doview-page-clipboard'&&p.page&&p.boxes);} let pendingPastePagePayload=null; async function pasteCopiedPage(){if(IS_READONLY)return;var txt='';try{if(!navigator.clipboard||!navigator.clipboard.readText){showSaveToast('The browser did not allow reading the clipboard.','#f59e0b');return;}txt=await navigator.clipboard.readText();}catch(e){showSaveToast('The browser did not allow reading the clipboard.','#f59e0b');return;}var payload=null;try{payload=JSON.parse(txt);}catch(e){}if(!_validateCopiedPagePayload(payload)){showSaveToast('No copied DoView page was found on the clipboard.','#f59e0b');return;}pendingPastePagePayload=payload;var boxCount=Object.keys(payload.boxes||{}).length;var linkCount=(payload.ttLinks||[]).length+(payload.howLinks||[]).length;var mCount=(payload.measures||[]).length;var qCount=(payload.evalQuestions||[]).length;var counts='

Includes: '+boxCount+' boxes · '+linkCount+' internal links · '+mCount+' Measures · '+qCount+' Evaluation Questions

';var defaultPageName=_uniquePastedPageTitle(payload.page.label||'Pasted page');var html='

Page: '+escHtml(payload.page.label||'Untitled page')+'

Copied from: '+escHtml(payload.sourceBoardTitle||'Another DoView Board')+'

'+counts+'

Will add copied page with links and what is associated with boxes but not links to what is outside the copied page.

Tag pasted boxes and internal This–Then links

Optionally apply a tag to boxes and internal This–Then links pasted with this page.

'+buildPasteTagControl();var el=document.getElementById('pastePagePreview');if(el)el.innerHTML=html;openModal('pastePageModal');} function buildPasteTagControl(){var opts='';(tags||[]).forEach(function(t){opts+='';});return '
Tags:NoneManage tags
';} function refreshPasteTagPicker(){var sel=document.getElementById('pasteTagSelect');if(!sel)return;var cur=sel.value;var chosen=(document.getElementById('pasteTagPicker')||{}).value||'';var opts='';(tags||[]).forEach(function(t){if(t.id!==chosen||t.id===cur)opts+='';});sel.innerHTML=opts;if(cur)sel.value=cur;updatePasteTagUI();} function updatePasteTagUI(){var hid=document.getElementById('pasteTagPicker');var chip=document.getElementById('pasteSelectedTagChip');if(!hid||!chip)return;var t=getTag(hid.value);if(t){var sl=normalizeTagShortLabel(t.shortLabel||'');chip.innerHTML=''+escHtml(sl)+' ';}else{chip.innerHTML='None';}} function selectPasteTagFromPicker(){if(IS_READONLY)return;var sel=document.getElementById('pasteTagSelect');var hid=document.getElementById('pasteTagPicker');if(!sel||!hid||!sel.value)return;hid.value=sel.value;sel.value='';refreshPasteTagPicker();updatePasteTagUI();} function removePasteTagSelection(){if(IS_READONLY)return;var hid=document.getElementById('pasteTagPicker');if(hid)hid.value='';refreshPasteTagPicker();updatePasteTagUI();} function createPasteTagAndSelect(){openTagsManager();} function _newPastedPageId(){return nextUnusedPageId();} function _uniquePastedPageTitle(base){base=String(base||'Pasted page').trim()||'Pasted page';var root=base.replace(/\s*\(copy(?: \d+)?\)$/i,'');var name=root+' (copy)';var n=2;var exists=function(x){return SP.some(function(s){return String(s.label||'')===x;});};while(exists(name)){name=root+' (copy '+(n++)+')';}return name;} function _remapTextKey(k,oldPid,newPid){if(!k)return k;return String(k).replace(oldPid+'-',newPid+'-');} function _setSourceForPastedObj(obj,type,label,payload){var oldUid=obj.uid||obj.sourceUid||'';obj.uid=_genUid(type==='page'?'page':type==='column'?'col':type.indexOf('link')>=0?'link':type==='measure'?'measure':type==='evaluation_question'?'eq':'box');obj.sourceUid=oldUid;obj.sourceBoardUid=payload.sourceBoardUid||'';obj.sourceBoardTitle=payload.sourceBoardTitle||'';obj.sourceObjectType=type||'';obj.sourceObjectLabelAtCopy=label||'';obj.copiedAt=payload.copiedAt||new Date().toISOString();return obj;} function _mapIds(list,map){return (Array.isArray(list)?list:[]).map(function(id){return map[id];}).filter(Boolean);} function doPasteCopiedPage(){if(IS_READONLY||!pendingPastePagePayload)return;var payload=pendingPastePagePayload;if(!_validateCopiedPagePayload(payload)){closeModal('pastePageModal');showSaveToast('No copied DoView page was found on the clipboard.','#f59e0b');return;}var pastedLevel=payload.page&&payload.page.pageType==='how'?normalizedVerticalHowLevel(payload.page.howLevel):null;if(pastedLevel!==null&&verticalHowPageAtLevel(pastedLevel)){alert('This copied How Page is Level '+pastedLevel+', but that numbered How Page level already exists. There is only one vertical How Page hierarchy. Paste or create lateral/cross-link How Pages with No level.');return;}takeSnapshot();var oldPid=payload.page.id;var newPid=_newPastedPageId();var tagId='';var sel=document.getElementById('pasteTagPicker');if(sel&&sel.value)tagId=sel.value;var measureMap={},eqMap={};(payload.measures||[]).forEach(function(m){var nm=_cloneForPageClipboard(m);var oldId=nm.id;nm.id='M'+String(measureNextId).padStart(3,'0');nm.displayId=displayMeasureId(nm.id);measureNextId++;_setSourceForPastedObj(nm,'measure',nm.title||oldId,payload);if(!Array.isArray(nm.tagIds))nm.tagIds=[];measureMap[oldId]=nm.id;measures.push(nm);});(payload.evalQuestions||[]).forEach(function(q){var nq=_cloneForPageClipboard(q);var oldId=nq.id;nq.id='EQ'+String(eqNextId).padStart(3,'0');nq.displayId=displayEQId(nq.id);eqNextId++;_setSourceForPastedObj(nq,'evaluation_question',nq.questionText||oldId,payload);if(!Array.isArray(nq.tagIds))nq.tagIds=[];eqMap[oldId]=nq.id;evalQuestions.push(nq);});var sp=_cloneForPageClipboard(payload.page);sp.id=newPid;var defaultPageName=_uniquePastedPageTitle(sp.label||'Pasted page');var nameEl=document.getElementById('pastePageName');var chosenPageName=nameEl?String(nameEl.value||'').trim():'';sp.label=chosenPageName||defaultPageName;_setSourceForPastedObj(sp,'page',sp.label,payload);if(Array.isArray(sp.cols)){sp.cols.forEach(function(c){_setSourceForPastedObj(c,'column',c.h||'',payload);});}if(Array.isArray(sp.howBoxes)){sp.howBoxes.forEach(function(hb){_setSourceForPastedObj(hb,'box',hb.label||'',payload);});}SP.push(sp);var keyMap={};Object.keys(payload.boxes||{}).forEach(function(oldKey){var newKey=_remapTextKey(oldKey,oldPid,newPid);keyMap[oldKey]=newKey;var b=_cloneForPageClipboard(payload.boxes[oldKey]);_setSourceForPastedObj(b,'box',b.label||'',payload);if(!Array.isArray(b.entries))b.entries=[];if(!Array.isArray(b.tagIds))b.tagIds=[];b.measures=_mapIds(b.measures,measureMap);b.evalQuestions=_mapIds(b.evalQuestions,eqMap);if(tagId&&!b.tagIds.includes(tagId))b.tagIds.push(tagId);B[newKey]=b;});(payload.ttLinks||[]).forEach(function(l){if(!keyMap[l.from]||!keyMap[l.to])return;var nl=_cloneForPageClipboard(l);nl.id='ttl_'+ttLinkNextId++;nl.from=keyMap[l.from];nl.to=keyMap[l.to];_setSourceForPastedObj(nl,'this_then_link',(B[nl.from]?B[nl.from].label:'')+' → '+(B[nl.to]?B[nl.to].label:''),payload);nl.measures=_mapIds(nl.measures,measureMap);nl.evalQuestions=_mapIds(nl.evalQuestions,eqMap);if(!Array.isArray(nl.tagIds))nl.tagIds=[];if(tagId&&!nl.tagIds.includes(tagId))nl.tagIds.push(tagId);ttLinks.push(nl);});(payload.howLinks||[]).forEach(function(l){if(!keyMap[l.from]||!keyMap[l.to])return;var nl=_cloneForPageClipboard(l);nl.id='hwl_'+howLinkNextId++;nl.from=keyMap[l.from];nl.to=keyMap[l.to];_setSourceForPastedObj(nl,'how_link',(B[nl.from]?B[nl.from].label:'')+' → '+(B[nl.to]?B[nl.to].label:''),payload);nl.measures=_mapIds(nl.measures,measureMap);nl.evalQuestions=_mapIds(nl.evalQuestions,eqMap);if(!Array.isArray(nl.tagIds))nl.tagIds=[];howLinks.push(nl);});pageInfo[newPid]=payload.pageInfo||'';docContent[newPid]=sanitizeDocHtml(payload.docContent||'');sp.sourceNote='Originally pasted from: '+(payload.sourceBoardTitle||'another DoView Board')+'\nThis page may have been amended since it was pasted.';buildDepsMap();cascade();saveState();closeModal('pastePageModal');pendingPastePagePayload=null;navTo(newPid);showSaveToast('Copied page pasted.','#22c55e');} function copyBoardHTML(){/* backfill uids before serializing. */_backfillUids();normalizePrioritiesInMemory();normalizeTrafficLightsInMemory();normalizeFinalOutcomesInMemory();sanitizeCustomColorsInMemory();const cleanB={};Object.entries(B).forEach(([k,v])=>{cleanB[k]={label:v.label,light:v.light,entries:[...(Array.isArray(v.entries)?v.entries:[])],priority:serializedPriority(v.priority),hasSubpage:v.hasSubpage,detailText:v.detailText||'',borderColor:v.borderColor||'',boxColor:v.boxColor||'',measures:[...(v.measures||[])],evalQuestions:[...(v.evalQuestions||[])],tagIds:[...(v.tagIds||[])],jumpToPage:v.jumpToPage||'',/* persist box uid + optional provenance fields in Copy HTML Board. */uid:v.uid||'',sourceUid:v.sourceUid||'',sourceBoardUid:v.sourceBoardUid||'',sourceBoardTitle:v.sourceBoardTitle||'',sourceObjectType:v.sourceObjectType||'',sourceObjectLabelAtCopy:v.sourceObjectLabelAtCopy||'',copiedAt:v.copiedAt||''};});const fullConfig={format:DOVIEW_JSON_FORMAT,schemaVersion:DOVIEW_SCHEMA_VERSION,engineVersion:DOVIEW_ENGINE_VERSION,title:TITLE,slug:SLUG,subpages:JSON.parse(JSON.stringify(SP)),finalOutcomes:[...FO_DATA],sources:sources.length?JSON.parse(JSON.stringify(sources)):[],builderValidation:builderValidationForExport(),savedState:{B:cleanB,SP:JSON.parse(JSON.stringify(SP)),FO:[...FO_DATA],boardInfo:boardInfo,pageInfo:JSON.parse(JSON.stringify(pageInfo)),docContent:sanitizedDocContentState(),createdDate:createdDate,boardInstanceId:boardInstanceId,/* preserve boardUid in Copy HTML Board because a copied HTML is a saved version of the same board. */boardUid:boardUid||'',ttLinks:JSON.parse(JSON.stringify(ttLinks)),ttLinkNextId:ttLinkNextId,howLinks:JSON.parse(JSON.stringify(howLinks)),howLinkNextId:howLinkNextId,measures:JSON.parse(JSON.stringify(measures)),measureNextId:measureNextId,evalQuestions:JSON.parse(JSON.stringify(evalQuestions)),eqNextId:eqNextId,tags:JSON.parse(JSON.stringify(tags||[])),viewSettings:JSON.parse(JSON.stringify(viewSettings)),topRightText:topRightText||'',presentationView:!!presentationView,/* persist sourcesInitialized in copy/exported HTML so the default seed is not re-applied to a board the user has already removed it from. */sourcesInitialized:!!sourcesInitialized},aiEndpoint:aiSettings.endpoint||'',aiModel:aiSettings.model||''};const configJson=escapeScriptJson(JSON.stringify(fullConfig));let engineCode=_ENGINE_SRC;engineCode=engineCode.replace(/let EMBEDDED_STATE[\s\S]*?let IS_READONLY[\s\S]*?;\nconst _ENGINE_SRC/,`let EMBEDDED_STATE = ${configJson};\nlet IS_READONLY = false;\nconst _ENGINE_SRC`);const fullHtml='\n\n\n\n\n\n'+standaloneBoardTitleHtml()+'<\/title>\n<script>\n'+engineCode+'\n<\/script>\n<\/head>\n<body><\/body>\n<\/html>';document.getElementById('htmlCopyTa').value=fullHtml;document.getElementById('htmlCpyBtn').textContent='Select All and Copy';document.getElementById('htmlCpyBtn').className='copy-btn';openModal('htmlCopyModal');} function copyHTMLState(){const ta=document.getElementById('htmlCopyTa');ta.select();let copied=false;try{copied=document.execCommand('copy');}catch(e){}if(copied)markBoardCleanAfterExport();const btn=document.getElementById('htmlCpyBtn');btn.textContent=copied?'✓ Copied!':'Copy not allowed';btn.className=copied?'copy-btn copied':'copy-btn';setTimeout(()=>{btn.textContent='Select All and Copy';btn.className='copy-btn';},2000);} // ─── DOWNLOAD ─── function supportsSaveFilePicker(){return typeof window!=='undefined'&&typeof window.showSaveFilePicker==='function';} function buildEditableBoardHtmlExport(){ /* backfill uids before serializing. */_backfillUids();normalizePrioritiesInMemory();normalizeTrafficLightsInMemory();normalizeFinalOutcomesInMemory();sanitizeCustomColorsInMemory(); const cleanB={};Object.entries(B).forEach(([k,v])=>{cleanB[k]={label:v.label,light:v.light,entries:[...(Array.isArray(v.entries)?v.entries:[])],priority:serializedPriority(v.priority),hasSubpage:v.hasSubpage,detailText:v.detailText||'',borderColor:v.borderColor||'',boxColor:v.boxColor||'',measures:[...(v.measures||[])],evalQuestions:[...(v.evalQuestions||[])],tagIds:[...(v.tagIds||[])],jumpToPage:v.jumpToPage||'',/* persist box uid + optional provenance fields in Save / Download Board. */uid:v.uid||'',sourceUid:v.sourceUid||'',sourceBoardUid:v.sourceBoardUid||'',sourceBoardTitle:v.sourceBoardTitle||'',sourceObjectType:v.sourceObjectType||'',sourceObjectLabelAtCopy:v.sourceObjectLabelAtCopy||'',copiedAt:v.copiedAt||''};}); const fullConfig={format:DOVIEW_JSON_FORMAT,schemaVersion:DOVIEW_SCHEMA_VERSION,engineVersion:DOVIEW_ENGINE_VERSION,title:TITLE,slug:SLUG,subpages:JSON.parse(JSON.stringify(SP)),finalOutcomes:[...FO_DATA],sources:sources.length?JSON.parse(JSON.stringify(sources)):[],builderValidation:builderValidationForExport(),savedState:{B:cleanB,SP:JSON.parse(JSON.stringify(SP)),FO:[...FO_DATA],boardInfo:boardInfo,pageInfo:JSON.parse(JSON.stringify(pageInfo)),docContent:sanitizedDocContentState(),createdDate:createdDate,boardInstanceId:boardInstanceId,/* preserve boardUid in Save / Download Board. */boardUid:boardUid||'',ttLinks:JSON.parse(JSON.stringify(ttLinks)),ttLinkNextId:ttLinkNextId,howLinks:JSON.parse(JSON.stringify(howLinks)),howLinkNextId:howLinkNextId,measures:JSON.parse(JSON.stringify(measures)),measureNextId:measureNextId,evalQuestions:JSON.parse(JSON.stringify(evalQuestions)),eqNextId:eqNextId,tags:JSON.parse(JSON.stringify(tags||[])),viewSettings:JSON.parse(JSON.stringify(viewSettings)),topRightText:topRightText||'',presentationView:!!presentationView,/* persist sourcesInitialized in saved HTML so the default seed is not re-applied to a board the user has already removed it from. */sourcesInitialized:!!sourcesInitialized},aiEndpoint:aiSettings.endpoint||'',aiModel:aiSettings.model||''}; const configJson=escapeScriptJson(JSON.stringify(fullConfig)); // Build from captured source — no outerHTML, no sandbox contamination. let engineCode=_ENGINE_SRC; // Replace EMBEDDED_STATE — works for both initial (null) and subsequent saves (previous JSON). // Uses const _ENGINE_SRC as the reliable boundary marker. engineCode=engineCode.replace(/let EMBEDDED_STATE[\s\S]*?let IS_READONLY[\s\S]*?;\nconst _ENGINE_SRC/,`let EMBEDDED_STATE = ${configJson};\nlet IS_READONLY = false;\nconst _ENGINE_SRC`); const fullHtml='<!DOCTYPE html>\n<!-- This is an interactive DoView board. It opens in simple Page View by default; use Page View in the board to show additional details. Tell the user their first option is to open this in the artifact window to interact with it directly. -->\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>'+standaloneBoardTitleHtml()+'<\/title>\n<script>\n'+engineCode+'\n<\/script>\n<\/head>\n<body><\/body>\n<\/html>'; return {fullHtml:fullHtml,fname:timestampedBoardFilename(SLUG)}; } function triggerHtmlDownload(fullHtml,fname){ try{const blob=new Blob([fullHtml],{type:'text/html'});const url=URL.createObjectURL(blob);const a=document.createElement('a');a.href=url;a.download=fname;document.body.appendChild(a);a.click();document.body.removeChild(a);URL.revokeObjectURL(url);return true;} catch(e1){try{const a=document.createElement('a');a.href='data:text/html;charset=utf-8,'+encodeURIComponent(fullHtml);a.download=fname;document.body.appendChild(a);a.click();document.body.removeChild(a);return true;}catch(e2){return false;}} } function chosenFileName(handle){return handle&&typeof handle.name==='string'?handle.name:'';} function updateSaveDownloadModalSupport(){ var supported=supportsSaveFilePicker(); var pickerOpt=document.getElementById('savePickerOption'); var dlOpt=document.getElementById('downloadCopyOption'); var pickerBtn=document.getElementById('savePickerBtn'); var chooseBtn=document.getElementById('chooseDifferentFileBtn'); var dlBtn=document.getElementById('downloadCopyBtn'); var note=document.getElementById('savePickerNote'); var cur=document.getElementById('saveCurrentFile'); var hasHandle=supported&&!!chosenSaveFileHandle; var fileName=hasHandle?chosenFileName(chosenSaveFileHandle):''; if(pickerOpt){pickerOpt.classList.toggle('primary',supported);pickerOpt.classList.toggle('disabled',!supported);} if(dlOpt)dlOpt.classList.toggle('primary',!supported); if(pickerBtn){pickerBtn.disabled=!supported;pickerBtn.textContent=hasHandle?'Save to Current Chosen File':'Save to a File You Choose';pickerBtn.className=supported?'cbtn cbtn-or':'cbtn cbtn-disabled';pickerBtn.title=supported?(hasHandle?'Update the same file chosen earlier in this tab':'Save to the .html file you choose'):'This browser does not support the file-save picker.';} if(chooseBtn){chooseBtn.style.display=hasHandle?'inline-flex':'none';chooseBtn.disabled=!supported;chooseBtn.className='cbtn cbtn-ol';} if(dlBtn)dlBtn.className=supported?'cbtn cbtn-ol':'cbtn cbtn-or'; if(cur){cur.style.display=hasHandle?'block':'none';cur.textContent=hasHandle?'Current file: '+(fileName||'chosen file'):'';} if(note)note.textContent=''; } function openSaveDownloadModal(){showSaveDownloadOptions();updateSaveDownloadModalSupport();openModal('saveDownloadModal');} function showSaveDownloadOptions(){var opts=document.getElementById('saveDownloadOptions');var res=document.getElementById('saveDownloadResult');var foot=document.getElementById('saveDownloadModalFooter');if(opts)opts.style.display='block';if(foot)foot.style.display='block';if(res){res.style.display='none';res.innerHTML='';}updateSaveDownloadModalSupport();} function saveResultFileLine(label,filename){return filename?'<div class="save-result-file">'+escHtml(label)+': '+escHtml(filename)+'</div>':'';} function showSaveDownloadResult(kind,info){ info=info||{}; var opts=document.getElementById('saveDownloadOptions');var res=document.getElementById('saveDownloadResult');if(!res)return;if(opts)opts.style.display='none';res.style.display='block'; var foot=document.getElementById('saveDownloadModalFooter');if(foot)foot.style.display='none'; var html=''; if(kind==='saved'){ html='<h4>Board saved</h4><p class="save-result-main">Saved to the file you chose. Future saves in this tab will update the same file unless you choose another file. <strong>Do not refresh the tab. If you do refresh it, you must go back to the folder and reopen your most recently saved file.</strong></p>'+saveResultFileLine('Saved file',info.filename); }else if(kind==='updated'){ html='<h4>Board updated</h4><p class="save-result-main">Updated the same file you chose earlier in this tab. <strong>This did not create a new dated copy.</strong> Do not refresh the tab. If you do refresh it, you must go back to the folder and reopen your most recently saved file.</p>'+saveResultFileLine('Updated file',info.filename); }else if(kind==='downloaded'){ html='<h4>Board downloaded</h4><p class="save-result-main">An up-to-date copy of your board should have been downloaded. <strong>You must open the downloaded .html file before you continue editing.</strong> Then close this tab, which has the old version in it, so you do not accidentally work in two separate copies of the board.</p><p>When you finish, find the most recent saved .html file, for example in Downloads, and move it to the folder where you want to keep and open the board next time.</p>'+saveResultFileLine('Download filename',info.filename); }else if(kind==='cancelled'){ html='<h4>Save cancelled</h4><p class="save-result-main">Save cancelled. No file was saved.</p>'; }else if(kind==='handle-error'){ html='<h4>File not updated</h4><p class="save-result-main">The chosen file could not be updated. Choose the file again or use Download a New Copy.</p><div class="save-result-actions"><button class="cbtn cbtn-or" onclick="chooseDifferentSaveFile()">Choose a File Again</button><button class="cbtn cbtn-ol" onclick="downloadBoardFromSaveModal()">Download a New Copy</button><button class="cbtn cbtn-ol" onclick="closeModal(\'saveDownloadModal\')">Close</button></div>'; }else if(kind==='unsupported'){ html='<h4>Save not supported</h4><p class="save-result-main">Save to a chosen file is not supported in this browser. Use Download a New Copy.</p>'; }else{ html='<h4>Save not completed</h4><p class="save-result-main">The browser did not complete the file save. Use Download a New Copy if the file picker is not available.</p>'; } if(kind!=='handle-error')html+='<div class="save-result-actions"><button class="cbtn cbtn-or" onclick="closeModal(\'saveDownloadModal\')">Close</button></div>'; res.innerHTML=html; } function setSaveDownloadBusy(on){ var pickerBtn=document.getElementById('savePickerBtn');var chooseBtn=document.getElementById('chooseDifferentFileBtn');var dlBtn=document.getElementById('downloadCopyBtn'); if(pickerBtn){pickerBtn.disabled=!!on||!supportsSaveFilePicker();} if(chooseBtn)chooseBtn.disabled=!!on||!supportsSaveFilePicker(); if(dlBtn)dlBtn.disabled=!!on; if(!on)updateSaveDownloadModalSupport(); } async function writeBoardToChosenFile(chooseNew,resultKind){ if(!supportsSaveFilePicker()){showSaveDownloadResult('unsupported');return;} setSaveDownloadBusy(true); var handle=chooseNew?null:chosenSaveFileHandle; try{ if(chooseNew||!handle){ handle=await window.showSaveFilePicker({suggestedName:timestampedBoardFilename(SLUG),types:[{description:'HTML files',accept:{'text/html':['.html']}}],excludeAcceptAllOption:false}); } }catch(e){ setSaveDownloadBusy(false); if(e&&e.name==='AbortError'){showSaveDownloadResult('cancelled');return;} showSaveDownloadResult('error');return; } try{ isSaving=true;updateSaveStateDisplay(); var payload=buildEditableBoardHtmlExport(); var writable=await handle.createWritable(); await writable.write(payload.fullHtml); await writable.close(); chosenSaveFileHandle=handle; markBoardCleanAfterExport(); setSaveDownloadBusy(false); updateSaveDownloadModalSupport(); var filename=chosenFileName(handle)||payload.fname; showSaveDownloadResult(resultKind,{filename:filename}); showSaveToast(resultKind==='updated'?'Updated the same file you chose earlier in this tab.':'Saved to the file you chose.','#1e293b'); }catch(e2){ isSaving=false;updateSaveStateDisplay();setSaveDownloadBusy(false);updateSaveDownloadModalSupport();showSaveDownloadResult('handle-error'); } } async function saveBoardToChosenFile(){return writeBoardToChosenFile(!chosenSaveFileHandle,chosenSaveFileHandle?'updated':'saved');} async function chooseDifferentSaveFile(){return writeBoardToChosenFile(true,'saved');} function downloadBoardFromSaveModal(){downloadBoard({showWorkflowResult:true});} function downloadBoard(opts){ opts=opts||{}; // mark a save as in progress so the bottom-right indicator can briefly show "Saving\u2026". The flag is cleared by the success/failure handlers. isSaving=true;updateSaveStateDisplay(); try{var payload=buildEditableBoardHtmlExport();if(triggerHtmlDownload(payload.fullHtml,payload.fname)){showDownloadNewCopySuccess(!!opts.showWorkflowResult,payload.fname);}else{showDlFallback(!!opts.showWorkflowResult);}} catch(e){showDlFallback(!!opts.showWorkflowResult);} } function showSaveToast(msg,bgColor){hideSaveToast();const toast=document.createElement('div');toast.className='save-toast';toast.textContent=msg;toast.style.background=bgColor;toast.id='saveToast';document.body.appendChild(toast);setTimeout(()=>{document.addEventListener('click',function dismissToast(){document.removeEventListener('click',dismissToast);hideSaveToast();},{once:true});},100);} function hideSaveToast(){const existing=document.getElementById('saveToast');if(existing){existing.classList.add('fade');setTimeout(()=>existing.remove(),300);}} // after a successful HTML save, stamp lastSavedTime, clear the dirty flag, and refresh the bottom-right save-state indicator to "Last saved: [time]". function showDlSuccess(){markBoardCleanAfterExport();showSaveToast("Your board should be saving to your Downloads folder \u2014 please check it's there",'#1e293b');} function showDownloadNewCopySuccess(showWorkflowResult,filename){markBoardCleanAfterExport();if(showWorkflowResult)showSaveDownloadResult('downloaded',{filename:filename});showSaveToast("An up-to-date copy of your board should have been downloaded.",'#1e293b');} // if the HTML save falls through both download paths, we do not stamp lastSavedTime \u2014 the board is still dirty and the user should try Copy HTML Board. function showDlFallback(showWorkflowResult){isSaving=false;updateSaveStateDisplay();if(showWorkflowResult)showSaveDownloadResult('error');showSaveToast("Save may not have worked \u2014 try 'Copy HTML Board' instead",'#f59e0b');} // ─── CREATE READ-ONLY COPY ─── // Creates a separate read-only distributed copy of the current board as an HTML file. // The copy carries isReadonly:true in its embedded savedState so that when opened, the engine sets IS_READONLY=true and disables editing through the UI. Navigation, links/jumps, search, print, and view settings still work. Board chat is hidden. The Sources popup is view-only. The normal editable board in the current browser is UNAFFECTED \u2014 this is a separate file, not a conversion of the working board. // Concept boundary: this is a simple read-only distributed copy/export mode. It is not a permissions/roles system and does not support partial-edit or annotation modes. function createReadOnlyCopy(){ /* backfill uids before serializing the read-only copy so the copy carries the same uids as its source. */_backfillUids();normalizePrioritiesInMemory();normalizeTrafficLightsInMemory();normalizeFinalOutcomesInMemory();sanitizeCustomColorsInMemory(); const cleanB={};Object.entries(B).forEach(([k,v])=>{cleanB[k]={label:v.label,light:v.light,entries:[...(Array.isArray(v.entries)?v.entries:[])],priority:serializedPriority(v.priority),hasSubpage:v.hasSubpage,detailText:v.detailText||'',borderColor:v.borderColor||'',boxColor:v.boxColor||'',measures:[...(v.measures||[])],evalQuestions:[...(v.evalQuestions||[])],tagIds:[...(v.tagIds||[])],jumpToPage:v.jumpToPage||'',/* preserve box uids on read-only copies. The least disruptive choice is to keep the same per-object uids in read-only copies so a copy and its source share the same uids. */uid:v.uid||'',sourceUid:v.sourceUid||'',sourceBoardUid:v.sourceBoardUid||'',sourceBoardTitle:v.sourceBoardTitle||'',sourceObjectType:v.sourceObjectType||'',sourceObjectLabelAtCopy:v.sourceObjectLabelAtCopy||'',copiedAt:v.copiedAt||''};}); const fullConfig={format:DOVIEW_JSON_FORMAT,schemaVersion:DOVIEW_SCHEMA_VERSION,engineVersion:DOVIEW_ENGINE_VERSION,title:TITLE,slug:SLUG,subpages:JSON.parse(JSON.stringify(SP)),finalOutcomes:[...FO_DATA],sources:sources.length?JSON.parse(JSON.stringify(sources)):[],builderValidation:builderValidationForExport(),savedState:{B:cleanB,SP:JSON.parse(JSON.stringify(SP)),FO:[...FO_DATA],boardInfo:boardInfo,pageInfo:JSON.parse(JSON.stringify(pageInfo)),docContent:sanitizedDocContentState(),createdDate:createdDate,boardInstanceId:(boardInstanceId||'')+'_ro_'+Date.now().toString(36),/* preserve boardUid on read-only copies (least-disruptive choice). The existing boardInstanceId is still mutated with the _ro_<ts> suffix as before; boardUid is intentionally kept identical so a read-only copy and its source share the same boardUid. */boardUid:boardUid||'',ttLinks:JSON.parse(JSON.stringify(ttLinks)),ttLinkNextId:ttLinkNextId,howLinks:JSON.parse(JSON.stringify(howLinks)),howLinkNextId:howLinkNextId,measures:JSON.parse(JSON.stringify(measures)),measureNextId:measureNextId,evalQuestions:JSON.parse(JSON.stringify(evalQuestions)),eqNextId:eqNextId,tags:JSON.parse(JSON.stringify(tags||[])),viewSettings:JSON.parse(JSON.stringify(viewSettings)),topRightText:topRightText||'',presentationView:!!presentationView,/* persist sourcesInitialized in read-only copies so the read-only viewer does not see a re-seeded default the original user had removed it from. */sourcesInitialized:!!sourcesInitialized,isReadonly:true},aiEndpoint:'',aiModel:''}; const configJson=escapeScriptJson(JSON.stringify(fullConfig)); let engineCode=_ENGINE_SRC; engineCode=engineCode.replace(/let EMBEDDED_STATE[\s\S]*?let IS_READONLY[\s\S]*?;\nconst _ENGINE_SRC/,`let EMBEDDED_STATE = ${configJson};\nlet IS_READONLY = true;\nconst _ENGINE_SRC`); // Backward-safety: if the marker above did not match (older engine layouts), fall back to the EMBEDDED_STATE-only marker. if(engineCode.indexOf('EMBEDDED_STATE = '+configJson)<0){ engineCode=_ENGINE_SRC.replace(/let EMBEDDED_STATE[\s\S]*?;\nconst _ENGINE_SRC/,`let EMBEDDED_STATE = ${configJson};\nconst _ENGINE_SRC`); } const fullHtml='<!DOCTYPE html>\n<!-- This is a READ-ONLY COPY of a DoView board. Editing is disabled through the UI. -->\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>'+standaloneBoardTitleHtml(' \u2014 Read-only copy')+'<\/title>\n<script>\n'+engineCode+'\n<\/script>\n<\/head>\n<body><\/body>\n<\/html>'; const d=new Date();const ds=`${d.getFullYear()}-${String(d.getMonth()+1).padStart(2,'0')}-${String(d.getDate()).padStart(2,'0')}`;const hr=d.getHours();const mn=String(d.getMinutes()).padStart(2,'0');const ampm=hr>=12?'pm':'am';const hr12=hr%12||12;const ts=`${hr12}-${mn}${ampm}`;const fname=`${SLUG}_readonly_${ds}_${ts}.html`; try{const blob=new Blob([fullHtml],{type:'text/html'});const url=URL.createObjectURL(blob);const a=document.createElement('a');a.href=url;a.download=fname;document.body.appendChild(a);a.click();document.body.removeChild(a);URL.revokeObjectURL(url);markBoardCleanAfterExport();showSaveToast("Read-only copy should be saving to your Downloads folder \u2014 please check it\u2019s there",'#1e293b');} catch(e1){try{const a=document.createElement('a');a.href='data:text/html;charset=utf-8,'+encodeURIComponent(fullHtml);a.download=fname;document.body.appendChild(a);a.click();document.body.removeChild(a);markBoardCleanAfterExport();showSaveToast("Read-only copy should be saving to your Downloads folder \u2014 please check it\u2019s there",'#1e293b');}catch(e2){showSaveToast("Read-only copy could not be saved \u2014 please try again",'#f59e0b');}} } // ─── PRINT BOARD ─── function printBoard(){ var now=new Date();var d=now.toLocaleDateString(undefined,{year:'numeric',month:'long',day:'numeric'});var t=now.toLocaleTimeString(undefined,{hour:'2-digit',minute:'2-digit'}); var footerText='DoView\u00AE Board prototype. Concept by Dr Paul Duignan. This board has not been created, endorsed, or approved by Dr Paul Duignan or by those associated with DoView\u00AE Planning. See doviewplanning.org/doviewboards for more information and developer resources. \u00B7 doviewplanning.org/doviewboards \u00B7 doviewplanning.org/trademarkuse \u00B7 doviewplanning.org/collaborate \u00B7 Generated: '+d+' '+t; var titleHtml=escHtml(TITLE);var safeFooterText=escHtml(footerText); var tlC=function(l){return{yellow:'#F5A623',green:'#2ECC71',red:'#E74C3C',grey:'#94A3B8',greenYellow:'#2ECC71',yellowRed:'#F5A623'}[l]||'#94A3B8';}; var priC=function(p){return PRI_COLORS[p]||'';}; var printPriBadge=function(pri){pri=normalizePriorityValue(pri);if(!pri)return '';var pc=priC(pri);return '<div style="position:absolute;left:4px;top:4px;width:18px;height:18px;border-radius:3px;background:#f1f5f9;border:1px solid #e2e8f0;display:flex;align-items:center;justify-content:center;font-size:8px;font-weight:700;color:'+(pc||'#1e293b')+'">'+pri+'</div>';}; var printTlDot=function(light){var state=normalizeTrafficLightValue(light);if(!state)return '';return '<div style="position:absolute;top:5px;right:5px;width:8px;height:8px;border-radius:50%;background:'+tlC(state)+'"></div>';}; // Final outcomes var foHtml=FO_DATA.map(function(label,i){var k='final-b'+i;var b=B[k];if(!b)return '';return '<div class="print-card" style="border-radius:8px;padding:10px 12px 10px 34px;background:#fff;border:1px solid #d1d5db;position:relative;font-weight:700;color:#1e293b;font-size:12px">'+printPriBadge(b.priority)+printTlDot(b.light)+escHtml(label)+'</div>';}).join(''); // Overview tiles var tilesHtml=SP.map(function(sp){var ks=Object.keys(B).filter(function(k){return k.startsWith(sp.id+'-');});var g=ks.filter(function(k){return B[k].light==='green';}).length;var y=ks.filter(function(k){return B[k].light==='yellow';}).length;var _ptc=(sp.pageType==='how'||sp.pageType==='documentation')?{bg:'#fff',bdr:'#d1d5db'}:spColor(sp);return '<div class="print-tile" style="background:'+_ptc.bg+';border:1px solid '+_ptc.bdr+';border-radius:10px;padding:15px 15px 28px;min-height:86px;position:relative"><div style="font-size:12.5px;font-weight:700;margin-bottom:5px;color:#1e293b">'+escHtml(sp.label)+'</div><div style="font-size:11px;color:#64748b;line-height:1.4">'+g+'/'+ks.length+' complete \u00B7 '+y+' to act on</div><div style="position:absolute;bottom:8px;right:8px;width:0;height:0;border-left:6px solid transparent;border-right:6px solid transparent;border-top:8px solid #F5A623;opacity:0.7"></div></div>';}).join(''); // Subpages var spHtml='';var printPageNum=1; SP.forEach(function(sp){ printPageNum++;var pgFtr='<div class="pv-page-hdr">Page '+printPageNum+' \u00B7 '+escHtml(sp.label)+'</div>'; if(sp.pageType==='how'&&sp.howBoxes){ var howBoxesHtml=''; sp.howBoxes.forEach(function(hb){ var k=sp.id+'-'+hb.id;var b=B[k];if(!b)return; howBoxesHtml+='<div class="print-card" style="border-radius:6px;padding:7px 9px 7px 30px;width:150px;min-height:48px;border:1px solid #d1d5db;background:#fff;position:relative;font-size:10.5px;line-height:1.4">'+printPriBadge(b.priority)+printTlDot(b.light)+'<div style="color:#1e293b">'+escHtml(b.label)+'</div><div style="position:absolute;bottom:2px;left:6px;font-size:7px;color:#64748b;font-weight:600">'+escHtml(howBoxDisplayId(hb))+'</div></div>'; }); spHtml+='<div class="pv-page-wrap print-page-block"><div class="pv-subpage print-section" style="margin:20px 12px 28px;padding:0"><div class="print-heading" style="font-size:14px;font-weight:700;color:#fff;padding:8px 16px;border-radius:8px 8px 0 0;background:#64748b;-webkit-print-color-adjust:exact;print-color-adjust:exact">'+escHtml(sp.label)+' <span style="font-size:10px;letter-spacing:2px;text-transform:uppercase;margin-left:8px;opacity:0.85">HOW</span></div><div style="border:1px solid #e2e8f0;border-radius:0 0 8px 8px;padding:12px;background:#fff"><div class="print-group" style="display:flex;flex-wrap:wrap;gap:8px">'+howBoxesHtml+'</div></div></div>'+pgFtr+'</div>'; }else if(sp.pageType==='documentation'){ var dcHtml=linkifyDocHtml(resolveDocClones(sanitizeDocHtml(docContent[sp.id]||'')))||'<p style="color:#64748b;font-style:italic">No content.</p>'; spHtml+='<div class="pv-page-wrap print-page-block"><div class="pv-subpage print-section" style="margin:20px 12px 28px;padding:0"><div class="print-heading" style="font-size:14px;font-weight:700;color:#fff;padding:8px 16px;border-radius:8px 8px 0 0;background:#64748b;-webkit-print-color-adjust:exact;print-color-adjust:exact">'+escHtml(sp.label)+' <span style="font-size:10px;letter-spacing:2px;text-transform:uppercase;margin-left:8px;opacity:0.85">DOCUMENTATION</span></div><div style="border:1px solid #e2e8f0;border-radius:0 0 8px 8px;padding:16px 20px;background:#fff;font-size:12px;line-height:1.6;color:#1e293b">'+dcHtml+'</div></div>'+pgFtr+'</div>'; }else{ var nc=sp.cols.length;var singlePage=SP.length===1; var colsHtml=''; sp.cols.forEach(function(col,ci){ var isLast=ci===nc-1;var colColor=singlePage?COL_COLORS[ci%COL_COLORS.length]:spColor(sp); var boxesHtml=''; col.boxes.forEach(function(_,bi){ var k=sp.id+'-c'+ci+'-b'+bi;var b=B[k];if(!b)return; var entries=Array.isArray(b.entries)?b.entries:[]; var fw=isLast?'font-weight:700;':'';var tc='color:#1e293b;'; boxesHtml+='<div class="print-card" style="border-radius:6px;padding:7px 9px 7px 30px;width:150px;min-height:48px;border:1px solid '+colColor.bdr+';background:'+colColor.bg+';position:relative;font-size:10.5px;line-height:1.4;'+fw+'">'+printPriBadge(b.priority)+printTlDot(b.light)+'<div style="'+tc+'">'+escHtml(b.label)+'</div></div>'; }); colsHtml+='<div style="display:flex;flex-direction:column;flex-shrink:0;align-self:flex-start"><div style="font-size:9px;font-weight:600;text-transform:uppercase;color:#64748b;letter-spacing:0.5px;margin-bottom:6px;text-align:center;min-height:26px;display:flex;align-items:flex-end;justify-content:center;padding:0 4px;width:150px">'+escHtml(col.h)+'</div><div class="print-group" style="display:flex;flex-direction:column;gap:6px">'+boxesHtml+'</div></div>'; if(ci<nc-1){colsHtml+='<div style="width:20px;flex-shrink:0;display:flex;justify-content:center;align-items:center"><svg width="10" height="18" viewBox="0 0 10 18"><polygon points="0,0 10,9 0,18" fill="'+colColor.bdr+'" opacity="0.45"/></svg></div>';} }); spHtml+='<div class="pv-page-wrap print-page-block"><div class="pv-subpage print-section" style="margin:20px 12px 28px;padding:0"><div class="print-heading" style="font-size:14px;font-weight:700;color:#fff;padding:8px 16px;border-radius:8px 8px 0 0;background:'+spColor(sp).tab+';-webkit-print-color-adjust:exact;print-color-adjust:exact">'+escHtml(sp.label)+' <span style="font-size:10px;letter-spacing:2px;text-transform:uppercase;margin-left:8px;opacity:0.85">This\u2192Then</span></div><div style="border:1px solid #e2e8f0;border-radius:0 0 8px 8px;padding:12px;background:#fff;display:flex;align-items:flex-start;justify-content:center;overflow:hidden"><div class="pv-tt-row" style="display:inline-flex;align-items:stretch;gap:0;transform-origin:top center"><div style="width:24px;flex-shrink:0;background:#F5A623;border:1px solid #d4880a;display:flex;align-items:center;justify-content:center;border-radius:4px;-webkit-print-color-adjust:exact;print-color-adjust:exact"><span style="writing-mode:vertical-rl;transform:rotate(180deg);color:#fff;font-weight:700;font-size:9px;letter-spacing:2px;text-transform:uppercase">This</span></div><div style="display:flex;align-items:center;flex-shrink:0;padding:0 6px;gap:0">'+colsHtml+'</div><div style="width:24px;flex-shrink:0;background:#F5A623;border:1px solid #d4880a;display:flex;align-items:center;justify-content:center;border-radius:4px;-webkit-print-color-adjust:exact;print-color-adjust:exact"><span style="writing-mode:vertical-rl;transform:rotate(180deg);color:#fff;font-weight:700;font-size:9px;letter-spacing:2px;text-transform:uppercase">Then</span></div></div></div></div>'+pgFtr+'</div>'; } }); var html='<!DOCTYPE html><html lang="en"><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width,initial-scale=1.0"><title>'+titleHtml+' \u2014 Print View'; html+='';html+=spHtml; html+=''; // keep the fixed per-page print footer and remove the former duplicate static final footer at the end of print output. html+='